install: keep trustedDependencies and patchedDependencies order in bun.lock

Merged
RO
3mo ago
+138-3

Problem

  • The first bun add / bun remove / bun update <pkg> run by a 1.4 build on a bun.lock written by 1.3.x rewrites the trustedDependencies array (and the patchedDependencies object) in a different order. Nothing in either section changed, so this is an unrelated hunk in every dependency PR, and a 1.3.x build doing the next add/remove reorders it back.
  • Both sections are written in the iteration order of the map they are collected in (src/install/lockfile/bun.lock.rs, found_trusted_dependencies / found_patched_dependencies, written at the "trustedDependencies" / "patchedDependencies" loops in save_from_binary_inner). That order has never been sorted, so it is effectively part of the file format.
  • The Zig writer collected them in a std.AutoHashMapUnmanaged(u64, ...): slot index = wyhash of the key's 8 bytes, masked by a capacity derived from the trustedDependencies / patchedDependencies count. bun_collections::HashMap is a port of that layout (src/collections/zig_hash_map.rs), but its AutoHashContext hashes integer keys through OneShotHasher::write_u64 (a single mum multiply, src/wyhash/lib.rs), so the same keys land in different slots and the sections come out permuted.
  • Reproduced with 1.3.14 and a 1.4 canary on the same package.json (7 trusted names, 5 patched): 1.3.14 writes bcrypt, esbuild, sharp, @prisma/engines, @prisma/client, core-js, prisma (the order reported), the 1.4 build writes bcrypt, esbuild, prisma, @prisma/engines, core-js, @prisma/client, sharp, and patchedDependencies swaps two entries.

Fix

  • Adds a HashContext for those two maps that hashes the key as wyhash over its little-endian bytes (bun_wyhash::hash is the same wyhash std.hash.Wyhash implements), so with the unchanged map layout and the unchanged reserve counts the slots, and therefore the written order, are the ones every earlier version produced. No other caller of the map is affected.
  • Restoring the old order rather than sorting: sorting would still rewrite both sections once in every existing lockfile and would still ping-pong against 1.3.x, so it would need to be tied to a lockfileVersion bump to be churn-free. This change makes 1.3.x and 1.4 agree byte for byte.
  • Test: test/cli/install/bun-lock.test.ts, "writes trustedDependencies and patchedDependencies in the order earlier versions wrote them". Two shapes, each installed as a workspace and compared against the text bun 1.3.14 writes for that package.json: the reported 7 trusted / 5 patched set, and a 13 trusted / 6 patched set that puts the trusted map in the next capacity and fills the patched map to its growth threshold, so a change to the hash or to how the maps size themselves shows up. Each shape is re-saved after adding a package and checked again. Both fail on current main (the first with the same two hunks as the report) and pass with this change.
  • Also checked: 1.3.14-written lockfile, bun add with this build produces only the added lines, and bun remove with 1.3.14 afterwards restores a byte-identical file; a differential run of 60 random workspace layouts (1 to 24 packages, so map capacities 8 through 32, some trusted names absent from the tree) writes identical sections with 1.3.14 and this build, while the unfixed build differs in 11 of 12; the rest of bun-lock.test.ts passes unchanged.

Background

  • bun.lock is the text lockfile. trustedDependencies lists the packages whose lifecycle scripts may run, patchedDependencies maps name@version to a patch file; the writer only emits the entries that are actually in the resolved tree, which is why they are collected into a map during the tree walk instead of being copied from package.json.
  • bun_collections::HashMap is an open-addressing table with linear probing whose iteration order is its slot order, which is determined by the hash function, the capacity, and insertion order on collisions. A HashContext is the type parameter that supplies the hash function for a given map, so one map can use a different hash than the default without touching the others.

no test proof · iteration 2 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-lock.test.ts