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
+