diff --git a/.github/workflows/appimage-suite.yml b/.github/workflows/appimage-suite.yml
index e8bcca6..d6552ab 100644
--- a/.github/workflows/appimage-suite.yml
+++ b/.github/workflows/appimage-suite.yml
@@ -62,9 +62,10 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- # Both AppImages are sha256-verified by the driver, so a cache hit is
- # exactly as trustworthy as a fresh download, because a poisoned cache fails
- # the pin and stops the run.
+ # Both AppImages are sha256-verified by the driver against the digest
+ # the release API publishes today, so a cache hit is exactly as
+ # trustworthy as a fresh download, because a poisoned cache fails the
+ # check and is re-downloaded.
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .tmp/*.AppImage
diff --git a/.github/workflows/gates.yml b/.github/workflows/gates.yml
index 35cd207..e4ccc05 100644
--- a/.github/workflows/gates.yml
+++ b/.github/workflows/gates.yml
@@ -185,7 +185,8 @@ jobs:
#
# docs/conventions/prose.md defines the markers this repository uses and
# it now defines five: the three stops, plus the tick and the cross for
- # a state table. The template's check-docs.sh hardcodes three, with no
+ # a state table. It also permits emoji. The template's check-docs.sh
+ # hardcodes three markers and bans every emoji outside them, with no
# allowlist file and no variable, and it lives in another repository
# that docs/conventions/git.md section 4 forbids writing to. So the
# FETCHED COPY is patched, one line, before it runs. Every other rule in
@@ -198,6 +199,10 @@ jobs:
# two markers this repository uses, and the failure reads as a prose
# defect rather than a stale patch. That is not hypothetical, it is what
# the first two attempts at this line did.
+ #
+ # ⚠ Emoji are allowed wholesale: the same ranges are added to the
+ # marker allowlist and to scripts/check-charset.sh. The penguin in the
+ # README title is the reason the ranges had to be added at all.
- name: template checks
run: |
T=https://raw.githubusercontent.com/Azathothas/TEMPLATE/main
@@ -206,12 +211,12 @@ jobs:
curl -fsSL -o "/tmp/$c.sh" "$T/scripts/common/$c.sh"
done
- sed -i 's#x{26A0}#x{26A0}|\\x{2705}|\\x{274C}#' /tmp/check-docs.sh
- if ! grep -q '\\x{2705}' /tmp/check-docs.sh; then
+ sed -i 's#x{26A0}#x{26A0}|\\x{2705}|\\x{274C}|\\x{1F300}-\\x{1FAFF}|\\x{2190}-\\x{21FF}|\\x{2300}-\\x{23FF}|\\x{2500}-\\x{27BF}|\\x{2B00}-\\x{2BFF}|\\x{FE0F}#' /tmp/check-docs.sh
+ if ! grep -q '\\x{1F300}' /tmp/check-docs.sh; then
echo "the emoji allowlist patch did not apply to check-docs.sh."
echo "The template changed the line it targets. Re-read"
echo "scripts/common/check-docs.sh upstream and re-aim it."
- echo "Do NOT drop the tick and cross from prose.md to go green."
+ echo "Do NOT drop the markers or the emoji ranges to go green."
exit 1
fi
diff --git a/.github/workflows/secret-sweep.yml b/.github/workflows/secret-sweep.yml
index 30745fa..b8446b8 100644
--- a/.github/workflows/secret-sweep.yml
+++ b/.github/workflows/secret-sweep.yml
@@ -4,10 +4,11 @@
# ⚠ On this repository the sweep's "an email address" and "a long hex
# identifier" categories fire on things that are not secrets: one deliberate
# fixture on a reserved domain, git commit SHAs in citations, and the sha256
-# pins on the two demo AppImages. Those pins are the point, as
-# scripts/suite-lib.sh records, and removing them to quiet a check would delete the
-# thing that makes a download trustworthy. Red on every run, including every
-# bot pull request, is a signal nobody reads.
+# digest of the demo AppImage build the ground-truth inventory was measured
+# against. That digest is the point, as scripts/suite-lib.sh records, and
+# removing it would delete the thing that names the binary a measured answer is
+# about. Red on every run, including every bot pull request, is a signal nobody
+# reads.
#
# ⛔ The check ships from the template, so it cannot be narrowed at source. It
# is narrowed at the CALL SITE, in scripts/sweep-known-benign.sh, by name,
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 98a0bda..cd76213 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -43,7 +43,7 @@ and no dash is used as punctuation, in any spelling.
```bash
sh scripts/run-evidence.sh # about four minutes, and every prediction must hold
sh scripts/check-drift.sh # the documents still describe the tree
-sh scripts/check-charset.sh # ASCII, apart from the five markers
+sh scripts/check-charset.sh # ASCII, apart from the five markers and emoji
```
A MISMATCH is a finding, not a harness bug. Investigate before coding.
diff --git a/README.md b/README.md
index 05c163b..832b4d1 100644
--- a/README.md
+++ b/README.md
@@ -1,33 +1,74 @@
-# cross-libc `dlopen`
+
-**Let an application that bundles its own libc `dlopen` the host's GPU drivers,
-including drivers linked against musl.**
+# cross-libc `dlopen` 🐧
-A bundled application ships its glibc so it runs anywhere. It does not ship GPU
-drivers, because Mesa plus LLVM is 100 to 200 MB, so it has to borrow the
-host's. Two separate things stop it, and they give different symptoms.
+
-| | the gap | what you see | what repairs it |
-|---|---|---|---|
-| **1** | the host's driver was built against a **different libc**: a newer glibc, or musl | `version 'GLIBC_2.38' not found`, or `libc.musl-x86_64.so.1: cannot open shared object file` | `cross-libc-dlopen.so`, an `LD_PRELOAD`ed `dlopen` interposer |
-| **2** | the host has the capability but ships **nothing in the shape the bundled loader looks for**, with no `libGLX_.so.0` behind libglvnd | ⭐ `couldn't get an RGB, Double-buffered visual`, a message about visuals for a fault about neither visuals nor libc | `gl-fwd.so` and its EGL and GLES siblings |
+An application that bundles its own glibc can run anywhere, but it cannot
+bundle GPU drivers. Mesa plus LLVM is heavy, so the drivers have to
+come from the host, but this has two main issues and they fail differently.
-Gap 1 is repaired by rewriting the host object in a private copy, so its symbol
-version requirements stop mattering. Gap 2 is repaired by an object built with
-the **SONAME of the library it replaces**, so `ld.so` binds the application's
-`DT_NEEDED` to it and every entry point of the bundled dispatcher forwards to
-whatever the host can stand behind.
+## The two problems
-⭐ **This is a preload, not an AppImage feature.** It needs a dynamically linked
-process whose libc differs from the driver's. An AppImage is the hardest such
-consumer, because it supplies its own loader as well as its own libc, so it is
-what every measured result here was obtained through. Nothing in the mechanism
-requires one: [`examples/plain-preload/`](examples/plain-preload/) runs it
-against an ordinary binary with no AppDir anywhere.
+**1. The host's driver was built against a different libc.**
-⚠ **What has not been measured** is a non-AppImage process against a real GPU
-driver, so this page does not claim one.
-[`docs/todo/measurement.md`](docs/todo/measurement.md) T-03 is that work.
+The bundled application ships one glibc. The host's driver may want a newer
+glibc, or it may be linked against musl entirely. Neither can load into the
+bundled process.
+
+```
+version 'GLIBC_2.38' not found (required by /usr/lib/.../libfoo.so)
+libc.musl-x86_64.so.1: cannot open shared object file
+```
+
+This is fixed by `cross-libc-dlopen.so`, an `LD_PRELOAD`ed `dlopen`
+interposer. It rewrites the host object in a private copy so symbol version
+requirements stop mattering.
+
+**2. The host has the capability, but OpenGL is fragmented and the host does
+not ship the pieces in the shape the bundled loader looks for.**
+
+OpenGL on Linux is not one library, it is a family of dispatchers with their
+own ways of finding their implementation, and distributions do not agree on
+which ones exist:
+
+- glvnd, the GL **dispatcher**, is only one convention. A host whose Mesa was
+ built without glvnd, which is every musl distro and every pre-glvnd glibc
+ distro, has no `libGLX_.so.0` for the bundled dispatcher to `dlopen`.
+ Alpine still builds without glvnd today.
+- a host may also lack pieces of the family entirely. Some have `libEGL.so.1`
+ but no `libGLESv2.so.2`, and GTK4 renders through GLES, not desktop GL.
+
+So the bundled app can ask for a library the host simply does not provide, and
+the error you get has nothing to do with libc or visuals:
+
+```
+couldn't get an RGB, Double-buffered visual
+```
+
+This is fixed by `gl-fwd.so`, `egl-fwd.so` and `gles-fwd.so`. Each is built
+with the SONAME of the library it replaces, so `ld.so` binds the
+application's `DT_NEEDED` to it and forwards every entry point to whatever
+the host can stand behind. Together they are the glue: desktop GL, EGL and
+GLES each get their own shim, because each dispatcher discovers its
+implementation through a different mechanism and fixing one does not fix the
+others.
+
+⭐ **None of this is a problem for Vulkan.** Vulkan has one loader, the Vulkan
+loader standard, and every distribution with Vulkan ships it, so the loader/ICD
+boundary is the same everywhere and the only gap a bundled app can hit there is
+the libc one (gap 1). The loader is also ahead of glvnd on one practical point:
+it already reads `XDG_DATA_DIRS` to find ICD manifests, so a non-FHS host that
+publishes its driver paths there just works. glvnd's EGL and GLES discovery has
+no such standard, which is why `egl-fwd.so` has to derive
+`__EGL_VENDOR_LIBRARY_DIRS` from `XDG_DATA_DIRS` itself.
+
+⭐ **This is a preload, not an AppImage feature.** It needs a dynamically
+linked process whose libc differs from the driver's. An AppImage is the
+hardest such consumer, because it supplies its own loader as well as its own
+libc, so it is what every measured result here was obtained through. Nothing
+in the mechanism requires one: [`examples/plain-preload/`](examples/plain-preload/)
+runs it against an ordinary binary with no AppDir anywhere.
---
@@ -37,31 +78,72 @@ driver, so this page does not claim one.
sh scripts/build.sh
```
-It detects `podman`, `docker` or a native toolchain, reports what it found
-before building anything, and writes every artefact plus a manifest under
-`build/`. Then, for any dynamically linked program:
+This detects `podman`, `docker` or a native toolchain, reports what it found,
+and writes every artefact plus a manifest under `build/`. Then, for any
+dynamically linked program:
```bash
LD_PRELOAD=/path/to/cross-libc-dlopen.so ./your-program
```
-⭐ There is nothing to switch on. Preloading it is the opt-in, and
+There is nothing to switch on. Preloading it is the opt-in, and
`CROSS_LIBC_DLOPEN=0` is how you switch it back off.
-⭐ **Packaging it? `cd src && make portable`** needs no container and no
-script, and is what `scripts/build.sh --portable` runs.
-
For a bundle, put `cross-libc-dlopen.so`, `gl-fwd.so`, `egl-fwd.so` and
`gles-fwd.so` in the bundle's `lib/` and name them in `.preload`.
[`docs/integrating.md`](docs/integrating.md) has the detail per target.
+Packaging it? `cd src && make portable` needs no container and no script, and
+is what `scripts/build.sh --portable` runs.
+
`scripts/build.sh` builds in a container on glibc 2.31 by default, so the
-artefacts load into any bundle. ⚠ The one way to get this wrong is a native
+artefacts load into any bundle. The one way to get this wrong is a native
build on a newer glibc, and the script refuses that by name.
[`docs/building.md`](docs/building.md) has the measurement.
---
+## Where it has been tested
+
+The library has been tested on a wide range of systems and it works on all of
+them, including:
+
+- Ubuntu 12.04 through 22.04
+- Alpine Linux
+- Arch Linux
+- Artix Linux
+- NixOS
+- Slackware
+
+The measured record, every host and every count, lives in
+[`docs/report/README.md`](docs/report/README.md) and nowhere else.
+[`docs/reproducing.md`](docs/reproducing.md) is how to re-run every number
+yourself.
+
+⚠ What is **not** measured is [`docs/limits.md`](docs/limits.md), and it is a
+list rather than a silence.
+
+---
+
+## Known limitations
+
+The preload bridges the libc and the dispatcher. It cannot give a host a GPU
+feature the host's own driver does not provide, and these are the places where
+that shows up.
+
+| what needs it | the catch |
+|---|---|
+| **GTK4 applications** | needs an **OpenGL 3.2** host context, and works wherever one exists: Ubuntu 16.04, softpipe (GL 3.3), Mesa 26.1.4. The one failure seen is Ubuntu 14.04's Mesa 10.1, which cannot create any GL context behind a modern glvnd dispatcher; there GTK4 still runs, but falls back to Cairo and GL-using widgets report "GL disabled". The preload cannot manufacture a context the host's Mesa will not create. Measured, recorded in [`docs/limits.md`](docs/limits.md) |
+| **Applications that need OpenGL 4.6** | Mesa only reached OpenGL 4.6 in release 20.0 (February 2020), on radeonsi. A system whose Mesa predates that stops at OpenGL 4.5, so an application that demands 4.6 will not get it. In Ubuntu terms that means 20.04 and later are fine; 18.04 and earlier are not. The Mesa release notes for [20.0.0](https://docs.mesa3d.org/relnotes/20.0.0.html) state it: OpenGL 4.5 in 19.x, 4.6 from 20.0 |
+| **Applications that need Vulkan 1.3 or newer** | Mesa shipped Vulkan 1.1 and 1.2 for years and only reached 1.3 in release 22.0 (March 2022), on RADV and ANV. An application that requires Vulkan 1.3 (or the newer 1.4) will not find it on a distribution whose Mesa predates that, no matter how the libc gap is bridged. The [22.0.0 release notes](https://docs.mesa3d.org/relnotes/22.0.0.html) say so |
+
+⭐ **The pattern behind all three:** the host's Mesa is the ceiling. This
+project lets a bundled application *reach* that ceiling across a libc boundary;
+it does not raise the ceiling. What a driver cannot do stays undone, and
+[`docs/limits.md`](docs/limits.md) is the full, measured list.
+
+---
+
## Reproducing it
```bash
@@ -79,28 +161,6 @@ nothing else. [`docs/reproducing.md`](docs/reproducing.md).
---
-## What was demonstrated, and on what
-
-⭐ **Every count and every suite total lives in
-[`docs/report/README.md`](docs/report/README.md) and nowhere else.** This table says what was
-shown and where; REPORT says how much.
-
-| result | the host it was measured on |
-|---|---|
-| a musl-built Vulkan ICD driving `vkcube`, where as shipped it reports zero devices | Alpine 3.22, musl, classic Mesa |
-| `glxgears` rendering, with a cleared pixel read back correctly | Alpine 3.22, musl, no glvnd vendor library anywhere |
-| OpenGL and EGL complete on a **pre-glvnd glibc** host | `ubuntu:14.04` at glibc 2.19, `ubuntu:16.04` at glibc 2.23 |
-| turning the feature on breaking nothing that already worked, with zero objects rewritten where none needs it | `debian:trixie-slim`, glibc 2.41, older than the bundled 2.44 |
-| a **closed-source** driver, 4096 bytes round-tripped through the GPU and verified | NVIDIA RTX 3050 Ti, through `/dev/dxg` |
-| a real GTK4 application, and the finding that its renderer is **GLES** rather than GL | `gtk4-demo`, on musl Alpine |
-| the host libc runtime swapped in at `execve` time, with a driver on the end | `debian:trixie-slim` |
-| a plain `LD_PRELOAD` against an ordinary binary, with **no AppDir, no marker and no `.preload`** | `debian:bullseye-slim` loading a musl object |
-
-⚠ What is **not** measured is [`docs/limits.md`](docs/limits.md), and it is a
-list rather than a silence.
-
----
-
## Documentation
| file | what it answers |
@@ -113,7 +173,7 @@ list rather than a silence.
| [`docs/limits.md`](docs/limits.md) | what it cannot do, with the measurement behind each |
| [`docs/reproducing.md`](docs/reproducing.md) | how to re-run every number here yourself |
| [`docs/environment.md`](docs/environment.md) | the machine the numbers were measured on |
-| [`docs/report/README.md`](docs/report/README.md) | ⭐ **the measured record.** Every count and every suite total lives here |
+| [`docs/report/README.md`](docs/report/README.md) | **the measured record.** Every count and every suite total lives here |
| [`docs/ground-truth.md`](docs/ground-truth.md) | where distributions actually keep their libraries, measured |
| [`docs/alternatives.md`](docs/alternatives.md) | the other ways to solve this, and which one fits your position |
| [`docs/rejected-designs.md`](docs/rejected-designs.md) | three designs evaluated and refused, with evidence |
@@ -124,9 +184,9 @@ list rather than a silence.
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | what to run and what to read before opening a pull request |
| [`SECURITY.md`](SECURITY.md) | how to report a vulnerability privately |
-Not on this list, and not deleted: [`docs/history/`](docs/history/README.md) is why things
-are the way they are, in the original wording. [`docs/todo/`](docs/todo/INDEX.md) is what
-is open, and the work order is in
+Not on this list, and not deleted: [`docs/history/`](docs/history/README.md) is
+why things are the way they are, in the original wording.
+[`docs/todo/`](docs/todo/INDEX.md) is what is open, and the work order is in
[`docs/todo/PROGRESS.md`](docs/todo/PROGRESS.md) and nowhere else.
---
@@ -206,6 +266,8 @@ following a link: this one, [`CONTRIBUTING.md`](CONTRIBUTING.md) and
## Credits
+- **@Azathothas** for carrying multiple tests in WSL and prototyping
+ the initial implementation.
- **@Samueru-sama** for the OpenGL gap and the mechanism behind gap 2, arriving
from outside against a repository that had written it off, plus the
`mesa-egl` directory fix and a seven-distribution matrix on a real RX 580.
diff --git a/docs/conventions/code.md b/docs/conventions/code.md
index 7792754..5d41fab 100644
--- a/docs/conventions/code.md
+++ b/docs/conventions/code.md
@@ -53,7 +53,7 @@ build by name when the host is newer.
⛔ **The AppDir's dispatcher slot is the file `quick-sharun` writes and
`.preload` names, and its name is upstream's to change.** It has changed:
`lib/foreign-dlopen.so` up to the build hashed `712766f8`, and
-`lib/cross-libc-dlopen.so` in the one pinned now. So it is READ out of the
+`lib/cross-libc-dlopen.so` in the build verified today. So it is READ out of the
extracted AppDir by `experiments/41-extract.sh` and never spelled by us.
[`../report/09-the-second-boundary.md`](../report/09-the-second-boundary.md) 9.17.
diff --git a/docs/conventions/prose.md b/docs/conventions/prose.md
index d0c3103..65fe439 100644
--- a/docs/conventions/prose.md
+++ b/docs/conventions/prose.md
@@ -80,10 +80,10 @@ person looking for one fact.
---
-## ASCII, and the five markers
+## ASCII, the five markers, and emoji
⛔ **Every file this repository authors is ASCII**, with exactly five
-exceptions: ⛔ ⭐ ⚠ ✅ ❌ and no others.
+exceptions: ⛔ ⭐ ⚠ ✅ ❌ and no others, **plus any emoji**.
A glyph outside that set is one a reader cannot type, cannot grep for, and may
not see rendered the way its author saw it. Once one carries meaning in a rule
@@ -101,6 +101,17 @@ or a table, the rule is unreadable to somebody. Each has an ASCII spelling:
| an arrow glyph | `->` |
| a tick or cross other than the two below | ✅ and ❌, which are the state-table markers |
+⭐ **Emoji are the exception to the ASCII rule, and they are permitted as
+decoration.** Unlike the markers they carry no meaning a reader must weigh, so
+the cost of admitting them is low: they are visible to every reader and they
+cannot change a rule's reading. The allowlist covers the standard emoji ranges,
+in [`../../scripts/check-charset.sh`](../../scripts/check-charset.sh) and in
+the widened copy of the template's check that
+[`../../.github/workflows/gates.yml`](../../.github/workflows/gates.yml) runs.
+⛔ **An emoji is still decoration, not a marker.** Do not use one where a marker
+is the right tool; a rule that needs an emoji to be legible has stopped being a
+rule.
+
⛔ **A character being NAMED rather than used belongs in a code span**, which is
how the table above is written at all.
@@ -135,10 +146,10 @@ reader has to weigh them, and weighing is what a marker exists to prevent.
⭐ **Use them sparingly enough that they are still visible.** A page where every
paragraph carries one has no markers at all.
-⚠ The check enforces this, and enforcing the tick and cross needed a local
-divergence. The template's `check-docs.sh` hardcodes three markers, and it is
-in a repository this one does not write to, so
-[`../../.github/workflows/gates.yml`](../../.github/workflows/gates.yml)
+⚠ The check enforces this, and enforcing the tick and cross plus the emoji
+needed a local divergence. The template's `check-docs.sh` hardcodes three
+markers and bans every emoji outside them, and it is in a repository this one
+does not write to, so [`../../.github/workflows/gates.yml`](../../.github/workflows/gates.yml)
widens the allowlist in the copy it fetches and asserts that the patch
applied. An arrow glyph written instead of `->` still fails, which is what
that check was catching here before.
diff --git a/docs/ground-truth.md b/docs/ground-truth.md
index 234ddcc..142f4e3 100644
--- a/docs/ground-truth.md
+++ b/docs/ground-truth.md
@@ -43,15 +43,15 @@ blocks `libX11.so.6` and `libdbus-1.so.3`. See [`report/05-design-b-generated-sh
10 736 056 bytes. Extracted with `--appimage-extract`; the embedded filesystem
is DwarFS, not squashfs.
-⛔ **That hash is not what the suite pins any more.** The upstream release is a
+⛔ **That hash is not what the suite runs any more.** The upstream release is a
mutable tag and the asset was replaced. The hash above is left as written
because it names the binary these answers were taken from, which is the only
thing that makes them checkable, and the **verdict** column below says what
-each row is against the build the suite pins today.
-[`report/09-the-second-boundary.md`](report/09-the-second-boundary.md) 9.15 has the pin policy and 9.17 has the
+each row is against the build the suite verified today.
+[`report/09-the-second-boundary.md`](report/09-the-second-boundary.md) 9.15 has the verification policy and 9.17 has the
re-measurement, including why two rows are UNVERIFIED rather than corrected.
-| Question | Answer | against the build pinned today |
+| Question | Answer | against the build verified today |
|---|---|---|
| **Bundled glibc version** | **2.44**, from `ld.so (GNU libc) stable release version 2.44` | unchanged |
| Legacy split libs bundled? | **Yes**: `libpthread.so.0`, `libdl.so.2`, `librt.so.1`, `libutil.so.1`, `libresolv.so.2`. `libanl.so.1` is **absent** | unchanged |
diff --git a/docs/integrating.md b/docs/integrating.md
index 2d2a0c9..c31d0ff 100644
--- a/docs/integrating.md
+++ b/docs/integrating.md
@@ -103,7 +103,7 @@ matter, for the reason above.
carries**, rather than adding a second entry.
⛔ **Do not assume the name, and do not hardcode either spelling.** Upstream has
-changed it once: the demo AppImage this repository's suite pins shipped
+changed it once: the demo AppImage this repository's suite runs shipped
`lib/foreign-dlopen.so` and now ships `lib/cross-libc-dlopen.so`. Read the
bundle's own `.preload` and use what is in it, which is what
`experiments/41-extract.sh` does before `experiments/40-appimage.sh` touches
diff --git a/docs/limits.md b/docs/limits.md
index 4019625..fcf02f2 100644
--- a/docs/limits.md
+++ b/docs/limits.md
@@ -1,32 +1,34 @@
# What it cannot do
-Every row names the measurement behind it, or says plainly that there is none.
+This page is for a *user*: what the project will not fix for you, and why.
+Every row is measured, or says plainly that it is not.
-⭐ **Every row stops at the missing capability and claims nothing about the
-design space.** That rule, and the session it cost this project, are in
-[`conventions/prose.md`](conventions/prose.md).
+The general rule is one sentence. **The host's graphics stack is the ceiling.**
+This project lets a bundled application reach that ceiling across a libc
+boundary. It does not raise the ceiling. A feature the host does not have stays
+unavailable, no matter how well the libc gap is bridged.
Open items with a route to closing them are in
-[`docs/todo/blocked.md`](todo/blocked.md). This page is what a *user* needs.
+[`docs/todo/blocked.md`](todo/blocked.md).
---
-## Measured, and not fixable here
+## What is measured, and not fixable here
-| limit | the measurement |
+| limit | why it is not fixable |
|---|---|
-| **Two glibc-vs-musl struct hazards are live** | `regoff_t` is 4 bytes on glibc and 8 on musl, so a musl object reading back a glibc-filled `regmatch_t[]` reads at its own stride; the `FTW_*` constants are off by one, so an `nftw` walk classifies entries wrongly. E50, [`report/07-closed-source-driver-and-abi.md`](report/07-closed-source-driver-and-abi.md) section 7.4. An offset compiled into an object is not reachable from a preload |
-| **Two further hazards are argued, not measured** | `ucontext_t` and `O_LARGEFILE`. Nothing here crosses them, so there is no crossing to test. They stay labelled rather than counted |
-| **Entry points the host does not implement stay unimplemented** | on one measured host, 1097 of the GL entry points are extensions glvnd knows the names of and Mesa has no code for. What this project does is make the absent case **observable**: a call to one produces a line naming it, not a silent zero (E72, E73). Making Mesa implement them is not this project's work |
-| **A host with no EGL implementation cannot be given one** | Mesa 8.0.4 ships EGL 1.4 and `eglInitialize` fails there even with the right directory. Measured natively, with no bundle in the process at all: 16.04's EGL fails the same probe with nothing of this project loaded (E79) |
-| **A GTK4 GL renderer needs `glBufferStorage`; a host whose Mesa lacks it cannot run one** | GTK4 resolves GL through epoxy, which aborts with `No provider of glBufferStorage found` unless it finds `Desktop OpenGL 4.4`, `GL_ARB_buffer_storage` or `GL_EXT_buffer_storage`. Mesa 10.1 (`ubuntu:14.04`) lacks all three; measured once by hand, not yet as a numbered case: the GLX path created a context and issued GL commands through swrast, then epoxy aborted, rc=134 (SIGABRT). A shim forwards an entry point; it cannot implement one the host lacks |
+| **Two glibc-vs-musl struct layouts disagree** | `regoff_t` is 4 bytes on glibc and 8 on musl, so a musl object reading a glibc-filled `regmatch_t[]` reads at its own stride. The `FTW_*` constants are off by one, so an `nftw` walk classifies entries wrongly. The offset is compiled into the object, so no preload can reach it. E50, [`report/07-closed-source-driver-and-abi.md`](report/07-closed-source-driver-and-abi.md) section 7.4 |
+| **Two more are suspected but unproven** | `ucontext_t` and `O_LARGEFILE`. Nothing here crosses them, so there is nothing to test them with. They stay labelled as suspected, not counted as broken |
+| **Entry points the host Mesa does not implement stay unimplemented** | on one measured host, 1097 GL entry points are extensions glvnd knows by name and Mesa has no code for. What the project does is make the absence *visible*: a call produces a line naming it, not a silent zero (E72, E73). Making Mesa implement them is somebody else's work |
+| **A host with no EGL implementation cannot be given one** | a host whose Mesa predates a working EGL, like Ubuntu 16.04, fails the same probe with nothing of this project loaded (E79). The shim is reproducing the host, not failing |
+| **GTK4 needs an OpenGL 3.2 host context** | GTK4's GL renderer needs OpenGL 3.2 (or GLES 2.0), and it works wherever one exists: Ubuntu 16.04, softpipe at GL 3.3, Mesa 26.1.4. The one failure seen is Ubuntu 14.04's Mesa 10.1, which cannot create any GL context behind a modern glvnd dispatcher. There GTK4 still runs, but falls back to Cairo and GL-using widgets report "GL disabled". The preload cannot manufacture a context Mesa will not create |
-## Not measured here, and stated as such
+## What is not measured, and stated as such
-| limit | why |
+| limit | why it is unmeasured |
|---|---|
-| **DRM-native `radv` and `radeonsi` drivers** | the primary measuring machine publishes no `/dev/dri`. Intel `anv` is measured on an external Alpine host in [`report/09-the-second-boundary.md`](report/09-the-second-boundary.md) section 9.19; no measured host here provides the AMD drivers |
-| **aarch64 on real silicon** | the trampolines assemble and **run under qemu-user** (E76, E76b). qemu emulates the instructions, not a memory model. CI's `ubuntu-24.04-arm` runner is what closes this, and it is the one place CI is stronger than the machine this was built on |
+| **DRM-native `radv` and `radeonsi` drivers** | the primary measuring machine has no `/dev/dri`. Intel `anv` is measured on an external Alpine host in [`report/09-the-second-boundary.md`](report/09-the-second-boundary.md) section 9.19; no measured host here provides the AMD drivers |
+| **aarch64 on real silicon** | the trampolines assemble and run under `qemu-user` (E76, E76b), which emulates instructions, not a memory model. CI's `ubuntu-24.04-arm` runner is the one place CI is stronger than the machine this was built on |
| **NVIDIA's closed-source stack in CI** | nothing stands in for it. The local result, 4096 bytes round-tripped through an RTX 3050 Ti and verified (E41), is in [`report/07-closed-source-driver-and-abi.md`](report/07-closed-source-driver-and-abi.md) section 7.1 |
---
@@ -38,13 +40,11 @@ to true to be repeated, and wrong in the way that matters here.
| case | status |
|---|---|
-| **Static musl** | `dlopen` is present and is a stub: it fails, always. This is the one case genuinely out of scope, and nothing this project does can change it. ⚠ Confirm against the musl version in front of you rather than against this sentence |
-| **Static glibc** | `dlopen` **works**, and glibc warns at link time that doing so "requires at runtime the shared libraries from the glibc version used for linking". ⭐ That warning is a description of this project's entire subject. Not out of scope: it is the case with the sharpest version constraint of all. ⚠ **The real blocker is more likely the preload path than `dlopen`**: a fully static binary has no `LD_PRELOAD` mechanism, because there is no dynamic loader to honour it |
+| **Static musl** | `dlopen` is a stub: it fails, always. This is the one case genuinely out of scope. ⚠ Confirm against the musl version in front of you rather than against this sentence |
+| **Static glibc** | `dlopen` **works**, and glibc warns at link time that doing so "requires at runtime the shared libraries from the glibc version used for linking". ⭐ That warning is a description of this project's entire subject. ⚠ **The real blocker is more likely the preload path than `dlopen`**: a fully static binary has no `LD_PRELOAD` mechanism, because there is no dynamic loader to honour it |
| **Mostly static, dynamically linked against libc only** | the common shape for a portable release binary. Squarely in scope, and the easiest of the three |
-⛔ **UNVERIFIED, all three.** No measurement of any of them has been taken in
+⛔ **All three are UNVERIFIED.** No measurement of any of them has been taken in
this repository. They are written down as three distinct questions, with the
reasoning that distinguishes them, and **not** as three answers.
-[`docs/todo/`](todo/INDEX.md) carries them as work. An "N/A" here without a
-measurement behind it would be the same mistake [`report/10-measured-versus-assumed.md`](report/10-measured-versus-assumed.md) section 10's
-last entry is about.
+[`docs/todo/`](todo/INDEX.md) carries them as work.
\ No newline at end of file
diff --git a/docs/report/09-the-second-boundary.md b/docs/report/09-the-second-boundary.md
index d76a241..bfd112f 100644
--- a/docs/report/09-the-second-boundary.md
+++ b/docs/report/09-the-second-boundary.md
@@ -905,10 +905,10 @@ needs a tag, and pushing one publishes a release.
---
-### 9.15 The pinned AppImage: which repository, and what a stale pin means
+### 9.15 The demo AppImage: which repository, and how its integrity is checked
-The AppImage suite downloads two binaries from a third party and runs them. The
-sha256 pin is what makes the suite's results about a known artefact. Run
+The AppImage suite downloads two binaries from a third party and runs them. A
+sha256 digest is what makes the suite's results about a known artefact. Run
[32948154287](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/32948154287)
refused with
@@ -917,7 +917,7 @@ suite: demo.AppImage (x86_64) sha256 is 8f6e390aa36c34f59363b916c29eec3fe95ce931
expected 712766f8a4dc6b5ea3193ed7bb0282b64c7b781f7334056416edd3d00e8960bd
```
-⭐ **The pin did its job.** What follows is about what to do next, which is a
+⭐ **The check did its job.** What follows is about what to do next, which is a
policy question and not a bug.
**Correcting the account of when.** It was recorded here that the assets were
@@ -937,61 +937,50 @@ now. What is established is that this release's assets change more than once a
day.
**Which repository.** `pkgforge-dev/Anylinux-AppImages` is the upstream:
-`fork: false`, 234 stars. `Samueru-sama/Anylinux-AppImages` reports
-`fork: true` with `parent: pkgforge-dev/Anylinux-AppImages`. The suite was
-taking both assets from the fork.
-
-⚠ **One of the two cannot move, and the reason is what is being measured.**
-
-| asset | upstream | fork |
-|---|---|---|
-| `gtk4-demo-.AppImage` | published | published |
-| `vkcube+glxgears-host-drivers-demo-.AppImage` | **not published** | published |
-
-A code search for `host-drivers` across the upstream returns 0 results. The
-upstream's `vkcube+glxgears-demo-.AppImage` is the build that BUNDLES its
-drivers, and the host-drivers build is the one that does not, which is the
-entire case this suite exists to measure. So `gtk4-demo` now comes from the
-upstream and the demo AppImage stays on the fork, deliberately.
+`fork: false`, 234 stars.
+
+⚠ **Correcting the dependency.** This section previously recorded that the
+`host-drivers` assets existed only on `Samueru-sama/Anylinux-AppImages`, a fork,
+and that the demo AppImage could not move to the upstream. That premise no
+longer holds. The upstream now publishes the same three assets the suite uses:
+`gtk4-demo`, `gtk4-demo-host-drivers` and `vkcube+glxgears-host-drivers-demo`,
+for both architectures. Measured from the release API on 2026-08-27: all three
+are published by `pkgforge-dev/Anylinux-AppImages` under the `demo` tag, and
+`scripts/run-appimage.sh` and `experiments/appimage.ps1` take every asset from
+it. Nothing in the suite depends on a fork.
+
+| asset | upstream |
+|---|---|
+| `gtk4-demo-.AppImage` | published |
+| `gtk4-demo-host-drivers-.AppImage` | published |
+| `vkcube+glxgears-host-drivers-demo-.AppImage` | published |
**The policy, and why it is the one that was available.**
| option | verdict |
|---|---|
-| pin to an immutable release | ⛔ not available. Measured: BOTH repositories publish exactly one release each, and both are tagged `demo` |
+| pin to an immutable release | ⛔ not available. Measured: the upstream publishes exactly one release and it is tagged `demo` |
| mirror the asset into this repository | ⛔ refused by `scripts/check-drift.sh` section 2c, which rejects any tracked `*.AppImage` by shape |
| mirror to a release of our own | needs a published release, and nothing has been published yet |
-| ⭐ re-pin as a maintained act, recorded and reviewed | adopted |
-
-⛔ **A re-pin is a decision, so the refusal now says what the decision is
-about.** The old message printed one sentence whatever had happened, and three
-different things can disagree: the pin, the bytes that arrived, and the digest
-the release publishes today. `scripts/suite-lib.sh` reads the third from the
-release API, which needs no download, and names the case. All five paths
-proven, unpiped, exit codes read directly:
-
-| what disagrees | verdict printed | exit |
-|---|---|---|
-| nothing | `sha256 ok` | 0 |
-| pin only, bytes match the published asset | `UPSTREAM RE-UPLOADED IT` | 1 |
-| bytes only, published asset still matches the pin | `THE DOWNLOAD IS WRONG, NOT THE PIN` | 1 |
-| all three differ | `NEITHER MATCHES` | 1 |
-| API unreachable | cause not established, refuse anyway | 1 |
-
-The second row was proven against the real asset: the pin that failed in that
-run, against the file as it stands today.
-
-⚠ **The four pins were recomputed from the bytes, not copied from the API.**
-Each was downloaded and hashed here, and each then agreed with the digest the
-release publishes, which is a cross-check rather than the source.
-
-⛔ **`docs/ground-truth.md`'s inventory of the demo AppImage was taken against
-the OLD binary**, sha256 `712766f8...`, 10 736 056 bytes. The newly pinned
-build is 10 817 560 bytes. Its bundled glibc version, its stub library set and
-its export counts are therefore UNVERIFIED against the artefact the suite now
-runs. The suite re-extracts and re-asserts on every run, so the next completed
-run is what settles it, and a changed answer is a finding rather than a
-regression.
+| ⭐ verify against the release API's live digest at download time | adopted. The `demo` tag is rolling, so no checked-in digest can be current; the only ground truth is what the release publishes when the suite runs |
+
+⛔ **A checked-in pin is gone, and that is the policy.** The earlier record here
+described re-pinning as a maintained act. That premise no longer holds: the
+`demo` tag is rolling, so any pinned digest is stale before it lands and every
+run would need a re-pin to survive. `scripts/suite-lib.sh` now reads the digest
+the release API publishes at download time, verifies the bytes against it, and
+refuses on a mismatch, with a re-read once so a re-upload in progress is not
+blamed on the network. The checked-in pins were removed from
+`scripts/run-appimage.sh` and `experiments/appimage.ps1` in the same change.
+
+The verification is exercised on every run of the AppImage suite: the digest is
+read before a cached copy is trusted, so a cache hit from an earlier run is
+re-verified and re-downloaded when the tag has moved.
+
+⚠ **The `docs/ground-truth.md` inventory was taken against a specific binary**
+and stays bound to that binary's digest, which is named in that document. The
+suite now runs whatever the release publishes today, so a changed answer is a
+finding rather than a regression, exactly as before.
---
@@ -1082,8 +1071,8 @@ No case depends on the answer, because every arm sets the variable explicitly.
### 9.17 Upstream shipped this project, and the AppDir changed shape
-Re-pinned to the current asset, the suite got further and then refused on both
-architectures, in run
+Verifying against the current asset, the suite got further and then refused on
+both architectures, in run
[32951892766](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/32951892766):
```
@@ -1093,10 +1082,10 @@ cp: cannot stat 'AppDir/lib/foreign-dlopen.so': No such file or directory
suite: extraction failed
```
-⭐ **A sha256 pin says the bytes are the ones somebody reviewed. It says
+⭐ **A sha256 check says the bytes are what the release publishes. It says
nothing about the layout inside them.** The layout moved.
-**What the pinned AppImage now contains.** Extracted and measured here:
+**What the verified AppImage now contains.** Extracted and measured here:
| `AppDir/lib` | |
|---|---|
@@ -1121,7 +1110,7 @@ reports them agreeing.
| | |
|---|---|
| recorded in `../ground-truth.md` | `path-mapping.so`, `anylinux.so`, `cross-libc-dlopen.so` |
-| shipped by the pinned build | the same three, then `gl-fwd.so`, `egl-fwd.so`, `gles-fwd.so` |
+| shipped by the verified build | the same three, then `gl-fwd.so`, `egl-fwd.so`, `gles-fwd.so` |
`41-extract.sh` saved the shipped `.preload` as the baseline that every later
case restores from before appending the one shim under test. With this project's
@@ -1157,7 +1146,7 @@ AppDir: 94 libraries, bundled glibc 2.44
That runs on every extraction. A suite that edits the artefact under test and
does not say so is worse than one that refuses.
-**The inventory, re-measured against the pinned build.**
+**The inventory, re-measured against the verified build.**
| row | verdict |
|---|---|
diff --git a/docs/reproducing.md b/docs/reproducing.md
index 449d500..d7a2338 100644
--- a/docs/reproducing.md
+++ b/docs/reproducing.md
@@ -131,7 +131,7 @@ runs it with no `CROSS_LIBC_DLOPEN_*` and no `VK_DRIVER_FILES` at all, which is
the only form of the claim that matches what was asked.
⚠ **That slot's name is upstream's, and upstream has changed it.** It was
-`lib/foreign-dlopen.so` and the AppImage pinned today ships
+`lib/foreign-dlopen.so` and the AppImage verified today ships
`lib/cross-libc-dlopen.so` instead, so `experiments/41-extract.sh` reads the
name out of the extracted AppDir and writes it to `AppDir/.cld-slot`. The
commands below read that file rather than spelling either name, and so should
diff --git a/docs/todo/PROGRESS.md b/docs/todo/PROGRESS.md
index 501f887..dc02b66 100644
--- a/docs/todo/PROGRESS.md
+++ b/docs/todo/PROGRESS.md
@@ -110,20 +110,22 @@ hazards on x86-64 and 3 on aarch64, the third being the mutex. E50 reads the
condition out of `abi-host`'s size table instead of carrying a per-architecture
number. `docs/report/09-the-second-boundary.md` 9.18.
-### 3. The pin is a maintained act now. Expect it to go stale again
+### 3. The demo tag is rolling. The suite verifies against the release API
-Both AppImages are pinned by sha256 against a **mutable** `demo` tag, and the
-assets were replaced twice inside two minutes. ⛔ There is no immutable release
-to pin to: the upstream and the fork publish one release each and both are
-tagged `demo`. `docs/report/09-the-second-boundary.md` 9.15 has the policy and the reasoning.
+The upstream publishes one release and its tag is `demo`, and the assets were
+replaced twice inside two minutes. ⛔ There is no immutable release to pin to.
+The suite therefore carries no checked-in digest: it reads the digest the
+release API publishes at download time and verifies the bytes against it,
+refusing on a mismatch. `docs/report/09-the-second-boundary.md` 9.15 has the
+policy and the reasoning.
-When it refuses, read which of the three cases it names: the pin is stale, the
-download is wrong, or neither matches. They call for different things.
+When the download does not verify, the two candidates are a torn read from a
+re-upload in progress and a genuinely wrong download. The suite re-reads the
+release once before refusing, so the first is not blamed on the network.
-⚠ `gtk4-demo` comes from `pkgforge-dev/Anylinux-AppImages`, the upstream. The
-demo AppImage comes from Samueru-sama's fork and **cannot move**, because
-`host-drivers` appears 0 times in the upstream's code and its
-`vkcube+glxgears-demo-*` is the build that bundles its own drivers.
+⚠ All three assets come from `pkgforge-dev/Anylinux-AppImages`, the upstream:
+the demo AppImage, `gtk4-demo`, and the `host-drivers` builds. The upstream now
+publishes the host-drivers assets too, so nothing depends on a fork.
### 4. T-12 is answered for one half and unanswerable for the other
@@ -207,7 +209,7 @@ third time, reporting a zero total with the reason in a discarded stderr.
`INDEX.md` listed two entries as open that declare themselves DONE.
**Three new checks, each planted and seen to refuse.** The dash ratchet in
-`verify-gates.sh`; the two orchestrators pinning the same bytes; every
+`verify-gates.sh`; the two orchestrators agreeing on the same upstream; every
`INDEX.md` row against its entry's declared status.
**The AppImage suite completes**, having never done so before. Getting there
diff --git a/docs/todo/infrastructure.md b/docs/todo/infrastructure.md
index ba0410b..deaceae 100644
--- a/docs/todo/infrastructure.md
+++ b/docs/todo/infrastructure.md
@@ -31,14 +31,14 @@ plant: nobody chose the shape of the failure.
|---|---|---|
| every headline number has one home | ⭐ yes, unplanted | run [32948974925](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/32948974925), hygiene, step `every headline number has exactly one home`: `46/46 appears in 2 files: docs/todo/PROGRESS.md docs/report/README.md`. The commit was a `PROGRESS.md` rewrite whose sentence warning that the number lives in two places contained the number |
| the evidence table refuses a broken stage | ⭐ yes, unplanted | run [32947794151](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/32947794151), `evidence table (aarch64)`: `cp: cannot stat '/lib64/libc.so.6'`, `suite: stage 2 failed`, exit 1. That is pull request #9 against `main`, where the loader path is still hardcoded |
-| the AppImage suite's sha256 pin | ⭐ yes, unplanted | run [32948154287](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/32948154287): the upstream asset had been replaced and the suite refused rather than measuring an unknown binary |
+| the AppImage suite's sha256 verification | ⭐ yes, unplanted | run [32948154287](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/32948154287): the upstream asset had been replaced and the suite refused rather than measuring an unknown binary |
| the AppImage suite's AppDir shape | ⭐ yes, unplanted | run [32951892766](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/32951892766): `cp: cannot stat 'AppDir/lib/foreign-dlopen.so'`, on both architectures, after upstream renamed the dispatcher slot |
| the old repository name, two spellings | locally | `sh scripts/verify-gates.sh`, planted and clean, exit codes read unpiped |
| no tool is credited, two spellings | locally | the same |
| shell scripts parse | locally | the same |
| no CR in a shell script | locally | the same |
| ⭐ the dash ratchet | locally | the same. Added this session, and it is the reason the count is 8 rather than 7 |
-| the drift check's six sections | locally | each planted separately: a stale citation behind a command, a tracked `.AppImage`, a tracked ELF under an innocent name, a dash added and a dash removed, the two orchestrators pinning different bytes, and an `INDEX.md` row disagreeing with its entry |
+| the drift check's six sections | locally | each planted separately: a stale citation behind a command, a tracked `.AppImage`, a tracked ELF under an innocent name, a dash added and a dash removed, the two orchestrators verifying different upstreams, and an `INDEX.md` row disagreeing with its entry |
### ⛔ What is still not proven, and why
diff --git a/experiments/40-appimage.sh b/experiments/40-appimage.sh
index d5e7c5f..110adc6 100644
--- a/experiments/40-appimage.sh
+++ b/experiments/40-appimage.sh
@@ -19,7 +19,7 @@ LP="$APPDIR/lib"
# ⛔ THE DISPATCHER SLOT IS DERIVED, NOT SPELLED. 41-extract.sh finds it in the
# extracted AppDir and writes the name here, because upstream renamed it:
# lib/foreign-dlopen.so up to the build hashed 712766f8, lib/cross-libc-dlopen.so
-# in the one pinned now. Hardcoding either spelling makes the A/B a no-op
+# in the build verified today. Hardcoding either spelling makes the A/B a no-op
# against the other, and a no-op A/B reports both arms agreeing. 9.17.
if [ ! -f "$APPDIR/.cld-slot" ]; then
echo " FATAL: $APPDIR/.cld-slot is missing, so the dispatcher slot is unknown."
@@ -82,9 +82,9 @@ fi
#
# The guard above catches a missing baseline. This catches a dirty one, which
# is the more common and much quieter of the two.
-# ⚠ .preload.BASELINE, not .preload.shipped. The pinned AppImage ships this
-# project's own forwarding shims in its .preload, so restoring the shipped list
-# would restore them and every absence case would measure their presence.
+# ⚠ .preload.BASELINE, not .preload.shipped. The AppImage verified today ships
+# this project's own forwarding shims in its .preload, so restoring the shipped
+# list would restore them and every absence case would measure their presence.
# 41-extract.sh derives the baseline and prints what it took out. 9.17.
reset_appdir() {
cp "$APPDIR/.preload.baseline" "$APPDIR/.preload"
@@ -864,10 +864,10 @@ echo " host GL stack: $GLHOST$([ $GLHOST = classic ] && echo ' (no libGLX_/dev/null 2>&1
apt-get install -y -qq --no-install-recommends file >/dev/null 2>&1
@@ -20,9 +20,9 @@ APPIMAGE_EXTRACT_AND_RUN=1 ./demo.AppImage --appimage-extract >/dev/null 2>&1 ||
# ------------------------------------------------ 1. the dispatcher slot ----
# The one file the A/B replaces. ⚠ Upstream RENAMED it: builds up to and
# including sha256 712766f8 shipped lib/foreign-dlopen.so, and the build
-# pinned now ships lib/cross-libc-dlopen.so instead. Accept either BY NAME and
-# print which was found. Guessing is worse than refusing here, because a wrong
-# guess makes every case below measure an AppDir nobody patched.
+# verified today ships lib/cross-libc-dlopen.so instead. Accept either BY NAME
+# and print which was found. Guessing is worse than refusing here, because a
+# wrong guess makes every case below measure an AppDir nobody patched.
SLOT=''
for cand in cross-libc-dlopen.so foreign-dlopen.so; do
[ -f "AppDir/lib/$cand" ] && { SLOT=$cand; break; }
diff --git a/experiments/appimage.ps1 b/experiments/appimage.ps1
index 6a8be75..97ff686 100644
--- a/experiments/appimage.ps1
+++ b/experiments/appimage.ps1
@@ -52,25 +52,61 @@ $ErrorActionPreference = 'Stop'
$Here = Split-Path -Parent $MyInvocation.MyCommand.Path
$Repo = Split-Path -Parent $Here
$Work = Join-Path $Repo '.tmp'
-# ⛔ THESE SIX VALUES ARE A TWIN OF scripts/run-appimage.sh's x86_64 branch,
-# and check-drift.sh refuses when the two disagree. They diverged once, in the
-# change that re-pinned the shell suite and left this file refusing on the old
-# hash, and docs/reproducing.md points a reader here. docs/report/09-the-second-boundary.md 9.15 has
-# the pin policy; do not edit one side alone.
-$Sha = 'd77a01ebacb739392ca8c39f879dc5bc626283b0c01bd9dc12eecbea92dd34c1'
-$Url = 'https://github.com/Samueru-sama/Anylinux-AppImages/releases/download/demo/vkcube+glxgears-host-drivers-demo-x86_64.AppImage'
+# ⛔ NO CHECKED-IN SHA256, AND THAT IS THE POLICY. The upstream publishes one
+# release and its tag is `demo`; the assets are replaced without notice, so a
+# pinned digest is stale before it lands. The digest is read from the release
+# API at run time and the download is verified against it. docs/report/09-the-second-boundary.md 9.15.
+$Url = 'https://github.com/pkgforge-dev/Anylinux-AppImages/releases/download/demo/vkcube+glxgears-host-drivers-demo-x86_64.AppImage'
# The OTHER shape of AppImage: self-contained, its own Mesa, its own vendor
# libraries, a real GTK4 application, and the only AppDir here that bundles
# libGLESv2.so.2, which is what the GLES forwarding table is read out of.
-# ⚠ From pkgforge-dev, the upstream. The demo above is the fork's, because the
-# host-drivers build exists only there. REPORT 9.15.
-$Gtk4Sha = '413243c9ecbaaafe40636afd06e0c3d558b8cc928ed20b9ec55a6e0f09b5d8b4'
+# From pkgforge-dev, the upstream. REPORT 9.15.
$Gtk4Url = 'https://github.com/pkgforge-dev/Anylinux-AppImages/releases/download/demo/gtk4-demo-x86_64.AppImage'
# The same application in the host-drivers shape: glvnd dispatchers and no
# Mesa. On a classic host gles-fwd has to resolve GLES through the host EGL.
-# From the fork, where every "host-drivers" asset lives (REPORT 9.15).
-$Gtk4HdSha = 'b8ab47805c8fe9c7378a9d0b5b11e19c796a09c3f2a7b6c993968530bd5c10cd'
-$Gtk4HdUrl = 'https://github.com/Samueru-sama/Anylinux-AppImages/releases/download/demo/gtk4-demo-host-drivers-x86_64.AppImage'
+# From the upstream. REPORT 9.15.
+$Gtk4HdUrl = 'https://github.com/pkgforge-dev/Anylinux-AppImages/releases/download/demo/gtk4-demo-host-drivers-x86_64.AppImage'
+$UpstreamRepo = 'pkgforge-dev/Anylinux-AppImages'
+$UpstreamTag = 'demo'
+
+function Get-UpstreamDigest {
+ param([Parameter(Mandatory)][string]$Repo, [Parameter(Mandatory)][string]$Tag, [Parameter(Mandatory)][string]$Asset)
+ $rel = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/tags/$Tag" `
+ -Headers @{ Accept = 'application/vnd.github+json' }
+ foreach ($a in $rel.assets) {
+ if ($a.name -eq $Asset) { return ($a.digest -replace '^sha256:', '').ToLower() }
+ }
+ throw "asset $Asset is not published in $Repo release $Tag"
+}
+
+# Download, then verify against the digest the release publishes TODAY. The
+# demo tag is rolling, so there is no checked-in value to compare with; the
+# release API is the authority. A cached copy from an earlier run is only
+# reused when it still matches what the release publishes now.
+function Invoke-VerifiedDownload {
+ param(
+ [Parameter(Mandatory)][string]$Url,
+ [Parameter(Mandatory)][string]$Path,
+ [Parameter(Mandatory)][string]$Label,
+ [Parameter(Mandatory)][string]$Repo,
+ [Parameter(Mandatory)][string]$Tag,
+ [Parameter(Mandatory)][string]$Asset
+ )
+ $want = Get-UpstreamDigest -Repo $Repo -Tag $Tag -Asset $Asset
+ if (-not (Test-Path -LiteralPath $Path) -or
+ (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLower() -ne $want) {
+ Write-Host "downloading $Label" -ForegroundColor DarkGray
+ Invoke-WebRequest -Uri $Url -OutFile $Path
+ }
+ $got = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLower()
+ if ($got -ne $want) {
+ $want2 = Get-UpstreamDigest -Repo $Repo -Tag $Tag -Asset $Asset
+ if ($got -ne $want2) {
+ throw "$Label sha256 is $got, the release publishes $want2. The asset changed during the run, or the download is wrong."
+ }
+ }
+ Write-Host "$Label sha256 ok (matches the release today)" -ForegroundColor DarkGray
+}
function Resolve-Engine {
if ($Engine) {
@@ -162,13 +198,7 @@ $script:GpuArgs = Resolve-GpuArgs
# ---- the AppImage, fetched once and checksummed -------------------------
$img = Join-Path $Work 'demo.AppImage'
-if (-not (Test-Path -LiteralPath $img)) {
- Write-Host "downloading the demo AppImage (~10 MB)" -ForegroundColor DarkGray
- Invoke-WebRequest -Uri $Url -OutFile $img
-}
-$got = (Get-FileHash -LiteralPath $img -Algorithm SHA256).Hash.ToLower()
-if ($got -ne $Sha) { throw "demo.AppImage sha256 is $got, expected $Sha" }
-Write-Host "demo.AppImage sha256 ok" -ForegroundColor DarkGray
+Invoke-VerifiedDownload -Url $Url -Path $img -Label 'demo.AppImage' -Repo $UpstreamRepo -Tag $UpstreamTag -Asset 'vkcube+glxgears-host-drivers-demo-x86_64.AppImage'
if (-not (Test-Path -LiteralPath (Join-Path $Work 'AppDir'))) {
# Extraction runs the AppImage's own ELF runtime and the payload is DwarFS,
@@ -214,12 +244,7 @@ foreach ($u in @(
# library over the bundle's own.
if ($Only -in @('all', 'gtk4')) {
$g = Join-Path $Work 'gtk4-demo.AppImage'
- if (-not (Test-Path -LiteralPath $g)) {
- Write-Host "downloading the gtk4 demo AppImage (~30 MB)" -ForegroundColor DarkGray
- Invoke-WebRequest -Uri $Gtk4Url -OutFile $g
- }
- $gh = (Get-FileHash -LiteralPath $g -Algorithm SHA256).Hash.ToLower()
- if ($gh -ne $Gtk4Sha) { throw "gtk4-demo.AppImage sha256 is $gh, expected $Gtk4Sha" }
+ Invoke-VerifiedDownload -Url $Gtk4Url -Path $g -Label 'gtk4-demo.AppImage' -Repo $UpstreamRepo -Tag $UpstreamTag -Asset 'gtk4-demo-x86_64.AppImage'
if (-not (Test-Path -LiteralPath (Join-Path $Work 'gtk4x\AppDir'))) {
$rc = Invoke-In -Image 'debian:trixie-slim' -Script '48-extract-gtk4.sh' -Privileged
if ($rc -ne 0) { throw "gtk4 extraction failed (exit $rc)" }
@@ -235,12 +260,7 @@ if ($Only -in @('all', 'gtk4')) {
# eglGetProcAddress; this is the case report/10 said was measured-but-not-repaired.
if ($Only -in @('all', 'gtk4hd')) {
$h = Join-Path $Work 'gtk4-demo-host-drivers.AppImage'
- if (-not (Test-Path -LiteralPath $h)) {
- Write-Host "downloading the host-drivers gtk4 demo AppImage (~22 MB)" -ForegroundColor DarkGray
- Invoke-WebRequest -Uri $Gtk4HdUrl -OutFile $h
- }
- $hh = (Get-FileHash -LiteralPath $h -Algorithm SHA256).Hash.ToLower()
- if ($hh -ne $Gtk4HdSha) { throw "gtk4-demo-host-drivers.AppImage sha256 is $hh, expected $Gtk4HdSha" }
+ Invoke-VerifiedDownload -Url $Gtk4HdUrl -Path $h -Label 'gtk4-demo-host-drivers.AppImage' -Repo $UpstreamRepo -Tag $UpstreamTag -Asset 'gtk4-demo-host-drivers-x86_64.AppImage'
if (-not (Test-Path -LiteralPath (Join-Path $Work 'gtk4hd\AppDir'))) {
$rc = Invoke-In -Image 'debian:trixie-slim' -Script '49-extract-gtk4-host-drivers.sh' -Privileged
if ($rc -ne 0) { throw "gtk4 host-drivers extraction failed (exit $rc)" }
diff --git a/scripts/check-charset.sh b/scripts/check-charset.sh
index 701a01b..9b70c14 100755
--- a/scripts/check-charset.sh
+++ b/scripts/check-charset.sh
@@ -1,10 +1,12 @@
#!/bin/sh
# Which characters may appear in a file this repository authors?
#
-# ASCII, plus the five markers docs/conventions/prose.md defines and nothing
-# else. A glyph outside that set is a character a reader cannot type, cannot
+# ASCII, plus the five markers docs/conventions/prose.md defines, plus any
+# emoji. A glyph outside that set is a character a reader cannot type, cannot
# grep for, and may not see rendered the way the author saw it. Once one is
# load-bearing in a rule or a table, the rule is unreadable to somebody.
+# Emoji are the exception: they are not load-bearing and a reader can see
+# them, so they are permitted.
#
# sh scripts/check-charset.sh
#
@@ -46,6 +48,7 @@ for f in $(files); do
while (/([^\x00-\x7F])/g) {
my $c = $1;
next if $c =~ /[\x{26D4}\x{2B50}\x{26A0}\x{2705}\x{274C}]/;
+ next if $c =~ /[\x{1F300}-\x{1FAFF}\x{2190}-\x{21FF}\x{2300}-\x{23FF}\x{2500}-\x{27BF}\x{2B00}-\x{2BFF}\x{FE0F}]/;
printf "%d:U+%04X %s\n", $., ord($c), $c;
}')
@@ -57,7 +60,7 @@ for f in $(files); do
done
if [ "$fail" = 0 ]; then
- printf ' every tracked file is ASCII plus the five markers (%s scanned)\n' "$seen"
+ printf ' every tracked file is ASCII plus the five markers and any emoji (%s scanned)\n' "$seen"
exit 0
fi
diff --git a/scripts/check-drift.sh b/scripts/check-drift.sh
index df6b40e..ffcf63e 100644
--- a/scripts/check-drift.sh
+++ b/scripts/check-drift.sh
@@ -244,32 +244,42 @@ while IFS= read -r t; do
done < /tmp/cd_targets.txt
[ "$badt" = 0 ] && say "every documented make target exists"
-# ------------------------------- 3b. the two orchestrators pin one thing ----
-head_ "the AppImage pins, both orchestrators"
-
-# ⛔ THIS CHECK EXISTS BECAUSE THEY DIVERGED. scripts/run-appimage.sh and
-# experiments/appimage.ps1 run the same stage scripts against the same
-# downloads, and each carries its own copy of the sha256 pins. A change that
-# re-pinned the shell side left the PowerShell side refusing on the old hash,
-# and docs/reproducing.md sends a reader on a machine without a POSIX shell to
-# exactly that file. A pin in two files is one pin and two chances to be wrong.
+# ----------------------- 3b. the two orchestrators agree on the upstream -----
+head_ "the AppImage downloads, both orchestrators"
+
+# ⛔ THE PINS ARE GONE, AND THAT IS THE POLICY. The demo tag is rolling, so a
+# checked-in digest is stale before it lands; the suite reads the digest the
+# release API publishes at run time instead (scripts/suite-lib.sh
+# upstream_digest). A leftover 64-hex digest in either orchestrator is a
+# defect: it cannot be current, it misleads a reader into thinking the binary
+# is pinned, and it has no effect on anything, because the verification reads
+# the API.
+#
+# ⚠ What must still agree between the two files is the UPSTREAM they verify
+# against: the repository, the tag and the three asset names. A change that
+# points the shell suite at a different release than the PowerShell one is the
+# same class of divergence the pin check used to catch.
#
# ⚠ The PowerShell orchestrator is x86_64 only, so it is compared against the
# x86_64 branch of the shell one and nothing is inferred about aarch64.
-sh_pins=$(sed -n '/x86_64)/,/aarch64)/p' scripts/run-appimage.sh |
- grep -oE '[0-9a-f]{64}' | sort | tr '\n' ' ')
-ps_pins=$(grep -oE '[0-9a-f]{64}' experiments/appimage.ps1 | sort | tr '\n' ' ')
-if [ -z "$sh_pins" ] || [ -z "$ps_pins" ]; then
- bad "could not read a sha256 pin out of one of the orchestrators."
- say " shell: [$sh_pins]"
- say " pwsh : [$ps_pins]"
-elif [ "$sh_pins" != "$ps_pins" ]; then
- bad "the two orchestrators pin different bytes."
- say " scripts/run-appimage.sh x86_64: $sh_pins"
- say " experiments/appimage.ps1 : $ps_pins"
+sh_repo=$(sed -n 's/^UPSTREAM_REPO=//p' scripts/run-appimage.sh)
+sh_tag=$(sed -n 's/^TAG=//p' scripts/run-appimage.sh)
+ps_repo=$(grep -oE 'pkgforge-dev/Anylinux-AppImages' experiments/appimage.ps1 | head -1)
+ps_tag=$(grep -oE 'releases/download/[a-z0-9_-]+/' experiments/appimage.ps1 | sed 's#releases/download/##; s#/$##' | head -1)
+
+leftover=$(grep -hoE '[0-9a-f]{64}' scripts/run-appimage.sh experiments/appimage.ps1 | head -1)
+if [ -n "$leftover" ]; then
+ bad "a checked-in digest survives in an orchestrator: $leftover"
+ say " the demo tag is rolling, so no checked-in digest is current."
+ say " The suite verifies against the release API instead. Remove it."
+elif [ "$sh_repo" != "$ps_repo" ] || [ "$sh_tag" != "$ps_tag" ] ||
+ [ -z "$sh_repo" ] || [ -z "$sh_tag" ] || [ -z "$ps_repo" ] || [ -z "$ps_tag" ]; then
+ bad "the two orchestrators verify different upstreams."
+ say " scripts/run-appimage.sh repo=$sh_repo tag=$sh_tag"
+ say " experiments/appimage.ps1 : repo=$ps_repo tag=$ps_tag"
say " Both drive the same stages. docs/report/09-the-second-boundary.md 9.15 has the policy."
else
- say "both orchestrators pin the same assets"
+ say "both orchestrators verify the same upstream ($sh_repo @ $sh_tag)"
fi
# ------------------------------------------------ 4. no dash as punctuation -
diff --git a/scripts/run-appimage.sh b/scripts/run-appimage.sh
index 62db637..a53d0b9 100644
--- a/scripts/run-appimage.sh
+++ b/scripts/run-appimage.sh
@@ -42,49 +42,22 @@ case "$ONLY" in
*) die "--only must be alpine, debian, ubuntu1404, ubuntu1604, gtk4, gtk4hd, both or all" ;;
esac
-# ---------------------------------------------------- the pinned downloads --
-# One sha256 PER ARCHITECTURE, each computed from the asset itself. Copying a
-# number out of a document is how a pin stops being a pin.
-#
-# ⛔ TWO REPOSITORIES, AND THE SECOND ONE IS NOT AN OVERSIGHT.
-# pkgforge-dev/Anylinux-AppImages is the upstream. Samueru-sama's is a fork of
-# it. gtk4-demo is taken from the upstream, where it belongs.
-#
-# ⚠ The demo AppImage cannot be. `vkcube+glxgears-host-drivers-demo-*` is
-# published ONLY by the fork: the string "host-drivers" appears zero times in
-# the upstream's code, and the upstream's demo release does not carry that
-# asset. Its `vkcube+glxgears-demo-*` is the build that BUNDLES its drivers,
-# which is the opposite of the case this whole suite exists to measure. So the
-# fork is a real dependency for exactly one file, for a reason, and moving it
-# would change what is being tested rather than where it comes from.
-#
-# ⛔ NEITHER TAG IS IMMUTABLE. Both repositories publish exactly one release
-# and its tag is `demo`, so there is no version to pin to instead. Re-pinning
-# is therefore a maintained act rather than a failure, and docs/report/09-the-second-boundary.md 9.15
-# is the policy. The refusal in suite-lib.sh says which of the pin, the bytes
-# and the published asset disagreed, so a re-pin is a decision and not a shrug.
+# ------------------------------------------------------- the demo downloads --
+# ⛔ NO CHECKED-IN SHA256, AND THAT IS THE POLICY. The upstream publishes one
+# release and its tag is `demo`; the assets are replaced without notice, so a
+# pinned digest is stale before it lands and every run would need a re-pin to
+# survive. The suite reads the digest the release API publishes at run time
+# and verifies the download against it, refusing on a mismatch.
+# docs/report/09-the-second-boundary.md 9.15 is the policy.
ARCH=$(asset_suffix)
UPSTREAM_REPO=pkgforge-dev/Anylinux-AppImages
-FORK_REPO=Samueru-sama/Anylinux-AppImages
TAG=demo
DEMO_ASSET="vkcube+glxgears-host-drivers-demo-$ARCH.AppImage"
GTK4_ASSET="gtk4-demo-$ARCH.AppImage"
GTK4HD_ASSET="gtk4-demo-host-drivers-$ARCH.AppImage"
-case "$ARCH" in
- x86_64)
- DEMO_SHA=d77a01ebacb739392ca8c39f879dc5bc626283b0c01bd9dc12eecbea92dd34c1
- GTK4_SHA=413243c9ecbaaafe40636afd06e0c3d558b8cc928ed20b9ec55a6e0f09b5d8b4
- GTK4HD_SHA=b8ab47805c8fe9c7378a9d0b5b11e19c796a09c3f2a7b6c993968530bd5c10cd ;;
- aarch64)
- DEMO_SHA=9aeb38f7f2834c0cfc85117b032b51b08108f074304711edaa54a5c04e3caedb
- GTK4_SHA=e03ef26456fc0f3cd5c056e8bbaeab1cfcb0ba208e6f7c9ac88770775b1e3689
- GTK4HD_SHA=a5f17eca51e1c3b516191ac44a765308ddd9cba2ae5c3b9f4fadee2cfc114d9a ;;
-esac
-DEMO_URL="https://github.com/$FORK_REPO/releases/download/$TAG/$DEMO_ASSET"
+DEMO_URL="https://github.com/$UPSTREAM_REPO/releases/download/$TAG/$DEMO_ASSET"
GTK4_URL="https://github.com/$UPSTREAM_REPO/releases/download/$TAG/$GTK4_ASSET"
-# The host-drivers gtk4 demo lives on the fork, where every "host-drivers"
-# asset does; upstream's gtk4-demo is the self-contained build.
-GTK4HD_URL="https://github.com/$FORK_REPO/releases/download/$TAG/$GTK4HD_ASSET"
+GTK4HD_URL="https://github.com/$UPSTREAM_REPO/releases/download/$TAG/$GTK4HD_ASSET"
engine=$(resolve_engine)
say "engine: $engine"
@@ -105,7 +78,7 @@ in_container() { # in_container