diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000000..407210823e --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,125 @@ +# 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. + +name: Java CI + +on: [push, pull_request] + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + fail-fast: false + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up cache for ~./m2/repository + uses: actions/cache@v1 + with: + path: ~/.m2/repository + key: maven-${{ matrix.os }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + maven-${{ matrix.os }}- + + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: 8 + + - name: Build with Maven + run: mvn verify -e -B -V -DdistributionFileName=apache-maven + + - name: Upload built Maven + uses: actions/upload-artifact@v2 + if: ${{ matrix.os == 'ubuntu-latest' }} + with: + name: built-maven + path: apache-maven/target/ + + - name: Upload built Maven Wrapper + uses: actions/upload-artifact@v2 + if: ${{ matrix.os == 'ubuntu-latest' }} + with: + name: built-maven-wrapper + path: maven-wrapper/target/maven-wrapper.jar + + integration-test: + needs: build + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + java: [8, 11, 14] + fail-fast: false + runs-on: ${{ matrix.os }} + + steps: + - name: Collect environment context variables + shell: bash + run: | + set +e + repo=maven-integration-testing + user=${GITHUB_REPOSITORY%/*} + branch=${GITHUB_REF#refs/heads/} + target_branch=master + target_user=apache + if [ $branch != "master" ]; then + git ls-remote https://github.com/$user/$repo.git | grep $GITHUB_REF > /dev/null + if [ $? -eq 0 ]; then + echo "Found a branch \"$branch\" in fork \"$user/$repo\", configuring this for the integration tests to be run against." + target_branch=$branch + target_user=$user + else + echo "Could not find fork \"$user/$repo\" or a branch \"$branch\" in this fork. Falling back to \"$target_branch\" in \"$target_user/$repo\"." + fi + else + echo "Integration tests will run against $target_user/$repo for master builds." + fi + echo "::set-env name=REPO_BRANCH::$target_branch" + echo "::set-env name=REPO_USER::$target_user" + + - name: Checkout maven-integration-testing + uses: actions/checkout@v2 + with: + repository: ${{ env.REPO_USER }}/maven-integration-testing + path: maven-integration-testing/ + ref: ${{ env.REPO_BRANCH }} + + - name: Download built Maven + uses: actions/download-artifact@v2 + with: + name: built-maven + path: built-maven/ + + - name: Download built Maven Wrapper + uses: actions/download-artifact@v2 + with: + name: built-maven-wrapper + path: built-maven-wrapper/ + + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + + - name: Running integration tests + shell: bash + run: mvn install -e -B -V -Prun-its,embedded -Dmaven.repo.local=$GITHUB_WORKSPACE/repo/ -DmavenDistro="$GITHUB_WORKSPACE/built-maven/apache-maven-bin.zip" -DwrapperDistroDir="$GITHUB_WORKSPACE/built-maven/" -DmavenWrapper="$GITHUB_WORKSPACE/built-maven-wrapper/maven-wrapper.jar" -f maven-integration-testing/pom.xml \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 278a19df1f..5786cea460 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -21,10 +21,10 @@ properties([buildDiscarder(logRotator(artifactNumToKeepStr: '5', numToKeepStr: e def buildOs = 'linux' def buildJdk = '8' -def buildMvn = '3.6.2' +def buildMvn = '3.6.3' def runITsOses = ['linux', 'windows'] def runITsJdks = ['8', '11', '14', '15'] -def runITsMvn = '3.6.2' +def runITsMvn = '3.6.3' def runITscommand = "mvn clean install -Prun-its,embedded -B -U -V" // -DmavenDistro=... -Dmaven.test.failure.ignore=true def tests diff --git a/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java b/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java index 987be48b3e..9a436f0158 100644 --- a/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java +++ b/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java @@ -530,8 +530,16 @@ public class ComparableVersion { return 0; // 1-0 = 1- (normalize) = 1 } - Item first = get( 0 ); - return first.compareTo( null ); + // Compare the entire list of items with null - not just the first one, MNG-6964 + for ( Item i : this ) + { + int result = i.compareTo( null ); + if ( result != 0 ) + { + return result; + } + } + return 0; } switch ( item.getType() ) { @@ -582,6 +590,32 @@ public class ComparableVersion } return buffer.toString(); } + + /** + * Return the contents in the same format that is used when you call toString() on a List. + */ + private String toListString() + { + StringBuilder buffer = new StringBuilder(); + buffer.append( "[" ); + for ( Item item : this ) + { + if ( buffer.length() > 1 ) + { + buffer.append( ", " ); + } + if ( item instanceof ListItem ) + { + buffer.append( ( (ListItem ) item ).toListString() ); + } + else + { + buffer.append( item ); + } + } + buffer.append( "]" ); + return buffer.toString(); + } } public ComparableVersion( String version ) @@ -768,7 +802,8 @@ public class ComparableVersion // CHECKSTYLE_ON: LineLength public static void main( String... args ) { - System.out.println( "Display parameters as parsed by Maven (in canonical form) and comparison result:" ); + System.out.println( "Display parameters as parsed by Maven (in canonical form and as a list of tokens) and" + + " comparison result:" ); if ( args.length == 0 ) { return; @@ -787,7 +822,7 @@ public class ComparableVersion + ( ( compare == 0 ) ? "==" : ( ( compare < 0 ) ? "<" : ">" ) ) + ' ' + version ); } - System.out.println( ( i++ ) + ". " + version + " == " + c.getCanonical() ); + System.out.println( ( i++ ) + ". " + version + " -> " + c.getCanonical() + " " + c.items.toListString() ); prev = c; } diff --git a/maven-artifact/src/test/java/org/apache/maven/artifact/versioning/ComparableVersionTest.java b/maven-artifact/src/test/java/org/apache/maven/artifact/versioning/ComparableVersionTest.java index 70fc1d8ecc..97fb46d55f 100644 --- a/maven-artifact/src/test/java/org/apache/maven/artifact/versioning/ComparableVersionTest.java +++ b/maven-artifact/src/test/java/org/apache/maven/artifact/versioning/ComparableVersionTest.java @@ -295,6 +295,21 @@ public class ComparableVersionTest checkVersionsArrayEqual( arr ); } + /** + * Test MNG-6964 edge cases + * for qualifiers that start with "-0.", which was showing A == C and B == C but A < B. + */ + public void testMng6964() + { + String a = "1-0.alpha"; + String b = "1-0.beta"; + String c = "1"; + + checkVersionsOrder( a, c ); // Now a < c, but before MNG-6964 they were equal + checkVersionsOrder( b, c ); // Now b < c, but before MNG-6964 they were equal + checkVersionsOrder( a, b ); // Should still be true + } + public void testLocaleIndependent() { Locale orig = Locale.getDefault(); 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 index 8889b8312a..b0e91db13c 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionData.java +++ b/maven-core/src/main/java/org/apache/maven/execution/BuildResumptionData.java @@ -20,6 +20,9 @@ package org.apache.maven.execution; */ import java.util.List; +import java.util.Optional; + +import static java.util.Collections.emptyList; /** * This class holds the information required to enable resuming a Maven build with {@code --resume}. @@ -32,7 +35,7 @@ public class BuildResumptionData private final String resumeFrom; /** - * List of projects to skip if the build would be resumed from {@link #resumeFrom}. + * List of projects to skip. */ private final List projectsToSkip; @@ -42,13 +45,23 @@ public class BuildResumptionData this.projectsToSkip = projectsToSkip; } - public String getResumeFrom() + /** + * Returns the project where the next build can resume from. + * This is usually the first failed project in the order of the reactor. + * @return An optional containing the group and artifact id of the project. It does not make sense to resume + * the build when the first project of the reactor has failed, so then it will return an empty optional. + */ + public Optional getResumeFrom() { - return this.resumeFrom; + return Optional.ofNullable( this.resumeFrom ); } + /** + * A list of projects which can be skipped in the next build. + * @return A list of group and artifact ids. Can be empty when no projects can be skipped. + */ public List getProjectsToSkip() { - return this.projectsToSkip; + return ( projectsToSkip != null ) ? projectsToSkip : emptyList(); } } 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 index 3d100dc891..ac7c1ff15a 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzer.java +++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzer.java @@ -22,6 +22,7 @@ package org.apache.maven.execution; import org.apache.maven.lifecycle.LifecycleExecutionException; import org.apache.maven.model.Dependency; import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,16 +57,31 @@ public class DefaultBuildResumptionAnalyzer implements BuildResumptionAnalyzer final MavenProject resumeFromProject = failedProjects.get( 0 ); + final String resumeFromSelector; + final List projectsToSkip; if ( isFailedProjectFirstInBuild( result, resumeFromProject ) ) { - LOGGER.info( "The first module in the build failed, resuming the build would not make sense." ); - return Optional.empty(); + // As the first module in the build failed, there is no need to specify this as the resumeFrom project. + resumeFromSelector = null; + projectsToSkip = determineProjectsToSkip( result, failedProjects, 0 ); + } + else + { + resumeFromSelector = resumeFromProject.getGroupId() + ":" + resumeFromProject.getArtifactId(); + List allProjects = result.getTopologicallySortedProjects(); + int resumeFromProjectIndex = allProjects.indexOf( resumeFromProject ); + projectsToSkip = determineProjectsToSkip( result, failedProjects, resumeFromProjectIndex + 1 ); } - final String resumeFromSelector = resumeFromProject.getGroupId() + ":" + resumeFromProject.getArtifactId(); - final List projectsToSkip = determineProjectsToSkip( result, failedProjects, resumeFromProject ); - - return Optional.of( new BuildResumptionData( resumeFromSelector, projectsToSkip ) ); + boolean canBuildBeResumed = StringUtils.isNotEmpty( resumeFromSelector ) || !projectsToSkip.isEmpty(); + if ( canBuildBeResumed ) + { + return Optional.of( new BuildResumptionData( resumeFromSelector, projectsToSkip ) ); + } + else + { + return Optional.empty(); + } } private boolean isFailedProjectFirstInBuild( final MavenExecutionResult result, final MavenProject failedProject ) @@ -82,6 +98,7 @@ public class DefaultBuildResumptionAnalyzer implements BuildResumptionAnalyzer .filter( LifecycleExecutionException.class::isInstance ) .map( LifecycleExecutionException.class::cast ) .map( LifecycleExecutionException::getProject ) + .filter( Objects::nonNull ) .sorted( comparing( sortedProjects::indexOf ) ) .collect( Collectors.toList() ); } @@ -92,16 +109,15 @@ public class DefaultBuildResumptionAnalyzer implements BuildResumptionAnalyzer * 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. + * @param startFromProjectIndex Start looking for projects which can be skipped from a certain index. * @return A list of projects which can be skipped in a later build. */ private List determineProjectsToSkip( MavenExecutionResult result, List failedProjects, - MavenProject resumeFromProject ) + int startFromProjectIndex ) { List allProjects = result.getTopologicallySortedProjects(); - int resumeFromProjectIndex = allProjects.indexOf( resumeFromProject ); - List remainingProjects = allProjects.subList( resumeFromProjectIndex + 1, allProjects.size() ); + List remainingProjects = allProjects.subList( startFromProjectIndex, allProjects.size() ); List failedProjectsGAList = failedProjects.stream() .map( GroupArtifactPair::new ) 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 index eea097f993..28862375ed 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java +++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java @@ -74,9 +74,15 @@ public class DefaultBuildResumptionDataRepository implements BuildResumptionData 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 ); + + buildResumptionData.getResumeFrom() + .ifPresent( resumeFrom -> properties.setProperty( RESUME_FROM_PROPERTY, resumeFrom ) ); + + if ( !buildResumptionData.getProjectsToSkip().isEmpty() ) + { + String excludedProjects = String.join( PROPERTY_DELIMITER, buildResumptionData.getProjectsToSkip() ); + properties.setProperty( EXCLUDED_PROJECTS_PROPERTY, excludedProjects ); + } return properties; } @@ -105,7 +111,7 @@ public class DefaultBuildResumptionDataRepository implements BuildResumptionData private Properties loadResumptionFile( Path rootBuildDirectory ) { Properties properties = new Properties(); - Path path = Paths.get( RESUME_PROPERTIES_FILENAME ).resolve( rootBuildDirectory ); + Path path = rootBuildDirectory.resolve( RESUME_PROPERTIES_FILENAME ); if ( !Files.exists( path ) ) { LOGGER.warn( "The {} file does not exist. The --resume / -r feature will not work.", path ); @@ -137,9 +143,12 @@ public class DefaultBuildResumptionDataRepository implements BuildResumptionData 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 ); + if ( !StringUtils.isEmpty( propertyValue ) ) + { + 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 9cbdebeddf..fd00df9872 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 @@ -607,9 +607,9 @@ public class DefaultMavenExecutionRequest } @Override - public MavenExecutionRequest setResume() + public MavenExecutionRequest setResume( boolean resume ) { - resume = true; + this.resume = resume; return this; } 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 542c34aade..1ccd7ecb23 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 @@ -173,9 +173,10 @@ public interface MavenExecutionRequest /** * Sets whether the build should be resumed from the data in the resume.properties file. + * @param resume Whether or not to resume a previous build. * @return This request, never {@code null}. */ - MavenExecutionRequest setResume(); + MavenExecutionRequest setResume( boolean resume ); /** * @return Whether the build should be resumed from the data in the resume.properties file. diff --git a/maven-core/src/main/resources/META-INF/maven/extension.xml b/maven-core/src/main/resources/META-INF/maven/extension.xml index 6bd8369273..67cedc0f49 100644 --- a/maven-core/src/main/resources/META-INF/maven/extension.xml +++ b/maven-core/src/main/resources/META-INF/maven/extension.xml @@ -30,6 +30,7 @@ under the License. org.apache.maven.exception org.apache.maven.execution org.apache.maven.execution.scope + org.apache.maven.graph org.apache.maven.lifecycle org.apache.maven.model org.apache.maven.monitor 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 index 59a8e63c14..6e037763e2 100644 --- a/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzerTest.java +++ b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionAnalyzerTest.java @@ -55,7 +55,7 @@ public class DefaultBuildResumptionAnalyzerTest Optional result = analyzer.determineBuildResumptionData( executionResult ); assertThat( result.isPresent(), is( true ) ); - assertThat( result.get().getResumeFrom(), is( "test:B" ) ); + assertThat( result.get().getResumeFrom(), is( Optional.of ( "test:B" ) ) ); } @Test @@ -111,7 +111,7 @@ public class DefaultBuildResumptionAnalyzerTest Optional result = analyzer.determineBuildResumptionData( executionResult ); assertThat( result.isPresent(), is( true ) ); - assertThat( result.get().getResumeFrom(), is( "test:B" ) ); + assertThat( result.get().getResumeFrom(), is( Optional.of ( "test:B" ) ) ); assertThat( result.get().getProjectsToSkip(), contains( "test:C" ) ); assertThat( result.get().getProjectsToSkip(), not( contains( "test:D" ) ) ); } 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 index 415b94698a..697c74f9a0 100644 --- a/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java +++ b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java @@ -19,6 +19,8 @@ package org.apache.maven.execution; * under the License. */ +import org.apache.maven.model.Build; +import org.apache.maven.project.MavenProject; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @@ -29,6 +31,7 @@ import java.util.Properties; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; @RunWith( MockitoJUnitRunner.class ) @@ -75,4 +78,31 @@ public class DefaultBuildResumptionDataRepositoryTest assertThat( request.getExcludedProjects(), contains( ":module-a", ":module-b", ":module-c" ) ); } + + @Test + public void excludedProjectsAreNotAddedWhenPropertyValueIsEmpty() + { + MavenExecutionRequest request = new DefaultMavenExecutionRequest(); + Properties properties = new Properties(); + properties.setProperty( "excludedProjects", "" ); + + repository.applyResumptionProperties( request, properties ); + + assertThat( request.getExcludedProjects(), is( empty() ) ); + } + + @Test + public void applyResumptionData_shouldLoadData() + { + MavenExecutionRequest request = new DefaultMavenExecutionRequest(); + Build build = new Build(); + build.setDirectory( "src/test/resources/org/apache/maven/execution/" ); + MavenProject rootProject = new MavenProject(); + rootProject.setBuild( build ); + + repository.applyResumptionData( request, rootProject ); + + assertThat( request.getResumeFrom(), is( "example:module-c" ) ); + assertThat( request.getExcludedProjects(), contains( "example:module-a", "example:module-b" ) ); + } } diff --git a/maven-core/src/test/resources/org/apache/maven/execution/resume.properties b/maven-core/src/test/resources/org/apache/maven/execution/resume.properties new file mode 100644 index 0000000000..26caeb53fc --- /dev/null +++ b/maven-core/src/test/resources/org/apache/maven/execution/resume.properties @@ -0,0 +1,2 @@ +resumeFrom=example:module-c +excludedProjects=example:module-a, example:module-b \ No newline at end of file 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 4136a1ea5f..b0d649081e 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,7 +48,6 @@ 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; @@ -116,6 +115,7 @@ import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.util.Comparator.comparing; import static org.apache.maven.cli.ResolveFile.resolveFile; import static org.apache.maven.shared.utils.logging.MessageUtils.buffer; @@ -169,8 +169,6 @@ public class MavenCli private Map configurationProcessors; - private BuildResumptionDataRepository buildResumptionDataRepository; - public MavenCli() { this( null ); @@ -708,8 +706,6 @@ public class MavenCli dispatcher = (DefaultSecDispatcher) container.lookup( SecDispatcher.class, "maven" ); - buildResumptionDataRepository = container.lookup( BuildResumptionDataRepository.class ); - return container; } @@ -991,7 +987,7 @@ public class MavenCli Map references = new LinkedHashMap<>(); - MavenProject project = null; + List failedProjects = new ArrayList<>(); for ( Throwable exception : result.getExceptions() ) { @@ -999,10 +995,9 @@ public class MavenCli logSummary( summary, references, "", cliRequest.showErrors ); - if ( project == null && exception instanceof LifecycleExecutionException ) + if ( exception instanceof LifecycleExecutionException ) { - LifecycleExecutionException lifecycleExecutionException = (LifecycleExecutionException) exception; - project = lifecycleExecutionException.getProject(); + failedProjects.add ( ( (LifecycleExecutionException) exception ).getProject() ); } } @@ -1031,15 +1026,23 @@ public class MavenCli } } - List sortedProjects = result.getTopologicallySortedProjects(); if ( result.canResume() ) { - logBuildResumeHint( "mvn -r " ); + logBuildResumeHint( "mvn -r" ); } - else if ( project != null && !project.equals( sortedProjects.get( 0 ) ) ) + else if ( !failedProjects.isEmpty() ) { - String resumeFromSelector = getResumeFromSelector( sortedProjects, project ); - logBuildResumeHint( "mvn -rf " + resumeFromSelector ); + List sortedProjects = result.getTopologicallySortedProjects(); + + // Sort the failedProjects list in the topologically sorted order. + failedProjects.sort( comparing( sortedProjects::indexOf ) ); + + MavenProject firstFailedProject = failedProjects.get( 0 ); + if ( !firstFailedProject.equals( sortedProjects.get( 0 ) ) ) + { + String resumeFromSelector = getResumeFromSelector( sortedProjects, firstFailedProject ); + logBuildResumeHint( "mvn -rf " + resumeFromSelector ); + } } if ( MavenExecutionRequest.REACTOR_FAIL_NEVER.equals( cliRequest.request.getReactorFailureBehavior() ) ) @@ -1079,22 +1082,22 @@ public class MavenCli * 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. + * @param firstFailedProject The first 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). */ - String getResumeFromSelector( List mavenProjects, MavenProject failedProject ) + String getResumeFromSelector( List mavenProjects, MavenProject firstFailedProject ) { boolean hasOverlappingArtifactId = mavenProjects.stream() - .filter( project -> failedProject.getArtifactId().equals( project.getArtifactId() ) ) + .filter( project -> firstFailedProject.getArtifactId().equals( project.getArtifactId() ) ) .count() > 1; if ( hasOverlappingArtifactId ) { - return failedProject.getGroupId() + ":" + failedProject.getArtifactId(); + return firstFailedProject.getGroupId() + ":" + firstFailedProject.getArtifactId(); } - return ":" + failedProject.getArtifactId(); + return ":" + firstFailedProject.getArtifactId(); } private void logSummary( ExceptionSummary summary, Map references, String indent, @@ -1536,7 +1539,7 @@ public class MavenCli if ( commandLine.hasOption( CLIManager.RESUME ) ) { - request.setResume(); + request.setResume( true ); } if ( commandLine.hasOption( CLIManager.RESUME_FROM ) )