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