From 658ad90b3850131e4a73fd6cca2ead30f6e5f213 Mon Sep 17 00:00:00 2001 From: rfscholte Date: Sat, 20 Jun 2020 13:55:48 +0200 Subject: [PATCH] [MNG-5760] Add `-r/--resume` to automatically resume from the last failure point Author: Martin Kanters --- .../java/org/apache/maven/DefaultMaven.java | 48 +++++- .../execution/BuildResumptionAnalyzer.java | 36 ++++ .../maven/execution/BuildResumptionData.java | 54 ++++++ .../BuildResumptionDataRepository.java | 56 ++++++ .../BuildResumptionPersistenceException.java | 32 ++++ .../DefaultBuildResumptionAnalyzer.java | 162 ++++++++++++++++++ .../DefaultBuildResumptionDataRepository.java | 145 ++++++++++++++++ .../DefaultMavenExecutionRequest.java | 16 ++ .../DefaultMavenExecutionResult.java | 14 ++ .../execution/MavenExecutionRequest.java | 11 ++ .../maven/execution/MavenExecutionResult.java | 15 ++ .../maven/graph/DefaultGraphBuilder.java | 17 ++ .../DefaultBuildResumptionAnalyzerTest.java | 150 ++++++++++++++++ ...aultBuildResumptionDataRepositoryTest.java | 78 +++++++++ .../java/org/apache/maven/cli/CLIManager.java | 3 + .../java/org/apache/maven/cli/MavenCli.java | 65 ++++--- .../org/apache/maven/cli/MavenCliTest.java | 38 ++++ 17 files changed, 918 insertions(+), 22 deletions(-) create mode 100644 maven-core/src/main/java/org/apache/maven/execution/BuildResumptionAnalyzer.java create mode 100644 maven-core/src/main/java/org/apache/maven/execution/BuildResumptionData.java create mode 100644 maven-core/src/main/java/org/apache/maven/execution/BuildResumptionDataRepository.java create mode 100644 maven-core/src/main/java/org/apache/maven/execution/BuildResumptionPersistenceException.java create mode 100644 maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzer.java create mode 100644 maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java create mode 100644 maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzerTest.java create mode 100644 maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java diff --git a/maven-core/src/main/java/org/apache/maven/DefaultMaven.java b/maven-core/src/main/java/org/apache/maven/DefaultMaven.java index fc262901b6..51d7752fbf 100644 --- a/maven-core/src/main/java/org/apache/maven/DefaultMaven.java +++ b/maven-core/src/main/java/org/apache/maven/DefaultMaven.java @@ -36,6 +36,9 @@ import javax.inject.Named; import javax.inject.Singleton; import org.apache.maven.artifact.ArtifactUtils; +import org.apache.maven.execution.BuildResumptionAnalyzer; +import org.apache.maven.execution.BuildResumptionDataRepository; +import org.apache.maven.execution.BuildResumptionPersistenceException; import org.apache.maven.execution.DefaultMavenExecutionResult; import org.apache.maven.execution.ExecutionEvent; import org.apache.maven.execution.MavenExecutionRequest; @@ -44,6 +47,7 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.execution.ProjectDependencyGraph; import org.apache.maven.graph.GraphBuilder; import org.apache.maven.internal.aether.DefaultRepositorySystemSessionFactory; +import org.apache.maven.lifecycle.LifecycleExecutionException; import org.apache.maven.lifecycle.internal.ExecutionEventCatapult; import org.apache.maven.lifecycle.internal.LifecycleStarter; import org.apache.maven.model.Prerequisites; @@ -99,6 +103,12 @@ public class DefaultMaven @Named( GraphBuilder.HINT ) private GraphBuilder graphBuilder; + @Inject + private BuildResumptionAnalyzer buildResumptionAnalyzer; + + @Inject + private BuildResumptionDataRepository buildResumptionDataRepository; + @Override public MavenExecutionResult execute( MavenExecutionRequest request ) { @@ -312,7 +322,16 @@ public class DefaultMaven if ( session.getResult().hasExceptions() ) { - return addExceptionToResult( result, session.getResult().getExceptions().get( 0 ) ); + addExceptionToResult( result, session.getResult().getExceptions().get( 0 ) ); + persistResumptionData( result, session ); + return result; + } + else + { + session.getAllProjects().stream() + .filter( MavenProject::isExecutionRoot ) + .findFirst() + .ifPresent( buildResumptionDataRepository::removeResumptionData ); } } finally @@ -349,6 +368,33 @@ public class DefaultMaven } } + private void persistResumptionData( MavenExecutionResult result, MavenSession session ) + { + boolean hasLifecycleExecutionExceptions = result.getExceptions().stream() + .anyMatch( LifecycleExecutionException.class::isInstance ); + + if ( hasLifecycleExecutionExceptions ) + { + MavenProject rootProject = session.getAllProjects().stream() + .filter( MavenProject::isExecutionRoot ) + .findFirst() + .orElseThrow( () -> new IllegalStateException( "No project in the session is execution root" ) ); + + buildResumptionAnalyzer.determineBuildResumptionData( result ).ifPresent( resumption -> + { + try + { + buildResumptionDataRepository.persistResumptionData( rootProject, resumption ); + result.setCanResume( true ); + } + catch ( BuildResumptionPersistenceException e ) + { + logger.warn( "Could not persist build resumption data", e ); + } + } ); + } + } + public RepositorySystemSession newRepositorySession( MavenExecutionRequest request ) { return repositorySessionFactory.newRepositorySession( request ); diff --git a/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionAnalyzer.java b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionAnalyzer.java new file mode 100644 index 0000000000..1778946b00 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionAnalyzer.java @@ -0,0 +1,36 @@ +package org.apache.maven.execution; + +/* + * 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 java.util.Optional; + +/** + * Instances of this class are responsible for determining whether it makes sense to "resume" a build (i.e., using + * the {@code --resume} flag. + */ +public interface BuildResumptionAnalyzer +{ + /** + * Construct an instance of {@link BuildResumptionData} based on the outcome of the current Maven build. + * @param result Outcome of the current Maven build. + * @return A {@link BuildResumptionData} instance or {@link Optional#empty()} if resuming the build is not possible. + */ + Optional determineBuildResumptionData( final MavenExecutionResult result ); +} diff --git a/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionData.java b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionData.java new file mode 100644 index 0000000000..8889b8312a --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionData.java @@ -0,0 +1,54 @@ +package org.apache.maven.execution; + +/* + * 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 java.util.List; + +/** + * This class holds the information required to enable resuming a Maven build with {@code --resume}. + */ +public class BuildResumptionData +{ + /** + * The project where the next build could resume from. + */ + private final String resumeFrom; + + /** + * List of projects to skip if the build would be resumed from {@link #resumeFrom}. + */ + private final List projectsToSkip; + + public BuildResumptionData ( final String resumeFrom, final List projectsToSkip ) + { + this.resumeFrom = resumeFrom; + this.projectsToSkip = projectsToSkip; + } + + public String getResumeFrom() + { + return this.resumeFrom; + } + + public List getProjectsToSkip() + { + return this.projectsToSkip; + } +} diff --git a/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionDataRepository.java b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionDataRepository.java new file mode 100644 index 0000000000..3d0be6f607 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionDataRepository.java @@ -0,0 +1,56 @@ +package org.apache.maven.execution; + +/* + * 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 org.apache.maven.project.MavenProject; + +/** + * Instances of this interface retrieve and store data for the --resume / -r feature. This data is used to ensure newer + * builds of the same project, that have the -r command-line flag, skip successfully built projects during earlier + * invocations of Maven. + */ +public interface BuildResumptionDataRepository +{ + /** + * Persists any data needed to resume the build at a later point in time, using a new Maven invocation. This method + * may also decide it is not needed or meaningful to persist such data, and return false to indicate + * so. + * + * @param rootProject The root project that is being built. + * @param buildResumptionData Information needed to resume the build. + * @throws BuildResumptionPersistenceException When an error occurs while persisting data. + */ + void persistResumptionData( final MavenProject rootProject, final BuildResumptionData buildResumptionData ) + throws BuildResumptionPersistenceException; + + /** + * Uses previously stored resumption data to enrich an existing execution request. + * @param request The execution request that will be enriched. + * @param rootProject The root project that is being built. + */ + void applyResumptionData( final MavenExecutionRequest request, final MavenProject rootProject ); + + /** + * Removes previously stored resumption data. + * @param rootProject The root project that is being built. + */ + void removeResumptionData( final MavenProject rootProject ); + +} diff --git a/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionPersistenceException.java b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionPersistenceException.java new file mode 100644 index 0000000000..1f9e8026a8 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionPersistenceException.java @@ -0,0 +1,32 @@ +package org.apache.maven.execution; + +/* + * 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. + */ + +/** + * This exception will be thrown when something fails while persisting build resumption data. + * @see BuildResumptionDataRepository#persistResumptionData + */ +public class BuildResumptionPersistenceException extends Exception +{ + public BuildResumptionPersistenceException( String message, Throwable cause ) + { + super( message, cause ); + } +} diff --git a/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzer.java b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzer.java new file mode 100644 index 0000000000..3d100dc891 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzer.java @@ -0,0 +1,162 @@ +package org.apache.maven.execution; + +/* + * 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 org.apache.maven.lifecycle.LifecycleExecutionException; +import org.apache.maven.model.Dependency; +import org.apache.maven.project.MavenProject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Named; +import javax.inject.Singleton; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import static java.util.Comparator.comparing; + +/** + * Default implementation of {@link BuildResumptionAnalyzer}. + */ +@Named +@Singleton +public class DefaultBuildResumptionAnalyzer implements BuildResumptionAnalyzer +{ + private static final Logger LOGGER = LoggerFactory.getLogger( DefaultBuildResumptionAnalyzer.class ); + + @Override + public Optional determineBuildResumptionData( final MavenExecutionResult result ) + { + final List failedProjects = getFailedProjectsInOrder( result ); + + if ( failedProjects.isEmpty() ) + { + LOGGER.info( "No failed projects found, resuming the build would not make sense." ); + return Optional.empty(); + } + + final MavenProject resumeFromProject = failedProjects.get( 0 ); + + if ( isFailedProjectFirstInBuild( result, resumeFromProject ) ) + { + LOGGER.info( "The first module in the build failed, resuming the build would not make sense." ); + return Optional.empty(); + } + + final String resumeFromSelector = resumeFromProject.getGroupId() + ":" + resumeFromProject.getArtifactId(); + final List projectsToSkip = determineProjectsToSkip( result, failedProjects, resumeFromProject ); + + return Optional.of( new BuildResumptionData( resumeFromSelector, projectsToSkip ) ); + } + + private boolean isFailedProjectFirstInBuild( final MavenExecutionResult result, final MavenProject failedProject ) + { + final List sortedProjects = result.getTopologicallySortedProjects(); + return sortedProjects.indexOf( failedProject ) == 0; + } + + private List getFailedProjectsInOrder( MavenExecutionResult result ) + { + List sortedProjects = result.getTopologicallySortedProjects(); + + return result.getExceptions().stream() + .filter( LifecycleExecutionException.class::isInstance ) + .map( LifecycleExecutionException.class::cast ) + .map( LifecycleExecutionException::getProject ) + .sorted( comparing( sortedProjects::indexOf ) ) + .collect( Collectors.toList() ); + } + + /** + * Projects after the first failed project could have succeeded by using -T or --fail-at-end. + * These projects can be skipped from later builds. + * This is not the case these projects are dependent on one of the failed projects. + * @param result The result of the Maven build. + * @param failedProjects The list of failed projects in the build. + * @param resumeFromProject The project where the build will be resumed with in the next run. + * @return A list of projects which can be skipped in a later build. + */ + private List determineProjectsToSkip( MavenExecutionResult result, + List failedProjects, + MavenProject resumeFromProject ) + { + List allProjects = result.getTopologicallySortedProjects(); + int resumeFromProjectIndex = allProjects.indexOf( resumeFromProject ); + List remainingProjects = allProjects.subList( resumeFromProjectIndex + 1, allProjects.size() ); + + List failedProjectsGAList = failedProjects.stream() + .map( GroupArtifactPair::new ) + .collect( Collectors.toList() ); + + return remainingProjects.stream() + .filter( project -> result.getBuildSummary( project ) instanceof BuildSuccess ) + .filter( project -> hasNoDependencyOnProjects( project, failedProjectsGAList ) ) + .map( project -> project.getGroupId() + ":" + project.getArtifactId() ) + .collect( Collectors.toList() ); + } + + private boolean hasNoDependencyOnProjects( MavenProject project, List projectsGAs ) + { + return project.getDependencies().stream() + .map( GroupArtifactPair::new ) + .noneMatch( projectsGAs::contains ); + } + + private static class GroupArtifactPair + { + private final String groupId; + private final String artifactId; + + GroupArtifactPair( MavenProject project ) + { + this.groupId = project.getGroupId(); + this.artifactId = project.getArtifactId(); + } + + GroupArtifactPair( Dependency dependency ) + { + this.groupId = dependency.getGroupId(); + this.artifactId = dependency.getArtifactId(); + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + GroupArtifactPair that = (GroupArtifactPair) o; + return Objects.equals( groupId, that.groupId ) && Objects.equals( artifactId, that.artifactId ); + } + + @Override + public int hashCode() + { + return Objects.hash( groupId, artifactId ); + } + } +} diff --git a/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java new file mode 100644 index 0000000000..eea097f993 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java @@ -0,0 +1,145 @@ +package org.apache.maven.execution; + +/* + * 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 org.apache.commons.lang3.StringUtils; +import org.apache.maven.project.MavenProject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Named; +import javax.inject.Singleton; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Properties; + +/** + * This implementation of {@link BuildResumptionDataRepository} persists information in a properties file. The file is + * stored in the build output directory under the Maven execution root. + */ +@Named +@Singleton +public class DefaultBuildResumptionDataRepository implements BuildResumptionDataRepository +{ + private static final String RESUME_PROPERTIES_FILENAME = "resume.properties"; + private static final String RESUME_FROM_PROPERTY = "resumeFrom"; + private static final String EXCLUDED_PROJECTS_PROPERTY = "excludedProjects"; + private static final String PROPERTY_DELIMITER = ", "; + private static final Logger LOGGER = LoggerFactory.getLogger( DefaultBuildResumptionDataRepository.class ); + + @Override + public void persistResumptionData( MavenProject rootProject, BuildResumptionData buildResumptionData ) + throws BuildResumptionPersistenceException + { + Properties properties = convertToProperties( buildResumptionData ); + + Path resumeProperties = Paths.get( rootProject.getBuild().getDirectory(), RESUME_PROPERTIES_FILENAME ); + try + { + Files.createDirectories( resumeProperties.getParent() ); + try ( Writer writer = Files.newBufferedWriter( resumeProperties ) ) + { + properties.store( writer, null ); + } + } + catch ( IOException e ) + { + String message = "Could not create " + RESUME_PROPERTIES_FILENAME + " file."; + throw new BuildResumptionPersistenceException( message, e ); + } + } + + private Properties convertToProperties( final BuildResumptionData buildResumptionData ) + { + Properties properties = new Properties(); + properties.setProperty( RESUME_FROM_PROPERTY, buildResumptionData.getResumeFrom() ); + String excludedProjects = String.join( PROPERTY_DELIMITER, buildResumptionData.getProjectsToSkip() ); + properties.setProperty( EXCLUDED_PROJECTS_PROPERTY, excludedProjects ); + + return properties; + } + + @Override + public void applyResumptionData( MavenExecutionRequest request, MavenProject rootProject ) + { + Properties properties = loadResumptionFile( Paths.get( rootProject.getBuild().getDirectory() ) ); + applyResumptionProperties( request, properties ); + } + + @Override + public void removeResumptionData( MavenProject rootProject ) + { + Path resumeProperties = Paths.get( rootProject.getBuild().getDirectory(), RESUME_PROPERTIES_FILENAME ); + try + { + Files.deleteIfExists( resumeProperties ); + } + catch ( IOException e ) + { + LOGGER.warn( "Could not delete {} file. ", RESUME_PROPERTIES_FILENAME, e ); + } + } + + private Properties loadResumptionFile( Path rootBuildDirectory ) + { + Properties properties = new Properties(); + Path path = Paths.get( RESUME_PROPERTIES_FILENAME ).resolve( rootBuildDirectory ); + if ( !Files.exists( path ) ) + { + LOGGER.warn( "The {} file does not exist. The --resume / -r feature will not work.", path ); + return properties; + } + + try ( Reader reader = Files.newBufferedReader( path ) ) + { + properties.load( reader ); + } + catch ( IOException e ) + { + LOGGER.warn( "Unable to read {}. The --resume / -r feature will not work.", path ); + } + + return properties; + } + + // This method is made package-private for testing purposes + void applyResumptionProperties( MavenExecutionRequest request, Properties properties ) + { + if ( properties.containsKey( RESUME_FROM_PROPERTY ) && StringUtils.isEmpty( request.getResumeFrom() ) ) + { + String propertyValue = properties.getProperty( RESUME_FROM_PROPERTY ); + request.setResumeFrom( propertyValue ); + LOGGER.info( "Resuming from {} due to the --resume / -r feature.", propertyValue ); + } + + if ( properties.containsKey( EXCLUDED_PROJECTS_PROPERTY ) ) + { + String propertyValue = properties.getProperty( EXCLUDED_PROJECTS_PROPERTY ); + String[] excludedProjects = propertyValue.split( PROPERTY_DELIMITER ); + request.getExcludedProjects().addAll( Arrays.asList( excludedProjects ) ); + LOGGER.info( "Additionally excluding projects '{}' due to the --resume / -r feature.", propertyValue ); + } + } +} diff --git a/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java b/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java index 4a039eb3e1..9cbdebeddf 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java +++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java @@ -116,6 +116,8 @@ public class DefaultMavenExecutionRequest private List excludedProjects; + private boolean resume = false; + private String resumeFrom; private String makeBehavior; @@ -300,6 +302,12 @@ public class DefaultMavenExecutionRequest return excludedProjects; } + @Override + public boolean isResume() + { + return resume; + } + @Override public String getResumeFrom() { @@ -598,6 +606,14 @@ public class DefaultMavenExecutionRequest return this; } + @Override + public MavenExecutionRequest setResume() + { + resume = true; + + return this; + } + @Override public MavenExecutionRequest setResumeFrom( String project ) { diff --git a/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionResult.java b/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionResult.java index 6ab1daac80..ecddd6608e 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionResult.java +++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionResult.java @@ -43,6 +43,8 @@ public class DefaultMavenExecutionResult private final Map buildSummaries = Collections.synchronizedMap( new IdentityHashMap<>() ); + private boolean canResume = false; + public MavenExecutionResult setProject( MavenProject project ) { this.project = project; @@ -108,4 +110,16 @@ public class DefaultMavenExecutionResult { buildSummaries.put( summary.getProject(), summary ); } + + @Override + public boolean canResume() + { + return canResume; + } + + @Override + public void setCanResume( boolean canResume ) + { + this.canResume = canResume; + } } diff --git a/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java b/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java index d006a434c5..542c34aade 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java +++ b/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java @@ -171,6 +171,17 @@ public interface MavenExecutionRequest */ List getExcludedProjects(); + /** + * Sets whether the build should be resumed from the data in the resume.properties file. + * @return This request, never {@code null}. + */ + MavenExecutionRequest setResume(); + + /** + * @return Whether the build should be resumed from the data in the resume.properties file. + */ + boolean isResume(); + MavenExecutionRequest setResumeFrom( String project ); String getResumeFrom(); diff --git a/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionResult.java b/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionResult.java index cb95fb1fa7..8a099bb8b0 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionResult.java +++ b/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionResult.java @@ -67,4 +67,19 @@ public interface MavenExecutionResult * @param summary The build summary to add, must not be {@code null}. */ void addBuildSummary( BuildSummary summary ); + + /** + * Indicates whether or not the build could be resumed by a second invocation of Maven. + * @see BuildResumptionDataRepository + * @return true when it is possible to resume the build, false otherwise. + */ + boolean canResume(); + + /** + * Indicate that the build can or cannot be resumed by a second invocation of Maven. + * @param canResume true when it is possible to resume the build, false otherwise. + * @see BuildResumptionDataRepository + * @see #canResume() + */ + void setCanResume( boolean canResume ); } diff --git a/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java b/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java index 99f0266a1b..07cb80ee6f 100644 --- a/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java +++ b/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java @@ -38,6 +38,7 @@ import org.apache.maven.DefaultMaven; import org.apache.maven.MavenExecutionException; import org.apache.maven.ProjectCycleException; import org.apache.maven.artifact.ArtifactUtils; +import org.apache.maven.execution.BuildResumptionDataRepository; import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.execution.MavenSession; import org.apache.maven.execution.ProjectDependencyGraph; @@ -73,6 +74,9 @@ public class DefaultGraphBuilder @Inject protected ProjectBuilder projectBuilder; + @Inject + private BuildResumptionDataRepository buildResumptionDataRepository; + @Override public Result build( MavenSession session ) { @@ -84,6 +88,7 @@ public class DefaultGraphBuilder { final List projects = getProjectsForMavenReactor( session ); validateProjects( projects ); + enrichRequestFromResumptionData( projects, session.getRequest() ); result = reactorDependencyGraph( session, projects ); } @@ -341,6 +346,18 @@ public class DefaultGraphBuilder return result; } + private void enrichRequestFromResumptionData( List projects, MavenExecutionRequest request ) + { + if ( request.isResume() ) + { + projects.stream() + .filter( MavenProject::isExecutionRoot ) + .findFirst() + .ifPresent( rootProject -> + buildResumptionDataRepository.applyResumptionData( request, rootProject ) ); + } + } + private String formatProjects( List projects ) { StringBuilder projectNames = new StringBuilder(); diff --git a/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzerTest.java b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzerTest.java new file mode 100644 index 0000000000..59a8e63c14 --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzerTest.java @@ -0,0 +1,150 @@ +package org.apache.maven.execution; + +/* + * 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 org.apache.maven.lifecycle.LifecycleExecutionException; +import org.apache.maven.model.Dependency; +import org.apache.maven.project.MavenProject; +import org.junit.Before; +import org.junit.Test; + +import java.util.Optional; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; + +public class DefaultBuildResumptionAnalyzerTest +{ + private final DefaultBuildResumptionAnalyzer analyzer = new DefaultBuildResumptionAnalyzer(); + + private MavenExecutionResult executionResult; + + @Before + public void before() { + executionResult = new DefaultMavenExecutionResult(); + } + + @Test + public void resumeFromGetsDetermined() + { + MavenProject projectA = createSucceededMavenProject( "A" ); + MavenProject projectB = createFailedMavenProject( "B" ); + executionResult.setTopologicallySortedProjects( asList( projectA, projectB ) ); + + Optional result = analyzer.determineBuildResumptionData( executionResult ); + + assertThat( result.isPresent(), is( true ) ); + assertThat( result.get().getResumeFrom(), is( "test:B" ) ); + } + + @Test + public void resumeFromIsIgnoredWhenFirstProjectFails() + { + MavenProject projectA = createFailedMavenProject( "A" ); + MavenProject projectB = createMavenProject( "B" ); + executionResult.setTopologicallySortedProjects( asList( projectA, projectB ) ); + + Optional result = analyzer.determineBuildResumptionData( executionResult ); + + assertThat( result.isPresent(), is( false ) ); + } + + @Test + public void projectsSucceedingAfterFailedProjectsAreExcluded() + { + MavenProject projectA = createSucceededMavenProject( "A" ); + MavenProject projectB = createFailedMavenProject( "B" ); + MavenProject projectC = createSucceededMavenProject( "C" ); + executionResult.setTopologicallySortedProjects( asList( projectA, projectB, projectC ) ); + + Optional result = analyzer.determineBuildResumptionData( executionResult ); + + assertThat( result.isPresent(), is( true ) ); + assertThat( result.get().getProjectsToSkip(), contains( "test:C" ) ); + } + + @Test + public void projectsDependingOnFailedProjectsAreNotExcluded() + { + MavenProject projectA = createSucceededMavenProject( "A" ); + MavenProject projectB = createFailedMavenProject( "B" ); + MavenProject projectC = createSucceededMavenProject( "C" ); + projectC.setDependencies( singletonList( toDependency( projectB ) ) ); + executionResult.setTopologicallySortedProjects( asList( projectA, projectB, projectC ) ); + + Optional result = analyzer.determineBuildResumptionData( executionResult ); + + assertThat( result.isPresent(), is( true ) ); + assertThat( result.get().getProjectsToSkip().isEmpty(), is( true ) ); + } + + @Test + public void projectsFailingAfterAnotherFailedProjectAreNotExcluded() + { + MavenProject projectA = createSucceededMavenProject( "A" ); + MavenProject projectB = createFailedMavenProject( "B" ); + MavenProject projectC = createSucceededMavenProject( "C" ); + MavenProject projectD = createFailedMavenProject( "D" ); + executionResult.setTopologicallySortedProjects( asList( projectA, projectB, projectC, projectD ) ); + + Optional result = analyzer.determineBuildResumptionData( executionResult ); + + assertThat( result.isPresent(), is( true ) ); + assertThat( result.get().getResumeFrom(), is( "test:B" ) ); + assertThat( result.get().getProjectsToSkip(), contains( "test:C" ) ); + assertThat( result.get().getProjectsToSkip(), not( contains( "test:D" ) ) ); + } + + private MavenProject createMavenProject( String artifactId ) + { + MavenProject project = new MavenProject(); + project.setGroupId( "test" ); + project.setArtifactId( artifactId ); + return project; + } + + private Dependency toDependency(MavenProject mavenProject ) + { + Dependency dependency = new Dependency(); + dependency.setGroupId( mavenProject.getGroupId() ); + dependency.setArtifactId( mavenProject.getArtifactId() ); + dependency.setVersion( mavenProject.getVersion() ); + return dependency; + } + + private MavenProject createSucceededMavenProject( String artifactId ) + { + MavenProject project = createMavenProject( artifactId ); + executionResult.addBuildSummary( new BuildSuccess( project, 0 ) ); + return project; + } + + private MavenProject createFailedMavenProject( String artifactId ) + { + MavenProject project = createMavenProject( artifactId ); + executionResult.addBuildSummary( new BuildFailure( project, 0, new Exception() ) ); + executionResult.addException( new LifecycleExecutionException( "", project ) ); + return project; + } +} \ No newline at end of file diff --git a/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java new file mode 100644 index 0000000000..415b94698a --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java @@ -0,0 +1,78 @@ +package org.apache.maven.execution; + +/* + * 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 org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; + +@RunWith( MockitoJUnitRunner.class ) +public class DefaultBuildResumptionDataRepositoryTest +{ + private final DefaultBuildResumptionDataRepository repository = new DefaultBuildResumptionDataRepository(); + + @Test + public void resumeFromPropertyGetsApplied() + { + MavenExecutionRequest request = new DefaultMavenExecutionRequest(); + Properties properties = new Properties(); + properties.setProperty( "resumeFrom", ":module-a" ); + + repository.applyResumptionProperties( request, properties ); + + assertThat( request.getResumeFrom(), is( ":module-a" ) ); + } + + @Test + public void resumeFromPropertyDoesNotOverrideExistingRequestParameters() + { + MavenExecutionRequest request = new DefaultMavenExecutionRequest(); + request.setResumeFrom( ":module-b" ); + Properties properties = new Properties(); + properties.setProperty( "resumeFrom", ":module-a" ); + + repository.applyResumptionProperties( request, properties ); + + assertThat( request.getResumeFrom(), is( ":module-b" ) ); + } + + @Test + public void excludedProjectsFromPropertyGetsAddedToExistingRequestParameters() + { + MavenExecutionRequest request = new DefaultMavenExecutionRequest(); + List excludedProjects = new ArrayList<>(); + excludedProjects.add( ":module-a" ); + request.setExcludedProjects( excludedProjects ); + Properties properties = new Properties(); + properties.setProperty( "excludedProjects", ":module-b, :module-c" ); + + repository.applyResumptionProperties( request, properties ); + + assertThat( request.getExcludedProjects(), contains( ":module-a", ":module-b", ":module-c" ) ); + } +} diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/CLIManager.java b/maven-embedder/src/main/java/org/apache/maven/cli/CLIManager.java index c9e002a10e..873fc97620 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/CLIManager.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/CLIManager.java @@ -83,6 +83,8 @@ public class CLIManager public static final String FAIL_NEVER = "fn"; + public static final String RESUME = "r"; + public static final String RESUME_FROM = "rf"; public static final String PROJECT_LIST = "pl"; @@ -134,6 +136,7 @@ public class CLIManager options.addOption( Option.builder( FAIL_FAST ).longOpt( "fail-fast" ).desc( "Stop at first failure in reactorized builds" ).build() ); options.addOption( Option.builder( FAIL_AT_END ).longOpt( "fail-at-end" ).desc( "Only fail the build afterwards; allow all non-impacted builds to continue" ).build() ); options.addOption( Option.builder( FAIL_NEVER ).longOpt( "fail-never" ).desc( "NEVER fail the build, regardless of project result" ).build() ); + options.addOption( Option.builder( RESUME ).longOpt( "resume" ).desc( "Resume reactor from the last failed project, using the resume.properties file in the build directory " ).build() ); options.addOption( Option.builder( RESUME_FROM ).longOpt( "resume-from" ).hasArg().desc( "Resume reactor from specified project" ).build() ); options.addOption( Option.builder( PROJECT_LIST ).longOpt( "projects" ).desc( "Comma-delimited list of specified reactor projects to build instead of all projects. A project can be specified by [groupId]:artifactId or by its relative path" ).hasArg().build() ); options.addOption( Option.builder( ALSO_MAKE ).longOpt( "also-make" ).desc( "If project list is specified, also build projects required by the list" ).build() ); diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java index cbdf47fdb5..4136a1ea5f 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java @@ -48,6 +48,7 @@ import org.apache.maven.eventspy.internal.EventSpyDispatcher; import org.apache.maven.exception.DefaultExceptionHandler; import org.apache.maven.exception.ExceptionHandler; import org.apache.maven.exception.ExceptionSummary; +import org.apache.maven.execution.BuildResumptionDataRepository; import org.apache.maven.execution.DefaultMavenExecutionRequest; import org.apache.maven.execution.ExecutionListener; import org.apache.maven.execution.MavenExecutionRequest; @@ -168,6 +169,8 @@ public class MavenCli private Map configurationProcessors; + private BuildResumptionDataRepository buildResumptionDataRepository; + public MavenCli() { this( null ); @@ -705,6 +708,8 @@ public class MavenCli dispatcher = (DefaultSecDispatcher) container.lookup( SecDispatcher.class, "maven" ); + buildResumptionDataRepository = container.lookup( BuildResumptionDataRepository.class ); + return container; } @@ -996,7 +1001,8 @@ public class MavenCli if ( project == null && exception instanceof LifecycleExecutionException ) { - project = ( (LifecycleExecutionException) exception ).getProject(); + LifecycleExecutionException lifecycleExecutionException = (LifecycleExecutionException) exception; + project = lifecycleExecutionException.getProject(); } } @@ -1025,12 +1031,15 @@ public class MavenCli } } - if ( project != null && !project.equals( result.getTopologicallySortedProjects().get( 0 ) ) ) + List sortedProjects = result.getTopologicallySortedProjects(); + if ( result.canResume() ) { - slf4jLogger.error( "" ); - slf4jLogger.error( "After correcting the problems, you can resume the build with the command" ); - slf4jLogger.error( buffer().a( " " ).strong( "mvn -rf " - + getResumeFrom( result.getTopologicallySortedProjects(), project ) ).toString() ); + logBuildResumeHint( "mvn -r " ); + } + else if ( project != null && !project.equals( sortedProjects.get( 0 ) ) ) + { + String resumeFromSelector = getResumeFromSelector( sortedProjects, project ); + logBuildResumeHint( "mvn -rf " + resumeFromSelector ); } if ( MavenExecutionRequest.REACTOR_FAIL_NEVER.equals( cliRequest.request.getReactorFailureBehavior() ) ) @@ -1050,32 +1059,41 @@ public class MavenCli } } + private void logBuildResumeHint( String resumeBuildHint ) + { + slf4jLogger.error( "" ); + slf4jLogger.error( "After correcting the problems, you can resume the build with the command" ); + slf4jLogger.error( buffer().a( " " ).strong( resumeBuildHint ).toString() ); + } + /** - * A helper method to determine the value to resume the build with {@code -rf} taking into account the - * edge case where multiple modules in the reactor have the same artifactId. + * A helper method to determine the value to resume the build with {@code -rf} taking into account the edge case + * where multiple modules in the reactor have the same artifactId. *

- * {@code -rf :artifactId} will pick up the first module which matches, but when multiple modules in the - * reactor have the same artifactId, effective failed module might be later in build reactor. - * This means that developer will either have to type groupId or wait for build execution of all modules - * which were fine, but they are still before one which reported errors. + * {@code -rf :artifactId} will pick up the first module which matches, but when multiple modules in the reactor + * have the same artifactId, effective failed module might be later in build reactor. + * This means that developer will either have to type groupId or wait for build execution of all modules which + * were fine, but they are still before one which reported errors. *

Then the returned value is {@code groupId:artifactId} when there is a name clash and * {@code :artifactId} if there is no conflict. + * This method is made package-private for testing purposes. * * @param mavenProjects Maven projects which are part of build execution. * @param failedProject Project which has failed. - * @return Value for -rf flag to resume build exactly from place where it failed ({@code :artifactId} in - * general and {@code groupId:artifactId} when there is a name clash). + * @return Value for -rf flag to resume build exactly from place where it failed ({@code :artifactId} in general + * and {@code groupId:artifactId} when there is a name clash). */ - private String getResumeFrom( List mavenProjects, MavenProject failedProject ) + String getResumeFromSelector( List mavenProjects, MavenProject failedProject ) { - for ( MavenProject buildProject : mavenProjects ) + boolean hasOverlappingArtifactId = mavenProjects.stream() + .filter( project -> failedProject.getArtifactId().equals( project.getArtifactId() ) ) + .count() > 1; + + if ( hasOverlappingArtifactId ) { - if ( failedProject.getArtifactId().equals( buildProject.getArtifactId() ) && !failedProject.equals( - buildProject ) ) - { - return failedProject.getGroupId() + ":" + failedProject.getArtifactId(); - } + return failedProject.getGroupId() + ":" + failedProject.getArtifactId(); } + return ":" + failedProject.getArtifactId(); } @@ -1516,6 +1534,11 @@ public class MavenCli request.setBaseDirectory( request.getPom().getParentFile() ); } + if ( commandLine.hasOption( CLIManager.RESUME ) ) + { + request.setResume(); + } + if ( commandLine.hasOption( CLIManager.RESUME_FROM ) ) { request.setResumeFrom( commandLine.getOptionValue( CLIManager.RESUME_FROM ) ); diff --git a/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java b/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java index 173b78c78a..b0e536f2db 100644 --- a/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java +++ b/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java @@ -19,6 +19,9 @@ package org.apache.maven.cli; * under the License. */ +import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -31,10 +34,12 @@ import static org.mockito.Mockito.times; import java.io.File; import java.util.Collections; +import java.util.List; import org.apache.commons.cli.ParseException; import org.apache.maven.Maven; import org.apache.maven.eventspy.internal.EventSpyDispatcher; +import org.apache.maven.project.MavenProject; import org.apache.maven.shared.utils.logging.MessageUtils; import org.apache.maven.toolchain.building.ToolchainsBuildingRequest; import org.apache.maven.toolchain.building.ToolchainsBuildingResult; @@ -346,4 +351,37 @@ public class MavenCliTest orderdEventSpyDispatcherMock.verify(eventSpyDispatcherMock, times(1)).onEvent(any(ToolchainsBuildingResult.class)); } + @Test + public void resumeFromSelectorIsSuggestedWithoutGroupId() + { + List allProjects = asList( + createMavenProject( "group", "module-a" ), + createMavenProject( "group", "module-b" ) ); + MavenProject failedProject = allProjects.get( 0 ); + + String selector = cli.getResumeFromSelector( allProjects, failedProject ); + + assertThat( selector, is( ":module-a" ) ); + } + + @Test + public void resumeFromSelectorContainsGroupIdWhenArtifactIdIsNotUnique() + { + List allProjects = asList( + createMavenProject( "group-a", "module" ), + createMavenProject( "group-b", "module" ) ); + MavenProject failedProject = allProjects.get( 0 ); + + String selector = cli.getResumeFromSelector( allProjects, failedProject ); + + assertThat( selector, is( "group-a:module" ) ); + } + + private MavenProject createMavenProject( String groupId, String artifactId ) + { + MavenProject project = new MavenProject(); + project.setGroupId( groupId ); + project.setArtifactId( artifactId ); + return project; + } }