You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/08/28 13:23:25 UTC

(maven) branch master updated: Fix #12646: project-local-repo clean race condition when root pom has parent (#12650)

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 5561ec5b3c Fix #12646: project-local-repo clean race condition when root pom has parent (#12650)
5561ec5b3c is described below

commit 5561ec5b3cfb430ac54d634e6b6e5772d7f2a57a
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Aug 28 15:23:19 2026 +0200

    Fix #12646: project-local-repo clean race condition when root pom has parent (#12650)
    
    * Fix #12646: project-local-repo clean race condition when root pom has parent
    
    When the root pom.xml has a parent (super-pom) in the reactor,
    MultiThreadedBuilder schedules the super-pom first, then builds the
    root pom and sibling modules in parallel. The root pom's clean phase
    (maven-clean-plugin) deletes the entire target/ directory, which
    includes project-local-repo. Concurrently, sibling modules that
    complete successfully write artifacts into target/project-local-repo
    via ReactorReader.installIntoProjectLocalRepository(). This race
    causes maven-clean-plugin to fail because the directory is being
    written to while it tries to delete it.
    
    Fix: Add a ReentrantReadWriteLock to ReactorReader that coordinates
    access to the project-local-repo directory between clean and install
    operations. When the project whose build directory contains the
    project-local-repo enters its clean phase, a write lock is acquired
    to block concurrent installs. The write lock is released when the
    clean mojo completes (succeeds or fails). Install operations acquire
    a read lock, allowing multiple concurrent installs but blocking while
    the owning project's clean is running.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    
    * Fix #12646: move project-local-repo from target/ to .mvn/ to eliminate clean race
    
    Move the project-local-repo directory from target/project-local-repo to
    .mvn/project-local-repo so it is not affected by maven-clean-plugin's
    deletion of target/. This eliminates the race condition in parallel builds
    where maven-clean-plugin deletes target/ while sibling modules concurrently
    write artifacts into project-local-repo.
    
    The previous lock-based approach prevented crashes but could not guarantee
    ordering between clean and install operations. Moving the directory outside
    target/ makes the race structurally impossible — ReactorReader fully owns
    the lifecycle of project-local-repo (per-GAV cleanup on clean, install on
    project success).
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    
    * Use .mvn/target/ as the parent directory for project-local-repo
    
    Place project-local-repo under .mvn/target/ instead of .mvn/ directly,
    establishing a clean semantic split: .mvn/ for config (tracked in git),
    .mvn/target/ for build output (gitignored). This convention can also
    host other cross-module build artifacts in the future.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    
    * Update integration tests for .mvn/target/project-local-repo path
    
    Update all ITs that assert on project-local-repo file paths to use
    the new .mvn/target/project-local-repo location. Add .mvn directories
    to test resources that were missing them to ensure proper rootDirectory
    resolution. Add .mvn/target cleanup to tests with manual directory
    deletion to prevent cross-test-method contamination.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    
    * Add maven.project.local.repo property to configure project-local-repo location
    
    Introduce a user property (maven.project.local.repo) that allows
    overriding the project-local repository path. The default remains
    .mvn/target/project-local-repo relative to the root directory.
    The property accepts absolute paths or paths relative to rootDirectory,
    and is documented in maven-user.properties.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    
    * Rename maven.project.local.repo to maven.reactor.outputRepository
    
    The previous name was too close to maven.repo.local and could easily
    be confused with the local repository property.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
 .../src/assembly/maven/conf/maven-user.properties  | 10 ++++
 .../main/java/org/apache/maven/api/Constants.java  | 15 ++++++
 .../main/java/org/apache/maven/ReactorReader.java  | 35 ++++++++-----
 ...avenITBomConsumerPomPropertyResolutionTest.java |  1 +
 .../MavenITConsumerPomBomFromSettingsRepoTest.java |  1 +
 .../it/MavenITgh11162ConsumerPomScopesTest.java    |  1 +
 .../it/MavenITgh11456MixinsConsumerPomTest.java    |  2 +
 ...avenITgh12646ProjectLocalRepoCleanRaceTest.java | 59 ++++++++++++++++++++++
 ...enITmng3043BestEffortReactorResolutionTest.java |  5 +-
 .../apache/maven/it/MavenITmng5102MixinsTest.java  | 15 +++---
 ...venITmng8414ConsumerPomWithNewFeaturesTest.java |  2 +
 ...mng8645ConsumerPomDependencyManagementTest.java |  1 +
 .../.mvn/.gitkeep                                  |  0
 .../gh-11162-consumer-pom-scopes/.mvn/.gitkeep     |  0
 .../.mvn/.gitkeep                                  |  0
 .../.mvn/.gitkeep                                  |  0
 .../module-a/pom.xml                               | 29 +++++++++++
 .../module-a/src/main/java/a/A.java                |  7 +++
 .../module-b/pom.xml                               | 29 +++++++++++
 .../module-b/src/main/java/b/B.java                |  7 +++
 .../module-c/pom.xml                               | 29 +++++++++++
 .../module-c/src/main/java/c/C.java                |  7 +++
 .../module-d/pom.xml                               | 29 +++++++++++
 .../module-d/src/main/java/d/D.java                |  7 +++
 .../gh-12646-project-local-repo-clean-race/pom.xml | 38 ++++++++++++++
 .../super-pom/pom.xml                              | 25 +++++++++
 .../src/test/resources/mng-3043/.mvn/.gitkeep      |  0
 .../classifier/project/.mvn/.gitkeep               |  0
 .../mng-5102-mixins/gav/project/.mvn/.gitkeep      |  0
 .../resources/mng-5102-mixins/path/.mvn/.gitkeep   |  0
 .../.mvn/.gitkeep                                  |  0
 .../mng-8645-consumer-pom-dep-mgmt/.mvn/.gitkeep   |  0
 32 files changed, 333 insertions(+), 21 deletions(-)

diff --git a/apache-maven/src/assembly/maven/conf/maven-user.properties b/apache-maven/src/assembly/maven/conf/maven-user.properties
index c664d06cc5..121584fb02 100644
--- a/apache-maven/src/assembly/maven/conf/maven-user.properties
+++ b/apache-maven/src/assembly/maven/conf/maven-user.properties
@@ -30,6 +30,16 @@
 ${includes} = ?"${maven.user.conf}/maven-user.properties", \
               ?"${maven.project.conf}/maven-user.properties"
 
+#
+# Reactor Output Repository
+#
+# Location of the reactor output repository used by the reactor to share artifacts between
+# modules during a build. This repository enables partial and resumable builds (e.g.
+# mvn verify -r :module) without requiring install. The path may be absolute or relative
+# to the root directory.
+#
+# maven.reactor.outputRepository = ${maven.rootDirectory}/.mvn/target/project-local-repo
+
 #
 # Maven Cache Configuration
 #
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
index 5e51cfb1ad..7d01a7c1a7 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
@@ -381,6 +381,21 @@ public final class Constants {
     @Config
     public static final String MAVEN_VERSION_FILTER = "maven.session.versionFilter";
 
+    /**
+     * User property for overriding the reactor output repository path used by the reactor reader to share
+     * artifacts between modules during a build. This repository enables partial and resumable builds
+     * (e.g. {@code mvn verify -r :module}) without requiring {@code install}.
+     * <p>
+     * The path may be absolute or relative to the root directory. If relative, it is resolved against
+     * the root directory of the project.
+     * </p>
+     * Default value: <code>${maven.rootDirectory}/.mvn/target/project-local-repo</code>.
+     *
+     * @since 4.0.0
+     */
+    @Config(defaultValue = "${maven.rootDirectory}/.mvn/target/project-local-repo")
+    public static final String MAVEN_REACTOR_OUTPUT_REPOSITORY = "maven.reactor.outputRepository";
+
     /**
      * User property for chained LRM: the new "head" local repository to use, and "push" the existing into tail.
      * Similar to <code>maven.repo.local.tail</code>, this property may contain comma separated list of paths to be
diff --git a/impl/maven-core/src/main/java/org/apache/maven/ReactorReader.java b/impl/maven-core/src/main/java/org/apache/maven/ReactorReader.java
index c1fb482945..8e301c2882 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/ReactorReader.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/ReactorReader.java
@@ -43,6 +43,7 @@
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import org.apache.maven.api.Constants;
 import org.apache.maven.api.model.Model;
 import org.apache.maven.api.services.Lookup;
 import org.apache.maven.eventspy.EventSpy;
@@ -353,6 +354,13 @@ private File findInProjectLocalRepository(Artifact artifact) {
      * The mojo started event is also captured to determine the lifecycle
      * phases the project has been through.
      *
+     * <p>When a project enters its clean phase, its artifacts are cleaned from the
+     * project-local-repo (per-GAV scope). Since the project-local-repo is located
+     * under {@code .mvn/target/} (not under the project's own {@code target/}),
+     * it is not affected by maven-clean-plugin's deletion of {@code target/},
+     * eliminating the race condition between clean and install operations in
+     * parallel builds.</p>
+     *
      * @param event the execution event
      */
     private void processEvent(ExecutionEvent event) {
@@ -365,9 +373,7 @@ private void processEvent(ExecutionEvent event) {
                     if (!Objects.equals(phase, phases.peekLast())) {
                         phases.addLast(phase);
                         if ("clean".equals(phase)) {
-                            synchronized (project) {
-                                cleanProjectLocalRepository(project);
-                            }
+                            cleanProjectLocalRepository(project);
                         }
                     }
                 }
@@ -403,6 +409,13 @@ private void installIntoProjectLocalRepository(MavenProject project) {
         }
     }
 
+    /**
+     * Cleans the project-local-repo artifacts for the given project's GAV coordinates.
+     * Since the project-local-repo is under {@code .mvn/target/} and not the project's
+     * own {@code target/}, it is not affected by maven-clean-plugin's deletion of
+     * {@code target/}, so there is no race between clean and install operations
+     * in parallel builds.
+     */
     private void cleanProjectLocalRepository(MavenProject project) {
         try {
             Path artifactPath = getProjectLocalRepo()
@@ -506,17 +519,13 @@ private Path getArtifactPath(
     private Path getProjectLocalRepo() {
         if (projectLocalRepository == null) {
             Path root = session.getRequest().getRootDirectory();
-            List<MavenProject> projects = session.getProjects();
-            if (projects != null) {
-                projectLocalRepository = projects.stream()
-                        .filter(project -> Objects.equals(root.toFile(), project.getBasedir()))
-                        .findFirst()
-                        .map(project -> project.getBuild().getDirectory())
-                        .map(Paths::get)
-                        .orElseGet(() -> root.resolve("target"))
-                        .resolve(PROJECT_LOCAL_REPO);
+            String userPath =
+                    session.getRequest().getUserProperties().getProperty(Constants.MAVEN_REACTOR_OUTPUT_REPOSITORY);
+            if (userPath != null && !userPath.isEmpty()) {
+                Path path = Paths.get(userPath);
+                projectLocalRepository = path.isAbsolute() ? path : root.resolve(path);
             } else {
-                return root.resolve("target").resolve(PROJECT_LOCAL_REPO);
+                projectLocalRepository = root.resolve(".mvn").resolve("target").resolve(PROJECT_LOCAL_REPO);
             }
         }
         return projectLocalRepository;
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITBomConsumerPomPropertyResolutionTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITBomConsumerPomPropertyResolutionTest.java
index eaca1545f5..de0a4e5d3e 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITBomConsumerPomPropertyResolutionTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITBomConsumerPomPropertyResolutionTest.java
@@ -156,6 +156,7 @@ void testConsumerPomInProjectLocalRepo() throws Exception {
 
         // Check the consumer POM in the project-local-repo
         Path consumerPom = basedir.resolve(Path.of(
+                ".mvn",
                 "target",
                 "project-local-repo",
                 "org.apache.maven.its.bom-property",
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITConsumerPomBomFromSettingsRepoTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITConsumerPomBomFromSettingsRepoTest.java
index 05be1fa3c7..50194b86f2 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITConsumerPomBomFromSettingsRepoTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITConsumerPomBomFromSettingsRepoTest.java
@@ -75,6 +75,7 @@ void testConsumerPomWithBomFromSettingsProfileRepo() throws Exception {
 
         // Verify the consumer POM was generated
         Path consumerPom = basedir.resolve(Path.of(
+                ".mvn",
                 "target",
                 "project-local-repo",
                 "org.apache.maven.its.cpbom",
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11162ConsumerPomScopesTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11162ConsumerPomScopesTest.java
index 5787c88f8d..89d5c93fcf 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11162ConsumerPomScopesTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11162ConsumerPomScopesTest.java
@@ -49,6 +49,7 @@ void testConsumerPomFiltersScopes() throws Exception {
         verifier.verifyErrorFreeLog();
 
         Path consumerPom = basedir.resolve(Paths.get(
+                ".mvn",
                 "target",
                 "project-local-repo",
                 "org.apache.maven.its.gh11162",
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11456MixinsConsumerPomTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11456MixinsConsumerPomTest.java
index 352b4083ac..8a770499f9 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11456MixinsConsumerPomTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11456MixinsConsumerPomTest.java
@@ -71,6 +71,7 @@ void testMixinsWithFlattening() throws Exception {
 
         // Verify consumer POM was created
         Path consumerPom = basedir.resolve(Paths.get(
+                ".mvn",
                 "target",
                 "project-local-repo",
                 "org.apache.maven.its.gh11456",
@@ -104,6 +105,7 @@ void testMixinsWithPreserveModelVersion() throws Exception {
 
         // Verify consumer POM was created
         Path consumerPom = basedir.resolve(Paths.get(
+                ".mvn",
                 "target",
                 "project-local-repo",
                 "org.apache.maven.its.gh11456",
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh12646ProjectLocalRepoCleanRaceTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh12646ProjectLocalRepoCleanRaceTest.java
new file mode 100644
index 0000000000..bc06787f36
--- /dev/null
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh12646ProjectLocalRepoCleanRaceTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * This is a test set for <a href="https://github.com/apache/maven/issues/12646">GH-12646</a>.
+ *
+ * Verifies that parallel builds with {@code clean install} do not fail with a race condition
+ * when the root pom has a parent (super-pom) and other modules write to
+ * {@code target/project-local-repo} while the root project's clean phase is running.
+ *
+ * @since 4.0.0-rc-6
+ */
+class MavenITgh12646ProjectLocalRepoCleanRaceTest extends AbstractMavenIntegrationTestCase {
+
+    /**
+     * Verify that a parallel {@code clean install} succeeds when the root pom has a parent
+     * that is also part of the reactor. In this scenario:
+     * <ol>
+     *   <li>super-pom builds first (no dependencies)</li>
+     *   <li>root pom and module-a..d start in parallel (all depend on super-pom)</li>
+     *   <li>root pom's clean phase runs maven-clean-plugin which deletes target/</li>
+     *   <li>modules complete and install artifacts into target/project-local-repo</li>
+     * </ol>
+     * Without the fix, step 3 and 4 race, causing maven-clean-plugin to fail because
+     * target/project-local-repo is being written to while it tries to delete target/.
+     */
+    @Test
+    void testParallelCleanInstallWithParentPom() throws Exception {
+        Path testDir = extractResources("gh-12646-project-local-repo-clean-race");
+
+        Verifier verifier = newVerifier(testDir);
+        verifier.addCliArgument("-T");
+        verifier.addCliArgument("4");
+        verifier.addCliArguments("clean", "install");
+        verifier.execute();
+        verifier.verifyErrorFreeLog();
+    }
+}
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3043BestEffortReactorResolutionTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3043BestEffortReactorResolutionTest.java
index d0ccbee40d..84c969a0e4 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3043BestEffortReactorResolutionTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3043BestEffortReactorResolutionTest.java
@@ -18,7 +18,6 @@
  */
 package org.apache.maven.it;
 
-import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
 
@@ -44,11 +43,11 @@ public class MavenITmng3043BestEffortReactorResolutionTest extends AbstractMaven
     @Test
     public void testitTestPhase() throws Exception {
         Path testDir = extractResources("mng-3043");
-        Files.createDirectories(testDir.resolve(".mvn"));
 
         Verifier verifier = newVerifier(testDir);
         verifier.setAutoclean(false);
         verifier.deleteDirectory("target");
+        verifier.deleteDirectory(".mvn/target");
         verifier.deleteDirectory("consumer-a/target");
         verifier.deleteDirectory("consumer-b/target");
         verifier.deleteDirectory("consumer-c/target");
@@ -105,6 +104,7 @@ public void testitPackagePhase() throws Exception {
         Verifier verifier = newVerifier(testDir);
         verifier.setAutoclean(false);
         verifier.deleteDirectory("target");
+        verifier.deleteDirectory(".mvn/target");
         verifier.deleteDirectory("consumer-a/target");
         verifier.deleteDirectory("consumer-b/target");
         verifier.deleteDirectory("consumer-c/target");
@@ -165,6 +165,7 @@ public void testitPackagePhasesSlitted() throws Exception {
         Verifier verifier = newVerifier(testDir);
         verifier.setAutoclean(false);
         verifier.deleteDirectory("target");
+        verifier.deleteDirectory(".mvn/target");
         verifier.deleteDirectory("consumer-a/target");
         verifier.deleteDirectory("consumer-b/target");
         verifier.deleteDirectory("consumer-c/target");
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5102MixinsTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5102MixinsTest.java
index 32a03ac001..206e35b5e2 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5102MixinsTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5102MixinsTest.java
@@ -47,6 +47,7 @@ public void testWithPath() throws Exception {
         Verifier verifier = newVerifier(testDir);
         verifier.setAutoclean(false);
         verifier.deleteDirectory("target");
+        verifier.deleteDirectory(".mvn/target");
         verifier.deleteArtifacts("org.apache.maven.its.mng5102");
         verifier.addCliArguments("install", "-Dmaven.consumer.pom.flatten=true");
         verifier.execute();
@@ -63,9 +64,9 @@ public void testWithPath() throws Exception {
         assertEquals("true", props.getProperty("project.properties.mixin3"));
 
         verifier.verifyFilePresent(
-                "target/project-local-repo/org.apache.maven.its.mng5102/child/0.1/child-0.1-consumer.pom");
+                ".mvn/target/project-local-repo/org.apache.maven.its.mng5102/child/0.1/child-0.1-consumer.pom");
         List<String> lines = verifier.loadLines(
-                "target/project-local-repo/org.apache.maven.its.mng5102/child/0.1/child-0.1-consumer.pom");
+                ".mvn/target/project-local-repo/org.apache.maven.its.mng5102/child/0.1/child-0.1-consumer.pom");
         assertTrue(lines.stream().noneMatch(l -> l.contains("<mixin>")));
     }
 
@@ -90,6 +91,7 @@ public void testWithGav() throws Exception {
         verifier = newVerifier(testDir.resolve("project"));
         verifier.setAutoclean(false);
         verifier.deleteDirectory("target");
+        verifier.deleteDirectory(".mvn/target");
         verifier.addCliArguments("install", "-Dmaven.consumer.pom.flatten");
         verifier.execute();
         verifier.verifyErrorFreeLog();
@@ -99,9 +101,9 @@ public void testWithGav() throws Exception {
         assertEquals("true", props.getProperty("project.properties.mixin2"));
 
         verifier.verifyFilePresent(
-                "target/project-local-repo/org.apache.maven.its.mng5102/gav/0.1/gav-0.1-consumer.pom");
+                ".mvn/target/project-local-repo/org.apache.maven.its.mng5102/gav/0.1/gav-0.1-consumer.pom");
         List<String> lines = verifier.loadLines(
-                "target/project-local-repo/org.apache.maven.its.mng5102/gav/0.1/gav-0.1-consumer.pom");
+                ".mvn/target/project-local-repo/org.apache.maven.its.mng5102/gav/0.1/gav-0.1-consumer.pom");
         assertTrue(lines.stream().anyMatch(l -> l.contains("<mixin>")));
     }
 
@@ -126,6 +128,7 @@ public void testWithClassifier() throws Exception {
         verifier = newVerifier(testDir.resolve("project"));
         verifier.setAutoclean(false);
         verifier.deleteDirectory("target");
+        verifier.deleteDirectory(".mvn/target");
         verifier.addCliArguments("install", "-Dmaven.consumer.pom.flatten");
         verifier.execute();
         verifier.verifyErrorFreeLog();
@@ -135,9 +138,9 @@ public void testWithClassifier() throws Exception {
         assertEquals("true", props.getProperty("project.properties.mixin4"));
 
         verifier.verifyFilePresent(
-                "target/project-local-repo/org.apache.maven.its.mng5102/classifier/0.1/classifier-0.1-consumer.pom");
+                ".mvn/target/project-local-repo/org.apache.maven.its.mng5102/classifier/0.1/classifier-0.1-consumer.pom");
         List<String> lines = verifier.loadLines(
-                "target/project-local-repo/org.apache.maven.its.mng5102/classifier/0.1/classifier-0.1-consumer.pom");
+                ".mvn/target/project-local-repo/org.apache.maven.its.mng5102/classifier/0.1/classifier-0.1-consumer.pom");
         assertTrue(lines.stream().anyMatch(l -> l.contains("<mixin>")));
     }
 }
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8414ConsumerPomWithNewFeaturesTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8414ConsumerPomWithNewFeaturesTest.java
index d99f624dae..9b297ad335 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8414ConsumerPomWithNewFeaturesTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8414ConsumerPomWithNewFeaturesTest.java
@@ -53,6 +53,7 @@ void testNotPreserving() throws Exception {
                 "The consumer POM for org.apache.maven.its:mng-8414:jar:1.0.0-SNAPSHOT cannot be downgraded to 4.0.0.");
 
         Path consumerPom = basedir.resolve(Paths.get(
+                ".mvn",
                 "target",
                 "project-local-repo",
                 "org.apache.maven.its",
@@ -83,6 +84,7 @@ void testPreserving() throws Exception {
         verifier.verifyTextNotInLog("cannot be downgraded to 4.0.0.");
 
         Path consumerPom = basedir.resolve(Paths.get(
+                ".mvn",
                 "target",
                 "project-local-repo",
                 "org.apache.maven.its",
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8645ConsumerPomDependencyManagementTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8645ConsumerPomDependencyManagementTest.java
index 43a1159670..2642da02c0 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8645ConsumerPomDependencyManagementTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8645ConsumerPomDependencyManagementTest.java
@@ -52,6 +52,7 @@ void testIt() throws Exception {
 
         // validate consumer pom
         Path consumerPom = basedir.resolve(Paths.get(
+                ".mvn",
                 "target",
                 "project-local-repo",
                 "org.apache.maven.its.mng8645",
diff --git a/its/core-it-suite/src/test/resources/bom-consumer-pom-property-resolution/.mvn/.gitkeep b/its/core-it-suite/src/test/resources/bom-consumer-pom-property-resolution/.mvn/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/its/core-it-suite/src/test/resources/gh-11162-consumer-pom-scopes/.mvn/.gitkeep b/its/core-it-suite/src/test/resources/gh-11162-consumer-pom-scopes/.mvn/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/.mvn/.gitkeep b/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/.mvn/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/.mvn/.gitkeep b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/.mvn/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-a/pom.xml b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-a/pom.xml
new file mode 100644
index 0000000000..0bd3367665
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-a/pom.xml
@@ -0,0 +1,29 @@
+<?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
+
+      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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0">
+  <parent>
+    <groupId>org.apache.maven.its.gh12646</groupId>
+    <artifactId>super-pom</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <relativePath>../super-pom</relativePath>
+  </parent>
+
+  <artifactId>module-a</artifactId>
+</project>
diff --git a/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-a/src/main/java/a/A.java b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-a/src/main/java/a/A.java
new file mode 100644
index 0000000000..4f37321283
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-a/src/main/java/a/A.java
@@ -0,0 +1,7 @@
+package a;
+
+public class A {
+    public String greet() {
+        return "Hello from A";
+    }
+}
diff --git a/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-b/pom.xml b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-b/pom.xml
new file mode 100644
index 0000000000..cc2481ca80
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-b/pom.xml
@@ -0,0 +1,29 @@
+<?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
+
+      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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0">
+  <parent>
+    <groupId>org.apache.maven.its.gh12646</groupId>
+    <artifactId>super-pom</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <relativePath>../super-pom</relativePath>
+  </parent>
+
+  <artifactId>module-b</artifactId>
+</project>
diff --git a/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-b/src/main/java/b/B.java b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-b/src/main/java/b/B.java
new file mode 100644
index 0000000000..0dda156c7e
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-b/src/main/java/b/B.java
@@ -0,0 +1,7 @@
+package b;
+
+public class B {
+    public String greet() {
+        return "Hello from B";
+    }
+}
diff --git a/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-c/pom.xml b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-c/pom.xml
new file mode 100644
index 0000000000..118156c2f7
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-c/pom.xml
@@ -0,0 +1,29 @@
+<?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
+
+      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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0">
+  <parent>
+    <groupId>org.apache.maven.its.gh12646</groupId>
+    <artifactId>super-pom</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <relativePath>../super-pom</relativePath>
+  </parent>
+
+  <artifactId>module-c</artifactId>
+</project>
diff --git a/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-c/src/main/java/c/C.java b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-c/src/main/java/c/C.java
new file mode 100644
index 0000000000..55e1d95b65
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-c/src/main/java/c/C.java
@@ -0,0 +1,7 @@
+package c;
+
+public class C {
+    public String greet() {
+        return "Hello from C";
+    }
+}
diff --git a/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-d/pom.xml b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-d/pom.xml
new file mode 100644
index 0000000000..88e8aa1638
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-d/pom.xml
@@ -0,0 +1,29 @@
+<?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
+
+      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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0">
+  <parent>
+    <groupId>org.apache.maven.its.gh12646</groupId>
+    <artifactId>super-pom</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <relativePath>../super-pom</relativePath>
+  </parent>
+
+  <artifactId>module-d</artifactId>
+</project>
diff --git a/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-d/src/main/java/d/D.java b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-d/src/main/java/d/D.java
new file mode 100644
index 0000000000..fba6e63f68
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/module-d/src/main/java/d/D.java
@@ -0,0 +1,7 @@
+package d;
+
+public class D {
+    public String greet() {
+        return "Hello from D";
+    }
+}
diff --git a/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/pom.xml b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/pom.xml
new file mode 100644
index 0000000000..9c80f90a35
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/pom.xml
@@ -0,0 +1,38 @@
+<?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
+
+      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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0" root="true">
+  <parent>
+    <groupId>org.apache.maven.its.gh12646</groupId>
+    <artifactId>super-pom</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <relativePath>super-pom</relativePath>
+  </parent>
+
+  <artifactId>root</artifactId>
+  <packaging>pom</packaging>
+
+  <modules>
+    <module>super-pom</module>
+    <module>module-a</module>
+    <module>module-b</module>
+    <module>module-c</module>
+    <module>module-d</module>
+  </modules>
+</project>
diff --git a/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/super-pom/pom.xml b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/super-pom/pom.xml
new file mode 100644
index 0000000000..9df54d716a
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-12646-project-local-repo-clean-race/super-pom/pom.xml
@@ -0,0 +1,25 @@
+<?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
+
+      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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0">
+  <groupId>org.apache.maven.its.gh12646</groupId>
+  <artifactId>super-pom</artifactId>
+  <version>1.0-SNAPSHOT</version>
+  <packaging>pom</packaging>
+</project>
diff --git a/its/core-it-suite/src/test/resources/mng-3043/.mvn/.gitkeep b/its/core-it-suite/src/test/resources/mng-3043/.mvn/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/its/core-it-suite/src/test/resources/mng-5102-mixins/classifier/project/.mvn/.gitkeep b/its/core-it-suite/src/test/resources/mng-5102-mixins/classifier/project/.mvn/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/its/core-it-suite/src/test/resources/mng-5102-mixins/gav/project/.mvn/.gitkeep b/its/core-it-suite/src/test/resources/mng-5102-mixins/gav/project/.mvn/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/its/core-it-suite/src/test/resources/mng-5102-mixins/path/.mvn/.gitkeep b/its/core-it-suite/src/test/resources/mng-5102-mixins/path/.mvn/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/its/core-it-suite/src/test/resources/mng-8414-consumer-pom-with-new-features/.mvn/.gitkeep b/its/core-it-suite/src/test/resources/mng-8414-consumer-pom-with-new-features/.mvn/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/its/core-it-suite/src/test/resources/mng-8645-consumer-pom-dep-mgmt/.mvn/.gitkeep b/its/core-it-suite/src/test/resources/mng-8645-consumer-pom-dep-mgmt/.mvn/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2