ReviewOS

pantry-pm/pantry

pantry.lock is not platform-independent: a Linux install rewrites a macOS-generated lock

#232
Closed chris opened this 8 days ago ·
8 days ago

A committed pantry.lock cannot survive pantry install on a second platform, which makes it unusable as a checked-in lockfile for any repository that builds on more than one.

Reproduced in stacksjs/stacks, pantry 0.11.55 on ubuntu-latest against a lock generated by 0.11.48 on macOS arm64. CI installs and then asks git whether the file moved: 144 additions, 1,278 deletions.

Added on Linux

Native toolchain records that platform needs and macOS does not:

gnu.org/gcc/libstdcxx   gnu.org/binutils   gnu.org/gmp
gnu.org/mpfr            gnu.org/mpc        gnu.org/gettext
gnu.org/libiconv        gnome.org/libxml2  tukaani.org/xz

This half is at least explicable - the platforms genuinely need different system packages. It still means one committed file cannot describe both, so it needs a representation (platform-conditional sections, or per-platform lock files) rather than a fix.

Removed on Linux, which is the part that looks wrong

About 80 records of the shape

"@stacksjs/<package>@0.74.32": {
  "name": "@stacksjs/<package>",
  "version": "0.74.32",
  "source": "npm",
  ...
}

These are npm workspace packages. They are not platform-specific, they resolve identically on both, and the macOS install records them while the Linux one drops them. A differing native record set is a platform requirement; a differing npm record set reads as non-deterministic set membership.

Line counts by shape among the removals:

248   "<name>": "<spec>:*",        (dependency edges)
 91   "<name>": { ... }            (record openings)
 91   "url": "https:..."
 80   "version": "0.74.32",
 80   "source": "npm",
 77   "integrity": "...=="
 54   "<name>": "^0.74.32",

Why it matters beyond one repository

Any project that commits pantry.lock and runs CI on a different OS than its developers gets a permanently dirty working tree in CI. It cannot be told apart from real drift, so the usual response is to stop checking - which is how a lock silently goes stale. In this case it had: the same file was carrying workspace records a full release behind, and nobody could see it under the platform noise. That is stacksjs/stacks#2533.

What would resolve it

  • pantry install produces byte-identical pantry.lock on macOS and Linux for the same inputs, or
  • the format records platform-conditional sections explicitly, so one committed file describes every platform and an install on any of them is a no-op.

Either way the npm-record asymmetry wants explaining first, since it is the half that has no platform justification.

8 days ago

Closing as covered — and correcting one of my own claims.

The native-record half is #231, which predates this issue, names the root cause precisely (workspace.zig starting from an empty lock, pipeline.zig filtering OS-guarded specs against the current host), and is now fixed and released in 0.11.56.

The npm half I reported here was probably not a platform asymmetry. I wrote that ~80 "source": "npm" workspace records disappear on Linux and that this had "no platform justification". Having since built a reproduction and instrumented the comparison, the likelier explanation is #233: on a workspace with a companion deps file, the generated lock is missing npm entries the committed one has, in a way that has nothing to do with which platform is running. My evidence for the asymmetry was a line-shape count taken from a CI diff, which cannot tell those two apart — a diff between a macOS-written lock and a Linux-generated one shows the same shape either way.

So there is one confirmed cross-platform bug (#231, fixed), one confirmed generated-lock bug (#233, open, present on main and not in 0.11.55), and no evidence for a third.

8 days ago

Reopening: I closed this too early. The carry-forward in #231 put the foreign pins back into the regenerated workspace lock, but the companion merge step then swept them out again, so a cross-platform union lock still lost records on a round trip - 6 of 7 Linux-only records survived a macOS install, and gnu.org/gettext@0.21.1 did not.

Diagnosis, with the closure instrumented:

[dbg] carryForward seeds=26 wanted=10
[dbg] carryForward keep gnu.org/gettext@0.21.1     <- restored correctly
...
[dbg] merge seeds=11 foreign_os_pins=6 gettext=false
[dbg] merge remove gnu.org/gettext@0.21.1          <- swept anyway

By the time mergeCompanionLockfileEntries runs, the lockfile holds only the pins the workspace step carried forward, and a foreign pin is a leaf - nothing in that set declares the guard that made it foreign. git-scm.org is the only package that says linux:gnu.org/gettext^0.21, and it is not in the lockfile there; it is in results, because this host resolved it natively. So the closure came back without gettext and the sweep treated the Linux pin as a stale duplicate of the macOS one.

Fixed in 7eff49a by seeding the closure from results as well as the lockfile.

After, on a 90-record cross-platform union lock:

union records: 90 | after install: 90
missing: []   extra: []
byte-identical
second install: byte-identical

and pruning is unaffected - an injected obsolete record and a stale same-name pin are both still removed, while the resolved pin and both gettext versions are kept:

obsolete pruned: True
stale curl pruned: True
resolved curl kept: True
gettext both kept: True

Will close once the release is out and stacks is consuming it.

8 days ago

Closing. 0.11.60 (03186d7) is the fix that holds on both platforms; 0.11.59 was right on macOS and wrong on Linux in the mirror image, which is worth recording because it is the same mistake seen from the other side.

The question being asked was "is this dependency guarded for an OS I am not". git-scm.org declares linux:gnu.org/gettext^0.21:

  • On macOS the guard does not apply, gettext looks foreign, its Linux pin is spared. ✅
  • On Linux the same guard is the native one. Nothing marks the macOS version as belonging to anybody else, so gnu.org/gettext@1.0.0 is swept as a stale duplicate of 0.21.1. ❌

Linux CI after 0.11.59:

pantry.lock | 6 ------
-    "gnu.org/gettext@1.0.0": {

Six of seven Linux-only records kept, and exactly one macOS-only record dropped — the mirror of what macOS was doing before.

The symmetric question is "is this dependency guarded at all", because an OS guard on a version CONSTRAINT is what makes the resolved version platform-dependent, whichever side of it you are standing on. parseOsGuardedDepSpec replaces parseForeignOsDepSpec at the seeding step, and foreignOsClosure becomes platformDependentClosure, which is what it was computing all along once the question is put that way.

Pruning is untouched, which was the thing to check rather than assert: a domain nothing declares any more is in neither set, so it still drops out.

macOS, on the 90-record union lock:

union: 90 after: 90 | missing: [] | extra: []
byte-identical
obsolete pruned: True
stale curl pruned: True
both gettext kept: True

Linux, via stacksjs/stacks CI with 0.11.60 pinned — the job runs pantry install then git diff --exit-code pantry.lock:

compile: success

Two tests guard it. One walks the catalog for a real guarded pair and asserts the closure reaches it from the package that declares the guard and not from the pin itself. The other builds a native and a foreign guard on the same domain and asserts the new question answers the same for both, where the old one answers differently — the bug stated directly rather than through a platform.

stacksjs/stacks#2533 is closed on the strength of this.

Sign in to comment on this issue.