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