[FIX] test-chroot: an audit that classifies, and one that was wrong

The soname check reported seventeen missing libraries. Four were false: it
built its "shipped" set from DT_SONAME alone, and tcl's libtcl9.0.so records
no SONAME at all. The file sits in usr/lib and ld.so resolves it by name, so
the check was calling a correct package broken -- which would have sent the
next reader hunting for a packaging bug that does not exist. Shipped filenames
and symlinks now count as supplied.

The remaining twelve needed separating, because an unclassified list is only
alarming. A missing soname whose library exists at a DIFFERENT version is
stage 1 working as designed: it linked the host, and stage 2 dissolves it. A
missing soname with no provider at any version is a closure gap. Nine and
three.

The three are each understood: pylibmount's cp313 extension (inert, no python
is shipped), makedb's libselinux (deliberate), and libtcl8.6 against our tcl
9.0 -- which is the honest cost of a decision made an hour before on closure
size without looking at ABI.

TODO.md records all twelve, with the note that stage 2 must install using the
host's pacman, because pacman inside the stage-1 rootfs cannot start.

--- FR ---

La vérification de sonames annonçait dix-sept bibliothèques manquantes. Quatre
étaient fausses : elle construisait son ensemble « livré » à partir du seul
DT_SONAME, et le libtcl9.0.so de tcl n'enregistre aucun SONAME. Le fichier est
dans usr/lib et ld.so le résout par son nom : la vérification déclarait donc
défectueux un paquet correct — de quoi envoyer le lecteur suivant chasser un
défaut d'empaquetage inexistant. Les noms de fichiers et les liens livrés
comptent désormais comme fournis.

Les douze restants demandaient d'être séparés, une liste non classée n'étant
qu'alarmante. Un soname manquant dont la bibliothèque existe à une AUTRE
version, c'est l'étage 1 fonctionnant comme prévu : il a lié l'hôte, et
l'étage 2 le dissout. Un soname sans aucun fournisseur est un trou de
fermeture. Neuf et trois.

Les trois sont compris : l'extension cp313 de pylibmount (inerte, aucun python
livré), le libselinux de makedb (délibéré), et libtcl8.6 contre notre tcl 9.0
— coût honnête d'une décision prise une heure plus tôt sur la taille de la
fermeture, sans regarder l'ABI.

TODO.md consigne les douze, avec la note que l'étage 2 devra installer avec le
pacman de l'hôte, celui du rootfs d'étage 1 ne pouvant pas démarrer.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-19 06:40:50 -04:00
parent 6f44fdb572
commit 7ec3d14851
2 changed files with 123 additions and 4 deletions

85
TODO.md
View file

@ -123,6 +123,91 @@ only moves it out of `/usr/local`, which an Arch package may not ship.
**To close:** stage 2 builds it with the port's own python.
### Host sonames the target does not have — `scripts/test-chroot.sh` check 3
Nine libraries are requested by a shipped binary at the HOST's version while
the repository ships the same library at Arch's:
| requested | by | we ship |
|---|---|---|
| `libgpgme.so.11` | pacman (via libalpm) | gpgme 2.x, `.45` |
| `libnettle.so.8`, `libhogweed.so.6` | gnutls, libcurl-gnutls | nettle |
| `libicuuc.so.76` | libxml2 | icu |
| `liblmdb.so.0` | krb5 | lmdb |
| `libnsl.so.2` | pam | libnsl |
| `libsasl2.so.2` | libldap, openldap | libsasl |
| `libsodium.so.23` | openldap | libsodium |
| `libdevmapper.so.1.02.1` | cryptsetup | device-mapper |
This is stage 1 working as designed, not a defect list: stage 1 links the
host, so it records the host's sonames. It is written down because the
consequence is concrete — `pacman` in the stage-1 rootfs does not start:
pacman: error while loading shared libraries: libgpgme.so.11
**To close:** stage 2. Every one of these dissolves when the package is
rebuilt inside the chroot against the repository's own libraries. Nothing here
should be hooked in stage 1; a hook would only hide it.
Note for stage 2's design: because pacman cannot run inside the stage-1
rootfs, stage 2 has to install with the HOST's pacman and `--root`, the way
scripts/test-chroot.sh already does, and use the chroot only for building.
### Three sonames with no provider at all — same check
Distinct from the nine above, and each for its own reason:
- `libpython3.13.so.1.0` ← `util-linux-libs`. It ships
`pylibmount.cpython-313-*.so`, built against the host's python. Inert: this
port ships no python, so nothing imports it. Stage 2 drops it by
construction — the chroot has no python either, so meson will not build it.
Worth naming because `arch-meson` was fixed to put this file in the right
PLACE (site-packages, not dist-packages) and the wrong ABI stayed invisible
until the soname audit.
- `libselinux.so.1` ← `glibc`'s `makedb`. Deliberate, see above.
- `libtcl8.6.so` ← `sqlite-tcl`, `sqlite-analyzer`. We ship tcl **9.0**;
sqlite's configure found the host's 8.6 tclConfig.sh. An honest admission:
the measurement that chose to BUILD tcl rather than drop these
sub-packages compared closure cost and never looked at the ABI. Stage 2
fixes it — tcl 9.0 is in the repository — but the reasoning had a blind
spot, and it was the same blind spot as the pylibmount one.
### `lvm2` — `patches/pkgbuild/lvm2.sh`
Only `device-mapper` is built. The `lvm2` half declares
`thin-provisioning-tools`, which is Rust now, so a language runtime arrived
through a fourth-order dependency. Nothing in the repository wants `lvm2`
itself; cryptsetup wants device-mapper, which the same source produces.
**To close:** build Rust, restore the sub-package, rebuild. A target that
wants LVM needs this; dm-crypt and systemd do not.
### SQL auxprop in libsasl — `patches/pkgbuild/libsasl.sh`
Disabled. Its two database headers exist on this host, on paths configure does
not try (`/usr/include/postgresql`, `/usr/include/mariadb`). Arch declares
`depends=(glibc)` for libsasl because the plugins are dlopened, so nothing is
lost.
**To close:** supply the include paths, or leave it — a bootstrap has no SQL
authentication backend to serve.
### fuse2fs — `patches/pkgbuild/e2fsprogs.sh`
Not built: no fuse3 on the host, and none in the closure.
**To close:** add `fuse3`, and both the sub-package and the removal that feeds
it come back on their own.
### kbd without xkb — `patches/pkgbuild/kbd.sh`
`loadkeys` cannot generate keymaps from an XKB database. Arch's kbd cannot
either — it declares libxkbcommon nowhere, so Arch's chroot fails the same
probe. Building it would want libxcb and xkeyboard-config behind it.
**To close:** it is already closed with respect to Arch. Reopen only if a
target grows a graphics stack.
---
## Architectural

View file

@ -125,17 +125,51 @@ for f in "$REPO"/*.pkg.tar.*; do
readelf -d "$b" 2>/dev/null | sed -n 's/.*Shared library: \[\(.*\)\].*/\1/p' \
| sed "s|^|$_pn |" >> "$_sa/want"
done < <(find "$_sa/x" -type f 2>/dev/null)
# Shipped FILENAMES count as supplied, not only recorded SONAMEs. ld.so
# resolves a DT_NEEDED entry by looking for a file of that name, and a
# library is free to record no DT_SONAME at all -- tcl's libtcl9.0.so does
# exactly that. Counting only SONAMEs reported it as missing while the file
# sat in usr/lib, which would have sent the next reader hunting for a
# packaging bug that does not exist. Symlinks count too: they are what
# ld.so follows.
find "$_sa/x" \( -type f -o -type l \) -name '*.so*' -printf '%f\n' \
2>/dev/null >> "$_sa/have"
done
sort -u "$_sa/have" > "$_sa/have.s"
awk '{print $2}' "$_sa/want" | sort -u > "$_sa/want.s"
if [ -s "$(comm -13 "$_sa/have.s" "$_sa/want.s" > "$_sa/miss"; echo "$_sa/miss")" ]; then
bad "$(wc -l < "$_sa/miss") soname(s) requested and never shipped:"
comm -13 "$_sa/have.s" "$_sa/want.s" > "$_sa/miss"
# CLASSIFY, because the two kinds need different work and an unclassified list
# is just alarming. A missing soname whose library exists in the repository at
# a DIFFERENT version is the ordinary stage-1 artefact: the binary linked the
# host's copy, and stage 2 dissolves it by rebuilding in the chroot. A missing
# soname with no provider at any version is a CLOSURE GAP -- a package that
# still has to be built, or a dependency nobody declared.
: > "$_sa/drift"; : > "$_sa/gap"
while read -r m; do
_base=${m%%.so*}
if grep -q "^${_base}\.so" "$_sa/have.s"; then
echo "$m" >> "$_sa/drift"
else
echo "$m" >> "$_sa/gap"
fi
done < "$_sa/miss"
_report() {
while read -r m; do
printf ' %-24s <- %s\n' "$m" \
"$(awk -v m="$m" '$2==m {print $1}' "$_sa/want" | sort -u | tr '\n' ' ')"
done < "$_sa/miss"
done < "$1"
}
if [ -s "$_sa/gap" ]; then
bad "$(wc -l < "$_sa/gap") soname(s) with NO provider at any version:"
_report "$_sa/gap"
else
good "every requested soname is shipped by some package"
good "every requested soname has a provider in the repository"
fi
if [ -s "$_sa/drift" ]; then
printf ' note %s soname(s) at the host version, provider present at another\n' \
"$(wc -l < "$_sa/drift")"
printf ' (stage-1 artefact by construction -- stage 2 rebuilds these)\n'
_report "$_sa/drift"
fi
rm -rf "$_sa"