View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.invoker;
20  
21  /*
22   * Licensed to the Apache Software Foundation (ASF) under one
23   * or more contributor license agreements.  See the NOTICE file
24   * distributed with this work for additional information
25   * regarding copyright ownership.  The ASF licenses this file
26   * to you under the Apache License, Version 2.0 (the
27   * "License"); you may not use this file except in compliance
28   * with the License.  You may obtain a copy of the License at
29   *
30   *  http://www.apache.org/licenses/LICENSE-2.0
31   *
32   * Unless required by applicable law or agreed to in writing,
33   * software distributed under the License is distributed on an
34   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
35   * KIND, either express or implied.  See the License for the
36   * specific language governing permissions and limitations
37   * under the License.
38   */
39  
40  import java.io.BufferedWriter;
41  import java.io.File;
42  import java.io.FileInputStream;
43  import java.io.FileOutputStream;
44  import java.io.FileWriter;
45  import java.io.IOException;
46  import java.io.InputStream;
47  import java.io.OutputStreamWriter;
48  import java.io.Reader;
49  import java.io.Writer;
50  import java.nio.file.Files;
51  import java.nio.file.Path;
52  import java.nio.file.Paths;
53  import java.text.DecimalFormat;
54  import java.text.DecimalFormatSymbols;
55  import java.util.ArrayList;
56  import java.util.Arrays;
57  import java.util.Collection;
58  import java.util.Collections;
59  import java.util.HashMap;
60  import java.util.HashSet;
61  import java.util.LinkedHashMap;
62  import java.util.LinkedHashSet;
63  import java.util.List;
64  import java.util.Locale;
65  import java.util.Map;
66  import java.util.Properties;
67  import java.util.Set;
68  import java.util.TreeSet;
69  import java.util.stream.Collectors;
70  
71  import org.apache.maven.artifact.Artifact;
72  import org.apache.maven.execution.MavenSession;
73  import org.apache.maven.model.Model;
74  import org.apache.maven.plugin.AbstractMojo;
75  import org.apache.maven.plugin.MojoExecution;
76  import org.apache.maven.plugin.MojoExecutionException;
77  import org.apache.maven.plugin.MojoFailureException;
78  import org.apache.maven.plugins.annotations.Component;
79  import org.apache.maven.plugins.annotations.Parameter;
80  import org.apache.maven.plugins.invoker.model.BuildJob;
81  import org.apache.maven.plugins.invoker.model.io.xpp3.BuildJobXpp3Writer;
82  import org.apache.maven.project.MavenProject;
83  import org.apache.maven.settings.Settings;
84  import org.apache.maven.settings.SettingsUtils;
85  import org.apache.maven.settings.TrackableBase;
86  import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
87  import org.apache.maven.settings.building.SettingsBuilder;
88  import org.apache.maven.settings.building.SettingsBuildingException;
89  import org.apache.maven.settings.building.SettingsBuildingRequest;
90  import org.apache.maven.settings.io.xpp3.SettingsXpp3Writer;
91  import org.apache.maven.shared.invoker.CommandLineConfigurationException;
92  import org.apache.maven.shared.invoker.DefaultInvocationRequest;
93  import org.apache.maven.shared.invoker.InvocationRequest;
94  import org.apache.maven.shared.invoker.InvocationResult;
95  import org.apache.maven.shared.invoker.Invoker;
96  import org.apache.maven.shared.invoker.MavenCommandLineBuilder;
97  import org.apache.maven.shared.invoker.MavenInvocationException;
98  import org.apache.maven.shared.scriptinterpreter.ScriptException;
99  import org.apache.maven.shared.scriptinterpreter.ScriptReturnException;
100 import org.apache.maven.shared.scriptinterpreter.ScriptRunner;
101 import org.apache.maven.shared.utils.logging.MessageBuilder;
102 import org.apache.maven.toolchain.MisconfiguredToolchainException;
103 import org.apache.maven.toolchain.ToolchainManagerPrivate;
104 import org.apache.maven.toolchain.ToolchainPrivate;
105 import org.codehaus.plexus.interpolation.InterpolationException;
106 import org.codehaus.plexus.interpolation.Interpolator;
107 import org.codehaus.plexus.interpolation.MapBasedValueSource;
108 import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
109 import org.codehaus.plexus.util.DirectoryScanner;
110 import org.codehaus.plexus.util.FileUtils;
111 import org.codehaus.plexus.util.IOUtil;
112 import org.codehaus.plexus.util.InterpolationFilterReader;
113 import org.codehaus.plexus.util.ReaderFactory;
114 import org.codehaus.plexus.util.ReflectionUtils;
115 import org.codehaus.plexus.util.StringUtils;
116 import org.codehaus.plexus.util.WriterFactory;
117 import org.codehaus.plexus.util.cli.CommandLineException;
118 import org.codehaus.plexus.util.cli.CommandLineUtils;
119 import org.codehaus.plexus.util.cli.Commandline;
120 import org.codehaus.plexus.util.cli.StreamConsumer;
121 import org.codehaus.plexus.util.xml.Xpp3Dom;
122 import org.codehaus.plexus.util.xml.Xpp3DomWriter;
123 
124 import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
125 
126 /**
127  * Provides common code for mojos invoking sub builds.
128  *
129  * @author Stephen Connolly
130  * @since 15-Aug-2009 09:09:29
131  */
132 public abstract class AbstractInvokerMojo extends AbstractMojo {
133     /**
134      * The zero-based column index where to print the invoker result.
135      */
136     private static final int RESULT_COLUMN = 60;
137 
138     /**
139      * Flag used to suppress certain invocations. This is useful in tailoring the build using profiles.
140      *
141      * @since 1.1
142      */
143     @Parameter(property = "invoker.skip", defaultValue = "false")
144     private boolean skipInvocation;
145 
146     /**
147      * Flag used to suppress the summary output notifying of successes and failures. If set to <code>true</code>, the
148      * only indication of the build's success or failure will be the effect it has on the main build (if it fails, the
149      * main build should fail as well). If {@link #streamLogs} is enabled, the sub-build summary will also provide an
150      * indication.
151      *
152      * @since 1.0
153      */
154     @Parameter(defaultValue = "false")
155     protected boolean suppressSummaries;
156 
157     /**
158      * Flag used to determine whether the build logs should be output to the normal mojo log.
159      *
160      * @since 1.0
161      */
162     @Parameter(property = "invoker.streamLogs", defaultValue = "false")
163     private boolean streamLogs;
164 
165     /**
166      * The local repository for caching artifacts. It is strongly recommended to specify a path to an isolated
167      * repository like <code>${project.build.directory}/it-repo</code>. Otherwise, your ordinary local repository will
168      * be used, potentially soiling it with broken artifacts.
169      *
170      * @since 1.0
171      */
172     @Parameter(property = "invoker.localRepositoryPath", defaultValue = "${settings.localRepository}")
173     private File localRepositoryPath;
174 
175     /**
176      * Directory to search for integration tests.
177      *
178      * @since 1.0
179      */
180     @Parameter(property = "invoker.projectsDirectory", defaultValue = "${basedir}/src/it/")
181     private File projectsDirectory;
182 
183     /**
184      * Base directory where all build reports are written to. Every execution of an integration test will produce an XML
185      * file which contains the information about success or failure of that particular build job. The format of the
186      * resulting XML file is documented in the given <a href="./build-job.html">build-job</a> reference.
187      *
188      * @since 1.4
189      */
190     @Parameter(property = "invoker.reportsDirectory", defaultValue = "${project.build.directory}/invoker-reports")
191     private File reportsDirectory;
192 
193     /**
194      * A flag to disable the generation of build reports.
195      *
196      * @since 1.4
197      */
198     @Parameter(property = "invoker.disableReports", defaultValue = "false")
199     private boolean disableReports;
200 
201     /**
202      * Directory to which projects should be cloned prior to execution. If set to {@code null}, each integration test
203      * will be run in the directory in which the corresponding IT POM was found. In this case, you most likely want to
204      * configure your SCM to ignore <code>target</code> and <code>build.log</code> in the test's base directory.
205      * (<b>Exception</b> when project using invoker plugin is of <i>maven-plugin</i> packaging:
206      * In such case IT projects will be cloned to and executed in <code>target/its</code> by default.)
207      *
208      * @since 1.1
209      */
210     @Parameter(property = "invoker.cloneProjectsTo")
211     private File cloneProjectsTo;
212 
213     // CHECKSTYLE_OFF: LineLength
214     /**
215      * Some files are normally excluded when copying the IT projects from the directory specified by the parameter
216      * projectsDirectory to the directory given by cloneProjectsTo (e.g. <code>.svn</code>, <code>CVS</code>,
217      * <code>*~</code>, etc: see <a href=
218      * "https://codehaus-plexus.github.io/plexus-utils/apidocs/org/codehaus/plexus/util/AbstractScanner.html#DEFAULTEXCLUDES">
219      * reference</a> for full list). Setting this parameter to <code>true</code> will cause all files to be copied to
220      * the <code>cloneProjectsTo</code> directory.
221      *
222      * @since 1.2
223      */
224     @Parameter(defaultValue = "false")
225     private boolean cloneAllFiles;
226     // CHECKSTYLE_ON: LineLength
227 
228     /**
229      * Ensure the {@link #cloneProjectsTo} directory is not polluted with files from earlier invoker runs.
230      *
231      * @since 1.6
232      */
233     @Parameter(defaultValue = "true")
234     private boolean cloneClean;
235 
236     /**
237      * A single POM to build, skipping any scanning parameters and behavior.
238      *
239      * @since 1.0
240      */
241     @Parameter(property = "invoker.pom")
242     private File pom;
243 
244     /**
245      * Include patterns for searching the integration test directory for projects. This parameter is meant to be set
246      * from the POM. If this parameter is not set, the plugin will search for all <code>pom.xml</code> files one
247      * directory below {@link #projectsDirectory} (i.e. <code>*&#47;pom.xml</code>).<br>
248      * <br>
249      * Starting with version 1.3, mere directories can also be matched by these patterns. For example, the include
250      * pattern <code>*</code> will run Maven builds on all immediate sub directories of {@link #projectsDirectory},
251      * regardless if they contain a <code>pom.xml</code>. This allows to perform builds that need/should not depend on
252      * the existence of a POM.
253      *
254      * @since 1.0
255      */
256     @Parameter
257     private List<String> pomIncludes = Collections.singletonList("*/pom.xml");
258 
259     /**
260      * Exclude patterns for searching the integration test directory. This parameter is meant to be set from the POM. By
261      * default, no POM files are excluded. For the convenience of using an include pattern like <code>*</code>, the
262      * custom settings file specified by the parameter {@link #settingsFile} will always be excluded automatically.
263      *
264      * @since 1.0
265      */
266     @Parameter
267     private List<String> pomExcludes = Collections.emptyList();
268 
269     /**
270      * Include patterns for searching the projects directory for projects that need to be run before the other projects.
271      * This parameter allows to declare projects that perform setup tasks like installing utility artifacts into the
272      * local repository. Projects matched by these patterns are implicitly excluded from the scan for ordinary projects.
273      * Also, the exclusions defined by the parameter {@link #pomExcludes} apply to the setup projects, too. Default
274      * value is: <code>setup*&#47;pom.xml</code>.
275      *
276      * @since 1.3
277      */
278     @Parameter
279     private List<String> setupIncludes = Collections.singletonList("setup*/pom.xml");
280 
281     /**
282      * The list of goals to execute on each project. Default value is: <code>package</code>.
283      *
284      * @since 1.0
285      */
286     @Parameter
287     private List<String> goals = Collections.singletonList("package");
288 
289     /**
290      * Relative path of a selector script to run prior in order to decide if the build should be executed. This script
291      * may be written with either BeanShell or Groovy. If the file extension is omitted (e.g. <code>selector</code>),
292      * the plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and
293      * <code>.groovy</code>. If this script exists for a particular project but returns any non-null value different
294      * from <code>true</code>, the corresponding build is flagged as skipped. In this case, none of the pre-build hook
295      * script, Maven nor the post-build hook script will be invoked. If this script throws an exception, the
296      * corresponding build is flagged as in error, and none of the pre-build hook script, Maven not the post-build hook
297      * script will be invoked.
298      *
299      * @since 1.5
300      */
301     @Parameter(property = "invoker.selectorScript", defaultValue = "selector")
302     private String selectorScript;
303 
304     /**
305      * Relative path of a pre-build hook script to run prior to executing the build. This script may be written with
306      * either BeanShell or Groovy (since 1.3). If the file extension is omitted (e.g. <code>prebuild</code>), the plugin
307      * searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>. If this
308      * script exists for a particular project but returns any non-null value different from <code>true</code> or throws
309      * an exception, the corresponding build is flagged as a failure. In this case, neither Maven nor the post-build
310      * hook script will be invoked.
311      *
312      * @since 1.0
313      */
314     @Parameter(property = "invoker.preBuildHookScript", defaultValue = "prebuild")
315     private String preBuildHookScript;
316 
317     /**
318      * Relative path of a cleanup/verification hook script to run after executing the build. This script may be written
319      * with either BeanShell or Groovy (since 1.3). If the file extension is omitted (e.g. <code>verify</code>), the
320      * plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>.
321      * If this script exists for a particular project but returns any non-null value different from <code>true</code> or
322      * throws an exception, the corresponding build is flagged as a failure.
323      *
324      * @since 1.0
325      */
326     @Parameter(property = "invoker.postBuildHookScript", defaultValue = "postbuild")
327     private String postBuildHookScript;
328 
329     /**
330      * Location of a properties file that defines CLI properties for the test.
331      *
332      * @since 1.0
333      */
334     @Parameter(property = "invoker.testPropertiesFile", defaultValue = "test.properties")
335     private String testPropertiesFile;
336 
337     /**
338      * Common set of properties to pass in on each project's command line, via -D parameters.
339      *
340      * @since 1.1
341      */
342     @Parameter
343     private Map<String, String> properties;
344 
345     /**
346      * Whether to show errors in the build output.
347      *
348      * @since 1.0
349      */
350     @Parameter(property = "invoker.showErrors", defaultValue = "false")
351     private boolean showErrors;
352 
353     /**
354      * Whether to show debug statements in the build output.
355      *
356      * @since 1.0
357      */
358     @Parameter(property = "invoker.debug", defaultValue = "false")
359     private boolean debug;
360 
361     /**
362      * Whether to execute Maven in quiet mode.
363      *
364      * @since 3.3.0
365      */
366     @Parameter(property = "invoker.quiet", defaultValue = "false")
367     private boolean quiet;
368 
369     /**
370      * Suppress logging to the <code>build.log</code> file.
371      *
372      * @since 1.0
373      */
374     @Parameter(property = "invoker.noLog", defaultValue = "false")
375     private boolean noLog;
376 
377     /**
378      * By default a {@code build.log} is created in the root of the project. By setting this folder
379      * files are written to a different folder, respecting the structure of the projectsDirectory.
380      *
381      * @since 3.2.0
382      */
383     @Parameter
384     private File logDirectory;
385 
386     /**
387      * List of profile identifiers to explicitly trigger in the build.
388      *
389      * @since 1.1
390      */
391     @Parameter
392     private List<String> profiles;
393 
394     /**
395      * A list of additional properties which will be used to filter tokens in POMs and goal files.
396      *
397      * @since 1.3
398      */
399     @Parameter
400     private Map<String, String> filterProperties;
401 
402     /**
403      * A comma separated list of projectname patterns to run. Specify this parameter to run individual tests by file
404      * name, overriding the {@link #setupIncludes}, {@link #pomIncludes} and {@link #pomExcludes} parameters. Each
405      * pattern you specify here will be used to create an include/exclude pattern formatted like
406      * <code>${projectsDirectory}/<i>pattern</i></code>. To exclude a test, prefix the pattern with a '<code>!</code>'.
407      * So you can just type <nobr><code>-Dinvoker.test=SimpleTest,Comp*Test,!Compare*</code></nobr> to run builds in
408      * <code>${projectsDirectory}/SimpleTest</code> and <code>${projectsDirectory}/ComplexTest</code>, but not
409      * <code>${projectsDirectory}/CompareTest</code>
410      *
411      * @since 1.1 (exclusion since 1.8)
412      */
413     @Parameter(property = "invoker.test")
414     private String invokerTest;
415 
416     /**
417      * Path to an alternate <code>settings.xml</code> to use for Maven invocation with all ITs. Note that the
418      * <code>&lt;localRepository&gt;</code> element of this settings file is always ignored, i.e. the path given by the
419      * parameter {@link #localRepositoryPath} is dominant.
420      *
421      * @since 1.2
422      */
423     @Parameter(property = "invoker.settingsFile")
424     private File settingsFile;
425 
426     /**
427      * The <code>MAVEN_OPTS</code> environment variable to use when invoking Maven. This value can be overridden for
428      * individual integration tests by using {@link #invokerPropertiesFile}.
429      *
430      * @since 1.2
431      */
432     @Parameter(property = "invoker.mavenOpts")
433     private String mavenOpts;
434 
435     /**
436      * The home directory of the Maven installation to use for the forked builds. Defaults to the current Maven
437      * installation.
438      *
439      * @since 1.3
440      */
441     @Parameter(property = "invoker.mavenHome")
442     private File mavenHome;
443 
444     /**
445      * mavenExecutable can either be a file relative to <code>${maven.home}/bin/</code>, test project workspace
446      * or an absolute file.
447      *
448      * @since 1.8
449      */
450     @Parameter(property = "invoker.mavenExecutable")
451     private File mavenExecutable;
452 
453     /**
454      * The <code>JAVA_HOME</code> environment variable to use for forked Maven invocations. Defaults to the current Java
455      * home directory.
456      *
457      * @since 1.3
458      */
459     @Parameter(property = "invoker.javaHome")
460     private File javaHome;
461 
462     /**
463      * The file encoding for the pre-/post-build scripts and the list files for goals and profiles.
464      *
465      * @since 1.2
466      */
467     @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}")
468     private String encoding;
469 
470     /**
471      * A flag whether the test class path of the project under test should be included in the class path of the
472      * pre-/post-build scripts. If set to <code>false</code>, the class path of script interpreter consists only of the
473      * <a href="dependencies.html">runtime dependencies</a> of the Maven Invoker Plugin. If set the <code>true</code>,
474      * the project's test class path will be prepended to the interpreter class path. Among others, this feature allows
475      * the scripts to access utility classes from the test sources of your project.
476      *
477      * @since 1.2
478      */
479     @Parameter(property = "invoker.addTestClassPath", defaultValue = "false")
480     private boolean addTestClassPath;
481 
482     /**
483      * The name of an optional project-specific file that contains properties used to specify settings for an individual
484      * Maven invocation. Any property present in the file will override the corresponding setting from the plugin
485      * configuration. The values of the properties are filtered and may use expressions like
486      * <code>${project.version}</code> to reference project properties or values from the parameter
487      * {@link #filterProperties}.<p/>
488      *
489      * <p>
490      * As of 3.2.0 it is possible to put this folder in any of the ancestor folders, where properties will be inherited.
491      * This way you can provide a single properties file for a group of projects
492      * </p>
493      *
494      * The snippet below describes the supported properties:
495      * <pre>
496      * # A comma or space separated list of goals/phases to execute, may
497      * # specify an empty list to execute the default goal of the IT project.
498      * # Environment variables used by maven plugins can be added here
499      * invoker.goals = clean install -Dplugin.variable=value
500      *
501      * # Or you can give things like this if you need.
502      * invoker.goals = -T2 clean verify
503      *
504      * # Optionally, a list of goals to run during further invocations of Maven
505      * invoker.goals.2 = ${project.groupId}:${project.artifactId}:${project.version}:run
506      *
507      * # A comma or space separated list of profiles to activate
508      * # can be indexed
509      * invoker.profiles = its,jdk15
510      *
511      * # The path to an alternative POM or base directory to invoke Maven on, defaults to the
512      * # project that was originally specified in the plugin configuration
513      * # Since plugin version 1.4
514      * # can be indexed
515      * invoker.project = sub-module
516      *
517      * # The maven executable can either be a file relative to <code>${maven.home}/bin/</code>, test project workspace
518      * # or an absolute file.
519      * # Since plugin version 3.3.0
520      * # can be indexed
521      * invoker.mavenExecutable = mvnw
522      *
523      * # The value for the environment variable MAVEN_OPTS
524      * # can be indexed
525      * invoker.mavenOpts = -Dfile.encoding=UTF-16 -Xms32m -Xmx256m
526      *
527      * # Possible values are &quot;fail-fast&quot; (default), &quot;fail-at-end&quot; and &quot;fail-never&quot;
528      * # can be indexed
529      * invoker.failureBehavior = fail-never
530      *
531      * # The expected result of the build, possible values are &quot;success&quot; (default) and &quot;failure&quot;
532      * # can be indexed
533      * invoker.buildResult = failure
534      *
535      * # A boolean value controlling the aggregator mode of Maven, defaults to &quot;false&quot;
536      *
537      * # can be indexed
538      * invoker.nonRecursive = true
539      *
540      * # A boolean value controlling the network behavior of Maven, defaults to &quot;false&quot;
541      * # Since plugin version 1.4
542      * # can be indexed
543      * invoker.offline = true
544      *
545      * # The path to the properties file from which to load system properties, defaults to the
546      * # filename given by the plugin parameter testPropertiesFile
547      * # Since plugin version 1.4
548      * # can be indexed
549      * invoker.systemPropertiesFile = test.properties
550      *
551      * # An optional human friendly name and description for this build job.
552      * # Both name and description have to be set to be included in the build reports.
553      * # Since plugin version 1.4
554      * invoker.name = Test Build 01
555      * invoker.description = Checks the support for build reports.
556      *
557      * # A comma separated list of JRE versions on which this build job should be run.
558      * # Since plugin version 1.4
559      * invoker.java.version = 1.4+, !1.4.1, 1.7-
560      *
561      * # A comma separated list of OS families on which this build job should be run.
562      * # Since plugin version 1.4
563      * invoker.os.family = !windows, unix, mac
564      *
565      * # A comma separated list of Maven versions on which this build should be run.
566      * # Since plugin version 1.5
567      * invoker.maven.version = 2.0.10+, !2.1.0, !2.2.0
568      *
569      * # A mapping for toolchain to ensure it exists
570      * # Since plugin version 3.2.0
571      * invoker.toolchain.&lt;type&gt;.&lt;provides&gt; = value
572      * invoker.toolchain.jdk.version = 11
573      *
574      * # For java.version, maven.version, os.family and toolchain it is possible to define multiple selectors.
575      * # If one of the indexed selectors matches, the test is executed.
576      * # With the invoker.x.y equivalents you can specify global matchers.
577      * selector.1.java.version = 1.8+
578      * selector.1.maven.version = 3.2.5+
579      * selector.1.os.family = !windows
580      * selector.2.maven.version = 3.0+
581      * selector.3.java.version = 9+
582      *
583      * # A boolean value controlling the debug logging level of Maven, , defaults to &quot;false&quot;
584      * # Since plugin version 1.8
585      * # can be indexed
586      * invoker.debug = true
587      *
588      * # Whether to execute Maven in quiet mode
589      * # Since plugin version 3.3.0
590      * # can be indexed
591      * invoker.quiet = true
592      *
593      * The execution timeout in seconds.
594      * # Since plugin version 3.0.2
595      * # can be indexed
596      * invoker.timeoutInSeconds = 5
597      *
598      * # Path to an alternate settings.xml to use for Maven invocation with this IT.
599      * # Since plugin version 3.0.1
600      * # can be indexed
601      * invoker.settingsFile = ../
602      *
603      * # An integer value to control run order of projects. sorted in the descending order of the ordinal.
604      * # In other words, the BuildJobs with the highest numbers will be executed first
605      * # Default value is 0 (zero)
606      * # Since plugin version 3.2.1
607      * invoker.ordinal = 3
608      *
609      * # The additional value for the environment variable.
610      * # Since plugin version 3.2.2
611      * invoker.environmentVariables.&lt;variableName&gt; = variableValue
612      * invoker.environmentVariables.MY_ENV_NAME = myEnvValue
613      *
614      * # A boolean value indicating a check for missing releases and updated snapshots on remote repositories to be done
615      * # Passed to the invoker. Same as passing -U, --update-snapshots flag on the command line
616      * # Since plugin version 3.4.0
617      * invoker.updateSnapshots = true
618      *
619      * </pre>
620      *
621      * @since 1.2
622      */
623     @Parameter(property = "invoker.invokerPropertiesFile", defaultValue = "invoker.properties")
624     private String invokerPropertiesFile;
625 
626     /**
627      * flag to enable show mvn version used for running its (cli option : -V,--show-version )
628      *
629      * @since 1.4
630      */
631     @Parameter(property = "invoker.showVersion", defaultValue = "false")
632     private boolean showVersion;
633 
634     /**
635      * <p>Number of threads for running tests in parallel. This will be the number of maven forked process in parallel.
636      * When terminated with "C", the number part is multiplied by the number of processors (cores) available
637      * to the Java virtual machine. Floating point value are only accepted together with "C".</p>
638      *
639      * <p>Example values: "1.5C", "4"</p>
640      *
641      * @since 1.6
642      */
643     @Parameter(property = "invoker.parallelThreads", defaultValue = "1")
644     private String parallelThreads;
645 
646     /**
647      * If enable and if you have a settings file configured for the execution, it will be merged with your user
648      * settings.
649      *
650      * @since 1.6
651      */
652     @Parameter(property = "invoker.mergeUserSettings", defaultValue = "false")
653     private boolean mergeUserSettings;
654 
655     /**
656      * Additional environment variables to set on the command line.
657      *
658      * @since 1.8
659      */
660     @Parameter
661     private Map<String, String> environmentVariables;
662 
663     /**
664      * Additional variables for use in the hook scripts.
665      *
666      * @since 1.9
667      */
668     @Parameter
669     private Map<String, String> scriptVariables;
670 
671     /**
672      *
673      * @since 3.0.2
674      */
675     @Parameter(defaultValue = "0", property = "invoker.timeoutInSeconds")
676     private int timeoutInSeconds;
677 
678     /**
679      * Write test result in junit format.
680      * @since 3.1.2
681      */
682     @Parameter(defaultValue = "false", property = "invoker.writeJunitReport")
683     private boolean writeJunitReport;
684 
685     /**
686      * The package name use in junit report
687      * @since 3.1.2
688      */
689     @Parameter(defaultValue = "maven.invoker.it", property = "invoker.junitPackageName")
690     private String junitPackageName = "maven.invoker.it";
691 
692     /**
693      * Only invoke maven projects if their sources have been modified since
694      * they were last built. Only works in conjunction with <code>cloneProjectsTo</code>.
695      *
696      * @since 3.2.2
697      */
698     @Parameter(defaultValue = "false", property = "invoker.updateOnly")
699     private boolean updateOnly = false;
700 
701     /**
702      * Force a check for missing releases and updated snapshots on remote repositories. This is passed to the invoked
703      * maven projects (it is the same as if you were to use the -U, --update-snapshots flag on the command line).
704      *
705      * @since 3.4.0
706      */
707     @Parameter(defaultValue = "false", property = "invoker.updateSnapshots")
708     private boolean updateSnapshots;
709 
710     // internal state variables
711 
712     /**
713      * The scripter runner that is responsible to execute hook scripts.
714      */
715     private ScriptRunner scriptRunner;
716 
717     /**
718      * A string used to prefix the file name of the filtered POMs in case the POMs couldn't be filtered in-place (i.e.
719      * the projects were not cloned to a temporary directory), can be <code>null</code>. This will be set to
720      * <code>null</code> if the POMs have already been filtered during cloning.
721      */
722     private String filteredPomPrefix = "interpolated-";
723 
724     /**
725      * The format for elapsed build time.
726      */
727     private final DecimalFormat secFormat = new DecimalFormat("(0.0 s)", new DecimalFormatSymbols(Locale.ENGLISH));
728 
729     /**
730      * The version of Maven which is used to run the builds
731      */
732     private String actualMavenVersion;
733 
734     // used components - readonly parameters
735 
736     @Parameter(property = "plugin.artifacts", required = true, readonly = true)
737     private List<Artifact> pluginArtifacts;
738 
739     @Parameter(defaultValue = "${project.testClasspathElements}", readonly = true)
740     private List<String> testClassPath;
741 
742     @Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
743     private MojoExecution mojoExecution;
744 
745     @Parameter(defaultValue = "${project}", readonly = true, required = true)
746     private MavenProject project;
747 
748     @Parameter(defaultValue = "${session}", readonly = true, required = true)
749     private MavenSession session;
750 
751     @Parameter(defaultValue = "${settings}", readonly = true, required = true)
752     private Settings settings;
753 
754     @Component
755     private Invoker invoker;
756 
757     @Component
758     private SettingsBuilder settingsBuilder;
759 
760     @Component
761     private ToolchainManagerPrivate toolchainManagerPrivate;
762 
763     /**
764      * Invokes Maven on the configured test projects.
765      *
766      * @throws org.apache.maven.plugin.MojoExecutionException If the goal encountered severe errors.
767      * @throws org.apache.maven.plugin.MojoFailureException If any of the Maven builds failed.
768      */
769     public void execute() throws MojoExecutionException, MojoFailureException {
770         if (skipInvocation) {
771             getLog().info("Skipping invocation per configuration."
772                     + " If this is incorrect, ensure the skipInvocation parameter is not set to true.");
773             return;
774         }
775 
776         if (StringUtils.isEmpty(encoding)) {
777             getLog().warn("File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
778                     + ", i.e. build is platform dependent!");
779         }
780 
781         // done it here to prevent issues with concurrent access in case of parallel run
782         if (!disableReports) {
783             setupReportsFolder();
784         }
785 
786         List<BuildJob> buildJobs;
787         if (pom == null) {
788             try {
789                 buildJobs = getBuildJobs();
790             } catch (final IOException e) {
791                 throw new MojoExecutionException(
792                         "Error retrieving POM list from includes, " + "excludes, and projects directory. Reason: "
793                                 + e.getMessage(),
794                         e);
795             }
796         } else {
797             try {
798                 projectsDirectory = pom.getCanonicalFile().getParentFile();
799             } catch (IOException e) {
800                 throw new MojoExecutionException(
801                         "Failed to discover projectsDirectory from " + "pom File parameter. Reason: " + e.getMessage(),
802                         e);
803             }
804 
805             buildJobs = Collections.singletonList(new BuildJob(pom.getName()));
806         }
807 
808         if (buildJobs.isEmpty()) {
809             doFailIfNoProjects();
810 
811             getLog().info("No projects were selected for execution.");
812             return;
813         }
814 
815         setupActualMavenVersion();
816 
817         handleScriptRunnerWithScriptClassPath();
818 
819         Collection<String> collectedProjects = new LinkedHashSet<>();
820         for (BuildJob buildJob : buildJobs) {
821             collectProjects(projectsDirectory, buildJob.getProject(), collectedProjects, true);
822         }
823 
824         File projectsDir = projectsDirectory;
825 
826         if (cloneProjectsTo == null && "maven-plugin".equals(project.getPackaging())) {
827             cloneProjectsTo = new File(project.getBuild().getDirectory(), "its");
828         }
829 
830         if (updateOnly) {
831             if (cloneProjectsTo == null) {
832                 getLog().warn("updateOnly functionality is not supported without cloning the projects");
833             } else if (lastModifiedRecursive(projectsDirectory) <= lastModifiedRecursive(cloneProjectsTo)) {
834                 getLog().debug("Skipping invocation as cloned projects are up-to-date "
835                         + "and updateOnly parameter is set to true.");
836                 return;
837             } else {
838                 getLog().debug("Cloned projects are out of date");
839             }
840         }
841 
842         if (cloneProjectsTo != null) {
843             cloneProjects(collectedProjects);
844             projectsDir = cloneProjectsTo;
845         } else {
846             getLog().warn("Filtering of parent/child POMs is not supported without cloning the projects");
847         }
848 
849         // First run setup jobs.
850         List<BuildJob> setupBuildJobs = getSetupJobs(buildJobs);
851 
852         if (!setupBuildJobs.isEmpty()) {
853             // Run setup jobs in single thread mode.
854             //
855             // Jobs are ordered according to ordinal value from invoker.properties
856             getLog().info("Running " + setupBuildJobs.size() + " setup job" + ((setupBuildJobs.size() < 2) ? "" : "s")
857                     + ":");
858             runBuilds(projectsDir, setupBuildJobs, 1);
859             getLog().info("Setup done.");
860         }
861 
862         List<BuildJob> nonSetupBuildJobs = getNonSetupJobs(buildJobs);
863 
864         if (setupBuildJobs.isEmpty() || setupBuildJobs.stream().allMatch(BuildJob::isNotError)) {
865             // We will run the non setup jobs with the configured
866             // parallelThreads number.
867             runBuilds(projectsDir, nonSetupBuildJobs, getParallelThreadsCount());
868         } else {
869             for (BuildJob buildJob : nonSetupBuildJobs) {
870                 buildJob.setResult(BuildJob.Result.SKIPPED);
871                 buildJob.setFailureMessage("Skipped due to setup job(s) failure");
872                 writeBuildReport(buildJob);
873             }
874         }
875 
876         writeSummaryFile(buildJobs);
877         processResults(new InvokerSession(buildJobs));
878     }
879 
880     private void setupActualMavenVersion() throws MojoExecutionException {
881         if (mavenHome != null) {
882             try {
883                 actualMavenVersion = SelectorUtils.getMavenVersion(mavenHome);
884             } catch (IOException e) {
885                 throw new MojoExecutionException(e.getMessage(), e);
886             }
887         } else {
888             actualMavenVersion = SelectorUtils.getMavenVersion();
889         }
890     }
891 
892     /**
893      * Find the latest lastModified recursively within a directory structure.
894      *
895      * @param file the root file to check.
896      * @return the latest lastModified time found.
897      */
898     private long lastModifiedRecursive(File file) {
899         long lastModified = file.lastModified();
900 
901         final File[] entries = file.listFiles();
902 
903         if (entries != null) {
904             for (File entry : entries) {
905                 lastModified = Math.max(lastModified, lastModifiedRecursive(entry));
906             }
907         }
908 
909         return lastModified;
910     }
911 
912     /**
913      * This will create the necessary folders for the reports.
914      *
915      * @throws MojoExecutionException in case of failure during creation of the reports folder.
916      */
917     private void setupReportsFolder() throws MojoExecutionException {
918         // If it exists from previous run...
919         if (reportsDirectory.exists()) {
920             try {
921                 FileUtils.deleteDirectory(reportsDirectory);
922             } catch (IOException e) {
923                 throw new MojoExecutionException(
924                         "Failure while trying to delete " + reportsDirectory.getAbsolutePath(), e);
925             }
926         }
927         if (!reportsDirectory.mkdirs()) {
928             throw new MojoExecutionException("Failure while creating the " + reportsDirectory.getAbsolutePath());
929         }
930     }
931 
932     private List<BuildJob> getSetupJobs(List<BuildJob> buildJobs) {
933         return buildJobs.stream()
934                 .filter(buildJob -> buildJob.getType().equals(BuildJob.Type.SETUP))
935                 .collect(Collectors.toList());
936     }
937 
938     private List<BuildJob> getNonSetupJobs(List<BuildJob> buildJobs) {
939         return buildJobs.stream()
940                 .filter(buildJob -> !buildJob.getType().equals(BuildJob.Type.SETUP))
941                 .collect(Collectors.toList());
942     }
943 
944     private void handleScriptRunnerWithScriptClassPath() {
945         final List<String> scriptClassPath;
946         if (addTestClassPath) {
947             scriptClassPath = new ArrayList<>(testClassPath);
948             for (Artifact pluginArtifact : pluginArtifacts) {
949                 scriptClassPath.remove(pluginArtifact.getFile().getAbsolutePath());
950             }
951         } else {
952             scriptClassPath = null;
953         }
954         scriptRunner = new ScriptRunner();
955         scriptRunner.setScriptEncoding(encoding);
956         scriptRunner.setGlobalVariable("localRepositoryPath", localRepositoryPath);
957         scriptRunner.setGlobalVariable("mavenVersion", actualMavenVersion);
958         if (scriptVariables != null) {
959             scriptVariables.forEach((key, value) -> scriptRunner.setGlobalVariable(key, value));
960         }
961         scriptRunner.setClassPath(scriptClassPath);
962     }
963 
964     private void writeSummaryFile(List<BuildJob> buildJobs) throws MojoExecutionException {
965 
966         File summaryReportFile = new File(reportsDirectory, "invoker-summary.txt");
967 
968         try (Writer writer = new BufferedWriter(new FileWriter(summaryReportFile))) {
969             for (BuildJob buildJob : buildJobs) {
970                 if (!BuildJob.Result.SUCCESS.equals(buildJob.getResult())) {
971                     writer.append(buildJob.getResult());
972                     writer.append(" [");
973                     writer.append(buildJob.getProject());
974                     writer.append("] ");
975                     if (buildJob.getFailureMessage() != null) {
976                         writer.append(" ");
977                         writer.append(buildJob.getFailureMessage());
978                     }
979                     writer.append("\n");
980                 }
981             }
982         } catch (IOException e) {
983             throw new MojoExecutionException("Failed to write summary report " + summaryReportFile, e);
984         }
985     }
986 
987     protected void doFailIfNoProjects() throws MojoFailureException {
988         // should only be used during run and verify
989     }
990 
991     /**
992      * Processes the results of invoking the build jobs.
993      *
994      * @param invokerSession The session with the build jobs, must not be <code>null</code>.
995      * @throws MojoFailureException If the mojo had failed as a result of invoking the build jobs.
996      * @since 1.4
997      */
998     abstract void processResults(InvokerSession invokerSession) throws MojoFailureException;
999 
1000     /**
1001      * Collects all projects locally reachable from the specified project. The method will as such try to read the POM
1002      * and recursively follow its parent/module elements.
1003      *
1004      * @param projectsDir The base directory of all projects, must not be <code>null</code>.
1005      * @param projectPath The relative path of the current project, can denote either the POM or its base directory,
1006      *            must not be <code>null</code>.
1007      * @param projectPaths The set of already collected projects to add new projects to, must not be <code>null</code>.
1008      *            This set will hold the relative paths to either a POM file or a project base directory.
1009      * @param included A flag indicating whether the specified project has been explicitly included via the parameter
1010      *            {@link #pomIncludes}. Such projects will always be added to the result set even if there is no
1011      *            corresponding POM.
1012      * @throws org.apache.maven.plugin.MojoExecutionException If the project tree could not be traversed.
1013      */
1014     private void collectProjects(
1015             File projectsDir, String projectPath, Collection<String> projectPaths, boolean included)
1016             throws MojoExecutionException {
1017         projectPath = projectPath.replace('\\', '/');
1018         File pomFile = new File(projectsDir, projectPath);
1019         if (pomFile.isDirectory()) {
1020             pomFile = new File(pomFile, "pom.xml");
1021             if (!pomFile.exists()) {
1022                 if (included) {
1023                     projectPaths.add(projectPath);
1024                 }
1025                 return;
1026             }
1027             if (!projectPath.endsWith("/")) {
1028                 projectPath += '/';
1029             }
1030             projectPath += "pom.xml";
1031         } else if (!pomFile.isFile()) {
1032             return;
1033         }
1034         if (!projectPaths.add(projectPath)) {
1035             return;
1036         }
1037         getLog().debug("Collecting parent/child projects of " + projectPath);
1038 
1039         Model model = PomUtils.loadPom(pomFile);
1040 
1041         try {
1042             String projectsRoot = projectsDir.getCanonicalPath();
1043             String projectDir = pomFile.getParent();
1044 
1045             String parentPath = "../pom.xml";
1046             if (model.getParent() != null
1047                     && StringUtils.isNotEmpty(model.getParent().getRelativePath())) {
1048                 parentPath = model.getParent().getRelativePath();
1049             }
1050             String parent = relativizePath(new File(projectDir, parentPath), projectsRoot);
1051             if (parent != null) {
1052                 collectProjects(projectsDir, parent, projectPaths, false);
1053             }
1054 
1055             Collection<String> modulePaths = new LinkedHashSet<>(model.getModules());
1056 
1057             model.getProfiles().forEach(profile -> modulePaths.addAll(profile.getModules()));
1058 
1059             for (String modulePath : modulePaths) {
1060                 String module = relativizePath(new File(projectDir, modulePath), projectsRoot);
1061                 if (module != null) {
1062                     collectProjects(projectsDir, module, projectPaths, false);
1063                 }
1064             }
1065         } catch (IOException e) {
1066             throw new MojoExecutionException("Failed to analyze POM: " + pomFile, e);
1067         }
1068     }
1069 
1070     /**
1071      * Copies the specified projects to the directory given by {@link #cloneProjectsTo}. A project may either be denoted
1072      * by a path to a POM file or merely by a path to a base directory. During cloning, the POM files will be filtered.
1073      *
1074      * @param projectPaths The paths to the projects to clone, relative to the projects directory, must not be
1075      *            <code>null</code> nor contain <code>null</code> elements.
1076      * @throws org.apache.maven.plugin.MojoExecutionException If the the projects could not be copied/filtered.
1077      */
1078     private void cloneProjects(Collection<String> projectPaths) throws MojoExecutionException {
1079         if (!cloneProjectsTo.mkdirs() && cloneClean) {
1080             try {
1081                 FileUtils.cleanDirectory(cloneProjectsTo);
1082             } catch (IOException e) {
1083                 throw new MojoExecutionException(
1084                         "Could not clean the cloneProjectsTo directory. Reason: " + e.getMessage(), e);
1085             }
1086         }
1087 
1088         // determine project directories to clone
1089         Collection<String> dirs = new LinkedHashSet<>();
1090         for (String projectPath : projectPaths) {
1091             if (!new File(projectsDirectory, projectPath).isDirectory()) {
1092                 projectPath = getParentPath(projectPath);
1093             }
1094             dirs.add(projectPath);
1095         }
1096 
1097         boolean filter;
1098 
1099         // clone project directories
1100         try {
1101             filter = !cloneProjectsTo.getCanonicalFile().equals(projectsDirectory.getCanonicalFile());
1102 
1103             List<String> clonedSubpaths = new ArrayList<>();
1104 
1105             for (String subpath : dirs) {
1106                 // skip this project if its parent directory is also scheduled for cloning
1107                 if (!".".equals(subpath) && dirs.contains(getParentPath(subpath))) {
1108                     continue;
1109                 }
1110 
1111                 // avoid copying subdirs that are already cloned.
1112                 if (!alreadyCloned(subpath, clonedSubpaths)) {
1113                     // avoid creating new files that point to dir/.
1114                     if (".".equals(subpath)) {
1115                         String cloneSubdir = relativizePath(cloneProjectsTo, projectsDirectory.getCanonicalPath());
1116 
1117                         // avoid infinite recursion if the cloneTo path is a subdirectory.
1118                         if (cloneSubdir != null) {
1119                             File temp = Files.createTempDirectory("pre-invocation-clone.")
1120                                     .toFile();
1121 
1122                             copyDirectoryStructure(projectsDirectory, temp);
1123 
1124                             FileUtils.deleteDirectory(new File(temp, cloneSubdir));
1125 
1126                             copyDirectoryStructure(temp, cloneProjectsTo);
1127                         } else {
1128                             copyDirectoryStructure(projectsDirectory, cloneProjectsTo);
1129                         }
1130                     } else {
1131                         File srcDir = new File(projectsDirectory, subpath);
1132                         File dstDir = new File(cloneProjectsTo, subpath);
1133                         copyDirectoryStructure(srcDir, dstDir);
1134                     }
1135 
1136                     clonedSubpaths.add(subpath);
1137                 }
1138             }
1139         } catch (IOException e) {
1140             throw new MojoExecutionException(
1141                     "Failed to clone projects from: " + projectsDirectory + " to: " + cloneProjectsTo + ". Reason: "
1142                             + e.getMessage(),
1143                     e);
1144         }
1145 
1146         // filter cloned POMs
1147         if (filter) {
1148             for (String projectPath : projectPaths) {
1149                 File pomFile = new File(cloneProjectsTo, projectPath);
1150                 if (pomFile.isFile()) {
1151                     buildInterpolatedFile(pomFile, pomFile);
1152                 }
1153 
1154                 // MINVOKER-186
1155                 // The following is a temporary solution to support Maven 3.3.1 (.mvn/extensions.xml) filtering
1156                 // Will be replaced by MINVOKER-117 with general filtering mechanism
1157                 File baseDir = pomFile.getParentFile();
1158                 File mvnDir = new File(baseDir, ".mvn");
1159                 if (mvnDir.isDirectory()) {
1160                     File extensionsFile = new File(mvnDir, "extensions.xml");
1161                     if (extensionsFile.isFile()) {
1162                         buildInterpolatedFile(extensionsFile, extensionsFile);
1163                     }
1164                 }
1165                 // END MINVOKER-186
1166             }
1167             filteredPomPrefix = null;
1168         }
1169     }
1170 
1171     /**
1172      * Gets the parent path of the specified relative path.
1173      *
1174      * @param path The relative path whose parent should be retrieved, must not be <code>null</code>.
1175      * @return The parent path or "." if the specified path has no parent, never <code>null</code>.
1176      */
1177     private String getParentPath(String path) {
1178         int lastSep = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
1179         return (lastSep < 0) ? "." : path.substring(0, lastSep);
1180     }
1181 
1182     /**
1183      * Copied a directory structure with default exclusions (.svn, CVS, etc)
1184      *
1185      * @param sourceDir The source directory to copy, must not be <code>null</code>.
1186      * @param destDir The target directory to copy to, must not be <code>null</code>.
1187      * @throws java.io.IOException If the directory structure could not be copied.
1188      */
1189     private void copyDirectoryStructure(File sourceDir, File destDir) throws IOException {
1190         DirectoryScanner scanner = new DirectoryScanner();
1191         scanner.setBasedir(sourceDir);
1192         if (!cloneAllFiles) {
1193             scanner.addDefaultExcludes();
1194         }
1195         scanner.scan();
1196 
1197         /*
1198          * NOTE: Make sure the destination directory is always there (even if empty) to support POM-less ITs.
1199          */
1200         destDir.mkdirs();
1201         // Create all the directories, including any symlinks present in source
1202         FileUtils.mkDirs(sourceDir, scanner.getIncludedDirectories(), destDir);
1203 
1204         for (String includedFile : scanner.getIncludedFiles()) {
1205             File sourceFile = new File(sourceDir, includedFile);
1206             File destFile = new File(destDir, includedFile);
1207             FileUtils.copyFile(sourceFile, destFile);
1208 
1209             // ensure clone project must be writable for additional changes
1210             destFile.setWritable(true);
1211         }
1212     }
1213 
1214     /**
1215      * Determines whether the specified sub path has already been cloned, i.e. whether one of its ancestor directories
1216      * was already cloned.
1217      *
1218      * @param subpath The sub path to check, must not be <code>null</code>.
1219      * @param clonedSubpaths The list of already cloned paths, must not be <code>null</code> nor contain
1220      *            <code>null</code> elements.
1221      * @return <code>true</code> if the specified path has already been cloned, <code>false</code> otherwise.
1222      */
1223     static boolean alreadyCloned(String subpath, List<String> clonedSubpaths) {
1224         for (String path : clonedSubpaths) {
1225             if (".".equals(path) || subpath.equals(path) || subpath.startsWith(path + File.separator)) {
1226                 return true;
1227             }
1228         }
1229 
1230         return false;
1231     }
1232 
1233     /**
1234      * Runs the specified build jobs.
1235      *
1236      * @param projectsDir The base directory of all projects, must not be <code>null</code>.
1237      * @param buildJobs The build jobs to run must not be <code>null</code> nor contain <code>null</code> elements.
1238      * @throws org.apache.maven.plugin.MojoExecutionException If any build could not be launched.
1239      */
1240     private void runBuilds(final File projectsDir, List<BuildJob> buildJobs, int runWithParallelThreads)
1241             throws MojoExecutionException {
1242         if (!localRepositoryPath.exists()) {
1243             localRepositoryPath.mkdirs();
1244         }
1245 
1246         // -----------------------------------------------
1247         // interpolate settings file
1248         // -----------------------------------------------
1249 
1250         File interpolatedSettingsFile = interpolateSettings(settingsFile);
1251 
1252         final File mergedSettingsFile = mergeSettings(interpolatedSettingsFile);
1253 
1254         final CharSequence actualJreVersion;
1255         // @todo if ( javaVersions ) ... to be picked up from toolchains
1256         if (javaHome != null) {
1257             actualJreVersion = resolveExternalJreVersion();
1258         } else {
1259             actualJreVersion = SelectorUtils.getJreVersion();
1260         }
1261 
1262         final Path projectsPath = this.projectsDirectory.toPath();
1263 
1264         Set<Path> folderGroupSet = new HashSet<>();
1265         folderGroupSet.add(Paths.get("."));
1266         for (BuildJob buildJob : buildJobs) {
1267             Path p = Paths.get(buildJob.getProject());
1268 
1269             if (Files.isRegularFile(projectsPath.resolve(p))) {
1270                 p = p.getParent();
1271             }
1272 
1273             if (p != null) {
1274                 p = p.getParent();
1275             }
1276 
1277             while (p != null && folderGroupSet.add(p)) {
1278                 p = p.getParent();
1279             }
1280         }
1281 
1282         List<Path> folderGroup = new ArrayList<>(folderGroupSet);
1283         Collections.sort(folderGroup);
1284 
1285         final Map<Path, Properties> globalInvokerProperties = new HashMap<>();
1286 
1287         for (Path path : folderGroup) {
1288             Properties ancestorProperties =
1289                     globalInvokerProperties.get(projectsPath.resolve(path).getParent());
1290 
1291             Path currentInvokerProperties = projectsPath.resolve(path).resolve(invokerPropertiesFile);
1292 
1293             Properties currentProperties;
1294             if (Files.isRegularFile(currentInvokerProperties)) {
1295                 if (ancestorProperties != null) {
1296                     currentProperties = new Properties(ancestorProperties);
1297 
1298                 } else {
1299                     currentProperties = new Properties();
1300                 }
1301             } else {
1302                 currentProperties = ancestorProperties;
1303             }
1304 
1305             if (Files.isRegularFile(currentInvokerProperties)) {
1306                 try (InputStream in = new FileInputStream(currentInvokerProperties.toFile())) {
1307                     currentProperties.load(in);
1308                 } catch (IOException e) {
1309                     throw new MojoExecutionException("Failed to read invoker properties: " + currentInvokerProperties);
1310                 }
1311             }
1312 
1313             if (currentProperties != null) {
1314                 globalInvokerProperties.put(projectsPath.resolve(path).normalize(), currentProperties);
1315             }
1316         }
1317 
1318         try {
1319             if (runWithParallelThreads > 1) {
1320                 getLog().info("use parallelThreads " + runWithParallelThreads);
1321             }
1322 
1323             JobExecutor jobExecutor = new JobExecutor(buildJobs, runWithParallelThreads);
1324             jobExecutor.forEach(job -> {
1325                 Path ancestorFolder = getAncestorFolder(projectsPath.resolve(job.getProject()));
1326 
1327                 runBuild(
1328                         projectsDir,
1329                         job,
1330                         mergedSettingsFile,
1331                         javaHome,
1332                         actualJreVersion,
1333                         globalInvokerProperties.get(ancestorFolder));
1334             });
1335         } finally {
1336             if (interpolatedSettingsFile != null && cloneProjectsTo == null) {
1337                 interpolatedSettingsFile.delete();
1338             }
1339             if (mergedSettingsFile != null && mergedSettingsFile.exists()) {
1340                 mergedSettingsFile.delete();
1341             }
1342         }
1343     }
1344 
1345     private Path getAncestorFolder(Path p) {
1346         Path ancestor = p;
1347         if (Files.isRegularFile(ancestor)) {
1348             ancestor = ancestor.getParent();
1349         }
1350         if (ancestor != null) {
1351             ancestor = ancestor.getParent();
1352         }
1353         return ancestor;
1354     }
1355 
1356     /**
1357      * Interpolate settings.xml file.
1358      * @param settingsFile a settings file
1359      *
1360      * @return The interpolated settings.xml file.
1361      * @throws MojoExecutionException in case of a problem.
1362      */
1363     private File interpolateSettings(File settingsFile) throws MojoExecutionException {
1364         File interpolatedSettingsFile = null;
1365         if (settingsFile != null) {
1366             if (cloneProjectsTo != null) {
1367                 interpolatedSettingsFile = new File(cloneProjectsTo, "interpolated-" + settingsFile.getName());
1368             } else {
1369                 interpolatedSettingsFile =
1370                         new File(settingsFile.getParentFile(), "interpolated-" + settingsFile.getName());
1371             }
1372             buildInterpolatedFile(settingsFile, interpolatedSettingsFile);
1373         }
1374         return interpolatedSettingsFile;
1375     }
1376 
1377     /**
1378      * Merge the settings file
1379      *
1380      * @param interpolatedSettingsFile The interpolated settings file.
1381      * @return The merged settings file.
1382      * @throws MojoExecutionException Fail the build in case the merged settings file can't be created.
1383      */
1384     private File mergeSettings(File interpolatedSettingsFile) throws MojoExecutionException {
1385         File mergedSettingsFile;
1386         Settings mergedSettings = this.settings;
1387         if (mergeUserSettings) {
1388             if (interpolatedSettingsFile != null) {
1389                 // Have to merge the specified settings file (dominant) and the one of the invoking Maven process
1390                 try {
1391                     SettingsBuildingRequest request = new DefaultSettingsBuildingRequest();
1392                     request.setGlobalSettingsFile(interpolatedSettingsFile);
1393 
1394                     Settings dominantSettings = settingsBuilder.build(request).getEffectiveSettings();
1395                     Settings recessiveSettings = cloneSettings();
1396                     SettingsUtils.merge(dominantSettings, recessiveSettings, TrackableBase.USER_LEVEL);
1397 
1398                     mergedSettings = dominantSettings;
1399                     getLog().debug("Merged specified settings file with settings of invoking process");
1400                 } catch (SettingsBuildingException e) {
1401                     throw new MojoExecutionException("Could not read specified settings file", e);
1402                 }
1403             }
1404         }
1405 
1406         if (this.settingsFile != null && !mergeUserSettings) {
1407             mergedSettingsFile = interpolatedSettingsFile;
1408         } else {
1409             try {
1410                 mergedSettingsFile = writeMergedSettingsFile(mergedSettings);
1411             } catch (IOException e) {
1412                 throw new MojoExecutionException("Could not create temporary file for invoker settings.xml", e);
1413             }
1414         }
1415         return mergedSettingsFile;
1416     }
1417 
1418     private File writeMergedSettingsFile(Settings mergedSettings) throws IOException {
1419         File mergedSettingsFile;
1420         mergedSettingsFile = File.createTempFile("invoker-settings", ".xml");
1421 
1422         SettingsXpp3Writer settingsWriter = new SettingsXpp3Writer();
1423 
1424         try (FileWriter fileWriter = new FileWriter(mergedSettingsFile)) {
1425             settingsWriter.write(fileWriter, mergedSettings);
1426         }
1427 
1428         if (getLog().isDebugEnabled()) {
1429             getLog().debug("Created temporary file for invoker settings.xml: " + mergedSettingsFile.getAbsolutePath());
1430         }
1431         return mergedSettingsFile;
1432     }
1433 
1434     private Settings cloneSettings() {
1435         Settings recessiveSettings = SettingsUtils.copySettings(this.settings);
1436 
1437         // MINVOKER-133: reset sourceLevelSet
1438         resetSourceLevelSet(recessiveSettings);
1439         for (org.apache.maven.settings.Mirror mirror : recessiveSettings.getMirrors()) {
1440             resetSourceLevelSet(mirror);
1441         }
1442         for (org.apache.maven.settings.Server server : recessiveSettings.getServers()) {
1443             resetSourceLevelSet(server);
1444         }
1445         for (org.apache.maven.settings.Proxy proxy : recessiveSettings.getProxies()) {
1446             resetSourceLevelSet(proxy);
1447         }
1448         for (org.apache.maven.settings.Profile profile : recessiveSettings.getProfiles()) {
1449             resetSourceLevelSet(profile);
1450         }
1451 
1452         return recessiveSettings;
1453     }
1454 
1455     private void resetSourceLevelSet(org.apache.maven.settings.TrackableBase trackable) {
1456         try {
1457             ReflectionUtils.setVariableValueInObject(trackable, "sourceLevelSet", Boolean.FALSE);
1458             getLog().debug("sourceLevelSet: "
1459                     + ReflectionUtils.getValueIncludingSuperclasses("sourceLevelSet", trackable));
1460         } catch (IllegalAccessException e) {
1461             // noop
1462         }
1463     }
1464 
1465     private CharSequence resolveExternalJreVersion() {
1466         Artifact pluginArtifact =
1467                 mojoExecution.getMojoDescriptor().getPluginDescriptor().getPluginArtifact();
1468         pluginArtifact.getFile();
1469 
1470         Commandline commandLine = new Commandline();
1471         commandLine.setExecutable(new File(javaHome, "bin/java").getAbsolutePath());
1472         commandLine.createArg().setValue("-cp");
1473         commandLine.createArg().setFile(pluginArtifact.getFile());
1474         commandLine.createArg().setValue(SystemPropertyPrinter.class.getName());
1475         commandLine.createArg().setValue("java.version");
1476 
1477         final StringBuilder actualJreVersion = new StringBuilder();
1478         StreamConsumer consumer = actualJreVersion::append;
1479         try {
1480             CommandLineUtils.executeCommandLine(commandLine, consumer, null);
1481         } catch (CommandLineException e) {
1482             getLog().warn(e.getMessage());
1483         }
1484         return actualJreVersion;
1485     }
1486 
1487     /**
1488      * Interpolate the pom file.
1489      *
1490      * @param pomFile The pom file.
1491      * @param basedir The base directory.
1492      * @return interpolated pom file location in case we have interpolated the pom file otherwise the original pom file
1493      *         will be returned.
1494      * @throws MojoExecutionException
1495      */
1496     private File interpolatePomFile(File pomFile, File basedir) throws MojoExecutionException {
1497         File interpolatedPomFile = null;
1498         if (pomFile != null) {
1499             if (StringUtils.isNotEmpty(filteredPomPrefix)) {
1500                 interpolatedPomFile = new File(basedir, filteredPomPrefix + pomFile.getName());
1501                 buildInterpolatedFile(pomFile, interpolatedPomFile);
1502             } else {
1503                 interpolatedPomFile = pomFile;
1504             }
1505         }
1506         return interpolatedPomFile;
1507     }
1508 
1509     /**
1510      * Runs the specified project.
1511      *
1512      * @param projectsDir The base directory of all projects, must not be <code>null</code>.
1513      * @param buildJob The build job to run, must not be <code>null</code>.
1514      * @param settingsFile The (already interpolated) user settings file for the build, may be <code>null</code> to use
1515      *            the current user settings.
1516      * @param globalInvokerProperties
1517      * @throws org.apache.maven.plugin.MojoExecutionException If the project could not be launched.
1518      */
1519     private void runBuild(
1520             File projectsDir,
1521             BuildJob buildJob,
1522             File settingsFile,
1523             File actualJavaHome,
1524             CharSequence actualJreVersion,
1525             Properties globalInvokerProperties)
1526             throws MojoExecutionException {
1527         // FIXME: Think about the following code part -- START
1528         File pomFile = new File(projectsDir, buildJob.getProject());
1529         File basedir;
1530         if (pomFile.isDirectory()) {
1531             basedir = pomFile;
1532             pomFile = new File(basedir, "pom.xml");
1533             if (!pomFile.exists()) {
1534                 pomFile = null;
1535             } else {
1536                 buildJob.setProject(buildJob.getProject() + File.separator + "pom.xml");
1537             }
1538         } else {
1539             basedir = pomFile.getParentFile();
1540         }
1541 
1542         File interpolatedPomFile = interpolatePomFile(pomFile, basedir);
1543         // FIXME: Think about the following code part -- ^^^^^^^ END
1544 
1545         getLog().info(buffer().a("Building: ").strong(buildJob.getProject()).toString());
1546 
1547         InvokerProperties invokerProperties = getInvokerProperties(basedir, globalInvokerProperties);
1548 
1549         // let's set what details we can
1550         buildJob.setName(invokerProperties.getJobName());
1551         buildJob.setDescription(invokerProperties.getJobDescription());
1552 
1553         try {
1554             int selection = getSelection(invokerProperties, actualJreVersion);
1555             if (selection == 0) {
1556                 long milliseconds = System.currentTimeMillis();
1557                 boolean executed;
1558 
1559                 FileLogger buildLogger = setupBuildLogFile(basedir);
1560                 if (buildLogger != null) {
1561                     buildJob.setBuildlog(buildLogger.getOutputFile().getAbsolutePath());
1562                 }
1563 
1564                 try {
1565                     executed = runBuild(
1566                             basedir, interpolatedPomFile, settingsFile, actualJavaHome, invokerProperties, buildLogger);
1567                 } finally {
1568                     milliseconds = System.currentTimeMillis() - milliseconds;
1569                     buildJob.setTime(milliseconds / 1000.0);
1570 
1571                     if (buildLogger != null) {
1572                         buildLogger.close();
1573                     }
1574                 }
1575 
1576                 if (executed) {
1577                     buildJob.setResult(BuildJob.Result.SUCCESS);
1578 
1579                     if (!suppressSummaries) {
1580                         getLog().info(pad(buildJob).success("SUCCESS").a(' ') + formatTime(buildJob.getTime()));
1581                     }
1582                 } else {
1583                     buildJob.setResult(BuildJob.Result.SKIPPED);
1584 
1585                     if (!suppressSummaries) {
1586                         getLog().info(pad(buildJob).warning("SKIPPED").a(' ') + formatTime(buildJob.getTime()));
1587                     }
1588                 }
1589             } else {
1590                 buildJob.setResult(BuildJob.Result.SKIPPED);
1591 
1592                 List<String> messages = new ArrayList<>();
1593 
1594                 if (selection == Selector.SELECTOR_MULTI) {
1595                     messages.add("non-matching selectors");
1596                 } else {
1597                     if ((selection & Selector.SELECTOR_MAVENVERSION) != 0) {
1598                         messages.add("Maven version");
1599                     }
1600                     if ((selection & Selector.SELECTOR_JREVERSION) != 0) {
1601                         messages.add("JRE version");
1602                     }
1603                     if ((selection & Selector.SELECTOR_OSFAMILY) != 0) {
1604                         messages.add("OS");
1605                     }
1606                     if ((selection & Selector.SELECTOR_TOOLCHAIN) != 0) {
1607                         messages.add("Toolchain");
1608                     }
1609                 }
1610 
1611                 String message = String.join(", ", messages);
1612                 if (!suppressSummaries) {
1613                     getLog().info(pad(buildJob).warning("SKIPPED") + " due to " + message);
1614                 }
1615 
1616                 // Abuse failureMessage, the field in the report which should contain the reason for skipping
1617                 // Consider skipCode + I18N
1618                 buildJob.setFailureMessage("Skipped due to " + message);
1619             }
1620         } catch (RunFailureException e) {
1621             buildJob.setResult(e.getType());
1622             buildJob.setFailureMessage(e.getMessage());
1623 
1624             if (!suppressSummaries) {
1625                 getLog().info("  " + e.getMessage());
1626                 getLog().info(pad(buildJob).failure("FAILED").a(' ') + formatTime(buildJob.getTime()));
1627             }
1628         } finally {
1629             deleteInterpolatedPomFile(interpolatedPomFile);
1630             writeBuildReport(buildJob);
1631         }
1632     }
1633 
1634     private MessageBuilder pad(BuildJob buildJob) {
1635         MessageBuilder buffer = buffer(128);
1636 
1637         buffer.a("          ");
1638         buffer.a(buildJob.getProject());
1639 
1640         int l = 10 + buildJob.getProject().length();
1641 
1642         if (l < RESULT_COLUMN) {
1643             buffer.a(' ');
1644             l++;
1645 
1646             if (l < RESULT_COLUMN) {
1647                 for (int i = RESULT_COLUMN - l; i > 0; i--) {
1648                     buffer.a('.');
1649                 }
1650             }
1651         }
1652 
1653         return buffer.a(' ');
1654     }
1655 
1656     /**
1657      * Delete the interpolated pom file if it has been created before.
1658      *
1659      * @param interpolatedPomFile The interpolated pom file.
1660      */
1661     private void deleteInterpolatedPomFile(File interpolatedPomFile) {
1662         if (interpolatedPomFile != null && StringUtils.isNotEmpty(filteredPomPrefix)) {
1663             interpolatedPomFile.delete();
1664         }
1665     }
1666 
1667     /**
1668      * Determines whether selector conditions of the specified invoker properties match the current environment.
1669      *
1670      * @param invokerProperties The invoker properties to check, must not be <code>null</code>.
1671      * @return <code>0</code> if the job corresponding to the properties should be run, otherwise a bitwise value
1672      *         representing the reason why it should be skipped.
1673      */
1674     private int getSelection(InvokerProperties invokerProperties, CharSequence actualJreVersion) {
1675         return new Selector(actualMavenVersion, actualJreVersion.toString(), getToolchainPrivateManager())
1676                 .getSelection(invokerProperties);
1677     }
1678 
1679     private ToolchainPrivateManager getToolchainPrivateManager() {
1680         return new ToolchainPrivateManager(toolchainManagerPrivate, session);
1681     }
1682 
1683     /**
1684      * Writes the XML report for the specified build job unless report generation has been disabled.
1685      *
1686      * @param buildJob The build job whose report should be written, must not be <code>null</code>.
1687      * @throws org.apache.maven.plugin.MojoExecutionException If the report could not be written.
1688      */
1689     private void writeBuildReport(BuildJob buildJob) throws MojoExecutionException {
1690         if (disableReports) {
1691             return;
1692         }
1693 
1694         String safeFileName =
1695                 buildJob.getProject().replace('/', '_').replace('\\', '_').replace(' ', '_');
1696         if (safeFileName.endsWith("_pom.xml")) {
1697             safeFileName = safeFileName.substring(0, safeFileName.length() - "_pom.xml".length());
1698         }
1699 
1700         File reportFile = new File(reportsDirectory, "BUILD-" + safeFileName + ".xml");
1701         try (FileOutputStream fos = new FileOutputStream(reportFile);
1702                 Writer osw = new OutputStreamWriter(fos, buildJob.getModelEncoding())) {
1703             BuildJobXpp3Writer writer = new BuildJobXpp3Writer();
1704 
1705             writer.write(osw, buildJob);
1706         } catch (IOException e) {
1707             throw new MojoExecutionException("Failed to write build report " + reportFile, e);
1708         }
1709 
1710         if (writeJunitReport) {
1711             writeJunitReport(buildJob, safeFileName);
1712         }
1713     }
1714 
1715     private void writeJunitReport(BuildJob buildJob, String safeFileName) throws MojoExecutionException {
1716         File reportFile = new File(reportsDirectory, "TEST-" + safeFileName + ".xml");
1717         Xpp3Dom testsuite = new Xpp3Dom("testsuite");
1718         testsuite.setAttribute("name", junitPackageName + "." + safeFileName);
1719         testsuite.setAttribute("time", Double.toString(buildJob.getTime()));
1720 
1721         // set default value for required attributes
1722         testsuite.setAttribute("tests", "1");
1723         testsuite.setAttribute("errors", "0");
1724         testsuite.setAttribute("skipped", "0");
1725         testsuite.setAttribute("failures", "0");
1726 
1727         Xpp3Dom testcase = new Xpp3Dom("testcase");
1728         testsuite.addChild(testcase);
1729         switch (buildJob.getResult()) {
1730             case BuildJob.Result.SUCCESS:
1731                 break;
1732             case BuildJob.Result.SKIPPED:
1733                 testsuite.setAttribute("skipped", "1");
1734                 // adding the failure element
1735                 Xpp3Dom skipped = new Xpp3Dom("skipped");
1736                 testcase.addChild(skipped);
1737                 skipped.setValue(buildJob.getFailureMessage());
1738                 break;
1739             case BuildJob.Result.ERROR:
1740                 testsuite.setAttribute("errors", "1");
1741                 break;
1742             default:
1743                 testsuite.setAttribute("failures", "1");
1744                 // adding the failure element
1745                 Xpp3Dom failure = new Xpp3Dom("failure");
1746                 testcase.addChild(failure);
1747                 failure.setAttribute("message", buildJob.getFailureMessage());
1748         }
1749         testcase.setAttribute("classname", junitPackageName + "." + safeFileName);
1750         testcase.setAttribute("name", safeFileName);
1751         testcase.setAttribute("time", Double.toString(buildJob.getTime()));
1752         Xpp3Dom systemOut = new Xpp3Dom("system-out");
1753         testcase.addChild(systemOut);
1754 
1755         File buildLogFile = buildJob.getBuildlog() != null ? new File(buildJob.getBuildlog()) : null;
1756 
1757         if (buildLogFile != null && buildLogFile.exists()) {
1758             getLog().debug("fileLogger:" + buildLogFile);
1759             try {
1760                 systemOut.setValue(FileUtils.fileRead(buildLogFile));
1761             } catch (IOException e) {
1762                 throw new MojoExecutionException("Failed to read logfile " + buildLogFile, e);
1763             }
1764         } else {
1765             getLog().debug(safeFileName + "not exists buildLogFile = " + buildLogFile);
1766         }
1767 
1768         try (FileOutputStream fos = new FileOutputStream(reportFile);
1769                 Writer osw = new OutputStreamWriter(fos, buildJob.getModelEncoding())) {
1770             Xpp3DomWriter.write(osw, testsuite);
1771         } catch (IOException e) {
1772             throw new MojoExecutionException("Failed to write JUnit build report " + reportFile, e);
1773         }
1774     }
1775 
1776     /**
1777      * Formats the specified build duration time.
1778      *
1779      * @param seconds The duration of the build.
1780      * @return The formatted time, never <code>null</code>.
1781      */
1782     private String formatTime(double seconds) {
1783         return secFormat.format(seconds);
1784     }
1785 
1786     /**
1787      * Runs the specified project.
1788      *
1789      * @param basedir The base directory of the project, must not be <code>null</code>.
1790      * @param pomFile The (already interpolated) POM file, may be <code>null</code> for a POM-less Maven invocation.
1791      * @param settingsFile The (already interpolated) user settings file for the build, may be <code>null</code>. Will
1792      *            be merged with the settings file of the invoking Maven process.
1793      * @param invokerProperties The properties to use.
1794      * @param logger file logger to write execution build.log
1795      * @return <code>true</code> if the project was launched or <code>false</code> if the selector script indicated that
1796      *         the project should be skipped.
1797      * @throws org.apache.maven.plugin.MojoExecutionException If the project could not be launched.
1798      * @throws RunFailureException If either a hook script or the build itself
1799      *             failed.
1800      */
1801     private boolean runBuild(
1802             File basedir,
1803             File pomFile,
1804             File settingsFile,
1805             File actualJavaHome,
1806             InvokerProperties invokerProperties,
1807             FileLogger logger)
1808             throws MojoExecutionException, RunFailureException {
1809         if (getLog().isDebugEnabled() && !invokerProperties.getProperties().isEmpty()) {
1810             Properties props = invokerProperties.getProperties();
1811             getLog().debug("Using invoker properties:");
1812             for (String key : new TreeSet<>(props.stringPropertyNames())) {
1813                 String value = props.getProperty(key);
1814                 getLog().debug("  " + key + " = " + value);
1815             }
1816         }
1817 
1818         Map<String, Object> context = new LinkedHashMap<>();
1819 
1820         boolean selectorResult = true;
1821 
1822         try {
1823             try {
1824                 scriptRunner.run("selector script", basedir, selectorScript, context, logger);
1825             } catch (ScriptReturnException e) {
1826                 selectorResult = false;
1827                 return false;
1828             } catch (ScriptException e) {
1829                 throw new RunFailureException(BuildJob.Result.ERROR, e);
1830             }
1831 
1832             try {
1833                 scriptRunner.run("pre-build script", basedir, preBuildHookScript, context, logger);
1834             } catch (ScriptException e) {
1835                 throw new RunFailureException(BuildJob.Result.FAILURE_PRE_HOOK, e);
1836             }
1837 
1838             for (int invocationIndex = 1; ; invocationIndex++) {
1839                 if (invocationIndex > 1 && !invokerProperties.isInvocationDefined(invocationIndex)) {
1840                     break;
1841                 }
1842 
1843                 final InvocationRequest request = new DefaultInvocationRequest();
1844 
1845                 request.setBatchMode(true);
1846 
1847                 // values only from Mojo configurations
1848                 request.setLocalRepositoryDirectory(localRepositoryPath);
1849                 request.setShowErrors(showErrors);
1850                 request.setShowVersion(showVersion);
1851                 request.setJavaHome(actualJavaHome);
1852                 request.setMavenHome(mavenHome);
1853                 setupLoggerForBuildJob(logger, request);
1854 
1855                 request.setBaseDirectory(basedir);
1856                 request.setPomFile(pomFile);
1857 
1858                 String customSettingsFile = invokerProperties.getSettingsFile(invocationIndex);
1859                 if (customSettingsFile != null) {
1860                     File interpolateSettingsFile = interpolateSettings(new File(customSettingsFile));
1861                     File mergeSettingsFile = mergeSettings(interpolateSettingsFile);
1862 
1863                     request.setUserSettingsFile(mergeSettingsFile);
1864                 } else {
1865                     request.setUserSettingsFile(settingsFile);
1866                 }
1867 
1868                 Properties systemProperties =
1869                         getSystemProperties(basedir, invokerProperties.getSystemPropertiesFile(invocationIndex));
1870                 request.setProperties(systemProperties);
1871 
1872                 invokerProperties.configureInvocation(request, invocationIndex);
1873 
1874                 if (getLog().isDebugEnabled()) {
1875                     try {
1876                         getLog().debug("Using MAVEN_OPTS: " + request.getMavenOpts());
1877                         getLog().debug("Executing: " + new MavenCommandLineBuilder().build(request));
1878                     } catch (CommandLineConfigurationException e) {
1879                         getLog().debug("Failed to display command line: " + e.getMessage());
1880                     }
1881                 }
1882 
1883                 try {
1884                     InvocationResult result = invoker.execute(request);
1885                     verify(result, invocationIndex, invokerProperties, logger);
1886                 } catch (final MavenInvocationException e) {
1887                     getLog().debug("Error invoking Maven: " + e.getMessage(), e);
1888                     throw new RunFailureException(
1889                             "Maven invocation failed. " + e.getMessage(), BuildJob.Result.FAILURE_BUILD);
1890                 }
1891             }
1892         } catch (IOException e) {
1893             throw new MojoExecutionException(e.getMessage(), e);
1894         } finally {
1895             if (selectorResult) {
1896                 runPostBuildHook(basedir, context, logger);
1897             }
1898         }
1899         return true;
1900     }
1901 
1902     int getParallelThreadsCount() {
1903         if (parallelThreads.endsWith("C")) {
1904             double parallelThreadsMultiple =
1905                     Double.parseDouble(parallelThreads.substring(0, parallelThreads.length() - 1));
1906             return (int) (parallelThreadsMultiple * Runtime.getRuntime().availableProcessors());
1907         } else {
1908             return Integer.parseInt(parallelThreads);
1909         }
1910     }
1911 
1912     private void runPostBuildHook(File basedir, Map<String, Object> context, FileLogger logger)
1913             throws MojoExecutionException, RunFailureException {
1914         try {
1915             scriptRunner.run("post-build script", basedir, postBuildHookScript, context, logger);
1916         } catch (IOException e) {
1917             throw new MojoExecutionException(e.getMessage(), e);
1918         } catch (ScriptException e) {
1919             throw new RunFailureException(e.getMessage(), BuildJob.Result.FAILURE_POST_HOOK, e);
1920         }
1921     }
1922 
1923     private void setupLoggerForBuildJob(final FileLogger logger, final InvocationRequest request) {
1924         if (logger != null) {
1925             request.setErrorHandler(logger);
1926             request.setOutputHandler(logger);
1927         }
1928     }
1929 
1930     /**
1931      * Initializes the build logger for the specified project. This will write the logging information into
1932      * {@code build.log}.
1933      *
1934      * @param basedir The base directory of the project, must not be <code>null</code>.
1935      * @return The build logger or <code>null</code> if logging has been disabled.
1936      * @throws org.apache.maven.plugin.MojoExecutionException If the log file could not be created.
1937      */
1938     private FileLogger setupBuildLogFile(File basedir) throws MojoExecutionException {
1939         FileLogger logger = null;
1940 
1941         if (!noLog) {
1942             Path projectLogDirectory;
1943             if (logDirectory == null) {
1944                 projectLogDirectory = basedir.toPath();
1945             } else if (cloneProjectsTo != null) {
1946                 projectLogDirectory =
1947                         logDirectory.toPath().resolve(cloneProjectsTo.toPath().relativize(basedir.toPath()));
1948             } else {
1949                 projectLogDirectory =
1950                         logDirectory.toPath().resolve(projectsDirectory.toPath().relativize(basedir.toPath()));
1951             }
1952 
1953             try {
1954                 if (streamLogs) {
1955                     logger = new FileLogger(
1956                             projectLogDirectory.resolve("build.log").toFile(), getLog());
1957                 } else {
1958                     logger = new FileLogger(
1959                             projectLogDirectory.resolve("build.log").toFile());
1960                 }
1961 
1962                 getLog().debug("Build log initialized in: " + projectLogDirectory);
1963             } catch (IOException e) {
1964                 throw new MojoExecutionException("Error initializing build logfile in: " + projectLogDirectory, e);
1965             }
1966         }
1967 
1968         return logger;
1969     }
1970 
1971     /**
1972      * Gets the system properties to use for the specified project.
1973      *
1974      * @param basedir The base directory of the project, must not be <code>null</code>.
1975      * @param filename The filename to the properties file to load, may be <code>null</code> to use the default path
1976      *            given by {@link #testPropertiesFile}.
1977      * @return The system properties to use, may be empty but never <code>null</code>.
1978      * @throws org.apache.maven.plugin.MojoExecutionException If the properties file exists but could not be read.
1979      */
1980     private Properties getSystemProperties(final File basedir, final String filename) throws MojoExecutionException {
1981         Properties collectedTestProperties = new Properties();
1982 
1983         if (properties != null) {
1984             // MINVOKER-118: property can have empty value, which is not accepted by collectedTestProperties
1985             for (Map.Entry<String, String> entry : properties.entrySet()) {
1986                 if (entry.getValue() != null) {
1987                     collectedTestProperties.put(entry.getKey(), entry.getValue());
1988                 }
1989             }
1990         }
1991 
1992         File propertiesFile = null;
1993         if (filename != null) {
1994             propertiesFile = new File(basedir, filename);
1995         } else if (testPropertiesFile != null) {
1996             propertiesFile = new File(basedir, testPropertiesFile);
1997         }
1998 
1999         if (propertiesFile != null && propertiesFile.isFile()) {
2000 
2001             try (InputStream fin = new FileInputStream(propertiesFile)) {
2002                 Properties loadedProperties = new Properties();
2003                 loadedProperties.load(fin);
2004                 collectedTestProperties.putAll(loadedProperties);
2005             } catch (IOException e) {
2006                 throw new MojoExecutionException("Error reading system properties from " + propertiesFile);
2007             }
2008         }
2009 
2010         return collectedTestProperties;
2011     }
2012 
2013     /**
2014      * Verifies the invocation result.
2015      *
2016      * @param result The invocation result to check, must not be <code>null</code>.
2017      * @param invocationIndex The index of the invocation for which to check the exit code, must not be negative.
2018      * @param invokerProperties The invoker properties used to check the exit code, must not be <code>null</code>.
2019      * @param logger The build logger, may be <code>null</code> if logging is disabled.
2020      */
2021     private void verify(
2022             InvocationResult result, int invocationIndex, InvokerProperties invokerProperties, FileLogger logger)
2023             throws RunFailureException {
2024         if (result.getExecutionException() != null) {
2025             throw new RunFailureException(
2026                     "The Maven invocation failed. "
2027                             + result.getExecutionException().getMessage(),
2028                     BuildJob.Result.ERROR);
2029         } else if (!invokerProperties.isExpectedResult(result.getExitCode(), invocationIndex)) {
2030             StringBuilder buffer = new StringBuilder(256);
2031             buffer.append("The build exited with code ")
2032                     .append(result.getExitCode())
2033                     .append(". ");
2034             if (logger != null) {
2035                 buffer.append("See ");
2036                 buffer.append(logger.getOutputFile().getAbsolutePath());
2037                 buffer.append(" for details.");
2038             } else {
2039                 buffer.append("See console output for details.");
2040             }
2041             throw new RunFailureException(buffer.toString(), BuildJob.Result.FAILURE_BUILD);
2042         }
2043     }
2044 
2045     private List<String> calculateIncludes() {
2046         if (invokerTest != null) {
2047             String[] testRegexes = StringUtils.split(invokerTest, ",");
2048             return Arrays.stream(testRegexes)
2049                     .map(String::trim)
2050                     .filter(s -> !s.isEmpty())
2051                     .filter(s -> !s.startsWith("!"))
2052                     .collect(Collectors.toList());
2053         } else {
2054             Set<String> uniqueIncludes = new HashSet<>();
2055             uniqueIncludes.addAll(pomIncludes);
2056             uniqueIncludes.addAll(setupIncludes);
2057             return new ArrayList<>(uniqueIncludes);
2058         }
2059     }
2060 
2061     private List<String> calculateExcludes() throws IOException {
2062         List<String> excludes;
2063 
2064         if (invokerTest != null) {
2065             String[] testRegexes = StringUtils.split(invokerTest, ",");
2066             excludes = Arrays.stream(testRegexes)
2067                     .map(String::trim)
2068                     .filter(s -> !s.isEmpty())
2069                     .filter(s -> s.startsWith("!"))
2070                     .map(s -> s.substring(1))
2071                     .collect(Collectors.toList());
2072         } else {
2073             excludes = pomExcludes != null ? new ArrayList<>(pomExcludes) : new ArrayList<>();
2074         }
2075 
2076         if (this.settingsFile != null) {
2077             String exclude = relativizePath(this.settingsFile, projectsDirectory.getCanonicalPath());
2078             if (exclude != null) {
2079                 excludes.add(exclude.replace('\\', '/'));
2080                 getLog().debug("Automatically excluded " + exclude + " from project scanning");
2081             }
2082         }
2083         return excludes;
2084     }
2085 
2086     /**
2087      * Gets the build jobs that should be processed. Note that the order of the returned build jobs is significant.
2088      *
2089      * @return The build jobs to process, may be empty but never <code>null</code>.
2090      * @throws java.io.IOException If the projects directory could not be scanned.
2091      */
2092     List<BuildJob> getBuildJobs() throws IOException, MojoExecutionException {
2093 
2094         List<String> includes = calculateIncludes();
2095         List<String> excludes = calculateExcludes();
2096         List<BuildJob> buildJobsAll = scanProjectsDirectory(includes, excludes);
2097         List<BuildJob> buildJobsSetup = scanProjectsDirectory(setupIncludes, excludes);
2098 
2099         List<String> setupProjects =
2100                 buildJobsSetup.stream().map(BuildJob::getProject).collect(Collectors.toList());
2101 
2102         for (BuildJob job : buildJobsAll) {
2103             if (setupProjects.contains(job.getProject())) {
2104                 job.setType(BuildJob.Type.SETUP);
2105             }
2106             InvokerProperties invokerProperties =
2107                     getInvokerProperties(new File(projectsDirectory, job.getProject()).getParentFile(), null);
2108             job.setOrdinal(invokerProperties.getOrdinal());
2109         }
2110 
2111         relativizeProjectPaths(buildJobsAll);
2112 
2113         return buildJobsAll;
2114     }
2115 
2116     /**
2117      * Scans the projects directory for projects to build. Both (POM) files and mere directories will be matched by the
2118      * scanner patterns. If the patterns match a directory which contains a file named "pom.xml", the results will
2119      * include the path to this file rather than the directory path in order to avoid duplicate invocations of the same
2120      * project.
2121      *
2122      * @param includes The include patterns for the scanner, may be <code>null</code>.
2123      * @param excludes The exclude patterns for the scanner, may be <code>null</code> to exclude nothing.
2124      * @return The build jobs matching the patterns, never <code>null</code>.
2125      * @throws java.io.IOException If the project directory could not be scanned.
2126      */
2127     private List<BuildJob> scanProjectsDirectory(List<String> includes, List<String> excludes) throws IOException {
2128         if (!projectsDirectory.isDirectory()) {
2129             return Collections.emptyList();
2130         }
2131 
2132         DirectoryScanner scanner = new DirectoryScanner();
2133         scanner.setBasedir(projectsDirectory.getCanonicalFile());
2134         scanner.setFollowSymlinks(false);
2135         if (includes != null) {
2136             scanner.setIncludes(includes.toArray(new String[0]));
2137         }
2138         if (excludes != null) {
2139             if ((includes == null || includes.isEmpty()) && !excludes.isEmpty()) {
2140                 scanner.setIncludes(new String[] {"*"});
2141             }
2142             scanner.setExcludes(excludes.toArray(new String[0]));
2143         }
2144         scanner.addDefaultExcludes();
2145         scanner.scan();
2146 
2147         Map<String, BuildJob> matches = new LinkedHashMap<>();
2148 
2149         for (String includedFile : scanner.getIncludedFiles()) {
2150             matches.put(includedFile, new BuildJob(includedFile));
2151         }
2152 
2153         for (String includedDir : scanner.getIncludedDirectories()) {
2154             String includedFile = includedDir + File.separatorChar + "pom.xml";
2155             if (new File(scanner.getBasedir(), includedFile).isFile()) {
2156                 matches.put(includedFile, new BuildJob(includedFile));
2157             } else {
2158                 matches.put(includedDir, new BuildJob(includedDir));
2159             }
2160         }
2161 
2162         return new ArrayList<>(matches.values());
2163     }
2164 
2165     /**
2166      * Relativizes the project paths of the specified build jobs against the directory specified by
2167      * {@link #projectsDirectory} (if possible). If a project path does not denote a sub path of the projects directory,
2168      * it is returned as is.
2169      *
2170      * @param buildJobs The build jobs whose project paths should be relativized, must not be <code>null</code> nor
2171      *            contain <code>null</code> elements.
2172      * @throws java.io.IOException If any path could not be relativized.
2173      */
2174     private void relativizeProjectPaths(List<BuildJob> buildJobs) throws IOException {
2175         String projectsDirPath = projectsDirectory.getCanonicalPath();
2176 
2177         for (BuildJob buildJob : buildJobs) {
2178             String projectPath = buildJob.getProject();
2179 
2180             File file = new File(projectPath);
2181 
2182             if (!file.isAbsolute()) {
2183                 file = new File(projectsDirectory, projectPath);
2184             }
2185 
2186             String relativizedPath = relativizePath(file, projectsDirPath);
2187 
2188             if (relativizedPath == null) {
2189                 relativizedPath = projectPath;
2190             }
2191 
2192             buildJob.setProject(relativizedPath);
2193         }
2194     }
2195 
2196     /**
2197      * Relativizes the specified path against the given base directory. Besides relativization, the returned path will
2198      * also be normalized, e.g. directory references like ".." will be removed.
2199      *
2200      * @param path The path to relativize, must not be <code>null</code>.
2201      * @param basedir The (canonical path of the) base directory to relativize against, must not be <code>null</code>.
2202      * @return The relative path in normal form or <code>null</code> if the input path does not denote a sub path of the
2203      *         base directory.
2204      * @throws java.io.IOException If the path could not be relativized.
2205      */
2206     private String relativizePath(File path, String basedir) throws IOException {
2207         String relativizedPath = path.getCanonicalPath();
2208 
2209         if (relativizedPath.startsWith(basedir)) {
2210             relativizedPath = relativizedPath.substring(basedir.length());
2211             if (relativizedPath.startsWith(File.separator)) {
2212                 relativizedPath = relativizedPath.substring(File.separator.length());
2213             }
2214 
2215             return relativizedPath;
2216         } else {
2217             return null;
2218         }
2219     }
2220 
2221     /**
2222      * Returns the map-based value source used to interpolate POMs and other stuff.
2223      *
2224      * @param escapeXml {@code true}, to escape any XML special characters in the property values; {@code false}, to not
2225      * escape any property values.
2226      *
2227      * @return The map-based value source for interpolation, never <code>null</code>.
2228      */
2229     private Map<String, Object> getInterpolationValueSource(final boolean escapeXml) {
2230         Map<String, Object> props = new HashMap<>();
2231 
2232         if (filterProperties != null) {
2233             props.putAll(filterProperties);
2234         }
2235         props.put("basedir", this.project.getBasedir().getAbsolutePath());
2236         props.put("baseurl", toUrl(this.project.getBasedir().getAbsolutePath()));
2237         if (settings.getLocalRepository() != null) {
2238             props.put("localRepository", settings.getLocalRepository());
2239             props.put("localRepositoryUrl", toUrl(settings.getLocalRepository()));
2240         }
2241 
2242         return new CompositeMap(this.project, props, escapeXml);
2243     }
2244 
2245     /**
2246      * Converts the specified filesystem path to a URL. The resulting URL has no trailing slash regardless whether the
2247      * path denotes a file or a directory.
2248      *
2249      * @param filename The filesystem path to convert, must not be <code>null</code>.
2250      * @return The <code>file:</code> URL for the specified path, never <code>null</code>.
2251      */
2252     private static String toUrl(String filename) {
2253         /*
2254          * NOTE: Maven fails to properly handle percent-encoded "file:" URLs (WAGON-111) so don't use File.toURI() here
2255          * as-is but use the decoded path component in the URL.
2256          */
2257         String url = "file://" + new File(filename).toURI().getPath();
2258         if (url.endsWith("/")) {
2259             url = url.substring(0, url.length() - 1);
2260         }
2261         return url;
2262     }
2263 
2264     /**
2265      * Interpolates the specified POM/settings file to a temporary file. The destination file may be same as the input
2266      * file, i.e. interpolation can be performed in-place.
2267      * <p>
2268      * <b>Note:</b>This methods expects the file to be a XML file and applies special XML escaping during interpolation.
2269      * </p>
2270      *
2271      * @param originalFile The XML file to interpolate, must not be <code>null</code>.
2272      * @param interpolatedFile The target file to write the interpolated contents of the original file to, must not be
2273      * <code>null</code>.
2274      *
2275      * @throws org.apache.maven.plugin.MojoExecutionException If the target file could not be created.
2276      */
2277     void buildInterpolatedFile(File originalFile, File interpolatedFile) throws MojoExecutionException {
2278         getLog().debug("Interpolate " + originalFile.getPath() + " to " + interpolatedFile.getPath());
2279 
2280         try {
2281             String xml;
2282 
2283             Map<String, Object> composite = getInterpolationValueSource(true);
2284 
2285             // interpolation with token @...@
2286             try (Reader reader =
2287                     new InterpolationFilterReader(ReaderFactory.newXmlReader(originalFile), composite, "@", "@")) {
2288                 xml = IOUtil.toString(reader);
2289             }
2290 
2291             try (Writer writer = WriterFactory.newXmlWriter(interpolatedFile)) {
2292                 interpolatedFile.getParentFile().mkdirs();
2293 
2294                 writer.write(xml);
2295             }
2296         } catch (IOException e) {
2297             throw new MojoExecutionException("Failed to interpolate file " + originalFile.getPath(), e);
2298         }
2299     }
2300 
2301     /**
2302      * Gets the (interpolated) invoker properties for an integration test.
2303      *
2304      * @param projectDirectory The base directory of the IT project, must not be <code>null</code>.
2305      * @return The invoker properties, may be empty but never <code>null</code>.
2306      * @throws org.apache.maven.plugin.MojoExecutionException If an I/O error occurred during reading the properties.
2307      */
2308     private InvokerProperties getInvokerProperties(final File projectDirectory, Properties globalInvokerProperties)
2309             throws MojoExecutionException {
2310         Properties props;
2311         if (globalInvokerProperties != null) {
2312             props = new Properties(globalInvokerProperties);
2313         } else {
2314             props = new Properties();
2315         }
2316 
2317         File propertiesFile = new File(projectDirectory, invokerPropertiesFile);
2318         if (propertiesFile.isFile()) {
2319             try (InputStream in = new FileInputStream(propertiesFile)) {
2320                 props.load(in);
2321             } catch (IOException e) {
2322                 throw new MojoExecutionException("Failed to read invoker properties: " + propertiesFile, e);
2323             }
2324         }
2325 
2326         Interpolator interpolator = new RegexBasedInterpolator();
2327         interpolator.addValueSource(new MapBasedValueSource(getInterpolationValueSource(false)));
2328         // CHECKSTYLE_OFF: LineLength
2329         for (String key : props.stringPropertyNames()) {
2330             String value = props.getProperty(key);
2331             try {
2332                 value = interpolator.interpolate(value, "");
2333             } catch (InterpolationException e) {
2334                 throw new MojoExecutionException("Failed to interpolate invoker properties: " + propertiesFile, e);
2335             }
2336             props.setProperty(key, value);
2337         }
2338 
2339         InvokerProperties invokerProperties = new InvokerProperties(props);
2340 
2341         // set default value for Invoker - it will be used if not present in properties
2342         invokerProperties.setDefaultDebug(debug);
2343         invokerProperties.setDefaultQuiet(quiet);
2344         invokerProperties.setDefaultGoals(goals);
2345         invokerProperties.setDefaultProfiles(profiles);
2346         invokerProperties.setDefaultMavenExecutable(mavenExecutable);
2347         invokerProperties.setDefaultMavenOpts(mavenOpts);
2348         invokerProperties.setDefaultTimeoutInSeconds(timeoutInSeconds);
2349         invokerProperties.setDefaultEnvironmentVariables(environmentVariables);
2350         invokerProperties.setDefaultUpdateSnapshots(updateSnapshots);
2351 
2352         return invokerProperties;
2353     }
2354 
2355     static class ToolchainPrivateManager {
2356         private ToolchainManagerPrivate manager;
2357 
2358         private MavenSession session;
2359 
2360         ToolchainPrivateManager(ToolchainManagerPrivate manager, MavenSession session) {
2361             this.manager = manager;
2362             this.session = session;
2363         }
2364 
2365         ToolchainPrivate[] getToolchainPrivates(String type) throws MisconfiguredToolchainException {
2366             return manager.getToolchainsForType(type, session);
2367         }
2368     }
2369 }