From e494fe346cc76b7095635f28e8de1f1b03a27e76 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Tue, 13 Oct 2026 13:46:58 -0300 Subject: [PATCH 1/2] Pin the builders' packages, carry patches as pedigree, take source URLs from the pins QEMU's and e2fsprogs' builders installed whatever Alpine's mirror served on the day of the build: the static libraries linked into released binaries were the mirror's, not a commit's. Each part now has an apk.list (what it asks for) and an apk.lock (every package installed, dependencies included, at a version), written by task apk:lock from a fresh install on the pinned Alpine and read back by hack/apklock. A pin the mirror has dropped fails a cold build at apk add; the fix is task apk:lock and the diff is the review. The SBOM's sources component now carries QEMU's, qboot's and the kernel's patches as pedigree, and SOURCES names the URL each tarball's sha256 was checked against (go tool versions url), not one written in hack/release. go-tools 99d7447 for versions url and sbom pins -patches. Co-Authored-By: Claude Opus 5.5 (1M context) --- Taskfile.yml | 16 ++++ docs/releasing.md | 9 ++- e2fsprogs/Dockerfile | 10 +-- e2fsprogs/apk.list | 6 ++ e2fsprogs/apk.lock | 75 +++++++++++++++++++ go.mod | 4 +- go.sum | 12 +-- hack/apklock/main.go | 80 ++++++++++++++++++++ hack/apklock/main_test.go | 74 +++++++++++++++++++ hack/release | 17 ++++- qemu/Dockerfile | 15 ++-- qemu/apk.list | 12 +++ qemu/apk.lock | 150 ++++++++++++++++++++++++++++++++++++++ 13 files changed, 452 insertions(+), 28 deletions(-) create mode 100644 e2fsprogs/apk.list create mode 100644 e2fsprogs/apk.lock create mode 100644 hack/apklock/main.go create mode 100644 hack/apklock/main_test.go create mode 100644 qemu/apk.list create mode 100644 qemu/apk.lock diff --git a/Taskfile.yml b/Taskfile.yml index b63b6d2..e9063d1 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -128,6 +128,22 @@ tasks: # Build # ========================================================================== + apk:lock: + desc: Pin every package QEMU's and e2fsprogs' builders install (each part's apk.list) at what the pinned Alpine's mirror has today, into its apk.lock. + # A fresh install of the list on the pinned Alpine, its database read back: the versions the + # mirror serves now, which is what a bump means. A new lock is a new QEMU or e2fsprogs build, + # and a changed QEMU is a changed machine: hack/fingerprint-diff says so in the release. + cmds: + - for: [qemu, e2fsprogs] + cmd: | + set -euo pipefail + db=$(mktemp) + docker run --rm -v {{.ROOT_DIR}}/{{.ITEM}}/apk.list:/apk.list:ro $({{.VERSIONS}} ref alpine) \ + sh -c 'apk add --no-cache $(grep -v "^#" /apk.list) > /dev/null && cat /lib/apk/db/installed' > "$db" + go run ./hack/apklock "$db" > {{.ITEM}}/apk.lock + rm -f "$db" + echo "{{.ITEM}}/apk.lock: $(wc -l < {{.ITEM}}/apk.lock) packages" + build: desc: Build QEMU, the guest kernel, e2fsprogs, the base image and the tools into _output/ cmds: diff --git a/docs/releasing.md b/docs/releasing.md index ae4596a..3567806 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -62,7 +62,14 @@ recorded - the apk databases QEMU and e2fsprogs were statically linked in (each component with no version fails the release. It is inside the tarball at `usr/share/spin-stack/sbom/machine.cdx.json`, beside it as `.cdx.json`, and attested over the tarball as `.cdx.sigstore.json` (`cosign verify-blob-attestation --type -cyclonedx`, with the identity above). +cyclonedx`, with the identity above). QEMU, the kernel and qboot carry their patches as pedigree: +each patch file by name and hash, so the SBOM says the binary is upstream's source plus these. + +The builders' packages are a commit's, not the mirror's on the day: `qemu/apk.list` and +`e2fsprogs/apk.list` say what each installs, and `qemu/apk.lock` and `e2fsprogs/apk.lock` pin every +package installed, dependencies included, at a version. A pin Alpine has dropped from its mirror +fails a cold build at `apk add`; `task apk:lock` writes both locks again from a fresh install on the +pinned Alpine, and the diff is the review. `workflows/workflows_test.go` holds every workflow to attesting before it publishes. ## The feature matrix diff --git a/e2fsprogs/Dockerfile b/e2fsprogs/Dockerfile index 1b6896b..656d726 100644 --- a/e2fsprogs/Dockerfile +++ b/e2fsprogs/Dockerfile @@ -27,12 +27,12 @@ ARG JOBS=8 SHELL ["/bin/sh", "-o", "pipefail", "-c"] +# e2fsprogs/apk.list says what; e2fsprogs/apk.lock the version of everything installed, so the +# libraries the static tools link are a commit's. A version the mirror no longer has fails here: +# task apk:lock moves the lock. +COPY e2fsprogs/apk.list e2fsprogs/apk.lock /tmp/apk/ RUN --mount=type=cache,sharing=locked,id=e2fsprogs-apk,target=/var/cache/apk \ - apk add --cache-dir /var/cache/apk \ - build-base linux-headers pkgconf curl xz binutils \ - libarchive-dev libarchive-static \ - acl-static zlib-static xz-static zstd-static bzip2-static lz4-static \ - expat-static openssl-libs-static + apk add --cache-dir /var/cache/apk $(grep -v '^#' /tmp/apk/apk.list) $(cat /tmp/apk/apk.lock) WORKDIR /build diff --git a/e2fsprogs/apk.list b/e2fsprogs/apk.list new file mode 100644 index 0000000..4915822 --- /dev/null +++ b/e2fsprogs/apk.list @@ -0,0 +1,6 @@ +# What e2fsprogs' builder installs, each package at the version e2fsprogs/apk.lock pins +# (task apk:lock). The -static ones are what the tools link. +build-base linux-headers pkgconf curl xz binutils +libarchive-dev libarchive-static +acl-static zlib-static xz-static zstd-static bzip2-static lz4-static +expat-static openssl-libs-static diff --git a/e2fsprogs/apk.lock b/e2fsprogs/apk.lock new file mode 100644 index 0000000..cfd6b75 --- /dev/null +++ b/e2fsprogs/apk.lock @@ -0,0 +1,75 @@ +acl-dev=2.3.2-r1 +acl-libs=2.3.2-r1 +acl-static=2.3.2-r1 +alpine-baselayout-data=3.7.2-r1 +alpine-baselayout=3.7.2-r1 +alpine-keys=2.6-r0 +alpine-release=3.24.2-r0 +apk-tools=3.0.8-r0 +binutils=2.45.1-r1 +brotli-libs=1.2.0-r1 +build-base=0.5-r4 +busybox-binsh=1.37.0-r31 +busybox=1.37.0-r31 +bzip2-dev=1.0.8-r6 +bzip2-static=1.0.8-r6 +c-ares=1.34.8-r0 +ca-certificates-bundle=20260909-r0 +curl=8.22.0-r0 +expat-dev=2.8.5-r0 +expat-static=2.8.5-r0 +expat=2.8.5-r0 +file=5.47-r2 +fortify-headers=3.0.1-r2 +g++=15.2.0-r5 +gcc=15.2.0-r5 +gmp=6.3.0-r4 +isl26=0.26-r2 +jansson=2.15.0-r0 +libapk=3.0.8-r0 +libarchive-dev=3.8.7-r0 +libarchive-static=3.8.7-r0 +libarchive=3.8.7-r0 +libatomic=15.2.0-r5 +libbz2=1.0.8-r6 +libcrypto3=3.5.9-r0 +libcurl=8.22.0-r0 +libexpat=2.8.5-r0 +libgcc-static=15.2.0-r5 +libgcc=15.2.0-r5 +libgomp=15.2.0-r5 +libidn2=2.3.8-r0 +libmagic=5.47-r2 +libpsl=0.21.5-r3 +libssl3=3.5.9-r0 +libstdc++-dev=15.2.0-r5 +libstdc++=15.2.0-r5 +libunistring=1.4.2-r0 +linux-headers=7.0.0-r1 +lz4-dev=1.10.0-r1 +lz4-libs=1.10.0-r1 +lz4-static=1.10.0-r1 +make=4.4.1-r4 +mpc1=1.3.1-r1 +mpfr4=4.2.2-r0 +musl-dev=1.2.6-r2 +musl-utils=1.2.6-r2 +musl=1.2.6-r2 +nghttp2-libs=1.70.0-r0 +openssl-dev=3.5.9-r0 +openssl-libs-static=3.5.9-r0 +patch=2.8-r0 +pkgconf=2.5.1-r0 +scanelf=1.3.9-r1 +ssl_client=1.37.0-r31 +xz-dev=5.8.4-r0 +xz-libs=5.8.4-r0 +xz-static=5.8.4-r0 +xz=5.8.4-r0 +zlib-dev=1.3.2-r1 +zlib-static=1.3.2-r1 +zlib=1.3.2-r1 +zstd-dev=1.5.7-r2 +zstd-libs=1.5.7-r2 +zstd-static=1.5.7-r2 +zstd=1.5.7-r2 diff --git a/go.mod b/go.mod index 3769587..b06a304 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ tool ( ) require ( - github.com/spin-stack/go-tools v0.0.0-20261013153230-6e2b16ee38be + github.com/spin-stack/go-tools v0.0.0-20261013164036-99d744796066 golang.org/x/sys v0.48.0 ) @@ -23,5 +23,5 @@ require ( go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/mod v0.41.0 // indirect golang.org/x/sync v0.23.0 // indirect - golang.org/x/tools v0.50.0 // indirect + golang.org/x/tools v0.51.0 // indirect ) diff --git a/go.sum b/go.sum index 8ddde08..2a062e0 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/in-toto/attestation v1.2.0 h1:aPRUZ3azbqD7yEBD5fP3TD8Dszf+YHo284SOcpa github.com/in-toto/attestation v1.2.0/go.mod h1:r79G45gOmzPismgObLSL+rZTFxUgZLOQJI6LofTZgXk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spin-stack/go-tools v0.0.0-20261013153230-6e2b16ee38be h1:pi7rsTCKW9m8ZYpPY/dk1di7/RYhM5fepPrh001sJI8= -github.com/spin-stack/go-tools v0.0.0-20261013153230-6e2b16ee38be/go.mod h1:cKPqYf5z2CcqsABqoifumRkiEEYSCBlRCwVNP+3Hz5A= +github.com/spin-stack/go-tools v0.0.0-20261013164036-99d744796066 h1:T4uAeYrQ9e9Fml3PcZGzgji5sIAoebXK9zy94u5IBwg= +github.com/spin-stack/go-tools v0.0.0-20261013164036-99d744796066/go.mod h1:cy1ojfIbEjrfZi0lGzE+wjueaWw1jrotHQ+vyFz9pZQ= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/terminalstatic/go-xsd-validate v0.1.8 h1:UVrTCy1j3DhwaYTTUF+QYO/Nan13S0tf+Jwi+p45Bf0= @@ -30,7 +30,7 @@ golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= -golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/hack/apklock/main.go b/hack/apklock/main.go new file mode 100644 index 0000000..cd305ef --- /dev/null +++ b/hack/apklock/main.go @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Command apklock writes the lock of an Alpine builder: every package an apk database says is +// installed, as name=version, sorted - what the builder's `apk add` is then held to, so the static +// libraries QEMU and e2fsprogs link are the versions a commit names, not whatever the mirror held +// that day. task apk:lock runs it over a fresh install of the part's apk.list. +// +// go run ./hack/apklock > /apk.lock +package main + +import ( + "fmt" + "io" + "os" + "slices" + "strings" +) + +func main() { + os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) // mutate-exempt: the process's own arguments, past its name; run is what the tests drive +} + +// run answers the exit status: 0 the lock written, 2 no database to read. +func run(args []string, stdout, stderr io.Writer) int { + if len(args) != 1 { + _, _ = fmt.Fprintln(stderr, "apklock: ") + return 2 + } + raw, err := os.ReadFile(args[0]) + if err != nil { + _, _ = fmt.Fprintln(stderr, "apklock:", err) + return 2 + } + lock, err := Lock(string(raw)) + if err != nil { + _, _ = fmt.Fprintln(stderr, "apklock:", err) + return 2 + } + _, _ = io.WriteString(stdout, lock) + return 0 +} + +// Lock is an apk installed database's packages as name=version lines, sorted. +func Lock(db string) (string, error) { + var pins []string + for _, block := range strings.Split(db, "\n\n") { + var name, version string + for _, line := range strings.Split(block, "\n") { + switch k, v, _ := strings.Cut(line, ":"); k { + case "P": + name = v + case "V": + version = v + } + } + switch { + case name == "" && version == "": + continue + case name == "" || version == "": + return "", fmt.Errorf("a package with name %q and version %q", name, version) + } + pins = append(pins, name+"="+version) + } + if len(pins) == 0 { + return "", fmt.Errorf("no package installed") + } + slices.Sort(pins) + return strings.Join(pins, "\n") + "\n", nil +} + +// Listed is the package names of an apk.list: one or more a line, # comments and blank lines left +// out. +func Listed(list string) []string { + var names []string + for _, line := range strings.Split(list, "\n") { + line, _, _ = strings.Cut(line, "#") + names = append(names, strings.Fields(line)...) + } + return names +} diff --git a/hack/apklock/main_test.go b/hack/apklock/main_test.go new file mode 100644 index 0000000..44d6717 --- /dev/null +++ b/hack/apklock/main_test.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +// A lock is every installed package, name=version, sorted; a package with half its record is no +// lock, and neither is none. +func TestALockIsEveryInstalledPackage(t *testing.T) { + db := "C:Q1abc=\nP:zlib-static\nV:1.3.1-r2\nA:x86_64\n\nP:glib-static\nV:2.88.1-r1\n\n" + got, err := Lock(db) + if err != nil || got != "glib-static=2.88.1-r1\nzlib-static=1.3.1-r2\n" { + t.Fatalf("lock %q, %v", got, err) + } + for name, db := range map[string]string{"no version": "P:glib\n", "no name": "V:1\n", "nothing": "\n"} { + if _, err := Lock(db); err == nil { + t.Errorf("%s: locked", name) + } + } +} + +// The command writes the lock of the database it is given, and nothing for none. +func TestTheCommandWritesTheLock(t *testing.T) { + path := filepath.Join(t.TempDir(), "installed") + if err := os.WriteFile(path, []byte("P:musl\nV:1.2.5-r10\n"), 0o600); err != nil { + t.Fatal(err) + } + var out, errs bytes.Buffer + if run([]string{path}, &out, &errs) != 0 || out.String() != "musl=1.2.5-r10\n" { + t.Fatalf("wrote %q: %s", out.String(), errs.String()) + } + broken := filepath.Join(t.TempDir(), "broken") + if err := os.WriteFile(broken, []byte("P:musl\n"), 0o600); err != nil { + t.Fatal(err) + } + for _, args := range [][]string{nil, {filepath.Join(t.TempDir(), "none")}, {broken}} { + if run(args, &out, &errs) != 2 { + t.Errorf("%v: no refusal", args) + } + } +} + +// Each builder's lock pins every package its list asks for: a name added to a list and not +// locked would be installed at whatever version the mirror has. +func TestEachListIsLocked(t *testing.T) { + for _, part := range []string{"qemu", "e2fsprogs"} { + list, err := os.ReadFile(filepath.Join("..", "..", part, "apk.list")) + if err != nil { + t.Fatal(err) + } + lock, err := os.ReadFile(filepath.Join("..", "..", part, "apk.lock")) + if err != nil { + t.Fatal(err) + } + names := Listed(string(list)) + if len(names) == 0 { + t.Fatalf("%s's list names nothing", part) + } + for _, n := range names { + if !strings.Contains("\n"+string(lock), "\n"+n+"=") { + t.Errorf("%s/apk.list asks for %s, which %s/apk.lock does not pin: task apk:lock", part, n, part) + } + } + } + if got := Listed("# a comment\nglib-static pixman-static # why\n\nzlib-static\n"); strings.Join(got, " ") != "glib-static pixman-static zlib-static" { + t.Errorf("listed %q", got) + } +} diff --git a/hack/release b/hack/release index 0d740a8..9677349 100755 --- a/hack/release +++ b/hack/release @@ -23,6 +23,11 @@ pins=$(go tool versions env qemu kernel e2fsprogs qboot) while IFS='=' read -r key value; do declare "${key}=${value}" done <<< "${pins}" +# Where each tarball is fetched from, as the pin says: SOURCES names the URL the build checked the +# sha256 of, never one written here, which named a mirror the kernel was not on. +QEMU_URL=$(go tool versions url qemu) +KERNEL_URL=$(go tool versions url kernel) +E2FSPROGS_URL=$(go tool versions url e2fsprogs) RELEASE_NAME="spin-machine-${VERSION}-linux-${KERNEL_ARCH}" RELEASE_DIR="${OUTPUT_DIR}/release/${RELEASE_NAME}" @@ -170,7 +175,7 @@ QEMU ${QEMU_VERSION} usr/share/spin-stack/bin/qemu-system-x86_64-tcg usr/share/spin-stack/bin/qemu-img firmware: usr/share/spin-stack/qemu/*, except patches/, qboot.bin and the qboot files below - source: https://download.qemu.org/qemu-${QEMU_VERSION}.tar.xz + source: ${QEMU_URL} sha256: ${QEMU_SHA256} built by: qemu/Dockerfile, with usr/share/spin-stack/qemu/patches applied to that source (each says what it changes and why) and the configure flags in that file @@ -186,14 +191,14 @@ qboot ${QBOOT_COMMIT} e2fsprogs ${E2FSPROGS_VERSION} binaries: usr/share/spin-stack/bin/mkfs.ext4, debugfs, e2fsck, dumpe2fs config: usr/share/spin-stack/e2fsprogs/mke2fs.conf, upstream's, from the same tarball - source: https://mirrors.edge.kernel.org/pub/linux/kernel/people/tytso/e2fsprogs/v${E2FSPROGS_VERSION}/e2fsprogs-${E2FSPROGS_VERSION}.tar.xz + source: ${E2FSPROGS_URL} sha256: ${E2FSPROGS_SHA256} built by: e2fsprogs/Dockerfile, unpatched, statically linked against the Alpine libraries named there; their sources are the Alpine package sources for that release Linux ${KERNEL_VERSION} binary: usr/share/spin-stack/kernel/vmlinux - source: https://cdn.kernel.org/pub/linux/kernel/v${KERNEL_VERSION%%.*}.x/linux-${KERNEL_VERSION}.tar.xz + source: ${KERNEL_URL} sha256: ${KERNEL_SHA256} built by: kernel/Dockerfile, with the patches shipped alongside the binary in usr/share/spin-stack/kernel/patches applied to that source (each says what @@ -242,7 +247,11 @@ sbom apk -name e2fsprogs-libraries -version "${E2FSPROGS_VERSION}" -distro alpin -only libarchive-static,acl-static,zlib-static,xz-static,zstd-static,bzip2-static,lz4-static,expat-static,openssl-libs-static,musl-dev,gcc \ -out "${PARTS}/e2fsprogs-libraries.cdx.json" "${OUTPUT_DIR}/sbom/e2fsprogs.apk" sbom dpkg -name base-image -version "${VERSION}" -distro ubuntu -out "${PARTS}/base-image.cdx.json" "${SHARE}/packages.txt" -sbom pins -name sources -version "${VERSION}" -out "${PARTS}/sources.cdx.json" qemu qboot e2fsprogs kernel ubuntu task git-lfs buildkit +# QEMU, qboot and the kernel are built with this repository's patches: each is in its component's +# pedigree, so the SBOM does not say upstream's source is what was compiled. +sbom pins -name sources -version "${VERSION}" -out "${PARTS}/sources.cdx.json" \ + -patches qemu=qemu/patches -patches qboot=qemu/qboot -patches kernel="${OUTPUT_DIR}/kernel/patches" \ + qemu qboot e2fsprogs kernel ubuntu task git-lfs buildkit # The files themselves, by their hashes: the firmware is the QEMU tarball's own build of it. for f in "${SHARE}"/bin/qemu-system-x86_64 "${SHARE}"/bin/qemu-system-x86_64-tcg "${SHARE}"/bin/qemu-img \ "${SHARE}"/qemu/bios.bin "${SHARE}"/qemu/bios-256k.bin "${SHARE}"/qemu/pvh.bin "${SHARE}"/qemu/kvmvapic.bin "${SHARE}"/qemu/efi-virtio.rom; do diff --git a/qemu/Dockerfile b/qemu/Dockerfile index e7b79a7..4939ac6 100644 --- a/qemu/Dockerfile +++ b/qemu/Dockerfile @@ -89,17 +89,12 @@ ARG QEMU_CONFIG_REV=6 SHELL ["/bin/sh", "-o", "pipefail", "-c"] -# Every -static package is load-bearing: with --static, a dependency present only as a .so -# fails at link time after the whole tree has compiled. liburing and libaio ship their .a -# inside the -dev package rather than a separate one, which is why they look inconsistent. +# qemu/apk.list says what, and why; qemu/apk.lock the version of everything installed, so the +# libraries a static QEMU links are a commit's and not the mirror's of the day. A version the +# mirror no longer has fails here: task apk:lock moves the lock, which is a new machine. +COPY qemu/apk.list qemu/apk.lock /tmp/apk/ RUN --mount=type=cache,sharing=locked,id=qemu-apk,target=/var/cache/apk \ - apk add --cache-dir /var/cache/apk \ - build-base meson ninja pkgconf python3 py3-setuptools bash coreutils curl xz \ - linux-headers bison flex perl git acpica \ - glib-dev glib-static pcre2-dev pcre2-static libffi-dev \ - pixman-dev pixman-static zlib-dev zlib-static \ - libseccomp-dev libseccomp-static libcap-ng-dev libcap-ng-static \ - liburing-dev libaio-dev gettext-dev gettext-static + apk add --cache-dir /var/cache/apk $(grep -v '^#' /tmp/apk/apk.list) $(cat /tmp/apk/apk.lock) WORKDIR /build diff --git a/qemu/apk.list b/qemu/apk.list new file mode 100644 index 0000000..ec9e8c5 --- /dev/null +++ b/qemu/apk.list @@ -0,0 +1,12 @@ +# What QEMU's builder installs, each package at the version qemu/apk.lock pins (task apk:lock). +# +# Every -static package is load-bearing: with --static, a dependency present only as a .so +# fails at link time after the whole tree has compiled. liburing and libaio ship their .a +# inside the -dev package rather than a separate one, which is why they look inconsistent. +# samurai is Alpine's ninja: the package that provides the name, which the lock pins. +build-base meson samurai pkgconf python3 py3-setuptools bash coreutils curl xz +linux-headers bison flex perl git acpica +glib-dev glib-static pcre2-dev pcre2-static libffi-dev +pixman-dev pixman-static zlib-dev zlib-static +libseccomp-dev libseccomp-static libcap-ng-dev libcap-ng-static +liburing-dev libaio-dev gettext-dev gettext-static diff --git a/qemu/apk.lock b/qemu/apk.lock new file mode 100644 index 0000000..d605a38 --- /dev/null +++ b/qemu/apk.lock @@ -0,0 +1,150 @@ +acl-libs=2.3.2-r1 +acpica=20250807-r0 +alpine-baselayout-data=3.7.2-r1 +alpine-baselayout=3.7.2-r1 +alpine-keys=2.6-r0 +alpine-release=3.24.2-r0 +apk-tools=3.0.8-r0 +bash=5.3.9-r1 +binutils=2.45.1-r1 +bison=3.8.2-r3 +brotli-libs=1.2.0-r1 +bsd-compat-headers=0.7.2-r6 +build-base=0.5-r4 +busybox-binsh=1.37.0-r31 +busybox=1.37.0-r31 +bzip2-dev=1.0.8-r6 +c-ares=1.34.8-r0 +ca-certificates-bundle=20260909-r0 +coreutils-env=9.11-r0 +coreutils-fmt=9.11-r0 +coreutils-sha512sum=9.11-r0 +coreutils=9.11-r0 +curl=8.22.0-r0 +docbook-xml=4.5-r10 +docbook-xsl-nons=1.79.2-r13 +docbook-xsl-ns=1.79.2-r13 +docbook-xsl=1.79.2-r13 +file=5.47-r2 +flex=2.6.4-r8 +fortify-headers=3.0.1-r2 +g++=15.2.0-r5 +gcc=15.2.0-r5 +gdbm=1.26-r0 +gettext-asprintf=1.0-r0 +gettext-dev=1.0-r0 +gettext-envsubst=1.0-r0 +gettext-libs=1.0-r0 +gettext-static=1.0-r0 +gettext=1.0-r0 +git-init-template=2.54.0-r0 +git-perl=2.54.0-r0 +git=2.54.0-r0 +glib-dev=2.88.1-r1 +glib-static=2.88.1-r1 +glib=2.88.1-r1 +gmp=6.3.0-r4 +isl26=0.26-r2 +jansson=2.15.0-r0 +libaio-dev=0.3.113-r2 +libaio=0.3.113-r2 +libapk=3.0.8-r0 +libatomic=15.2.0-r5 +libattr=2.5.2-r2 +libblkid=2.42.3-r1 +libbz2=1.0.8-r6 +libcap-ng-dev=0.8.5-r2 +libcap-ng-static=0.8.5-r2 +libcap-ng=0.8.5-r2 +libcrypto3=3.5.8-r0 +libcurl=8.22.0-r0 +libeconf=0.8.3-r0 +libedit-dev=20260508.3.1-r1 +libedit=20260508.3.1-r1 +libexpat=2.8.5-r0 +libfdisk=2.42.3-r1 +libffi-dev=3.5.2-r1 +libffi=3.5.2-r1 +libformw=6.6_p20260516-r0 +libgcc-static=15.2.0-r5 +libgcc=15.2.0-r5 +libgomp=15.2.0-r5 +libidn2=2.3.8-r0 +libintl=1.0-r0 +liblastlog2=2.42.3-r1 +libmagic=5.47-r2 +libmenuw=6.6_p20260516-r0 +libmount=2.42.3-r1 +libncurses++=6.6_p20260516-r0 +libncursesw=6.6_p20260516-r0 +libpanelw=6.6_p20260516-r0 +libpcre2-16=10.49-r0 +libpcre2-32=10.49-r0 +libpsl=0.21.5-r3 +libseccomp-dev=2.6.1-r0 +libseccomp-static=2.6.1-r0 +libseccomp=2.6.1-r0 +libsmartcols=2.42.3-r1 +libssl3=3.5.8-r0 +libstdc++-dev=15.2.0-r5 +libstdc++=15.2.0-r5 +libunistring=1.4.2-r0 +liburing-dev=2.14-r0 +liburing-ffi=2.14-r0 +liburing=2.14-r0 +libuuid=2.42.3-r1 +libxml2-utils=2.13.9-r2 +libxml2=2.13.9-r2 +libxslt=1.1.43-r3 +linux-headers=7.0.0-r1 +linux-pam=1.7.1-r2 +m4=1.4.20-r1 +make=4.4.1-r4 +meson-pyc=1.11.1-r0 +meson=1.11.1-r0 +mpc1=1.3.1-r1 +mpdecimal=4.0.1-r0 +mpfr4=4.2.2-r0 +musl-dev=1.2.6-r2 +musl-utils=1.2.6-r2 +musl=1.2.6-r2 +ncurses-dev=6.6_p20260516-r0 +ncurses-terminfo-base=6.6_p20260516-r0 +nghttp2-libs=1.70.0-r0 +patch=2.8-r0 +pcre2-dev=10.49-r0 +pcre2-static=10.49-r0 +pcre2=10.49-r0 +perl-error=0.17030-r0 +perl-git=2.54.0-r0 +perl=5.42.2-r1 +pixman-dev=0.46.4-r0 +pixman-static=0.46.4-r0 +pixman=0.46.4-r0 +pkgconf=2.5.1-r0 +py3-packaging-pyc=26.2-r0 +py3-packaging=26.2-r0 +py3-parsing-pyc=3.3.2-r1 +py3-parsing=3.3.2-r1 +py3-setuptools-pyc=82.0.1-r1 +py3-setuptools=82.0.1-r1 +pyc=3.14.8-r0 +python3-pyc=3.14.8-r0 +python3-pycache-pyc0=3.14.8-r0 +python3=3.14.8-r0 +readline=8.3.3-r1 +samurai=1.2-r8 +scanelf=1.3.9-r1 +skalibs-libs=2.15.0.0-r0 +sqlite-dev=3.53.4-r0 +sqlite-libs=3.53.4-r0 +sqlite=3.53.4-r0 +ssl_client=1.37.0-r31 +util-linux-dev=2.42.3-r1 +utmps-libs=0.1.3.3-r0 +xz-libs=5.8.4-r0 +xz=5.8.4-r0 +zlib-dev=1.3.2-r1 +zlib-static=1.3.2-r1 +zlib=1.3.2-r1 +zstd-libs=1.5.7-r2 From 91199f007fb53c14e7feb06c1b8bed4464472f80 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Tue, 13 Oct 2026 13:47:19 -0300 Subject: [PATCH 2/2] apklock: a half record beside a whole one is refused Co-Authored-By: Claude Opus 5.5 (1M context) --- hack/apklock/main_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hack/apklock/main_test.go b/hack/apklock/main_test.go index 44d6717..65ec341 100644 --- a/hack/apklock/main_test.go +++ b/hack/apklock/main_test.go @@ -18,7 +18,12 @@ func TestALockIsEveryInstalledPackage(t *testing.T) { if err != nil || got != "glib-static=2.88.1-r1\nzlib-static=1.3.1-r2\n" { t.Fatalf("lock %q, %v", got, err) } - for name, db := range map[string]string{"no version": "P:glib\n", "no name": "V:1\n", "nothing": "\n"} { + for name, db := range map[string]string{ + "no version": "P:glib\n", + "no name": "V:1\n", + "nothing": "\n", + "one whole, one half": "P:musl\nV:1.2.5-r10\n\nP:glib\n", + } { if _, err := Lock(db); err == nil { t.Errorf("%s: locked", name) }