build: add musl static target (clang/gcc)

This commit is contained in:
2026-08-28 20:10:42 -04:00
parent d02b5aabcd
commit fa5d22af4b
2 changed files with 309 additions and 1 deletions
+301
View File
@@ -0,0 +1,301 @@
#!/bin/sh
#
# scripts/build-static.sh - build the stupidtools binary as a FULLY STATIC
# musl-linked executable and verify it with file(1) + ldd(1).
#
# Toolchain probe order (first probe that can actually PRODUCE a static
# binary wins - `command -v` alone is not enough, the wrapper must work):
#
# 1. musl-gcc -> CC=musl-gcc, LDFLAGS=-static (musl-tools package)
# 2. musl-clang -> CC=musl-clang, LDFLAGS=-static
# 3. clang --target=x86_64-linux-musl --sysroot="$MUSL_SYSROOT" -static
# -fuse-ld=lld -> needs a musl sysroot; point the MUSL_SYSROOT env var
# at it (e.g. /opt/x86_64-linux-musl from a musl.cc
# cross toolchain, or /usr/lib/musl from musl-tools)
# 4. apt-get install -y musl-tools (only if root or passwordless sudo)
# 5. Download the musl.cc cross toolchain
# https://musl.cc/x86_64-linux-musl-cross.tgz
# (fallback mirror: https://more.musl.cc/x86_64-linux-musl-cross.tgz)
# into /tmp - NEVER into the repo - and use its
# bin/x86_64-linux-musl-gcc with -static.
#
# glibc -static is deliberately NOT a target: NSS and dlopen are broken in
# fully-static glibc builds. musl is the only accepted static libc.
# The DEFAULT build stays dynamic; static is an explicit opt-in via
# `make static` (or running this script directly).
#
# CROSS-COMPILE VARIANT (aarch64) - documented, not wired as a flag:
#
# Download https://musl.cc/aarch64-linux-musl-cross.tgz into /tmp, then:
# tar -C /tmp -xzf aarch64-linux-musl-cross.tgz
# cd <project-root> && make clean
# make CC=/tmp/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc \
# CFLAGS=-O2 LDFLAGS=-static
# verify the result with:
# file src/stupidtools # expect: "... statically linked ... ARM aarch64"
# (for a cross build, ldd must run under an emulator: use
# `qemu-aarch64 /path/to/ldd src/stupidtools` or file(1) alone as proof.)
#
# Env knobs (optional):
# MUSL_SYSROOT - musl sysroot for the clang --sysroot route (route 3)
# STUPIDTOOLS_TOPDIR - project root override (default: this script's ../)
#
# Exit codes: 0 = static binary built AND verified; 1 = failure.
msg() { printf '%s\n' "$*" >&2; }
thisdir=$(CDPATH= cd "$(dirname "$0")" && pwd) || exit 1
topdir=${STUPIDTOOLS_TOPDIR:-"$thisdir/.."}
topdir=$(CDPATH= cd "$topdir" && pwd) || exit 1
out=src/stupidtools # relative to topdir (plan acceptance target)
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/stupidtools-static.XXXXXX") || exit 1
trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM
# ---------------------------------------------------------------------------
# Verification gate - the ONLY definition of success. file(1) must report
# "statically linked" and ldd(1) must report "not a dynamic executable".
# Protects against stale dynamic binaries being mistaken for static ones.
# ---------------------------------------------------------------------------
verify_static() {
bin=$1
command -v file >/dev/null 2>&1 || { msg "ERROR: file(1) not available"; return 1; }
command -v ldd >/dev/null 2>&1 || { msg "ERROR: ldd(1) not available"; return 1; }
f=$(file "$bin") || { msg "ERROR: file(1) failed on $bin"; return 1; }
case "$f" in
*"statically linked"*) ;;
*)
msg "FAIL: $bin is NOT statically linked:"
msg " $f"
return 1
;;
esac
if l=$(ldd "$bin" 2>&1); then
# ldd exiting 0 means it resolved dynamic dependencies
msg "FAIL: $bin is dynamically linked (ldd resolved dependencies):"
msg "$l"
return 1
fi
case "$l" in
*"not a dynamic executable"*) ;;
*)
msg "FAIL: ldd(1) output for $bin was unexpected:"
msg "$l"
return 1
;;
esac
return 0
}
# ---------------------------------------------------------------------------
# probe_cc <cc> [flags...] - can this compiler produce a static binary?
# ---------------------------------------------------------------------------
probe_cc() {
cc=$1
shift
command -v file >/dev/null 2>&1 || return 1
printf 'int main(void){return 0;}\n' > "$tmpdir/probe.c" || return 1
if "$cc" "$@" "$tmpdir/probe.c" -o "$tmpdir/probe" 2>"$tmpdir/probe.log"; then
if file "$tmpdir/probe" 2>/dev/null | grep -q 'statically linked'; then
return 0
fi
msg "note: $cc linked a probe, but the result is not static; skipping"
return 1
fi
msg "note: $cc probe failed:"
sed 's/^/ /' "$tmpdir/probe.log" >&2
return 1
}
# ---------------------------------------------------------------------------
# build_via_make <cc> - preferred path: reuse the automake tree (no source
# list duplication). Requires ./configure to have been run (Makefile exists).
# ---------------------------------------------------------------------------
build_via_make() {
cc=$1
[ -f "$topdir/Makefile" ] || return 1
(
cd "$topdir" || exit 1
unset MAKEFLAGS MFLAGS
make clean >"$tmpdir/make-clean.log" 2>&1 || {
msg "ERROR: make clean failed:"
sed 's/^/ /' "$tmpdir/make-clean.log" >&2
exit 1
}
make CC="$cc" CFLAGS="-O2 $CC_CFLAGS" LDFLAGS="$CC_LDFLAGS" \
>"$tmpdir/make.log" 2>&1 || {
msg "ERROR: make CC=$cc failed:"
sed 's/^/ /' "$tmpdir/make.log" >&2
exit 1
}
) || return 1
return 0
}
# ---------------------------------------------------------------------------
# build_direct <cc> - fallback when the tree is not configured: compile the
# source list straight out of src/Makefile.am (single source of truth).
# ---------------------------------------------------------------------------
build_direct() {
cc=$1
srcs=$(sed -n 's/^stupidtools_SOURCES *= *//p' "$topdir/src/Makefile.am") || {
msg "ERROR: cannot read the source list from src/Makefile.am"
return 1
}
[ -n "$srcs" ] || {
msg "ERROR: empty stupidtools_SOURCES in src/Makefile.am"
return 1
}
set --
for s in $srcs; do
set -- "$@" "$topdir/src/$s"
done
(
cd "$topdir" || exit 1
# shellcheck disable=SC2086 # CC_CFLAGS/CC_LDFLAGS are flag words
"$cc" $CC_CFLAGS -O2 -std=c23 -Wall -Wextra -Wpedantic \
-I"$topdir/src" -I"$topdir/include" \
$CC_LDFLAGS "$@" -o "$out" 2>"$tmpdir/direct.log" || {
msg "ERROR: direct compile with $cc failed:"
sed 's/^/ /' "$tmpdir/direct.log" >&2
exit 1
}
) || return 1
return 0
}
# ---------------------------------------------------------------------------
# try_apt_install - install musl-tools (route 4), non-interactively only.
# ---------------------------------------------------------------------------
try_apt_install() {
if ! command -v apt-get >/dev/null 2>&1; then
msg "note: apt-get not present (non-Debian host); skipping apt route"
return 1
fi
if [ "$(id -u)" -eq 0 ]; then
apt-get install -y musl-tools
return $?
fi
if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
sudo apt-get install -y musl-tools
return $?
fi
msg "note: no root and no passwordless sudo; cannot apt-get musl-tools"
return 1
}
# ---------------------------------------------------------------------------
# try_muslcc_download - musl.cc cross toolchain into $tmpdir (route 5).
# ---------------------------------------------------------------------------
try_muslcc_download() {
tarball=x86_64-linux-musl-cross.tgz
if command -v curl >/dev/null 2>&1; then
msg "downloading $tarball from musl.cc (curl; ~100 MB, allow time)..."
if ! curl -fL --connect-timeout 15 --max-time 900 \
-o "$tmpdir/$tarball" "https://musl.cc/$tarball" 2>"$tmpdir/dl.log"; then
msg "note: primary mirror failed, trying more.musl.cc..."
curl -fL --connect-timeout 15 --max-time 900 \
-o "$tmpdir/$tarball" "https://more.musl.cc/$tarball" 2>"$tmpdir/dl.log" || {
msg "ERROR: musl.cc download failed:"
sed 's/^/ /' "$tmpdir/dl.log" >&2
return 1
}
fi
elif command -v wget >/dev/null 2>&1; then
msg "downloading $tarball from musl.cc (wget; ~100 MB, allow time)..."
if ! wget -q --timeout=15 -O "$tmpdir/$tarball" \
"https://musl.cc/$tarball" 2>"$tmpdir/dl.log"; then
msg "note: primary mirror failed, trying more.musl.cc..."
wget -q --timeout=15 -O "$tmpdir/$tarball" \
"https://more.musl.cc/$tarball" 2>"$tmpdir/dl.log" || {
msg "ERROR: musl.cc download failed:"
sed 's/^/ /' "$tmpdir/dl.log" >&2
return 1
}
fi
else
msg "ERROR: neither curl nor wget available for the musl.cc route"
return 1
fi
size=$(wc -c < "$tmpdir/$tarball")
msg "downloaded $size bytes; extracting..."
tar -C "$tmpdir" -xzf "$tmpdir/$tarball" || {
msg "ERROR: tar extraction of $tarball failed"
return 1
}
dlcc=$tmpdir/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc
[ -x "$dlcc" ] || { msg "ERROR: $dlcc missing after extraction"; return 1; }
if probe_cc "$dlcc" -static -O2; then
CC_STATIC=$dlcc
return 0
fi
msg "ERROR: downloaded gcc could not produce a static binary"
return 1
}
# ---------------------------------------------------------------------------
# Route selection
# ---------------------------------------------------------------------------
CC_STATIC=
CC_CFLAGS=
CC_LDFLAGS=-static
if command -v musl-gcc >/dev/null 2>&1 && probe_cc musl-gcc -static -O2; then
CC_STATIC=musl-gcc
elif command -v musl-clang >/dev/null 2>&1 && probe_cc musl-clang -static -O2; then
CC_STATIC=musl-clang
elif [ -n "${MUSL_SYSROOT:-}" ] && command -v clang >/dev/null 2>&1 \
&& probe_cc clang --target=x86_64-linux-musl \
--sysroot="$MUSL_SYSROOT" -static -fuse-ld=lld; then
CC_STATIC=clang
CC_CFLAGS="--target=x86_64-linux-musl --sysroot=$MUSL_SYSROOT"
CC_LDFLAGS="$CC_CFLAGS -fuse-ld=lld -static"
fi
if [ -z "$CC_STATIC" ]; then
msg "no local musl toolchain found; trying apt-get (route 4)..."
if try_apt_install; then
if command -v musl-gcc >/dev/null 2>&1 && probe_cc musl-gcc -static -O2; then
CC_STATIC=musl-gcc
fi
fi
fi
if [ -z "$CC_STATIC" ]; then
msg "trying musl.cc cross toolchain (route 5)..."
try_muslcc_download
fi
if [ -z "$CC_STATIC" ]; then
msg "ERROR: no usable musl toolchain could be obtained."
msg " Options: install musl-tools (Debian/Ubuntu: apt-get install"
msg " musl-tools; Arch: pacman -S musl), download a musl.cc cross"
msg " toolchain, or set MUSL_SYSROOT and use clang (route 3)."
exit 1
fi
msg "building static binary with $CC_STATIC ..."
if ! build_via_make "$CC_STATIC"; then
msg "note: automake route unavailable or failed; trying direct compile"
build_direct "$CC_STATIC" || {
msg "restoring default dynamic build (best effort)..."
if [ -f "$topdir/Makefile" ]; then
(cd "$topdir" && unset MAKEFLAGS MFLAGS && make >/dev/null 2>&1) || true
fi
msg "ERROR: static build failed"
exit 1
}
fi
if ! verify_static "$topdir/$out"; then
msg "restoring default dynamic build (best effort)..."
if [ -f "$topdir/Makefile" ]; then
(cd "$topdir" && unset MAKEFLAGS MFLAGS && make >/dev/null 2>&1) || true
fi
exit 1
fi
msg "OK: $out is a fully static musl-linked binary."
file "$topdir/$out" 2>/dev/null || true
ldd "$topdir/$out" 2>&1 || true
exit 0