diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 2baf3cae6..363e65c02 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -14,6 +14,17 @@ code. - **Hyprland** — BSD-3-Clause; the reviewed v0.56.1 source and rounded-border coverage backport are pinned in `guest/spec.json`. The guest package retains Hyprland's upstream license and dependency metadata. +- **aquamarine** — BSD-3-Clause; Arch Linux ARM currently publishes only + `libaquamarine.so=14`, while the pinned Hyprland still requires `.so=13`. The + factory therefore rebuilds `aquamarine 0.14.0-2` from the reviewed Arch + PKGBUILD and `hyprwm/aquamarine` v0.14.0 tarball (pinned in `guest/spec.json`) + for factory builds. The finished guest holds aquamarine and Hyprtoolkit on + `IgnorePkg` with Hyprland so updates cannot mix incompatible ABIs. +- **Hyprtoolkit** — BSD-3-Clause; the factory rebuilds the reviewed + `hyprwm/hyprtoolkit` v0.5.4 source using the Arch 0.5.4-6 recipe adapted for + aarch64 and package release 6.1, linked against aquamarine 0.14. Source, + recipe, packaging commit, and library hashes are pinned in `guest/spec.json`; + the package retains its upstream license. - **Glaze** — MIT; the pinned v7.2.0 headers are used by the Hyprland build, and their verified upstream license is retained in the rebuilt guest package. - **ANGLE, VirGLRenderer, libepoxy, SDL, libslirp, GLib, Pixman, and other QEMU diff --git a/docs/architecture.md b/docs/architecture.md index 690a23555..1d6e6ac0e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -165,7 +165,15 @@ creates the account on first boot. - The guest normally consumes upstream Arch Linux ARM packages. Hyprland is the documented exception: an upstream package is reproducibly rebuilt with a guarded rounded-border coverage patch for the VM graphics path, then held in - the guest's immutable local repository. + the guest's immutable local repository. While that pin still needs + `libaquamarine.so=13`, the factory rebuilds `aquamarine 0.14.0-2` from the + reviewed Arch PKGBUILD and upstream tarball, then rebuilds Hyprtoolkit + against that library. Both packages are provided by the disposable builder + repository and held alongside Hyprland on guest `IgnorePkg`; mixing the + newer mirror Hyprtoolkit with the older aquamarine cannot resolve. Source + and library hashes are verified, and build paths are remapped for repeatable + output. The ABI builder must pass from an empty cache before refreshing the + transaction lock. - The final Arch Linux ARM pacman files live under `/usr/share/try-omarchy/`. An Omarchy-supported `pre-refresh-pacman` hook restores them after a channel refresh writes its x86_64 templates to `/etc`; the upstream templates remain diff --git a/guest/Containerfile b/guest/Containerfile index e5a73a9ef..a50714db2 100644 --- a/guest/Containerfile +++ b/guest/Containerfile @@ -32,7 +32,7 @@ COPY --from=bootstrapper /rootfs/ / RUN pacman-key --init && pacman-key --populate archlinuxarm && \ pacman -Syu --noconfirm && \ - pacman -S --needed --noconfirm arch-install-scripts e2fsprogs git python rust=1:1.98.0-1 zstd && \ + pacman -S --needed --noconfirm arch-install-scripts e2fsprogs git python rust=1:1.98.1-1 zstd && \ pacman -Scc --noconfirm WORKDIR /workspace diff --git a/guest/build.sh b/guest/build.sh index aa5dc796b..0f5f34dac 100755 --- a/guest/build.sh +++ b/guest/build.sh @@ -91,6 +91,7 @@ fi root=$(mktemp -d "$work/rootfs.XXXXXX") resolution_db=$(mktemp -d "$work/pacman-db.XXXXXX") pinned_repo="" +abi_pin_repo="" chmod 0755 "$resolution_db" cleanup() { if (( keep_rootfs )); then @@ -102,6 +103,9 @@ cleanup() { if [[ -n $pinned_repo ]]; then rm -rf "$pinned_repo" fi + if [[ -n $abi_pin_repo ]]; then + rm -rf "$abi_pin_repo" + fi } trap cleanup EXIT @@ -162,30 +166,35 @@ if ((${#pinned_records[@]})); then "$pinned_repo/"*.pkg.tar.zst >/dev/null fi -# Preserve the pinned repository configuration exactly, adding only builder -# options. The generated file is temporary build input; configure-rootfs later -# installs Omarchy's unmodified pacman configuration into the guest. -options_sections=0 -pinned_repo_inserted=0 -while IFS= read -r line || [[ -n $line ]]; do - if [[ -n $pinned_repo && $line =~ ^\[[^]]+\]$ && $line != "[options]" && $pinned_repo_inserted == 0 ]]; then - printf '[try-omarchy-pinned-cache]\n' - printf 'SigLevel = Required DatabaseOptional\n' - printf 'Server = file://%s\n\n' "$pinned_repo" - pinned_repo_inserted=1 - fi - printf '%s\n' "$line" - if [[ $line == "[options]" ]]; then - printf 'CacheDir = %s\n' "$package_cache" - if [[ ${OMARCHY_PACMAN_DISABLE_SANDBOX:-0} == "1" ]]; then - printf 'DisableSandbox\n' - fi - options_sections=$((options_sections + 1)) - fi -done <"$upstream_pacman_config" >"$pacman_config" -(( options_sections == 1 )) || fail "pinned pacman configuration must contain one [options] section" -(( ${#pinned_records[@]} == 0 || pinned_repo_inserted == 1 )) \ - || fail "pinned pacman configuration does not contain a repository section" +# Guest pacman.conf is installed unchanged by configure-rootfs. The builder copy +# may add reviewed ABI pins (packages mirrors no longer publish) and must drop +# those names from IgnorePkg so the empty-root transaction can install them once. +abi_pin_count=$(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1])).get("inputs", {}).get("abiPackagePins", [])))' "$spec") +builder_conf_args=( + python3 "$guest_dir/scripts/write-builder-pacman-conf.py" + --spec "$spec" + --guest-dir "$guest_dir" + --guest-config "$upstream_pacman_config" + --package-lock "$package_lock_file" + --output "$pacman_config" + --package-cache "$package_cache" +) +if [[ ${OMARCHY_PACMAN_DISABLE_SANDBOX:-0} == "1" ]]; then + builder_conf_args+=(--disable-sandbox) +fi +if (( abi_pin_count > 0 )); then + abi_pin_repo=$(mktemp -d "$work/abi-pin-repo.XXXXXX") + "$guest_dir/scripts/build-pinned-abi-packages.sh" \ + --spec "$spec" \ + --guest-dir "$guest_dir" \ + --output-repo "$abi_pin_repo" \ + --work "$work" || fail "could not rebuild the reviewed ABI pins" + builder_conf_args+=(--abi-repo "$abi_pin_repo") +fi +if [[ -n $pinned_repo ]]; then + builder_conf_args+=(--pinned-cache-repo "$pinned_repo") +fi +"${builder_conf_args[@]}" || fail "could not derive the factory builder pacman configuration" # Resolve against an empty target database and require the reviewed transitive # version lock before any multi-gigabyte package transaction begins. diff --git a/guest/packages.lock.json b/guest/packages.lock.json index b5d1dd80b..0f1eec7a8 100644 --- a/guest/packages.lock.json +++ b/guest/packages.lock.json @@ -4,7 +4,7 @@ "abseil-cpp": "20260817.0-2", "acl": "2.4.0-1", "adwaita-cursors": "50.0-1", - "adwaita-fonts": "50.0-1", + "adwaita-fonts": "51.0-2", "adwaita-icon-theme": "50.0-1", "adwaita-icon-theme-legacy": "46.2-3", "alsa-card-profiles": "1:1.6.8-1", @@ -14,7 +14,7 @@ "aom": "3.15.0-1", "appstream": "1.2.0-1", "aquamarine": "0.14.0-2", - "archlinux-keyring": "20260727-1", + "archlinux-keyring": "20260909-1", "archlinuxarm-keyring": "20240419-2", "at-spi2-core": "2.60.6-1", "attr": "2.6.0-1", @@ -36,11 +36,11 @@ "ca-certificates-utils": "20240618-1", "cairo": "1.18.4-1", "cfitsio": "1:4.7.0-1", - "chromium": "152.0.7977.64-1", + "chromium": "152.0.7977.75-1", "coreutils": "9.11-2", "cpptrace": "1.0.4-2", - "cryptsetup": "2.8.7-1", - "curl": "8.21.0-1", + "cryptsetup": "2.8.8-1", + "curl": "8.22.0-1", "dav1d": "1.5.4-1", "db5.3": "5.3.28-7", "dbus": "1.16.2-1", @@ -60,12 +60,12 @@ "elfutils": "0.196-1", "exempi": "2.6.6-3", "exiv2": "0.28.9-1", - "expat": "2.8.3-1", + "expat": "2.8.4-1", "eza": "0.23.5-2", "fakeroot": "1:1.37.2-3", - "fastfetch": "2.67.1-1", + "fastfetch": "2.68.1-1", "fcft": "3.3.3-1", - "fd": "10.5.0-2", + "fd": "10.5.0-3", "ffmpeg": "2:9.0.1-4", "fftw": "3.3.11-1", "file": "5.48-1", @@ -73,7 +73,7 @@ "findutils": "4.11.0-1", "flac": "1.5.0-1", "fontconfig": "2:2.18.3-2", - "foot": "1.27.0-1", + "foot": "1.28.0-2", "freeglut": "3.8.0-1", "freetype2": "2.14.3-1", "fribidi": "1.0.16-2", @@ -101,17 +101,17 @@ "glycin": "2.1.5-2", "glycin-gtk4": "2.1.5-2", "gmp": "6.3.0-3", - "gnome-autoar": "0.5.1-1", + "gnome-autoar": "0.5.2-1", "gnome-desktop-4": "1:44.5-1", "gnome-desktop-common": "1:44.5-1", "gnome-keyring": "1:50.0-1", "gnulib-l10n": "20241231-1", "gnupg": "2.4.9-3", - "gnutls": "3.8.13-2", + "gnutls": "3.8.13-3", "gobject-introspection-runtime": "1.86.0-2", "gperftools": "2.18.1-1", - "gpgme": "2.1.2-1", - "gpgmepp": "2.1.0-1", + "gpgme": "2.2.0-1", + "gpgmepp": "2.2.0-1", "gpm": "1.20.7.r38.ge82d1a6-6", "graphene": "1.10.8-2.1", "graphite": "1:1.3.15-1", @@ -121,9 +121,9 @@ "gsettings-system-schemas": "50.1-1", "gsm": "1.0.24-1", "gssdp": "1.6.6-1", - "gst-plugins-bad-libs": "1.28.6-3", - "gst-plugins-base-libs": "1.28.6-3", - "gstreamer": "1.28.6-3", + "gst-plugins-bad-libs": "1.28.7-1", + "gst-plugins-base-libs": "1.28.7-1", + "gstreamer": "1.28.7-1", "gtest": "1.18.0-1", "gtk-update-icon-cache": "1:4.22.4-1", "gtk3": "1:3.24.52-1", @@ -140,22 +140,22 @@ "hicolor-icon-theme": "0.18-1", "hidapi": "0.15.0-1", "highway": "1.4.0-1", - "hwdata": "0.410-1", + "hwdata": "0.411-1", "hwloc": "2.14.0-1", "hyprcursor": "0.1.13-7", "hyprgraphics": "0.5.1-4", "hyprland": "0.56.1-3", "hyprland-guiutils": "0.2.2-2", "hyprlang": "0.6.8-5", - "hyprpicker": "0.4.7-3", + "hyprpicker": "0.4.7-4", "hyprsunset": "0.4.0-3", - "hyprtoolkit": "0.5.4-4", - "hyprutils": "0.14.1-1", + "hyprtoolkit": "0.5.4-6.1", + "hyprutils": "0.14.2-1", "hyprwayland-scanner": "0.4.6-1", "hyprwire": "0.3.1-3", "iana-etc": "20260617-1", "icu": "78.3-1", - "imagemagick": "7.1.2.30-1", + "imagemagick": "7.1.2.31-1", "imath": "3.2.3-1", "imv": "5.0.1-2", "iniparser": "4.2.6-2", @@ -179,7 +179,7 @@ "l-smash": "2.14.5-4", "lame": "4.0-1", "lcms2": "2.19.1-1", - "leancrypto": "1.8.0-1", + "leancrypto": "1.9.0-1", "less": "1:704-1", "libadwaita": "1:1.9.3-1", "libaec": "1.1.7-1", @@ -211,14 +211,14 @@ "libcap-ng": "0.9.5-1", "libcdio": "2.4.0-1", "libcdio-paranoia": "10.2+2.0.2-3", - "libcgif": "0.5.3-1", + "libcgif": "0.5.4-1", "libcloudproviders": "0.4.1-1", "libcolord": "1.4.8-1", "libcue": "2.3.0-1", "libcups": "2:2.4.19-1", "libdaemon": "0.14-6", "libdatrie": "0.2.14-1", - "libde265": "1.1.1-1", + "libde265": "1.1.2-1", "libdecor": "0.2.5-1", "libdeflate": "1.26-1", "libdisplay-info": "0.3.0-1", @@ -251,7 +251,7 @@ "libgsf": "1.14.58-1", "libgudev": "238-3", "libgxps": "0.3.2-5", - "libheif": "1.23.2-1", + "libheif": "1.23.4-1", "libice": "1.1.2-1", "libidn2": "2.3.8-1", "libiec61883": "1.2.0-9", @@ -280,12 +280,12 @@ "libmodplug": "0.8.9.0-7", "libmpc": "1.4.1-1", "libmysofa": "1.3.5-1", - "libnautilus-extension": "50.3-1", + "libnautilus-extension": "50.3.1-1", "libndp": "1.9-1", "libnetfilter_conntrack": "1.1.1-1", "libnewt": "0.52.25-2", "libnfnetlink": "1.0.2-2", - "libnftnl": "1.3.1-1", + "libnftnl": "1.3.2-1", "libnghttp2": "1.70.0-1", "libnghttp3": "1.18.0-1", "libngtcp2": "1.25.0-1", @@ -302,7 +302,7 @@ "libopenmpt": "0.8.9-1", "libosinfo": "1.12.0-3", "libp11-kit": "0.26.5-1", - "libpcap": "1.10.6-1", + "libpcap": "1.10.7-1", "libpciaccess": "0.19-1", "libpgm": "5.3.128-4", "libpipewire": "1:1.6.8-1", @@ -346,7 +346,7 @@ "libubsan": "16.1.1+r12+g301eb08fa2c5-1", "libunibreak": "7.0-1", "libunistring": "1.4.2-1", - "libunwind": "1.8.2-1", + "libunwind": "1.8.3-1", "liburing": "2.15-1", "libusb": "1.0.30-1", "libusbmuxd": "2.1.1-2", @@ -360,9 +360,9 @@ "libvorbis": "1.3.7-4", "libvpx": "1.17.0-1", "libvterm": "0.3.3-2", - "libwacom": "2.19.1-1", + "libwacom": "2.20.0-1", "libwebp": "1.6.0-2", - "libwireplumber": "0.5.16-1", + "libwireplumber": "0.5.17-1", "libx11": "1.8.13-1", "libxau": "1.0.12-1", "libxcb": "1.17.0-1", @@ -381,7 +381,7 @@ "libxkbcommon": "1.13.2-1", "libxkbcommon-x11": "1.13.2-1", "libxkbfile": "1.2.0-1", - "libxml2": "2.15.3-1", + "libxml2": "2.15.4-1", "libxmlb": "0.3.29-1", "libxmu": "1.3.1-1", "libxpresent": "1.0.2-1", @@ -398,8 +398,8 @@ "libzip": "1.11.4-1", "licenses": "20240728-1", "lilv": "0.28.0-1", - "linux-aarch64": "7.2.2-2", - "linux-aarch64-headers": "7.2.2-2", + "linux-aarch64": "7.2.3-2", + "linux-aarch64-headers": "7.2.3-2", "linux-api-headers": "7.2-1", "llhttp": "9.3.1-1", "llvm-libs": "22.1.8-2", @@ -409,14 +409,14 @@ "lua": "5.5.1-1", "lua51-lpeg": "1.1.0-5", "lua54": "5.4.9-1", - "luajit": "2.1.1787165859+1ee778a-1", + "luajit": "2.1.1788856981+c6ffc14-1", "lv2": "1.18.10-2", "lz4": "1:1.10.0-2", "lzo": "2.10-5", "make": "4.4.1-3", "md4c": "0.5.3-1", "mdadm": "4.6-2", - "mesa": "1:26.2.1-1", + "mesa": "1:26.2.2-1", "minizip": "1:1.3.2-3", "mkinitcpio": "41.1-1", "mkinitcpio-busybox": "1.36.1-1", @@ -429,12 +429,12 @@ "mtdev": "1.1.7-1", "mujs": "1.3.9-1", "muparser": "2.3.5-2", - "nautilus": "50.3-1", + "nautilus": "50.3.1-1", "ncurses": "6.6-2", "neovim": "0.12.5-1", "nettle": "4.0-1", "networkmanager": "1.58.1-1", - "nftables": "1:1.1.6-3", + "nftables": "1:1.1.7-3", "noto-fonts": "1:2026.09.01-1", "noto-fonts-emoji": "1:2.051-1", "npth": "1.8-1", @@ -466,7 +466,7 @@ "patch": "2.8-1", "pciutils": "3.15.0-1", "pcre": "8.45-5", - "pcre2": "10.47-1", + "pcre2": "10.48-1", "pcsclite": "2.5.1-1", "perl": "5.42.2-2", "perl-error": "0.17030-3", @@ -507,8 +507,8 @@ "sbc": "2.2-1", "sdbus-cpp": "2.3.1-1", "sddm": "0.21.0-7", - "sdl2-compat": "2.32.70-1", - "sdl3": "3.4.14-1", + "sdl2-compat": "2.32.72-1", + "sdl3": "3.4.16-1", "seatd": "0.9.3-1", "sed": "4.10-2", "serd": "0.32.10-1", @@ -557,22 +557,22 @@ "unibilium": "2.1.4-1", "unzip": "6.0-23", "upower": "1.91.3-1", - "util-linux": "2.42.2-1", - "util-linux-libs": "2.42.2-1", + "util-linux": "2.42.3-1", + "util-linux-libs": "2.42.3-1", "uwsm": "0.26.7-1", "v4l-utils": "1.32.0-2", "v4l2loopback-dkms": "0.15.4-2", - "vapoursynth": "77-2", - "vid.stab": "1.1.1-3", + "vapoursynth": "79-1", + "vid.stab": "1.1.2-1", "vmaf": "3.2.0-1", "volume_key": "0.3.12-12", "vulkan-icd-loader": "1.4.357.0-1", - "vulkan-mesa-implicit-layers": "1:26.2.1-1", - "vulkan-swrast": "1:26.2.1-1", + "vulkan-mesa-implicit-layers": "1:26.2.2-1", + "vulkan-swrast": "1:26.2.2-1", "wayland": "1.26.0-1", "wayland-protocols": "1.49-1", "webrtc-audio-processing-1": "1.3-5", - "wireplumber": "0.5.16-1", + "wireplumber": "0.5.17-1", "wl-clipboard": "1:2.3.0-1", "woff2-font-awesome": "7.3.1-1", "wpa_supplicant": "2:2.12-1", @@ -588,7 +588,7 @@ "xcb-util-wm": "0.4.2-2", "xdg-desktop-portal": "1.22.1-2", "xdg-desktop-portal-gtk": "1.15.3-1", - "xdg-desktop-portal-hyprland": "1.4.1-1", + "xdg-desktop-portal-hyprland": "1.4.1-2", "xdg-user-dirs": "0.20-1", "xdg-user-dirs-gtk": "0.16-1", "xdg-utils": "1.2.1-2", diff --git a/guest/pacman.aarch64.conf b/guest/pacman.aarch64.conf index d18f2f4b2..c983494cd 100644 --- a/guest/pacman.aarch64.conf +++ b/guest/pacman.aarch64.conf @@ -10,10 +10,10 @@ HoldPkg = pacman glibc # QEMU direct-boots the kernel and initramfs paired with this VM disk. Keep its # installed modules and DKMS headers on that exact ABI. Hyprland is likewise # held until upstream includes our rounded-border coverage backport; otherwise -# a routine full-system update can silently restore the defect. Keep aquamarine -# on the same hold: newer ALARM aquamarine bumps libaquamarine.so past what the -# pinned Hyprland was built against and aborts Omarchy's updater mid-transaction. -IgnorePkg = linux-aarch64 linux-aarch64-headers hyprland aquamarine +# a routine full-system update can silently restore the defect. Hold the +# compatible aquamarine/Hyprtoolkit pair too: newer ALARM builds require a +# different libaquamarine.so and abort the updater with mixed ABI requirements. +IgnorePkg = linux-aarch64 linux-aarch64-headers hyprland aquamarine hyprtoolkit Architecture = auto CheckSpace ParallelDownloads = 5 diff --git a/guest/pinned-packages/README.md b/guest/pinned-packages/README.md new file mode 100644 index 000000000..d171eb80b --- /dev/null +++ b/guest/pinned-packages/README.md @@ -0,0 +1,20 @@ +# Reviewed ABI package pins + +Recipes here are factory-build inputs for the compatible Hyprland dependency +set that Arch Linux ARM's rolling repositories no longer provide together. + +- `aquamarine/PKGBUILD` adapts Arch `0.14.0-2` packaging for aarch64. +- `hyprtoolkit/PKGBUILD` adapts Arch `0.5.4-6` packaging for aarch64 and uses + package release `6.1` to distinguish the rebuild against aquamarine 0.14. + +Packaging commits, recipe digests, upstream tarball digests, and reproduced +library digests are pinned in `guest/spec.json`. The factory builds aquamarine +first, installs that verified result in the disposable builder, then builds +Hyprtoolkit against `libaquamarine.so=13`. Random build paths are remapped out +of compiler output. Both package metadata and library digests are checked +before publishing the temporary `[try-omarchy-abi-pins]` repository. + +The finished guest never receives that builder repository. It holds aquamarine +and Hyprtoolkit alongside the patched Hyprland on `IgnorePkg`. Remove these +holds together only when the patched compositor and its dependency set have +been validated against a newer ABI. diff --git a/guest/pinned-packages/aquamarine/PKGBUILD b/guest/pinned-packages/aquamarine/PKGBUILD new file mode 100644 index 000000000..5b39b3c75 --- /dev/null +++ b/guest/pinned-packages/aquamarine/PKGBUILD @@ -0,0 +1,44 @@ +# Maintainer: Caleb Maclennan + +pkgname=aquamarine +pkgver=0.14.0 +pkgrel=2 +pkgdesc='a very light linux rendering backend library' +arch=(x86_64 aarch64) +url="https://github.com/hyprwm/$pkgname" +license=(BSD-3-Clause) +depends=(libgcc + libstdc++ + glibc # libc.so libm.so + hyprutils libhyprutils.so + hyprwayland-scanner + libdisplay-info libdisplay-info.so + libdrm # libdrm.so + libglvnd libEGL.so + libinput # libinput.so + mesa # libgbm.so + opengl-driver + pixman + seatd libseat.so + systemd-libs libudev.so + wayland libwayland-client.so + wayland-protocols) +makedepends=(cmake) +provides=("lib$pkgname.so") +_archive="$pkgname-$pkgver" +source=("$url/archive/v$pkgver/$_archive.tar.gz") +sha256sums=('5dcf0b17f7dd51539fd7e79d68484f04240b3b63cf9f5f21d5b6dea0088168f9') + +build() { + cd "$_archive" + cmake -B build \ + -D CMAKE_INSTALL_PREFIX=/usr \ + -D CMAKE_BUILD_TYPE=Release + cmake --build build +} + +package() { + cd "$_archive" + DESTDIR="$pkgdir" cmake --install build + install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname" LICENSE +} diff --git a/guest/pinned-packages/hyprtoolkit/PKGBUILD b/guest/pinned-packages/hyprtoolkit/PKGBUILD new file mode 100644 index 000000000..230cdbc67 --- /dev/null +++ b/guest/pinned-packages/hyprtoolkit/PKGBUILD @@ -0,0 +1,50 @@ +# Maintainer: Caleb Maclennan + +pkgname=hyprtoolkit +pkgver=0.5.4 +pkgrel=6.1 +pkgdesc='A modern C++ Wayland-native GUI toolkit' +arch=(x86_64 aarch64) +url="https://github.com/hyprwm/$pkgname" +license=(BSD-3-Clause) +depends=( + libgcc + libstdc++ + aquamarine libaquamarine.so + cairo libcairo.so + glib2 libglib-2.0.so + glibc # libc.so libm.so + hyprgraphics libhyprgraphics.so + hyprlang libhyprlang.so + hyprutils libhyprutils.so + iniparser libiniparser.so + libdrm # libdrm.so + libglvnd libEGL.so libOpenGL.so + libxkbcommon libxkbcommon.so + mesa # libgbm.so + pango libpango-1.0.so # libpango.so + pixman libpixman-1.so + wayland libwayland-client.so +) +makedepends=(cmake + hyprwayland-scanner) +provides=(libhyprtoolkit.so) +_archive="$pkgname-$pkgver" +source=("$url/archive/v$pkgver/$_archive.tar.gz") +sha256sums=('2fb59789f231c1c4e9154ceffc1e7524c0cae154807c0d57e6166806255b570f') + +build() { + cd "$_archive" + local cmake_options=( + -D CMAKE_BUILD_TYPE=None + -D CMAKE_INSTALL_PREFIX=/usr + ) + cmake -B build -W no-dev ${cmake_options[@]} + cmake --build build +} + +package() { + cd "$_archive" + DESTDIR="$pkgdir" cmake --install build + install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname/" LICENSE +} diff --git a/guest/scripts/build-pinned-abi-packages.sh b/guest/scripts/build-pinned-abi-packages.sh new file mode 100755 index 000000000..b869be626 --- /dev/null +++ b/guest/scripts/build-pinned-abi-packages.sh @@ -0,0 +1,313 @@ +#!/bin/bash + +# Rebuild the compatible aquamarine/Hyprtoolkit pair from verified source. +# Packages are exposed only through the disposable factory repository. +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: build-pinned-abi-packages.sh --spec FILE --guest-dir DIR --output-repo DIR [--work DIR] +USAGE +} + +fail() { + echo "build-pinned-abi-packages: $*" >&2 + exit 1 +} + +spec="" +guest_dir="" +output_repo="" +work="" + +while (($#)); do + case "$1" in + --spec) + spec=${2:-} + shift 2 + ;; + --guest-dir) + guest_dir=${2:-} + shift 2 + ;; + --output-repo) + output_repo=${2:-} + shift 2 + ;; + --work) + work=${2:-} + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + fail "unknown option: $1" + ;; + esac +done + +[[ -f $spec ]] || fail "spec not found: $spec" +[[ -d $guest_dir ]] || fail "guest directory not found: $guest_dir" +[[ -n $output_repo ]] || fail "--output-repo is required" +[[ $(uname -s) == Linux && $(uname -m) == aarch64 ]] \ + || fail "ABI pins must be built natively on aarch64 Linux" +(( EUID == 0 )) || fail "run as root so build dependencies can be installed" +for command in curl install makepkg pacman python3 repo-add runuser sha256sum tar useradd zstd; do + command -v "$command" >/dev/null || fail "$command is required" +done + +verify_file() { + local expected=$1 + local path=$2 + printf '%s %s\n' "$expected" "$path" | sha256sum -c - >/dev/null +} + +if [[ -z $work ]]; then + work=$(mktemp -d) + chmod 0755 "$work" + cleanup_work=1 +else + [[ $work == /* && -d $work ]] || fail "--work must be an absolute directory" + cleanup_work=0 +fi +stage="" +added_build_user=0 +cleanup() { + if (( added_build_user )); then + userdel abi-build >/dev/null 2>&1 || true + fi + if [[ -n $stage && -d $stage && $stage == "$work/"abi-package.* ]]; then + rm -rf -- "$stage" + fi + if (( cleanup_work )); then + rm -rf -- "$work" + fi +} +trap cleanup EXIT +install -d -m 0755 "$output_repo" +[[ -z $(ls -A "$output_repo") ]] || fail "output repository must be empty" + +# Install build dependencies before either ABI pin. Hyprtoolkit then builds +# against our verified aquamarine, never the incompatible mirror package. +pacman -S --needed --noconfirm \ + base-devel cmake hyprutils hyprwayland-scanner libdisplay-info libdrm libglvnd \ + libinput mesa pixman seatd systemd-libs wayland wayland-protocols \ + cairo glib2 hyprgraphics hyprlang iniparser libxkbcommon pango >/dev/null + +for name in aquamarine hyprtoolkit; do + case "$name" in + aquamarine) expected_version=0.14.0; expected_pkgrel=2 ;; + hyprtoolkit) expected_version=0.5.4; expected_pkgrel=6.1 ;; + esac +mapfile -t metadata < <(python3 - "$spec" "$guest_dir" "$name" <<'PY' +import json +import pathlib +import sys + +spec = json.loads(pathlib.Path(sys.argv[1]).read_text()) +guest = pathlib.Path(sys.argv[2]).resolve(strict=True) +pins = spec.get("inputs", {}).get("abiPackagePins") +if pins != [{"name": "aquamarine", "version": "0.14.0-2"}, {"name": "hyprtoolkit", "version": "0.5.4-6.1"}]: + raise SystemExit("abiPackagePins must contain the reviewed compatible pair") +component = spec["supplyChain"][sys.argv[3]] +required = ( + "version", + "pkgrel", + "repository", + "url", + "sha256", + "pkgbuild", + "pkgbuildSha256", + "packagingRepository", + "packagingCommit", + "license", + "binarySha256", +) +if set(component) != set(required): + raise SystemExit("ABI component keys are not the reviewed set") +pkgbuild = pathlib.PurePosixPath(component["pkgbuild"]) +if pkgbuild.is_absolute() or any(part in {"", ".", ".."} for part in pkgbuild.parts): + raise SystemExit("ABI PKGBUILD path escapes the guest directory") +candidate = (guest / pathlib.Path(*pkgbuild.parts)).resolve(strict=True) +if candidate == guest or guest not in candidate.parents or not candidate.is_file(): + raise SystemExit("ABI PKGBUILD path is invalid") +print(component["version"]) +print(component["pkgrel"]) +print(component["repository"]) +print(component["url"]) +print(component["sha256"]) +print(candidate) +print(component["pkgbuildSha256"]) +print(component["packagingRepository"]) +print(component["packagingCommit"]) +print(component["license"]) +print(component["binarySha256"]) +print(spec["image"]["sourceDateEpoch"]) +PY +) || fail "could not read pinned ABI metadata" +(( ${#metadata[@]} == 12 )) || fail "pinned ABI metadata is incomplete" + +version=${metadata[0]} +pkgrel=${metadata[1]} +repository=${metadata[2]} +url=${metadata[3]} +sha256=${metadata[4]} +pkgbuild_path=${metadata[5]} +pkgbuild_sha256=${metadata[6]} +packaging_repository=${metadata[7]} +packaging_commit=${metadata[8]} +license=${metadata[9]} +binary_sha256=${metadata[10]} +source_date_epoch=${metadata[11]} + +[[ $version == "$expected_version" ]] || fail "unexpected ABI component version: $version" +[[ $pkgrel == "$expected_pkgrel" ]] || fail "unexpected ABI component pkgrel: $pkgrel" +[[ $repository == "https://github.com/hyprwm/$name" ]] || fail "unexpected ABI component repository" +[[ $url == "$repository/archive/v$version/$name-$version.tar.gz" ]] || + fail "ABI source URL does not match the pinned release" +[[ $packaging_repository == "https://gitlab.archlinux.org/archlinux/packaging/packages/$name.git" ]] || + fail "unexpected ABI component packaging repository" +[[ $packaging_commit =~ ^[0-9a-f]{40}$ ]] || fail "invalid ABI packaging commit" +[[ $license == BSD-3-Clause ]] || fail "unexpected ABI component license: $license" +[[ $source_date_epoch =~ ^[0-9]+$ && $source_date_epoch -gt 0 ]] || fail "invalid source date epoch" +for digest in "$sha256" "$pkgbuild_sha256" "$binary_sha256"; do + [[ $digest =~ ^[0-9a-f]{64}$ ]] || fail "invalid ABI content digest" +done +[[ ! -L $pkgbuild_path ]] || fail "refusing symlinked ABI PKGBUILD" + +verify_file "$pkgbuild_sha256" "$pkgbuild_path" || fail "ABI PKGBUILD digest mismatch" +grep -F "sha256sums=('$sha256')" "$pkgbuild_path" >/dev/null || + fail "ABI PKGBUILD does not pin the reviewed source digest" +grep -F "pkgver=$version" "$pkgbuild_path" >/dev/null || fail "ABI PKGBUILD version mismatch" +grep -F "pkgrel=$pkgrel" "$pkgbuild_path" >/dev/null || fail "ABI PKGBUILD pkgrel mismatch" + +cache_dir="$work/$name-download-cache" +stage=$(mktemp -d "$work/abi-package.XXXXXX") +# makepkg runs as an unprivileged user and must traverse this parent. +chmod 0755 "$stage" +install -d -m 0755 "$cache_dir" +source_cache="$cache_dir/$name-$version.tar.gz" + +download_verified() { + local source_url=$1 + local expected=$2 + local destination=$3 + local temporary="" + + [[ ! -L $destination ]] || fail "refusing symlinked download cache entry: $destination" + if [[ -f $destination ]] && verify_file "$expected" "$destination"; then + return 0 + fi + rm -f -- "$destination" + temporary=$(mktemp "$cache_dir/.download.XXXXXX") + if ! curl --fail --location --proto '=https' --tlsv1.2 --silent --show-error \ + "$source_url" --output "$temporary"; then + rm -f -- "$temporary" + fail "download failed: $source_url" + fi + if ! verify_file "$expected" "$temporary"; then + rm -f -- "$temporary" + fail "download digest mismatch: $source_url" + fi + chmod 0644 "$temporary" + mv "$temporary" "$destination" +} + +download_verified "$url" "$sha256" "$source_cache" + +python3 - "$source_cache" "$name-$version" <<'PY' || fail "ABI source archive has an unsafe member set" +import pathlib +import sys +import tarfile + +archive = pathlib.Path(sys.argv[1]) +expected_root = sys.argv[2] +with tarfile.open(archive, "r:gz") as source: + members = source.getmembers() + if not members: + raise SystemExit("ABI source archive is empty") + for member in members: + name = member.name[:-1] if member.isdir() and member.name.endswith("/") else member.name + if not name or name.startswith("/") or ".." in pathlib.PurePosixPath(name).parts: + raise SystemExit(f"unsafe ABI source member: {member.name}") + roots = {pathlib.PurePosixPath(member.name).parts[0] for member in members if member.name} + if roots != {expected_root}: + raise SystemExit("ABI source archive has an unexpected member set") +PY + +if ! id -u abi-build >/dev/null 2>&1; then + useradd --system --create-home --home-dir "$stage/home" --shell /usr/bin/nologin \ + abi-build + added_build_user=1 +else + added_build_user=0 +fi +build_home=$(getent passwd abi-build | cut -d: -f6) +build_dir="$stage/build" +install -d -m 0755 -o abi-build -g abi-build "$build_dir" "$build_home" +install -m 0644 -o abi-build -g abi-build "$pkgbuild_path" "$build_dir/PKGBUILD" +install -m 0644 -o abi-build -g abi-build "$source_cache" \ + "$build_dir/$name-$version.tar.gz" + +runuser -u abi-build -- test -w "$build_dir" || fail "build user cannot access $build_dir" +makepkg_config="$stage/makepkg.conf" +cp /etc/makepkg.conf "$makepkg_config" +printf '\nCFLAGS+=" -ffile-prefix-map=%s=/usr/src/try-omarchy-%s"\nCXXFLAGS+=" -ffile-prefix-map=%s=/usr/src/try-omarchy-%s"\nOPTIONS+=(!debug)\nPKGEXT=\".pkg.tar.zst\"\n' \ + "$stage" "$name" "$stage" "$name" >> "$makepkg_config" + +export SOURCE_DATE_EPOCH="$source_date_epoch" +export PACKAGER='Try Omarchy factory ' +( + cd "$build_dir" + runuser -u abi-build -- env HOME="$build_home" SOURCE_DATE_EPOCH="$source_date_epoch" \ + PACKAGER="$PACKAGER" CMAKE_BUILD_PARALLEL_LEVEL=4 \ + makepkg --config "$makepkg_config" --noconfirm --skippgpcheck +) + +package_archive="$build_dir/$name-$version-$pkgrel-aarch64.pkg.tar.zst" +[[ -f $package_archive ]] || fail "makepkg did not produce $package_archive" + +python3 - "$package_archive" "$binary_sha256" "$name" "$version" "$pkgrel" <<'PY' || fail "ABI package provenance mismatch" +import hashlib +import io +import subprocess +import sys +import tarfile + +archive = sys.argv[1] +expected = sys.argv[2] +name, version, pkgrel = sys.argv[3:] +raw = subprocess.check_output(["zstd", "-d", "-c", archive]) +with tarfile.open(fileobj=io.BytesIO(raw), mode="r:") as package: + info = package.extractfile(".PKGINFO") + if info is None: + raise SystemExit("ABI package is missing .PKGINFO") + pkginfo = info.read().decode() + if f"pkgname = {name}\n" not in pkginfo or f"pkgver = {version}-{pkgrel}\n" not in pkginfo or "arch = aarch64\n" not in pkginfo: + raise SystemExit("ABI package identity mismatch") + abi = "provides = libaquamarine.so=13-64" if name == "aquamarine" else "depend = libaquamarine.so=13-64" + if abi not in pkginfo: + raise SystemExit("ABI package does not provide or depend on libaquamarine.so=13") + member = package.extractfile(f"usr/lib/lib{name}.so.{version}") + if member is None: + raise SystemExit("ABI package is missing its versioned library") + digest = hashlib.sha256(member.read()).hexdigest() + if digest != expected: + raise SystemExit(f"{name} reproducible library digest mismatch: {digest}") +PY + +install -m 0644 "$package_archive" "$output_repo/$name-$version-$pkgrel-aarch64.pkg.tar.zst" +if [[ $name == aquamarine ]]; then + pacman -U --needed --noconfirm "$package_archive" >/dev/null +fi +if (( added_build_user )); then + userdel abi-build + added_build_user=0 +fi +rm -rf -- "$stage" +stage="" +echo "Rebuilt $name $version-$pkgrel from $packaging_commit (library $binary_sha256)" +done +repo-add "$output_repo/try-omarchy-abi-pins.db.tar.gz" "$output_repo/"*.pkg.tar.zst >/dev/null diff --git a/guest/scripts/refresh-package-lock.sh b/guest/scripts/refresh-package-lock.sh index d3de68181..cd5a029c5 100755 --- a/guest/scripts/refresh-package-lock.sh +++ b/guest/scripts/refresh-package-lock.sh @@ -73,10 +73,31 @@ chmod 0755 "$temporary" mkdir -p "$temporary/db" chmod 0755 "$temporary/db" config="$temporary/pacman.conf" -cp "$upstream_pacman_config" "$config" +package_lock_input=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["inputs"]["packageLock"])' "$spec") +package_lock_file="$guest_dir/$package_lock_input" +[[ -f $package_lock_file ]] || fail "package lock not found: $package_lock_file" + +builder_conf_args=( + python3 "$script_dir/write-builder-pacman-conf.py" + --spec "$spec" + --guest-dir "$guest_dir" + --guest-config "$upstream_pacman_config" + --package-lock "$package_lock_file" + --output "$config" +) if [[ ${OMARCHY_PACMAN_DISABLE_SANDBOX:-0} == "1" ]]; then - sed -i '/^\[options\]$/a DisableSandbox' "$config" + builder_conf_args+=(--disable-sandbox) +fi +abi_pin_count=$(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1])).get("inputs", {}).get("abiPackagePins", [])))' "$spec") +if (( abi_pin_count > 0 )); then + "$script_dir/build-pinned-abi-packages.sh" \ + --spec "$spec" \ + --guest-dir "$guest_dir" \ + --output-repo "$temporary/abi-pin-repo" \ + --work "$temporary" || fail "could not rebuild the reviewed ABI pins" + builder_conf_args+=(--abi-repo "$temporary/abi-pin-repo") fi +"${builder_conf_args[@]}" || fail "could not derive the factory builder pacman configuration" pacman -Syy --noconfirm --config "$config" \ --dbpath "$temporary/db" --logfile "$temporary/pacman.log" diff --git a/guest/scripts/register-patched-hyprland.sh b/guest/scripts/register-patched-hyprland.sh index da01609c6..11e2d5c51 100755 --- a/guest/scripts/register-patched-hyprland.sh +++ b/guest/scripts/register-patched-hyprland.sh @@ -398,7 +398,8 @@ import sys source = pathlib.Path(sys.argv[1]).read_text().splitlines() sections = [line[1:-1] for line in source if re.fullmatch(r"\[[A-Za-z0-9@._+-]+\]", line)] -if sections != ["options", "core", "extra", "alarm", "aur", "omarchy"]: +allowed = {"options", "core", "extra", "alarm", "aur", "omarchy", "try-omarchy-abi-pins", "try-omarchy-pinned-cache"} +if not sections or sections[0] != "options" or "omarchy" not in sections or any(s not in allowed for s in sections): raise SystemExit(1) output = [] @@ -406,7 +407,9 @@ section = None for line in source: if re.fullmatch(r"\[[A-Za-z0-9@._+-]+\]", line): section = line[1:-1] - if section == "omarchy" or line.startswith("IgnorePkg"): + # Keep try-omarchy-abi-pins so Hyprland build deps can still resolve + # libaquamarine.so=13 while ALA only publishes .so=14. + if section in {"omarchy", "try-omarchy-pinned-cache"} or line.startswith("IgnorePkg"): continue output.append(line) diff --git a/guest/scripts/register-pinned-ttfx.sh b/guest/scripts/register-pinned-ttfx.sh index 75171c285..ce819d888 100755 --- a/guest/scripts/register-pinned-ttfx.sh +++ b/guest/scripts/register-pinned-ttfx.sh @@ -125,7 +125,7 @@ source_date_epoch=${metadata[20]} [[ $architecture == aarch64 ]] || fail "pinned ttfx component supports only aarch64" [[ $target == aarch64-unknown-linux-gnu ]] || fail "unexpected ttfx build target: $target" -[[ $rust_package_version == "rust 1:1.98.0-1" ]] || fail "unexpected ttfx Rust package version" +[[ $rust_package_version == "rust 1:1.98.1-1" ]] || fail "unexpected ttfx Rust package version" [[ $(pacman -Q rust) == "$rust_package_version" ]] || fail "ttfx Rust package identity mismatch" [[ $(rustc --version) == "$rustc_version" ]] || fail "ttfx rustc identity mismatch" [[ $(cargo --version) == "$cargo_version" ]] || fail "ttfx Cargo identity mismatch" diff --git a/guest/scripts/write-builder-pacman-conf.py b/guest/scripts/write-builder-pacman-conf.py new file mode 100755 index 000000000..4a4180eab --- /dev/null +++ b/guest/scripts/write-builder-pacman-conf.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Derive the disposable factory builder pacman.conf from the guest config. + +Guest IgnorePkg holds (kernel / Hyprland / aquamarine) stay in the reviewed +guest file. The builder config must: + - expose ABI pins rebuilt from reviewed upstream source + Arch PKGBUILD + - omit those pin names from IgnorePkg so pacstrap can install them once + - keep optional signed packageCachePins ahead of rolling mirrors + +[try-omarchy-abi-pins] is unsigned because repo-add writes an unsigned database +for the just-built package. Origin is the reproducible rebuild, not TrustAll. +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +from pathlib import Path + + +SECTION_RE = re.compile(r"^\[([A-Za-z0-9@._+-]+)\]$") + + +def fail(message: str) -> None: + raise SystemExit(f"write-builder-pacman-conf: {message}") + + +def load_abi_pins(spec: dict, lock_packages: dict[str, str]) -> list[dict]: + pins = spec.get("inputs", {}).get("abiPackagePins", []) + if not isinstance(pins, list): + fail("inputs.abiPackagePins must be a list") + names = [pin.get("name") for pin in pins] + if names != sorted(set(names)): + fail("abiPackagePins names must be sorted and unique") + supply = spec.get("supplyChain", {}) + resolved = [] + for pin in pins: + if not isinstance(pin, dict): + fail("abiPackagePins entries must be objects") + required = {"name", "version"} + if set(pin) != required: + fail(f"abiPackagePins entry keys must be exactly {sorted(required)}") + name = pin["name"] + version = pin["version"] + if not re.fullmatch(r"[a-z0-9@._+-]+", name or ""): + fail(f"invalid abi package name: {name}") + if not re.fullmatch(r"[A-Za-z0-9_.+:~-]+", version or ""): + fail(f"invalid abi package version: {version}") + if name not in {"aquamarine", "hyprtoolkit"}: + fail(f"unsupported abi pin: {name}") + component = supply.get(name) + if not isinstance(component, dict): + fail(f"abi pin {name} is missing supplyChain.{name}") + expected = f"{component.get('version')}-{component.get('pkgrel')}" + if version != expected: + fail(f"abi pin {name} version {version} does not match supply chain {expected}") + locked = lock_packages.get(name) + if locked != version: + fail(f"abi package {name} version {version} does not match lock {locked}") + resolved.append(pin) + return resolved + + +def ensure_abi_repo(pins: list[dict], repo_dir: Path) -> None: + if not pins: + return + if not repo_dir.is_dir(): + fail(f"abi pin repository is missing: {repo_dir}") + archives = [] + for pin in pins: + matches = sorted(repo_dir.glob(f"{pin['name']}-{pin['version']}-*.pkg.tar.zst")) + matches = [path for path in matches if path.is_file() and not path.is_symlink()] + if len(matches) != 1: + fail(f"expected exactly one rebuilt archive for {pin['name']}={pin['version']}") + archives.append(matches[0]) + database = repo_dir / "try-omarchy-abi-pins.db.tar.gz" + if database.exists(): + return + subprocess.run( + ["repo-add", str(database), *map(str, archives)], + check=True, + stdout=subprocess.DEVNULL, + ) + + +def strip_ignore_pkg(line: str, drop: set[str]) -> str | None: + if not line.startswith("IgnorePkg"): + return line + _, _, value = line.partition("=") + packages = [pkg for pkg in value.split() if pkg not in drop] + if not packages: + return None + return "IgnorePkg = " + " ".join(packages) + + +def write_builder_config( + *, + guest_config: Path, + output: Path, + package_cache: Path | None, + disable_sandbox: bool, + abi_repo: Path | None, + pinned_cache_repo: Path | None, + drop_ignore: set[str], +) -> None: + lines = guest_config.read_text().splitlines() + options_sections = 0 + abi_inserted = pinned_inserted = False + out: list[str] = [] + + for line in lines: + section = SECTION_RE.fullmatch(line) + if section and section.group(1) != "options": + if abi_repo is not None and not abi_inserted: + out.extend( + [ + "[try-omarchy-abi-pins]", + "SigLevel = Optional TrustAll", + f"Server = file://{abi_repo}", + "", + ] + ) + abi_inserted = True + if pinned_cache_repo is not None and not pinned_inserted: + out.extend( + [ + "[try-omarchy-pinned-cache]", + "SigLevel = Required DatabaseOptional", + f"Server = file://{pinned_cache_repo}", + "", + ] + ) + pinned_inserted = True + + rewritten = strip_ignore_pkg(line, drop_ignore) + if rewritten is None: + continue + out.append(rewritten) + + if line == "[options]": + options_sections += 1 + if package_cache is not None: + out.append(f"CacheDir = {package_cache}") + if disable_sandbox: + out.append("DisableSandbox") + + if options_sections != 1: + fail("guest pacman configuration must contain one [options] section") + if abi_repo is not None and not abi_inserted: + fail("guest pacman configuration has no repository section for abi pins") + if pinned_cache_repo is not None and not pinned_inserted: + fail("guest pacman configuration has no repository section for cache pins") + + output.write_text("\n".join(out).rstrip() + "\n") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--spec", required=True, type=Path) + parser.add_argument("--guest-dir", required=True, type=Path) + parser.add_argument("--guest-config", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--package-lock", required=True, type=Path) + parser.add_argument("--abi-repo", type=Path) + parser.add_argument("--pinned-cache-repo", type=Path) + parser.add_argument("--package-cache", type=Path) + parser.add_argument("--disable-sandbox", action="store_true") + args = parser.parse_args() + + spec = json.loads(args.spec.read_text()) + lock_packages = json.loads(args.package_lock.read_text())["packages"] + pins = load_abi_pins(spec, lock_packages) + + abi_repo = args.abi_repo + if pins: + if abi_repo is None: + fail("--abi-repo is required when abiPackagePins are declared") + ensure_abi_repo(pins, abi_repo) + elif abi_repo is not None: + fail("--abi-repo was provided without abiPackagePins") + + write_builder_config( + guest_config=args.guest_config, + output=args.output, + package_cache=args.package_cache, + disable_sandbox=args.disable_sandbox, + abi_repo=abi_repo if pins else None, + pinned_cache_repo=args.pinned_cache_repo, + drop_ignore={pin["name"] for pin in pins}, + ) + + +if __name__ == "__main__": + main() diff --git a/guest/spec.json b/guest/spec.json index 79a43124f..774678cd1 100644 --- a/guest/spec.json +++ b/guest/spec.json @@ -52,13 +52,13 @@ "glazeUrl": "https://github.com/stephenberry/glaze/archive/refs/tags/v7.2.0.tar.gz", "glazeSha256": "17dba19ae63ae48f94994f00d49d5cb3c8f1306db1046c534c4828662490b7d4", "glazeLicenseSha256": "5d49e66411a0807a7c8d6b911b9a26b59e940c71aebe561a3ad8b0b80ac4b7b6", - "binarySha256": "c668b05275f2d5cbff66fdb8f4ea4cbbfb7d5a7f9e682f358f3fbcff8494c68a", + "binarySha256": "b0c96f3057f9f4000c5e50adba0f6020dd7f63747e64371adcd4b30b97eabdb9", "license": "BSD-3-Clause", "issue": "https://github.com/omacom/try-omarchy/issues/5", "buildPackages": { "base-devel": "1-2", "binutils": "2.46+r70+g155188ea10a7-1", - "cmake": "4.4.3-1", + "cmake": "4.4.3-2", "gcc": "16.1.1+r12+g301eb08fa2c5-1", "gcc-libs": "16.1.1+r12+g301eb08fa2c5-1", "glibc": "2.43+r22+g8362e8ce10b2-2", @@ -67,10 +67,23 @@ "make": "4.4.1-3", "meson": "1.12.0-1", "ninja": "1.13.2-3", - "pkgconf": "3.0.6-1", + "pkgconf": "3.0.7-1", "xorgproto": "2025.1-1" } }, + "aquamarine": { + "version": "0.14.0", + "pkgrel": "2", + "repository": "https://github.com/hyprwm/aquamarine", + "url": "https://github.com/hyprwm/aquamarine/archive/v0.14.0/aquamarine-0.14.0.tar.gz", + "sha256": "5dcf0b17f7dd51539fd7e79d68484f04240b3b63cf9f5f21d5b6dea0088168f9", + "pkgbuild": "pinned-packages/aquamarine/PKGBUILD", + "pkgbuildSha256": "1bd4197238a4f0092216ab2dfd723126d618cceb977d45865e140a488a8f56ff", + "packagingRepository": "https://gitlab.archlinux.org/archlinux/packaging/packages/aquamarine.git", + "packagingCommit": "8489a8358817a964a923f05ba324996378d81a5d", + "license": "BSD-3-Clause", + "binarySha256": "7da003aa60e008e9f514c312f01c1e967983e2c46732d58953735bfaee3fd8aa" + }, "mise": { "version": "2026.8.11", "url": "https://github.com/jdx/mise/releases/download/v2026.8.11/mise-v2026.8.11-linux-arm64.tar.xz", @@ -89,11 +102,11 @@ "url": "https://github.com/omacom-io/ttfx/archive/refs/tags/v0.3.2.tar.gz", "sha256": "d0c0df4867e7f03142fb7f77c66670d0e8da15534239c1a7abfd89f19dfc00f6", "cargoLockSha256": "49e2091962fc4d425b4cf3bde1a105719b5b50eed0583ec90e85922adb45e2ce", - "binarySha256": "9171a07c752b202a21f80a4ad336a9d093be06a6c96b062e8b5e0c158d2a86d2", + "binarySha256": "d034cc5b9a8d410ce93113ef0a5d27b5ee2327948562bf2b0e756eebd326fa8f", "target": "aarch64-unknown-linux-gnu", - "rustPackageVersion": "rust 1:1.98.0-1", - "rustcVersion": "rustc 1.98.0 (88d9e12ae 2026-08-18) (Arch Linux rust 1:1.98.0-1)", - "cargoVersion": "cargo 1.98.0 (797e8a9bc 2026-08-05) (Arch Linux rust 1:1.98.0-1)", + "rustPackageVersion": "rust 1:1.98.1-1", + "rustcVersion": "rustc 1.98.1 (48a229cea 2026-09-01) (Arch Linux rust 1:1.98.1-1)", + "cargoVersion": "cargo 1.98.1 (797e8a9bc 2026-08-05) (Arch Linux rust 1:1.98.1-1)", "reportedVersion": "ttfx 0.3.2", "license": "MIT", "licenseSha256": "175441de2eb9a0d3f0627c404ad71929336fd98d75926cc27b9e364d35cc7977", @@ -173,6 +186,19 @@ "signatureSha256": "f48b8071f78fde0b2d20072e875a8b8e0d8fab4caf6c8ec91cd8e2aa0b031063" } } + }, + "hyprtoolkit": { + "version": "0.5.4", + "pkgrel": "6.1", + "repository": "https://github.com/hyprwm/hyprtoolkit", + "url": "https://github.com/hyprwm/hyprtoolkit/archive/v0.5.4/hyprtoolkit-0.5.4.tar.gz", + "sha256": "2fb59789f231c1c4e9154ceffc1e7524c0cae154807c0d57e6166806255b570f", + "pkgbuild": "pinned-packages/hyprtoolkit/PKGBUILD", + "pkgbuildSha256": "803f1db19ad1d42e48b638e35256d3dabbe19d1d0b4b3fd584eedf20121256ce", + "packagingRepository": "https://gitlab.archlinux.org/archlinux/packaging/packages/hyprtoolkit.git", + "packagingCommit": "1ed230388a2ccb2c857af980235cf25a4f86e39e", + "license": "BSD-3-Clause", + "binarySha256": "dc814fad9723bfcf66dbd29b7f8c5cc96fd63a1ff623909e466dd9d011c0cba8" } }, "themes": [ @@ -186,7 +212,17 @@ "inputs": { "packages": "packages.txt", "packageLock": "packages.lock.json", - "pacmanConfig": "pacman.aarch64.conf" + "pacmanConfig": "pacman.aarch64.conf", + "abiPackagePins": [ + { + "name": "aquamarine", + "version": "0.14.0-2" + }, + { + "name": "hyprtoolkit", + "version": "0.5.4-6.1" + } + ] }, "authenticity": { "verbatimRuntimeTrees": [ diff --git a/guest/tests/test_builder_pacman_conf.py b/guest/tests/test_builder_pacman_conf.py new file mode 100644 index 000000000..a3223074b --- /dev/null +++ b/guest/tests/test_builder_pacman_conf.py @@ -0,0 +1,59 @@ +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest + + +GUEST = Path(__file__).resolve().parents[1] +MODULE = importlib.util.spec_from_file_location( + "builder_pacman_conf", GUEST / "scripts/write-builder-pacman-conf.py" +) +builder = importlib.util.module_from_spec(MODULE) +MODULE.loader.exec_module(builder) + + +class BuilderPacmanConfigTests(unittest.TestCase): + def setUp(self): + self.spec = json.loads((GUEST / "spec.json").read_text()) + self.lock = json.loads((GUEST / "packages.lock.json").read_text())["packages"] + + def test_builder_exposes_pair_without_changing_guest_holds(self): + guest_config = GUEST / "pacman.aarch64.conf" + original = guest_config.read_text() + pins = builder.load_abi_pins(self.spec, self.lock) + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "pacman.conf" + builder.write_builder_config( + guest_config=guest_config, + output=output, + package_cache=None, + disable_sandbox=True, + abi_repo=Path(directory) / "abi-repo", + pinned_cache_repo=None, + drop_ignore={pin["name"] for pin in pins}, + ) + config = output.read_text() + self.assertEqual(guest_config.read_text(), original) + self.assertIn("hyprland aquamarine hyprtoolkit\n", original) + self.assertIn("IgnorePkg = linux-aarch64 linux-aarch64-headers hyprland\n", config) + self.assertLess(config.index("[try-omarchy-abi-pins]"), config.index("[extra]")) + self.assertIn("DisableSandbox\n", config) + + def test_missing_toolkit_archive_is_rejected(self): + pins = builder.load_abi_pins(self.spec, self.lock) + with tempfile.TemporaryDirectory() as directory: + repo = Path(directory) + (repo / "aquamarine-0.14.0-2-aarch64.pkg.tar.zst").touch() + (repo / "try-omarchy-abi-pins.db.tar.gz").touch() + with self.assertRaisesRegex(SystemExit, "hyprtoolkit"): + builder.ensure_abi_repo(pins, repo) + + def test_mirror_toolkit_version_cannot_replace_the_reviewed_pin(self): + self.lock["hyprtoolkit"] = "0.5.4-6" + with self.assertRaisesRegex(SystemExit, "does not match lock"): + builder.load_abi_pins(self.spec, self.lock) + + +if __name__ == "__main__": + unittest.main() diff --git a/guest/tests/verify.py b/guest/tests/verify.py index 814cd274f..72a48152e 100755 --- a/guest/tests/verify.py +++ b/guest/tests/verify.py @@ -130,9 +130,79 @@ def main() -> None: "SSH preset and boot activation are an exact loopback-only runtime contract", ) check(spec["runtime"]["storage"]["expandedSizeMiB"] == 24576, "working disk expands to 24 GiB") - check(set(spec["inputs"]) == {"packages", "packageLock", "pacmanConfig"}, "spec has a minimal input set") - for path in spec["inputs"].values(): - check((GUEST / path).is_file(), f"spec input exists: {path}") + check( + set(spec["inputs"]) == {"packages", "packageLock", "pacmanConfig", "abiPackagePins"}, + "spec has a minimal input set", + ) + for key, value in spec["inputs"].items(): + if key == "abiPackagePins": + continue + check((GUEST / value).is_file(), f"spec input exists: {value}") + abi_pins = spec["inputs"]["abiPackagePins"] + check( + abi_pins == [{"name": "aquamarine", "version": "0.14.0-2"}, {"name": "hyprtoolkit", "version": "0.5.4-6.1"}], + "factory abi pins keep aquamarine on libaquamarine.so=13 for the locked Hyprland", + ) + aquamarine = spec.get("supplyChain", {}).get("aquamarine", {}) + pkgbuild = GUEST / aquamarine.get("pkgbuild", "") + pkgbuild_text = pkgbuild.read_text() if pkgbuild.is_file() else "" + check( + aquamarine + == { + "version": "0.14.0", + "pkgrel": "2", + "repository": "https://github.com/hyprwm/aquamarine", + "url": "https://github.com/hyprwm/aquamarine/archive/v0.14.0/aquamarine-0.14.0.tar.gz", + "sha256": "5dcf0b17f7dd51539fd7e79d68484f04240b3b63cf9f5f21d5b6dea0088168f9", + "pkgbuild": "pinned-packages/aquamarine/PKGBUILD", + "pkgbuildSha256": "1bd4197238a4f0092216ab2dfd723126d618cceb977d45865e140a488a8f56ff", + "packagingRepository": "https://gitlab.archlinux.org/archlinux/packaging/packages/aquamarine.git", + "packagingCommit": "8489a8358817a964a923f05ba324996378d81a5d", + "license": "BSD-3-Clause", + "binarySha256": "7da003aa60e008e9f514c312f01c1e967983e2c46732d58953735bfaee3fd8aa", + } + and pkgbuild.is_file() + and hashlib.sha256(pkgbuild.read_bytes()).hexdigest() == aquamarine["pkgbuildSha256"] + and f"sha256sums=('{aquamarine['sha256']}')" in pkgbuild_text + and "pkgver=0.14.0" in pkgbuild_text + and "pkgrel=2" in pkgbuild_text, + "factory rebuilds aquamarine 0.14 from the reviewed Arch PKGBUILD and upstream tarball", + ) + hyprtoolkit = spec.get("supplyChain", {}).get("hyprtoolkit", {}) + toolkit_recipe = GUEST / hyprtoolkit.get("pkgbuild", "") + check( + hyprtoolkit == { + "version": "0.5.4", + "pkgrel": "6.1", + "repository": "https://github.com/hyprwm/hyprtoolkit", + "url": "https://github.com/hyprwm/hyprtoolkit/archive/v0.5.4/hyprtoolkit-0.5.4.tar.gz", + "sha256": "2fb59789f231c1c4e9154ceffc1e7524c0cae154807c0d57e6166806255b570f", + "pkgbuild": "pinned-packages/hyprtoolkit/PKGBUILD", + "pkgbuildSha256": "803f1db19ad1d42e48b638e35256d3dabbe19d1d0b4b3fd584eedf20121256ce", + "packagingRepository": "https://gitlab.archlinux.org/archlinux/packaging/packages/hyprtoolkit.git", + "packagingCommit": "1ed230388a2ccb2c857af980235cf25a4f86e39e", + "license": "BSD-3-Clause", + "binarySha256": "dc814fad9723bfcf66dbd29b7f8c5cc96fd63a1ff623909e466dd9d011c0cba8" +} + and toolkit_recipe.is_file() + and hashlib.sha256(toolkit_recipe.read_bytes()).hexdigest() == hyprtoolkit["pkgbuildSha256"], + "factory rebuilds Hyprtoolkit against the compatible aquamarine ABI", + ) + builder_conf_writer = read(GUEST / "scripts/write-builder-pacman-conf.py") + build_aquamarine = read(GUEST / "scripts/build-pinned-abi-packages.sh") + check( + "try-omarchy-abi-pins" in builder_conf_writer + and "drop_ignore" in builder_conf_writer + and "reproducible rebuild" in builder_conf_writer + and "write-builder-pacman-conf.py" in read(GUEST / "build.sh") + and "write-builder-pacman-conf.py" in read(GUEST / "scripts/refresh-package-lock.sh") + and "build-pinned-abi-packages.sh" in read(GUEST / "build.sh") + and "build-pinned-abi-packages.sh" in read(GUEST / "scripts/refresh-package-lock.sh") + and "download digest mismatch" in build_aquamarine + and "reproducible library digest mismatch" in build_aquamarine + and "ABI source archive has an unsafe member set" in build_aquamarine, + "factory builder pacman derivation rebuilds abi pins from source and strips them from IgnorePkg", + ) wallpaper = DEFAULT_WALLPAPER.read_bytes() check( @@ -251,7 +321,7 @@ def main() -> None: "factory pacman retains the ARM Omarchy keyring repository", ) check( - "IgnorePkg = linux-aarch64 linux-aarch64-headers hyprland aquamarine" + "IgnorePkg = linux-aarch64 linux-aarch64-headers hyprland aquamarine hyprtoolkit" in pacman_conf, "factory pacman holds the QEMU-booted kernel, matching headers, patched compositor, and its aquamarine ABI", ) @@ -271,6 +341,11 @@ def main() -> None: ) packages = package_lock.get("packages") check(isinstance(packages, dict) and len(packages) > 100, "package transaction is fully locked") + for pin in spec["inputs"]["abiPackagePins"]: + check( + packages.get(pin["name"]) == pin["version"], + f"abi pin version matches the transaction lock: {pin['name']}", + ) requested_packages = { line.strip() for line in package_text.decode().splitlines() @@ -412,11 +487,11 @@ def main() -> None: "url": "https://github.com/omacom-io/ttfx/archive/refs/tags/v0.3.2.tar.gz", "sha256": "d0c0df4867e7f03142fb7f77c66670d0e8da15534239c1a7abfd89f19dfc00f6", "cargoLockSha256": "49e2091962fc4d425b4cf3bde1a105719b5b50eed0583ec90e85922adb45e2ce", - "binarySha256": "9171a07c752b202a21f80a4ad336a9d093be06a6c96b062e8b5e0c158d2a86d2", + "binarySha256": "d034cc5b9a8d410ce93113ef0a5d27b5ee2327948562bf2b0e756eebd326fa8f", "target": "aarch64-unknown-linux-gnu", - "rustPackageVersion": "rust 1:1.98.0-1", - "rustcVersion": "rustc 1.98.0 (88d9e12ae 2026-08-18) (Arch Linux rust 1:1.98.0-1)", - "cargoVersion": "cargo 1.98.0 (797e8a9bc 2026-08-05) (Arch Linux rust 1:1.98.0-1)", + "rustPackageVersion": "rust 1:1.98.1-1", + "rustcVersion": "rustc 1.98.1 (48a229cea 2026-09-01) (Arch Linux rust 1:1.98.1-1)", + "cargoVersion": "cargo 1.98.1 (797e8a9bc 2026-08-05) (Arch Linux rust 1:1.98.1-1)", "reportedVersion": "ttfx 0.3.2", "license": "MIT", "licenseSha256": "175441de2eb9a0d3f0627c404ad71929336fd98d75926cc27b9e364d35cc7977", @@ -443,13 +518,13 @@ def main() -> None: "glazeUrl": "https://github.com/stephenberry/glaze/archive/refs/tags/v7.2.0.tar.gz", "glazeSha256": "17dba19ae63ae48f94994f00d49d5cb3c8f1306db1046c534c4828662490b7d4", "glazeLicenseSha256": "5d49e66411a0807a7c8d6b911b9a26b59e940c71aebe561a3ad8b0b80ac4b7b6", - "binarySha256": "c668b05275f2d5cbff66fdb8f4ea4cbbfb7d5a7f9e682f358f3fbcff8494c68a", + "binarySha256": "b0c96f3057f9f4000c5e50adba0f6020dd7f63747e64371adcd4b30b97eabdb9", "license": "BSD-3-Clause", "issue": "https://github.com/omacom/try-omarchy/issues/5", "buildPackages": { "base-devel": "1-2", "binutils": "2.46+r70+g155188ea10a7-1", - "cmake": "4.4.3-1", + "cmake": "4.4.3-2", "gcc": "16.1.1+r12+g301eb08fa2c5-1", "gcc-libs": "16.1.1+r12+g301eb08fa2c5-1", "glibc": "2.43+r22+g8362e8ce10b2-2", @@ -458,7 +533,7 @@ def main() -> None: "make": "4.4.1-3", "meson": "1.12.0-1", "ninja": "1.13.2-3", - "pkgconf": "3.0.6-1", + "pkgconf": "3.0.7-1", "xorgproto": "2025.1-1", }, } @@ -499,7 +574,11 @@ def main() -> None: 'supply_chain.get("hyprland")' in launcher and '"build spec hyprland component"' in launcher and '"build spec hyprland build packages"' in launcher - and hyprland_identity in launcher, + and hyprland_identity in launcher + and 'supply_chain.get("aquamarine")' in launcher + and '"build spec aquamarine component"' in launcher + and aquamarine["pkgbuildSha256"] in launcher + and aquamarine["binarySha256"] in launcher, "native launcher accepts and pins the patched Hyprland component", ) check( @@ -551,7 +630,7 @@ def main() -> None: check("try-omarchy-guest-work" in container, "guest cache has a project-scoped Docker volume") containerfile = read(GUEST / "Containerfile") check( - "arch-install-scripts e2fsprogs git python rust=1:1.98.0-1 zstd" in containerfile, + "arch-install-scripts e2fsprogs git python rust=1:1.98.1-1 zstd" in containerfile, "guest builder pins Rust for source-built components", ) @@ -927,6 +1006,13 @@ def main() -> None: "**Hyprland**" in third_party_notices and "BSD-3-Clause" in third_party_notices, "third-party notices cover the patched Hyprland redistribution", ) + check( + "**aquamarine**" in third_party_notices + and "hyprwm/aquamarine" in third_party_notices + and "PKGBUILD" in third_party_notices + and "IgnorePkg" in third_party_notices, + "third-party notices cover the rebuilt aquamarine ABI pin", + ) check( "**Glaze**" in third_party_notices and "MIT" in third_party_notices, "third-party notices cover the Hyprland build's bundled Glaze headers", diff --git a/macos/run-qemu-gpu.sh b/macos/run-qemu-gpu.sh index 42fb86e07..cca759289 100755 --- a/macos/run-qemu-gpu.sh +++ b/macos/run-qemu-gpu.sh @@ -526,6 +526,8 @@ if ( fail("upstream identity is not pinned") supply_chain_keys = { + "aquamarine", + "hyprtoolkit", "archLinuxArmPackagesCommit", "archLinuxArmPackagesRepository", "hyprland", @@ -592,8 +594,58 @@ exact_keys( hyprland_identity = hashlib.sha256( json.dumps(hyprland, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8") ).hexdigest() -if hyprland_identity != "edd58c17fc115b375d8e8b9b5eb7eb78008e89c05867b3ed2f2834287badcae8": +if hyprland_identity != "ae82ce3f989eff555f1faa2400ff0ecb3d7b52b4797c6e3f4fca29959e5a7790": fail("factory Hyprland component is not the reviewed rounded-border build") +aquamarine = exact_keys( + supply_chain.get("aquamarine"), + { + "binarySha256", + "license", + "packagingCommit", + "packagingRepository", + "pkgbuild", + "pkgbuildSha256", + "pkgrel", + "repository", + "sha256", + "url", + "version", + }, + "build spec aquamarine component", +) +if aquamarine != { + "version": "0.14.0", + "pkgrel": "2", + "repository": "https://github.com/hyprwm/aquamarine", + "url": "https://github.com/hyprwm/aquamarine/archive/v0.14.0/aquamarine-0.14.0.tar.gz", + "sha256": "5dcf0b17f7dd51539fd7e79d68484f04240b3b63cf9f5f21d5b6dea0088168f9", + "pkgbuild": "pinned-packages/aquamarine/PKGBUILD", + "pkgbuildSha256": "1bd4197238a4f0092216ab2dfd723126d618cceb977d45865e140a488a8f56ff", + "packagingRepository": "https://gitlab.archlinux.org/archlinux/packaging/packages/aquamarine.git", + "packagingCommit": "8489a8358817a964a923f05ba324996378d81a5d", + "license": "BSD-3-Clause", + "binarySha256": "7da003aa60e008e9f514c312f01c1e967983e2c46732d58953735bfaee3fd8aa", +}: + fail("factory aquamarine component is not the reviewed libaquamarine.so=13 rebuild") +hyprtoolkit = exact_keys( + supply_chain.get("hyprtoolkit"), + set(aquamarine), + "build spec hyprtoolkit component", +) +if hyprtoolkit != { + "version": "0.5.4", + "pkgrel": "6.1", + "repository": "https://github.com/hyprwm/hyprtoolkit", + "url": "https://github.com/hyprwm/hyprtoolkit/archive/v0.5.4/hyprtoolkit-0.5.4.tar.gz", + "sha256": "2fb59789f231c1c4e9154ceffc1e7524c0cae154807c0d57e6166806255b570f", + "pkgbuild": "pinned-packages/hyprtoolkit/PKGBUILD", + "pkgbuildSha256": "803f1db19ad1d42e48b638e35256d3dabbe19d1d0b4b3fd584eedf20121256ce", + "packagingRepository": "https://gitlab.archlinux.org/archlinux/packaging/packages/hyprtoolkit.git", + "packagingCommit": "1ed230388a2ccb2c857af980235cf25a4f86e39e", + "license": "BSD-3-Clause", + "binarySha256": "dc814fad9723bfcf66dbd29b7f8c5cc96fd63a1ff623909e466dd9d011c0cba8" +}: + fail("factory hyprtoolkit component is not the reviewed libaquamarine.so=13 rebuild") mise = exact_keys( supply_chain.get("mise"), {"binarySha256", "license", "reportedVersion", "sha256", "url", "version"}, @@ -642,11 +694,11 @@ if ttfx != { "url": "https://github.com/omacom-io/ttfx/archive/refs/tags/v0.3.2.tar.gz", "sha256": "d0c0df4867e7f03142fb7f77c66670d0e8da15534239c1a7abfd89f19dfc00f6", "cargoLockSha256": "49e2091962fc4d425b4cf3bde1a105719b5b50eed0583ec90e85922adb45e2ce", - "binarySha256": "9171a07c752b202a21f80a4ad336a9d093be06a6c96b062e8b5e0c158d2a86d2", + "binarySha256": "d034cc5b9a8d410ce93113ef0a5d27b5ee2327948562bf2b0e756eebd326fa8f", "target": "aarch64-unknown-linux-gnu", - "rustPackageVersion": "rust 1:1.98.0-1", - "rustcVersion": "rustc 1.98.0 (88d9e12ae 2026-08-18) (Arch Linux rust 1:1.98.0-1)", - "cargoVersion": "cargo 1.98.0 (797e8a9bc 2026-08-05) (Arch Linux rust 1:1.98.0-1)", + "rustPackageVersion": "rust 1:1.98.1-1", + "rustcVersion": "rustc 1.98.1 (48a229cea 2026-09-01) (Arch Linux rust 1:1.98.1-1)", + "cargoVersion": "cargo 1.98.1 (797e8a9bc 2026-08-05) (Arch Linux rust 1:1.98.1-1)", "reportedVersion": "ttfx 0.3.2", "license": "MIT", "licenseSha256": "175441de2eb9a0d3f0627c404ad71929336fd98d75926cc27b9e364d35cc7977",