File D:\maven-qcr-plugin-testsunitaires\qcr4java\jenkins-master\target\site\qc\core\src\main\java\hudson\tasks\Maven.java.html

1 /*
2 * The MIT License
3 *
4 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Jene Jasper, Stephen Connolly, Tom Huybrechts, Yahoo! Inc.
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
23 */
24 package hudson.tasks;
25
26 import hudson.Extension;
27 import hudson.Launcher;
28 import hudson.Functions;
29 import hudson.EnvVars;
30 import hudson.Util;
31 import hudson.CopyOnWrite;
32 import hudson.Launcher.LocalLauncher;
33 import hudson.FilePath.FileCallable;
34 import hudson.model.AbstractBuild;
35 import hudson.model.AbstractProject;
36 import hudson.model.BuildListener;
37 import hudson.model.Computer;
38 import hudson.model.EnvironmentSpecific;
39 import hudson.model.Node;
40 import jenkins.model.Jenkins;
41 import jenkins.mvn.DefaultGlobalSettingsProvider;
42 import jenkins.mvn.DefaultSettingsProvider;
43 import jenkins.mvn.GlobalSettingsProvider;
44 import jenkins.mvn.SettingsProvider;
45 import hudson.model.TaskListener;
46 import hudson.remoting.Callable;
47 import hudson.remoting.VirtualChannel;
48 import hudson.slaves.NodeSpecific;
49 import hudson.tasks._maven.MavenConsoleAnnotator;
50 import hudson.tools.ToolDescriptor;
51 import hudson.tools.ToolInstallation;
52 import hudson.tools.DownloadFromUrlInstaller;
53 import hudson.tools.ToolInstaller;
54 import hudson.tools.ToolProperty;
55 import hudson.util.ArgumentListBuilder;
56 import hudson.util.NullStream;
57 import hudson.util.StreamTaskListener;
58 import hudson.util.VariableResolver;
59 import hudson.util.FormValidation;
60 import hudson.util.XStream2;
61 import net.sf.json.JSONObject;
62
63 import org.apache.commons.lang.StringUtils;
64 import org.kohsuke.stapler.DataBoundConstructor;
65 import org.kohsuke.stapler.StaplerRequest;
66 import org.kohsuke.stapler.QueryParameter;
67
68 import java.io.File;
69 import java.io.IOException;
70 import java.util.ArrayList;
71 import java.util.Properties;
72 import java.util.StringTokenizer;
73 import java.util.List;
74 import java.util.Collections;
75 import java.util.Set;
76 import java.util.jar.Attributes;
77 import java.util.jar.JarFile;
78 import java.util.jar.Manifest;
79 import java.util.regex.Pattern;
80
81 /**
82 * Build by using Maven.
83 *
84 * @author Kohsuke Kawaguchi
85 */
86 public class Maven extends Builder {
87 /**
88 * The targets and other maven options.
89 * Can be separated by SP or NL.
90 */
91 public final String targets;
92
93 /**
94 * Identifies {@link MavenInstallation} to be used.
95 */
96 public final String mavenName;
97
98 /**
99 * MAVEN_OPTS if not null.
100 */
101 public final String jvmOptions;
102
103 /**
104 * Optional POM file path relative to the workspace.
105 * Used for the Maven '-f' option.
106 */
107 public final String pom;
108
109 /**
110 * Optional properties to be passed to Maven. Follows {@link Properties} syntax.
111 */
112 public final String properties;
113
114 /**
115 * If true, the build will use its own local Maven repository
116 * via "-Dmaven.repo.local=...".
117 * <p>
118 * This would consume additional disk space, but provides isolation with other builds on the same machine,
119 * such as mixing SNAPSHOTS. Maven also doesn't try to coordinate the concurrent access to Maven repositories
120 * from multiple Maven process, so this helps there too.
121 *
122 * Identical to logic used in maven-plugin.
123 *
124 * @since 1.322
125 */
126 public boolean usePrivateRepository = false;
127
128 /**
129 * Provides access to the settings.xml to be used for a build.
130 * @since 1.491
131 */
132 private SettingsProvider settings = new DefaultSettingsProvider();
133
134 /**
135 * Provides access to the global settings.xml to be used for a build.
136 * @since 1.491
137 */
138 private GlobalSettingsProvider globalSettings = new DefaultGlobalSettingsProvider();
139
140 private final static String MAVEN_1_INSTALLATION_COMMON_FILE = "bin/maven";
141 private final static String MAVEN_2_INSTALLATION_COMMON_FILE = "bin/mvn";
142
143 private static final Pattern S_PATTERN = Pattern.compile("(^| )-s ");
144 private static final Pattern GS_PATTERN = Pattern.compile("(^| )-gs ");
145
146 public Maven(String targets,String name) {
147 this(targets,name,null,null,null,false, null, null);
148 }
149
150 public Maven(String targets, String name, String pom, String properties, String jvmOptions) {
151 this(targets, name, pom, properties, jvmOptions, false, null, null);
152 }
153
154 public Maven(String targets,String name, String pom, String properties, String jvmOptions, boolean usePrivateRepository) {
155 this(targets, name, pom, properties, jvmOptions, usePrivateRepository, null, null);
156 }
157
158 @DataBoundConstructor
159 public Maven(String targets,String name, String pom, String properties, String jvmOptions, boolean usePrivateRepository, SettingsProvider settings, GlobalSettingsProvider globalSettings) {
160 this.targets = targets;
161 this.mavenName = name;
162 this.pom = Util.fixEmptyAndTrim(pom);
163 this.properties = Util.fixEmptyAndTrim(properties);
164 this.jvmOptions = Util.fixEmptyAndTrim(jvmOptions);
165 this.usePrivateRepository = usePrivateRepository;
166 this.settings = settings != null ? settings : new DefaultSettingsProvider();
167 this.globalSettings = globalSettings != null ? globalSettings : new DefaultGlobalSettingsProvider();
168 }
169
170 public String getTargets() {
171 return targets;
172 }
173
174 /**
175 * @since 1.491
176 */
177 public SettingsProvider getSettings() {
178 return settings != null ? settings : new DefaultSettingsProvider();
179 }
180
181 /**
182 * @since 1.491
183 */
184 public GlobalSettingsProvider getGlobalSettings() {
185 return globalSettings != null ? globalSettings : new DefaultGlobalSettingsProvider();
186 }
187
188 public void setUsePrivateRepository(boolean usePrivateRepository) {
189 this.usePrivateRepository = usePrivateRepository;
190 }
191
192 public boolean usesPrivateRepository() {
193 return usePrivateRepository;
194 }
195
196 /**
197 * Gets the Maven to invoke,
198 * or null to invoke the default one.
199 */
200 public MavenInstallation getMaven() {
201 for( MavenInstallation i : getDescriptor().getInstallations() ) {
202 if(mavenName !=null && mavenName.equals(i.getName()))
203 return i;
204 }
205 return null;
206 }
207
208 /**
209 * Looks for <tt>pom.xlm</tt> or <tt>project.xml</tt> to determine the maven executable
210 * name.
211 */
212 private static final class DecideDefaultMavenCommand implements FileCallable<String> {
213 private static final long serialVersionUID = -2327576423452215146L;
214 // command line arguments.
215 private final String arguments;
216
217 public DecideDefaultMavenCommand(String arguments) {
218 this.arguments = arguments;
219 }
220
221 public String invoke(File ws, VirtualChannel channel) throws IOException {
222 String seed=null;
223
224 // check for the -f option
225 StringTokenizer tokens = new StringTokenizer(arguments);
226 while(tokens.hasMoreTokens()) {
227 String t = tokens.nextToken();
228 if(t.equals("-f") && tokens.hasMoreTokens()) {
229 File file = new File(ws,tokens.nextToken());
230 if(!file.exists())
231 continue; // looks like an error, but let the execution fail later
232 seed = file.isDirectory() ?
233 /* in M1, you specify a directory in -f */ "maven"
234 /* in M2, you specify a POM file name. */ : "mvn";
235 break;
236 }
237 }
238
239 if(seed==null) {
240 // as of 1.212 (2008 April), I think Maven2 mostly replaced Maven1, so
241 // switching to err on M2 side.
242 seed = new File(ws,"project.xml").exists() ? "maven" : "mvn";
243 }
244
245 if(Functions.isWindows())
246 seed += ".bat";
247 return seed;
248 }
249 }
250
251 @Override
252 public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
253 VariableResolver<String> vr = build.getBuildVariableResolver();
254
255 EnvVars env = build.getEnvironment(listener);
256
257 String targets = Util.replaceMacro(this.targets,vr);
258 targets = env.expand(targets);
259 String pom = env.expand(this.pom);
260 String properties = env.expand(this.properties);
261
262 int startIndex = 0;
263 int endIndex;
264 do {
265 // split targets into multiple invokations of maven separated by |
266 endIndex = targets.indexOf('|', startIndex);
267 if (-1 == endIndex) {
268 endIndex = targets.length();
269 }
270
271 String normalizedTarget = targets
272 .substring(startIndex, endIndex)
273 .replaceAll("[\t\r\n]+"," ");
274
275 ArgumentListBuilder args = new ArgumentListBuilder();
276 MavenInstallation mi = getMaven();
277 if(mi==null) {
278 String execName = build.getWorkspace().act(new DecideDefaultMavenCommand(normalizedTarget));
279 args.add(execName);
280 } else {
281 mi = mi.forNode(Computer.currentComputer().getNode(), listener);
282 mi = mi.forEnvironment(env);
283 String exec = mi.getExecutable(launcher);
284 if(exec==null) {
285 listener.fatalError(Messages.Maven_NoExecutable(mi.getHome()));
286 return false;
287 }
288 args.add(exec);
289 }
290 if(pom!=null)
291 args.add("-f",pom);
292
293
294 if(!S_PATTERN.matcher(targets).find()){ // check the given target/goals do not contain settings parameter already
295 String settingsPath = SettingsProvider.getSettingsRemotePath(getSettings(), build, listener);
296 if(StringUtils.isNotBlank(settingsPath)){
297 args.add("-s", settingsPath);
298 }
299 }
300 if(!GS_PATTERN.matcher(targets).find()){
301 String settingsPath = GlobalSettingsProvider.getSettingsRemotePath(getGlobalSettings(), build, listener);
302 if(StringUtils.isNotBlank(settingsPath)){
303 args.add("-gs", settingsPath);
304 }
305 }
306
307 Set<String> sensitiveVars = build.getSensitiveBuildVariables();
308
309 args.addKeyValuePairs("-D",build.getBuildVariables(),sensitiveVars);
310 args.addKeyValuePairsFromPropertyString("-D",properties,vr,sensitiveVars);
311 if (usesPrivateRepository())
312 args.add("-Dmaven.repo.local=" + build.getWorkspace().child(".repository"));
313 args.addTokenized(normalizedTarget);
314 wrapUpArguments(args,normalizedTarget,build,launcher,listener);
315
316 buildEnvVars(env, mi);
317
318 try {
319 MavenConsoleAnnotator mca = new MavenConsoleAnnotator(listener.getLogger(),build.getCharset());
320 int r = launcher.launch().cmds(args).envs(env).stdout(mca).pwd(build.getModuleRoot()).join();
321 if (0 != r) {
322 return false;
323 }
324 } catch (IOException e) {
325 Util.displayIOException(e,listener);
326 e.printStackTrace( listener.fatalError(Messages.Maven_ExecFailed()) );
327 return false;
328 }
329 startIndex = endIndex + 1;
330 } while (startIndex < targets.length());
331 return true;
332 }
333
334 /**
335 * Allows the derived type to make additional modifications to the arguments list.
336 *
337 * @since 1.344
338 */
339 protected void wrapUpArguments(ArgumentListBuilder args, String normalizedTarget, AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
340 }
341
342 /**
343 * Build up the environment variables toward the Maven launch.
344 */
345 protected void buildEnvVars(EnvVars env, MavenInstallation mi) throws IOException, InterruptedException {
346 if(mi!=null) {
347 // if somebody has use M2_HOME they will get a classloading error
348 // when M2_HOME points to a different version of Maven2 from
349 // MAVEN_HOME (as Maven 2 gives M2_HOME priority.)
350 //
351 // The other solution would be to set M2_HOME if we are calling Maven2
352 // and MAVEN_HOME for Maven1 (only of use for strange people that
353 // are calling Maven2 from Maven1)
354 mi.buildEnvVars(env);
355 }
356 // just as a precaution
357 // see http://maven.apache.org/continuum/faqs.html#how-does-continuum-detect-a-successful-build
358 env.put("MAVEN_TERMINATE_CMD","on");
359
360 String jvmOptions = env.expand(this.jvmOptions);
361 if(jvmOptions!=null)
362 env.put("MAVEN_OPTS",jvmOptions.replaceAll("[\t\r\n]+"," "));
363 }
364
365 @Override
366 public DescriptorImpl getDescriptor() {
367 return (DescriptorImpl)super.getDescriptor();
368 }
369
370 /**
371 * @deprecated as of 1.286
372 * Use {@link jenkins.model.Jenkins#getDescriptorByType(Class)} to obtain the current instance.
373 * For compatibility, this field retains the last created {@link DescriptorImpl}.
374 * TODO: fix sonar plugin that depends on this. That's the only plugin that depends on this field.
375 */
376 public static DescriptorImpl DESCRIPTOR;
377
378 @Extension
379 public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
380 @CopyOnWrite
381 private volatile MavenInstallation[] installations = new MavenInstallation[0];
382
383 public DescriptorImpl() {
384 DESCRIPTOR = this;
385 load();
386 }
387
388 public boolean isApplicable(Class<? extends AbstractProject> jobType) {
389 return true;
390 }
391
392 @Override
393 public String getHelpFile() {
394 return "/help/project-config/maven.html";
395 }
396
397 @Override
398 public String getHelpFile(String fieldName) {
399 if (fieldName != null && fieldName.equals("globalSettings")) fieldName = "settings"; // same help file
400 return super.getHelpFile(fieldName);
401 }
402
403 public String getDisplayName() {
404 return Messages.Maven_DisplayName();
405 }
406
407 public MavenInstallation[] getInstallations() {
408 return installations;
409 }
410
411 public void setInstallations(MavenInstallation... installations) {
412 List<MavenInstallation> tmpList = new ArrayList<Maven.MavenInstallation>();
413 // remote empty Maven installation :
414 if(installations != null) {
415 Collections.addAll(tmpList, installations);
416 for(MavenInstallation installation : installations) {
417 if(Util.fixEmptyAndTrim(installation.getName()) == null) {
418 tmpList.remove(installation);
419 }
420 }
421 }
422 this.installations = tmpList.toArray(new MavenInstallation[tmpList.size()]);
423 save();
424 }
425
426 @Override
427 public Builder newInstance(StaplerRequest req, JSONObject formData) throws FormException {
428 return req.bindJSON(Maven.class,formData);
429 }
430 }
431
432 /**
433 * Represents a Maven installation in a system.
434 */
435 public static final class MavenInstallation extends ToolInstallation implements EnvironmentSpecific<MavenInstallation>, NodeSpecific<MavenInstallation> {
436 /**
437 * Constants for describing Maven versions for comparison.
438 */
439 public static final int MAVEN_20 = 0;
440 public static final int MAVEN_21 = 1;
441 public static final int MAVEN_30 = 2;
442
443
444 /**
445 * @deprecated since 2009-02-25.
446 */
447 @Deprecated // kept for backward compatiblity - use getHome()
448 private transient String mavenHome;
449
450 /**
451 * @deprecated as of 1.308.
452 * Use {@link #MavenInstallation(String, String, List)}
453 */
454 public MavenInstallation(String name, String home) {
455 super(name, home);
456 }
457
458 @DataBoundConstructor
459 public MavenInstallation(String name, String home, List<? extends ToolProperty<?>> properties) {
460 super(Util.fixEmptyAndTrim(name), Util.fixEmptyAndTrim(home), properties);
461 }
462
463 /**
464 * install directory.
465 *
466 * @deprecated as of 1.308. Use {@link #getHome()}.
467 */
468 public String getMavenHome() {
469 return getHome();
470 }
471
472 public File getHomeDir() {
473 return new File(getHome());
474 }
475
476 @Override
477 public void buildEnvVars(EnvVars env) {
478 String home = getHome();
479 env.put("M2_HOME", home);
480 env.put("MAVEN_HOME", home);
481 env.put("PATH+MAVEN", home + "/bin");
482 }
483
484 /**
485 * Compares the version of this Maven installation to the minimum required version specified.
486 *
487 * @param launcher
488 * Represents the node on which we evaluate the path.
489 * @param mavenReqVersion
490 * Represents the minimum required Maven version - constants defined above.
491 */
492 public boolean meetsMavenReqVersion(Launcher launcher, int mavenReqVersion) throws IOException, InterruptedException {
493 // FIXME using similar stuff as in the maven plugin could be better
494 // olamy : but will add a dependency on maven in core -> so not so good
495 String mavenVersion = launcher.getChannel().call(new Callable<String,IOException>() {
496 private static final long serialVersionUID = -4143159957567745621L;
497
498 public String call() throws IOException {
499 File[] jars = new File(getHomeDir(),"lib").listFiles();
500 if(jars!=null) { // be defensive
501 for (File jar : jars) {
502 if (jar.getName().startsWith("maven-")) {
503 JarFile jf = null;
504 try {
505 jf = new JarFile(jar);
506 Manifest manifest = jf.getManifest();
507 String version = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
508 if(version != null) return version;
509 } finally {
510 if(jf != null) jf.close();
511 }
512 }
513 }
514 }
515 return "";
516 }
517 });
518
519 if (!mavenVersion.equals("")) {
520 if (mavenReqVersion == MAVEN_20) {
521 if(mavenVersion.startsWith("2."))
522 return true;
523 }
524 else if (mavenReqVersion == MAVEN_21) {
525 if(mavenVersion.startsWith("2.") && !mavenVersion.startsWith("2.0"))
526 return true;
527 }
528 else if (mavenReqVersion == MAVEN_30) {
529 if(mavenVersion.startsWith("3."))
530 return true;
531 }
532 }
533 return false;
534
535 }
536
537 /**
538 * Is this Maven 2.1.x or 2.2.x - but not Maven 3.x?
539 *
540 * @param launcher
541 * Represents the node on which we evaluate the path.
542 */
543 public boolean isMaven2_1(Launcher launcher) throws IOException, InterruptedException {
544 return meetsMavenReqVersion(launcher, MAVEN_21);
545 }
546
547 /**
548 * Gets the executable path of this maven on the given target system.
549 */
550 public String getExecutable(Launcher launcher) throws IOException, InterruptedException {
551 return launcher.getChannel().call(new Callable<String,IOException>() {
552 private static final long serialVersionUID = 2373163112639943768L;
553
554 public String call() throws IOException {
555 File exe = getExeFile("mvn");
556 if(exe.exists())
557 return exe.getPath();
558 exe = getExeFile("maven");
559 if(exe.exists())
560 return exe.getPath();
561 return null;
562 }
563 });
564 }
565
566 private File getExeFile(String execName) {
567 if(File.separatorChar=='\\')
568 execName += ".bat";
569
570 String m2Home = Util.replaceMacro(getHome(),EnvVars.masterEnvVars);
571
572 return new File(m2Home, "bin/" + execName);
573 }
574
575 /**
576 * Returns true if the executable exists.
577 */
578 public boolean getExists() {
579 try {
580 return getExecutable(new LocalLauncher(new StreamTaskListener(new NullStream())))!=null;
581 } catch (IOException e) {
582 return false;
583 } catch (InterruptedException e) {
584 return false;
585 }
586 }
587
588 private static final long serialVersionUID = 1L;
589
590 public MavenInstallation forEnvironment(EnvVars environment) {
591 return new MavenInstallation(getName(), environment.expand(getHome()), getProperties().toList());
592 }
593
594 public MavenInstallation forNode(Node node, TaskListener log) throws IOException, InterruptedException {
595 return new MavenInstallation(getName(), translateFor(node, log), getProperties().toList());
596 }
597
598 @Extension
599 public static class DescriptorImpl extends ToolDescriptor<MavenInstallation> {
600 @Override
601 public String getDisplayName() {
602 return "Maven";
603 }
604
605 @Override
606 public List<? extends ToolInstaller> getDefaultInstallers() {
607 return Collections.singletonList(new MavenInstaller(null));
608 }
609
610 // overriding them for backward compatibility.
611 // newer code need not do this
612 @Override
613 public MavenInstallation[] getInstallations() {
614 return Jenkins.getInstance().getDescriptorByType(Maven.DescriptorImpl.class).getInstallations();
615 }
616
617 // overriding them for backward compatibility.
618 // newer code need not do this
619 @Override
620 public void setInstallations(MavenInstallation... installations) {
621 Jenkins.getInstance().getDescriptorByType(Maven.DescriptorImpl.class).setInstallations(installations);
622 }
623
624 /**
625 * Checks if the MAVEN_HOME is valid.
626 */
627 public FormValidation doCheckMavenHome(@QueryParameter File value) {
628 // this can be used to check the existence of a file on the server, so needs to be protected
629 if(!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER))
630 return FormValidation.ok();
631
632 if(value.getPath().equals(""))
633 return FormValidation.ok();
634
635 if(!value.isDirectory())
636 return FormValidation.error(Messages.Maven_NotADirectory(value));
637
638 File maven1File = new File(value,MAVEN_1_INSTALLATION_COMMON_FILE);
639 File maven2File = new File(value,MAVEN_2_INSTALLATION_COMMON_FILE);
640
641 if(!maven1File.exists() && !maven2File.exists())
642 return FormValidation.error(Messages.Maven_NotMavenDirectory(value));
643
644 return FormValidation.ok();
645 }
646
647 public FormValidation doCheckName(@QueryParameter String value) {
648 return FormValidation.validateRequired(value);
649 }
650 }
651
652 public static class ConverterImpl extends ToolConverter {
653 public ConverterImpl(XStream2 xstream) { super(xstream); }
654 @Override protected String oldHomeField(ToolInstallation obj) {
655 return ((MavenInstallation)obj).mavenHome;
656 }
657 }
658 }
659
660 /**
661 * Automatic Maven installer from apache.org.
662 */
663 public static class MavenInstaller extends DownloadFromUrlInstaller {
664 @DataBoundConstructor
665 public MavenInstaller(String id) {
666 super(id);
667 }
668
669 @Extension
670 public static final class DescriptorImpl extends DownloadFromUrlInstaller.DescriptorImpl<MavenInstaller> {
671 public String getDisplayName() {
672 return Messages.InstallFromApache();
673 }
674
675 @Override
676 public boolean isApplicable(Class<? extends ToolInstallation> toolType) {
677 return toolType==MavenInstallation.class;
678 }
679 }
680 }
681
682 /**
683 * Optional interface that can be implemented by {@link AbstractProject}
684 * that has "contextual" {@link MavenInstallation} associated with it.
685 *
686 * <p>
687 * Code like RedeployPublisher uses this interface in an attempt
688 * to use the consistent Maven installation attached to the project.
689 *
690 * @since 1.235
691 */
692 public interface ProjectWithMaven {
693 /**
694 * Gets the {@link MavenInstallation} associated with the project.
695 * Can be null.
696 *
697 * <p>
698 * If the Maven installation can not be uniquely determined,
699 * it's often better to return just one of them, rather than returning
700 * null, since this method is currently ultimately only used to
701 * decide where to parse <tt>conf/settings.xml</tt> from.
702 */
703 MavenInstallation inferMavenInstallation();
704 }
705
706 }
707