diff --git a/common/changes/@microsoft/rush/fix-package-extensions-hash_2025-10-03-17-31.json b/common/changes/@microsoft/rush/fix-package-extensions-hash_2025-10-03-17-31.json new file mode 100644 index 00000000000..18f5787a522 --- /dev/null +++ b/common/changes/@microsoft/rush/fix-package-extensions-hash_2025-10-03-17-31.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Fix an issue with validation of the `pnpm-lock.yaml` `packageExtensionsChecksum` field in pnpm v10.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index 7b3939e4cf7..164d3881b80 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -846,6 +846,10 @@ "name": "npm-packlist", "allowedCategories": [ "libraries" ] }, + { + "name": "object-hash", + "allowedCategories": [ "libraries" ] + }, { "name": "open", "allowedCategories": [ "libraries" ] diff --git a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml index 29b7e3f3324..0827f796a75 100644 --- a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml +++ b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml @@ -5100,6 +5100,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + /object-inspect@1.13.3: resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} engines: {node: '>= 0.4'} @@ -7241,6 +7245,7 @@ packages: js-yaml: 4.1.0 npm-check: 6.0.1 npm-package-arg: 6.1.1 + object-hash: 3.0.0 pnpm-sync-lib: 0.3.2 read-package-tree: 5.1.6 rxjs: 6.6.7 diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index 3ce7bd98eca..1d2781d97f7 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -1,6 +1,6 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "9eaf55ba29de8d94e17af33463f3b6ce0480400b", + "pnpmShrinkwrapHash": "f2c06df75a96061e31624e1a556e34660a569623", "preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9", - "packageJsonInjectedDependenciesHash": "5f2a311100f4ad0bc6735377670f5d7cfd664127" + "packageJsonInjectedDependenciesHash": "14f4881943e5d03a361f079944eb76e1501b3e18" } diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index c51e1839bce..b9ceb1be59b 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -3684,6 +3684,9 @@ importers: npm-package-arg: specifier: ~6.1.0 version: 6.1.1 + object-hash: + specifier: 3.0.0 + version: 3.0.0 pnpm-sync-lib: specifier: 0.3.2 version: 0.3.2 @@ -3745,6 +3748,9 @@ importers: '@types/npm-package-arg': specifier: 6.1.0 version: 6.1.0 + '@types/object-hash': + specifier: ~3.0.6 + version: 3.0.6 '@types/read-package-tree': specifier: 5.1.0 version: 5.1.0 @@ -14084,6 +14090,10 @@ packages: '@types/node': 17.0.41 dev: true + /@types/object-hash@3.0.6: + resolution: {integrity: sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==} + dev: true + /@types/overlayscrollbars@1.12.5: resolution: {integrity: sha512-1yMmgFrq1DQ3sCHyb3DNfXnE0dB463MjG47ugX3cyade3sOt3U8Fjxk/Com0JJguTLPtw766TSDaO4NC65Wgkw==} dev: true @@ -25073,6 +25083,11 @@ packages: define-property: 0.2.5 kind-of: 3.2.2 + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: false + /object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index d0a87256738..eb22dd89def 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "b14e05fb6e67142fd6a574fe462bee93cb828511", + "pnpmShrinkwrapHash": "c643d2cc7e2c60ef034a6557fc89760140d42f66", "preferredVersionsHash": "61cd419c533464b580f653eb5f5a7e27fe7055ca" } diff --git a/libraries/rush-lib/package.json b/libraries/rush-lib/package.json index dc6c270b5c3..419bff81377 100644 --- a/libraries/rush-lib/package.json +++ b/libraries/rush-lib/package.json @@ -57,6 +57,7 @@ "js-yaml": "~4.1.0", "npm-check": "~6.0.1", "npm-package-arg": "~6.1.0", + "object-hash": "3.0.0", "pnpm-sync-lib": "0.3.2", "read-package-tree": "~5.1.5", "rxjs": "~6.6.7", @@ -70,8 +71,6 @@ "devDependencies": { "@pnpm/lockfile.types": "~1.0.3", "@pnpm/logger": "4.0.0", - "eslint": "~9.25.1", - "local-node-rig": "workspace:*", "@rushstack/heft-webpack5-plugin": "workspace:*", "@rushstack/heft": "workspace:*", "@rushstack/operation-graph": "workspace:*", @@ -81,12 +80,15 @@ "@types/inquirer": "7.3.1", "@types/js-yaml": "4.0.9", "@types/npm-package-arg": "6.1.0", + "@types/object-hash": "~3.0.6", "@types/read-package-tree": "5.1.0", "@types/semver": "7.5.0", "@types/ssri": "~7.1.0", "@types/strict-uri-encode": "2.0.0", "@types/tar": "6.1.6", "@types/webpack-env": "1.18.8", + "eslint": "~9.25.1", + "local-node-rig": "workspace:*", "webpack": "~5.98.0" }, "publishOnlyDependencies": { diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index c97af72bba1..15984bb2704 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -399,11 +399,41 @@ export class WorkspaceInstallManager extends BaseInstallManager { } // Check if packageExtensionsChecksum matches globalPackageExtension's hash - const packageExtensionsChecksum: string | undefined = this._getPackageExtensionChecksum( - pnpmOptions.globalPackageExtensions - ); + let packageExtensionsChecksum: string | undefined; + let existingPackageExtensionsChecksum: string | undefined; + if (shrinkwrapFile) { + existingPackageExtensionsChecksum = shrinkwrapFile.packageExtensionsChecksum; + let packageExtensionsChecksumAlgorithm: string | undefined; + if (existingPackageExtensionsChecksum) { + const dashIndex: number = existingPackageExtensionsChecksum.indexOf('-'); + if (dashIndex === -1) { + packageExtensionsChecksumAlgorithm = existingPackageExtensionsChecksum.substring(0, dashIndex); + } + + if (packageExtensionsChecksumAlgorithm && packageExtensionsChecksumAlgorithm !== 'sha256') { + this._terminal.writeErrorLine( + `The existing packageExtensionsChecksum algorithm "${packageExtensionsChecksumAlgorithm}" is not supported. ` + + `This may indicate that the shrinkwrap was created with a newer version of PNPM than Rush supports.` + ); + throw new AlreadyReportedError(); + } + } + + const globalPackageExtensions: Record | undefined = + pnpmOptions.globalPackageExtensions; + // https://github.com/pnpm/pnpm/blob/ba9409ffcef0c36dc1b167d770a023c87444822d/pkg-manager/core/src/install/index.ts#L331 + if (globalPackageExtensions && Object.keys(globalPackageExtensions).length !== 0) { + if (packageExtensionsChecksumAlgorithm) { + // In PNPM v10, the algorithm changed to SHA256 and the digest changed from hex to base64 + packageExtensionsChecksum = await createObjectChecksumAsync(globalPackageExtensions); + } else { + packageExtensionsChecksum = createObjectChecksumLegacy(globalPackageExtensions); + } + } + } + const packageExtensionsChecksumAreEqual: boolean = - packageExtensionsChecksum === shrinkwrapFile?.packageExtensionsChecksum; + packageExtensionsChecksum === existingPackageExtensionsChecksum; if (!packageExtensionsChecksumAreEqual) { shrinkwrapWarnings.push("The package extension hash doesn't match the current shrinkwrap."); @@ -420,18 +450,6 @@ export class WorkspaceInstallManager extends BaseInstallManager { return { shrinkwrapIsUpToDate, shrinkwrapWarnings }; } - private _getPackageExtensionChecksum( - packageExtensions: Record | undefined - ): string | undefined { - // https://github.com/pnpm/pnpm/blob/ba9409ffcef0c36dc1b167d770a023c87444822d/pkg-manager/core/src/install/index.ts#L331 - const packageExtensionsChecksum: string | undefined = - Object.keys(packageExtensions ?? {}).length === 0 - ? undefined - : createObjectChecksum(packageExtensions!); - - return packageExtensionsChecksum; - } - protected async canSkipInstallAsync( lastModifiedDate: Date, subspace: Subspace, @@ -793,10 +811,36 @@ export class WorkspaceInstallManager extends BaseInstallManager { /** * Source: https://github.com/pnpm/pnpm/blob/ba9409ffcef0c36dc1b167d770a023c87444822d/pkg-manager/core/src/install/index.ts#L821-L824 - * @param obj - * @returns */ -function createObjectChecksum(obj: Record): string { +function createObjectChecksumLegacy(obj: Record): string { const s: string = JSON.stringify(Sort.sortKeys(obj)); return createHash('md5').update(s).digest('hex'); } + +/** + * Source: https://github.com/pnpm/pnpm/blob/bdbd31aa4fa6546d65b6eee50a79b51879340d40/crypto/object-hasher/src/index.ts#L8-L12 + */ +const defaultOptions: import('object-hash').NormalOption = { + respectType: false, + algorithm: 'sha256', + encoding: 'base64' +}; + +/** + * https://github.com/pnpm/pnpm/blob/bdbd31aa4fa6546d65b6eee50a79b51879340d40/crypto/object-hasher/src/index.ts#L21-L26 + */ +const withSortingOptions: import('object-hash').NormalOption = { + ...defaultOptions, + unorderedArrays: true, + unorderedObjects: true, + unorderedSets: true +}; + +/** + * Source: https://github.com/pnpm/pnpm/blob/bdbd31aa4fa6546d65b6eee50a79b51879340d40/crypto/object-hasher/src/index.ts#L45-L49 + */ +async function createObjectChecksumAsync(obj: Record): Promise { + const { default: hash } = await import('object-hash'); + const packageExtensionsChecksum: string = hash(obj, withSortingOptions); + return `${defaultOptions.algorithm}-${packageExtensionsChecksum}`; +} diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index 4c1bad0346c..ab61818fee1 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -580,7 +580,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { */ private _parseDependencyPath(packagePath: string): string { let depPath: string = packagePath; - if (this.shrinkwrapFileMajorVersion >= 6) { + if (this.shrinkwrapFileMajorVersion >= ShrinkwrapFileMajorVersion.V6) { depPath = this._convertLockfileV6DepPathToV5DepPath(packagePath); } const pkgInfo: ReturnType = @@ -998,7 +998,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { const allDependencies: PackageJsonDependency[] = [...dependencyList, ...devDependencyList]; - if (this.shrinkwrapFileMajorVersion < 6) { + if (this.shrinkwrapFileMajorVersion < ShrinkwrapFileMajorVersion.V6) { // PNPM <= v7 // Then get the unique package names and map them to package versions.