[rush] Reuse the existing installation when a repo is moved, renamed, or cloned
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 62/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- nodejs, typescript
- Domain
- build-system, tooling
Research direction
Start with LastInstallFlag and BaseInstallManager in @microsoft/rush-lib, where Rush decides whether the last installation is valid. Review the unit tests and the end-to-end check mentioned in the issue, then verify moved or copied repositories reuse their installation while external store paths and other installation state still require exact matches.
Written by the indexing model from the issue text.
Description
Summary
I keep a second copy of my Rush monorepo, made with a copy-on-write clone (cp -c on APFS, btrfs snapshot on Linux), so I can work on two branches with dependencies already installed. Rush refuses to reuse the install in the copy: rush install fails with "Current PNPM store path does not match the last one used" and demands --purge, which deletes and re-downloads everything even though every file is already present and correct at the new path. Plain renaming of the repo folder triggers the same thing.
The cause is that Rush records the repository's absolute path in last-install.flag (rushJsonFolder, storePath) and pnpm records the absolute storeDir in .modules.yaml, and Rush compares them as exact strings. This issue proposes making that install state relocatable.
Related: #5633 (git worktree support) shares the root cause. This proposal is narrower: it does not change where the store lives, only whether a moved store is accepted.
Repro steps
- Create a minimal Rush repo with one project that has one dependency, and install it:
mkdir repo-a && cd repo-a && rush init # rush.json: pnpmVersion 9.15.9, one project at packages/app depending on semver rush update - Clone the installed repo to a new path.
cp -Rworks too; the copy-on-write flag only makes it fast:cd .. && cp -c -R -p repo-a repo-b - Run Rush in the copy:
cd repo-b && rush install
Expected result: Rush notices nothing changed except the folder location and reports "Installation is already up-to-date." The store, node_modules, and lockfile in repo-b are byte-for-byte the ones from repo-a.
Actual result:
Checking installation in ".../repo-b/common/temp"
ERROR: Current PNPM store path does not match the last one used. This may cause inconsistency in your builds.
If you wish to install with the new store path, please run "rush install --purge"
Old Path: .../repo-a/common/temp/pnpm-store
New Path: .../repo-b/common/temp/pnpm-store
Running rush install --purge then deletes common/temp and reinstalls from the registry. For a real monorepo that is minutes and gigabytes.
Details
Why Rush behaves differently from plain pnpm
| pnpm on its own | Rush |
|---|---|
| One content-addressable store per machine, in the user's home folder | One store per repository, inside it at common/temp/pnpm-store (pnpmStore: "local", the default) |
| Projects hard-link into that store | The repo's node_modules hard-links into the in-repo store |
| Nothing in the project records where the project folder is | last-install.flag records the absolute repo root and store path; pnpm's .modules.yaml records the absolute storeDir |
| Rename or copy the project folder and it keeps working | Rename or copy the repo and Rush refuses to reuse the install |
Where this bites
In each diagram, the flow forks at the yellow node where Rush checks last-install.flag. Green is what the user expects; red is what Rush does today.
Cloning an installed repo to work on two branches at once
flowchart TD
A["Copy repo A to B with cp -c (seconds)"] --> B["cd into B"]
B --> C{"rush build: Rush checks last-install.flag"}
C -->|What the user expects| G1["Paths match, install reused"]
G1 --> G2["Build starts immediately"]
C -->|What Rush does today| R1["ERROR: store path does not match"]
R1 --> R2["rush install --purge"]
R2 --> R3["Copied store and node_modules deleted"]
R3 --> R4["Full reinstall: minutes, gigabytes"]
R4 --> R5["Build finally starts"]
classDef step fill:#F1F3F4,stroke:#5F6368,color:#202124
classDef fork fill:#FFF4C2,stroke:#E37400,color:#202124
classDef good fill:#E6F4EA,stroke:#1E8E3E,color:#202124
classDef bad fill:#FCE8E6,stroke:#C5221F,color:#202124
classDef err fill:#F5B5B1,stroke:#C5221F,color:#202124
class A,B step
class C fork
class G1,G2 good
class R1 err
class R2,R3,R4,R5 bad
linkStyle 2,3 stroke:#1E8E3E,stroke-width:2px
linkStyle 4,5,6,7,8 stroke:#C5221F,stroke-width:2px
A team at ByteDance ended up writing an internal CLI whose only job is to rewrite the two recorded paths so the clone is accepted.
Renaming or moving the repo folder
flowchart TD
A["Rename ~/code/repo to ~/code/repo-main"] --> C{"rush build: Rush checks last-install.flag"}
C -->|What the user expects| G1["Paths match, install reused"]
G1 --> G2["Keep working"]
C -->|What Rush does today| R1["ERROR: store path does not match"]
R1 --> R2["rush install --purge"]
R2 --> R3["Full reinstall: minutes, gigabytes"]
R3 --> R4["Back to work"]
classDef step fill:#F1F3F4,stroke:#5F6368,color:#202124
classDef fork fill:#FFF4C2,stroke:#E37400,color:#202124
classDef good fill:#E6F4EA,stroke:#1E8E3E,color:#202124
classDef bad fill:#FCE8E6,stroke:#C5221F,color:#202124
classDef err fill:#F5B5B1,stroke:#C5221F,color:#202124
class A step
class C fork
class G1,G2 good
class R1 err
class R2,R3,R4 bad
linkStyle 1,2 stroke:#1E8E3E,stroke-width:2px
linkStyle 3,4,5,6 stroke:#C5221F,stroke-width:2px
Restoring a pre-installed workspace on a build machine
A CI image or cache is prepared with the repo installed at one path, and a job restores it to a different path.
flowchart TD
A["Restore workspace to the job folder"] --> C{"rush install: Rush checks last-install.flag"}
C -->|What the user expects| G1["Nothing changed, install reused"]
G1 --> G2["rush build within seconds"]
C -->|What Rush does today| R1["ERROR: store path does not match"]
R1 --> R2["Pipeline adds --purge to every job"]
R2 --> R3["Full install on every job"]
R3 --> R4["Pre-installed image gains nothing"]
classDef step fill:#F1F3F4,stroke:#5F6368,color:#202124
classDef fork fill:#FFF4C2,stroke:#E37400,color:#202124
classDef good fill:#E6F4EA,stroke:#1E8E3E,color:#202124
classDef bad fill:#FCE8E6,stroke:#C5221F,color:#202124
classDef err fill:#F5B5B1,stroke:#C5221F,color:#202124
class A step
class C fork
class G1,G2 good
class R1 err
class R2,R3,R4 bad
linkStyle 1,2 stroke:#1E8E3E,stroke-width:2px
linkStyle 3,4,5,6 stroke:#C5221F,stroke-width:2px
Proposed fix
Make the install state relocatable, in the code that decides whether the last install is still valid (LastInstallFlag and BaseInstallManager in @microsoft/rush-lib).
1. Compare recorded paths relative to the repo root. When Rush reads last-install.flag, it would first check whether the recorded rushJsonFolder matches the current repo root. If not, the repo has moved. Rush would then re-root every recorded path that was inside the old root onto the new root before comparing. Example: the store was recorded as /Users/me/code/repo-a/common/temp/pnpm-store and the repo now lives at /Users/me/code/repo-b; Rush rewrites the recorded value to /Users/me/code/repo-b/common/temp/pnpm-store, which matches the actual store, so the install counts as unchanged.
Two constraints keep this safe:
- Paths outside the repo are never adjusted. A store configured via
RUSH_PNPM_STORE_PATHorpnpmStore: "global"is still compared as an exact string. - Every other recorded value must still match exactly: Node.js version, pnpm version, selected projects, and so on. Anything else changing still triggers a clean install.
2. Patch pnpm's own record of the store. Rush accepting the move is not enough. pnpm records storeDir in node_modules/.modules.yaml and throws ERR_PNPM_UNEXPECTED_STORE if it does not match the --store argument. When Rush detects a move and everything else is valid, it would rewrite storeDir (and on Windows virtualStoreDir, which pnpm writes as an absolute path there) to point under the new root, and say so:
The repository was moved from "/Users/me/code/repo-a" to "/Users/me/code/repo-b" since the last install. Reusing the existing installation.
Updated "storeDir" in .modules.yaml: /Users/me/code/repo-b/common/temp/pnpm-store/v3
Installation is already up-to-date.
At the end of the command Rush writes a fresh last-install.flag with the new paths, so this happens once per move.
Effect on the three cases. The clone builds immediately after cp -c. A renamed or moved repo keeps working with no extra command. A restored CI workspace passes rush install in seconds and the pre-installed image is actually reused.
Known limits. rush-pnpm run directly in a moved repo before the first rush install would still hit pnpm's store error, since only the install path patches .modules.yaml. Injected dependencies write .pnpm-sync.json files with absolute paths; those are out of scope here.
Alternatives considered
- Store repo-relative paths in
last-install.flag. Cleaner data, but a format change: every existing checkout reinstalls once, and pnpm's file still needs the patch above. - An explicit
rush relocatecommand. Works, but users still hit the error first and have to know the command exists. - Default
pnpmStoretoglobal. Sidesteps the problem by not having repo-local absolute state, but changes the default for everyone and breaks store sharing across copy-on-write clones.
I have a working implementation of the proposal, with unit tests and an end-to-end check against the repro above, and can open a PR if maintainers are open to the approach.
Standard questions
Please answer these questions to help us investigate your issue more quickly:
| Question | Answer |
|---|---|
@microsoft/rush globally installed version? |
5.178.1 |
rushVersion from rush.json? |
5.178.1 |
pnpmVersion, npmVersion, or yarnVersion from rush.json? |
pnpm@9.15.9 (also reproduced with pnpm@10.27.0) |
(if pnpm) useWorkspaces from pnpm-config.json? |
true |
| Operating system? | Mac (also affects Linux with btrfs snapshots) |
| Would you consider contributing a PR? | Yes |
Node.js version (node -v)? |
v22.22.3 |
- Dominant language
- TypeScript
- Stars
- 6.5k
- Forks
- 708
- Avg merge
- 5d 7h
- Merged PRs (30d)
- 44
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/rushstack
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
All issues in microsoft/rushstack
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Eynzof/Hermes-CN-Desktop#610 ·
-
bug clawsweeper:linked-pr-open clawsweeper:needs-live-repro clawsweeper:no-new-fix-pr impact:message-loss issue-rating: 🐚 platinum hermit P2 regression
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
calcite-components needs triage refactor
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Esri/calcite-design-system#15203 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 78/100
fullcalendar/fullcalendar#8106 ·