DUE TO SPAM, SIGN-UP IS DISABLED. Goto Selfserve wiki signup and request an account.
...
This document focuses exclusively on the Maven 4.x core implementation (master branch of 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
| Anchor | ||||
|---|---|---|---|---|
|
Compiler Plugin Compensation
...
The goal should be to implement proper validation in the Maven core, providing early, consistent feedback with precise error locations via ModelProblem.
| Anchor | ||||
|---|---|---|---|---|
|
Origin of This Discussion
...
| Code Block | ||||
|---|---|---|---|---|
| ||||
<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:
...
| Code Block | ||||
|---|---|---|---|---|
| ||||
<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).
...
| Code Block | ||||
|---|---|---|---|---|
| ||||
<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.
...
| Code Block | ||||
|---|---|---|---|---|
| ||||
<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 |
...
Analysis
Core Design Question
...
| Code Block | ||
|---|---|---|
| ||
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) |
Strict vs Lenient Approach
...
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:
...
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 |
...
Current Implementation Status
...
| Code Block | ||||
|---|---|---|---|---|
| ||||
// 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) | 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.
...
Implementation Approaches
| Anchor | ||||
|---|---|---|---|---|
|
Phase 1: Module-Aware Resource Handling (Implemented)
...
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:
...
Per-module tracking instead of project-level booleans:
Code Block language java collapse false 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
ModelProblemwith line numbers and severity - Early feedback during POM processing
- Move conflict detection to
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>definitionERROR "Duplicate source for scope='X', lang='Y', module='Z'" R4 <sources>configured but classic dir existsWARN "Directory 'src/main/java' exists but will be ignored" Fix test-only modular project injection (Problem 2):
Code Block language java collapse false // Don't inject legacy main sources for modular projects if (!hasMain && !isModularProject) { project.addCompileSourceRoot(build.getSourceDirectory()); }Warning when explicit configuration is ignored (Problem 4):
Code Block language java collapse false if (hasMain && isExplicitSourceDirectory(build)) { // Report ModelProblem with line number }InputLocation-based detection for explicit vs inherited configuration:
Code Block language java collapse false 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 (PR 11505) |
| Phase 2 | Full validation + per-module tracking + ModelValidator integration | Pending |
...
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:
...
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.javaimpl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.javaimpl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.javaimpl/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)
...