This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new 9b95526182 Fix resource targetPath resolution to be relative to output 
directory (fixes #11381) (#11394)
9b95526182 is described below

commit 9b95526182ed2be5e72b191308896aebf0ff73b2
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Nov 7 14:11:07 2025 +0100

    Fix resource targetPath resolution to be relative to output directory 
(fixes #11381) (#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 #11381
---
 .../main/java/org/apache/maven/api/Project.java    | 106 ++++++++++++--
 .../main/java/org/apache/maven/api/SourceRoot.java | 153 +++++++++++++++++++--
 .../apache/maven/project/ConnectedResource.java    |   1 +
 .../apache/maven/project/ResourceIncludeTest.java  |   6 +-
 .../org/apache/maven/impl/DefaultSourceRoot.java   |  21 ++-
 .../apache/maven/impl/DefaultSourceRootTest.java   |  27 ++--
 .../it/MavenITgh11381ResourceTargetPathTest.java   |  61 ++++++++
 .../src/test/resources/gh-11381/pom.xml            |  44 ++++++
 .../resources/gh-11381/rest/subdir/another.yml     |   4 +
 .../src/test/resources/gh-11381/rest/test.yml      |   4 +
 10 files changed, 391 insertions(+), 36 deletions(-)

diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Project.java 
b/api/maven-api-core/src/main/java/org/apache/maven/api/Project.java
index 2fb4f4f7ba..22115c3572 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/Project.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Project.java
@@ -174,19 +174,109 @@ default Build getBuild() {
     Path getBasedir();
 
     /**
-     * Returns the directory where files generated by the build are placed.
-     * The directory depends on the scope:
-     *
+     * {@return the absolute path to the directory where files generated by 
the build are placed}
+     * <p>
+     * <strong>Purpose:</strong> This method provides the base output 
directory for a given scope,
+     * which serves as the destination for compiled classes, processed 
resources, and other generated files.
+     * The returned path is always absolute.
+     * </p>
+     * <p>
+     * <strong>Scope-based Directory Resolution:</strong>
+     * </p>
+     * <table class="striped">
+     *   <caption>Output Directory by Scope</caption>
+     *   <thead>
+     *     <tr>
+     *       <th>Scope Parameter</th>
+     *       <th>Build Configuration</th>
+     *       <th>Typical Path</th>
+     *       <th>Contents</th>
+     *     </tr>
+     *   </thead>
+     *   <tbody>
+     *     <tr>
+     *       <td>{@link ProjectScope#MAIN}</td>
+     *       <td>{@code build.getOutputDirectory()}</td>
+     *       <td>{@code target/classes}</td>
+     *       <td>Compiled application classes and processed main resources</td>
+     *     </tr>
+     *     <tr>
+     *       <td>{@link ProjectScope#TEST}</td>
+     *       <td>{@code build.getTestOutputDirectory()}</td>
+     *       <td>{@code target/test-classes}</td>
+     *       <td>Compiled test classes and processed test resources</td>
+     *     </tr>
+     *     <tr>
+     *       <td>{@code null} or other</td>
+     *       <td>{@code build.getDirectory()}</td>
+     *       <td>{@code target}</td>
+     *       <td>Parent directory for all build outputs</td>
+     *     </tr>
+     *   </tbody>
+     * </table>
+     * <p>
+     * <strong>Role in {@link SourceRoot} Path Resolution:</strong>
+     * </p>
+     * <p>
+     * This method is the foundation for {@link 
SourceRoot#targetPath(Project)} path resolution.
+     * When a {@link SourceRoot} has a relative {@code targetPath}, it is 
resolved against the
+     * output directory returned by this method for the source root's scope. 
This ensures that:
+     * </p>
      * <ul>
-     *   <li>If {@link ProjectScope#MAIN}, returns the directory where 
compiled application classes are placed.</li>
-     *   <li>If {@link ProjectScope#TEST}, returns the directory where 
compiled test classes are placed.</li>
-     *   <li>Otherwise (including {@code null}), returns the parent directory 
where all generated files are placed.</li>
+     *   <li>Main resources with {@code targetPath="META-INF"} are copied to 
{@code target/classes/META-INF}</li>
+     *   <li>Test resources with {@code targetPath="test-data"} are copied to 
{@code target/test-classes/test-data}</li>
+     *   <li>Resources without an explicit {@code targetPath} are copied to 
the root of the output directory</li>
      * </ul>
+     * <p>
+     * <strong>Maven 3 Compatibility:</strong>
+     * </p>
+     * <p>
+     * This behavior maintains the Maven 3.x semantic where resource {@code 
targetPath} elements
+     * are resolved relative to the appropriate output directory ({@code 
project.build.outputDirectory}
+     * or {@code project.build.testOutputDirectory}), <strong>not</strong> the 
project base directory.
+     * </p>
+     * <p>
+     * In Maven 3, when a resource configuration specifies:
+     * </p>
+     * <pre>{@code
+     * <resource>
+     *   <directory>src/main/resources</directory>
+     *   <targetPath>META-INF/resources</targetPath>
+     * </resource>
+     * }</pre>
+     * <p>
+     * The maven-resources-plugin resolves {@code targetPath} as:
+     * {@code project.build.outputDirectory + "/" + targetPath}, which results 
in
+     * {@code target/classes/META-INF/resources}. This method provides the 
same base directory
+     * ({@code target/classes}) for Maven 4 API consumers.
+     * </p>
+     * <p>
+     * <strong>Example:</strong>
+     * </p>
+     * <pre>{@code
+     * Project project = ...; // project at /home/user/myproject
+     *
+     * // Get main output directory
+     * Path mainOutput = project.getOutputDirectory(ProjectScope.MAIN);
+     * // Result: /home/user/myproject/target/classes
+     *
+     * // Get test output directory
+     * Path testOutput = project.getOutputDirectory(ProjectScope.TEST);
+     * // Result: /home/user/myproject/target/test-classes
+     *
+     * // Get build directory
+     * Path buildDir = project.getOutputDirectory(null);
+     * // Result: /home/user/myproject/target
+     * }</pre>
      *
-     * @param scope the scope of the generated files for which to get the 
directory, or {@code null} for all
-     * @return the output directory of files that are generated for the given 
scope
+     * @param scope the scope of the generated files for which to get the 
directory, or {@code null} for the build directory
+     * @return the absolute path to the output directory for the given scope
      *
      * @see SourceRoot#targetPath(Project)
+     * @see SourceRoot#targetPath()
+     * @see Build#getOutputDirectory()
+     * @see Build#getTestOutputDirectory()
+     * @see Build#getDirectory()
      */
     @Nonnull
     default Path getOutputDirectory(@Nullable ProjectScope scope) {
diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/SourceRoot.java 
b/api/maven-api-core/src/main/java/org/apache/maven/api/SourceRoot.java
index 12ac480044..c8451e66dd 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/SourceRoot.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/SourceRoot.java
@@ -144,24 +144,159 @@ default Optional<Version> targetVersion() {
 
     /**
      * {@return an explicit target path, overriding the default value}
-     * When a target path is explicitly specified, the values of the {@link 
#module()} and {@link #targetVersion()}
-     * elements are not used for inferring the path (they are still used as 
compiler options however).
-     * It means that for scripts and resources, the files below the path 
specified by {@link #directory()}
+     * <p>
+     * <strong>Important:</strong> This method returns the target path <em>as 
specified in the configuration</em>,
+     * which may be relative or absolute. It does <strong>not</strong> perform 
any path resolution.
+     * For the fully resolved absolute path, use {@link #targetPath(Project)} 
instead.
+     * </p>
+     * <p>
+     * <strong>Return Value Semantics:</strong>
+     * </p>
+     * <ul>
+     *   <li><strong>Empty Optional</strong> - No explicit target path was 
specified. Files should be copied
+     *       to the root of the output directory (see {@link 
Project#getOutputDirectory(ProjectScope)}).</li>
+     *   <li><strong>Relative Path</strong> (e.g., {@code 
Path.of("META-INF/resources")}) - The path is
+     *       <em>intended to be resolved</em> relative to the output directory 
for this source root's {@link #scope()}.
+     *       <ul>
+     *         <li>For {@link ProjectScope#MAIN}: relative to {@code 
target/classes}</li>
+     *         <li>For {@link ProjectScope#TEST}: relative to {@code 
target/test-classes}</li>
+     *       </ul>
+     *       The actual resolution is performed by {@link 
#targetPath(Project)}.</li>
+     *   <li><strong>Absolute Path</strong> (e.g., {@code 
Path.of("/tmp/custom")}) - The path is used as-is
+     *       without any resolution. Files will be copied to this exact 
location.</li>
+     * </ul>
+     * <p>
+     * <strong>Maven 3 Compatibility:</strong> This behavior maintains 
compatibility with Maven 3.x,
+     * where resource {@code targetPath} elements were always interpreted as 
relative to the output directory
+     * ({@code project.build.outputDirectory} or {@code 
project.build.testOutputDirectory}),
+     * not the project base directory. Maven 3 plugins (like 
maven-resources-plugin) expect to receive
+     * the relative path and perform the resolution themselves.
+     * </p>
+     * <p>
+     * <strong>Effect on Module and Target Version:</strong>
+     * When a target path is explicitly specified, the values of {@link 
#module()} and {@link #targetVersion()}
+     * are not used for inferring the output path (they are still used as 
compiler options however).
+     * This means that for scripts and resources, the files below the path 
specified by {@link #directory()}
      * are copied to the path specified by {@code targetPath()} with the exact 
same directory structure.
+     * </p>
+     * <p>
+     * <strong>Usage Guidance:</strong>
+     * </p>
+     * <ul>
+     *   <li><strong>For Maven 4 API consumers:</strong> Use {@link 
#targetPath(Project)} to get the
+     *       fully resolved absolute path where files should be copied.</li>
+     *   <li><strong>For Maven 3 compatibility layer:</strong> Use this method 
to get the path as specified
+     *       in the configuration, which can then be passed to legacy plugins 
that expect to perform
+     *       their own resolution.</li>
+     *   <li><strong>For implementers:</strong> Store the path exactly as 
provided in the configuration.
+     *       Do not resolve relative paths at storage time.</li>
+     * </ul>
+     *
+     * @see #targetPath(Project)
+     * @see Project#getOutputDirectory(ProjectScope)
      */
     default Optional<Path> targetPath() {
         return Optional.empty();
     }
 
     /**
-     * {@return the explicit target path resolved against the default target 
path}
-     * Invoking this method is equivalent to getting the default output 
directory
-     * by a call to {@code project.getOutputDirectory(scope())}, then 
resolving the
-     * {@linkplain #targetPath() target path} (if present) against that 
default directory.
-     * Note that if the target path is absolute, the result is that target 
path unchanged.
+     * {@return the fully resolved absolute target path where files should be 
copied}
+     * <p>
+     * <strong>Purpose:</strong> This method performs the complete path 
resolution logic, converting
+     * the potentially relative {@link #targetPath()} into an absolute 
filesystem path. This is the
+     * method that Maven 4 API consumers should use when they need to know the 
actual destination
+     * directory for copying files.
+     * </p>
+     * <p>
+     * <strong>Resolution Algorithm:</strong>
+     * </p>
+     * <ol>
+     *   <li>Obtain the {@linkplain #targetPath() configured target path} 
(which may be empty, relative, or absolute)</li>
+     *   <li>If the configured target path is absolute (e.g., {@code 
/tmp/custom}):
+     *       <ul><li>Return it unchanged (no resolution needed)</li></ul></li>
+     *   <li>Otherwise, get the output directory for this source root's {@link 
#scope()} by calling
+     *       {@code project.getOutputDirectory(scope())}:
+     *       <ul>
+     *         <li>For {@link ProjectScope#MAIN}: typically {@code 
/path/to/project/target/classes}</li>
+     *         <li>For {@link ProjectScope#TEST}: typically {@code 
/path/to/project/target/test-classes}</li>
+     *       </ul></li>
+     *   <li>If the configured target path is empty:
+     *       <ul><li>Return the output directory as-is</li></ul></li>
+     *   <li>If the configured target path is relative (e.g., {@code 
META-INF/resources}):
+     *       <ul><li>Resolve it against the output directory using {@code 
outputDirectory.resolve(targetPath)}</li></ul></li>
+     * </ol>
+     * <p>
+     * <strong>Concrete Examples:</strong>
+     * </p>
+     * <p>
+     * Given a project at {@code /home/user/myproject} with {@link 
ProjectScope#MAIN}:
+     * </p>
+     * <table class="striped">
+     *   <caption>Target Path Resolution Examples</caption>
+     *   <thead>
+     *     <tr>
+     *       <th>Configuration ({@code targetPath()})</th>
+     *       <th>Output Directory</th>
+     *       <th>Result ({@code targetPath(project)})</th>
+     *       <th>Explanation</th>
+     *     </tr>
+     *   </thead>
+     *   <tbody>
+     *     <tr>
+     *       <td>{@code Optional.empty()}</td>
+     *       <td>{@code /home/user/myproject/target/classes}</td>
+     *       <td>{@code /home/user/myproject/target/classes}</td>
+     *       <td>No explicit path → use output directory</td>
+     *     </tr>
+     *     <tr>
+     *       <td>{@code Optional.of(Path.of("META-INF"))}</td>
+     *       <td>{@code /home/user/myproject/target/classes}</td>
+     *       <td>{@code /home/user/myproject/target/classes/META-INF}</td>
+     *       <td>Relative path → resolve against output directory</td>
+     *     </tr>
+     *     <tr>
+     *       <td>{@code Optional.of(Path.of("WEB-INF/classes"))}</td>
+     *       <td>{@code /home/user/myproject/target/classes}</td>
+     *       <td>{@code 
/home/user/myproject/target/classes/WEB-INF/classes}</td>
+     *       <td>Relative path with subdirectories</td>
+     *     </tr>
+     *     <tr>
+     *       <td>{@code Optional.of(Path.of("/tmp/custom"))}</td>
+     *       <td>{@code /home/user/myproject/target/classes}</td>
+     *       <td>{@code /tmp/custom}</td>
+     *       <td>Absolute path → use as-is (no resolution)</td>
+     *     </tr>
+     *   </tbody>
+     * </table>
+     * <p>
+     * <strong>Relationship to {@link #targetPath()}:</strong>
+     * </p>
+     * <p>
+     * This method is the <em>resolution</em> counterpart to {@link 
#targetPath()}, which is the
+     * <em>storage</em> method. While {@code targetPath()} returns the path as 
configured (potentially relative),
+     * this method returns the absolute path where files will actually be 
written. The separation allows:
+     * </p>
+     * <ul>
+     *   <li>Maven 4 API consumers to get absolute paths via this method</li>
+     *   <li>Maven 3 compatibility layer to get relative paths via {@code 
targetPath()} for legacy plugins</li>
+     *   <li>Implementations to store paths without premature resolution</li>
+     * </ul>
+     * <p>
+     * <strong>Implementation Note:</strong> The default implementation is 
equivalent to:
+     * </p>
+     * <pre>{@code
+     * Optional<Path> configured = targetPath();
+     * if (configured.isPresent() && configured.get().isAbsolute()) {
+     *     return configured.get();
+     * }
+     * Path outputDir = project.getOutputDirectory(scope());
+     * return configured.map(outputDir::resolve).orElse(outputDir);
+     * }</pre>
      *
-     * @param project the project to use for getting default directories
+     * @param project the project to use for obtaining the output directory
+     * @return the absolute path where files from {@link #directory()} should 
be copied
      *
+     * @see #targetPath()
      * @see Project#getOutputDirectory(ProjectScope)
      */
     @Nonnull
diff --git 
a/impl/maven-core/src/main/java/org/apache/maven/project/ConnectedResource.java 
b/impl/maven-core/src/main/java/org/apache/maven/project/ConnectedResource.java
index df0ebc711f..cbb0629b21 100644
--- 
a/impl/maven-core/src/main/java/org/apache/maven/project/ConnectedResource.java
+++ 
b/impl/maven-core/src/main/java/org/apache/maven/project/ConnectedResource.java
@@ -31,6 +31,7 @@
  * A Resource wrapper that maintains a connection to the underlying project 
model.
  * When includes/excludes are modified, the changes are propagated back to the 
project's SourceRoots.
  */
+@SuppressWarnings("deprecation")
 class ConnectedResource extends Resource {
     private final SourceRoot originalSourceRoot;
     private final ProjectScope scope;
diff --git 
a/impl/maven-core/src/test/java/org/apache/maven/project/ResourceIncludeTest.java
 
b/impl/maven-core/src/test/java/org/apache/maven/project/ResourceIncludeTest.java
index 9d639fafc6..519dbd5770 100644
--- 
a/impl/maven-core/src/test/java/org/apache/maven/project/ResourceIncludeTest.java
+++ 
b/impl/maven-core/src/test/java/org/apache/maven/project/ResourceIncludeTest.java
@@ -46,6 +46,10 @@ void setUp() {
         // Set a dummy pom file to establish the base directory
         project.setFile(new java.io.File("./pom.xml"));
 
+        // Set build output directories
+        project.getBuild().setOutputDirectory("target/classes");
+        project.getBuild().setTestOutputDirectory("target/test-classes");
+
         // Add a resource source root to the project
         project.addSourceRoot(
                 new DefaultSourceRoot(ProjectScope.MAIN, Language.RESOURCES, 
Path.of("src/main/resources")));
@@ -199,7 +203,7 @@ void testTargetPathPreservedWithConnectedResource() {
         resourceWithTarget.setDirectory("src/main/custom");
         resourceWithTarget.setTargetPath("custom-output");
 
-        // Convert through DefaultSourceRoot to ensure targetPath extraction 
works
+        // Convert through DefaultSourceRoot to ensure targetPath is preserved
         DefaultSourceRoot sourceRootFromResource =
                 new DefaultSourceRoot(project.getBaseDirectory(), 
ProjectScope.MAIN, resourceWithTarget.getDelegate());
 
diff --git 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.java 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.java
index 6ffec2ce88..870b429517 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.java
@@ -142,18 +142,22 @@ public static DefaultSourceRoot fromModel(
                 source.getIncludes(),
                 source.getExcludes(),
                 source.isStringFiltering(),
-                nonBlank(source.getTargetPath())
-                        .map((targetPath) ->
-                                
baseDir.resolve(outputDir.apply(scope)).resolve(targetPath))
-                        .orElse(null),
+                nonBlank(source.getTargetPath()).map(Path::of).orElse(null),
                 source.isEnabled());
     }
 
     /**
      * Creates a new instance from the given resource.
      * This is used for migration from the previous way of declaring resources.
+     * <p>
+     * <strong>Important:</strong> The {@code targetPath} from the resource is 
stored as-is
+     * (converted to a {@link Path} but not resolved against any directory). 
This preserves
+     * the Maven 3.x behavior where {@code targetPath} is relative to the 
output directory,
+     * not the project base directory. The actual resolution happens later via
+     * {@link SourceRoot#targetPath(Project)}.
+     * </p>
      *
-     * @param baseDir the base directory for resolving relative paths
+     * @param baseDir the base directory for resolving relative paths (used 
only for the source directory)
      * @param scope the scope of the resource (main or test)
      * @param resource a resource element from the model
      */
@@ -169,7 +173,7 @@ public DefaultSourceRoot(final Path baseDir, ProjectScope 
scope, Resource resour
                 resource.getIncludes(),
                 resource.getExcludes(),
                 Boolean.parseBoolean(resource.getFiltering()),
-                
nonBlank(resource.getTargetPath()).map(baseDir::resolve).orElse(null),
+                nonBlank(resource.getTargetPath()).map(Path::of).orElse(null),
                 true);
     }
 
@@ -220,6 +224,11 @@ public Optional<Version> targetVersion() {
 
     /**
      * {@return an explicit target path, overriding the default value}
+     * <p>
+     * The returned path, if present, is stored as provided in the 
configuration and is typically
+     * relative to the output directory. Use {@link #targetPath(Project)} to 
get the fully
+     * resolved absolute path.
+     * </p>
      */
     @Override
     public Optional<Path> targetPath() {
diff --git 
a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSourceRootTest.java
 
b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSourceRootTest.java
index 6ceedfea56..b74b5917bd 100644
--- 
a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSourceRootTest.java
+++ 
b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSourceRootTest.java
@@ -42,6 +42,7 @@
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
 
+@SuppressWarnings("deprecation")
 @ExtendWith(MockitoExtension.class)
 public class DefaultSourceRootTest {
 
@@ -148,7 +149,7 @@ void testModuleTestDirectory() {
     }
 
     /**
-     * Tests that relative target paths are resolved against the right base 
directory.
+     * Tests that relative target paths are stored as relative paths.
      */
     @Test
     void testRelativeMainTargetPath() {
@@ -160,13 +161,11 @@ void testRelativeMainTargetPath() {
 
         assertEquals(ProjectScope.MAIN, source.scope());
         assertEquals(Language.JAVA_FAMILY, source.language());
-        assertEquals(
-                Path.of("myproject", "target", "classes", "user-output"),
-                source.targetPath().orElseThrow());
+        assertEquals(Path.of("user-output"), 
source.targetPath().orElseThrow());
     }
 
     /**
-     * Tests that relative target paths are resolved against the right base 
directory.
+     * Tests that relative target paths are stored as relative paths.
      */
     @Test
     void testRelativeTestTargetPath() {
@@ -178,15 +177,14 @@ void testRelativeTestTargetPath() {
 
         assertEquals(ProjectScope.TEST, source.scope());
         assertEquals(Language.JAVA_FAMILY, source.language());
-        assertEquals(
-                Path.of("myproject", "target", "test-classes", "user-output"),
-                source.targetPath().orElseThrow());
+        assertEquals(Path.of("user-output"), 
source.targetPath().orElseThrow());
     }
 
     /*MNG-11062*/
     @Test
     void testExtractsTargetPathFromResource() {
-        // Test the Resource constructor that was broken in the regression
+        // Test the Resource constructor with relative targetPath
+        // targetPath should be kept as relative path
         Resource resource = Resource.newBuilder()
                 .directory("src/test/resources")
                 .targetPath("test-output")
@@ -196,7 +194,7 @@ void testExtractsTargetPathFromResource() {
 
         Optional<Path> targetPath = sourceRoot.targetPath();
         assertTrue(targetPath.isPresent(), "targetPath should be present");
-        assertEquals(Path.of("myproject", "test-output"), targetPath.get());
+        assertEquals(Path.of("test-output"), targetPath.get(), "targetPath 
should be relative to output directory");
         assertEquals(Path.of("myproject", "src", "test", "resources"), 
sourceRoot.directory());
         assertEquals(ProjectScope.TEST, sourceRoot.scope());
         assertEquals(Language.RESOURCES, sourceRoot.language());
@@ -244,7 +242,10 @@ void testHandlesPropertyPlaceholderInTargetPath() {
 
         Optional<Path> targetPath = sourceRoot.targetPath();
         assertTrue(targetPath.isPresent(), "Property placeholder targetPath 
should be present");
-        assertEquals(Path.of("myproject", 
"${project.build.directory}/custom"), targetPath.get());
+        assertEquals(
+                Path.of("${project.build.directory}/custom"),
+                targetPath.get(),
+                "Property placeholder should be kept as-is (relative path)");
     }
 
     /*MNG-11062*/
@@ -276,7 +277,9 @@ void testResourceConstructorPreservesOtherProperties() {
 
         // Verify all properties are preserved
         assertEquals(
-                Path.of("myproject", "test-classes"), 
sourceRoot.targetPath().orElseThrow());
+                Path.of("test-classes"),
+                sourceRoot.targetPath().orElseThrow(),
+                "targetPath should be relative to output directory");
         assertTrue(sourceRoot.stringFiltering(), "Filtering should be true");
         assertEquals(1, sourceRoot.includes().size());
         assertTrue(sourceRoot.includes().contains("*.properties"));
diff --git 
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11381ResourceTargetPathTest.java
 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11381ResourceTargetPathTest.java
new file mode 100644
index 0000000000..33d68092f1
--- /dev/null
+++ 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11381ResourceTargetPathTest.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.it;
+
+import java.io.File;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * This is a test set for <a 
href="https://github.com/apache/maven/issues/11381";>GH-11381</a>.
+ *
+ * Verifies that relative targetPath in resources is resolved relative to the 
output directory
+ * (target/classes) and not relative to the project base directory, 
maintaining Maven 3.x behavior.
+ *
+ * @since 4.0.0-rc-4
+ */
+class MavenITgh11381ResourceTargetPathTest extends 
AbstractMavenIntegrationTestCase {
+
+    /**
+     * Verify that resources with relative targetPath are copied to 
target/classes/targetPath
+     * and not to the project root directory.
+     *
+     * @throws Exception in case of failure
+     */
+    @Test
+    void testRelativeTargetPathInResources() throws Exception {
+        File testDir = extractResources("/gh-11381");
+
+        Verifier verifier = newVerifier(testDir.getAbsolutePath());
+        verifier.setAutoclean(false);
+        verifier.deleteDirectory("target");
+        verifier.addCliArgument("process-resources");
+        verifier.execute();
+        verifier.verifyErrorFreeLog();
+
+        // Verify that resources were copied to target/classes/target-dir 
(Maven 3.x behavior)
+        verifier.verifyFilePresent("target/classes/target-dir/test.yml");
+        
verifier.verifyFilePresent("target/classes/target-dir/subdir/another.yml");
+
+        // Verify that resources were NOT copied to the project root 
target-dir directory
+        verifier.verifyFileNotPresent("target-dir/test.yml");
+        verifier.verifyFileNotPresent("target-dir/subdir/another.yml");
+    }
+}
+
diff --git a/its/core-it-suite/src/test/resources/gh-11381/pom.xml 
b/its/core-it-suite/src/test/resources/gh-11381/pom.xml
new file mode 100644
index 0000000000..fcfde0d56b
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-11381/pom.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.its.gh11381</groupId>
+  <artifactId>test</artifactId>
+  <version>1.0-SNAPSHOT</version>
+  <packaging>jar</packaging>
+
+  <name>Maven Integration Test :: GH-11381</name>
+  <description>Test for relative targetPath in resources - should be relative 
to output directory</description>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>${project.basedir}/rest</directory>
+        <targetPath>target-dir</targetPath>
+        <includes>
+          <include>**/*.yml</include>
+        </includes>
+      </resource>
+    </resources>
+  </build>
+</project>
+
diff --git 
a/its/core-it-suite/src/test/resources/gh-11381/rest/subdir/another.yml 
b/its/core-it-suite/src/test/resources/gh-11381/rest/subdir/another.yml
new file mode 100644
index 0000000000..25ca8b36b0
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-11381/rest/subdir/another.yml
@@ -0,0 +1,4 @@
+# Another test YAML file for GH-11381
+another:
+  test: data
+
diff --git a/its/core-it-suite/src/test/resources/gh-11381/rest/test.yml 
b/its/core-it-suite/src/test/resources/gh-11381/rest/test.yml
new file mode 100644
index 0000000000..2ecf8edd3d
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-11381/rest/test.yml
@@ -0,0 +1,4 @@
+# Test YAML file for GH-11381
+test:
+  key: value
+

Reply via email to