You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 -R works 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
Loading
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
Loading
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
Loading
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_PATH or pnpmStore: "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 relocate command. Works, but users still hit the error first and have to know the command exists.
Default pnpmStore to global. 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?
Summary
I keep a second copy of my Rush monorepo, made with a copy-on-write clone (
cp -con 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 installfails 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 absolutestoreDirin.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
cp -Rworks too; the copy-on-write flag only makes it fast:Expected result: Rush notices nothing changed except the folder location and reports "Installation is already up-to-date." The store,
node_modules, and lockfile inrepo-bare byte-for-byte the ones fromrepo-a.Actual result:
Running
rush install --purgethen deletescommon/tempand reinstalls from the registry. For a real monorepo that is minutes and gigabytes.Details
Why Rush behaves differently from plain pnpm
common/temp/pnpm-store(pnpmStore: "local", the default)node_moduleshard-links into the in-repo storelast-install.flagrecords the absolute repo root and store path; pnpm's.modules.yamlrecords the absolutestoreDirWhere 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:2pxA 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:2pxRestoring 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:2pxProposed fix
Make the install state relocatable, in the code that decides whether the last install is still valid (
LastInstallFlagandBaseInstallManagerin@microsoft/rush-lib).1. Compare recorded paths relative to the repo root. When Rush reads
last-install.flag, it would first check whether the recordedrushJsonFoldermatches 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-storeand 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:
RUSH_PNPM_STORE_PATHorpnpmStore: "global"is still compared as an exact string.2. Patch pnpm's own record of the store. Rush accepting the move is not enough. pnpm records
storeDirinnode_modules/.modules.yamland throwsERR_PNPM_UNEXPECTED_STOREif it does not match the--storeargument. When Rush detects a move and everything else is valid, it would rewritestoreDir(and on WindowsvirtualStoreDir, which pnpm writes as an absolute path there) to point under the new root, and say so:At the end of the command Rush writes a fresh
last-install.flagwith 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 passesrush installin seconds and the pre-installed image is actually reused.Known limits.
rush-pnpmrun directly in a moved repo before the firstrush installwould still hit pnpm's store error, since only the install path patches.modules.yaml. Injected dependencies write.pnpm-sync.jsonfiles with absolute paths; those are out of scope here.Alternatives considered
last-install.flag. Cleaner data, but a format change: every existing checkout reinstalls once, and pnpm's file still needs the patch above.rush relocatecommand. Works, but users still hit the error first and have to know the command exists.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:
@microsoft/rushglobally installed version?rushVersionfrom rush.json?pnpmVersion,npmVersion, oryarnVersionfrom rush.json?useWorkspacesfrom pnpm-config.json?node -v)?