diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/ConsumerPomArtifactTransformer.java b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/ConsumerPomArtifactTransformer.java index 9444f972a9be..f32c96288657 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/ConsumerPomArtifactTransformer.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/ConsumerPomArtifactTransformer.java @@ -96,7 +96,7 @@ public void injectTransformedArtifacts(RepositorySystemSession session, MavenPro } } - TransformedArtifact createConsumerPomArtifact( + private TransformedArtifact createConsumerPomArtifact( MavenProject project, Path consumer, RepositorySystemSession session) { Path actual = project.getFile().toPath(); Path parent = project.getBaseDirectory(); diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java index c46d3d5b6d31..151c7b2ff3bd 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java @@ -37,6 +37,7 @@ import org.apache.maven.api.model.DistributionManagement; import org.apache.maven.api.model.Model; import org.apache.maven.api.model.ModelBase; +import org.apache.maven.api.model.Parent; import org.apache.maven.api.model.Profile; import org.apache.maven.api.model.Repository; import org.apache.maven.api.model.Scm; @@ -50,6 +51,7 @@ import org.apache.maven.impl.InternalSession; import org.apache.maven.model.v4.MavenModelVersion; import org.apache.maven.project.MavenProject; +import org.apache.maven.project.ProjectSourcesHelper; import org.eclipse.aether.RepositorySystemSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -342,7 +344,7 @@ static Model transformNonPom(Model model, MavenProject project) { return model; } - static Model transformBom(Model model, MavenProject project) { + private static Model transformBom(Model model, MavenProject project) { boolean preserveModelVersion = model.isPreserveModelVersion(); Model.Builder builder = prune( @@ -369,11 +371,25 @@ static Model transformPom(Model model, MavenProject project) { // raw to consumer transform model = model.withRoot(false).withModules(null).withSubprojects(null); - if (model.getParent() != null) { - model = model.withParent(model.getParent().withRelativePath(null)); + Parent parent = model.getParent(); + if (parent != null) { + model = model.withParent(parent.withRelativePath(null)); + } + var sources = new ProjectSourcesHelper(project); + if (sources.useModuleSourceHierarchy()) { + // Dependencies are dispatched by maven-jar-plugin in the POM generated for each module. + model = model.withDependencies(null); } - if (!preserveModelVersion) { + /* + * If tne contains elements, it is not compatible with the Maven 4.0.0 model. + * Remove the full element instead of removing only the element, because the + * build without sources does not mean much. Reminder: this removal can be disabled by setting + * the `preserveModelVersion` XML attribute or `preserve.model.version` property to true. + */ + if (sources.hasEnabledSources()) { + model = model.withBuild(null); + } model = model.withPreserveModelVersion(false); String modelVersion = new MavenModelVersion().getModelVersion(model); model = model.withModelVersion(modelVersion); @@ -381,7 +397,7 @@ static Model transformPom(Model model, MavenProject project) { return model; } - static void warnNotDowngraded(MavenProject project) { + private static void warnNotDowngraded(MavenProject project) { LOGGER.warn("The consumer POM for " + project.getId() + " cannot be downgraded to 4.0.0. " + "If you intent your build to be consumed with Maven 3 projects, you need to remove " + "the features that request a newer model version. If you're fine with having the " diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java index 9de2ca1348ff..f7a92aa190b5 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java @@ -655,7 +655,6 @@ private void initProject(MavenProject project, ModelBuilderResult result) { // only set those on 2nd phase, ignore on 1st pass if (project.getFile() != null) { Build build = project.getBuild().getDelegate(); - List sources = build.getSources(); Path baseDir = project.getBaseDirectory(); Function outputDirectory = (scope) -> { if (scope == ProjectScope.MAIN) { @@ -666,23 +665,11 @@ private void initProject(MavenProject project, ModelBuilderResult result) { return build.getDirectory(); } }; - // Extract modules from sources to detect modular projects - Set modules = extractModules(sources); - boolean isModularProject = !modules.isEmpty(); - - logger.trace( - "Module detection for project {}: found {} module(s) {} - modular project: {}.", - project.getId(), - modules.size(), - modules, - isModularProject); - // Create source handling context for unified tracking of all lang/scope combinations - SourceHandlingContext sourceContext = - new SourceHandlingContext(project, baseDir, modules, isModularProject, result); + final SourceHandlingContext sourceContext = new SourceHandlingContext(project, baseDir, result); // Process all sources, tracking enabled ones and detecting duplicates - for (var source : sources) { + for (org.apache.maven.api.model.Source source : sourceContext.sources) { var sourceRoot = DefaultSourceRoot.fromModel(session, baseDir, outputDirectory, source); // Track enabled sources for duplicate detection and hasSources() queries // Only add source if it's not a duplicate enabled source (first enabled wins) @@ -705,7 +692,7 @@ private void initProject(MavenProject project, ModelBuilderResult result) { if (!sourceContext.hasSources(Language.SCRIPT, ProjectScope.MAIN)) { project.addScriptSourceRoot(build.getScriptSourceDirectory()); } - if (isModularProject) { + if (sourceContext.useModuleSourceHierarchy()) { // Modular projects: unconditionally ignore legacy directories, warn if explicitly set warnIfExplicitLegacyDirectory( build.getSourceDirectory(), @@ -1171,22 +1158,6 @@ public Set> entrySet() { } } - /** - * Extracts unique module names from the given list of source elements. - * A project is considered modular if it has at least one module name. - * - * @param sources list of source elements from the build - * @return set of non-blank module names - */ - private static Set extractModules(List sources) { - return sources.stream() - .map(org.apache.maven.api.model.Source::getModule) - .filter(Objects::nonNull) - .map(String::trim) - .filter(s -> !s.isBlank()) - .collect(Collectors.toSet()); - } - private Model injectLifecycleBindings( Model model, ModelBuilderRequest request, diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/ProjectSourcesHelper.java b/impl/maven-core/src/main/java/org/apache/maven/project/ProjectSourcesHelper.java new file mode 100644 index 000000000000..6d9940f8ef3e --- /dev/null +++ b/impl/maven-core/src/main/java/org/apache/maven/project/ProjectSourcesHelper.java @@ -0,0 +1,103 @@ +/* + * 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. + */ +package org.apache.maven.project; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +import org.apache.maven.api.model.Source; + +/** + * Utility methods for analyzing the {@code } elements of a project. + * Warning: This is an internal utility class that is only public for technical reasons. + * It is not part of the public API. In particular, this class can be changed or deleted without + * prior notice. + */ +public class ProjectSourcesHelper { + /** + * All sources of the project. + */ + protected final Collection sources; + + /** + * Creates a new helper for the given project. + * + * @param project the Maven project from which to get the sources + */ + public ProjectSourcesHelper(final MavenProject project) { + sources = project.getBuild().getDelegate().getSources(); + } + + /** + * Returns whether the project declares at least one {@code } element which is enabled. + * This is regardless if the source declares a module or not. + * + * @return whether the project declares at least one {@code } element which is enabled + */ + public boolean hasEnabledSources() { + for (Source source : sources) { + if (source.isEnabled()) { + return true; + } + } + return false; + } + + /** + * Returns a stream of non-blank module names. + * The stream may contain duplicated values. + * This method does not filter disabled sources. + * + * @return a stream of non-blank module names + */ + private Stream streamOfModuleNames() { + return sources.stream() + .map(org.apache.maven.api.model.Source::getModule) + .filter(Objects::nonNull) + .map(String::trim) + .filter(s -> !s.isBlank()); + } + + /** + * Extracts unique module names from the list of source elements. + * A project uses module source hierarchy if it has at least one module name. + * + * @return set of non-blank module names in declaration order + */ + public Set getModuleNames() { + var modules = new LinkedHashSet(); // Preferred to `Collectors.toSet()` for preserving order. + streamOfModuleNames().forEach(modules::add); + return modules; + } + + /** + * Whether the project uses module source hierarchy. This method returns {@code true} it at least one + * {@code } element declares a Java modules. While modular and non-modular sources should not be mixed, + * this code is tolerant to such mixes because non-modular source elements may have been incorrectly generated + * by non module-aware codes. + * + * @return whether the project uses module source hierarchy + */ + public boolean useModuleSourceHierarchy() { + return streamOfModuleNames().findAny().isPresent(); + } +} diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java b/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java index e7691eb86bcf..4e1e403e1ec1 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java @@ -37,9 +37,7 @@ /** * Handles source configuration for Maven projects with unified tracking for all language/scope combinations. - *

- * This class replaces the previous approach of hardcoded boolean flags (hasMain, hasTest, etc.) - * with a flexible set-based tracking mechanism that works for any language and scope combination. + * This class uses a flexible set-based tracking mechanism that works for any language and scope combination. *

* Key features: *

    @@ -51,7 +49,7 @@ * * @since 4.0.0 */ -class SourceHandlingContext { +final class SourceHandlingContext extends ProjectSourcesHelper { private static final Logger LOGGER = LoggerFactory.getLogger(SourceHandlingContext.class); @@ -67,19 +65,30 @@ record SourceKey(Language language, ProjectScope scope, String module, Path dire private final ModelBuilderResult result; private final Set declaredSources; - SourceHandlingContext( - MavenProject project, - Path baseDir, - Set modules, - boolean modularProject, - ModelBuilderResult result) { + SourceHandlingContext(MavenProject project, Path baseDir, ModelBuilderResult result) { + super(project); this.project = project; this.baseDir = baseDir; - this.modules = modules; - this.modularProject = modularProject; + this.modules = getModuleNames(); + this.modularProject = !modules.isEmpty(); this.result = result; // Each module typically has main, test, main resources, test resources = 4 sources this.declaredSources = new HashSet<>(4 * modules.size()); + LOGGER.trace( + "Module detection for project {}: found {} module(s) {} - modular project: {}.", + project.getId(), + modules.size(), + modules, + modularProject); + } + + /** + * Whether the project uses module source hierarchy. + * Overridden for returning the cached value. + */ + @Override + public boolean useModuleSourceHierarchy() { + return modularProject; } /** diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java index 11dc8cd9c7ef..7862298b3310 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java @@ -51,6 +51,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -88,15 +89,22 @@ protected List getSessionServices() { return services; } - @Test - void testTrivialConsumer() throws Exception { - InternalMavenSession.from(InternalSession.from(session)) + /** + * Configures {@link #session} with the root directory of a test in {@code src/test/resources/consumer}. + * Returns the request in case the caller wants to apply more configuration. + */ + private MavenExecutionRequest setRootDirectory(String test) { + MavenExecutionRequest request = InternalMavenSession.from(InternalSession.from(session)) .getMavenSession() - .getRequest() - .setRootDirectory(Paths.get("src/test/resources/consumer/trivial")); - - Path file = Paths.get("src/test/resources/consumer/trivial/child/pom.xml"); + .getRequest(); + request.setRootDirectory(Paths.get("src/test/resources/consumer", test)); + return request; + } + /** + * Builds the effective model for the given {@code pom.xml} file. + */ + private MavenProject getEffectiveModel(Path file) { ModelBuilder.ModelBuilderSession mbs = modelBuilder.newSession(); InternalSession.from(session).getData().set(SessionData.key(ModelBuilder.ModelBuilderSession.class), mbs); Model orgModel = mbs.build(ModelBuilderRequest.builder() @@ -108,39 +116,50 @@ void testTrivialConsumer() throws Exception { MavenProject project = new MavenProject(orgModel); project.setOriginalModel(new org.apache.maven.model.Model(orgModel)); + return project; + } + + @Test + void testTrivialConsumer() throws Exception { + setRootDirectory("trivial"); + Path file = Paths.get("src/test/resources/consumer/trivial/child/pom.xml"); + + MavenProject project = getEffectiveModel(file); Model model = builder.build(session, project, Sources.buildSource(file)); assertNotNull(model); + assertNotNull(model.getDependencies()); } @Test void testSimpleConsumer() throws Exception { - MavenExecutionRequest request = InternalMavenSession.from(InternalSession.from(session)) - .getMavenSession() - .getRequest(); - request.setRootDirectory(Paths.get("src/test/resources/consumer/simple")); + MavenExecutionRequest request = setRootDirectory("simple"); request.getUserProperties().setProperty("changelist", "MNG6957"); - Path file = Paths.get("src/test/resources/consumer/simple/simple-parent/simple-weather/pom.xml"); - ModelBuilder.ModelBuilderSession mbs = modelBuilder.newSession(); - InternalSession.from(session).getData().set(SessionData.key(ModelBuilder.ModelBuilderSession.class), mbs); - Model orgModel = mbs.build(ModelBuilderRequest.builder() - .session(InternalSession.from(session)) - .source(Sources.buildSource(file)) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .build()) - .getEffectiveModel(); - - MavenProject project = new MavenProject(orgModel); - project.setOriginalModel(new org.apache.maven.model.Model(orgModel)); + MavenProject project = getEffectiveModel(file); request.setRootDirectory(Paths.get("src/test/resources/consumer/simple")); Model model = builder.build(session, project, Sources.buildSource(file)); assertNotNull(model); + assertFalse(model.getDependencies().isEmpty()); assertTrue(model.getProfiles().isEmpty()); } + @Test + void testMultiModuleConsumer() throws Exception { + setRootDirectory("multi-module"); + Path file = Paths.get("src/test/resources/consumer/multi-module/pom.xml"); + + MavenProject project = getEffectiveModel(file); + Model model = builder.build(session, project, Sources.buildSource(file)); + + assertNotNull(model); + assertNull(model.getBuild()); + assertTrue(model.getDependencies().isEmpty()); + assertFalse(model.getDependencyManagement().getDependencies().isEmpty()); + } + @Test void testScmInheritance() throws Exception { Model model = Model.newBuilder() diff --git a/impl/maven-core/src/test/resources/consumer/multi-module/pom.xml b/impl/maven-core/src/test/resources/consumer/multi-module/pom.xml new file mode 100644 index 000000000000..972e7c9be2b4 --- /dev/null +++ b/impl/maven-core/src/test/resources/consumer/multi-module/pom.xml @@ -0,0 +1,41 @@ + + org.my.group + parent + 1.0-SNAPSHOT + pom + + + + + org.slf4j + slf4j-api + 2.0.9 + + + + + + + org.slf4j + slf4j-api + + + org.junit.jupiter + junit-jupiter-api + 5.10.1 + test + + + + + + + org.foo + + + org.foo.bar + + + + +