This document captures the discussion about validating <sources> configuration in Maven 4.x.
This document focuses exclusively on the Maven 4.x core implementation (master branch of apache/maven). Specifically:
<sources> element introduced in POM model version 4.1.0DefaultProjectBuilder.javaSome 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
However, relying on plugin-level validation is suboptimal:
The goal should be to implement proper validation in the Maven core, providing early, consistent feedback with precise error locations via ModelProblem.
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 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 and revealed the broader set of validation and configuration handling issues documented here.
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.
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) for drawbacks of this approach.
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
Impact:
src/main/java doesn't exist → probably harmless (empty directory)src/main/java exists with old/unrelated code → it gets compiled!Use cases affected:
Note: Unlike Problem 1, this is not caught by the compiler plugin - it silently succeeds but with potentially wrong behavior.
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 for the implementation.
Workaround (no longer needed): See Origin of This Discussion for how to configure the maven-resources-plugin with explicit executions for each module.
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 |
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.
When a project uses the new <sources> element with modular configuration (<module> element), the modular approach should take precedence:
<sources> with <module>) demand proper modular layout and behavior<sources> without <module>) should not be used (error or warning) in a modular project<sourceDirectory>, <resources>) is used only as fallback when no <sources> are configuredThis principle ensures:
From DefaultSourceRoot.fromModel() — abbreviated pseudo code of the current implementation:
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) |
Two approaches are possible for handling configuration conflicts:
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" |
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" |
| 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 |
Approach A (Strict) is recommended for new projects because:
Approach B (Lenient) could be considered if:
This matrix shows all configuration combinations and their expected behavior under each approach.
| 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 |
| # | 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 ⚠️ |
| # | 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 |
As of commit 25c80d8
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));
} |
| 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.
<source> elements and adding them as source roots<sourceDirectory> when no Java sources in <sources>Goal: Enable modular resource handling without requiring explicit maven-resources-plugin configuration.
Status: ✅ Implemented in PR 11505
Implementation:
Resource tracking via <sources>: (Lines 672-673)
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, extractModules() at L1256)
Set<String> modules = extractModules(sources); boolean isModularProject = !modules.isEmpty(); |
Module-aware resource injection for modular projects: (Lines 729-786, createModularResourceRoot() at L1275)
<sources>: use them (already added during iteration)<sources>: inject module-aware defaults for each moduleModelProblem) if legacy <resources> is present but ignoredif (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)
hasOnlySuperPomDefaults() checks if <resources> contains only inherited defaultsPriority 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/resourcessrc/org.foo.moduleB/main/resourcesWhat's NOT Addressed in This Proposal:
<sourceDirectory> is ignoredGoal: 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):
validateEffectiveModel()ModelProblem with line numbers and severityStrict 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) |
| Phase | Scope | Status |
|---|---|---|
| Phase 1 | Module-aware resource handling | ✅ Implemented (PR 11505) |
| Phase 2 | Full validation + per-module tracking + ModelValidator integration | Pending |
| 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> |
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 pluginscript - deprecated, use resources insteadExtensible via LanguageProvider SPI for other languages (Kotlin, Groovy, etc.).
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────┘ └─────────────────────────┘ |
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); |
| 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 |
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)validateRawModel() or validateEffectiveModel()?