diff --git a/Makefile.am b/Makefile.am index d010198..7672d04 100644 --- a/Makefile.am +++ b/Makefile.am @@ -5,4 +5,11 @@ SUBDIRS = src TESTS = tests/run.sh -EXTRA_DIST = tests/run.sh +EXTRA_DIST = tests/run.sh scripts/build-static.sh + +# Fully-static musl build (explicit opt-in; the default build stays dynamic). +# Requires a musl toolchain on the host - see scripts/build-static.sh for +# the probe order (musl-gcc -> musl-clang -> clang+sysroot -> apt -> musl.cc). +.PHONY: static +static: + $(SHELL) "$(srcdir)/scripts/build-static.sh" diff --git a/scripts/build-static.sh b/scripts/build-static.sh new file mode 100755 index 0000000..1e7f0af --- /dev/null +++ b/scripts/build-static.sh @@ -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 && 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 [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 - 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 - 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