blob: 519ce90f0464b45bb97a5d6a7cb7395533077968 [file] [log] [blame]
package org.apache.maven.plugin.invoker;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Model;
import org.apache.maven.model.Profile;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.invoker.model.BuildJob;
import org.apache.maven.plugin.invoker.model.io.xpp3.BuildJobXpp3Writer;
import org.apache.maven.plugin.registry.TrackableBase;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Settings;
import org.apache.maven.settings.SettingsUtils;
import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
import org.apache.maven.settings.building.SettingsBuilder;
import org.apache.maven.settings.building.SettingsBuildingException;
import org.apache.maven.settings.building.SettingsBuildingRequest;
import org.apache.maven.settings.io.xpp3.SettingsXpp3Writer;
import org.apache.maven.shared.invoker.CommandLineConfigurationException;
import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.InvocationRequest;
import org.apache.maven.shared.invoker.InvocationResult;
import org.apache.maven.shared.invoker.Invoker;
import org.apache.maven.shared.invoker.MavenCommandLineBuilder;
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.apache.maven.shared.scriptinterpreter.RunErrorException;
import org.apache.maven.shared.scriptinterpreter.RunFailureException;
import org.apache.maven.shared.scriptinterpreter.ScriptRunner;
import org.apache.maven.shared.utils.logging.MessageUtils;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.Interpolator;
import org.codehaus.plexus.interpolation.MapBasedValueSource;
import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.InterpolationFilterReader;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.ReflectionUtils;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.WriterFactory;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.util.cli.StreamConsumer;
/**
* Provides common code for mojos invoking sub builds.
*
* @author Stephen Connolly
* @since 15-Aug-2009 09:09:29
*/
public abstract class AbstractInvokerMojo
extends AbstractMojo
{
private static final int SELECTOR_MAVENVERSION = 1;
private static final int SELECTOR_JREVERSION = 2;
private static final int SELECTOR_OSFAMILY = 4;
/**
* Flag used to suppress certain invocations. This is useful in tailoring the build using profiles.
*
* @since 1.1
*/
@Parameter( property = "invoker.skip", defaultValue = "false" )
private boolean skipInvocation;
/**
* Flag used to suppress the summary output notifying of successes and failures. If set to <code>true</code>, the
* only indication of the build's success or failure will be the effect it has on the main build (if it fails, the
* main build should fail as well). If {@link #streamLogs} is enabled, the sub-build summary will also provide an
* indication.
*/
@Parameter( defaultValue = "false" )
protected boolean suppressSummaries;
/**
* Flag used to determine whether the build logs should be output to the normal mojo log.
*/
@Parameter( property = "invoker.streamLogs", defaultValue = "false" )
private boolean streamLogs;
/**
* The local repository for caching artifacts. It is strongly recommended to specify a path to an isolated
* repository like <code>${project.build.directory}/it-repo</code>. Otherwise, your ordinary local repository will
* be used, potentially soiling it with broken artifacts.
*/
@Parameter( property = "invoker.localRepositoryPath", defaultValue = "${settings.localRepository}" )
private File localRepositoryPath;
/**
* Directory to search for integration tests.
*/
@Parameter( property = "invoker.projectsDirectory", defaultValue = "${basedir}/src/it/" )
private File projectsDirectory;
/**
* Base directory where all build reports are written to. Every execution of an integration test will produce an XML
* file which contains the information about success or failure of that particular build job. The format of the
* resulting XML file is documented in the given <a href="./build-job.html">build-job</a> reference.
*
* @since 1.4
*/
@Parameter( property = "invoker.reportsDirectory", defaultValue = "${project.build.directory}/invoker-reports" )
private File reportsDirectory;
/**
* A flag to disable the generation of build reports.
*
* @since 1.4
*/
@Parameter( property = "invoker.disableReports", defaultValue = "false" )
private boolean disableReports;
/**
* Directory to which projects should be cloned prior to execution. If not specified, each integration test will be
* run in the directory in which the corresponding IT POM was found. In this case, you most likely want to configure
* your SCM to ignore <code>target</code> and <code>build.log</code> in the test's base directory.
*
* @since 1.1
*/
@Parameter
private File cloneProjectsTo;
// CHECKSTYLE_OFF: LineLength
/**
* Some files are normally excluded when copying the IT projects from the directory specified by the parameter
* projectsDirectory to the directory given by cloneProjectsTo (e.g. <code>.svn</code>, <code>CVS</code>,
* <code>*~</code>, etc: see <a href=
* "https://codehaus-plexus.github.io/plexus-utils/apidocs/org/codehaus/plexus/util/AbstractScanner.html#DEFAULTEXCLUDES">
* reference</a> for full list). Setting this parameter to <code>true</code> will cause all files to be copied to
* the <code>cloneProjectsTo</code> directory.
*
* @since 1.2
*/
@Parameter( defaultValue = "false" )
private boolean cloneAllFiles;
// CHECKSTYLE_ON: LineLength
/**
* Ensure the {@link #cloneProjectsTo} directory is not polluted with files from earlier invoker runs.
*
* @since 1.6
*/
@Parameter( defaultValue = "false" )
private boolean cloneClean;
/**
* A single POM to build, skipping any scanning parameters and behavior.
*/
@Parameter( property = "invoker.pom" )
private File pom;
/**
* Include patterns for searching the integration test directory for projects. This parameter is meant to be set
* from the POM. If this parameter is not set, the plugin will search for all <code>pom.xml</code> files one
* directory below {@link #projectsDirectory} (i.e. <code>*&#47;pom.xml</code>).<br>
* <br>
* Starting with version 1.3, mere directories can also be matched by these patterns. For example, the include
* pattern <code>*</code> will run Maven builds on all immediate sub directories of {@link #projectsDirectory},
* regardless if they contain a <code>pom.xml</code>. This allows to perform builds that need/should not depend on
* the existence of a POM.
*/
@Parameter
private List<String> pomIncludes = Collections.singletonList( "*/pom.xml" );
/**
* Exclude patterns for searching the integration test directory. This parameter is meant to be set from the POM. By
* default, no POM files are excluded. For the convenience of using an include pattern like <code>*</code>, the
* custom settings file specified by the parameter {@link #settingsFile} will always be excluded automatically.
*/
@Parameter
private List<String> pomExcludes = Collections.emptyList();
/**
* Include patterns for searching the projects directory for projects that need to be run before the other projects.
* This parameter allows to declare projects that perform setup tasks like installing utility artifacts into the
* local repository. Projects matched by these patterns are implicitly excluded from the scan for ordinary projects.
* Also, the exclusions defined by the parameter {@link #pomExcludes} apply to the setup projects, too. Default
* value is: <code>setup*&#47;pom.xml</code>.
*
* @since 1.3
*/
@Parameter
private List<String> setupIncludes = Collections.singletonList( "setup*/pom.xml" );
/**
* The list of goals to execute on each project. Default value is: <code>package</code>.
*/
@Parameter
private List<String> goals = Collections.singletonList( "package" );
/**
*/
@Component
private Invoker invoker;
@Component
private SettingsBuilder settingsBuilder;
/**
* Relative path of a selector script to run prior in order to decide if the build should be executed. This script
* may be written with either BeanShell or Groovy. If the file extension is omitted (e.g. <code>selector</code>),
* the plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and
* <code>.groovy</code>. If this script exists for a particular project but returns any non-null value different
* from <code>true</code>, the corresponding build is flagged as skipped. In this case, none of the pre-build hook
* script, Maven nor the post-build hook script will be invoked. If this script throws an exception, the
* corresponding build is flagged as in error, and none of the pre-build hook script, Maven not the post-build hook
* script will be invoked.
*
* @since 1.5
*/
@Parameter( property = "invoker.selectorScript", defaultValue = "selector" )
private String selectorScript;
/**
* Relative path of a pre-build hook script to run prior to executing the build. This script may be written with
* either BeanShell or Groovy (since 1.3). If the file extension is omitted (e.g. <code>prebuild</code>), the plugin
* searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>. If this
* script exists for a particular project but returns any non-null value different from <code>true</code> or throws
* an exception, the corresponding build is flagged as a failure. In this case, neither Maven nor the post-build
* hook script will be invoked.
*/
@Parameter( property = "invoker.preBuildHookScript", defaultValue = "prebuild" )
private String preBuildHookScript;
/**
* Relative path of a cleanup/verification hook script to run after executing the build. This script may be written
* with either BeanShell or Groovy (since 1.3). If the file extension is omitted (e.g. <code>verify</code>), the
* plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>.
* If this script exists for a particular project but returns any non-null value different from <code>true</code> or
* throws an exception, the corresponding build is flagged as a failure.
*/
@Parameter( property = "invoker.postBuildHookScript", defaultValue = "postbuild" )
private String postBuildHookScript;
/**
* Location of a properties file that defines CLI properties for the test.
*/
@Parameter( property = "invoker.testPropertiesFile", defaultValue = "test.properties" )
private String testPropertiesFile;
/**
* Common set of properties to pass in on each project's command line, via -D parameters.
*
* @since 1.1
*/
@Parameter
private Map<String, String> properties;
/**
* Whether to show errors in the build output.
*/
@Parameter( property = "invoker.showErrors", defaultValue = "false" )
private boolean showErrors;
/**
* Whether to show debug statements in the build output.
*/
@Parameter( property = "invoker.debug", defaultValue = "false" )
private boolean debug;
/**
* Suppress logging to the <code>build.log</code> file.
*/
@Parameter( property = "invoker.noLog", defaultValue = "false" )
private boolean noLog;
/**
* List of profile identifiers to explicitly trigger in the build.
*
* @since 1.1
*/
@Parameter
private List<String> profiles;
/**
* A list of additional properties which will be used to filter tokens in POMs and goal files.
*
* @since 1.3
*/
@Parameter
private Map<String, String> filterProperties;
/**
* The Maven Project Object
*
* @since 1.1
*/
@Parameter( defaultValue = "${project}", readonly = true, required = true )
private MavenProject project;
@Parameter( defaultValue = "${mojoExecution}", readonly = true, required = true )
private MojoExecution mojoExecution;
/**
* A comma separated list of projectname patterns to run. Specify this parameter to run individual tests by file
* name, overriding the {@link #setupIncludes}, {@link #pomIncludes} and {@link #pomExcludes} parameters. Each
* pattern you specify here will be used to create an include/exclude pattern formatted like
* <code>${projectsDirectory}/<i>pattern</i></code>. To exclude a test, prefix the pattern with a '<code>!</code>'.
* So you can just type <nobr><code>-Dinvoker.test=SimpleTest,Comp*Test,!Compare*</code></nobr> to run builds in
* <code>${projectsDirectory}/SimpleTest</code> and <code>${projectsDirectory}/ComplexTest</code>, but not
* <code>${projectsDirectory}/CompareTest</code>
*
* @since 1.1 (exclusion since 1.8)
*/
@Parameter( property = "invoker.test" )
private String invokerTest;
/**
* Path to an alternate <code>settings.xml</code> to use for Maven invocation with all ITs. Note that the
* <code>&lt;localRepository&gt;</code> element of this settings file is always ignored, i.e. the path given by the
* parameter {@link #localRepositoryPath} is dominant.
*
* @since 1.2
*/
@Parameter( property = "invoker.settingsFile" )
private File settingsFile;
/**
* The <code>MAVEN_OPTS</code> environment variable to use when invoking Maven. This value can be overridden for
* individual integration tests by using {@link #invokerPropertiesFile}.
*
* @since 1.2
*/
@Parameter( property = "invoker.mavenOpts" )
private String mavenOpts;
/**
* The home directory of the Maven installation to use for the forked builds. Defaults to the current Maven
* installation.
*
* @since 1.3
*/
@Parameter( property = "invoker.mavenHome" )
private File mavenHome;
/**
* mavenExecutable can either be a file relative to <code>${maven.home}/bin/</code> or an absolute file.
*
* @since 1.8
* @see Invoker#setMavenExecutable(File)
*/
@Parameter( property = "invoker.mavenExecutable" )
private String mavenExecutable;
/**
* The <code>JAVA_HOME</code> environment variable to use for forked Maven invocations. Defaults to the current Java
* home directory.
*
* @since 1.3
*/
@Parameter( property = "invoker.javaHome" )
private File javaHome;
/**
* The file encoding for the pre-/post-build scripts and the list files for goals and profiles.
*
* @since 1.2
*/
@Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
private String encoding;
/**
* The current user system settings for use in Maven.
*
* @since 1.2
*/
@Parameter( defaultValue = "${settings}", readonly = true, required = true )
private Settings settings;
/**
* A flag whether the test class path of the project under test should be included in the class path of the
* pre-/post-build scripts. If set to <code>false</code>, the class path of script interpreter consists only of the
* <a href="dependencies.html">runtime dependencies</a> of the Maven Invoker Plugin. If set the <code>true</code>,
* the project's test class path will be prepended to the interpreter class path. Among others, this feature allows
* the scripts to access utility classes from the test sources of your project.
*
* @since 1.2
*/
@Parameter( property = "invoker.addTestClassPath", defaultValue = "false" )
private boolean addTestClassPath;
/**
* The test class path of the project under test.
*/
@Parameter( defaultValue = "${project.testClasspathElements}", readonly = true )
private List<String> testClassPath;
/**
* The name of an optional project-specific file that contains properties used to specify settings for an individual
* Maven invocation. Any property present in the file will override the corresponding setting from the plugin
* configuration. The values of the properties are filtered and may use expressions like
* <code>${project.version}</code> to reference project properties or values from the parameter
* {@link #filterProperties}. The snippet below describes the supported properties:
* <p/>
*
* <pre>
* # A comma or space separated list of goals/phases to execute, may
* # specify an empty list to execute the default goal of the IT project.
* # Environment variables used by maven plugins can be added here
* invoker.goals = clean install -Dplugin.variable=value
*
* # Or you can give things like this if you need.
* invoker.goals = -T2 clean verify
*
* # Optionally, a list of goals to run during further invocations of Maven
* invoker.goals.2 = ${project.groupId}:${project.artifactId}:${project.version}:run
*
* # A comma or space separated list of profiles to activate
* invoker.profiles = its,jdk15
*
* # The path to an alternative POM or base directory to invoke Maven on, defaults to the
* # project that was originally specified in the plugin configuration
* # Since plugin version 1.4
* invoker.project = sub-module
*
* # The value for the environment variable MAVEN_OPTS
* invoker.mavenOpts = -Dfile.encoding=UTF-16 -Xms32m -Xmx256m
*
* # Possible values are &quot;fail-fast&quot; (default), &quot;fail-at-end&quot; and &quot;fail-never&quot;
* invoker.failureBehavior = fail-never
*
* # The expected result of the build, possible values are &quot;success&quot; (default) and &quot;failure&quot;
* invoker.buildResult = failure
*
* # A boolean value controlling the aggregator mode of Maven, defaults to &quot;false&quot;
* invoker.nonRecursive = true
*
* # A boolean value controlling the network behavior of Maven, defaults to &quot;false&quot;
* # Since plugin version 1.4
* invoker.offline = true
*
* # The path to the properties file from which to load system properties, defaults to the
* # filename given by the plugin parameter testPropertiesFile
* # Since plugin version 1.4
* invoker.systemPropertiesFile = test.properties
*
* # An optional human friendly name for this build job to be included in the build reports.
* # Since plugin version 1.4
* invoker.name = Test Build 01
*
* # An optional description for this build job to be included in the build reports.
* # Since plugin version 1.4
* invoker.description = Checks the support for build reports.
*
* # A comma separated list of JRE versions on which this build job should be run.
* # Since plugin version 1.4
* invoker.java.version = 1.4+, !1.4.1, 1.7-
*
* # A comma separated list of OS families on which this build job should be run.
* # Since plugin version 1.4
* invoker.os.family = !windows, unix, mac
*
* # A comma separated list of Maven versions on which this build should be run.
* # Since plugin version 1.5
* invoker.maven.version = 2.0.10+, !2.1.0, !2.2.0
*
* # A boolean value controlling the debug logging level of Maven, , defaults to &quot;false&quot;
* # Since plugin version 1.8
* invoker.debug = true
* </pre>
*
* @since 1.2
*/
@Parameter( property = "invoker.invokerPropertiesFile", defaultValue = "invoker.properties" )
private String invokerPropertiesFile;
/**
* flag to enable show mvn version used for running its (cli option : -V,--show-version )
*
* @since 1.4
*/
@Parameter( property = "invoker.showVersion", defaultValue = "false" )
private boolean showVersion;
/**
* number of threads for running tests in parallel. This will be the number of maven forked process in parallel.
*
* @since 1.6
*/
@Parameter( property = "invoker.parallelThreads", defaultValue = "1" )
private int parallelThreads;
/**
* @since 1.6
*/
@Parameter( property = "plugin.artifacts", required = true, readonly = true )
private List<Artifact> pluginArtifacts;
/**
* If enable and if you have a settings file configured for the execution, it will be merged with your user
* settings.
*
* @since 1.6
*/
@Parameter( property = "invoker.mergeUserSettings", defaultValue = "false" )
private boolean mergeUserSettings;
/**
* Additional environment variables to set on the command line.
*
* @since 1.8
*/
@Parameter
private Map<String, String> environmentVariables;
/**
* Additional variables for use in the hook scripts.
*
* @since 1.9
*/
@Parameter
private Map<String, String> scriptVariables;
/**
* The scripter runner that is responsible to execute hook scripts.
*/
private ScriptRunner scriptRunner;
/**
* A string used to prefix the file name of the filtered POMs in case the POMs couldn't be filtered in-place (i.e.
* the projects were not cloned to a temporary directory), can be <code>null</code>. This will be set to
* <code>null</code> if the POMs have already been filtered during cloning.
*/
private String filteredPomPrefix = "interpolated-";
/**
* The format for elapsed build time.
*/
private final DecimalFormat secFormat = new DecimalFormat( "(0.0 s)", new DecimalFormatSymbols( Locale.ENGLISH ) );
/**
* The version of Maven which is used to run the builds
*/
private String actualMavenVersion;
/**
* Invokes Maven on the configured test projects.
*
* @throws org.apache.maven.plugin.MojoExecutionException If the goal encountered severe errors.
* @throws org.apache.maven.plugin.MojoFailureException If any of the Maven builds failed.
*/
public void execute()
throws MojoExecutionException, MojoFailureException
{
if ( skipInvocation )
{
getLog().info( "Skipping invocation per configuration."
+ " If this is incorrect, ensure the skipInvocation parameter is not set to true." );
return;
}
if ( StringUtils.isEmpty( encoding ) )
{
getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
+ ", i.e. build is platform dependent!" );
}
// done it here to prevent issues with concurrent access in case of parallel run
if ( !disableReports )
{
// If it exists from previous run...
if ( reportsDirectory.exists() )
{
try
{
FileUtils.deleteDirectory( reportsDirectory );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Failure while trying to delete "
+ reportsDirectory.getAbsolutePath(), e );
}
}
if ( !reportsDirectory.mkdirs() )
{
throw new MojoExecutionException( "Failure while creating the " + reportsDirectory.getAbsolutePath() );
}
}
BuildJob[] buildJobs;
if ( pom == null )
{
try
{
buildJobs = getBuildJobs();
}
catch ( final IOException e )
{
throw new MojoExecutionException( "Error retrieving POM list from includes, "
+ "excludes, and projects directory. Reason: " + e.getMessage(), e );
}
}
else
{
try
{
projectsDirectory = pom.getCanonicalFile().getParentFile();
}
catch ( IOException e )
{
throw new MojoExecutionException( "Failed to discover projectsDirectory from "
+ "pom File parameter. Reason: " + e.getMessage(), e );
}
buildJobs = new BuildJob[] { new BuildJob( pom.getName(), BuildJob.Type.NORMAL ) };
}
if ( ( buildJobs == null ) || ( buildJobs.length < 1 ) )
{
doFailIfNoProjects();
getLog().info( "No projects were selected for execution." );
return;
}
handleScriptRunnerWithScriptClassPath();
Collection<String> collectedProjects = new LinkedHashSet<String>();
for ( BuildJob buildJob : buildJobs )
{
collectProjects( projectsDirectory, buildJob.getProject(), collectedProjects, true );
}
File projectsDir = projectsDirectory;
if ( cloneProjectsTo != null )
{
cloneProjects( collectedProjects );
projectsDir = cloneProjectsTo;
}
else
{
getLog().warn( "Filtering of parent/child POMs is not supported without cloning the projects" );
}
// First run setup jobs.
BuildJob[] setupBuildJobs = null;
try
{
setupBuildJobs = getSetupBuildJobsFromFolders();
}
catch ( IOException e )
{
getLog().error( "Failure during scanning of folders.", e );
}
if ( setupBuildJobs != null )
{
// Run setup jobs in single thread
// mode.
//
// Some Idea about ordering?
getLog().info( "Running Setup Jobs" );
runBuilds( projectsDir, setupBuildJobs, 1 );
}
// Afterwards run all other jobs.
BuildJob[] nonSetupBuildJobs = getNonSetupJobs( buildJobs );
// We will run the non setup jobs with the configured
// parallelThreads number.
runBuilds( projectsDir, nonSetupBuildJobs, parallelThreads );
writeSummaryFile( nonSetupBuildJobs );
processResults( new InvokerSession( nonSetupBuildJobs ) );
}
private BuildJob[] getNonSetupJobs( BuildJob[] buildJobs )
{
List<BuildJob> result = new LinkedList<BuildJob>();
for ( int i = 0; i < buildJobs.length; i++ )
{
if ( !buildJobs[i].getType().equals( BuildJob.Type.SETUP ) )
{
result.add( buildJobs[i] );
}
}
BuildJob[] buildNonSetupJobs = result.toArray( new BuildJob[result.size()] );
return buildNonSetupJobs;
}
private void handleScriptRunnerWithScriptClassPath()
{
final List<String> scriptClassPath;
if ( addTestClassPath )
{
scriptClassPath = new ArrayList<String>( testClassPath );
for ( Artifact pluginArtifact : pluginArtifacts )
{
scriptClassPath.remove( pluginArtifact.getFile().getAbsolutePath() );
}
}
else
{
scriptClassPath = null;
}
scriptRunner = new ScriptRunner( getLog() );
scriptRunner.setScriptEncoding( encoding );
scriptRunner.setGlobalVariable( "localRepositoryPath", localRepositoryPath );
if ( scriptVariables != null )
{
for ( Entry<String, String> entry : scriptVariables.entrySet() )
{
scriptRunner.setGlobalVariable( entry.getKey(), entry.getValue() );
}
}
scriptRunner.setClassPath( scriptClassPath );
}
private void writeSummaryFile( BuildJob[] buildJobs )
throws MojoExecutionException
{
File summaryReportFile = new File( reportsDirectory, "invoker-summary.txt" );
try
{
Writer writer = new BufferedWriter( new FileWriter( summaryReportFile ) );
for ( int i = 0; i < buildJobs.length; i++ )
{
BuildJob buildJob = buildJobs[i];
if ( !buildJob.getResult().equals( BuildJob.Result.SUCCESS ) )
{
writer.append( buildJob.getResult() );
writer.append( " [" );
writer.append( buildJob.getProject() );
writer.append( "] " );
if ( buildJob.getFailureMessage() != null )
{
writer.append( " " );
writer.append( buildJob.getFailureMessage() );
}
writer.append( "\n" );
}
}
writer.close();
}
catch ( IOException e )
{
throw new MojoExecutionException( "Failed to write summary report " + summaryReportFile, e );
}
}
protected void doFailIfNoProjects()
throws MojoFailureException
{
// should only be used during run and verify
}
/**
* Processes the results of invoking the build jobs.
*
* @param invokerSession The session with the build jobs, must not be <code>null</code>.
* @throws MojoFailureException If the mojo had failed as a result of invoking the build jobs.
* @since 1.4
*/
abstract void processResults( InvokerSession invokerSession )
throws MojoFailureException;
/**
* Creates a new reader for the specified file, using the plugin's {@link #encoding} parameter.
*
* @param file The file to create a reader for, must not be <code>null</code>.
* @return The reader for the file, never <code>null</code>.
* @throws java.io.IOException If the specified file was not found or the configured encoding is not supported.
*/
private Reader newReader( File file )
throws IOException
{
if ( StringUtils.isNotEmpty( encoding ) )
{
return ReaderFactory.newReader( file, encoding );
}
else
{
return ReaderFactory.newPlatformReader( file );
}
}
/**
* Collects all projects locally reachable from the specified project. The method will as such try to read the POM
* and recursively follow its parent/module elements.
*
* @param projectsDir The base directory of all projects, must not be <code>null</code>.
* @param projectPath The relative path of the current project, can denote either the POM or its base directory,
* must not be <code>null</code>.
* @param projectPaths The set of already collected projects to add new projects to, must not be <code>null</code>.
* This set will hold the relative paths to either a POM file or a project base directory.
* @param included A flag indicating whether the specified project has been explicitly included via the parameter
* {@link #pomIncludes}. Such projects will always be added to the result set even if there is no
* corresponding POM.
* @throws org.apache.maven.plugin.MojoExecutionException If the project tree could not be traversed.
*/
private void collectProjects( File projectsDir, String projectPath, Collection<String> projectPaths,
boolean included )
throws MojoExecutionException
{
projectPath = projectPath.replace( '\\', '/' );
File pomFile = new File( projectsDir, projectPath );
if ( pomFile.isDirectory() )
{
pomFile = new File( pomFile, "pom.xml" );
if ( !pomFile.exists() )
{
if ( included )
{
projectPaths.add( projectPath );
}
return;
}
if ( !projectPath.endsWith( "/" ) )
{
projectPath += '/';
}
projectPath += "pom.xml";
}
else if ( !pomFile.isFile() )
{
return;
}
if ( !projectPaths.add( projectPath ) )
{
return;
}
getLog().debug( "Collecting parent/child projects of " + projectPath );
Model model = PomUtils.loadPom( pomFile );
try
{
String projectsRoot = projectsDir.getCanonicalPath();
String projectDir = pomFile.getParent();
String parentPath = "../pom.xml";
if ( model.getParent() != null && StringUtils.isNotEmpty( model.getParent().getRelativePath() ) )
{
parentPath = model.getParent().getRelativePath();
}
String parent = relativizePath( new File( projectDir, parentPath ), projectsRoot );
if ( parent != null )
{
collectProjects( projectsDir, parent, projectPaths, false );
}
Collection<String> modulePaths = new LinkedHashSet<String>();
modulePaths.addAll( model.getModules() );
for ( Profile profile : model.getProfiles() )
{
modulePaths.addAll( profile.getModules() );
}
for ( String modulePath : modulePaths )
{
String module = relativizePath( new File( projectDir, modulePath ), projectsRoot );
if ( module != null )
{
collectProjects( projectsDir, module, projectPaths, false );
}
}
}
catch ( IOException e )
{
throw new MojoExecutionException( "Failed to analyze POM: " + pomFile, e );
}
}
/**
* Copies the specified projects to the directory given by {@link #cloneProjectsTo}. A project may either be denoted
* by a path to a POM file or merely by a path to a base directory. During cloning, the POM files will be filtered.
*
* @param projectPaths The paths to the projects to clone, relative to the projects directory, must not be
* <code>null</code> nor contain <code>null</code> elements.
* @throws org.apache.maven.plugin.MojoExecutionException If the the projects could not be copied/filtered.
*/
private void cloneProjects( Collection<String> projectPaths )
throws MojoExecutionException
{
if ( !cloneProjectsTo.mkdirs() && cloneClean )
{
try
{
FileUtils.cleanDirectory( cloneProjectsTo );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Could not clean the cloneProjectsTo directory. Reason: "
+ e.getMessage(), e );
}
}
// determine project directories to clone
Collection<String> dirs = new LinkedHashSet<String>();
for ( String projectPath : projectPaths )
{
if ( !new File( projectsDirectory, projectPath ).isDirectory() )
{
projectPath = getParentPath( projectPath );
}
dirs.add( projectPath );
}
boolean filter;
// clone project directories
try
{
filter = !cloneProjectsTo.getCanonicalFile().equals( projectsDirectory.getCanonicalFile() );
List<String> clonedSubpaths = new ArrayList<String>();
for ( String subpath : dirs )
{
// skip this project if its parent directory is also scheduled for cloning
if ( !".".equals( subpath ) && dirs.contains( getParentPath( subpath ) ) )
{
continue;
}
// avoid copying subdirs that are already cloned.
if ( !alreadyCloned( subpath, clonedSubpaths ) )
{
// avoid creating new files that point to dir/.
if ( ".".equals( subpath ) )
{
String cloneSubdir = relativizePath( cloneProjectsTo, projectsDirectory.getCanonicalPath() );
// avoid infinite recursion if the cloneTo path is a subdirectory.
if ( cloneSubdir != null )
{
File temp = File.createTempFile( "pre-invocation-clone.", "" );
temp.delete();
temp.mkdirs();
copyDirectoryStructure( projectsDirectory, temp );
FileUtils.deleteDirectory( new File( temp, cloneSubdir ) );
copyDirectoryStructure( temp, cloneProjectsTo );
}
else
{
copyDirectoryStructure( projectsDirectory, cloneProjectsTo );
}
}
else
{
File srcDir = new File( projectsDirectory, subpath );
File dstDir = new File( cloneProjectsTo, subpath );
copyDirectoryStructure( srcDir, dstDir );
}
clonedSubpaths.add( subpath );
}
}
}
catch ( IOException e )
{
throw new MojoExecutionException( "Failed to clone projects from: " + projectsDirectory + " to: "
+ cloneProjectsTo + ". Reason: " + e.getMessage(), e );
}
// filter cloned POMs
if ( filter )
{
for ( String projectPath : projectPaths )
{
File pomFile = new File( cloneProjectsTo, projectPath );
if ( pomFile.isFile() )
{
buildInterpolatedFile( pomFile, pomFile );
}
// MINVOKER-186
// The following is a temporary solution to support Maven 3.3.1 (.mvn/extensions.xml) filtering
// Will be replaced by MINVOKER-117 with general filtering mechanism
File baseDir = pomFile.getParentFile();
File mvnDir = new File( baseDir, ".mvn" );
if ( mvnDir.isDirectory() )
{
File extensionsFile = new File( mvnDir, "extensions.xml" );
if ( extensionsFile.isFile() )
{
buildInterpolatedFile( extensionsFile, extensionsFile );
}
}
// END MINVOKER-186
}
filteredPomPrefix = null;
}
}
/**
* Gets the parent path of the specified relative path.
*
* @param path The relative path whose parent should be retrieved, must not be <code>null</code>.
* @return The parent path or "." if the specified path has no parent, never <code>null</code>.
*/
private String getParentPath( String path )
{
int lastSep = Math.max( path.lastIndexOf( '/' ), path.lastIndexOf( '\\' ) );
return ( lastSep < 0 ) ? "." : path.substring( 0, lastSep );
}
/**
* Copied a directory structure with default exclusions (.svn, CVS, etc)
*
* @param sourceDir The source directory to copy, must not be <code>null</code>.
* @param destDir The target directory to copy to, must not be <code>null</code>.
* @throws java.io.IOException If the directory structure could not be copied.
*/
private void copyDirectoryStructure( File sourceDir, File destDir )
throws IOException
{
DirectoryScanner scanner = new DirectoryScanner();
scanner.setBasedir( sourceDir );
if ( !cloneAllFiles )
{
scanner.addDefaultExcludes();
}
scanner.scan();
/*
* NOTE: Make sure the destination directory is always there (even if empty) to support POM-less ITs.
*/
destDir.mkdirs();
// Create all the directories, including any symlinks present in source
FileUtils.mkDirs( sourceDir, scanner.getIncludedDirectories(), destDir );
for ( String includedFile : scanner.getIncludedFiles() )
{
File sourceFile = new File( sourceDir, includedFile );
File destFile = new File( destDir, includedFile );
FileUtils.copyFile( sourceFile, destFile );
// ensure clone project must be writable for additional changes
destFile.setWritable( true );
}
}
/**
* Determines whether the specified sub path has already been cloned, i.e. whether one of its ancestor directories
* was already cloned.
*
* @param subpath The sub path to check, must not be <code>null</code>.
* @param clonedSubpaths The list of already cloned paths, must not be <code>null</code> nor contain
* <code>null</code> elements.
* @return <code>true</code> if the specified path has already been cloned, <code>false</code> otherwise.
*/
static boolean alreadyCloned( String subpath, List<String> clonedSubpaths )
{
for ( String path : clonedSubpaths )
{
if ( ".".equals( path ) || subpath.equals( path ) || subpath.startsWith( path + File.separator ) )
{
return true;
}
}
return false;
}
/**
* Runs the specified build jobs.
*
* @param projectsDir The base directory of all projects, must not be <code>null</code>.
* @param buildJobs The build jobs to run must not be <code>null</code> nor contain <code>null</code> elements.
* @throws org.apache.maven.plugin.MojoExecutionException If any build could not be launched.
*/
private void runBuilds( final File projectsDir, BuildJob[] buildJobs, int runWithParallelThreads )
throws MojoExecutionException
{
if ( !localRepositoryPath.exists() )
{
localRepositoryPath.mkdirs();
}
// -----------------------------------------------
// interpolate settings file
// -----------------------------------------------
File interpolatedSettingsFile = interpolateSettings();
final File mergedSettingsFile = mergeSettings( interpolatedSettingsFile );
if ( mavenHome != null )
{
actualMavenVersion = SelectorUtils.getMavenVersion( mavenHome );
}
else
{
actualMavenVersion = SelectorUtils.getMavenVersion();
}
scriptRunner.setGlobalVariable( "mavenVersion", actualMavenVersion );
final CharSequence actualJreVersion;
// @todo if ( javaVersions ) ... to be picked up from toolchains
if ( javaHome != null )
{
actualJreVersion = resolveExternalJreVersion();
}
else
{
actualJreVersion = SelectorUtils.getJreVersion();
}
try
{
MessageUtils.systemInstall(); // prepare JAnsi if not run with Maven 3.4+
if ( runWithParallelThreads > 1 )
{
getLog().info( "use parallelThreads " + runWithParallelThreads );
ExecutorService executorService = Executors.newFixedThreadPool( runWithParallelThreads );
for ( final BuildJob job : buildJobs )
{
executorService.execute( new Runnable()
{
public void run()
{
try
{
runBuild( projectsDir, job, mergedSettingsFile, javaHome, actualJreVersion );
}
catch ( MojoExecutionException e )
{
throw new RuntimeException( e.getMessage(), e );
}
}
} );
}
try
{
executorService.shutdown();
// TODO add a configurable time out
executorService.awaitTermination( Long.MAX_VALUE, TimeUnit.MILLISECONDS );
}
catch ( InterruptedException e )
{
throw new MojoExecutionException( e.getMessage(), e );
}
}
else
{
for ( BuildJob job : buildJobs )
{
runBuild( projectsDir, job, mergedSettingsFile, javaHome, actualJreVersion );
}
}
}
finally
{
if ( interpolatedSettingsFile != null && cloneProjectsTo == null )
{
interpolatedSettingsFile.delete();
}
if ( mergedSettingsFile != null && mergedSettingsFile.exists() )
{
mergedSettingsFile.delete();
}
MessageUtils.systemUninstall();
}
}
/**
* Interpolate settings.xml file.
* @return The interpolated settings.xml file.
* @throws MojoExecutionException in case of a problem.
*/
private File interpolateSettings()
throws MojoExecutionException
{
File interpolatedSettingsFile = null;
if ( settingsFile != null )
{
if ( cloneProjectsTo != null )
{
interpolatedSettingsFile = new File( cloneProjectsTo, "interpolated-" + settingsFile.getName() );
}
else
{
interpolatedSettingsFile =
new File( settingsFile.getParentFile(), "interpolated-" + settingsFile.getName() );
}
buildInterpolatedFile( settingsFile, interpolatedSettingsFile );
}
return interpolatedSettingsFile;
}
/**
* Merge the settings file
*
* @param interpolatedSettingsFile The interpolated settings file.
* @return The merged settings file.
* @throws MojoExecutionException Fail the build in case the merged settings file can't be created.
*/
private File mergeSettings( File interpolatedSettingsFile )
throws MojoExecutionException
{
File mergedSettingsFile;
Settings mergedSettings = this.settings;
if ( mergeUserSettings )
{
if ( interpolatedSettingsFile != null )
{
// Have to merge the specified settings file (dominant) and the one of the invoking Maven process
try
{
SettingsBuildingRequest request = new DefaultSettingsBuildingRequest();
request.setGlobalSettingsFile( interpolatedSettingsFile );
Settings dominantSettings = settingsBuilder.build( request ).getEffectiveSettings();
Settings recessiveSettings = cloneSettings();
SettingsUtils.merge( dominantSettings, recessiveSettings, TrackableBase.USER_LEVEL );
mergedSettings = dominantSettings;
getLog().debug( "Merged specified settings file with settings of invoking process" );
}
catch ( SettingsBuildingException e )
{
throw new MojoExecutionException( "Could not read specified settings file", e );
}
}
}
if ( this.settingsFile != null && !mergeUserSettings )
{
mergedSettingsFile = interpolatedSettingsFile;
}
else
{
try
{
mergedSettingsFile = writeMergedSettingsFile( mergedSettings );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Could not create temporary file for invoker settings.xml", e );
}
}
return mergedSettingsFile;
}
private File writeMergedSettingsFile( Settings mergedSettings )
throws IOException
{
File mergedSettingsFile;
mergedSettingsFile = File.createTempFile( "invoker-settings", ".xml" );
SettingsXpp3Writer settingsWriter = new SettingsXpp3Writer();
FileWriter fileWriter = null;
try
{
fileWriter = new FileWriter( mergedSettingsFile );
settingsWriter.write( fileWriter, mergedSettings );
fileWriter.close();
fileWriter = null;
}
finally
{
IOUtil.close( fileWriter );
}
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Created temporary file for invoker settings.xml: "
+ mergedSettingsFile.getAbsolutePath() );
}
return mergedSettingsFile;
}
private Settings cloneSettings()
{
Settings recessiveSettings = SettingsUtils.copySettings( this.settings );
// MINVOKER-133: reset sourceLevelSet
resetSourceLevelSet( recessiveSettings );
for ( org.apache.maven.settings.Mirror mirror : recessiveSettings.getMirrors() )
{
resetSourceLevelSet( mirror );
}
for ( org.apache.maven.settings.Server server : recessiveSettings.getServers() )
{
resetSourceLevelSet( server );
}
for ( org.apache.maven.settings.Proxy proxy : recessiveSettings.getProxies() )
{
resetSourceLevelSet( proxy );
}
for ( org.apache.maven.settings.Profile profile : recessiveSettings.getProfiles() )
{
resetSourceLevelSet( profile );
}
return recessiveSettings;
}
private void resetSourceLevelSet( org.apache.maven.settings.TrackableBase trackable )
{
try
{
ReflectionUtils.setVariableValueInObject( trackable, "sourceLevelSet", Boolean.FALSE );
getLog().debug( "sourceLevelSet: "
+ ReflectionUtils.getValueIncludingSuperclasses( "sourceLevelSet", trackable ) );
}
catch ( IllegalAccessException e )
{
// noop
}
}
private CharSequence resolveExternalJreVersion()
{
Artifact pluginArtifact = mojoExecution.getMojoDescriptor().getPluginDescriptor().getPluginArtifact();
pluginArtifact.getFile();
Commandline commandLine = new Commandline();
commandLine.setExecutable( new File( javaHome, "bin/java" ).getAbsolutePath() );
commandLine.createArg().setValue( "-cp" );
commandLine.createArg().setFile( pluginArtifact.getFile() );
commandLine.createArg().setValue( SystemPropertyPrinter.class.getName() );
commandLine.createArg().setValue( "java.version" );
final StringBuilder actualJreVersion = new StringBuilder();
StreamConsumer consumer = new StreamConsumer()
{
public void consumeLine( String line )
{
actualJreVersion.append( line );
}
};
try
{
CommandLineUtils.executeCommandLine( commandLine, consumer, null );
}
catch ( CommandLineException e )
{
getLog().warn( e.getMessage() );
}
return actualJreVersion;
}
/**
* Runs the specified project.
*
* @param projectsDir The base directory of all projects, must not be <code>null</code>.
* @param buildJob The build job to run, must not be <code>null</code>.
* @param settingsFile The (already interpolated) user settings file for the build, may be <code>null</code> to use
* the current user settings.
* @throws org.apache.maven.plugin.MojoExecutionException If the project could not be launched.
*/
private void runBuild( File projectsDir, BuildJob buildJob, File settingsFile, File actualJavaHome,
CharSequence actualJreVersion )
throws MojoExecutionException
{
File pomFile = new File( projectsDir, buildJob.getProject() );
File basedir;
if ( pomFile.isDirectory() )
{
basedir = pomFile;
pomFile = new File( basedir, "pom.xml" );
if ( !pomFile.exists() )
{
pomFile = null;
}
else
{
buildJob.setProject( buildJob.getProject() + File.separator + "pom.xml" );
}
}
else
{
basedir = pomFile.getParentFile();
}
getLog().info( buffer().a( "Building: " ).strong( buildJob.getProject() ).toString() );
File interpolatedPomFile = null;
if ( pomFile != null )
{
if ( filteredPomPrefix != null )
{
interpolatedPomFile = new File( basedir, filteredPomPrefix + pomFile.getName() );
buildInterpolatedFile( pomFile, interpolatedPomFile );
}
else
{
interpolatedPomFile = pomFile;
}
}
InvokerProperties invokerProperties = getInvokerProperties( basedir );
// let's set what details we can
buildJob.setName( invokerProperties.getJobName() );
buildJob.setDescription( invokerProperties.getJobDescription() );
try
{
int selection = getSelection( invokerProperties, actualJreVersion );
if ( selection == 0 )
{
long milliseconds = System.currentTimeMillis();
boolean executed;
try
{
// CHECKSTYLE_OFF: LineLength
executed =
runBuild( basedir, interpolatedPomFile, settingsFile, actualJavaHome, invokerProperties );
// CHECKSTYLE_ON: LineLength
}
finally
{
milliseconds = System.currentTimeMillis() - milliseconds;
buildJob.setTime( milliseconds / 1000.0 );
}
if ( executed )
{
buildJob.setResult( BuildJob.Result.SUCCESS );
if ( !suppressSummaries )
{
getLog().info( ".." + buffer().success( "SUCCESS " ) + formatTime( buildJob.getTime() ) );
}
}
else
{
buildJob.setResult( BuildJob.Result.SKIPPED );
if ( !suppressSummaries )
{
getLog().info( ".." + buffer().warning( "SKIPPED " ) + formatTime( buildJob.getTime() ) );
}
}
}
else
{
buildJob.setResult( BuildJob.Result.SKIPPED );
StringBuilder message = new StringBuilder();
if ( ( selection & SELECTOR_MAVENVERSION ) != 0 )
{
message.append( "Maven version" );
}
if ( ( selection & SELECTOR_JREVERSION ) != 0 )
{
if ( message.length() > 0 )
{
message.append( ", " );
}
message.append( "JRE version" );
}
if ( ( selection & SELECTOR_OSFAMILY ) != 0 )
{
if ( message.length() > 0 )
{
message.append( ", " );
}
message.append( "OS" );
}
if ( !suppressSummaries )
{
getLog().info( ".." + buffer().warning( "SKIPPED " ) + " due to " + message.toString() );
}
// Abuse failureMessage, the field in the report which should contain the reason for skipping
// Consider skipCode + I18N
buildJob.setFailureMessage( "Skipped due to " + message.toString() );
}
}
catch ( RunErrorException e )
{
buildJob.setResult( BuildJob.Result.ERROR );
buildJob.setFailureMessage( e.getMessage() );
if ( !suppressSummaries )
{
getLog().info( ".." + buffer().failure( "ERROR " ) + formatTime( buildJob.getTime() ) );
getLog().info( " " + e.getMessage() );
}
}
catch ( RunFailureException e )
{
buildJob.setResult( e.getType() );
buildJob.setFailureMessage( e.getMessage() );
if ( !suppressSummaries )
{
getLog().info( ".." + buffer().failure( "FAILED " ) + formatTime( buildJob.getTime() ) );
getLog().info( " " + e.getMessage() );
}
}
finally
{
if ( interpolatedPomFile != null && StringUtils.isNotEmpty( filteredPomPrefix ) )
{
interpolatedPomFile.delete();
}
writeBuildReport( buildJob );
}
}
/**
* Determines whether selector conditions of the specified invoker properties match the current environment.
*
* @param invokerProperties The invoker properties to check, must not be <code>null</code>.
* @return <code>0</code> if the job corresponding to the properties should be run, otherwise a bitwise value
* representing the reason why it should be skipped.
*/
private int getSelection( InvokerProperties invokerProperties, CharSequence actualJreVersion )
{
int selection = 0;
if ( !SelectorUtils.isMavenVersion( invokerProperties.getMavenVersion(), actualMavenVersion ) )
{
selection |= SELECTOR_MAVENVERSION;
}
if ( !SelectorUtils.isJreVersion( invokerProperties.getJreVersion(), actualJreVersion.toString() ) )
{
selection |= SELECTOR_JREVERSION;
}
if ( !SelectorUtils.isOsFamily( invokerProperties.getOsFamily() ) )
{
selection |= SELECTOR_OSFAMILY;
}
return selection;
}
/**
* Writes the XML report for the specified build job unless report generation has been disabled.
*
* @param buildJob The build job whose report should be written, must not be <code>null</code>.
* @throws org.apache.maven.plugin.MojoExecutionException If the report could not be written.
*/
private void writeBuildReport( BuildJob buildJob )
throws MojoExecutionException
{
if ( disableReports )
{
return;
}
String safeFileName = buildJob.getProject().replace( '/', '_' ).replace( '\\', '_' ).replace( ' ', '_' );
if ( safeFileName.endsWith( "_pom.xml" ) )
{
safeFileName = safeFileName.substring( 0, safeFileName.length() - "_pom.xml".length() );
}
File reportFile = new File( reportsDirectory, "BUILD-" + safeFileName + ".xml" );
try
{
FileOutputStream fos = new FileOutputStream( reportFile );
try
{
Writer osw = new OutputStreamWriter( fos, buildJob.getModelEncoding() );
BuildJobXpp3Writer writer = new BuildJobXpp3Writer();
writer.write( osw, buildJob );
osw.close();
}
finally
{
fos.close();
}
}
catch ( IOException e )
{
throw new MojoExecutionException( "Failed to write build report " + reportFile, e );
}
}
/**
* Formats the specified build duration time.
*
* @param seconds The duration of the build.
* @return The formatted time, never <code>null</code>.
*/
private String formatTime( double seconds )
{
return secFormat.format( seconds );
}
/**
* Runs the specified project.
*
* @param basedir The base directory of the project, must not be <code>null</code>.
* @param pomFile The (already interpolated) POM file, may be <code>null</code> for a POM-less Maven invocation.
* @param settingsFile The (already interpolated) user settings file for the build, may be <code>null</code>. Will
* be merged with the settings file of the invoking Maven process.
* @param invokerProperties The properties to use.
* @return <code>true</code> if the project was launched or <code>false</code> if the selector script indicated that
* the project should be skipped.
* @throws org.apache.maven.plugin.MojoExecutionException If the project could not be launched.
* @throws org.apache.maven.shared.scriptinterpreter.RunFailureException If either a hook script or the build itself
* failed.
*/
private boolean runBuild( File basedir, File pomFile, File settingsFile, File actualJavaHome,
InvokerProperties invokerProperties )
throws MojoExecutionException, RunFailureException
{
if ( getLog().isDebugEnabled() && !invokerProperties.getProperties().isEmpty() )
{
Properties props = invokerProperties.getProperties();
getLog().debug( "Using invoker properties:" );
for ( String key : new TreeSet<String>( (Set) props.keySet() ) )
{
String value = props.getProperty( key );
getLog().debug( " " + key + " = " + value );
}
}
List<String> goals = getGoals( basedir );
List<String> profiles = getProfiles( basedir );
Map<String, Object> context = new LinkedHashMap<String, Object>();
FileLogger logger = setupLogger( basedir );
try
{
try
{
scriptRunner.run( "selector script", basedir, selectorScript, context, logger, BuildJob.Result.SKIPPED,
false );
}
catch ( RunErrorException e )
{
throw e;
}
catch ( RunFailureException e )
{
return false;
}
scriptRunner.run( "pre-build script", basedir, preBuildHookScript, context, logger,
BuildJob.Result.FAILURE_PRE_HOOK, false );
final InvocationRequest request = new DefaultInvocationRequest();
request.setLocalRepositoryDirectory( localRepositoryPath );
request.setBatchMode( true );
request.setShowErrors( showErrors );
request.setDebug( debug );
request.setShowVersion( showVersion );
setupLoggerForBuildJob( logger, request );
if ( mavenHome != null )
{
invoker.setMavenHome( mavenHome );
// FIXME: Should we really take care of M2_HOME?
request.addShellEnvironment( "M2_HOME", mavenHome.getAbsolutePath() );
}
if ( mavenExecutable != null )
{
invoker.setMavenExecutable( new File( mavenExecutable ) );
}
if ( actualJavaHome != null )
{
request.setJavaHome( actualJavaHome );
}
if ( environmentVariables != null )
{
for ( Map.Entry<String, String> variable : environmentVariables.entrySet() )
{
request.addShellEnvironment( variable.getKey(), variable.getValue() );
}
}
for ( int invocationIndex = 1;; invocationIndex++ )
{
if ( invocationIndex > 1 && !invokerProperties.isInvocationDefined( invocationIndex ) )
{
break;
}
request.setBaseDirectory( basedir );
request.setPomFile( pomFile );
request.setGoals( goals );
request.setProfiles( profiles );
request.setMavenOpts( mavenOpts );
request.setOffline( false );
request.setUserSettingsFile( settingsFile );
Properties systemProperties =
getSystemProperties( basedir, invokerProperties.getSystemPropertiesFile( invocationIndex ) );
request.setProperties( systemProperties );
invokerProperties.configureInvocation( request, invocationIndex );
if ( getLog().isDebugEnabled() )
{
try
{
getLog().debug( "Using MAVEN_OPTS: " + request.getMavenOpts() );
getLog().debug( "Executing: " + new MavenCommandLineBuilder().build( request ) );
}
catch ( CommandLineConfigurationException e )
{
getLog().debug( "Failed to display command line: " + e.getMessage() );
}
}
InvocationResult result;
try
{
result = invoker.execute( request );
}
catch ( final MavenInvocationException e )
{
getLog().debug( "Error invoking Maven: " + e.getMessage(), e );
throw new RunFailureException( "Maven invocation failed. " + e.getMessage(),
BuildJob.Result.FAILURE_BUILD );
}
verify( result, invocationIndex, invokerProperties, logger );
}
scriptRunner.run( "post-build script", basedir, postBuildHookScript, context, logger,
BuildJob.Result.FAILURE_POST_HOOK, true );
}
catch ( IOException e )
{
throw new MojoExecutionException( e.getMessage(), e );
}
finally
{
if ( logger != null )
{
logger.close();
}
}
return true;
}
private void setupLoggerForBuildJob( FileLogger logger, final InvocationRequest request )
{
if ( logger != null )
{
request.setErrorHandler( logger );
request.setOutputHandler( logger );
}
}
/**
* Initializes the build logger for the specified project.
*
* @param basedir The base directory of the project, must not be <code>null</code>.
* @return The build logger or <code>null</code> if logging has been disabled.
* @throws org.apache.maven.plugin.MojoExecutionException If the log file could not be created.
*/
private FileLogger setupLogger( File basedir )
throws MojoExecutionException
{
FileLogger logger = null;
if ( !noLog )
{
File outputLog = new File( basedir, "build.log" );
try
{
if ( streamLogs )
{
logger = new FileLogger( outputLog, getLog() );
}
else
{
logger = new FileLogger( outputLog );
}
getLog().debug( "Build log initialized in: " + outputLog );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Error initializing build logfile in: " + outputLog, e );
}
}
return logger;
}
/**
* Gets the system properties to use for the specified project.
*
* @param basedir The base directory of the project, must not be <code>null</code>.
* @param filename The filename to the properties file to load, may be <code>null</code> to use the default path
* given by {@link #testPropertiesFile}.
* @return The system properties to use, may be empty but never <code>null</code>.
* @throws org.apache.maven.plugin.MojoExecutionException If the properties file exists but could not be read.
*/
private Properties getSystemProperties( final File basedir, final String filename )
throws MojoExecutionException
{
Properties collectedTestProperties = new Properties();
if ( properties != null )
{
// MINVOKER-118: property can have empty value, which is not accepted by collectedTestProperties
for ( Map.Entry<String, String> entry : properties.entrySet() )
{
if ( entry.getValue() != null )
{
collectedTestProperties.put( entry.getKey(), entry.getValue() );
}
}
}
File propertiesFile = null;
if ( filename != null )
{
propertiesFile = new File( basedir, filename );
}
else if ( testPropertiesFile != null )
{
propertiesFile = new File( basedir, testPropertiesFile );
}
if ( propertiesFile != null && propertiesFile.isFile() )
{
InputStream fin = null;
try
{
fin = new FileInputStream( propertiesFile );
Properties loadedProperties = new Properties();
loadedProperties.load( fin );
fin.close();
fin = null;
collectedTestProperties.putAll( loadedProperties );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Error reading system properties from " + propertiesFile );
}
finally
{
IOUtil.close( fin );
}
}
return collectedTestProperties;
}
/**
* Verifies the invocation result.
*
* @param result The invocation result to check, must not be <code>null</code>.
* @param invocationIndex The index of the invocation for which to check the exit code, must not be negative.
* @param invokerProperties The invoker properties used to check the exit code, must not be <code>null</code>.
* @param logger The build logger, may be <code>null</code> if logging is disabled.
* @throws org.apache.maven.shared.scriptinterpreter.RunFailureException If the invocation result indicates a build
* failure.
*/
private void verify( InvocationResult result, int invocationIndex, InvokerProperties invokerProperties,
FileLogger logger )
throws RunFailureException
{
if ( result.getExecutionException() != null )
{
throw new RunFailureException( "The Maven invocation failed. "
+ result.getExecutionException().getMessage(), BuildJob.Result.ERROR );
}
else if ( !invokerProperties.isExpectedResult( result.getExitCode(), invocationIndex ) )
{
StringBuilder buffer = new StringBuilder( 256 );
buffer.append( "The build exited with code " ).append( result.getExitCode() ).append( ". " );
if ( logger != null )
{
buffer.append( "See " );
buffer.append( logger.getOutputFile().getAbsolutePath() );
buffer.append( " for details." );
}
else
{
buffer.append( "See console output for details." );
}
throw new RunFailureException( buffer.toString(), BuildJob.Result.FAILURE_BUILD );
}
}
/**
* Gets the goal list for the specified project.
*
* @param basedir The base directory of the project, must not be <code>null</code>.
* @return The list of goals to run when building the project, may be empty but never <code>null</code>.
* @throws org.apache.maven.plugin.MojoExecutionException If the profile file could not be read.
*/
List<String> getGoals( final File basedir )
throws MojoExecutionException
{
try
{
// FIXME: Currently we have null for goalsFile which has been removed.
// This might mean we can remove getGoals() at all ? Check this.
return getTokens( basedir, null, goals );
}
catch ( IOException e )
{
throw new MojoExecutionException( "error reading goals", e );
}
}
/**
* Gets the profile list for the specified project.
*
* @param basedir The base directory of the project, must not be <code>null</code>.
* @return The list of profiles to activate when building the project, may be empty but never <code>null</code>.
* @throws org.apache.maven.plugin.MojoExecutionException If the profile file could not be read.
*/
List<String> getProfiles( File basedir )
throws MojoExecutionException
{
try
{
return getTokens( basedir, null, profiles );
}
catch ( IOException e )
{
throw new MojoExecutionException( "error reading profiles", e );
}
}
private List<String> calculateExcludes()
throws IOException
{
List<String> excludes =
( pomExcludes != null ) ? new ArrayList<String>( pomExcludes ) : new ArrayList<String>();
if ( this.settingsFile != null )
{
String exclude = relativizePath( this.settingsFile, projectsDirectory.getCanonicalPath() );
if ( exclude != null )
{
excludes.add( exclude.replace( '\\', '/' ) );
getLog().debug( "Automatically excluded " + exclude + " from project scanning" );
}
}
return excludes;
}
/**
* @return The list of setupUp jobs.
* @throws IOException
* @see {@link #setupIncludes}
*/
private BuildJob[] getSetupBuildJobsFromFolders()
throws IOException
{
List<String> excludes = calculateExcludes();
BuildJob[] setupPoms = scanProjectsDirectory( setupIncludes, excludes, BuildJob.Type.SETUP );
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Setup projects: " + Arrays.asList( setupPoms ) );
}
return setupPoms;
}
/**
* Gets the build jobs that should be processed. Note that the order of the returned build jobs is significant.
*
* @return The build jobs to process, may be empty but never <code>null</code>.
* @throws java.io.IOException If the projects directory could not be scanned.
*/
BuildJob[] getBuildJobs()
throws IOException
{
BuildJob[] buildJobs;
if ( invokerTest == null )
{
List<String> excludes = calculateExcludes();
BuildJob[] setupPoms = scanProjectsDirectory( setupIncludes, excludes, BuildJob.Type.SETUP );
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Setup projects: " + Arrays.asList( setupPoms ) );
}
BuildJob[] normalPoms = scanProjectsDirectory( pomIncludes, excludes, BuildJob.Type.NORMAL );
Map<String, BuildJob> uniquePoms = new LinkedHashMap<String, BuildJob>();
for ( BuildJob setupPom : setupPoms )
{
uniquePoms.put( setupPom.getProject(), setupPom );
}
for ( BuildJob normalPom : normalPoms )
{
if ( !uniquePoms.containsKey( normalPom.getProject() ) )
{
uniquePoms.put( normalPom.getProject(), normalPom );
}
}
buildJobs = uniquePoms.values().toArray( new BuildJob[uniquePoms.size()] );
}
else
{
String[] testRegexes = StringUtils.split( invokerTest, "," );
List<String> includes = new ArrayList<String>( testRegexes.length );
List<String> excludes = new ArrayList<String>();
for ( String regex : testRegexes )
{
// user just use -Dinvoker.test=MWAR191,MNG111 to use a directory thats the end is not pom.xml
if ( regex.startsWith( "!" ) )
{
excludes.add( regex.substring( 1 ) );
}
else
{
includes.add( regex );
}
}
// it would be nice if we could figure out what types these are... but perhaps
// not necessary for the -Dinvoker.test=xxx t
buildJobs = scanProjectsDirectory( includes, excludes, BuildJob.Type.DIRECT );
}
relativizeProjectPaths( buildJobs );
return buildJobs;
}
/**
* Scans the projects directory for projects to build. Both (POM) files and mere directories will be matched by the
* scanner patterns. If the patterns match a directory which contains a file named "pom.xml", the results will
* include the path to this file rather than the directory path in order to avoid duplicate invocations of the same
* project.
*
* @param includes The include patterns for the scanner, may be <code>null</code>.
* @param excludes The exclude patterns for the scanner, may be <code>null</code> to exclude nothing.
* @param type The type to assign to the resulting build jobs, must not be <code>null</code>.
* @return The build jobs matching the patterns, never <code>null</code>.
* @throws java.io.IOException If the project directory could not be scanned.
*/
private BuildJob[] scanProjectsDirectory( List<String> includes, List<String> excludes, String type )
throws IOException
{
if ( !projectsDirectory.isDirectory() )
{
return new BuildJob[0];
}
DirectoryScanner scanner = new DirectoryScanner();
scanner.setBasedir( projectsDirectory.getCanonicalFile() );
scanner.setFollowSymlinks( false );
if ( includes != null )
{
scanner.setIncludes( includes.toArray( new String[includes.size()] ) );
}
if ( excludes != null )
{
scanner.setExcludes( excludes.toArray( new String[excludes.size()] ) );
}
scanner.addDefaultExcludes();
scanner.scan();
Map<String, BuildJob> matches = new LinkedHashMap<String, BuildJob>();
for ( String includedFile : scanner.getIncludedFiles() )
{
matches.put( includedFile, new BuildJob( includedFile, type ) );
}
for ( String includedDir : scanner.getIncludedDirectories() )
{
String includedFile = includedDir + File.separatorChar + "pom.xml";
if ( new File( scanner.getBasedir(), includedFile ).isFile() )
{
matches.put( includedFile, new BuildJob( includedFile, type ) );
}
else
{
matches.put( includedDir, new BuildJob( includedDir, type ) );
}
}
return matches.values().toArray( new BuildJob[matches.size()] );
}
/**
* Relativizes the project paths of the specified build jobs against the directory specified by
* {@link #projectsDirectory} (if possible). If a project path does not denote a sub path of the projects directory,
* it is returned as is.
*
* @param buildJobs The build jobs whose project paths should be relativized, must not be <code>null</code> nor
* contain <code>null</code> elements.
* @throws java.io.IOException If any path could not be relativized.
*/
private void relativizeProjectPaths( BuildJob[] buildJobs )
throws IOException
{
String projectsDirPath = projectsDirectory.getCanonicalPath();
for ( BuildJob buildJob : buildJobs )
{
String projectPath = buildJob.getProject();
File file = new File( projectPath );
if ( !file.isAbsolute() )
{
file = new File( projectsDirectory, projectPath );
}
String relativizedPath = relativizePath( file, projectsDirPath );
if ( relativizedPath == null )
{
relativizedPath = projectPath;
}
buildJob.setProject( relativizedPath );
}
}
/**
* Relativizes the specified path against the given base directory. Besides relativization, the returned path will
* also be normalized, e.g. directory references like ".." will be removed.
*
* @param path The path to relativize, must not be <code>null</code>.
* @param basedir The (canonical path of the) base directory to relativize against, must not be <code>null</code>.
* @return The relative path in normal form or <code>null</code> if the input path does not denote a sub path of the
* base directory.
* @throws java.io.IOException If the path could not be relativized.
*/
private String relativizePath( File path, String basedir )
throws IOException
{
String relativizedPath = path.getCanonicalPath();
if ( relativizedPath.startsWith( basedir ) )
{
relativizedPath = relativizedPath.substring( basedir.length() );
if ( relativizedPath.startsWith( File.separator ) )
{
relativizedPath = relativizedPath.substring( File.separator.length() );
}
return relativizedPath;
}
else
{
return null;
}
}
/**
* Returns the map-based value source used to interpolate POMs and other stuff.
*
* @return The map-based value source for interpolation, never <code>null</code>.
*/
private Map<String, Object> getInterpolationValueSource()
{
Map<String, Object> props = new HashMap<String, Object>();
if ( filterProperties != null )
{
props.putAll( filterProperties );
}
props.put( "basedir", this.project.getBasedir().getAbsolutePath() );
props.put( "baseurl", toUrl( this.project.getBasedir().getAbsolutePath() ) );
if ( settings.getLocalRepository() != null )
{
props.put( "localRepository", settings.getLocalRepository() );
props.put( "localRepositoryUrl", toUrl( settings.getLocalRepository() ) );
}
return new CompositeMap( this.project, props );
}
/**
* Converts the specified filesystem path to a URL. The resulting URL has no trailing slash regardless whether the
* path denotes a file or a directory.
*
* @param filename The filesystem path to convert, must not be <code>null</code>.
* @return The <code>file:</code> URL for the specified path, never <code>null</code>.
*/
private static String toUrl( String filename )
{
/*
* NOTE: Maven fails to properly handle percent-encoded "file:" URLs (WAGON-111) so don't use File.toURI() here
* as-is but use the decoded path component in the URL.
*/
String url = "file://" + new File( filename ).toURI().getPath();
if ( url.endsWith( "/" ) )
{
url = url.substring( 0, url.length() - 1 );
}
return url;
}
/**
* Gets goal/profile names for the specified project, either directly from the plugin configuration or from an
* external token file.
*
* @param basedir The base directory of the test project, must not be <code>null</code>.
* @param filename The (simple) name of an optional file in the project base directory from which to read
* goals/profiles, may be <code>null</code>.
* @param defaultTokens The list of tokens to return in case the specified token file does not exist, may be
* <code>null</code>.
* @return The list of goal/profile names, may be empty but never <code>null</code>.
* @throws java.io.IOException If the token file exists but could not be parsed.
*/
private List<String> getTokens( File basedir, String filename, List<String> defaultTokens )
throws IOException
{
List<String> tokens = ( defaultTokens != null ) ? defaultTokens : new ArrayList<String>();
if ( StringUtils.isNotEmpty( filename ) )
{
File tokenFile = new File( basedir, filename );
if ( tokenFile.exists() )
{
tokens = readTokens( tokenFile );
}
}
return tokens;
}
/**
* Reads the tokens from the specified file. Tokens are separated either by line terminators or commas. During
* parsing, the file contents will be interpolated.
*
* @param tokenFile The file to read the tokens from, must not be <code>null</code>.
* @return The list of tokens, may be empty but never <code>null</code>.
* @throws java.io.IOException If the token file could not be read.
*/
private List<String> readTokens( final File tokenFile )
throws IOException
{
List<String> result = new ArrayList<String>();
BufferedReader reader = null;
try
{
Map<String, Object> composite = getInterpolationValueSource();
reader = new BufferedReader( new InterpolationFilterReader( newReader( tokenFile ), composite ) );
for ( String line = reader.readLine(); line != null; line = reader.readLine() )
{
result.addAll( collectListFromCSV( line ) );
}
reader.close();
reader = null;
}
finally
{
IOUtil.close( reader );
}
return result;
}
/**
* Gets a list of comma separated tokens from the specified line.
*
* @param csv The line with comma separated tokens, may be <code>null</code>.
* @return The list of tokens from the line, may be empty but never <code>null</code>.
*/
private List<String> collectListFromCSV( final String csv )
{
final List<String> result = new ArrayList<String>();
if ( ( csv != null ) && ( csv.trim().length() > 0 ) )
{
final StringTokenizer st = new StringTokenizer( csv, "," );
while ( st.hasMoreTokens() )
{
result.add( st.nextToken().trim() );
}
}
return result;
}
/**
* Interpolates the specified POM/settings file to a temporary file. The destination file may be same as the input
* file, i.e. interpolation can be performed in-place.
*
* @param originalFile The XML file to interpolate, must not be <code>null</code>.
* @param interpolatedFile The target file to write the interpolated contents of the original file to, must not be
* <code>null</code>.
* @throws org.apache.maven.plugin.MojoExecutionException If the target file could not be created.
*/
void buildInterpolatedFile( File originalFile, File interpolatedFile )
throws MojoExecutionException
{
getLog().debug( "Interpolate " + originalFile.getPath() + " to " + interpolatedFile.getPath() );
try
{
String xml;
Reader reader = null;
try
{
// interpolation with token @...@
Map<String, Object> composite = getInterpolationValueSource();
reader =
new InterpolationFilterReader( ReaderFactory.newXmlReader( originalFile ), composite, "@", "@" );
xml = IOUtil.toString( reader );
reader.close();
reader = null;
}
finally
{
IOUtil.close( reader );
}
Writer writer = null;
try
{
interpolatedFile.getParentFile().mkdirs();
writer = WriterFactory.newXmlWriter( interpolatedFile );
writer.write( xml );
writer.close();
writer = null;
}
finally
{
IOUtil.close( writer );
}
}
catch ( IOException e )
{
throw new MojoExecutionException( "Failed to interpolate file " + originalFile.getPath(), e );
}
}
/**
* Gets the (interpolated) invoker properties for an integration test.
*
* @param projectDirectory The base directory of the IT project, must not be <code>null</code>.
* @return The invoker properties, may be empty but never <code>null</code>.
* @throws org.apache.maven.plugin.MojoExecutionException If an I/O error occurred during reading the properties.
*/
private InvokerProperties getInvokerProperties( final File projectDirectory )
throws MojoExecutionException
{
Properties props = new Properties();
if ( invokerPropertiesFile != null )
{
File propertiesFile = new File( projectDirectory, invokerPropertiesFile );
if ( propertiesFile.isFile() )
{
InputStream in = null;
try
{
in = new FileInputStream( propertiesFile );
props.load( in );
in.close();
in = null;
}
catch ( IOException e )
{
throw new MojoExecutionException( "Failed to read invoker properties: " + propertiesFile, e );
}
finally
{
IOUtil.close( in );
}
}
Interpolator interpolator = new RegexBasedInterpolator();
interpolator.addValueSource( new MapBasedValueSource( getInterpolationValueSource() ) );
// CHECKSTYLE_OFF: LineLength
for ( String key : (Set<String>) ( (Map) props ).keySet() )
{
String value = props.getProperty( key );
try
{
value = interpolator.interpolate( value, "" );
}
catch ( InterpolationException e )
{
throw new MojoExecutionException( "Failed to interpolate invoker properties: " + propertiesFile,
e );
}
props.setProperty( key, value );
}
// CHECKSTYLE_ON: LineLength
}
return new InvokerProperties( props );
}
protected boolean isParallelRun()
{
return parallelThreads > 1;
}
}