Hello Gerd
Thanks for your in-depth analysis.
* Note for other readers: original Gerd's email is below this reply.
It has a lot of content, but I try to digest the main points below.
* Note for Gerd: the email contains links to "http://localhost", which
we cannot see. In addition, I think that the originally intended
formatting has been lost in the email. It may be worth to provide
this content in GitHub issues, maybe with one issue one problem.
Gerd pointed out the following problems in the current Maven behavior
regarding Java modules. My comments are inline:
Problem 1: Mixed Modular/Non-Modular Sources Accepted Silently
I agree that this problem should be detected by Maven core instead of
Maven compiler plugin, for the reasons stated by Gerd in his email
(earlier and more complete validation, validation done for all plugins,
provides line numbers were problems occur).
Problem 2: Test-Only Modular Projects Get Classic Main Sources Injected
If src/main/java exists with old/unrelated code → it gets compiled!
Modular test project silently becomes a mixed modular-test +
classic-main project
I see that as a Maven Compiler Plugin bug. Maybe the description of this
problem should move to a GitHub issue on
https://github.com/apache/maven-compiler-plugin/?
Problem 3: Resources Are Not Module-Aware
When a project uses modular sources, resources should follow the same
modular layout. Currently, they don't.
I propose to separate this problem in three parts:
1. In the current model, users must declare modular resources
explicitly with <source> elements having <lang>resources</lang>.
However, the current Maven Resource Plugin has not yet been updated
for honoring that configuration. This is a first problem to resolve.
2. The second problem is what to do if the user did not declare
resources explicitly. In the current Maven core implementation, we
inherit the default Maven 3 configuration, which is incorrect for a
modular project.
3. A third problem is what to do if the user mixes modular and
classical configuration. The current Maven core implementation
raises no warning.
The https://github.com/apache/maven/pull/11505 pull request seems to
address 2 and 3, but not 1, is that right? If so, should we resolve 1 first?
Problem 4: Legacy Configuration Silently Ignored
When <sources> is present, legacy elements like <sourceDirectory> are
silently ignored without warning.
I agree that it is a risk of unpredictable (or at least non-obvious)
behavior and should cause a warning or error. Gerd's email proposes two
alternative approaches: lenient or strict. I would favor the strict
approach on the argument that the unified <source> element is a new
feature of Maven 4 (so there is no backward compatibility constraint),
and it is easier to be too strict at first and relax later if needed
than doing the opposite.
Regarding the proposal in the email copied below, I propose two
amendments. First, I suggest to drop the following rule:
R3 Duplicate <source> (same scope/lang/module) ERROR "Duplicate
source definition for scope='X', lang='Y', module='Z'"
The rational is to support projects having their sources split in many
directories for the same module. This is documented as a replacement for
the `build-helper-maven-plugin` in the "Declaration of many source
directories" section of
https://maven.apache.org/plugins/maven-compiler-plugin-4.x/sources.html
(that particular section is about classical projects, but can applied to
modular projects too).
I propose to also drop the "Filesystem mismatch" warning, if
"filesystem" is in the sense of `Path.getFileSystem()`. The codes that
have been revisited in the compiler plugin, clean plugin and parts of
Maven core try to be careful in the way that they resolve paths. They
should work fine even with mixed filesystems (if not, it would be bugs
to fix).
Martin
Le 30/11/2025 à 14:45, Gerd Aschemann a écrit :
Hi,
based on my work to migrate a number of Java Modules (aka. Jigsaw/JPMS) to
Maven (4), I did a deep investigation of the current implementation. I already
discussed with some of you on Slack about my work and one thing we found is the
missing handling of resources when (Java) modules are used.
At first I just tried to implement proper resource handling (cf. my proposed, yet
draft, PR). But the deeper I looked into it, I found Maven support for Java
Modules incomplete, in particular wrt the old (Maven <= 3) conventions.
Therefore, I have now created a deep analysis based on some typical examples. The
analysis culminates in a full matrix of combinations of sources/resources etc. I
also propose how to handle the different cases.
Please have a look into it and provide feedback about it. The subject is rather
complex, so I might have missed important view points.
Perhaps other media are better suited to discuss the different cases and their
consequences (in particular wrt. to warnings and errors)? If its helpful, I’d
be happy to place the stuff into the Maven Wiki (Confluence).
However, in the end, the work should result in some decisions. I propose the
following
Handle model loading and proper handling of sources/resources completely in the
core (currently, the compiler plugin contributes to some handling, see below)
Implement a strict approach how to handle misconfigurations (this is mostly
about warnings vs. errors, see below). Personally, I plead for a very strict
handling (errors) here as the feature is new with Maven 4 and have no need to
handle legacy code about Java modules.
Build this handling into the Maven core and provide proper documentation.
Perhaps redraw some of the existing error handling from the compiler plugin as
it much better suites into the core (sorry, Martin).
Of course, I hereby volunteer to perform the implementation. I have already
started with some of it, to be honest.
For phase 1 (proper handling of resources in the context of <sources> and <modules> I
have implemented a solution (cf. Draft PR<https://github.com/apache/maven/pull/11505> on
maven-core). The nice thing about this implementation is, that even M-Resources-P in version 3.x
can handle the resources with this Maven core as it is using the proper abstractions.
Regards
--
Gerd Aschemann --- Veröffentlichen heißt Verändern (Carmen Thomas)
+49/173/3264070 [email protected] --http://www.aschemann.net
Build Sources Validation
This document captures a discussion about validating <sources> configuration in
Maven 4.x.
0. Scope
This document focuses exclusively on the Maven 4.x core implementation (master branch
of apache/maven<https://github.com/apache/maven>). Specifically:
The <sources> element introduced in POM model version 4.1.0
The source/resource handling logic in DefaultProjectBuilder.java
Validation gaps in the core that should be addressed
0.1 Compiler Plugin Compensation
Some validation gaps in the core are currently compensated by the
maven-compiler-plugin, which performs its own validation at compile time, e.g.,
Mixed modular/non-modular sources → compiler plugin fails with "Mix of modular and
non-modular sources", cf.
ToolExecutor.java#L595-L608<https://github.com/apache/maven-compiler-plugin/blob/050664790af28be9107aff6172f8e61adcb8ed8a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java#L595-L608>
However, relying on plugin-level validation is suboptimal:
Later feedback: Errors appear during compilation, not during POM processing
No line numbers: Plugin errors cannot reference the exact POM location
Plugin-specific: Other language plugins (Kotlin, Groovy, Scala) may not have
equivalent validation
Incomplete coverage: Some invalid configurations are not caught at all (e.g.,
test-only modular projects)
The goal should be to implement proper validation in the Maven core, providing
early, consistent feedback with precise error locations via ModelProblem.
0.2 Origin of This Discussion
This discussion was triggered by investigating how to best implement resource
handling in the new modular source directory hierarchy. Maven 4.x introduces a
unified <sources> element that supports modular layouts like:
src/
├── org.foo.moduleA/
│ ├── main/
│ │ ├── java/
│ │ └── resources/ ← Module-specific resources
│ └── test/
│ ├── java/
│ └── resources/
└── org.foo.moduleB/
├── main/
│ ├── java/
│ └── resources/ ← Module-specific resources
└── test/
However, the current implementation does not automatically pick up resources from the
modular paths (src/<module>/main/resources). Instead, it always uses the legacy
<resources> element which defaults to src/main/resources.
Solution: See Phase
1<http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#41-phase-1-module-aware-resource-handling-implemented>
for the implementation that automatically injects module-aware resources.
Current Workaround: Users can configure the maven-resources-plugin with
explicit executions for each module:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources-moduleA</id>
<phase>process-resources</phase>
<goals><goal>copy-resources</goal></goals>
<configuration>
<resources>
<resource>
<directory>src/org.foo.moduleA/main/resources</directory>
</resource>
</resources>
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
</configuration>
</execution>
<execution>
<id>copy-resources-moduleB</id>
<phase>process-resources</phase>
<goals><goal>copy-resources</goal></goals>
<configuration>
<resources>
<resource>
<directory>src/org.foo.moduleB/main/resources</directory>
</resource>
</resources>
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
This workaround is verbose and error-prone. The investigation into implementing this
properly in the core led to the Phase 1
implementation<http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#41-phase-1-module-aware-resource-handling-implemented>
and revealed the broader set of validation and configuration handling issues
documented here.
1. Problem Statement
Let's investigate some of the problems with the current implementation of <sources> handling in Maven 4.x. Maven 4.x
introduces a new <sources> element that supports modular project layouts (src/<module>/<scope>/<lang>).
However, the current implementation has several issues when handling the interaction between the new <sources> element
and legacy configuration elements (<sourceDirectory>, <resources>, etc.).
This is not a comprehensive list, but highlights key problems that should be
addressed in the core. A systematic Analysis and Proposed Solutions follow in
subsequent sections.
Problem 1: Mixed Modular/Non-Modular Sources Accepted Silently
Mixing modular and non-modular sources in <sources> is an invalid configuration
that will fail at compile time, but DefaultProjectBuilder accepts it silently without
early validation.
Example - Mixed configuration:
<build>
<sources>
<source>
<scope>main</scope>
<lang>java</lang>
<module>org.foo.moduleA</module> <!-- Modular -->
</source>
<source>
<scope>main</scope>
<lang>java</lang>
<!-- No module - classic style -->
</source>
</sources>
</build>
Expected Behavior Actual Behavior
Early warning/error about inconsistent configuration Silent acceptance by
DefaultProjectBuilder
Clear feedback during project build Delayed failure at compile time
Current Workaround: The maven-compiler-plugin validates this at compile time
and fails with:
"Mix of modular and non-modular sources."
See above (Compiler Plugin
Compensation<http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#compiler-plugin-compensation>)
for drawbacks of this approach.
Problem 2: Test-Only Modular Projects Get Classic Main Sources Injected
When a project configures only test sources in <sources> (no main sources),
DefaultProjectBuilder silently injects the classic src/main/java as a main source
directory.
Example - Integration test module with only test sources:
<build>
<sources>
<source>
<scope>test</scope>
<lang>java</lang>
<module>org.foo.integrationTests</module>
</source>
</sources>
</build>
Expected Behavior Actual Behavior
No main sources (project is test-only) src/main/java silently added as main
source
Test sources from src/org.foo.integrationTests/test/java Test sources
work correctly
Root cause: The hasMain boolean remains false when no <source> has scope=main +
lang=java. Line 697-698 then adds build.getSourceDirectory() (defaults to
src/main/java from Super POM).
See:
DefaultProjectBuilder.java#L697-L698<https://github.com/apache/maven/blob/25c80d8ece4252421c9dce5c34fb46a21c7b9f23/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L697-L698>
Impact:
If src/main/java doesn't exist → probably harmless (empty directory)
If src/main/java exists with old/unrelated code → it gets compiled!
Modular test project silently becomes a mixed modular-test + classic-main
project
Use cases affected:
Integration test modules (only contain tests)
Test fixtures modules (shared test utilities)
BOM/parent projects with test verification but no main code
Note: Unlike Problem 1, this is not caught by the compiler plugin - it silently
succeeds but with potentially wrong behavior.
Problem 3: Resources Are Not Module-Aware
When a project uses modular sources, resources should follow the same modular
layout. Currently, they don't.
Example - Modular Java sources without explicit resources:
<build>
<sources>
<source>
<scope>main</scope>
<lang>java</lang>
<module>org.foo.moduleA</module>
</source>
<source>
<scope>main</scope>
<lang>java</lang>
<module>org.foo.moduleB</module>
</source>
</sources>
</build>
Expected Behavior Actual Behavior
Resources from src/org.foo.moduleA/main/resources Resources from
src/main/resources only
Resources from src/org.foo.moduleB/main/resources (legacy path, not
module-aware)
Impact: Module-specific resources (like module-specific configs) cannot be
organized per-module.
Solution: See Phase
1<http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#41-phase-1-module-aware-resource-handling-implemented>
for the implementation.
Workaround (no longer needed): See Origin of This
Discussion<http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#origin-of-this-discussion>
for how to configure the maven-resources-plugin with explicit executions for each
module.
Problem 4: Legacy Configuration Silently Ignored
When <sources> is present, legacy elements like <sourceDirectory> are silently
ignored without warning.
Example - Explicit sourceDirectory with sources:
<build>
<sourceDirectory>src/custom/java</sourceDirectory> <!-- User expects this to
be used -->
<sources>
<source>
<scope>main</scope>
<lang>java</lang>
<module>org.foo.bar</module>
</source>
</sources>
</build>
Expected Behavior Actual Behavior
Warning: "sourceDirectory is ignored because sources are configured" Silent -
user doesn't know their config is ignored
Clear feedback to user Confusion when src/custom/java isn't used
2. Analysis
2.1 Core Design Question
How should the source reading and validation logic work?
The fundamental question is about priority and conflict resolution when both new
(<sources>) and legacy (<sourceDirectory>, <resources>) configuration elements
are present.
Key Design Principle: Modular Sources Have Priority
When a project uses the new <sources> element with modular configuration
(<module> element), the modular approach should take precedence:
Modular sources (<sources> with <module>) demand proper modular layout and
behavior
Non-modular sources (<sources> without <module>) should not be used (error or
warning) in a modular project
Legacy configuration (<sourceDirectory>, <resources>) is used only as fallback when
no <sources> are configured
This principle ensures:
Clear, predictable behavior
No silent mixing of old and new approaches
Users explicitly opt-in to the new model
Path Resolution Logic
From DefaultSourceRoot.fromModel() — abbreviated pseudo code of the current
implementation<https://github.com/apache/maven/blob/25c80d8ece4252421c9dce5c34fb46a21c7b9f23/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.java#L135-L141>:
if (module specified) {
// Modular mode
path = directory specified ? baseDir.resolve(directory)
: baseDir/src/<module>/<scope>/<lang>
} else {
// Classic mode
path = directory specified ? baseDir.resolve(directory)
: baseDir/src/<scope>/<lang>
}
Module Directory Mode Result
No No Classic src/<scope>/<lang> (default)
No Yes Classic <directory> (override)
Yes No Modular src/<module>/<scope>/<lang> (default)
Yes Yes Modular <directory> (override, module = metadata)
2.2 Strict vs Lenient Approach
Two approaches are possible for handling configuration conflicts:
Approach A: Strict (Fail Fast)
Configuration conflicts result in errors that fail the build.
Rule Condition Severity Message
R1 <sources> present AND explicit <sourceDirectory> ERROR "Cannot combine
<sources> with explicit <sourceDirectory>. Use one or the other."
R2 <sources> with mixed module/no-module elements ERROR "Cannot mix modular and
non-modular sources. All <source> elements must either have a module or none."
R3 Duplicate <source> (same scope/lang/module) ERROR "Duplicate source
definition for scope='X', lang='Y', module='Z'"
R4 <sources> configured but classic directory exists on filesystem WARN
"Directory 'src/main/java' exists but will be ignored because <sources> is configured"
Approach B: Lenient (Warn and Continue)
Configuration conflicts result in warnings but the build continues.
Rule Condition Severity Message
R1' <sources> present AND explicit <sourceDirectory> WARN "Explicit
<sourceDirectory> will be ignored because <sources> is configured."
R2' <sources> with mixed module/no-module elements WARN "Mixing modular
and non-modular sources may lead to unexpected behavior."
R3 Duplicate <source> (same scope/lang/module) ERROR "Duplicate source
definition for scope='X', lang='Y', module='Z'"
R4 <sources> configured but classic directory exists on filesystem WARN
"Directory 'src/main/java' exists but will be ignored because <sources> is configured"
Comparison
Aspect Strict (A) Lenient (B)
<sources> + explicit SD/TSD ERROR WARN
Mixed module/no-module ERROR WARN
Duplicate sources ERROR ERROR
Filesystem mismatch WARN WARN
Migration friendliness Lower Higher
User confusion risk Lower Higher
Fail-fast principle Yes No
Recommendation
Approach A (Strict) is recommended for new projects because:
Configuration conflicts indicate user error/misunderstanding
Silent ignoring of explicit configuration is confusing
Better to fail early than have unexpected behavior at compile time
Users can easily fix by removing the conflicting element
Approach B (Lenient) could be considered if:
Migration from Maven 3.x to 4.x needs to be smoother
Tooling (IDEs) may generate both elements during transition
A deprecation period is desired before enforcing strict rules
2.3 Unified Permutation Matrix
This matrix shows all configuration combinations and their expected behavior
under each approach.
Legend
Abbreviation Meaning
SD <sourceDirectory> - classic Maven 3.x element for main Java sources
TSD <testSourceDirectory> - classic Maven 3.x element for test Java sources
S <source> element within <sources> - Maven 4.x way to define sources
R <resources> / <resource> - classic Maven 3.x element for resources
M <module> element within <source> - specifies JPMS module name
Java Sources
# Configuration Current Lenient Strict Compiler Plugin
No <sources> (classic mode)
1 SD=implicit (Super POM) src/main/java OK OK -
2 SD=explicit user's path OK OK -
<sources> present (new mode)
3 S(no M), SD=implicit src/main/java OK OK -
4 S(no M), SD=explicit Silent ignore WARN ERROR Not caught
5 S(M=X), SD=implicit src/X/main/java OK OK -
6 S(M=X), SD=explicit Silent ignore WARN ERROR Not caught
7 S(M=X) + S(M=Y) Both paths OK OK -
8 S(M=X) + S(no M) Silent accept WARN ERROR Caught ✓
9 S(dir=custom, no M) custom path OK OK -
10 S(dir=custom, M=X) custom (M=meta) OK OK -
11 Duplicate S Silent accept ERROR ERROR Not caught
12 S(M=X) + src/main/java exists Silent ignore WARN WARN Not
caught
13 S(scope=test, M=X) only Injects src/main/java No inject No
inject Not caught ⚠️
Resources
# S Config R Config Current Proposed (Phase 1) Lenient
Strict
1 S(lang=resources) R=any Duplicates! Use S, WARN if R
explicit Use S, WARN Use S, WARN
2 S(java, no M) R=implicit src/main/resources
src/main/resources OK OK
3 S(java, no M) R=explicit User's path User's path OK
OK
4 S(java, M=X) R=implicit src/main/resources Inject modular
Inject modular Inject modular
5 S(java, M=X) R=explicit User's path Inject modular, WARN
WARN ERROR
3. Current Implementation Status
As of commit
25c80d8<https://github.com/apache/maven/blob/25c80d8ece4252421c9dce5c34fb46a21c7b9f23/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L645-L708>
Implementation Location
The source/resource handling logic is in DefaultProjectBuilder.initProject():
// Lines 669-686: Iterate over <sources> and track what's present
boolean hasScript = false;
boolean hasMain = false; // true if ANY <source> has lang=java, scope=main
boolean hasTest = false; // true if ANY <source> has lang=java, scope=test
for (var source : sources) {
var src = DefaultSourceRoot.fromModel(session, baseDir, outputDirectory,
source);
project.addSourceRoot(src);
// ... tracking logic
}
// Lines 694-701: Decide whether to use legacy
sourceDirectory/testSourceDirectory
// (Lines 687-692 contain a comment explaining this behavior)
if (!hasScript) {
project.addScriptSourceRoot(build.getScriptSourceDirectory());
}
if (!hasMain) {
project.addCompileSourceRoot(build.getSourceDirectory()); // Silent
fallback
}
if (!hasTest) {
project.addTestCompileSourceRoot(build.getTestSourceDirectory()); //
Silent fallback
}
// Lines 703-708: Resources are ALWAYS added from legacy elements (no checks!)
for (Resource resource : project.getBuild().getDelegate().getResources()) {
project.addSourceRoot(new DefaultSourceRoot(baseDir, ProjectScope.MAIN,
resource));
}
for (Resource resource : project.getBuild().getDelegate().getTestResources()) {
project.addSourceRoot(new DefaultSourceRoot(baseDir, ProjectScope.TEST,
resource));
}
Gap Analysis
Issue Description Core Status Compiler Plugin
No warnings Explicit <sourceDirectory> silently ignored when <sources>
present Not implemented Not caught
No mixed validation Mixing S(M=X) with S(no M) silently accepted Not
implemented Caught ✓
Test-only injection src/main/java injected for test-only modular projects
Not implemented Not caught ⚠️
No duplicates check Same source can be defined multiple times Not
implemented Not caught
No modular resources Resources always from legacy <resources> element ✅
Implemented (Phase
1<http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#41-phase-1-module-aware-resource-handling-implemented>)
N/A
Key insight: Only the mixed modular/non-modular validation (#8) is currently
caught by the compiler plugin. All other issues require core implementation.
What Works
Processing <source> elements and adding them as source roots
Fallback to legacy <sourceDirectory> when no Java sources in <sources>
Path resolution for both modular and classic layouts
What's Missing
Warnings when explicit configuration is ignored
Validation for mixed modular/non-modular sources (currently deferred to
compiler plugin)
Correct handling of test-only modular projects (no main injection)
Duplicate detection
Modular resource handling ✅ Implemented in Phase
1<http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#41-phase-1-module-aware-resource-handling-implemented>
4. Implementation Approaches
4.1 Phase 1: Module-Aware Resource Handling (Implemented)
Goal: Enable modular resource handling without requiring explicit
maven-resources-plugin configuration.
Status: ✅ Implemented in
3e2d01f<https://github.com/support-and-care/maven/commit/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3>
Implementation:
Resource tracking via <sources>: (Lines
672-673<https://github.com/support-and-care/maven/blob/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L672-L673>)
boolean hasMainResources = false;
boolean hasTestResources = false;
for (var source : sources) {
if (Language.RESOURCES.equals(language)) {
if (ProjectScope.MAIN.equals(scope)) {
hasMainResources = true;
} else {
hasTestResources |= ProjectScope.TEST.equals(scope);
}
}
}
Module extraction to detect modular projects: (Lines
713-714<https://github.com/support-and-care/maven/blob/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L713-L714>,
extractModules() at
L1256<https://github.com/support-and-care/maven/blob/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L1256-L1272>)
Set<String> modules = extractModules(sources);
boolean isModularProject = !modules.isEmpty();
Module-aware resource injection for modular projects: (Lines
729-770<https://github.com/support-and-care/maven/blob/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L729-L770>,
createModularResourceRoot() at
L1275<https://github.com/support-and-care/maven/blob/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L1275-L1300>)
If resources configured via <sources>: use them (already added during iteration)
If no resources in <sources>: inject module-aware defaults for each module
Warn (as ModelProblem) if legacy <resources> is present but ignored
if (isModularProject) {
if (hasMainResources) {
// Already added via <sources>, warn if legacy <resources> present
} else {
// Inject module-aware defaults: src/<module>/main/resources
for (String module : modules) {
project.addSourceRoot(createModularResourceRoot(baseDir, module,
ProjectScope.MAIN, ...));
}
}
}
Super POM default detection: (hasOnlySuperPomDefaults() at
L1303<https://github.com/support-and-care/maven/blob/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L1303-L1331>)
hasOnlySuperPomDefaults() checks if <resources> contains only inherited defaults
Warnings are only issued for explicitly configured legacy resources, not Super
POM defaults
Priority Hierarchy (Proposed):
Priority Condition Behavior
1 Modular project + resources in <sources> Use <sources> resources, warn
if legacy present
2 Modular project + no resources in <sources> Inject
src/<module>/<scope>/resources for each module
3 Classic project + resources in <sources> Use <sources> resources, warn
if legacy present
4 Classic project + no resources in <sources> Use legacy
<resources> element
Example - Would Work Without Explicit Plugin Configuration:
<build>
<sources>
<source>
<scope>main</scope>
<lang>java</lang>
<module>org.foo.moduleA</module>
</source>
<source>
<scope>main</scope>
<lang>java</lang>
<module>org.foo.moduleB</module>
</source>
</sources>
</build>
Resources would be automatically picked up from:
src/org.foo.moduleA/main/resources
src/org.foo.moduleB/main/resources
What's NOT Addressed in This Proposal:
Problem 1: Mixed modular/non-modular sources validation (still deferred to
compiler plugin)
Problem 2: Test-only modular project main injection
Problem 4: Warning when explicit <sourceDirectory> is ignored
Duplicate source detection
Per-module tracking (project-level booleans still used)
4.2 Phase 2: Comprehensive Refactoring
Goal: Full implementation of the design principles from Section 2 with proper
validation.
Scope:
Per-module tracking instead of project-level booleans:
record ModuleConfig(boolean hasMain, boolean hasTest, boolean hasMainResources,
boolean hasTestResources) {}
Map<String, ModuleConfig> moduleConfigs = new HashMap<>();
Validation in ModelValidator (not DefaultProjectBuilder):
Move conflict detection to validateEffectiveModel()
Report ModelProblem with line numbers and severity
Early feedback during POM processing
Strict validation rules (from Section 2.2):
Rule Condition Severity Message
R1 <sources> + explicit <sourceDirectory> ERROR "Cannot combine <sources> with
explicit <sourceDirectory>"
R2 Mixed modular/non-modular sources ERROR "Cannot mix modular and
non-modular sources"
R3 Duplicate <source> definition ERROR "Duplicate source for scope='X',
lang='Y', module='Z'"
R4 <sources> configured but classic dir exists WARN "Directory
'src/main/java' exists but will be ignored"
Fix test-only modular project injection (Problem 2):
// Don't inject legacy main sources for modular projects
if (!hasMain && !isModularProject) {
project.addCompileSourceRoot(build.getSourceDirectory());
}
Warning when explicit configuration is ignored (Problem 4):
if (hasMain && isExplicitSourceDirectory(build)) {
// Report ModelProblem with line number
}
InputLocation-based detection for explicit vs inherited configuration:
InputLocation location = build.getLocation("sourceDirectory");
boolean isExplicit = location != null && !isSuperPomLocation(location);
Implementation Location:
Component Responsibility
DefaultModelValidator Validation rules R1-R4, early feedback with line numbers
DefaultProjectBuilder Source root creation, fallback logic
DefaultSourceRoot Path resolution (already implemented)
Implementation Timeline
Phase Scope Status
Phase 1 Module-aware resource handling ✅ Implemented
(3e2d01f<https://github.com/support-and-care/maven/commit/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3>)
Phase 2 Full validation + per-module tracking + ModelValidator integration
Pending
5. Reference Material
Model Version vs Maven Version
Model Version Maven Version Notes
4.0.0 Maven 3.x Classic POM model, no <sources> element
4.1.0 Maven 4.x Introduces <sources> element
4.2.0 Future Reserved for future use
The <sources> element requires model version 4.1.0:
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0
https://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
...
</project>
The Maven 4.x Unified Source Model
In Maven 4.x, all source types can be configured via <sources>:
<sources>
<!-- Java sources -->
<source>
<scope>main</scope>
<lang>java</lang>
<module>org.foo.bar</module>
</source>
<!-- Resources -->
<source>
<scope>main</scope>
<lang>resources</lang>
<module>org.foo.bar</module>
</source>
</sources>
The <lang> element accepts:
java - compiled by compiler plugin (default if omitted)
resources - processed by resources plugin
script - deprecated, use resources instead
Extensible via LanguageProvider SPI for other languages (Kotlin, Groovy, etc.).
Processing Flow
┌─────────────────────────────────────────────────────────────────┐
│ POM Model │
│ <sources> │
│ <source><lang>java</lang><module>X</module></source> │
│ <source><lang>resources</lang><module>X</module></source> │
│ </sources> │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ DefaultProjectBuilder │
│ Creates SourceRoot objects from <source> elements │
│ Adds to project.addSourceRoot(...) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ MavenProject │
│ getEnabledSourceRoots(ProjectScope scope, Language language) │
│ - Plugins query this to find relevant sources │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Compiler Plugin │ │ Resources Plugin │
│ Queries: lang=java │ │ Queries: lang=resources │
└─────────────────────────┘ └─────────────────────────┘
Detecting Explicit vs Implicit Configuration
To detect whether <sourceDirectory> was explicitly configured (vs inherited
from Super POM):
// Option 1: Compare to known Super POM default
String superPomDefault = "${project.basedir}/src/main/java";
String resolvedDefault = baseDir.resolve("src/main/java").toString();
boolean isExplicit = !build.getSourceDirectory().equals(resolvedDefault);
// Option 2: Use InputLocation tracking
InputLocation location = build.getLocation("sourceDirectory");
boolean isExplicit = location != null && !isSuperPomLocation(location);
Where to Implement Validation
Option Location Pros Cons
ModelValidator (Recommended) validateEffectiveModel() Line numbers,
early, standard Values not fully resolved
DefaultProjectBuilder initProject() All values resolved Later, less
standard
Lifecycle phase validate - Too late
Related Code Locations
impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java
impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.java
impl/maven-impl/src/main/resources/org/apache/maven/model/pom-4.0.0.xml (Super
POM 4.0)
impl/maven-impl/src/main/resources/org/apache/maven/model/pom-4.1.0.xml (Super
POM 4.1)
Open Questions
Should we validate at validateRawModel() or validateEffectiveModel()?
How to reliably detect "explicit" configuration (InputLocation vs value
comparison)?
Should the filesystem warning (R4) be optional/configurable?