Support -r / --resume option, fixes #351

This commit is contained in:
Guillaume Nodet
2021-05-19 09:27:14 +02:00
parent 7621dfe4c4
commit 3c9f787b00
7 changed files with 522 additions and 11 deletions

View File

@@ -25,6 +25,8 @@ import java.io.FileOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -100,6 +102,9 @@ import org.mvndaemon.mvnd.cache.invalidating.InvalidatingPluginArtifactsCache;
import org.mvndaemon.mvnd.cache.invalidating.InvalidatingPluginRealmCache;
import org.mvndaemon.mvnd.cache.invalidating.InvalidatingProjectArtifactsCache;
import org.mvndaemon.mvnd.common.Environment;
import org.mvndaemon.mvnd.execution.BuildResumptionPersistenceException;
import org.mvndaemon.mvnd.execution.DefaultBuildResumptionAnalyzer;
import org.mvndaemon.mvnd.execution.DefaultBuildResumptionDataRepository;
import org.mvndaemon.mvnd.logging.internal.Slf4jLoggerManager;
import org.mvndaemon.mvnd.logging.smart.BuildEventListener;
import org.mvndaemon.mvnd.logging.smart.LoggingExecutionListener;
@@ -113,6 +118,7 @@ import org.slf4j.LoggerFactory;
import org.sonatype.plexus.components.sec.dispatcher.DefaultSecDispatcher;
import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher;
import static java.util.Comparator.comparing;
import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
/**
@@ -142,6 +148,8 @@ public class DaemonMavenCli {
public static final String STYLE_COLOR_PROPERTY = "style.color";
public static final String RESUME = "r";
private final Slf4jLoggerManager plexusLoggerManager;
private final ILoggerFactory slf4jLoggerFactory;
@@ -260,7 +268,7 @@ public class DaemonMavenCli {
void cli(CliRequest cliRequest)
throws Exception {
CLIManager cliManager = new CLIManager();
CLIManager cliManager = newCLIManager();
List<String> args = new ArrayList<>();
CommandLine mavenConfig = null;
@@ -301,10 +309,16 @@ public class DaemonMavenCli {
private void help(CliRequest cliRequest) throws Exception {
if (cliRequest.commandLine.hasOption(CLIManager.HELP)) {
buildEventListener.log(MvndHelpFormatter.displayHelp(new CLIManager()));
buildEventListener.log(MvndHelpFormatter.displayHelp(newCLIManager()));
throw new ExitException(0);
}
}
private CLIManager newCLIManager() {
CLIManager cliManager = new CLIManager();
cliManager.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());
return cliManager;
}
private CommandLine cliMerge(CommandLine mavenArgs, CommandLine mavenConfig) {
@@ -739,15 +753,15 @@ public class DaemonMavenCli {
Map<String, String> references = new LinkedHashMap<>();
MavenProject project = null;
List<MavenProject> failedProjects = new ArrayList<>();
for (Throwable exception : result.getExceptions()) {
ExceptionSummary summary = handler.handleException(exception);
logSummary(summary, references, "", cliRequest.showErrors);
if (project == null && exception instanceof LifecycleExecutionException) {
project = ((LifecycleExecutionException) exception).getProject();
if (exception instanceof LifecycleExecutionException) {
failedProjects.add(((LifecycleExecutionException) exception).getProject());
}
}
@@ -772,11 +786,30 @@ public class DaemonMavenCli {
}
}
if (project != null && !project.equals(result.getTopologicallySortedProjects().get(0))) {
slf4jLogger.error("");
slf4jLogger.error("After correcting the problems, you can resume the build with the command");
slf4jLogger.error(buffer().a(" ").strong("mvn <args> -rf "
+ getResumeFrom(result.getTopologicallySortedProjects(), project)).toString());
boolean canResume = new DefaultBuildResumptionAnalyzer().determineBuildResumptionData(result).map(resumption -> {
try {
Path directory = Paths.get(request.getBaseDirectory()).resolve("target");
new DefaultBuildResumptionDataRepository().persistResumptionData(directory, resumption);
return true;
} catch (BuildResumptionPersistenceException e) {
slf4jLogger.warn("Could not persist build resumption data", e);
}
return false;
}).orElse(false);
if (canResume) {
logBuildResumeHint("mvn <args> -r");
} else if (!failedProjects.isEmpty()) {
List<MavenProject> 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 <args> -rf " + resumeFromSelector);
}
}
if (MavenExecutionRequest.REACTOR_FAIL_NEVER.equals(cliRequest.request.getReactorFailureBehavior())) {
@@ -787,10 +820,18 @@ public class DaemonMavenCli {
return 1;
}
} else {
Path directory = Paths.get(request.getBaseDirectory()).resolve("target");
new DefaultBuildResumptionDataRepository().removeResumptionData(directory);
return 0;
}
}
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.
@@ -808,7 +849,7 @@ public class DaemonMavenCli {
* @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<MavenProject> mavenProjects, MavenProject failedProject) {
private String getResumeFromSelector(List<MavenProject> mavenProjects, MavenProject failedProject) {
for (MavenProject buildProject : mavenProjects) {
if (failedProject.getArtifactId().equals(buildProject.getArtifactId()) && !failedProject.equals(
buildProject)) {
@@ -1172,6 +1213,11 @@ public class DaemonMavenCli {
request.setBaseDirectory(request.getPom().getParentFile());
}
if (commandLine.hasOption(RESUME)) {
new DefaultBuildResumptionDataRepository()
.applyResumptionData(request, Paths.get(request.getBaseDirectory()).resolve("target"));
}
if (commandLine.hasOption(CLIManager.RESUME_FROM)) {
request.setResumeFrom(commandLine.getOptionValue(CLIManager.RESUME_FROM));
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed 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.
*/
package org.mvndaemon.mvnd.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;
import org.apache.maven.execution.MavenExecutionResult;
/**
* 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<BuildResumptionData> determineBuildResumptionData(final MavenExecutionResult result);
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed 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.
*/
package org.mvndaemon.mvnd.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 list of projects that remain to be built.
*/
private final List<String> remainingProjects;
public BuildResumptionData(final List<String> remainingProjects) {
this.remainingProjects = remainingProjects;
}
/**
* Returns the projects that still need to be built when resuming.
*
* @return A list containing the group and artifact id of the projects.
*/
public List<String> getRemainingProjects() {
return this.remainingProjects;
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed 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.
*/
package org.mvndaemon.mvnd.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.execution.MavenExecutionRequest;
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 <code>false</code> 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);
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed 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.
*/
package org.mvndaemon.mvnd.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);
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed 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.
*/
package org.mvndaemon.mvnd.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;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.maven.execution.BuildFailure;
import org.apache.maven.execution.BuildSuccess;
import org.apache.maven.execution.MavenExecutionResult;
import org.apache.maven.project.MavenProject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Default implementation of {@link BuildResumptionAnalyzer}.
*/
@Named
@Singleton
public class DefaultBuildResumptionAnalyzer implements BuildResumptionAnalyzer {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultBuildResumptionAnalyzer.class);
@Override
public Optional<BuildResumptionData> determineBuildResumptionData(final MavenExecutionResult result) {
if (!result.hasExceptions()) {
return Optional.empty();
}
List<MavenProject> sortedProjects = result.getTopologicallySortedProjects();
boolean hasNoSuccess = sortedProjects.stream()
.noneMatch(project -> result.getBuildSummary(project) instanceof BuildSuccess);
if (hasNoSuccess) {
return Optional.empty();
}
List<String> remainingProjects = sortedProjects.stream()
.filter(project -> result.getBuildSummary(project) == null
|| result.getBuildSummary(project) instanceof BuildFailure)
.map(project -> project.getGroupId() + ":" + project.getArtifactId())
.collect(Collectors.toList());
if (remainingProjects.isEmpty()) {
LOGGER.info("No remaining projects found, resuming the build would not make sense.");
return Optional.empty();
}
return Optional.of(new BuildResumptionData(remainingProjects));
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed 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.
*/
package org.mvndaemon.mvnd.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.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.Properties;
import java.util.stream.Stream;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.project.MavenProject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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 REMAINING_PROJECTS = "remainingProjects";
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 {
Path directory = Paths.get(rootProject.getBuild().getDirectory());
persistResumptionData(directory, buildResumptionData);
}
public void persistResumptionData(Path directory, BuildResumptionData buildResumptionData)
throws BuildResumptionPersistenceException {
Properties properties = convertToProperties(buildResumptionData);
Path resumeProperties = directory.resolve(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();
String value = String.join(PROPERTY_DELIMITER, buildResumptionData.getRemainingProjects());
properties.setProperty(REMAINING_PROJECTS, value);
return properties;
}
@Override
public void applyResumptionData(MavenExecutionRequest request, MavenProject rootProject) {
Path directory = Paths.get(rootProject.getBuild().getDirectory());
applyResumptionData(request, directory);
}
public void applyResumptionData(MavenExecutionRequest request, Path directory) {
Properties properties = loadResumptionFile(directory);
applyResumptionProperties(request, properties);
}
@Override
public void removeResumptionData(MavenProject rootProject) {
Path directory = Paths.get(rootProject.getBuild().getDirectory());
removeResumptionData(directory);
}
public void removeResumptionData(Path directory) {
Path resumeProperties = directory.resolve(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 = rootBuildDirectory.resolve(RESUME_PROPERTIES_FILENAME);
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(REMAINING_PROJECTS)
&& StringUtils.isEmpty(request.getResumeFrom())) {
String propertyValue = properties.getProperty(REMAINING_PROJECTS);
Stream.of(propertyValue.split(PROPERTY_DELIMITER))
.filter(StringUtils::isNotEmpty)
.forEach(request.getSelectedProjects()::add);
LOGGER.info("Resuming from {} due to the --resume / -r feature.", propertyValue);
}
}
}