[DISCUSS] Fix for #12646 — moving project-local-re po out of target/

Guillaume Nodet <[email protected]> Mon, 3 Aug 2026 14:09:20 +0200
Newsgroups gmane.comp.jakarta.turbine.maven.devel
Message-ID <CAA66TpoiJzqxEov4b4u2_G9=Xhgw2YF6BTD6xD_DKaSA_kb-dw@mail.gmail.com>
--0000000000001dac6806582369dd
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

Hi all,

I'd like to get your input on the fix for GH-12646 =E2=80=94 a race conditi=
on with
project-local-repo during parallel clean install builds.

The problem

When the root pom.xml has a parent that is also part of the reactor (e.g. a
super-pom), MultiThreadedBuilder schedules the parent first, then runs the
root project and child modules concurrently. Since clean and install are
placed into the same TaskSegment, there is no barrier between them =E2=80=
=94 the
root project's maven-clean-plugin can delete target/ while sibling modules
are concurrently writing artifacts into target/project-local-repo, causing
the build to fail.

Approaches considered

1. Lock-based synchronization (ReentrantReadWriteLock in ReactorReader)

Acquire a write lock when the project owning target/ enters its clean
phase, and a read lock when installing artifacts. This prevents the crash
(concurrent access) but does not guarantee ordering =E2=80=94 a module coul=
d
install artifacts before the root's clean starts, only to have them wiped.
It also adds complexity to ReactorReader for what is fundamentally an
architectural problem.

2. Move project-local-repo to ~/.m2/ (user home)

E.g. ~/.m2/local-repository/${projectName}_${hash}, similar to how IntelliJ
stores project caches. This avoids both the race and the git concern, but:
- Hard links (already used by ReactorReader) cannot cross filesystem
boundaries =E2=80=94 if ~/.m2/ is on a different volume, every artifact bec=
omes a
full copy
- Stale directories accumulate when projects are deleted/moved, with no
cleanup mechanism
- CI containers often share ~/.m2/ across builds, causing unwanted
accumulation
- Loses project locality (harder to inspect/debug)

3. Move project-local-repo to .mvn/project-local-repo (chosen)

Move the directory from target/project-local-repo to
.mvn/project-local-repo. Since maven-clean-plugin only deletes target/, the
race becomes structurally impossible. ReactorReader fully owns the
lifecycle =E2=80=94 per-GAV cleanup when a project enters its clean phase, =
install
on project success. The existing hard-link optimization continues to work
since both directories are on the same filesystem.

The trade-off is that .mvn/project-local-repo needs to be gitignored. This
follows the Gradle convention where .gradle/ at the project root is
universally in .gitignore templates. Maven could also document this
convention or consider auto-appending the entry.

The fix itself is minimal =E2=80=94 the core change is a single line in
getProjectLocalRepo(), and the rest is removing the lock infrastructure
(net -40 lines).

4. Keep in target/ but use maven-clean-plugin fast mode

The fast clean option (maven.clean.fast=3Dtrue, since plugin 3.2) atomicall=
y
renames target/ instead of recursively deleting it. This would likely avoid
the race, but it's opt-in and not the default =E2=80=94 users hitting the b=
ug would
need to know about it.

FWIW, a PR has been raised on m-clean-p master branch (4.x) to refactor the
fast cleaner (fixing problems) and make it the default.

Open questions

- Is .mvn/ the right location, or should we consider a different
project-root directory?
- Should Maven auto-add .mvn/project-local-repo to .gitignore, or document
it as a convention?
- Are there other considerations I'm missing?

PR: https://github.com/apache/maven/pull/12650
Issue: https://github.com/apache/maven/issues/12646

Looking forward to your thoughts.

 Cheers
Guillaume Nodet

--0000000000001dac6806582369dd--