Skip to content

Commit 73dc373

Browse files
Consumer POM of multi-module project should exclude <build> and <dependencies> elements (#11639)
* Move some of the codes needed by `ProjectSourcesHelper` in an utility class that we can reuse in other packages. * Consumer POM of multi-module project should exclude <build> and <dependencies> elements. * Add a test that verifies that `<build>` is preserved when `preserveModelVersion=true`. Co-authored-by: Gerd Aschemann <gerd@aschemann.net>
1 parent e95fc6e commit 73dc373

7 files changed

Lines changed: 281 additions & 112 deletions

File tree

impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/ConsumerPomArtifactTransformer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public void injectTransformedArtifacts(RepositorySystemSession session, MavenPro
9696
}
9797
}
9898

99-
TransformedArtifact createConsumerPomArtifact(
99+
private TransformedArtifact createConsumerPomArtifact(
100100
MavenProject project, Path consumer, RepositorySystemSession session) {
101101
Path actual = project.getFile().toPath();
102102
Path parent = project.getBaseDirectory();

impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.apache.maven.api.model.DistributionManagement;
3838
import org.apache.maven.api.model.Model;
3939
import org.apache.maven.api.model.ModelBase;
40+
import org.apache.maven.api.model.Parent;
4041
import org.apache.maven.api.model.Profile;
4142
import org.apache.maven.api.model.Repository;
4243
import org.apache.maven.api.model.Scm;
@@ -49,6 +50,7 @@
4950
import org.apache.maven.impl.InternalSession;
5051
import org.apache.maven.model.v4.MavenModelVersion;
5152
import org.apache.maven.project.MavenProject;
53+
import org.apache.maven.project.SourceQueries;
5254
import org.eclipse.aether.RepositorySystemSession;
5355
import org.slf4j.Logger;
5456
import org.slf4j.LoggerFactory;
@@ -287,7 +289,7 @@ static Model transformNonPom(Model model, MavenProject project) {
287289
return model;
288290
}
289291

290-
static Model transformBom(Model model, MavenProject project) {
292+
private static Model transformBom(Model model, MavenProject project) {
291293
boolean preserveModelVersion = model.isPreserveModelVersion();
292294

293295
Model.Builder builder = prune(
@@ -314,19 +316,33 @@ static Model transformPom(Model model, MavenProject project) {
314316

315317
// raw to consumer transform
316318
model = model.withRoot(false).withModules(null).withSubprojects(null);
317-
if (model.getParent() != null) {
318-
model = model.withParent(model.getParent().withRelativePath(null));
319+
Parent parent = model.getParent();
320+
if (parent != null) {
321+
model = model.withParent(parent.withRelativePath(null));
322+
}
323+
var projectSources = project.getBuild().getDelegate().getSources();
324+
if (SourceQueries.usesModuleSourceHierarchy(projectSources)) {
325+
// Dependencies are dispatched by maven-jar-plugin in the POM generated for each module.
326+
model = model.withDependencies(null).withPackaging(POM_PACKAGING);
319327
}
320-
321328
if (!preserveModelVersion) {
329+
/*
330+
* If the <build> contains <source> elements, it is not compatible with the Maven 4.0.0 model.
331+
* Remove the full <build> element instead of removing only the <sources> element, because the
332+
* build without sources does not mean much. Reminder: this removal can be disabled by setting
333+
* the `preserveModelVersion` XML attribute or `preserve.model.version` property to true.
334+
*/
335+
if (SourceQueries.hasEnabledSources(projectSources)) {
336+
model = model.withBuild(null);
337+
}
322338
model = model.withPreserveModelVersion(false);
323339
String modelVersion = new MavenModelVersion().getModelVersion(model);
324340
model = model.withModelVersion(modelVersion);
325341
}
326342
return model;
327343
}
328344

329-
static void warnNotDowngraded(MavenProject project) {
345+
private static void warnNotDowngraded(MavenProject project) {
330346
LOGGER.warn("The consumer POM for " + project.getId() + " cannot be downgraded to 4.0.0. "
331347
+ "If you intent your build to be consumed with Maven 3 projects, you need to remove "
332348
+ "the features that request a newer model version. If you're fine with having the "

impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,6 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
649649
// only set those on 2nd phase, ignore on 1st pass
650650
if (project.getFile() != null) {
651651
Build build = project.getBuild().getDelegate();
652-
List<org.apache.maven.api.model.Source> sources = build.getSources();
653652
Path baseDir = project.getBaseDirectory();
654653
Function<ProjectScope, String> outputDirectory = (scope) -> {
655654
if (scope == ProjectScope.MAIN) {
@@ -660,23 +659,11 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
660659
return build.getDirectory();
661660
}
662661
};
663-
// Extract modules from sources to detect modular projects
664-
Set<String> modules = extractModules(sources);
665-
boolean isModularProject = !modules.isEmpty();
666-
667-
logger.trace(
668-
"Module detection for project {}: found {} module(s) {} - modular project: {}.",
669-
project.getId(),
670-
modules.size(),
671-
modules,
672-
isModularProject);
673-
674662
// Create source handling context for unified tracking of all lang/scope combinations
675-
SourceHandlingContext sourceContext =
676-
new SourceHandlingContext(project, baseDir, modules, isModularProject, result);
663+
final SourceHandlingContext sourceContext = new SourceHandlingContext(project, result);
677664

678665
// Process all sources, tracking enabled ones and detecting duplicates
679-
for (var source : sources) {
666+
for (org.apache.maven.api.model.Source source : sourceContext.sources) {
680667
var sourceRoot = DefaultSourceRoot.fromModel(session, baseDir, outputDirectory, source);
681668
// Track enabled sources for duplicate detection and hasSources() queries
682669
// 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) {
705692
implicit fallback (only if they match the default, e.g., inherited)
706693
- This allows incremental adoption (e.g., custom resources + default Java)
707694
*/
708-
if (sources.isEmpty()) {
695+
if (sourceContext.sources.isEmpty()) {
709696
// Classic fallback: no <sources> configured, use legacy directories
710697
project.addScriptSourceRoot(build.getScriptSourceDirectory());
711698
project.addCompileSourceRoot(build.getSourceDirectory());
@@ -718,8 +705,7 @@ implicit fallback (only if they match the default, e.g., inherited)
718705
if (!sourceContext.hasSources(Language.SCRIPT, ProjectScope.MAIN)) {
719706
project.addScriptSourceRoot(build.getScriptSourceDirectory());
720707
}
721-
722-
if (isModularProject) {
708+
if (sourceContext.usesModuleSourceHierarchy()) {
723709
// Modular: reject ALL legacy directory configurations
724710
failIfLegacyDirectoryPresent(
725711
build.getSourceDirectory(),
@@ -1237,22 +1223,6 @@ public Set<Entry<K, V>> entrySet() {
12371223
}
12381224
}
12391225

1240-
/**
1241-
* Extracts unique module names from the given list of source elements.
1242-
* A project is considered modular if it has at least one module name.
1243-
*
1244-
* @param sources list of source elements from the build
1245-
* @return set of non-blank module names
1246-
*/
1247-
private static Set<String> extractModules(List<org.apache.maven.api.model.Source> sources) {
1248-
return sources.stream()
1249-
.map(org.apache.maven.api.model.Source::getModule)
1250-
.filter(Objects::nonNull)
1251-
.map(String::trim)
1252-
.filter(s -> !s.isBlank())
1253-
.collect(Collectors.toSet());
1254-
}
1255-
12561226
private Model injectLifecycleBindings(
12571227
Model model,
12581228
ModelBuilderRequest request,

impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.maven.api.ProjectScope;
2828
import org.apache.maven.api.SourceRoot;
2929
import org.apache.maven.api.model.Resource;
30+
import org.apache.maven.api.model.Source;
3031
import org.apache.maven.api.services.BuilderProblem.Severity;
3132
import org.apache.maven.api.services.ModelBuilderResult;
3233
import org.apache.maven.api.services.ModelProblem.Version;
@@ -37,9 +38,7 @@
3738

3839
/**
3940
* Handles source configuration for Maven projects with unified tracking for all language/scope combinations.
40-
* <p>
41-
* This class replaces the previous approach of hardcoded boolean flags (hasMain, hasTest, etc.)
42-
* with a flexible set-based tracking mechanism that works for any language and scope combination.
41+
* This class uses a flexible set-based tracking mechanism that works for any language and scope combination.
4342
* <p>
4443
* Key features:
4544
* <ul>
@@ -51,7 +50,7 @@
5150
*
5251
* @since 4.0.0
5352
*/
54-
class SourceHandlingContext {
53+
final class SourceHandlingContext {
5554

5655
private static final Logger LOGGER = LoggerFactory.getLogger(SourceHandlingContext.class);
5756

@@ -60,26 +59,38 @@ class SourceHandlingContext {
6059
*/
6160
record SourceKey(Language language, ProjectScope scope, String module, Path directory) {}
6261

62+
/**
63+
* The {@code <source>} elements declared in the {@code <build>} elements.
64+
*/
65+
final List<Source> sources;
66+
6367
private final MavenProject project;
64-
private final Path baseDir;
6568
private final Set<String> modules;
66-
private final boolean modularProject;
6769
private final ModelBuilderResult result;
6870
private final Set<SourceKey> declaredSources;
6971

70-
SourceHandlingContext(
71-
MavenProject project,
72-
Path baseDir,
73-
Set<String> modules,
74-
boolean modularProject,
75-
ModelBuilderResult result) {
72+
SourceHandlingContext(MavenProject project, ModelBuilderResult result) {
7673
this.project = project;
77-
this.baseDir = baseDir;
78-
this.modules = modules;
79-
this.modularProject = modularProject;
74+
this.sources = project.getBuild().getDelegate().getSources();
75+
this.modules = SourceQueries.getModuleNames(sources);
8076
this.result = result;
8177
// Each module typically has main, test, main resources, test resources = 4 sources
8278
this.declaredSources = new HashSet<>(4 * modules.size());
79+
if (usesModuleSourceHierarchy()) {
80+
LOGGER.trace("Found {} module(s) in the \"{}\" project: {}.", project.getId(), modules.size(), modules);
81+
} else {
82+
LOGGER.trace("Project \"{}\" is non-modular.", project.getId());
83+
}
84+
}
85+
86+
/**
87+
* Whether the project uses module source hierarchy.
88+
* Note that this is not synonymous of whether the project is modular,
89+
* because it is possible to create a single Java module in a classic Maven project
90+
* (i.e., using package hierarchy).
91+
*/
92+
boolean usesModuleSourceHierarchy() {
93+
return !modules.isEmpty();
8394
}
8495

8596
/**
@@ -112,7 +123,7 @@ boolean shouldAddSource(SourceRoot sourceRoot) {
112123
SourceKey key = new SourceKey(
113124
sourceRoot.language(), sourceRoot.scope(), sourceRoot.module().orElse(null), normalizedDir);
114125

115-
if (declaredSources.contains(key)) {
126+
if (!declaredSources.add(key)) {
116127
String message = String.format(
117128
"Duplicate enabled source detected: lang=%s, scope=%s, module=%s, directory=%s. "
118129
+ "First enabled source wins, this duplicate is ignored.",
@@ -130,7 +141,6 @@ boolean shouldAddSource(SourceRoot sourceRoot) {
130141
return false; // Don't add duplicate enabled source
131142
}
132143

133-
declaredSources.add(key);
134144
LOGGER.debug(
135145
"Adding and tracking enabled source: lang={}, scope={}, module={}, dir={}",
136146
key.language(),
@@ -151,6 +161,13 @@ boolean hasSources(Language language, ProjectScope scope) {
151161
return declaredSources.stream().anyMatch(key -> language.equals(key.language()) && scope.equals(key.scope()));
152162
}
153163

164+
/**
165+
* {@return the source directory as defined by Maven conventions}
166+
*/
167+
private Path getStandardSourceDirectory() {
168+
return project.getBaseDirectory().resolve("src");
169+
}
170+
154171
/**
155172
* Fails the build if modular and classic (non-modular) sources are mixed within {@code <sources>}.
156173
* <p>
@@ -164,30 +181,32 @@ boolean hasSources(Language language, ProjectScope scope) {
164181
void failIfMixedModularAndClassicSources() {
165182
for (ProjectScope scope : List.of(ProjectScope.MAIN, ProjectScope.TEST)) {
166183
for (Language language : List.of(Language.JAVA_FAMILY, Language.RESOURCES)) {
167-
boolean hasModular = declaredSources.stream()
168-
.anyMatch(key ->
169-
language.equals(key.language()) && scope.equals(key.scope()) && key.module() != null);
170-
boolean hasClassic = declaredSources.stream()
171-
.anyMatch(key ->
172-
language.equals(key.language()) && scope.equals(key.scope()) && key.module() == null);
173-
174-
if (hasModular && hasClassic) {
175-
String message = String.format(
176-
"Mixed modular and classic sources detected for lang=%s, scope=%s. "
177-
+ "A project must be either fully modular (all sources have a module) "
178-
+ "or fully classic (no sources have a module). "
179-
+ "The compiler plugin cannot handle mixed configurations.",
180-
language.id(), scope.id());
181-
LOGGER.error(message);
182-
result.getProblemCollector()
183-
.reportProblem(new DefaultModelProblem(
184-
message,
185-
Severity.ERROR,
186-
Version.V41,
187-
project.getModel().getDelegate(),
188-
-1,
189-
-1,
190-
null));
184+
boolean hasModular = false;
185+
boolean hasClassic = false;
186+
for (SourceKey key : declaredSources) {
187+
if (language.equals(key.language()) && scope.equals(key.scope())) {
188+
String module = key.module();
189+
hasModular |= (module != null);
190+
hasClassic |= (module == null);
191+
if (hasModular && hasClassic) {
192+
String message = String.format(
193+
"Mixed modular and classic sources detected for lang=%s, scope=%s. "
194+
+ "A project must be either fully modular (all sources have a module) "
195+
+ "or fully classic (no sources have a module).",
196+
language.id(), scope.id());
197+
LOGGER.error(message);
198+
result.getProblemCollector()
199+
.reportProblem(new DefaultModelProblem(
200+
message,
201+
Severity.ERROR,
202+
Version.V41,
203+
project.getModel().getDelegate(),
204+
-1,
205+
-1,
206+
null));
207+
break;
208+
}
209+
}
191210
}
192211
}
193212
}
@@ -219,7 +238,7 @@ void handleResourceConfiguration(ProjectScope scope) {
219238
? "<source><lang>resources</lang></source>"
220239
: "<source><lang>resources</lang><scope>test</scope></source>";
221240

222-
if (modularProject) {
241+
if (usesModuleSourceHierarchy()) {
223242
if (hasResourcesInSources) {
224243
// Modular project with resources configured via <sources> - already added above
225244
if (hasExplicitLegacyResources(resources, scopeId)) {
@@ -298,6 +317,7 @@ void handleResourceConfiguration(ProjectScope scope) {
298317
// Use legacy resources element
299318
LOGGER.debug(
300319
"Using explicit or default {} resources ({} resources configured).", scopeId, resources.size());
320+
Path baseDir = project.getBaseDirectory();
301321
for (Resource resource : resources) {
302322
project.addSourceRoot(new DefaultSourceRoot(baseDir, scope, resource));
303323
}
@@ -315,7 +335,7 @@ void handleResourceConfiguration(ProjectScope scope) {
315335
*/
316336
private DefaultSourceRoot createModularResourceRoot(String module, ProjectScope scope) {
317337
Path resourceDir =
318-
baseDir.resolve("src").resolve(module).resolve(scope.id()).resolve("resources");
338+
getStandardSourceDirectory().resolve(module).resolve(scope.id()).resolve("resources");
319339

320340
return new DefaultSourceRoot(
321341
scope,
@@ -345,12 +365,10 @@ private boolean hasExplicitLegacyResources(List<Resource> resources, String scop
345365
}
346366

347367
// Super POM default paths
348-
String defaultPath =
349-
baseDir.resolve("src").resolve(scope).resolve("resources").toString();
350-
String defaultFilteredPath = baseDir.resolve("src")
351-
.resolve(scope)
352-
.resolve("resources-filtered")
353-
.toString();
368+
Path srcDir = getStandardSourceDirectory();
369+
String defaultPath = srcDir.resolve(scope).resolve("resources").toString();
370+
String defaultFilteredPath =
371+
srcDir.resolve(scope).resolve("resources-filtered").toString();
354372

355373
// Check if any resource differs from Super POM defaults
356374
for (Resource resource : resources) {

0 commit comments

Comments
 (0)