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