From 497cb2e51dd56ce477d473da9cf5d5a4a4f44449 Mon Sep 17 00:00:00 2001 From: webbeef Date: Tue, 11 Aug 2026 10:41:10 -0700 Subject: [PATCH] build: use the wild linker instead of ld Signed-off-by: webbeef --- packaging/alpine/APKBUILD.in | 12 +++++ packaging/alpine/README.md | 16 +++++++ scripts/_build-apk-cross-inside.sh | 13 +++++- scripts/_build-apk-inside.sh | 18 +++++++- scripts/build-apk-cross.sh | 12 +++-- scripts/build-apk.sh | 11 ++++- support/docker/Dockerfile.apk-aarch64 | 7 +++ support/docker/Dockerfile.apk-cross | 7 +++ support/docker/Dockerfile.apk-x86_64 | 7 +++ support/docker/install-wild.sh | 65 +++++++++++++++++++++++++++ 10 files changed, 161 insertions(+), 7 deletions(-) create mode 100755 support/docker/install-wild.sh diff --git a/packaging/alpine/APKBUILD.in b/packaging/alpine/APKBUILD.in index 552c441..b47cb9e 100644 --- a/packaging/alpine/APKBUILD.in +++ b/packaging/alpine/APKBUILD.in @@ -83,6 +83,18 @@ build() { # against musl dynamically — that's also the standard Alpine ABI for # anything depending on gtk / gstreamer / etc. export RUSTFLAGS="-C target-feature=-crt-static" + # Link with wild when the builder image provides it (installed by + # support/docker/install-wild.sh); linking beavershell is the longest step + # of a rebuild. The probe is a file test rather than an env var because + # abuild scrubs most of the environment before build() runs. gcc gained + # -fuse-ld=wild only in 16.1 and Alpine 3.20 ships 13, so point it at the + # directory holding wild's `ld` symlink instead. A plain Alpine builder + # without wild falls through to the toolchain default; cross builds bring + # their own linker wrapper (see scripts/_build-apk-cross-inside.sh). + if [ -z "${CARGO_BUILD_TARGET:-}" ] && [ -x /usr/local/lib/wild/ld ]; then + RUSTFLAGS="$RUSTFLAGS -C link-arg=-B/usr/local/lib/wild" + echo "native build: linking with $(/usr/local/bin/wild --version)" + fi # Alpine's abuild.conf exports hardening CFLAGS / CXXFLAGS that # include `-Werror=format-security`; mozjs's makefile rewrites flags # in a way that drops the matching `-Wformat`, leaving gcc with a diff --git a/packaging/alpine/README.md b/packaging/alpine/README.md index 218f250..e82ffcd 100644 --- a/packaging/alpine/README.md +++ b/packaging/alpine/README.md @@ -10,6 +10,22 @@ Use the relevant script for each target: - `./scripts/build-apk-cross.sh `. Using `production` for the profile produces a more optimized build, but takes a long time and a lot of memory to link. - `./scripts/build-apk.sh `. +## Linker + +All three builder images link with [wild](https://github.com/wild-linker/wild) +instead of GNU ld (what the native images used to get by default) or lld (what +the cross image used). Linking `beavershell` is the longest single step of a +rebuild, so this is where build time goes. + +`support/docker/install-wild.sh` installs it, pinned by version and sha256; that +script is also where the version gets bumped. It is not an apk package, so like +rustup it is installed out-of-band and deliberately absent from `makedepends` — +`APKBUILD.in` probes for it and falls back to the toolchain default, which keeps +the recipe usable on a plain Alpine builder (e.g. pmbootstrap). + +The C/C++ sub-builds (mozjs, mozangle) still use their own defaults; only the +final Rust link was switched. + ## Layout produced by the .apk ``` diff --git a/scripts/_build-apk-cross-inside.sh b/scripts/_build-apk-cross-inside.sh index 38086e3..b570d0a 100755 --- a/scripts/_build-apk-cross-inside.sh +++ b/scripts/_build-apk-cross-inside.sh @@ -111,13 +111,24 @@ export PKG_CONFIG_SYSROOT_DIR="$SYSROOT" export PKG_CONFIG_LIBDIR="${SYSROOT}/usr/lib/pkgconfig:${SYSROOT}/usr/share/pkgconfig" unset PKG_CONFIG_PATH +# The final Rust link is by far the most expensive link in the build, so it goes +# through wild rather than lld. A single wild binary carries every architecture +# it supports, so the host-arch build emits aarch64 output without a cross +# build of the linker itself; clang's --ld-path takes it directly (no -fuse-ld +# symlink lookup). The C/C++ sub-builds above stay on lld: their links are small +# and mozjs's configure probes are already known to work with it. CROSS_LINKER=/tmp/cross-clang-linker cat > "$CROSS_LINKER" < "$WORK/APKBUILD" cp packaging/alpine/beaver.post-install "$WORK/beaver.post-install" cp packaging/alpine/beaver.post-deinstall "$WORK/beaver.post-deinstall" @@ -44,6 +45,21 @@ sudo cp "$HOME"/.abuild/*.rsa.pub /etc/apk/keys/ 2>/dev/null || true # Build + package. abuild -F +# Confirm the link actually went through wild. gcc's -B fails open: if it does +# not find our `ld` it silently uses its own, and the build still succeeds, just +# slowly. wild stamps .comment, so check that rather than trust the flag. The +# default `release` profile keeps .comment; `production` strips it, so a miss is +# reported without being treated as an error. +BIN="/src/target/${PROFILE}/beavershell" +if [ -f "$BIN" ]; then + stamp=$(readelf --string-dump .comment "$BIN" 2>/dev/null | grep "Linker: Wild" | head -1) + if [ -n "$stamp" ]; then + echo "linker check: ${stamp#*Linker: }" + else + echo "linker check: no Wild stamp in .comment (stripped, or wild was not used)" + fi +fi + # abuild stores .apks under $HOME/packages///, where # is the basename of the parent directory of the APKBUILD's directory. # Find ours instead of computing the path. diff --git a/scripts/build-apk-cross.sh b/scripts/build-apk-cross.sh index 70cd075..f904bed 100755 --- a/scripts/build-apk-cross.sh +++ b/scripts/build-apk-cross.sh @@ -7,6 +7,8 @@ # ./scripts/build-apk-cross.sh [profile] # profile: production (default), # # profiling, release # +# Set BEAVER_REBUILD_IMAGE=1 to rebuild the builder image first. +# # Output: dist/beaver__aarch64.apk set -e @@ -22,10 +24,14 @@ CARGO_HOME_DIR=/home/builder/.cargo CARGO_REGISTRY_VOLUME=beaver-apk-cargo-registry CARGO_GIT_VOLUME=beaver-apk-cargo-git -if ! docker image inspect "$IMAGE_NAME" &>/dev/null; then +# The builder image is only rebuilt when it is missing, since it takes a while. +# Set BEAVER_REBUILD_IMAGE=1 after editing the Dockerfile or install-wild.sh. +# Context is support/docker, not the repo root: the image needs nothing else from +# the tree (the repo is bind-mounted at run time), and `.` would send target/, +# which is tens of GB. +if [ -n "${BEAVER_REBUILD_IMAGE:-}" ] || ! docker image inspect "$IMAGE_NAME" &>/dev/null; then echo "Building cross packaging image (${IMAGE_NAME})..." - # No COPY/ADD in the Dockerfile since the repo is bind-mounted at run time. - DOCKER_BUILDKIT=1 docker build -t "$IMAGE_NAME" - < "$DOCKERFILE" + docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" support/docker fi echo "Cross-compiling beaver-shell .apk for ${ARCH} (profile: ${PROFILE})..." diff --git a/scripts/build-apk.sh b/scripts/build-apk.sh index c085886..8a5cd0b 100755 --- a/scripts/build-apk.sh +++ b/scripts/build-apk.sh @@ -9,6 +9,8 @@ # ./scripts/build-apk.sh aarch64 # for pmOS phones # ./scripts/build-apk.sh x86_64 # for Alpine VM testing # +# Set BEAVER_REBUILD_IMAGE=1 to rebuild the builder image first. +# # Output: dist/beaver__.apk set -e @@ -31,9 +33,14 @@ esac TARGET_VOLUME="beaver-apk-target-${ARCH}" -if ! docker image inspect "$IMAGE_NAME" &>/dev/null; then +# The builder image is only rebuilt when it is missing, since it takes a while. +# Set BEAVER_REBUILD_IMAGE=1 after editing the Dockerfile or install-wild.sh. +# Context is support/docker, not the repo root: the image needs nothing else from +# the tree (the repo is bind-mounted at run time), and `.` would send target/, +# which is tens of GB. +if [ -n "${BEAVER_REBUILD_IMAGE:-}" ] || ! docker image inspect "$IMAGE_NAME" &>/dev/null; then echo "Building packaging image (${IMAGE_NAME})..." - docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" . + docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" support/docker fi echo "Building beaver-shell .apk for ${ARCH} (profile: ${PROFILE})..." diff --git a/support/docker/Dockerfile.apk-aarch64 b/support/docker/Dockerfile.apk-aarch64 index 8a19ba9..ace4086 100644 --- a/support/docker/Dockerfile.apk-aarch64 +++ b/support/docker/Dockerfile.apk-aarch64 @@ -47,6 +47,13 @@ RUN apk add --no-cache \ gst-libav \ ca-certificates +# The wild linker, in place of GNU ld / lld. See the script for why it comes +# from wild's own musl release rather than `cargo install`. The build context is +# this directory (support/docker), not the repo root, so that adding a COPY does +# not mean tarring up a 29GB target/ - see scripts/build-apk*.sh. +COPY install-wild.sh /tmp/install-wild.sh +RUN sh /tmp/install-wild.sh && rm /tmp/install-wild.sh + # abuild refuses to run as root (its `id -u` check is unconditional — # fakeroot doesn't fool it). The Alpine convention is a regular user # in the abuild group; passwordless sudo gives that user a fallback for diff --git a/support/docker/Dockerfile.apk-cross b/support/docker/Dockerfile.apk-cross index 6f600fb..1e20433 100644 --- a/support/docker/Dockerfile.apk-cross +++ b/support/docker/Dockerfile.apk-cross @@ -58,6 +58,13 @@ RUN apk add -U --no-cache --arch aarch64 -p /sysroot --initdb --no-scripts \ eudev-dev \ alsa-lib-dev +# The wild linker, in place of GNU ld / lld. See the script for why it comes +# from wild's own musl release rather than `cargo install`. The build context is +# this directory (support/docker), not the repo root, so that adding a COPY does +# not mean tarring up a 29GB target/ - see scripts/build-apk*.sh. +COPY install-wild.sh /tmp/install-wild.sh +RUN sh /tmp/install-wild.sh && rm /tmp/install-wild.sh + # abuild refuses to run as root (its id -u check is unconditional). The Alpine # convention is a regular user in the abuild group; passwordless sudo lets it # fix ownership on bind-mounted dirs. diff --git a/support/docker/Dockerfile.apk-x86_64 b/support/docker/Dockerfile.apk-x86_64 index 9e9e53f..67a9fa5 100644 --- a/support/docker/Dockerfile.apk-x86_64 +++ b/support/docker/Dockerfile.apk-x86_64 @@ -48,6 +48,13 @@ RUN apk add --no-cache \ ca-certificates \ cage +# The wild linker, in place of GNU ld / lld. See the script for why it comes +# from wild's own musl release rather than `cargo install`. The build context is +# this directory (support/docker), not the repo root, so that adding a COPY does +# not mean tarring up a 29GB target/ - see scripts/build-apk*.sh. +COPY install-wild.sh /tmp/install-wild.sh +RUN sh /tmp/install-wild.sh && rm /tmp/install-wild.sh + # abuild refuses to run as root; create a non-root builder in the abuild # group, with passwordless sudo as a fallback for bind-mount write perms. RUN adduser -D -u 1000 builder && \ diff --git a/support/docker/install-wild.sh b/support/docker/install-wild.sh new file mode 100755 index 0000000..e8a495c --- /dev/null +++ b/support/docker/install-wild.sh @@ -0,0 +1,65 @@ +#!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Installs the wild linker (https://github.com/wild-linker/wild) into an Alpine +# builder image. Run as root from the Dockerfile, before it drops to `builder`. +# +# Why wild: linking beavershell is the single longest step of a rebuild, and it +# runs every time any crate changes. wild is much faster than both GNU ld (what +# the native images got by default) and lld (what the cross image used). +# +# Why the project's own release rather than `cargo install wild-linker`: +# - The musl releases are built with the `mimalloc` feature. wild's +# PACKAGING.md singles this out: musl's allocator is *much* worse than +# glibc's for wild's allocation pattern, so a plain `cargo install` on +# Alpine would give up a large part of the speedup we came for. +# - They are built `--profile dist`, and static-pie, so the binary needs +# nothing from the image and works in any of the three builders. +# - No compile step, so the image stays cheap to rebuild. +# +# Alpine has no wild package, so this is out-of-band like rustup (see the note +# in packaging/alpine/APKBUILD.in about why neither is in makedepends). +# +# To bump: change VERSION and replace BOTH checksums. `uname -m` values match +# wild's asset names exactly, so no arch translation is needed. + +set -e + +VERSION=0.10.0 + +ARCH="$(uname -m)" +case "$ARCH" in + x86_64) + SHA256=2d920a6c21372026c1c2a7711c547b2da33708f93bcaae1da196151bdab69453 ;; + aarch64) + SHA256=49cfca01b10a4cd15ef4932687bef1708a052828875bc30f5124bb5085c23d40 ;; + *) + echo "install-wild.sh: no wild musl release for $ARCH" >&2 + exit 1 ;; +esac + +NAME="wild-linker-${VERSION}-${ARCH}-unknown-linux-musl" +URL="https://github.com/wild-linker/wild/releases/download/${VERSION}/${NAME}.tar.gz" + +cd /tmp +curl --proto '=https' --tlsv1.2 -sSfLO "$URL" +echo "${SHA256} ${NAME}.tar.gz" | sha256sum -c - +tar -xzf "${NAME}.tar.gz" + +install -Dm755 "${NAME}/wild" /usr/local/bin/wild +# `ld.wild` is what makes clang's -fuse-ld=wild work; wild's PACKAGING.md asks +# packagers to ship it. +ln -sf /usr/local/bin/wild /usr/local/bin/ld.wild +# A directory whose only entry is an `ld` symlink, for gcc. gcc grew +# -fuse-ld=wild only in 16.1 (Alpine 3.20 ships 13), so gcc callers pass +# -B/usr/local/lib/wild instead: gcc's linker search hits our `ld` first and +# finds crt objects, libgcc and everything else in its normal path, since that +# is the only file here. +install -d /usr/local/lib/wild +ln -sf /usr/local/bin/wild /usr/local/lib/wild/ld + +rm -rf "${NAME}" "${NAME}.tar.gz" + +# Fail the image build now rather than mid-link if the download was for the +# wrong arch or is otherwise unusable. +wild --version -- 2.51.2