Skip to content

[rush] Reuse the existing installation when a repo is moved, renamed, or cloned #6035

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

  1. 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
  2. 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
  3. 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? 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    • Status
      Needs triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions