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