Skip to content

[rush] Avoid unnecessary package manager install lock#5844

Open
EscapeB wants to merge 1 commit into
microsoft:mainfrom
EscapeB:codex/package-manager-install-lock
Open

[rush] Avoid unnecessary package manager install lock#5844
EscapeB wants to merge 1 commit into
microsoft:mainfrom
EscapeB:codex/package-manager-install-lock

Conversation

@EscapeB

@EscapeB EscapeB commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

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

path: `${tempFolderPath}/rush-global`,
nodeSpecificPath: `${tempFolderPath}/rush-global/node-${process.version}`
} as RushGlobalFolder;
const packageManagerToolFolder: string = path.join(rushGlobalFolder.nodeSpecificPath, 'pnpm-10.27.0');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that the lockAcquireSpy returns false so that the test doesn't try to actually install the package if something goes wrong.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

});
}

private static _doesPackageManagerInstallLockFileExist(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do this async and make this a loose function, not a private static

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this should be a utility on LastInstallFlag

}

function getPackageManagerToolFolder(rushGlobalFolder: RushGlobalFolder): string {
return path.join(rushGlobalFolder.nodeSpecificPath, `${packageManager}-${packageManagerVersion}`);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use a template string

Comment thread libraries/rush-lib/src/logic/test/InstallHelpers.test.ts Outdated
{ ensureFolderExists: true }
);

JsonFile.save(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do this async and in parallel with the other one

name: packageManager,
version: packageManagerVersion
},
path.join(packageManagerToolFolder, 'node_modules', packageManager, 'package.json'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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), '');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
FileSystem.ensureFolder(packageManagerBinFolder);


expect(lockAcquireSpy).not.toHaveBeenCalled();
expect(installSpy).not.toHaveBeenCalled();
expect(FileSystem.exists(`${rushConfiguration.commonTempFolder}/pnpm-local`)).toEqual(true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assign the async result to a variable outside of the if to avoid refactoring mistakes.

@EscapeB
EscapeB force-pushed the codex/package-manager-install-lock branch from fe56e6f to 37fea3f Compare July 20, 2026 11:48
Comment on lines +202 to +208
if (itemName === `${lockFileResourceName}.lock`) {
return true;
}

if (itemName.startsWith(`${lockFileResourceName}#`) && itemName.endsWith('.lock')) {
return true;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do these names come from? Can you share code with whatever generates them when they're written?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const packageManagerAndVersion: string = `${packageManager}-${packageManagerVersion}`;

const lockFilePath: string = LockFile.getLockFilePath(resourceFolder, resourceName);

Original from here, but I noticed that we should reuse this part of name generating logic instead of re-copy it.

Comment thread libraries/rush-lib/src/api/LastInstallFlag.ts Outdated
Co-authored-by: Ian Clanton-Thuon <iclanton@users.noreply.github.com>
@EscapeB
EscapeB force-pushed the codex/package-manager-install-lock branch from 0b789cf to f5d1d4b Compare July 21, 2026 08:29
Comment on lines +5 to +6
"comment": "Add a LockFile helper for matching lock file names to lock resources.",
"type": "patch"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"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"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted changes in node-core-library, just do a minimal fix.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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

Labels

None yet

Projects

Status: Needs triage

Development

Successfully merging this pull request may close these issues.

4 participants