Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

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
compiler-plugin-compensation

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
origin-of-this-discussion

Origin of This Discussion

...

Code Block
languagexml
collapsefalse
<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 BehaviorActual Behavior
Early warning/error about inconsistent configurationSilent acceptance by DefaultProjectBuilder
Clear feedback during project buildDelayed failure at compile time

Current Workaround: The maven-compiler-plugin validates this at compile time and fails with:

...

Code Block
languagexml
collapsefalse
<build>
  <sources>
    <source>
      <scope>test</scope>
      <lang>java</lang>
      <module>org.foo.integrationTests</module>
    </source>
  </sources>
</build>
Expected BehaviorActual Behavior
No main sources (project is test-only)src/main/java silently added as main source
Test sources from src/org.foo.integrationTests/test/javaTest 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
languagexml
collapsefalse
<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 BehaviorActual Behavior
Resources from src/org.foo.moduleA/main/resourcesResources 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
languagexml
collapsefalse
<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 BehaviorActual Behavior
Warning: "sourceDirectory is ignored because sources are configured"Silent - user doesn't know their config is ignored
Clear feedback to userConfusion when src/custom/java isn't used

...

Analysis

Core Design Question

...

Code Block
collapsefalse
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>
}
ModuleDirectoryModeResult
NoNoClassicsrc/<scope>/<lang> (default)
NoYesClassic<directory> (override)
YesNoModularsrc/<module>/<scope>/<lang> (default)
YesYesModular<directory> (override, module = metadata)

Strict vs Lenient Approach

...

Configuration conflicts result in errors that fail the build.

RuleConditionSeverityMessage
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 elementsERROR"Cannot mix modular and non-modular sources. All <source> elements must either have a module or none."
R3Duplicate <source> (same scope/lang/module)ERROR"Duplicate source definition for scope='X', lang='Y', module='Z'"
R4<sources> configured but classic directory exists on filesystemWARN"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.

RuleConditionSeverityMessage
R1'<sources> present AND explicit <sourceDirectory>WARN"Explicit <sourceDirectory> will be ignored because <sources> is configured."
R2'<sources> with mixed module/no-module elementsWARN"Mixing modular and non-modular sources may lead to unexpected behavior."
R3Duplicate <source> (same scope/lang/module)ERROR"Duplicate source definition for scope='X', lang='Y', module='Z'"
R4<sources> configured but classic directory exists on filesystemWARN"Directory 'src/main/java' exists but will be ignored because <sources> is configured"

Comparison

AspectStrict (A)Lenient (B)
<sources> + explicit SD/TSDERRORWARN
Mixed module/no-moduleERRORWARN
Duplicate sourcesERRORERROR
Filesystem mismatchWARNWARN
Migration friendlinessLowerHigher
User confusion riskLowerHigher
Fail-fast principleYesNo

Recommendation

Approach A (Strict) is recommended for new projects because:

...

This matrix shows all configuration combinations and their expected behavior under each approach.

Legend

AbbreviationMeaning
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

#ConfigurationCurrentLenientStrictCompiler Plugin

No <sources> (classic mode)



1SD=implicit (Super POM)src/main/javaOKOK-
2SD=explicituser's pathOKOK-

<sources> present (new mode)



3S(no M), SD=implicitsrc/main/javaOKOK-
4S(no M), SD=explicitSilent ignoreWARNERRORNot caught
5S(M=X), SD=implicitsrc/X/main/javaOKOK-
6S(M=X), SD=explicitSilent ignoreWARNERRORNot caught
7S(M=X) + S(M=Y)Both pathsOKOK-
8S(M=X) + S(no M)Silent acceptWARNERRORCaught
9S(dir=custom, no M)custom pathOKOK-
10S(dir=custom, M=X)custom (M=meta)OKOK-
11Duplicate SSilent acceptERRORERRORNot caught
12S(M=X) + src/main/java existsSilent ignoreWARNWARNNot caught
13S(scope=test, M=X) onlyInjects src/main/javaNo injectNo injectNot caught ⚠️

Resources

#S ConfigR ConfigCurrentProposed (Phase 1)LenientStrict
1S(lang=resources)R=anyDuplicates!Use S, WARN if R explicitUse S, WARNUse S, WARN
2S(java, no M)R=implicitsrc/main/resourcessrc/main/resourcesOKOK
3S(java, no M)R=explicitUser's pathUser's pathOKOK
4S(java, M=X)R=implicitsrc/main/resourcesInject modularInject modularInject modular
5S(java, M=X)R=explicitUser's pathInject modular, WARNWARNERROR

...

Current Implementation Status

...

Code Block
languagejava
collapsefalse
// 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

IssueDescriptionCore StatusCompiler Plugin
No warningsExplicit <sourceDirectory> silently ignored when <sources> presentNot implementedNot caught
No mixed validationMixing S(M=X) with S(no M) silently acceptedNot implementedCaught
Test-only injectionsrc/main/java injected for test-only modular projectsNot implementedNot caught ⚠️
No duplicates checkSame source can be defined multiple timesNot implementedNot caught
No modular resourcesResources 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
phase-1

Phase 1: Module-Aware Resource Handling (Implemented)

...

Priority Hierarchy (Proposed):

PriorityConditionBehavior
1Modular project + resources in <sources>Use <sources> resources, warn if legacy present
2Modular project + no resources in <sources>Inject src/<module>/<scope>/resources for each module
3Classic project + resources in <sources>Use <sources> resources, warn if legacy present
4Classic project + no resources in <sources>Use legacy <resources> element

Example - Would Work Without Explicit Plugin Configuration:

...

  1. Per-module tracking instead of project-level booleans:

    Code Block
    languagejava
    collapsefalse
    record ModuleConfig(boolean hasMain, boolean hasTest, boolean hasMainResources, boolean hasTestResources) {}
    Map<String, ModuleConfig> moduleConfigs = new HashMap<>();
  2. Validation in ModelValidator (not DefaultProjectBuilder):

    • Move conflict detection to validateEffectiveModel()
    • Report ModelProblem with line numbers and severity
    • Early feedback during POM processing
  3. Strict validation rules (from Section 2.2):

    RuleConditionSeverityMessage
    R1<sources> + explicit <sourceDirectory>ERROR"Cannot combine <sources> with explicit <sourceDirectory>"
    R2Mixed modular/non-modular sourcesERROR"Cannot mix modular and non-modular sources"
    R3Duplicate <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"
  4. Fix test-only modular project injection (Problem 2):

    Code Block
    languagejava
    collapsefalse
    // Don't inject legacy main sources for modular projects
    if (!hasMain && !isModularProject) {
        project.addCompileSourceRoot(build.getSourceDirectory());
    }
  5. Warning when explicit configuration is ignored (Problem 4):

    Code Block
    languagejava
    collapsefalse
    if (hasMain && isExplicitSourceDirectory(build)) {
        // Report ModelProblem with line number
    }
  6. InputLocation-based detection for explicit vs inherited configuration:

    Code Block
    languagejava
    collapsefalse
    InputLocation location = build.getLocation("sourceDirectory");
    boolean isExplicit = location != null && !isSuperPomLocation(location);

Implementation Location:

ComponentResponsibility
DefaultModelValidatorValidation rules R1-R4, early feedback with line numbers
DefaultProjectBuilderSource root creation, fallback logic
DefaultSourceRootPath resolution (already implemented)

Implementation Timeline

PhaseScopeStatus
Phase 1Module-aware resource handling✅ Implemented (PR 11505)
Phase 2Full validation + per-module tracking + ModelValidator integrationPending

...

Reference Material

Model Version vs Maven Version

Model VersionMaven VersionNotes
4.0.0Maven 3.xClassic POM model, no <sources> element
4.1.0Maven 4.xIntroduces <sources> element
4.2.0FutureReserved for future use

The <sources> element requires model version 4.1.0:

...

Where to Implement Validation

OptionLocationProsCons
ModelValidator (Recommended)validateEffectiveModel()Line numbers, early, standardValues not fully resolved
DefaultProjectBuilderinitProject()All values resolvedLater, less standard
Lifecycle phasevalidate-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)

...