|
| 1 | +--- |
| 2 | +name: check-glibc-floor |
| 3 | +description: > |
| 4 | + Verify the Linux glibc floor is consistent everywhere it is stated: the |
| 5 | + release workflow's runner image is the source of truth, and scripts/install.sh, |
| 6 | + docs/, and the release download labels must all agree with it. Also confirms |
| 7 | + the statically linked musl CLI is still built and published. |
| 8 | +--- |
| 9 | + |
| 10 | +# check-glibc-floor |
| 11 | + |
| 12 | +The prebuilt `*-linux-gnu` CLI and the desktop bundles are linked against the |
| 13 | +glibc of the runner image that builds them. glibc versions its symbols, and the |
| 14 | +compatibility runs one way only: a newer glibc runs older binaries, an older one |
| 15 | +cannot run newer binaries. So that image's version is a hard floor -- below it |
| 16 | +the binary exits with ``libc.so.6: version `GLIBC_2.34' not found`` before |
| 17 | +running any code. |
| 18 | + |
| 19 | +That floor is a single fact restated across six files: `release.yml` itself, |
| 20 | +`scripts/install.sh`, `scripts/release-downloads.mjs`, `docs/getting-started.md`, |
| 21 | +`docs/troubleshooting.md`, and `CHANGES.md`. Several of them say it more than |
| 22 | +once, so check every occurrence rather than counting them. This skill verifies |
| 23 | +they all still say the same number, and that the musl escape hatch is still |
| 24 | +shipped. |
| 25 | + |
| 26 | +**The source of truth is the Linux runner image in |
| 27 | +`.github/workflows/release.yml`.** Nothing else. When they disagree, the image |
| 28 | +is right and the other places are stale. |
| 29 | + |
| 30 | +## Runner image to glibc |
| 31 | + |
| 32 | +| Image | glibc | |
| 33 | +|---|---| |
| 34 | +| `ubuntu-20.04` | 2.31 (image retired; here for reading older workflows) | |
| 35 | +| `ubuntu-22.04`, `ubuntu-22.04-arm` | 2.35 | |
| 36 | +| `ubuntu-24.04`, `ubuntu-24.04-arm` | 2.39 | |
| 37 | +| `ubuntu-latest` | whatever it currently aliases (2.39 at the time of writing) | |
| 38 | + |
| 39 | +If the image is not in this table, look up its glibc before continuing rather |
| 40 | +than guessing, and add the row. |
| 41 | + |
| 42 | +## Steps |
| 43 | + |
| 44 | +### 1. Read the floor from the release workflow |
| 45 | + |
| 46 | +```bash |
| 47 | +grep -n "os: ubuntu" .github/workflows/release.yml |
| 48 | +``` |
| 49 | + |
| 50 | +Map every Linux leg through the table above. Expect one image across all Linux |
| 51 | +legs; if the x64 and arm64 legs differ, the floor is the HIGHER of the two and |
| 52 | +that divergence is itself a finding worth reporting. |
| 53 | + |
| 54 | +`ubuntu-latest` on a Linux leg is a finding on its own: it is a moving alias, so |
| 55 | +it silently raises the floor whenever the platform re-points it. The Linux legs |
| 56 | +must pin an explicit image. |
| 57 | + |
| 58 | +Call the result FLOOR (for example `2.35`). |
| 59 | + |
| 60 | +### 2. Verify scripts/install.sh |
| 61 | + |
| 62 | +```bash |
| 63 | +grep -n "MIN_GLIBC_MAJOR\|MIN_GLIBC_MINOR" scripts/install.sh |
| 64 | +``` |
| 65 | + |
| 66 | +- `MIN_GLIBC_MAJOR` and `MIN_GLIBC_MINOR` must equal FLOOR. |
| 67 | + |
| 68 | +Then confirm each of these is still present and wired up; every one of them is |
| 69 | +load-bearing, and the script silently installs an unrunnable binary if any is |
| 70 | +dropped: |
| 71 | + |
| 72 | +- `glibc_version` detects the host version, trying `getconf GNU_LIBC_VERSION` |
| 73 | + first and `ldd --version` second, and yields nothing on a musl host. |
| 74 | +- It rejects any value that is not a dotted number, because the comparison |
| 75 | + below it is arithmetic. |
| 76 | +- `linux_libc` compares against `MIN_GLIBC_*` and returns `musl` when the host |
| 77 | + is below the floor, when there is no glibc at all, and when the version could |
| 78 | + not be read. |
| 79 | +- `SKILLKEEPER_LIBC` is validated at the TOP LEVEL of the script, not inside |
| 80 | + `linux_libc`. `linux_libc` runs in a command substitution, where `err` would |
| 81 | + exit only the subshell and leave the caller building a truncated target |
| 82 | + triple. |
| 83 | +- The Linux branch composes the triple from the detected libc, rather than |
| 84 | + hardcoding `-gnu`. |
| 85 | +- A missing musl asset falls back to the gnu archive with a warning (older |
| 86 | + releases predate the musl archives), and that fallback is skipped when |
| 87 | + `SKILLKEEPER_LIBC` named a flavour explicitly. |
| 88 | +- The final smoke run points at `SKILLKEEPER_LIBC=musl` when the installed gnu |
| 89 | + binary fails to start. |
| 90 | + |
| 91 | +Exercise the detection rather than only reading it. This stubs `getconf` and |
| 92 | +`ldd` on PATH, sources the two real functions out of the script, and reads the |
| 93 | +real `MIN_GLIBC_*`, so it tests the shipped threshold rather than a copy of it. |
| 94 | +It runs anywhere, the developer's macOS included: |
| 95 | + |
| 96 | +```bash |
| 97 | +d=$(mktemp -d) |
| 98 | +# Anchored on the function syntax, not on comment text, and then checked: a |
| 99 | +# range that failed to find its end would run to EOF and capture the whole |
| 100 | +# install flow, which the `.` below would EXECUTE -- downloading and writing to |
| 101 | +# $HOME, once per row. The two guards make that impossible rather than unlikely. |
| 102 | +sed -n '/^glibc_version() {/,/^}/p;/^linux_libc() {/,/^}/p' scripts/install.sh > "$d/funcs.sh" |
| 103 | +grep -qE 'download |INSTALL_DIR|asset_url|Done\.' "$d/funcs.sh" && |
| 104 | + { echo "ABORT: extraction captured installer flow, do not source it"; exit 1; } |
| 105 | +[ "$(grep -c '^}' "$d/funcs.sh")" = 2 ] || |
| 106 | + { echo "ABORT: expected exactly 2 functions"; exit 1; } |
| 107 | + |
| 108 | +# Heredocs, not printf: a printf format string containing %s consumes it |
| 109 | +# itself and writes a stub that ignores its argument, which silently turns |
| 110 | +# every row below into "no glibc" and the whole table into musl. |
| 111 | +cat > "$d/getconf" <<'EOF' |
| 112 | +#!/bin/sh |
| 113 | +[ "${1:-}" = GNU_LIBC_VERSION ] || exit 1 |
| 114 | +[ -n "${SK_GLIBC:-}" ] || exit 1 |
| 115 | +printf 'glibc %s\n' "$SK_GLIBC" |
| 116 | +EOF |
| 117 | +cat > "$d/ldd" <<'EOF' |
| 118 | +#!/bin/sh |
| 119 | +echo "musl libc (x86_64)" |
| 120 | +EOF |
| 121 | +chmod +x "$d/getconf" "$d/ldd" |
| 122 | + |
| 123 | +for v in 2.28 2.31 2.34 2.35 2.39 ""; do |
| 124 | + out=$(SK_GLIBC="$v" PATH="$d:$PATH" SK_F="$d/funcs.sh" sh -c ' |
| 125 | + eval "$(grep "^MIN_GLIBC_" scripts/install.sh)" |
| 126 | + LIBC=auto |
| 127 | + err() { echo "error: $1" >&2; exit 1; } |
| 128 | + . "$SK_F" |
| 129 | + linux_libc') |
| 130 | + printf 'glibc %-6s -> %s\n' "${v:-none}" "$out" |
| 131 | +done |
| 132 | +rm -rf "$d" |
| 133 | +``` |
| 134 | + |
| 135 | +Expected, for a floor of 2.35. The boundary is exercised from both sides, and |
| 136 | +`none` stands for a host with no glibc at all, as on Alpine: |
| 137 | + |
| 138 | +``` |
| 139 | +glibc 2.28 -> musl |
| 140 | +glibc 2.31 -> musl |
| 141 | +glibc 2.34 -> musl |
| 142 | +glibc 2.35 -> gnu |
| 143 | +glibc 2.39 -> gnu |
| 144 | +glibc none -> musl |
| 145 | +``` |
| 146 | + |
| 147 | +Any `ABORT` line, or a table differing from this one, is a FAIL: either the |
| 148 | +detection changed or the extraction no longer matches the functions. Do not |
| 149 | +adjust the test to make it pass. Also run `sh -n scripts/install.sh`. |
| 150 | + |
| 151 | +### 3. Verify the musl builds are still published |
| 152 | + |
| 153 | +```bash |
| 154 | +grep -n "musl" .github/workflows/release.yml |
| 155 | +``` |
| 156 | + |
| 157 | +- Both Linux legs carry a `musl_target` (`x86_64-unknown-linux-musl` and |
| 158 | + `aarch64-unknown-linux-musl`); every non-Linux leg carries an empty one. |
| 159 | +- The toolchain step installs the musl target alongside the leg's own. |
| 160 | +- A `Build the CLI (static, musl)` step and its `tar.gz` archive step both run |
| 161 | + when `musl_target` is non-empty, and the archive lands in `dist-cli/`, which |
| 162 | + the upload step collects. |
| 163 | + |
| 164 | +The musl archive is published ALONGSIDE the gnu one, never instead of it: |
| 165 | +`SKILLKEEPER_VERSION` can pin an older release, and existing installs resolve |
| 166 | +the asset name they already know. |
| 167 | + |
| 168 | +### 4. Verify the documentation states FLOOR |
| 169 | + |
| 170 | +```bash |
| 171 | +grep -rn "glibc" docs/ README.md CHANGES.md |
| 172 | +``` |
| 173 | + |
| 174 | +`CHANGES.md` is in scope deliberately: it states the floor in the entry that |
| 175 | +introduced the musl builds, and a runner-image bump would otherwise leave the |
| 176 | +changelog quietly asserting the old number while this gate reported CONSISTENT. |
| 177 | + |
| 178 | +Every stated version must equal FLOOR. At the time of writing that is: |
| 179 | + |
| 180 | +- `docs/getting-started.md` -- the System requirements table (CLI and desktop |
| 181 | + rows) and the install-script paragraph. |
| 182 | +- `docs/troubleshooting.md` -- the `GLIBC_2.34' not found` section. |
| 183 | +- `CHANGES.md` -- the musl entry under the current version. |
| 184 | + |
| 185 | +Distribution versions named as remedies are part of this check, not decoration: |
| 186 | +verify each one actually clears FLOOR before repeating it. RHEL 9, for |
| 187 | +instance, ships glibc 2.34 and does NOT clear a floor of 2.35. |
| 188 | + |
| 189 | +The troubleshooting section must still say all of: the download is not corrupt, |
| 190 | +the musl build is the fix, `SKILLKEEPER_LIBC=musl` is the command, building from |
| 191 | +source also works, and upgrading glibc in place is not the answer (the |
| 192 | +distribution release pins it). Removing the last point invites someone to break |
| 193 | +a machine. |
| 194 | + |
| 195 | +### 5. Verify the release download labels |
| 196 | + |
| 197 | +```bash |
| 198 | +grep -n "note:" scripts/release-downloads.mjs |
| 199 | +``` |
| 200 | + |
| 201 | +The two `*-linux-gnu` entries must carry a `glibc <FLOOR>+` note and the two |
| 202 | +`*-linux-musl` entries a `static, musl` note, so a reader picking an asset by |
| 203 | +hand can tell which one runs on their host. All four Linux triples must be in |
| 204 | +`CLI_TARGETS`; an unlisted one degrades to a bare triple label. |
| 205 | + |
| 206 | +### 6. Check whether the floor rose since the last release |
| 207 | + |
| 208 | +This is the one case that reaches users who already have the app installed. |
| 209 | + |
| 210 | +```bash |
| 211 | +git diff "$(git describe --tags --abbrev=0)" -- .github/workflows/release.yml | grep -E "^[+-].*os: ubuntu" |
| 212 | +``` |
| 213 | + |
| 214 | +No `..HEAD` on purpose: that form compares committed history only, so during |
| 215 | +release prep -- exactly when this skill runs -- a runner-image bump still |
| 216 | +sitting in the working tree would read as "floor unchanged". The two-dot-free |
| 217 | +form covers committed and uncommitted alike. No output means the image did not |
| 218 | +move. |
| 219 | + |
| 220 | +If the Linux runner image changed since the last release tag, say so loudly and |
| 221 | +report the old and new floors. A raised floor means hosts between the two |
| 222 | +versions run the current desktop app fine but cannot run the next one, and the |
| 223 | +self-updater has no glibc gate: `platform.rs::asset_key` selects on os/arch |
| 224 | +only, and `versions.json` records no minimum (see `scanAssets` in |
| 225 | +`scripts/gen-versions-json.mjs`). Those hosts would download, install, and end |
| 226 | +up with an app that will not start. |
| 227 | + |
| 228 | +Raising the floor is therefore a release decision, not an incidental bump. Flag |
| 229 | +it for the developer; do not decide it inside this skill. |
| 230 | + |
| 231 | +### 7. Report |
| 232 | + |
| 233 | +``` |
| 234 | +runner image / floor: ubuntu-22.04 -> glibc 2.35 |
| 235 | +install.sh MIN_GLIBC_*: PASS / FAIL |
| 236 | +install.sh detection: PASS / FAIL |
| 237 | +musl builds published: PASS / FAIL |
| 238 | +docs state the floor: PASS / FAIL |
| 239 | +download labels: PASS / FAIL |
| 240 | +floor unchanged since tag: PASS / RAISED (2.35 -> 2.39) |
| 241 | +
|
| 242 | +glibc floor: CONSISTENT / INCONSISTENT |
| 243 | +``` |
| 244 | + |
| 245 | +List every mismatch with its file and line and the value it should hold. Do not |
| 246 | +edit files automatically -- propose the corrections and let the developer apply |
| 247 | +them. |
0 commit comments