#!/bin/sh # Rebuild static-deps/libcurl.a and static-deps/libcares.a. # # Why this exists: the distro libcurl.a cannot be linked statically on Alpine # because it pulls in libpsl, libbrotli, libzstd and libnghttp2, and those # static archives ship LTO bytecode produced by GCC 14 while the installed # compiler is GCC 15. The linker cannot read it: # bytecode stream in file '.../libpsl.a' generated with LTO version 14.0 # instead of the expected 15.1 # so psl_*, BrotliDecoder* and ZSTD_* simply never resolve. Passing # -fno-lto does not help: the objects are LTO bytecode, not C. # # The fix is a libcurl built without those four libraries. What is left # (ssl, crypto, z, cares, idn2, unistring) is all LTO-free, so it links # cleanly into a fully static binary. Cost: no HTTP/2, no brotli/zstd # Content-Encoding and no public-suffix cookie checks, none of which a # plain JSON API client needs. # # c-ares is built here too, for two reasons. On a minimal system there is often # no libcares.a at all (Void's c-ares-devel ships the shared library only), so # `make static` would fail on -lcares; and where an archive does exist it can # carry the same LTO bytecode problem as the distro libcurl.a. Building our own # pins the version and guarantees the archive is plain objects. # # curl 8.22 takes c-ares as --enable-ares[=PREFIX], not the older # --with-cares. It reads PREFIX/lib/pkgconfig/libcares.pc, which `make install` # of c-ares writes, so the prefix built below is enough. Both spellings are # wrong in one way or another: --with-cares no longer exists, and unknown # configure options only produce a warning, so a typo here silently disables # async DNS and you would not notice until the link failed. Same for # --without-librtmp, which curl 8.22 no longer knows either; it is gone below. # # Overridable: CURL_VERSION, CARES_VERSION, OUT (or $1), FORCE=1. set -e CURL_VERSION=${CURL_VERSION:-8.22.0} CARES_VERSION=${CARES_VERSION:-1.34.8} OUT=${1:-$(pwd)/static-deps} FORCE=${FORCE:-0} WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT INT TERM JOBS=$(nproc 2>/dev/null || echo 2) CA="" for c in /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-bundle.crt \ /etc/pki/tls/certs/ca-bundle.crt; do [ -f "$c" ] && { CA=$c; break; } done # fetch DEST URL...: first URL that downloads wins. fetch() { _dest=$1 shift for _u in "$@"; do echo "fetching $_u" if curl -fsSL -o "$_dest" "$_u"; then return 0 fi done echo "mkstaticdeps: no mirror worked for $_dest" >&2 return 1 } # An archive is only reused when the stamp beside it still names the version we # would build. Without this a stale libcurl.a from some earlier build silently # skips the whole curl step, which is exactly the kind of thing that hides a # broken configure line. fresh() { [ "$FORCE" != 1 ] && [ -f "$1" ] && [ -f "$2" ] && [ "$(cat "$2" 2>/dev/null)" = "$3" ] } stamp() { echo "$2" > "$1"; } mkdir -p "$OUT" # --- c-ares ------------------------------------------------------------- # curl's configure wants the headers and the .pc file, not just the archive, so # this prefix outlives the run. It lives under static-deps/, which is ignored, # and it is what makes the skip below safe: without the .pc file a rerun would # point --enable-ares at a prefix that no longer exists. CARES_PREFIX=${CARES_PREFIX:-$OUT/cares-prefix} CARES_STAMP=$OUT/.cares-version if fresh "$OUT/libcares.a" "$CARES_STAMP" "$CARES_VERSION" && [ -f "$CARES_PREFIX/lib/pkgconfig/libcares.pc" ]; then echo "libcares.a $CARES_VERSION present, skipping (FORCE=1 to rebuild)" else fetch "$WORK/cares.tgz" \ "https://c-ares.dev/downloads/c-ares-$CARES_VERSION.tar.gz" \ "https://github.com/c-ares/c-ares/releases/download/v$CARES_VERSION/c-ares-$CARES_VERSION.tar.gz" echo "extracting c-ares $CARES_VERSION" tar xzf "$WORK/cares.tgz" -C "$WORK" cd "$WORK/c-ares-$CARES_VERSION" # c-ares 1.34 has no --disable-tools; only the test suite is optional. ./configure --prefix="$CARES_PREFIX" \ --enable-static --disable-shared \ --disable-tests \ CFLAGS="-O2 -fno-lto" make -j"$JOBS" make install cp "$CARES_PREFIX/lib/libcares.a" "$OUT/libcares.a" stamp "$CARES_STAMP" "$CARES_VERSION" echo "wrote $OUT/libcares.a" fi # --- curl --------------------------------------------------------------- CURL_STAMP=$OUT/.curl-version if fresh "$OUT/libcurl.a" "$CURL_STAMP" "$CURL_VERSION"; then echo "libcurl.a $CURL_VERSION present, skipping (FORCE=1 to rebuild)" else # curl tags its GitHub releases with underscores in the version (8.22.0 -> # curl-8_22_0), the file name keeps the dots. Verified 2026-10-03. CURL_TAG=$(printf '%s' "$CURL_VERSION" | tr . _) fetch "$WORK/curl.tar.xz" \ "https://curl.se/download/curl-$CURL_VERSION.tar.xz" \ "https://github.com/curl/curl/releases/download/curl-$CURL_TAG/curl-$CURL_VERSION.tar.xz" echo "extracting curl $CURL_VERSION" tar xf "$WORK/curl.tar.xz" -C "$WORK" cd "$WORK/curl-$CURL_VERSION" set -- --disable-shared --enable-static \ --with-ssl \ --enable-ares="$CARES_PREFIX" \ --without-libpsl --without-brotli --without-zstd --without-nghttp2 \ --without-libssh2 \ --disable-ldap --disable-ldaps --disable-rtsp --disable-dict --disable-telnet \ --disable-tftp --disable-pop3 --disable-imap --disable-smtp --disable-gopher \ --disable-mqtt --disable-manual --disable-docs \ CFLAGS="-O2 -fno-lto" [ -n "$CA" ] && set -- "$@" --with-ca-bundle="$CA" ./configure "$@" make -j"$JOBS" mkdir -p "$OUT" cp lib/.libs/libcurl.a "$OUT/libcurl.a" stamp "$CURL_STAMP" "$CURL_VERSION" echo "wrote $OUT/libcurl.a" fi