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..65ec341 --- /dev/null +++ b/hack/apklock/main_test.go @@ -0,0 +1,79 @@ +// 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", + "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) + } + } +} + +// 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