ci: a three-state contract for verification scripts, and a closure check that runs - #549
Merged
Merged
Conversation
…eck that runs Split out of the single large PR so its CI cost matches its risk: this touches no pkgs/ file at all, so the per-package install tests skip entirely. THE CONTRACT 0 proven / 1 broken / 2 inconclusive / 3 could-not-run-here, written into .agents/tools/README.md and obeyed by eleven scripts. A caller must map 3 to "not run" and must never fold it into a pass. It exists because skip() in selfcontained-check.sh used to exit 0, and 0 is what the caller reads as "S1-S4 pass" -- so a machine without bwrap printed a tick for a check that ran nothing. Applying the contract turned up two more of the same: verify-host-link's check 4 and verify-stack's app-RPATH cell both printed a pass with no patchelf present. BUILD INPUTS, NOT JUST OUTPUTS build-in-subos.sh verified the produced payload and never what the build linked against, so a build configured entirely against /usr passed "no host references". The sealed-bwrap approach was tried first and does not work here (the subos has no POSIX userland, so clean and dirty builds fail alike); configure-log parsing instead, filtered to resolved records. It reproduces both known instances -- the host libzstd in the LLVM tree behind our mesa, and a constructed case -- and found a third: the --deps .pc rewrite patched only prefix=, leaving libdir=/usr/lib in 40 files. DECLARED DEPS vs REAL DT_NEEDED dep-closure-check.sh, run per package from posix-test.sh. D1: a soname provided by an installed package must be declared DIRECTLY, because a transitive dep does not put its libdir in the closure. D2: if the payload uses our interpreter, a soname with no provider is fatal -- our ld.so has a cache path that exists on no machine, so there is no host fallback behind it. Strictness follows the payload's own state (interpreter, or an RPATH into xpkgs), so host-integrated packages are reported and not failed. A check that fires on correct recipes gets switched off. The tooling step matters as much as the script: linux-install-test is a separate job from linux-test, so lua5.4 was absent and the check exited 3 on every run -- "not evaluated", job green. Measured after fixing it: 20 passes, 6 not-evaluated, 0 failures across a 25-package changed set. Two uninstall tolerances, each keyed on its exact diagnostic rather than on the package type alone: a type="config" package registers no xvm version (11 of the 12 in this index call xvm.add zero times), and a bump PR cannot remove the xlings it is running. The latter is new since #543 -- overlaying the recipe into the index made it install as xim:xlings rather than local:xlings, which the running-binary guard matches. Left alone it is red on every future release bump. Also stops posix-test.sh from printing hundreds of "ignored null byte in input" warnings: it read ELF magic with head -c4 inside a command substitution while walking the whole store.
Sunrisepeak
pushed a commit
that referenced
this pull request
Aug 7, 2026
Batch 2 of the index-wide sweep. Ten packages, so the install test stays short.
The guard that enforces the rule follows in its own PR once both batches have
landed -- it cannot pass while either half still has bare names, and a guard
merged red is a guard someone turns off.
Rationale, evidence and method are in part 1.
cpp is worth naming separately. Its uninstall fails for a reason that has nothing
to do with deps:
uninstall failed: xvm removal selection failed for xim:cpp@gnu:
removal version is not registered (target='cpp', version='gnu')
It has no config() and calls xvm.add zero times -- the norm for its type, since
11 of the 12 `type = "config"` recipes here register nothing. The install test
tolerates exactly that diagnostic as of #549, so merge that first. The question
underneath it (should removing a package that registered no version be a no-op,
so the recipe's own uninstall() cleanup actually runs?) is xlings-side and filed
as openxlings/xlings#503.
Verified: each file re-parsed and its deps re-enumerated per platform; stripping
the namespace prefixes from both sides makes every one byte-identical to main.
1116 static tests pass.
Sunrisepeak
pushed a commit
that referenced
this pull request
Aug 8, 2026
Batch 2 of the index-wide sweep. Ten packages, so the install test stays short.
The guard that enforces the rule follows in its own PR once both batches have
landed -- it cannot pass while either half still has bare names, and a guard
merged red is a guard someone turns off.
Rationale, evidence and method are in part 1.
cpp is worth naming separately. Its uninstall fails for a reason that has nothing
to do with deps:
uninstall failed: xvm removal selection failed for xim:cpp@gnu:
removal version is not registered (target='cpp', version='gnu')
It has no config() and calls xvm.add zero times -- the norm for its type, since
11 of the 12 `type = "config"` recipes here register nothing. The install test
tolerates exactly that diagnostic as of #549, so merge that first. The question
underneath it (should removing a package that registered no version be a no-op,
so the recipe's own uninstall() cleanup actually runs?) is xlings-side and filed
as openxlings/xlings#503.
Verified: each file re-parsed and its deps re-enumerated per platform; stripping
the namespace prefixes from both sides makes every one byte-identical to main.
1116 static tests pass.
Sunrisepeak
added a commit
that referenced
this pull request
Aug 8, 2026
* fix(deps): qualify bare dep names, part 2 of 2
Batch 2 of the index-wide sweep. Ten packages, so the install test stays short.
The guard that enforces the rule follows in its own PR once both batches have
landed -- it cannot pass while either half still has bare names, and a guard
merged red is a guard someone turns off.
Rationale, evidence and method are in part 1.
cpp is worth naming separately. Its uninstall fails for a reason that has nothing
to do with deps:
uninstall failed: xvm removal selection failed for xim:cpp@gnu:
removal version is not registered (target='cpp', version='gnu')
It has no config() and calls xvm.add zero times -- the norm for its type, since
11 of the 12 `type = "config"` recipes here register nothing. The install test
tolerates exactly that diagnostic as of #549, so merge that first. The question
underneath it (should removing a package that registered no version be a no-op,
so the recipe's own uninstall() cleanup actually runs?) is xlings-side and filed
as openxlings/xlings#503.
Verified: each file re-parsed and its deps re-enumerated per platform; stripping
the namespace prefixes from both sides makes every one byte-identical to main.
1116 static tests pass.
* fix(shortcut-tool, code): wscript blocks forever in a non-interactive session
windows-test on this branch stopped dead and had to be cancelled. The last thing
in the log was vc6's compat-mode `reg add` printing "The operation completed
successfully." -- then nothing.
The next call is vc6's config() running `shortcut-tool create`, and
shortcut-tool ran `wscript <vbs>`. wscript is the GUI-mode Windows Script Host:
it needs a window station, and in a non-interactive session (CI, a service,
WinRM, a scheduled task) it does not get one and never returns. cscript is the
console host, which is what automation wants. Nothing is printed and there is no
timeout, so from outside it is indistinguishable from a slow download -- the
same reason rust's io.read() cost four hours before anyone could see it.
Both call sites are fixed: shortcut-tool.lua and a second copy in code.lua,
found by sweeping for it rather than by waiting for another job to hang. Neither
had ever been install-tested on Windows -- the per-package test only runs
recipes in a PR's changed set, and vc6 entering it via the namespace sweep is
the only reason this ran at all.
check-no-blocking-input.sh now rejects wscript alongside unguarded io.read: same
class, one layer down, and the remedy is a fixed string worth printing. Verified
both ways -- PASS on the fixed tree, FAIL naming file and line when wscript is
put back.
Also worth recording what the log shows above the hang: `[warn] skipping
shortcut-tool: resource has neither url nor source for windows@0.0.1`. The
package it depends on installs no payload at all, so this is a second question
for vc6 (is the shortcut step meaningful on a runner?) that is not this PR's.
* ci: bound each install/uninstall so a hang names itself
I diagnosed the vc6 stall as wscript and was wrong -- the job that hung ran
293a1a7, with the cscript fix in it, and stopped at exactly the same point. I
have now guessed twice, so this stops guessing and makes the hang observable
instead.
Every install and uninstall runs under a 1200s limit. On Windows that means
Start-Process with output redirected to a file, and on timeout: kill the process
TREE (the blocked thing is usually a child -- cscript, an installer -- so
killing xlings alone leaves the runner occupied), then print the last 40 lines
of what was captured and fail as `<pkg> (install-timeout)`.
The tail is the point. GitHub serves no logs for an in-progress job, so a stall
today shows a log that ends mid-package and nothing else -- "slow" and "stuck"
are indistinguishable, and each attempt costs a full run to learn nothing. With
this, the next run names the package and shows the call that stalled.
The posix legs get the same bound via timeout(1). Neither has hung, but the
class is not platform-specific and a 6-hour park is worse there too.
The cscript change in the previous commit stays: wscript IS the GUI-mode script
host and does block in a non-interactive session, so both call sites were real
defects. It just was not this one.
* fix(ci): the bound I just added does not exist on macOS
macos-install-test failed in 19 seconds with
.github/scripts/posix-test.sh: line 313: timeout: command not found
[FAIL] install failed
once per package. `timeout` is GNU coreutils and is not on a stock macOS
runner; Homebrew installs it as `gtimeout`, which may or may not be present.
My own change, and the fourth POSIX assumption in this repo's history to hold
on Linux and break on macOS.
run_bounded() uses whichever binary exists and, when neither does, does the
same job in shell -- background, poll, TERM, then KILL -- returning 124 on
expiry to match timeout(1). Deliberately not "skip the bound when the tool is
missing": an unbounded run is exactly the failure this was added to prevent, so
"no timeout available" must not quietly become "no timeout needed".
Verified both code paths, forcing the fallback by clearing TIMEOUT_BIN:
expiry 124, success 0, failure 1, on each.
---------
Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Split out of one large PR so CI cost matches risk. Touches no
pkgs/file, so the per-package install tests skip entirely.The three-state contract
0 proven / 1 broken / 2 inconclusive / 3 could-not-run-here, in.agents/tools/README.md, obeyed by 11 scripts. Callers must map 3 to "not run", never to a pass.It exists because
skip()inselfcontained-check.shused toexit 0— so a machine without bwrap printed✓ S1-S4 passfor a check that ran nothing. Applying it found two more:verify-host-link's check 4 andverify-stack's app-RPATH cell both printed a pass with no patchelf present.Build inputs, not just outputs
build-in-subos.shverified the produced payload and never what the build linked against. Sealed bwrap was tried first and does not work here (the subos has no POSIX userland — clean and dirty builds fail alike), so it parses configure logs, filtered to resolved records. Reproduces both known instances and found a third: the--deps.pcrewrite patched onlyprefix=, leavinglibdir=/usr/libin 40 files.Declared deps vs real DT_NEEDED
dep-closure-check.sh, per package. D1 direct declaration required — a transitive dep is not in the closure. D2 if the payload uses our interpreter, a soname with no provider is fatal, because ourld.so's cache path exists on no machine and there is no host fallback.The tooling step matters as much as the script:
linux-install-testis a separate job fromlinux-test, solua5.4was absent and the check exited 3 every run — "not evaluated", job green. After fixing: 20 passes, 6 not-evaluated, 0 failures over a 25-package set.Two uninstall tolerances
Each keyed on its exact diagnostic, not on the package type alone:
type = "config"package registers no xvm version (11 of 12 callxvm.addzero times)Plus:
posix-test.shno longer floods the log with hundreds ofignored null byte in inputwarnings.🤖 Generated with Claude Code