Skip to content

Conversation

@gnodet
Copy link
Contributor

@gnodet gnodet commented Nov 5, 2025

This PR fixes a regression in Maven 4 where resource targetPath was being resolved relative to the project base directory instead of the output directory (target/classes or target/test-classes).

Problem

In Maven 3.x, when a resource has a relative targetPath, it's resolved relative to the output directory. For example:

<resource>
  <directory>${project.basedir}/rest</directory>
  <targetPath>target-dir</targetPath>
  <includes>
    <include>**/*.yml</include>
  </includes>
</resource>

In Maven 3.x, files would be copied to target/classes/target-dir/, but in Maven 4, they were being copied to target-dir/ (relative to project root).

Root Cause

The bug was in the DefaultSourceRoot constructor that takes a Resource parameter. On line 172, the targetPath was being resolved as:

nonBlank(resource.getTargetPath()).map(baseDir::resolve).orElse(null)

This resolves the targetPath relative to the base directory, not the output directory.

Solution

  1. Created a new constructor in DefaultSourceRoot.java that accepts an outputDir parameter and correctly resolves targetPath relative to the output directory
  2. Deprecated the old constructor to maintain backward compatibility
  3. Updated all call sites to use the new constructor:
    • DefaultProjectBuilder.java
    • ConnectedResource.java
    • MavenProject.java
  4. Updated unit tests in DefaultSourceRootTest.java to verify the correct behavior
  5. Created an integration test MavenITgh11381ResourceTargetPathTest.java to verify the fix works end-to-end

Testing

The integration test verifies that:

  • Resources with relative targetPath are copied to the correct location under the output directory
  • The fix works for both main and test scopes

Test results:

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Fixes #11381


Pull Request opened by Augment Code with guidance from the PR author

… output directory

This commit fixes a regression in Maven 4 where resource targetPath was being
resolved relative to the project base directory instead of the output directory
(target/classes or target/test-classes).

The issue was in the DefaultSourceRoot constructor that takes a Resource parameter.
It was resolving targetPath relative to baseDir instead of the output directory.

Changes made:
- Added a new constructor to DefaultSourceRoot that accepts an outputDir parameter
- Deprecated the old constructor to maintain backward compatibility
- Updated all call sites in DefaultProjectBuilder, ConnectedResource, and MavenProject
  to use the new constructor with the appropriate output directory
- Updated unit tests in DefaultSourceRootTest to verify correct behavior
- Added integration test MavenITgh11381ResourceTargetPathTest to verify the fix

This restores Maven 3.x behavior where relative targetPath values in resources
are correctly resolved relative to the output directory.

Fixes apache#11381
…elative targetPath

This commit fixes two issues in ConnectedResource:

1. Handle null output directories: When the Build object doesn't have output
   directories set (e.g., in tests), use default values (target/classes for
   main scope, target/test-classes for test scope).

2. Compute relative targetPath: The SourceRoot stores the targetPath as a
   resolved path (absolute or relative to baseDir), but the Resource model
   expects it to be relative to the output directory. Added a helper method
   computeRelativeTargetPath() to extract the relative path from the resolved
   path by relativizing it against the output directory.

Also updated ResourceIncludeTest to:
- Set build output directories in setUp() method
- Use the new DefaultSourceRoot constructor with output directory parameter
  instead of the deprecated constructor

This ensures that the targetPath is correctly preserved through the conversion
chain from Resource -> SourceRoot -> ConnectedResource.
@gnodet gnodet added bug Something isn't working backport-to-4.0.x labels Nov 5, 2025
@gnodet gnodet changed the title [GH-11381] Fix resource targetPath resolution to be relative to output directory Fix resource targetPath resolution to be relative to output directory (fixes #11381) Nov 5, 2025
@gnodet gnodet requested a review from desruisseaux November 5, 2025 21:23
@gnodet gnodet marked this pull request as draft November 5, 2025 22:43
@gnodet
Copy link
Contributor Author

gnodet commented Nov 6, 2025

This does not look correct to me, I need to have another look.

The root cause of the issue was that DefaultSourceRoot was resolving the
targetPath against baseDir (or baseDir + outputDir), which required complex
logic in ConnectedResource to extract the relative path back out.

This commit simplifies the approach by keeping the targetPath as a relative
path in DefaultSourceRoot (using Path::of instead of resolving it). This
eliminates the need for:
- The outputDir parameter in the DefaultSourceRoot constructor
- The computeRelativeTargetPath() method in ConnectedResource
- Complex path resolution and relativization logic

Changes:
- DefaultSourceRoot: Keep targetPath as relative path using Path::of
- DefaultSourceRoot: Remove the outputDir parameter from constructor
- DefaultSourceRoot: Remove the deprecated constructor (both constructors
  are now identical)
- ConnectedResource: Simplify to just convert targetPath to string
- ConnectedResource: Remove outputDir parameter from updateProjectSourceRoot
- DefaultSourceRootTest: Update tests to expect relative paths
- ResourceIncludeTest: Remove outputDir parameter from test

This ensures that the targetPath is correctly preserved through the conversion
chain from Resource -> SourceRoot -> ConnectedResource.
…rojectBuilder

Since the targetPath is now kept as a relative path in DefaultSourceRoot,
the outputDir parameter is no longer needed when creating DefaultSourceRoot
instances from Resources.

Changes:
- MavenProject.addResource(): Remove outputDir calculation and parameter
- DefaultProjectBuilder: Remove outputDir parameter from DefaultSourceRoot calls
@gnodet gnodet marked this pull request as ready for review November 6, 2025 09:08
@gnodet gnodet requested a review from cstamas November 6, 2025 09:08
resource.getExcludes(),
Boolean.parseBoolean(resource.getFiltering()),
nonBlank(resource.getTargetPath()).map(baseDir::resolve).orElse(null),
nonBlank(resource.getTargetPath()).map(Path::of).orElse(null),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is CWD relative?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path.of(String) creates a path relative the to current working directory, which is not the desired output directory. I think that DefaultSourceRoot is okay, and the change rather needs to be applied in the codes that call this constructor. The problem seems to be in the following classes:

  • org.apache.maven.project.ConnectedResource line 124.
  • org.apache.maven.project.DefaultProjectBuilder lines 704 and 707.
  • org.apache.maven.project.MavenProject line 840

The above-cited codes set the first parameter to project.getBaseDirectory() while it should be project.getBuild().getOutputDirectory().

Note: the baseDir parameter name of DefaultSourceRoot is misleading. The Javadoc said "the base directory for resolving relative paths", which can be the baseDir of the Maven project but not necessarily. It should be understood as any base directory for resolving paths. This parameter could be renamed parentDir if baseDir is too misleading.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is maybe resource.getTargetPath() absolute? So CWD does not matter?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be absolute, but I'm not sure that this guaranteed. It may depend on which code path as leaded to this constructor. I don't think that there is any reason to not be robust. We just need to find all calls to this constructor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And maybe rename the baseDir parameter as outputDir (but without change in the call to Path.resolve(String)).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fwiw, the expectation in Maven 3 is that Resource.getTargetPath() will be resolved against either project.build.outputDirectory or project.build.testOutputDirectory.

Yes I agree, but the replacement of baseDir::resolve by Path::of does not fix that. The correction is rather to fix the baseDir argument given in calls to the DefaultSourceRoot constructor. Actually, the first commit of this pull request seems correct to me, but it seems to have been reverted in subsequent commits.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you think it does not ?
It makes the path truly relative, and later resolved against the correct directory.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes the path truly relative, and later resolved against the correct directory.

Ah, I see. The responsibility of resolving the path is shifted to the SourceRoot user instead of the SourceRoot constructor. But why not resolving at construction time? It would probably be less code to check for correctness.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I think that would be desired behavior, but then we need to make it truly relative in the ConnectedResource to not break the existing plugin.
So the behavior would be different between mvn3 and mvn4 api, but that's fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the behavior would be different between mvn3 and mvn4 api

If the difference would be that mvn3 would be relative to target while mvn4 would be relative to baseDir, I think that this difference was a bug partially fixed in #11322, but we forgot the case of resources in that fix.

I will prepare an alternative pull request tomorrow for illustrating the fix that I propose. The actual pull request at the end may be, possibly, a mix of the two.

Based on PR comments, the solution has been updated:

1. SourceRoot.targetPath() returns a Path relative to the project's basedir
   (e.g., 'target/classes/custom-output')
   - Reverted DefaultSourceRoot to resolve targetPath against baseDir

2. ConnectedResource.getTargetPath() (Maven 3 API) returns a path relative to
   the output directory (e.g., 'custom-output')
   - Added computeRelativeTargetPath() method to make the path relative to
     the output directory when converting from SourceRoot to Resource

This maintains backward compatibility with Maven 3 API while using the new
Maven 4 API internally.

Changes:
- DefaultSourceRoot: Reverted to resolve targetPath against baseDir
- ConnectedResource: Added computeRelativeTargetPath() to convert from
  basedir-relative to outputdir-relative paths
- DefaultSourceRootTest: Updated tests to expect basedir-relative paths
- ResourceIncludeTest: Tests still pass with the new conversion logic
@gnodet gnodet force-pushed the fix-gh-11381-resource-targetpath branch from 17d51e1 to 708e059 Compare November 6, 2025 21:53
/**
* Computes the targetPath relative to the output directory.
* In Maven 3 API, Resource.getTargetPath() is expected to be relative to the output directory
* (e.g., "custom-output"), while SourceRoot.targetPath() is relative to the project basedir
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while SourceRoot.targetPath() is relative to the project basedir

Actually it was a bug. The specification that we wrote in maven.mdo said that targetPath shall be relative to the target directory. Fixing that bug was the subject of #11322, but that correction was incomplete. We could said that this pull request #11394 completes #11322.

…directory

This commit fixes the regression where resources with a relative targetPath
were being copied to the project root instead of relative to the output
directory (target/classes or target/test-classes).

Changes:

1. DefaultSourceRoot.fromModel: Store targetPath as a relative path instead
   of resolving it against baseDir and outputDir. This ensures that
   SourceRoot.targetPath() returns a relative path as intended by the
   Maven 4 API javadoc.

2. ConnectedResource.computeRelativeTargetPath: Simplified to directly
   return the relative targetPath from SourceRoot, since it's now always
   stored as relative.

3. Updated tests to expect relative paths from SourceRoot.targetPath().

Maven 4 API Conformance:
- SourceRoot.targetPath() returns an Optional<Path> containing the explicit
  target path, which should be relative to the output directory (or absolute
  if explicitly specified as absolute).
- SourceRoot.targetPath(Project) resolves this relative path against the
  project's output directory to produce an absolute path.

Maven 3 Compatibility:
- Resource.getTargetPath() in Maven 3 was always relative to the output
  directory. This behavior is preserved by storing targetPath as relative
  in SourceRoot and converting it back to relative for the Resource API
  via ConnectedResource.

Example: With <targetPath>custom-dir</targetPath>:
- Maven 3: Resources copied to target/classes/custom-dir
- Maven 4 (before fix): Resources copied to project-root/custom-dir
- Maven 4 (after fix): Resources copied to target/classes/custom-dir

Fixes apache#11381
@gnodet
Copy link
Contributor Author

gnodet commented Nov 6, 2025

Implementation Details

I've implemented a fix for this issue that ensures SourceRoot.targetPath() and ConnectedResource behave correctly according to the Maven 4 API while preserving Maven 3 compatibility.

Root Cause

The regression occurred because DefaultSourceRoot.fromModel() was storing targetPath as an absolute path (resolved against baseDir and outputDir), when it should have been stored as a relative path according to the Maven 4 API design.

Maven 4 API Design

The Maven 4 API defines two methods for SourceRoot:

  1. targetPath() - Returns an Optional<Path> containing the explicit target path. According to the javadoc and the presence of the resolution method below, this should be a relative path (or absolute if explicitly specified as absolute).

  2. targetPath(Project) - Returns the resolved absolute path by taking the relative targetPath() and resolving it against the project's output directory:

    Path base = project.getOutputDirectory(scope());
    return targetPath.map(base::resolve).orElse(base);

This two-method design clearly indicates that targetPath() should return a relative path that gets resolved by targetPath(Project).

Implementation

Fixed DefaultSourceRoot.fromModel() (impl/maven-impl)

// Before (incorrect):
nonBlank(source.getTargetPath())
    .map((targetPath) -> baseDir.resolve(outputDir.apply(scope)).resolve(targetPath))
    .orElse(null)

// After (correct):
nonBlank(source.getTargetPath()).map(Path::of).orElse(null)

Now targetPath() returns a relative path as intended, and targetPath(Project) handles the resolution.

Maven 3 Compatibility

In Maven 3, Resource.getTargetPath() was always relative to the output directory. This behavior is preserved:

  • SourceRoot.targetPath() stores the path as relative (e.g., "custom-dir")
  • ConnectedResource converts it back to a String for the Maven 3 Resource API
  • Resources are copied to target/classes/custom-dir (Maven 3 behavior) ✅

Example

With <targetPath>custom-dir</targetPath> in a resource configuration:

  • Maven 3: Resources copied to target/classes/custom-dir
  • Maven 4 (before fix): Resources copied to project-root/custom-dir
  • Maven 4 (after fix): Resources copied to target/classes/custom-dir

Testing

All existing tests pass, including:

  • DefaultSourceRootTest - Updated to expect relative paths from targetPath()
  • ResourceIncludeTest - Validates the Maven 3 compatibility layer
  • Integration test MavenITgh11381ResourceTargetPathTest should now pass

The fix is minimal, focused, and aligns with the Maven 4 API design while maintaining backward compatibility.

Enhanced the javadoc for SourceRoot.targetPath() and targetPath(Project)
to make it explicit that:

1. targetPath() returns a path that is typically relative to the output
   directory (e.g., 'custom-dir' or 'META-INF/resources'), but may be
   absolute if explicitly specified.

2. targetPath(Project) returns the fully resolved absolute path where
   files should be copied, with clear examples of how it handles:
   - Empty Optional: returns the default output directory
   - Relative path: resolves against the output directory
   - Absolute path: returns unchanged

These clarifications help implementers understand the intended behavior
and prevent regressions like apacheGH-11381 where targetPath was incorrectly
stored as an absolute path.
…ctory()

This commit improves the documentation for resource targetPath handling to clarify
the Maven 4 API semantics while preserving Maven 3 backward compatibility.

Key documentation improvements:

1. SourceRoot.targetPath():
   - Clarifies that relative paths are resolved relative to the output directory
     (target/classes for MAIN scope, target/test-classes for TEST scope)
   - Documents that absolute paths are used as-is
   - Explicitly states Maven 3 compatibility is maintained
   - Adds concrete examples showing path resolution

2. SourceRoot.targetPath(Project):
   - Documents the complete resolution algorithm step-by-step
   - Provides detailed examples with actual paths
   - Cross-references Project.getOutputDirectory()

3. Project.getOutputDirectory():
   - Clarifies that it returns the directory for both compiled classes AND resources
   - Documents its role in SourceRoot targetPath resolution
   - Explicitly states Maven 3 compatibility semantics

4. DefaultSourceRoot constructor from Resource:
   - Documents that targetPath is stored as-is (not resolved against basedir)
   - Clarifies that baseDir parameter is only used for source directory resolution
   - Explains how this preserves Maven 3.x behavior

Implementation details:
- DefaultSourceRoot stores targetPath as provided (relative or absolute)
- ConnectedResource extracts targetPath as-is for maven-resources-plugin
- Resolution to absolute paths happens via SourceRoot.targetPath(Project)
- This maintains Maven 3 behavior where resource targetPath is relative to
  the output directory, not the project base directory

Related to apache#11381
@gnodet
Copy link
Contributor Author

gnodet commented Nov 7, 2025

Latest Update: Enhanced Javadoc for API Clarity

I've just pushed an update that significantly improves the documentation to clarify the Maven 4 API semantics while making the Maven 3 backward compatibility guarantees explicit.

What Changed

Enhanced javadoc for three key methods:

  1. SourceRoot.targetPath() - Now clearly documents:

    • Relative paths are resolved relative to the output directory (not project basedir)
    • Absolute paths are used as-is
    • Empty/null means files go to output directory root
    • Explicitly states Maven 3 compatibility is maintained
  2. SourceRoot.targetPath(Project) - Now includes:

    • Step-by-step resolution algorithm
    • Concrete examples with actual paths
    • Cross-references to related methods
  3. Project.getOutputDirectory(ProjectScope) - Now clarifies:

    • Returns directory for both compiled classes AND resources
    • Its role in SourceRoot targetPath resolution
    • Maven 3 compatibility semantics
  4. DefaultSourceRoot constructor from Resource - Now documents:

    • targetPath is stored as-is (not resolved against basedir)
    • baseDir parameter is only used for source directory resolution
    • How this preserves Maven 3.x behavior

Why This Matters

The enhanced documentation makes it crystal clear that:

  • For Maven 4 API users: Use targetPath(Project) to get the fully resolved absolute path
  • For Maven 3 compatibility: The targetPath() method returns the relative path that plugins expect
  • For implementers: The separation of concerns between storage and resolution is now explicit

Implementation Verification

The documentation now matches the actual implementation:

DefaultSourceRoot stores targetPath as-is (relative or absolute)

ConnectedResource extracts targetPath as a relative path string

✅ maven-resources-plugin receives the relative path and resolves it (Maven 3 behavior)

✅ Maven 4 API consumers can use targetPath(Project) for absolute paths

See the detailed explanation in this comment on #11381 for the complete design rationale.

This commit significantly enhances the javadoc documentation to make the
targetPath handling semantics even more explicit and clear for all audiences:
API consumers, plugin developers, and implementers.

Key improvements:

1. SourceRoot.targetPath():
   - Explicitly states this returns the path AS CONFIGURED (no resolution)
   - Clearly distinguishes between storage (this method) and resolution (targetPath(Project))
   - Provides detailed "Return Value Semantics" section explaining empty/relative/absolute cases
   - Adds "Usage Guidance" section for different audiences:
     * Maven 4 API consumers: use targetPath(Project)
     * Maven 3 compatibility: use this method for legacy plugins
     * Implementers: store path as-is, don't resolve at storage time
   - Emphasizes Maven 3 compatibility with explicit reference to
     project.build.outputDirectory and project.build.testOutputDirectory

2. SourceRoot.targetPath(Project):
   - Adds "Purpose" section explaining this is THE resolution method
   - Provides detailed step-by-step resolution algorithm with all cases
   - Includes comprehensive table with concrete examples showing:
     * Configuration input
     * Output directory
     * Final resolved result
     * Explanation of each case
   - Adds "Relationship to targetPath()" section explaining storage vs resolution
   - Includes implementation note with equivalent code for clarity

3. Project.getOutputDirectory():
   - Adds comprehensive table showing scope-to-directory mapping
   - Includes "Role in SourceRoot Path Resolution" section with concrete examples
   - Expands Maven 3 compatibility section with actual XML configuration example
   - Shows how maven-resources-plugin uses this in Maven 3
   - Provides code examples demonstrating usage

4. HTML5 Compliance:
   - Replaced deprecated table attributes (cellpadding, cellspacing, border)
   - Uses modern HTML5 table styling with class="striped"

The refined documentation makes it crystal clear that:
- targetPath() is for STORAGE (returns path as configured)
- targetPath(Project) is for RESOLUTION (returns absolute path)
- getOutputDirectory() provides the base for resolution
- Maven 3 compatibility is maintained by storing relative paths
- Different audiences have clear guidance on which method to use

Related to apache#11381
@gnodet
Copy link
Contributor Author

gnodet commented Nov 7, 2025

Further Javadoc Refinements for Maximum Clarity

I've pushed another update that significantly refines the Maven 4 API javadoc to make it even more explicit and clear. The documentation now leaves no ambiguity about how targetPath handling works.

Key Enhancements

1. SourceRoot.targetPath() - The Storage Method

New sections added:

  • "Important" callout: Explicitly states this returns the path AS CONFIGURED (no resolution)
  • "Return Value Semantics": Detailed explanation of what each return value means:
    • Empty Optional → files go to output directory root
    • Relative Path → intended to be resolved against output directory (resolution happens in targetPath(Project))
    • Absolute Path → used as-is
  • "Usage Guidance": Clear instructions for three audiences:
    • Maven 4 API consumers: use targetPath(Project) instead
    • Maven 3 compatibility layer: use this method for legacy plugins
    • Implementers: store path as-is, don't resolve at storage time

Why this matters: Makes it crystal clear that this method is for STORAGE, not RESOLUTION.

2. SourceRoot.targetPath(Project) - The Resolution Method

New sections added:

  • "Purpose": Explicitly states this is THE method for path resolution
  • Detailed algorithm: Step-by-step explanation with all edge cases
  • Comprehensive table: Shows concrete examples with:
    • Configuration input (what targetPath() returns)
    • Output directory (from getOutputDirectory())
    • Final result (what this method returns)
    • Explanation of each case
  • "Relationship to targetPath()": Explains the storage vs resolution separation
  • "Implementation Note": Shows equivalent code for clarity

Example from the table:

Configuration Output Directory Result Explanation
Optional.empty() /home/user/myproject/target/classes /home/user/myproject/target/classes No explicit path → use output directory
Optional.of(Path.of("META-INF")) /home/user/myproject/target/classes /home/user/myproject/target/classes/META-INF Relative path → resolve against output directory
Optional.of(Path.of("/tmp/custom")) /home/user/myproject/target/classes /tmp/custom Absolute path → use as-is

Why this matters: Developers can see exactly how each case is handled with concrete examples.

3. Project.getOutputDirectory() - The Base Directory Provider

New sections added:

  • "Purpose": Explains this provides the base for path resolution
  • Comprehensive table: Maps scope to directory with build configuration and contents
  • "Role in SourceRoot Path Resolution": Shows concrete examples:
    • Main resources with targetPath="META-INF"target/classes/META-INF
    • Test resources with targetPath="test-data"target/test-classes/test-data
  • Expanded Maven 3 compatibility: Includes actual XML configuration example showing how maven-resources-plugin uses this
  • Code examples: Demonstrates usage for all three scopes

Why this matters: Makes the connection between this method and SourceRoot.targetPath(Project) explicit.

HTML5 Compliance

Fixed javadoc generation errors by replacing deprecated HTML attributes:

  • <table border="1" cellpadding="5" cellspacing="0">
  • <table class="striped">

Javadoc now builds cleanly without errors.

Documentation Philosophy

The refined documentation follows a clear pattern:

  1. Separation of Concerns:

    • targetPath() = STORAGE (returns as configured)
    • targetPath(Project) = RESOLUTION (returns absolute path)
    • getOutputDirectory() = BASE PROVIDER (provides resolution base)
  2. Audience-Specific Guidance:

    • Maven 4 API consumers know to use targetPath(Project)
    • Maven 3 compatibility layer knows to use targetPath()
    • Implementers know to store without resolving
  3. Concrete Examples:

    • Tables with actual paths
    • XML configuration snippets
    • Code examples
  4. Explicit Contracts:

    • What each method returns
    • When to use which method
    • How methods relate to each other

Verification

✅ Javadoc builds successfully without errors

✅ All three methods have coherent, cross-referenced documentation

✅ Maven 3 compatibility guarantees are explicit

✅ Maven 4 API semantics are clear

The documentation now provides complete clarity on the targetPath handling design, making it easy for developers to understand and use the API correctly.

@gnodet
Copy link
Contributor Author

gnodet commented Nov 7, 2025

Commit Description for Merge

Here's a comprehensive commit message explaining the chosen fix:


Fix resource targetPath resolution to maintain Maven 3 compatibility

This commit fixes a regression in Maven 4 where resource targetPath was being
resolved relative to the project base directory instead of the output directory,
breaking compatibility with Maven 3 and causing resources to be copied to
incorrect locations.

## Problem

In Maven 3.x, when a resource has a relative targetPath, it is resolved relative
to the output directory (target/classes for main, target/test-classes for test).

Example configuration:
```xml
<resource>
  <directory>${project.basedir}/rest</directory>
  <targetPath>target-dir</targetPath>
</resource>

Maven 3 behavior: Files copied to target/classes/target-dir ✓
Maven 4 behavior (before fix): Files copied to target-dir (project root) ✗

Root Cause

The DefaultSourceRoot constructor that creates SourceRoot instances from Resource
model objects was incorrectly resolving the targetPath against the project base
directory instead of storing it as-is for later resolution against the output
directory.

Solution Design

The fix implements a clear separation of concerns between storage and resolution:

1. Storage Layer (SourceRoot.targetPath())

This method returns the targetPath exactly as specified in the configuration:

  • Relative paths (e.g., "META-INF/resources") are stored as relative paths
  • Absolute paths (e.g., "/tmp/custom") are stored as absolute paths
  • Empty/null means no explicit target path was specified

No resolution happens at this layer. The path is stored as-is.

2. Resolution Layer (SourceRoot.targetPath(Project))

This method performs the actual path resolution:

Algorithm:

  1. Get the configured targetPath from targetPath()
  2. If absolute → return it unchanged
  3. Get the output directory for the source root's scope:
    • ProjectScope.MAIN → target/classes
    • ProjectScope.TEST → target/test-classes
  4. If targetPath is empty → return output directory
  5. If targetPath is relative → resolve against output directory

This matches Maven 3 behavior exactly.

3. Implementation Changes

DefaultSourceRoot.java:

  • Constructor from Resource now stores targetPath as-is (line 176):
    nonBlank(resource.getTargetPath()).map(Path::of).orElse(null)
  • Does NOT resolve against baseDir (which would make it project-relative)
  • Added comprehensive javadoc explaining this preserves Maven 3 behavior

ConnectedResource.java:

  • Extracts targetPath as a string (still relative) for maven-resources-plugin
  • The plugin receives the relative path and resolves it (Maven 3 behavior)

Project.getOutputDirectory():

  • Returns the scope-specific output directory
  • Used by SourceRoot.targetPath(Project) for resolution
  • Documented as the base for targetPath resolution

4. Maven 4 API Documentation

Extensive javadoc enhancements make the design explicit:

SourceRoot.targetPath():

  • Clearly states it returns path AS CONFIGURED (no resolution)
  • Documents return value semantics for empty/relative/absolute cases
  • Provides usage guidance for different audiences:
    • Maven 4 API consumers: use targetPath(Project)
    • Maven 3 compatibility: use this method
    • Implementers: store as-is, don't resolve

SourceRoot.targetPath(Project):

  • Documents complete resolution algorithm
  • Provides table with concrete examples
  • Explains relationship to targetPath() (storage vs resolution)

Project.getOutputDirectory():

  • Documents scope-to-directory mapping
  • Explains role in targetPath resolution
  • Shows Maven 3 compatibility with XML examples

Why This Approach

This design was chosen because:

  1. Preserves Maven 3 Compatibility:

    • maven-resources-plugin receives relative paths (as in Maven 3)
    • Plugin resolves against output directory (as in Maven 3)
    • Existing POMs work without modification
  2. Clean Maven 4 API:

    • Clear separation: storage (targetPath) vs resolution (targetPath(Project))
    • Maven 4 consumers get absolute paths via targetPath(Project)
    • Legacy plugins get relative paths via targetPath()
  3. No Premature Resolution:

    • Paths stored as configured (relative or absolute)
    • Resolution happens when needed, with proper context (Project)
    • Allows different resolution strategies for different scopes
  4. Explicit Contracts:

    • Comprehensive javadoc explains semantics
    • Clear guidance for API consumers, plugin developers, implementers
    • Examples show expected behavior

Testing

Integration test MavenITgh11381ResourceTargetPathTest verifies:

  • Resources with relative targetPath are copied to correct location
  • Works for both main and test scopes
  • Maintains Maven 3 compatibility

Impact

This fix:
✓ Restores Maven 3 compatibility for resource targetPath
✓ Provides clean Maven 4 API with clear semantics
✓ Maintains backward compatibility with existing POMs
✓ Enables maven-resources-plugin to work correctly
✓ Documents the design for future maintainers

Fixes #11381


---

Feel free to use this as-is or adapt it for the merge commit!

@gnodet gnodet merged commit 9b95526 into apache:master Nov 7, 2025
19 checks passed
@github-actions github-actions bot added this to the 4.1.0 milestone Nov 7, 2025
gnodet added a commit to gnodet/maven that referenced this pull request Nov 7, 2025
…fixes apache#11381) (apache#11394)

This commit fixes the regression where resources with a relative targetPath
were being copied to the project root instead of relative to the output
directory (target/classes or target/test-classes).

Changes:

1. DefaultSourceRoot.fromModel: Store targetPath as a relative path instead
   of resolving it against baseDir and outputDir. This ensures that
   SourceRoot.targetPath() returns a relative path as intended by the
   Maven 4 API javadoc.

2. ConnectedResource.computeRelativeTargetPath: Simplified to directly
   return the relative targetPath from SourceRoot, since it's now always
   stored as relative.

3. Updated tests to expect relative paths from SourceRoot.targetPath().

Maven 4 API Conformance:
- SourceRoot.targetPath() returns an Optional<Path> containing the explicit
  target path, which should be relative to the output directory (or absolute
  if explicitly specified as absolute).
- SourceRoot.targetPath(Project) resolves this relative path against the
  project's output directory to produce an absolute path.

Maven 3 Compatibility:
- Resource.getTargetPath() in Maven 3 was always relative to the output
  directory. This behavior is preserved by storing targetPath as relative
  in SourceRoot and converting it back to relative for the Resource API
  via ConnectedResource.

Example: With <targetPath>custom-dir</targetPath>:
- Maven 3: Resources copied to target/classes/custom-dir
- Maven 4 (before fix): Resources copied to project-root/custom-dir
- Maven 4 (after fix): Resources copied to target/classes/custom-dir

Fixes apache#11381

(cherry picked from commit 9b95526)
@gnodet
Copy link
Contributor Author

gnodet commented Nov 7, 2025

💚 All backports created successfully

Status Branch Result
maven-4.0.x

Questions ?

Please refer to the Backport tool documentation

gnodet added a commit to gnodet/maven that referenced this pull request Nov 7, 2025
…fixes apache#11381) (apache#11394)

This commit fixes the regression where resources with a relative targetPath
were being copied to the project root instead of relative to the output
directory (target/classes or target/test-classes).

Changes:

1. DefaultSourceRoot.fromModel: Store targetPath as a relative path instead
   of resolving it against baseDir and outputDir. This ensures that
   SourceRoot.targetPath() returns a relative path as intended by the
   Maven 4 API javadoc.

2. ConnectedResource.computeRelativeTargetPath: Simplified to directly
   return the relative targetPath from SourceRoot, since it's now always
   stored as relative.

3. Updated tests to expect relative paths from SourceRoot.targetPath().

Maven 4 API Conformance:
- SourceRoot.targetPath() returns an Optional<Path> containing the explicit
  target path, which should be relative to the output directory (or absolute
  if explicitly specified as absolute).
- SourceRoot.targetPath(Project) resolves this relative path against the
  project's output directory to produce an absolute path.

Maven 3 Compatibility:
- Resource.getTargetPath() in Maven 3 was always relative to the output
  directory. This behavior is preserved by storing targetPath as relative
  in SourceRoot and converting it back to relative for the Resource API
  via ConnectedResource.

Example: With <targetPath>custom-dir</targetPath>:
- Maven 3: Resources copied to target/classes/custom-dir
- Maven 4 (before fix): Resources copied to project-root/custom-dir
- Maven 4 (after fix): Resources copied to target/classes/custom-dir

Fixes apache#11381

(cherry picked from commit 9b95526)
@gnodet gnodet deleted the fix-gh-11381-resource-targetpath branch November 7, 2025 13:12
gnodet added a commit that referenced this pull request Nov 7, 2025
…fixes #11381) (#11394) (#11406)

This commit fixes the regression where resources with a relative targetPath
were being copied to the project root instead of relative to the output
directory (target/classes or target/test-classes).

Changes:

1. DefaultSourceRoot.fromModel: Store targetPath as a relative path instead
   of resolving it against baseDir and outputDir. This ensures that
   SourceRoot.targetPath() returns a relative path as intended by the
   Maven 4 API javadoc.

2. ConnectedResource.computeRelativeTargetPath: Simplified to directly
   return the relative targetPath from SourceRoot, since it's now always
   stored as relative.

3. Updated tests to expect relative paths from SourceRoot.targetPath().

Maven 4 API Conformance:
- SourceRoot.targetPath() returns an Optional<Path> containing the explicit
  target path, which should be relative to the output directory (or absolute
  if explicitly specified as absolute).
- SourceRoot.targetPath(Project) resolves this relative path against the
  project's output directory to produce an absolute path.

Maven 3 Compatibility:
- Resource.getTargetPath() in Maven 3 was always relative to the output
  directory. This behavior is preserved by storing targetPath as relative
  in SourceRoot and converting it back to relative for the Resource API
  via ConnectedResource.

Example: With <targetPath>custom-dir</targetPath>:
- Maven 3: Resources copied to target/classes/custom-dir
- Maven 4 (before fix): Resources copied to project-root/custom-dir
- Maven 4 (after fix): Resources copied to target/classes/custom-dir

Fixes #11381

(cherry picked from commit 9b95526)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-to-4.0.x bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Maven 4] Maven resources plugin - relative targetPath is not the same as in Maven 3.9.x

3 participants