Skip to content

pacquet: pnpm-lock.yaml map ordering is non-canonical (HashMap iteration order) #12117

Description

@zkochan

Summary

pacquet's pnpm-lock.yaml map ordering is non-canonical: the importers, packages, snapshots, and per-importer dependencies/optionalDependencies/devDependencies maps are serialized in std::HashMap iteration order. Because std::HashMap uses a per-instance random seed, two installs that produce the same resolution can emit the maps in different orders, so the on-disk lockfile is not byte-stable.

This diverges from pnpm, whose pnpm-lock.yaml is deterministically ordered.

Root cause

  • The lockfile maps are std::collections::HashMap (pacquet/crates/lockfile/src/lib.rs: importers, packages, snapshots; project_snapshot.rs: ResolvedDependencyMap = HashMap<PkgName, _>).
  • pacquet/crates/lockfile/src/serialize_yaml.rs serializes the struct as-is with serde_saphyr and does not sort keys.

So the emitted order reflects per-HashMap random seeding, not a canonical sort.

Impact

  • Spurious lockfile churn: an unrelated re-install can reorder pnpm-lock.yaml, producing a large, meaningless git diff.
  • The pacquet test suite already works around this by parsing lockfiles for comparison rather than byte-comparing (e.g. pacquet/crates/cli/tests/inject_workspace_packages.rs).
  • It became user-visible while implementing lockfile-resolution reuse (perf(pacquet): reuse lockfile resolutions on re-resolution #12113): a reuse-built tree and a fresh-resolved tree are content-identical but byte-different purely due to map order, so the equivalence test there compares parsed Lockfile structs instead of bytes.

Suggested fix

Make lockfile serialization canonical, matching pnpm's exact per-section ordering (not merely an arbitrary sort):

  1. Emit the lockfile maps sorted — either switch the lockfile maps to BTreeMap where the key types are Ord, or sort keys at serialization time in serialize_yaml.
  2. Verify byte-parity against real pnpm pnpm-lock.yaml output for representative fixtures (pnpm has section-specific ordering rules, so confirm rather than assume pure lexicographic).
  3. Regenerate the affected lockfile snapshot/insta tests deliberately and review the diffs.
  4. Add a byte-stability test: install twice (and/or reuse vs. fresh) and assert the lockfile bytes are identical.

References


Written by an agent (Claude Code, claude-opus-4-8).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions