[rush] Avoid unnecessary package manager install lock#5844
Conversation
| path: `${tempFolderPath}/rush-global`, | ||
| nodeSpecificPath: `${tempFolderPath}/rush-global/node-${process.version}` | ||
| } as RushGlobalFolder; | ||
| const packageManagerToolFolder: string = path.join(rushGlobalFolder.nodeSpecificPath, 'pnpm-10.27.0'); |
There was a problem hiding this comment.
How is this version of pnpm selected, and when was it installed? I don't see anything mocking the file system checks to ensure that the version exists and/or writing files to do so.
There was a problem hiding this comment.
Thanks for review !
I updated the setup to explicitly create a fake installed pnpm in the temp Rush global folder. The fixture now writes the root package.json, node_modules/pnpm/package.json, node_modules/.bin/pnpm, and the last-install.flag. The version comes from the test constant, currently matching this repo's rush.json pnpmVersion: 10.27.0.
| packageManagerToolVersion: '10.27.0' | ||
| } as RushConfiguration; | ||
|
|
||
| await InstallHelpers.ensureLocalPackageManagerAsync(rushConfiguration, rushGlobalFolder, 1, true); |
There was a problem hiding this comment.
Ensure that the lockAcquireSpy returns false so that the test doesn't try to actually install the package if something goes wrong.
| }); | ||
| } | ||
|
|
||
| private static _doesPackageManagerInstallLockFileExist( |
There was a problem hiding this comment.
Do this async and make this a loose function, not a private static
There was a problem hiding this comment.
Seems like this should be a utility on LastInstallFlag
| } | ||
|
|
||
| function getPackageManagerToolFolder(rushGlobalFolder: RushGlobalFolder): string { | ||
| return path.join(rushGlobalFolder.nodeSpecificPath, `${packageManager}-${packageManagerVersion}`); |
| { ensureFolderExists: true } | ||
| ); | ||
|
|
||
| JsonFile.save( |
There was a problem hiding this comment.
Do this async and in parallel with the other one
| name: packageManager, | ||
| version: packageManagerVersion | ||
| }, | ||
| path.join(packageManagerToolFolder, 'node_modules', packageManager, 'package.json'), |
There was a problem hiding this comment.
Use a template string. Forward slashes work fine on Windows.
|
|
||
| const packageManagerBinFolder: string = path.join(packageManagerToolFolder, 'node_modules', '.bin'); | ||
| FileSystem.ensureFolder(packageManagerBinFolder); | ||
| FileSystem.writeFile(path.join(packageManagerBinFolder, packageManager), ''); |
There was a problem hiding this comment.
| FileSystem.writeFile(path.join(packageManagerBinFolder, packageManager), ''); | |
| await FileSystem.writeFileAsync(`${packageManagerBinFolder}/${packageManager}`, '', { ensureFolderExists: true }); |
| ); | ||
|
|
||
| const packageManagerBinFolder: string = path.join(packageManagerToolFolder, 'node_modules', '.bin'); | ||
| FileSystem.ensureFolder(packageManagerBinFolder); |
There was a problem hiding this comment.
| FileSystem.ensureFolder(packageManagerBinFolder); |
|
|
||
| expect(lockAcquireSpy).not.toHaveBeenCalled(); | ||
| expect(installSpy).not.toHaveBeenCalled(); | ||
| expect(FileSystem.exists(`${rushConfiguration.commonTempFolder}/pnpm-local`)).toEqual(true); |
There was a problem hiding this comment.
| expect(FileSystem.exists(`${rushConfiguration.commonTempFolder}/pnpm-local`)).toEqual(true); | |
| await expect(FileSystem.existsAsync(`${rushConfiguration.commonTempFolder}/pnpm-local`)).resolves.toEqual(true); |
| expect(lockAcquireSpy).toHaveBeenCalledTimes(1); | ||
| expect(installSpy).toHaveBeenCalledTimes(1); | ||
| expect(releaseLockMock).toHaveBeenCalledTimes(1); | ||
| expect(FileSystem.exists(`${rushConfiguration.commonTempFolder}/pnpm-local`)).toEqual(true); |
There was a problem hiding this comment.
| expect(FileSystem.exists(`${rushConfiguration.commonTempFolder}/pnpm-local`)).toEqual(true); | |
| await expect(FileSystem.existsAsync(`${rushConfiguration.commonTempFolder}/pnpm-local`)).resolves.toEqual(true); |
| node: process.versions.node | ||
| }); | ||
|
|
||
| if ( |
There was a problem hiding this comment.
Assign the async result to a variable outside of the if to avoid refactoring mistakes.
fe56e6f to
37fea3f
Compare
| if (itemName === `${lockFileResourceName}.lock`) { | ||
| return true; | ||
| } | ||
|
|
||
| if (itemName.startsWith(`${lockFileResourceName}#`) && itemName.endsWith('.lock')) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Where do these names come from? Can you share code with whatever generates them when they're written?
There was a problem hiding this comment.
Original from here, but I noticed that we should reuse this part of name generating logic instead of re-copy it.
Co-authored-by: Ian Clanton-Thuon <iclanton@users.noreply.github.com>
0b789cf to
f5d1d4b
Compare
| "comment": "Add a LockFile helper for matching lock file names to lock resources.", | ||
| "type": "patch" |
There was a problem hiding this comment.
| "comment": "Add a LockFile helper for matching lock file names to lock resources.", | |
| "type": "patch" | |
| "comment": "Add a `LockFile.isLockFileNameForResource` helper for matching lock file names to lock resources.", | |
| "type": "minor" |
There was a problem hiding this comment.
I reverted changes in node-core-library, just do a minimal fix.
There was a problem hiding this comment.
Pull request overview
This PR refines Rush’s package-manager bootstrapping path by avoiding the global package manager install lock when it’s not needed (package manager already installed and no install lock file is present), while preserving the existing “stale/dirty recovery” behavior when a lock file exists.
Changes:
- Skip acquiring the global package-manager install lock when the existing install is valid and no corresponding lock file exists.
- Add an internal helper to detect presence of matching lock files for a given lock resource name.
- Add unit tests covering the “skip lock” and “lock when lockfile exists” behaviors, plus a change log entry.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| libraries/rush-lib/src/logic/installManager/InstallHelpers.ts | Adds the early-exit path that avoids acquiring the global install lock when safe, and refactors symlink creation into a helper. |
| libraries/rush-lib/src/api/LastInstallFlag.ts | Adds doesLastInstallFlagLockFileExistAsync() to detect existing lock files for the same lock resource, supporting the optimization. |
| libraries/rush-lib/src/logic/test/InstallHelpers.test.ts | Adds test coverage ensuring the lock is skipped when already installed, and still acquired when a lock file is present. |
| common/changes/@microsoft/rush/package-manager-install-lock_2026-06-23-13-27.json | Records the behavior change as a patch. |
Summary
This PR extracts the package-manager install lock optimization from #5830 into a standalone change.
Rush now skips acquiring the global package manager install lock when the requested package manager is already installed and no matching install lock file exists. If a lock file exists, Rush still acquires the lock to preserve stale/dirty recovery behavior.
Testing
node common/scripts/install-run-rush.js test --to @microsoft/rush-lib --verbose