diff --git a/.changeset/patched-deps-lockfile-dir.md b/.changeset/patched-deps-lockfile-dir.md new file mode 100644 index 0000000000..ece0a5883f --- /dev/null +++ b/.changeset/patched-deps-lockfile-dir.md @@ -0,0 +1,8 @@ +--- +"@pnpm/lockfile.settings-checker": minor +"@pnpm/deps.status": patch +"@pnpm/installing.deps-installer": patch +"pnpm": patch +--- + +Relative paths in `patchedDependencies` are now resolved against the lockfile directory when computing patch file hashes, so running `pnpm install` from a subdirectory no longer fails with `ENOENT` looking for the patch file in the wrong location [#12762](https://github.com/pnpm/pnpm/pull/12762). diff --git a/pacquet/crates/config/src/lib.rs b/pacquet/crates/config/src/lib.rs index 57b701fa55..b81bddb585 100644 --- a/pacquet/crates/config/src/lib.rs +++ b/pacquet/crates/config/src/lib.rs @@ -1665,8 +1665,6 @@ impl Config { /// `patchedDependencies` map the lockfile records: each configured /// key mapped to its patch file's SHA-256 hex digest. /// - /// The configured patch paths are resolved against the manifest - /// dir before hashing. /// Distinct from [`Self::resolved_patched_dependencies`], which /// groups the same entries by package name for the resolver — this /// keeps the user's verbatim keys so the lockfile is byte-faithful diff --git a/pacquet/crates/patching/src/resolve.rs b/pacquet/crates/patching/src/resolve.rs index 20920a7b58..9e7ac78506 100644 --- a/pacquet/crates/patching/src/resolve.rs +++ b/pacquet/crates/patching/src/resolve.rs @@ -46,7 +46,11 @@ impl From for ResolvePatchedDependenciesError { /// Each raw path is resolved against `workspace_dir`, hashed into a /// [`PatchInput`] entry, and then grouped. pnpm v11 reads install /// settings (including `patchedDependencies`) from the *workspace* -/// manifest, not from `package.json`'s `pnpm` field. +/// manifest, not from `package.json`'s `pnpm` field, and anchors +/// still-relative patch paths on `lockfileDir` when hashing and +/// applying patches. Pacquet's lockfile always lives at the workspace +/// root, so `workspace_dir` is that same base; if pacquet ever gains a +/// separate `lockfileDir`, this base must follow it to stay in parity. /// /// [`BTreeMap`]: std::collections::BTreeMap /// [`PatchGroup::range`]: crate::PatchGroup::range diff --git a/pnpm11/deps/status/src/checkDepsStatus.ts b/pnpm11/deps/status/src/checkDepsStatus.ts index 0f73a83ae6..96f04ec96b 100644 --- a/pnpm11/deps/status/src/checkDepsStatus.ts +++ b/pnpm11/deps/status/src/checkDepsStatus.ts @@ -23,6 +23,7 @@ import { calcPatchHashes, createOverridesMapFromParsed, getOutdatedLockfileSetting, + resolvePatchedDependencies, } from '@pnpm/lockfile.settings-checker' import { getWorkspacePackagesByDirectory, @@ -660,11 +661,12 @@ async function assertWantedLockfileUpToDate ( wantedLockfileDir, } = opts + const resolvedPatchedDeps = resolvePatchedDependencies(config.patchedDependencies, wantedLockfileDir) const [ patchedDependencies, pnpmfileChecksum, ] = await Promise.all([ - calcPatchHashes(config.patchedDependencies ?? {}), + calcPatchHashes(resolvedPatchedDeps ?? {}), config.hooks?.calculatePnpmfileChecksum?.(), ]) diff --git a/pnpm11/installing/deps-installer/src/install/index.ts b/pnpm11/installing/deps-installer/src/install/index.ts index cc19ff13cb..1517908887 100644 --- a/pnpm11/installing/deps-installer/src/install/index.ts +++ b/pnpm11/installing/deps-installer/src/install/index.ts @@ -62,6 +62,7 @@ import { calcPatchHashes, createOverridesMapFromParsed, getOutdatedLockfileSetting, + resolvePatchedDependencies, } from '@pnpm/lockfile.settings-checker' import { PACKAGE_MAP_FILENAME, writePackageMap, writePnpFile } from '@pnpm/lockfile.to-pnp' import { allProjectsAreUpToDate, satisfiesPackageManifest } from '@pnpm/lockfile.verification' @@ -638,21 +639,18 @@ export async function mutateModules ( } const packageExtensionsChecksum = hashObjectNullableWithPrefix(opts.packageExtensions) const pnpmfileChecksum = await opts.hooks.calculatePnpmfileChecksum?.() + const resolvedPatchedDeps = resolvePatchedDependencies(opts.patchedDependencies, opts.lockfileDir) const patchedDependencies = opts.ignorePackageManifest ? ctx.wantedLockfile.patchedDependencies - : (opts.patchedDependencies ? await calcPatchHashes(opts.patchedDependencies) : {}) - const patchGroupInput = opts.patchedDependencies + : (resolvedPatchedDeps ? await calcPatchHashes(resolvedPatchedDeps) : {}) + const patchGroupInput = resolvedPatchedDeps ? Object.fromEntries( Object.entries(patchedDependencies ?? {}).map(([key, hash]) => { - let patchFilePath = opts.patchedDependencies![key] - ? path.resolve(opts.lockfileDir, opts.patchedDependencies![key]) - : undefined + let patchFilePath: string | undefined = resolvedPatchedDeps[key] if (!patchFilePath) { const lastAt = key.lastIndexOf('@') const pkgName = lastAt > 0 ? key.slice(0, lastAt) : key - if (opts.patchedDependencies![pkgName]) { - patchFilePath = path.resolve(opts.lockfileDir, opts.patchedDependencies![pkgName]) - } + patchFilePath = resolvedPatchedDeps[pkgName] } return [key, { hash, patchFilePath }] }) diff --git a/pnpm11/installing/deps-installer/test/install/patch.ts b/pnpm11/installing/deps-installer/test/install/patch.ts index 70cca54ac8..e324f79607 100644 --- a/pnpm11/installing/deps-installer/test/install/patch.ts +++ b/pnpm11/installing/deps-installer/test/install/patch.ts @@ -2,15 +2,17 @@ import fs from 'node:fs' import path from 'node:path' import { afterAll, expect, jest, test } from '@jest/globals' -import { ENGINE_NAME } from '@pnpm/constants' +import { ENGINE_NAME, WANTED_LOCKFILE } from '@pnpm/constants' import { createHexHashFromFile } from '@pnpm/crypto.hash' import { install } from '@pnpm/installing.deps-installer' +import type { LockfileFile } from '@pnpm/lockfile.types' import { prepareEmpty } from '@pnpm/prepare' import type { PackageFilesIndex } from '@pnpm/store.cafs' import { StoreIndex, storeIndexKey } from '@pnpm/store.index' import { fixtures } from '@pnpm/test-fixtures' import { getIntegrity } from '@pnpm/testing.registry-mock' import { rimrafSync } from '@zkochan/rimraf' +import { readYamlFileSync } from 'read-yaml-file' import { testDefaults } from '../utils/index.js' @@ -570,3 +572,39 @@ test('patch package should fail when the name-only range patch fails to apply', expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).not.toContain('// patched') }) + +test('patch with relative paths resolved against lockfileDir', async () => { + prepareEmpty() + // The lockfile and the patches dir live in the parent of the project dir + // (and of the cwd), so the still-relative patchedDependencies paths can + // only be read when resolved against lockfileDir. + const lockfileDir = path.resolve('..') + const patchesDir = path.join(lockfileDir, 'patches') + fs.mkdirSync(patchesDir, { recursive: true }) + fs.copyFileSync( + path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch'), + path.join(patchesDir, 'is-positive@1.0.0.patch') + ) + + const patchedDependencies = { + 'is-positive@1.0.0': 'patches/is-positive@1.0.0.patch', + } + const opts = testDefaults({ + fastUnpack: false, + patchedDependencies, + lockfileDir, + }, {}, {}, { packageImportMethod: 'hardlink' }) + await install({ + dependencies: { + 'is-positive': '1.0.0', + }, + }, opts) + + expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).toContain('// patched') + + const patchFileHash = await createHexHashFromFile(path.join(patchesDir, 'is-positive@1.0.0.patch')) + const lockfile = readYamlFileSync(path.join(lockfileDir, WANTED_LOCKFILE)) + expect(lockfile.patchedDependencies).toStrictEqual({ + 'is-positive@1.0.0': patchFileHash, + }) +}) diff --git a/pnpm11/lockfile/settings-checker/src/index.ts b/pnpm11/lockfile/settings-checker/src/index.ts index 4bc88b9270..0091087320 100644 --- a/pnpm11/lockfile/settings-checker/src/index.ts +++ b/pnpm11/lockfile/settings-checker/src/index.ts @@ -1,3 +1,4 @@ export { calcPatchHashes } from './calcPatchHashes.js' export { createOverridesMapFromParsed } from './createOverridesMapFromParsed.js' export { type ChangedField, getOutdatedLockfileSetting } from './getOutdatedLockfileSetting.js' +export { resolvePatchedDependencies } from './resolvePatchedDependencies.js' diff --git a/pnpm11/lockfile/settings-checker/src/resolvePatchedDependencies.ts b/pnpm11/lockfile/settings-checker/src/resolvePatchedDependencies.ts new file mode 100644 index 0000000000..f8283833b6 --- /dev/null +++ b/pnpm11/lockfile/settings-checker/src/resolvePatchedDependencies.ts @@ -0,0 +1,11 @@ +import path from 'node:path' + +import { map as mapValues } from 'ramda' + +export function resolvePatchedDependencies ( + patchedDependencies: Record | undefined, + baseDir: string +): Record | undefined { + if (!patchedDependencies) return undefined + return mapValues((patchFile) => path.resolve(baseDir, patchFile), patchedDependencies) +}