assemble the phone sysroot from declared inputs
It was a directory on one machine with no record of its contents, which made the only build that can take the panel reproducible by one person. 155 arm64 packages now resolve from 12 requirements derived from the kms feature, each verified against the archive index.
This commit is contained in:
parent
9e9e433938
commit
6ddcffe334
4 changed files with 274 additions and 13 deletions
|
|
@ -35,7 +35,7 @@ jobs:
|
|||
sudo apt-get install -y --no-install-recommends \
|
||||
libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev \
|
||||
libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev \
|
||||
libdisplay-info-dev gcc-aarch64-linux-gnu
|
||||
libdisplay-info-dev gcc-aarch64-linux-gnu binutils python3 file
|
||||
|
||||
- name: fmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
|
@ -66,11 +66,23 @@ jobs:
|
|||
- name: build the x86_64 release (nested + kms)
|
||||
run: cargo build --release -p viewtop-compositor --features nested,kms --bin viewtop
|
||||
|
||||
# scripts/build-cross.sh (the phone KMS build) is deliberately absent from
|
||||
# this branch and is NOT dropped: it requires an aarch64 sysroot carrying
|
||||
# .pc files for libseat, libinput, libudev, gbm, EGL and libdrm, which is
|
||||
# machine state this workflow cannot declare. Reproducing it here means
|
||||
# provisioning an arm64 multiarch root, and the path layout differs from
|
||||
# the sysroot the internal build uses. Until that is proven, the internal
|
||||
# workflow on main is the only place the phone backend is compiled, and
|
||||
# this file says so rather than implying the gate is complete.
|
||||
# The phone backend links real native libraries, so it needs a target
|
||||
# sysroot. That sysroot used to be a directory on one machine with no
|
||||
# record of what was in it, which made the only build that can take the
|
||||
# panel reproducible by one person. It is assembled here from declared
|
||||
# inputs and the distribution's own dependency metadata.
|
||||
- name: assemble the aarch64 sysroot
|
||||
run: python3 scripts/make-aarch64-sysroot.py --out "$HOME/aarch64-sysroot"
|
||||
|
||||
# The phone target, and the only build that produces something which can
|
||||
# take the panel. The script refuses to start if pkg-config answers from
|
||||
# anywhere but the sysroot, and refuses to finish if the result is not
|
||||
# aarch64.
|
||||
- name: cross-build the phone backend (kms)
|
||||
run: ./scripts/build-cross.sh --release
|
||||
|
||||
- name: record what the phone binary linked against
|
||||
run: |
|
||||
cat "$HOME/aarch64-sysroot/SYSROOT-MANIFEST.txt"
|
||||
aarch64-linux-gnu-readelf -d target/aarch64-unknown-linux-gnu/release/viewtop \
|
||||
| grep NEEDED
|
||||
|
|
|
|||
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1683,7 +1683,6 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
|||
[[package]]
|
||||
name = "smithay"
|
||||
version = "0.7.0"
|
||||
source = "git+https://forge.caseytunturi.com/Fimeg/smithay.git?rev=227c7e43389317278e7d2b313d59d95c55ea9f36#227c7e43389317278e7d2b313d59d95c55ea9f36"
|
||||
dependencies = [
|
||||
"appendlist",
|
||||
"atomic_float",
|
||||
|
|
@ -1781,7 +1780,6 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "smithay-drm-extras"
|
||||
version = "0.1.0"
|
||||
source = "git+https://forge.caseytunturi.com/Fimeg/smithay.git?rev=227c7e43389317278e7d2b313d59d95c55ea9f36#227c7e43389317278e7d2b313d59d95c55ea9f36"
|
||||
dependencies = [
|
||||
"drm",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -27,8 +27,24 @@ TARGET=aarch64-unknown-linux-gnu
|
|||
# search path outright rather than adding to it: prepending leaves the host's
|
||||
# .pc files reachable, and one of them silently winning is worse than a clean
|
||||
# failure.
|
||||
# Two layouts are supported because the sysroot has two legitimate origins:
|
||||
# a flat one built by hand, and the multiarch one scripts/make-aarch64-sysroot.py
|
||||
# assembles from distribution packages. Detect rather than assume; the flat path
|
||||
# is not privileged just because one machine happens to be arranged that way.
|
||||
TRIPLE=aarch64-linux-gnu
|
||||
if [[ -d "$SYSROOT/usr/lib/$TRIPLE/pkgconfig" ]]; then
|
||||
PC_DIR="$SYSROOT/usr/lib/$TRIPLE/pkgconfig"
|
||||
LIB_DIR="$SYSROOT/usr/lib/$TRIPLE"
|
||||
else
|
||||
PC_DIR="$SYSROOT/usr/lib/pkgconfig"
|
||||
LIB_DIR="$SYSROOT/usr/lib"
|
||||
fi
|
||||
if [[ -d "$SYSROOT/usr/share/pkgconfig" ]]; then
|
||||
PC_DIR="$PC_DIR:$SYSROOT/usr/share/pkgconfig"
|
||||
fi
|
||||
|
||||
export PKG_CONFIG_ALLOW_CROSS=1
|
||||
export PKG_CONFIG_LIBDIR="$SYSROOT/usr/lib/pkgconfig"
|
||||
export PKG_CONFIG_LIBDIR="$PC_DIR"
|
||||
export PKG_CONFIG_SYSROOT_DIR="$SYSROOT"
|
||||
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
|
||||
|
|
@ -38,7 +54,28 @@ export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
|
|||
# project: ld resolves the sysroot `libc.so` linker script's paths against the
|
||||
# wrong prefix and falls back to the host's `/lib/libc.so.6`. The explicit `-L`
|
||||
# is what makes it find the right one. See the packages DUMP, 2026-07-30.
|
||||
export RUSTFLAGS="${RUSTFLAGS:-} -C link-arg=--sysroot=$SYSROOT -L $SYSROOT/usr/lib"
|
||||
# -L resolves the libraries named on the command line. It does NOT resolve
|
||||
# what those libraries themselves need: ld follows libseat.so's DT_NEEDED to
|
||||
# libsystemd.so.0 and libinput.so's to libevdev.so.2, and searches -rpath-link
|
||||
# for those, not -L. Without it the link fails on undefined references to
|
||||
# symbols that are sitting right there in the sysroot.
|
||||
export RUSTFLAGS="${RUSTFLAGS:-} -C link-arg=--sysroot=$SYSROOT -L $LIB_DIR -L $SYSROOT/usr/lib \
|
||||
-C link-arg=-Wl,-rpath-link=$LIB_DIR -C link-arg=-Wl,-rpath-link=$SYSROOT/usr/lib"
|
||||
|
||||
# Prove the search path answers for the target before spending a build on it.
|
||||
# A host .pc file winning silently is the failure this whole arrangement exists
|
||||
# to prevent, and it is cheap to rule out.
|
||||
for lib in libdrm gbm libinput libudev libseat egl glesv2 xkbcommon; do
|
||||
flags=$(pkg-config --libs --cflags "$lib") || {
|
||||
echo "pkg-config cannot resolve $lib inside $SYSROOT" >&2
|
||||
exit 1
|
||||
}
|
||||
leaked=$(tr ' ' '\n' <<<"$flags" | grep -E '^-(L|I)/' | grep -v "^-.$SYSROOT" || true)
|
||||
if [[ -n "$leaked" ]]; then
|
||||
echo "$lib resolved outside the sysroot: $leaked" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
cargo build --target "$TARGET" -p viewtop-compositor --features kms --bin viewtop "$@"
|
||||
|
||||
|
|
@ -46,3 +83,7 @@ BIN="target/$TARGET/debug/viewtop"
|
|||
[[ -f "target/$TARGET/release/viewtop" ]] && BIN="target/$TARGET/release/viewtop"
|
||||
echo "== built: $BIN =="
|
||||
file "$BIN"
|
||||
file -b "$BIN" | grep -q 'ARM aarch64' || {
|
||||
echo "built binary is not aarch64" >&2
|
||||
exit 1
|
||||
}
|
||||
|
|
|
|||
210
scripts/make-aarch64-sysroot.py
Executable file
210
scripts/make-aarch64-sysroot.py
Executable file
|
|
@ -0,0 +1,210 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Assemble the aarch64 sysroot the phone build links against.
|
||||
|
||||
The sysroot used to be a directory that existed on one machine, built by hand,
|
||||
with no record of what was in it. That made the phone backend -- the only build
|
||||
that produces something able to take the panel -- reproducible by exactly one
|
||||
person. This script replaces that with declared inputs: a pinned suite, a
|
||||
top-level requirement list, and the distribution's own dependency metadata.
|
||||
|
||||
scripts/make-aarch64-sysroot.py [--out DIR] [--suite noble]
|
||||
|
||||
Everything is fetched over the archive's index, checked against the SHA256 the
|
||||
index publishes, and extracted. Nothing binary is committed. The resolved set,
|
||||
with versions, is written to <out>/SYSROOT-MANIFEST.txt so a build can say what
|
||||
it linked against.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import gzip
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
MIRROR = "http://ports.ubuntu.com/ubuntu-ports"
|
||||
COMPONENTS = ("main", "universe")
|
||||
ARCH = "arm64"
|
||||
|
||||
# What scripts/build-cross.sh actually needs to resolve. Derived from the `kms`
|
||||
# feature in crates/compositor/Cargo.toml: backend_drm, backend_gbm,
|
||||
# backend_libinput, backend_session_libseat, backend_udev, renderer_gl,
|
||||
# backend_egl. Everything else in the sysroot arrives as a declared dependency
|
||||
# of one of these, not as a guess.
|
||||
REQUIRED = [
|
||||
"libdrm-dev",
|
||||
"libgbm-dev",
|
||||
"libinput-dev",
|
||||
"libudev-dev",
|
||||
"libseat-dev",
|
||||
"libegl-dev",
|
||||
"libgles-dev",
|
||||
"libwayland-dev",
|
||||
"libxkbcommon-dev",
|
||||
"libdisplay-info-dev",
|
||||
"libc6-dev",
|
||||
"linux-libc-dev",
|
||||
]
|
||||
|
||||
# Virtual/toolchain packages the cross-gcc supplies itself. Pulling these in
|
||||
# would drag a second compiler into the sysroot for nothing.
|
||||
SKIP = {"gcc", "gcc-13", "libgcc-13-dev", "libc-dev", "libc6-dev-arm64-cross",
|
||||
"pkg-config", "pkgconf", "debconf", "dpkg", "install-info"}
|
||||
|
||||
|
||||
def fetch(url):
|
||||
# The archive rejects urllib's default agent with 403.
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "viewtop-sysroot/1"})
|
||||
with urllib.request.urlopen(req, timeout=120) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def load_index(suite, component):
|
||||
raw = fetch(f"{MIRROR}/dists/{suite}/{component}/binary-{ARCH}/Packages.gz")
|
||||
text = gzip.decompress(raw).decode("utf-8", "replace")
|
||||
index, provides = {}, {}
|
||||
for block in text.split("\n\n"):
|
||||
if not block.strip():
|
||||
continue
|
||||
fields = {}
|
||||
key = None
|
||||
for line in block.split("\n"):
|
||||
if line.startswith(" ") and key:
|
||||
continue
|
||||
if ":" in line:
|
||||
key, _, value = line.partition(":")
|
||||
fields[key] = value.strip()
|
||||
name = fields.get("Package")
|
||||
if not name:
|
||||
continue
|
||||
index[name] = fields
|
||||
for prov in re.split(r",\s*", fields.get("Provides", "")):
|
||||
prov = prov.split("(")[0].strip()
|
||||
if prov:
|
||||
provides.setdefault(prov, name)
|
||||
return index, provides
|
||||
|
||||
|
||||
def dependencies(fields):
|
||||
out = []
|
||||
for group in re.split(r",\s*", fields.get("Depends", "")):
|
||||
if not group.strip():
|
||||
continue
|
||||
# An alternation "a | b" is satisfied by the first that exists.
|
||||
out.append([alt.split("(")[0].strip() for alt in group.split("|")])
|
||||
return out
|
||||
|
||||
|
||||
def resolve(index, provides, roots):
|
||||
chosen, queue, missing = {}, list(roots), []
|
||||
while queue:
|
||||
name = queue.pop(0)
|
||||
if name in chosen or name in SKIP:
|
||||
continue
|
||||
fields = index.get(name) or index.get(provides.get(name, ""))
|
||||
if fields is None:
|
||||
missing.append(name)
|
||||
continue
|
||||
chosen[fields["Package"]] = fields
|
||||
for alternatives in dependencies(fields):
|
||||
for alt in alternatives:
|
||||
if alt in chosen or alt in SKIP:
|
||||
break
|
||||
if alt in index or alt in provides:
|
||||
queue.append(alt)
|
||||
break
|
||||
return chosen, missing
|
||||
|
||||
|
||||
def download(fields, cache):
|
||||
url = f"{MIRROR}/{fields['Filename']}"
|
||||
path = os.path.join(cache, os.path.basename(fields["Filename"]))
|
||||
if not os.path.exists(path):
|
||||
data = fetch(url)
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
if digest != fields["SHA256"]:
|
||||
raise SystemExit(f"{fields['Package']}: sha256 mismatch against the index")
|
||||
with open(path, "wb") as fh:
|
||||
fh.write(data)
|
||||
return path
|
||||
|
||||
|
||||
def extract(deb, out):
|
||||
# `ar x` then unpack data.tar.*; dpkg-deb is a Debian tool and this has to
|
||||
# run on whatever the runner and Casey's Arch box provide.
|
||||
tmp = os.path.join(out, ".deb-tmp")
|
||||
os.makedirs(tmp, exist_ok=True)
|
||||
subprocess.run(["ar", "x", deb], cwd=tmp, check=True)
|
||||
data = next(os.path.join(tmp, f) for f in os.listdir(tmp) if f.startswith("data.tar"))
|
||||
subprocess.run(["tar", "-xf", data, "-C", out], check=True)
|
||||
shutil.rmtree(tmp)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--out", default=os.path.expanduser("~/aarch64-sysroot"))
|
||||
ap.add_argument("--suite", default="noble")
|
||||
ap.add_argument("--cache", default=os.path.expanduser("~/.cache/viewtop-sysroot"))
|
||||
args = ap.parse_args()
|
||||
|
||||
index, provides = {}, {}
|
||||
for component in COMPONENTS:
|
||||
i, p = load_index(args.suite, component)
|
||||
index.update(i)
|
||||
provides.update(p)
|
||||
print(f"{len(index)} {ARCH} packages in {args.suite} "
|
||||
f"({'+'.join(COMPONENTS)})")
|
||||
|
||||
chosen, missing = resolve(index, provides, REQUIRED)
|
||||
if missing:
|
||||
print("unresolved: " + ", ".join(sorted(set(missing))), file=sys.stderr)
|
||||
print(f"{len(chosen)} packages resolved from {len(REQUIRED)} declared requirements")
|
||||
|
||||
os.makedirs(args.cache, exist_ok=True)
|
||||
if os.path.exists(args.out):
|
||||
shutil.rmtree(args.out)
|
||||
os.makedirs(args.out)
|
||||
|
||||
lines = [f"# {args.suite}/{ARCH} from {MIRROR}",
|
||||
f"# {len(chosen)} packages resolved from: {' '.join(REQUIRED)}"]
|
||||
for name in sorted(chosen):
|
||||
fields = chosen[name]
|
||||
extract(download(fields, args.cache), args.out)
|
||||
lines.append(f"{name} {fields['Version']} {fields['SHA256']}")
|
||||
# Merged-/usr. libc6 ships libm.so as a linker script naming
|
||||
# /lib/aarch64-linux-gnu/libm.so.6, and on a real Ubuntu root /lib is a
|
||||
# symlink into usr/lib supplied by the bootstrap, not by any .deb. Without
|
||||
# these the link fails on libm and libmvec -- correctly, having refused to
|
||||
# fall back to the host's copies.
|
||||
for link in ("lib", "lib64", "bin", "sbin"):
|
||||
target = os.path.join(args.out, "usr", link)
|
||||
path = os.path.join(args.out, link)
|
||||
if not os.path.isdir(target):
|
||||
continue
|
||||
if not os.path.exists(path):
|
||||
os.symlink(os.path.join("usr", link), path)
|
||||
continue
|
||||
# /lib already exists as a real directory because libinput and systemd
|
||||
# ship ./lib/udev and ./lib/systemd, so the whole-directory symlink
|
||||
# cannot be made. Link each missing entry instead of flattening either
|
||||
# side: the loader and the multiarch directory both have to be findable
|
||||
# at the paths libc's linker scripts name.
|
||||
for entry in os.listdir(target):
|
||||
inner = os.path.join(path, entry)
|
||||
if not os.path.exists(inner):
|
||||
os.symlink(os.path.join("..", "usr", link, entry), inner)
|
||||
|
||||
with open(os.path.join(args.out, "SYSROOT-MANIFEST.txt"), "w") as fh:
|
||||
fh.write("\n".join(lines) + "\n")
|
||||
|
||||
pc = os.path.join(args.out, "usr", "lib", f"{ARCH.replace('arm64', 'aarch64')}-linux-gnu",
|
||||
"pkgconfig")
|
||||
print(f"sysroot at {args.out}")
|
||||
print(f"pkgconfig {pc} ({len(os.listdir(pc)) if os.path.isdir(pc) else 0} files)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue