This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/master by this push: new c1a900190 [MNG-7629] Change reactor reader to copy packaged artifacts and reuse them across builds if needed (#954) c1a900190 is described below commit c1a900190f6e0fccf23889cb32f701459babfb7f Author: Guillaume Nodet <gno...@gmail.com> AuthorDate: Thu Jan 19 11:55:20 2023 +0100 [MNG-7629] Change reactor reader to copy packaged artifacts and reuse them across builds if needed (#954) --- .../main/java/org/apache/maven/ReactorReader.java | 393 +++++++++++++++------ 1 file changed, 287 insertions(+), 106 deletions(-) diff --git a/maven-core/src/main/java/org/apache/maven/ReactorReader.java b/maven-core/src/main/java/org/apache/maven/ReactorReader.java index 7e6551f40..6228dd090 100644 --- a/maven-core/src/main/java/org/apache/maven/ReactorReader.java +++ b/maven-core/src/main/java/org/apache/maven/ReactorReader.java @@ -20,41 +20,42 @@ package org.apache.maven; import javax.inject.Inject; import javax.inject.Named; +import javax.inject.Singleton; import java.io.File; import java.io.IOException; +import java.nio.file.DirectoryNotEmptyException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; +import java.util.Deque; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Predicate; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.maven.artifact.ArtifactUtils; +import org.apache.maven.eventspy.EventSpy; +import org.apache.maven.execution.ExecutionEvent; import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Model; import org.apache.maven.project.MavenProject; +import org.apache.maven.project.artifact.ProjectArtifact; import org.apache.maven.repository.internal.MavenWorkspaceReader; +import org.codehaus.plexus.PlexusContainer; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.repository.WorkspaceRepository; import org.eclipse.aether.util.artifact.ArtifactIdUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toMap; - /** * An implementation of a workspace reader that knows how to search the Maven reactor for artifacts, either as packaged * jar if it has been built, or only compile output directory if packaging hasn't happened yet. @@ -66,30 +67,25 @@ import static java.util.stream.Collectors.toMap; class ReactorReader implements MavenWorkspaceReader { public static final String HINT = "reactor"; + public static final String PROJECT_LOCAL_REPO = "project-local-repo"; + private static final Collection<String> COMPILE_PHASE_TYPES = Arrays.asList("jar", "ejb-client", "war", "rar", "ejb3", "par", "sar", "wsr", "har", "app-client"); private static final Logger LOGGER = LoggerFactory.getLogger(ReactorReader.class); private final MavenSession session; - private final Map<String, MavenProject> projectsByGAV; - private final Map<String, List<MavenProject>> projectsByGA; private final WorkspaceRepository repository; - - private Function<MavenProject, String> projectIntoKey = - s -> ArtifactUtils.key(s.getGroupId(), s.getArtifactId(), s.getVersion()); - - private Function<MavenProject, String> projectIntoVersionlessKey = - s -> ArtifactUtils.versionlessKey(s.getGroupId(), s.getArtifactId()); + // groupId -> (artifactId -> (version -> project))) + private Map<String, Map<String, Map<String, MavenProject>>> projects; + private Path projectLocalRepository; + // projectId -> Deque<lifecycle> + private final Map<String, Deque<String>> lifecycles = new ConcurrentHashMap<>(); @Inject ReactorReader(MavenSession session) { this.session = session; - this.projectsByGAV = session.getAllProjects().stream().collect(toMap(projectIntoKey, identity())); - - this.projectsByGA = projectsByGAV.values().stream().collect(groupingBy(projectIntoVersionlessKey)); - - repository = new WorkspaceRepository("reactor", new HashSet<>(projectsByGAV.keySet())); + this.repository = new WorkspaceRepository("reactor", null); } // @@ -101,34 +97,39 @@ class ReactorReader implements MavenWorkspaceReader { } public File findArtifact(Artifact artifact) { - String projectKey = ArtifactUtils.key(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()); - - MavenProject project = projectsByGAV.get(projectKey); + MavenProject project = getProject(artifact); if (project != null) { - File file = find(project, artifact); + File file = findArtifact(project, artifact); if (file == null && project != project.getExecutionProject()) { - file = find(project.getExecutionProject(), artifact); + file = findArtifact(project.getExecutionProject(), artifact); } return file; } + // No project, but most certainly a dependency which has been built previously + File packagedArtifactFile = findInProjectLocalRepository(artifact); + if (packagedArtifactFile != null && packagedArtifactFile.exists()) { + return packagedArtifactFile; + } + return null; } public List<String> findVersions(Artifact artifact) { - String key = ArtifactUtils.versionlessKey(artifact.getGroupId(), artifact.getArtifactId()); - - return Optional.ofNullable(projectsByGA.get(key)).orElse(Collections.emptyList()).stream() - .filter(s -> Objects.nonNull(find(s, artifact))) + return getProjects() + .getOrDefault(artifact.getGroupId(), Collections.emptyMap()) + .getOrDefault(artifact.getArtifactId(), Collections.emptyMap()) + .values() + .stream() + .filter(p -> Objects.nonNull(findArtifact(p, artifact))) .map(MavenProject::getVersion) .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); } @Override public Model findModel(Artifact artifact) { - String projectKey = ArtifactUtils.key(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()); - MavenProject project = projectsByGAV.get(projectKey); + MavenProject project = getProject(artifact); return project == null ? null : project.getModel(); } @@ -136,23 +137,31 @@ class ReactorReader implements MavenWorkspaceReader { // Implementation // - private File find(MavenProject project, Artifact artifact) { + private File findArtifact(MavenProject project, Artifact artifact) { + // POMs are always returned from the file system if ("pom".equals(artifact.getExtension())) { return project.getFile(); } - Artifact projectArtifact = findMatchingArtifact(project, artifact); - File packagedArtifactFile = determinePreviouslyPackagedArtifactFile(project, projectArtifact); - - if (hasArtifactFileFromPackagePhase(projectArtifact)) { - return projectArtifact.getFile(); - } - // Check whether an earlier Maven run might have produced an artifact that is still on disk. - else if (packagedArtifactFile != null + // First check in the project local repository + File packagedArtifactFile = findInProjectLocalRepository(artifact); + if (packagedArtifactFile != null && packagedArtifactFile.exists() - && isPackagedArtifactUpToDate(project, packagedArtifactFile, artifact)) { + && isPackagedArtifactUpToDate(project, packagedArtifactFile)) { return packagedArtifactFile; - } else if (!hasBeenPackagedDuringThisSession(project)) { + } + + // Get the matching artifact from the project + Artifact projectArtifact = findMatchingArtifact(project, artifact); + if (projectArtifact != null) { + // If the artifact has been associated to a file, use it + packagedArtifactFile = projectArtifact.getFile(); + if (packagedArtifactFile != null && packagedArtifactFile.exists()) { + return packagedArtifactFile; + } + } + + if (!hasBeenPackagedDuringThisSession(project)) { // fallback to loose class files only if artifacts haven't been packaged yet // and only for plain old jars. Not war files, not ear files, not anything else. return determineBuildOutputDirectoryForArtifact(project, artifact); @@ -192,22 +201,7 @@ class ReactorReader implements MavenWorkspaceReader { return null; } - private File determinePreviouslyPackagedArtifactFile(MavenProject project, Artifact artifact) { - if (artifact == null) { - return null; - } - - String fileName = String.format("%s.%s", project.getBuild().getFinalName(), artifact.getExtension()); - return new File(project.getBuild().getDirectory(), fileName); - } - - private boolean hasArtifactFileFromPackagePhase(Artifact projectArtifact) { - return projectArtifact != null - && projectArtifact.getFile() != null - && projectArtifact.getFile().exists(); - } - - private boolean isPackagedArtifactUpToDate(MavenProject project, File packagedArtifactFile, Artifact artifact) { + private boolean isPackagedArtifactUpToDate(MavenProject project, File packagedArtifactFile) { Path outputDirectory = Paths.get(project.getBuild().getOutputDirectory()); if (!outputDirectory.toFile().exists()) { return true; @@ -226,10 +220,7 @@ class ReactorReader implements MavenWorkspaceReader { } } - Iterator<Path> iterator = outputFiles.iterator(); - while (iterator.hasNext()) { - Path outputFile = iterator.next(); - + for (Path outputFile : (Iterable<Path>) outputFiles::iterator) { if (Files.isDirectory(outputFile)) { continue; } @@ -237,21 +228,12 @@ class ReactorReader implements MavenWorkspaceReader { long outputFileLastModified = Files.getLastModifiedTime(outputFile).toMillis(); if (outputFileLastModified > artifactLastModified) { - File alternative = determineBuildOutputDirectoryForArtifact(project, artifact); - if (alternative != null) { - LOGGER.warn( - "File '{}' is more recent than the packaged artifact for '{}'; using '{}' instead", - relativizeOutputFile(outputFile), - project.getArtifactId(), - relativizeOutputFile(alternative.toPath())); - } else { - LOGGER.warn( - "File '{}' is more recent than the packaged artifact for '{}'; " - + "cannot use the build output directory for this type of artifact", - relativizeOutputFile(outputFile), - project.getArtifactId()); - } - return false; + LOGGER.warn( + "File '{}' is more recent than the packaged artifact for '{}', " + + "please run a full `mvn package` build", + relativizeOutputFile(outputFile), + project.getArtifactId()); + return true; } } @@ -267,9 +249,22 @@ class ReactorReader implements MavenWorkspaceReader { } private boolean hasBeenPackagedDuringThisSession(MavenProject project) { - return project.hasLifecyclePhase("package") - || project.hasLifecyclePhase("install") - || project.hasLifecyclePhase("deploy"); + boolean packaged = false; + for (String phase : getLifecycles(project)) { + switch (phase) { + case "clean": + packaged = false; + break; + case "package": + case "install": + case "deploy": + packaged = true; + break; + default: + break; + } + } + return packaged; } private Path relativizeOutputFile(final Path outputFile) { @@ -287,33 +282,13 @@ class ReactorReader implements MavenWorkspaceReader { */ private Artifact findMatchingArtifact(MavenProject project, Artifact requestedArtifact) { String requestedRepositoryConflictId = ArtifactIdUtils.toVersionlessId(requestedArtifact); - - Artifact mainArtifact = RepositoryUtils.toArtifact(project.getArtifact()); - if (requestedRepositoryConflictId.equals(ArtifactIdUtils.toVersionlessId(mainArtifact))) { - return mainArtifact; - } - - return RepositoryUtils.toArtifacts(project.getAttachedArtifacts()).stream() - .filter(isRequestedArtifact(requestedArtifact)) + return getProjectArtifacts(project) + .filter(artifact -> + Objects.equals(requestedRepositoryConflictId, ArtifactIdUtils.toVersionlessId(artifact))) .findFirst() .orElse(null); } - /** - * We are taking as much as we can from the DefaultArtifact.equals(). The requested artifact has no file, so we want - * to remove that from the comparison. - * - * @param requestArtifact checked against the given artifact. - * @return true if equals, false otherwise. - */ - private Predicate<Artifact> isRequestedArtifact(Artifact requestArtifact) { - return s -> s.getArtifactId().equals(requestArtifact.getArtifactId()) - && s.getGroupId().equals(requestArtifact.getGroupId()) - && s.getVersion().equals(requestArtifact.getVersion()) - && s.getExtension().equals(requestArtifact.getExtension()) - && s.getClassifier().equals(requestArtifact.getClassifier()); - } - /** * Determines whether the specified artifact refers to test classes. * @@ -324,4 +299,210 @@ class ReactorReader implements MavenWorkspaceReader { return ("test-jar".equals(artifact.getProperty("type", ""))) || ("jar".equals(artifact.getExtension()) && "tests".equals(artifact.getClassifier())); } + + private File findInProjectLocalRepository(Artifact artifact) { + Path target = getArtifactPath(artifact); + return Files.isRegularFile(target) ? target.toFile() : null; + } + + /** + * We are interested in project success events, in which case we call + * the {@link #installIntoProjectLocalRepository(MavenProject)} method. + * The mojo started event is also captured to determine the lifecycle + * phases the project has been through. + * + * @param event the execution event + */ + private void processEvent(ExecutionEvent event) { + MavenProject project = event.getProject(); + switch (event.getType()) { + case MojoStarted: + String phase = event.getMojoExecution().getLifecyclePhase(); + Deque<String> phases = getLifecycles(project); + if (!Objects.equals(phase, phases.peekLast())) { + phases.addLast(phase); + if ("clean".equals(phase)) { + cleanProjectLocalRepository(project); + } + } + break; + case ProjectSucceeded: + case ForkedProjectSucceeded: + installIntoProjectLocalRepository(project); + break; + default: + break; + } + } + + private Deque<String> getLifecycles(MavenProject project) { + return lifecycles.computeIfAbsent(project.getId(), k -> new ArrayDeque<>()); + } + + /** + * Copy packaged and attached artifacts from this project to the + * project local repository. + * This allows a subsequent build to resume while still being able + * to locate attached artifacts. + * + * @param project the project to copy artifacts from + */ + private void installIntoProjectLocalRepository(MavenProject project) { + if ("pom".equals(project.getPackaging()) + && !"clean".equals(getLifecycles(project).peekLast()) + || hasBeenPackagedDuringThisSession(project)) { + getProjectArtifacts(project).filter(this::isRegularFile).forEach(this::installIntoProjectLocalRepository); + } + } + + private void cleanProjectLocalRepository(MavenProject project) { + try { + Path artifactPath = getProjectLocalRepo() + .resolve(project.getGroupId()) + .resolve(project.getArtifactId()) + .resolve(project.getVersion()); + if (Files.isDirectory(artifactPath)) { + try (Stream<Path> paths = Files.list(artifactPath)) { + for (Path path : (Iterable<Path>) paths::iterator) { + Files.delete(path); + } + } + try { + Files.delete(artifactPath); + Files.delete(artifactPath.getParent()); + Files.delete(artifactPath.getParent().getParent()); + } catch (DirectoryNotEmptyException e) { + // ignore + } + } + } catch (IOException e) { + LOGGER.error("Error while cleaning project local repository", e); + } + } + + /** + * Retrieve a stream of the project's artifacts + */ + private Stream<Artifact> getProjectArtifacts(MavenProject project) { + Stream<org.apache.maven.artifact.Artifact> artifacts = Stream.concat( + Stream.concat( + // pom artifact + Stream.of(new ProjectArtifact(project)), + // main project artifact if not a pom + "pom".equals(project.getPackaging()) ? Stream.empty() : Stream.of(project.getArtifact())), + // attached artifacts + project.getAttachedArtifacts().stream()); + return artifacts.map(RepositoryUtils::toArtifact); + } + + private boolean isRegularFile(Artifact artifact) { + return artifact.getFile() != null && artifact.getFile().isFile(); + } + + private void installIntoProjectLocalRepository(Artifact artifact) { + Path target = getArtifactPath(artifact); + try { + LOGGER.info("Copying {} to project local repository", artifact); + Files.createDirectories(target.getParent()); + Files.copy( + artifact.getFile().toPath(), + target, + StandardCopyOption.REPLACE_EXISTING, + StandardCopyOption.COPY_ATTRIBUTES); + } catch (IOException e) { + LOGGER.error("Error while copying artifact to project local repository", e); + } + } + + private Path getArtifactPath(Artifact artifact) { + String groupId = artifact.getGroupId(); + String artifactId = artifact.getArtifactId(); + String version = artifact.getBaseVersion(); + String classifier = artifact.getClassifier(); + String extension = artifact.getExtension(); + Path repo = getProjectLocalRepo(); + return repo.resolve(groupId) + .resolve(artifactId) + .resolve(version) + .resolve(artifactId + + "-" + version + + (classifier != null && !classifier.isEmpty() ? "-" + classifier : "") + + "." + extension); + } + + private Path getProjectLocalRepo() { + if (projectLocalRepository == null) { + Path root = session.getRequest().getMultiModuleProjectDirectory().toPath(); + List<MavenProject> projects = session.getProjects(); + if (projects != null) { + projectLocalRepository = projects.stream() + .filter(project -> Objects.equals(root.toFile(), project.getBasedir())) + .findFirst() + .map(project -> project.getBuild().getDirectory()) + .map(Paths::get) + .orElseGet(() -> root.resolve("target")) + .resolve(PROJECT_LOCAL_REPO); + } else { + return root.resolve("target").resolve(PROJECT_LOCAL_REPO); + } + } + return projectLocalRepository; + } + + private MavenProject getProject(Artifact artifact) { + return getProjects() + .getOrDefault(artifact.getGroupId(), Collections.emptyMap()) + .getOrDefault(artifact.getArtifactId(), Collections.emptyMap()) + .getOrDefault(artifact.getBaseVersion(), null); + } + + // groupId -> (artifactId -> (version -> project))) + private Map<String, Map<String, Map<String, MavenProject>>> getProjects() { + // compute the projects mapping + if (projects == null) { + List<MavenProject> allProjects = session.getAllProjects(); + if (allProjects != null) { + Map<String, Map<String, Map<String, MavenProject>>> map = new HashMap<>(); + allProjects.forEach(project -> map.computeIfAbsent(project.getGroupId(), k -> new HashMap<>()) + .computeIfAbsent(project.getArtifactId(), k -> new HashMap<>()) + .put(project.getVersion(), project)); + this.projects = map; + } else { + return Collections.emptyMap(); + } + } + return projects; + } + + /** + * Singleton class used to receive events by implementing the EventSpy. + * It simply forwards all {@code ExecutionEvent}s to the {@code ReactorReader}. + */ + @Named + @Singleton + @SuppressWarnings("unused") + static class ReactorReaderSpy implements EventSpy { + + final PlexusContainer container; + + @Inject + ReactorReaderSpy(PlexusContainer container) { + this.container = container; + } + + @Override + public void init(Context context) throws Exception {} + + @Override + @SuppressWarnings("checkstyle:MissingSwitchDefault") + public void onEvent(Object event) throws Exception { + if (event instanceof ExecutionEvent) { + ReactorReader reactorReader = container.lookup(ReactorReader.class); + reactorReader.processEvent((ExecutionEvent) event); + } + } + + @Override + public void close() throws Exception {} + } }