Something went wrong. Try again.
Build official language runtimes and databases from source as position independent executables, from typed Pkl recipes.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246#!/usr/bin/env bash## Builds every third-party library CPython links against as a *static* archive# compiled -fPIC, installed into $DEPS_PREFIX.## Only .a files end up in $DEPS_PREFIX/lib, which is the whole trick: when# CPython later links -lssl / -lsqlite3 / -lz, the linker has nothing but static# archives to choose from, so the interpreter absorbs them. -fPIC keeps every# object eligible for the final PIE link.## The result is a python that is dynamically linked against glibc only — the# same shape as the python-build-standalone distributions uv downloads.## Inputs (environment):# DEPS_PREFIX install prefix (required)# DEPS_SRC scratch dir for sources (default $PWD/deps-src)# DEPS_LOCK sha256 manifest to verify against (default alongside this script)# DEPS_DIGESTS where to write observed digests (default $PWD/deps-digests.txt)# OPENSSL_VERSION override the pinned OpenSSL version (optional)
set -euo pipefail
DEPS_PREFIX="${DEPS_PREFIX:?DEPS_PREFIX must be set}"DEPS_SRC="${DEPS_SRC:-$PWD/deps-src}"DEPS_LOCK="${DEPS_LOCK:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deps.lock}"DEPS_DIGESTS="${DEPS_DIGESTS:-$PWD/deps-digests.txt}"JOBS="$(nproc)"
# Pinned versions. Bump these deliberately; a 404 here means a version was# withdrawn upstream and the pin needs updating.ZLIB_VERSION=1.3.1BZIP2_VERSION=1.0.8XZ_VERSION=5.4.7 # deliberately pre-5.6.0 / post-backdoor-safeOPENSSL_VERSION="${OPENSSL_VERSION:-3.5.0}"LIBFFI_VERSION=3.4.6SQLITE_YEAR=2024SQLITE_VERSION=3460100 # 3.46.1NCURSES_VERSION=6.5READLINE_VERSION=8.2UTIL_LINUX_SERIES=2.40UTIL_LINUX_VERSION=2.40.2MPDECIMAL_VERSION=4.0.0
mkdir -p "$DEPS_PREFIX" "$DEPS_SRC": > "$DEPS_DIGESTS"
export CFLAGS="-fPIC -O2 -fstack-protector-strong"export CXXFLAGS="$CFLAGS"export CPPFLAGS="-I${DEPS_PREFIX}/include"export LDFLAGS="-L${DEPS_PREFIX}/lib"export PKG_CONFIG_PATH="${DEPS_PREFIX}/lib/pkgconfig"
log() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
# Downloads $1 into $DEPS_SRC, verifies it against $DEPS_LOCK when the manifest# has an entry for it, and always records the digest so an unpinned dependency# can be pinned after the first run.fetch() { local url="$1" file file="$(basename "$url")"
if [ ! -f "$DEPS_SRC/$file" ]; then curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -o "$DEPS_SRC/$file" "$url" fi
local actual expected='' actual="$(sha256sum "$DEPS_SRC/$file" | awk '{print $1}')" if [ -f "$DEPS_LOCK" ]; then expected="$(awk -v f="$file" '$2 == f {print $1}' "$DEPS_LOCK" | head -n1)" fi
if [ -n "$expected" ]; then if [ "$actual" != "$expected" ]; then echo "::error::checksum mismatch for $file: expected $expected, got $actual" >&2 exit 1 fi echo "verified $file" else echo "::warning::$file is not pinned in $(basename "$DEPS_LOCK") — recording $actual" >&2 fi
printf '%s %s\n' "$actual" "$file" >> "$DEPS_DIGESTS" printf '%s' "$DEPS_SRC/$file"}
# Unpacks $1 into $DEPS_SRC/$2 and cds there.unpack() { local tarball="$1" dir="$2" rm -rf "${DEPS_SRC:?}/$dir" mkdir -p "$DEPS_SRC/$dir" tar -xf "$tarball" -C "$DEPS_SRC/$dir" --strip-components=1 cd "$DEPS_SRC/$dir"}
build_zlib() { log "zlib $ZLIB_VERSION" # The fossils/ directory keeps every release forever; the top level does not. unpack "$(fetch "https://zlib.net/fossils/zlib-${ZLIB_VERSION}.tar.gz")" zlib ./configure --prefix="$DEPS_PREFIX" --static make -j"$JOBS" make install}
build_bzip2() { log "bzip2 $BZIP2_VERSION" unpack "$(fetch "https://sourceware.org/pub/bzip2/bzip2-${BZIP2_VERSION}.tar.gz")" bzip2 # bzip2 has no configure; build only the static library and install by hand so # no shared object is produced. make -j"$JOBS" libbz2.a CFLAGS="$CFLAGS -D_FILE_OFFSET_BITS=64" install -Dm644 libbz2.a "$DEPS_PREFIX/lib/libbz2.a" install -Dm644 bzlib.h "$DEPS_PREFIX/include/bzlib.h"}
build_xz() { log "xz $XZ_VERSION" unpack "$(fetch "https://github.com/tukaani-project/xz/releases/download/v${XZ_VERSION}/xz-${XZ_VERSION}.tar.gz")" xz ./configure --prefix="$DEPS_PREFIX" \ --disable-shared --enable-static --with-pic \ --disable-xz --disable-xzdec --disable-lzmadec --disable-lzmainfo \ --disable-lzma-links --disable-scripts --disable-doc --disable-nls make -j"$JOBS" make install}
build_openssl() { log "openssl $OPENSSL_VERSION" unpack "$(fetch "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz")" openssl # --libdir=lib stops OpenSSL from using lib64 on 64-bit hosts, which keeps a # single -L path working for every dependency. ./Configure \ --prefix="$DEPS_PREFIX" \ --openssldir="$DEPS_PREFIX/ssl" \ --libdir=lib \ no-shared no-module no-tests no-docs \ -fPIC make -j"$JOBS" make install_sw}
build_libffi() { log "libffi $LIBFFI_VERSION" unpack "$(fetch "https://github.com/libffi/libffi/releases/download/v${LIBFFI_VERSION}/libffi-${LIBFFI_VERSION}.tar.gz")" libffi ./configure --prefix="$DEPS_PREFIX" \ --disable-shared --enable-static --with-pic \ --disable-multi-os-directory --disable-docs make -j"$JOBS" make install}
build_sqlite() { log "sqlite $SQLITE_VERSION" unpack "$(fetch "https://www.sqlite.org/${SQLITE_YEAR}/sqlite-autoconf-${SQLITE_VERSION}.tar.gz")" sqlite # Feature set mirrors what general-purpose Python distributions enable. ./configure --prefix="$DEPS_PREFIX" --disable-shared --enable-static --with-pic \ CFLAGS="$CFLAGS \ -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_RTREE \ -DSQLITE_ENABLE_COLUMN_METADATA -DSQLITE_ENABLE_MATH_FUNCTIONS \ -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_SECURE_DELETE \ -DSQLITE_MAX_VARIABLE_NUMBER=250000" make -j"$JOBS" make install}
build_ncurses() { log "ncurses $NCURSES_VERSION" unpack "$(fetch "https://ftp.gnu.org/gnu/ncurses/ncurses-${NCURSES_VERSION}.tar.gz")" ncurses # --disable-db-install: the terminfo database is NOT bundled (it is large and # every Linux ships one). --with-fallbacks compiles entries for the common # terminals straight into the archive so curses still works without a DB. ./configure --prefix="$DEPS_PREFIX" \ --without-shared --with-normal --with-pic \ --enable-widec --enable-pc-files \ --with-pkg-config-libdir="$DEPS_PREFIX/lib/pkgconfig" \ --without-cxx-binding --without-ada --without-manpages --without-tests \ --without-debug --without-progs --disable-db-install \ --with-fallbacks=linux,xterm,xterm-256color,screen,screen-256color,vt100,dumb \ --disable-stripping make -j"$JOBS" make install
# readline and CPython variously look for -lncurses / -ltinfo. With # --with-termlib omitted everything lives in libncursesw.a, so alias the rest. local l for l in libncurses libtinfo libtinfow; do ln -sf libncursesw.a "$DEPS_PREFIX/lib/${l}.a" done}
build_readline() { log "readline $READLINE_VERSION" unpack "$(fetch "https://ftp.gnu.org/gnu/readline/readline-${READLINE_VERSION}.tar.gz")" readline # readline probes for a termcap provider; point it at the static ncursesw we # just installed instead of letting it find a system shared library. ./configure --prefix="$DEPS_PREFIX" \ --disable-shared --enable-static --with-curses \ bash_cv_termcap_lib=libncursesw \ CPPFLAGS="$CPPFLAGS -I${DEPS_PREFIX}/include/ncursesw" make -j"$JOBS" SHLIB_LIBS="-lncursesw" make install}
build_libuuid() { log "util-linux (libuuid only) $UTIL_LINUX_VERSION" unpack "$(fetch "https://www.kernel.org/pub/linux/utils/util-linux/v${UTIL_LINUX_SERIES}/util-linux-${UTIL_LINUX_VERSION}.tar.xz")" util-linux ./configure --prefix="$DEPS_PREFIX" \ --disable-all-programs --enable-libuuid \ --disable-shared --enable-static --with-pic \ --disable-nls --without-python --without-systemd --disable-asciidoc make -j"$JOBS" make install}
build_mpdecimal() { log "mpdecimal $MPDECIMAL_VERSION" unpack "$(fetch "https://www.bytereef.org/software/mpdecimal/releases/mpdecimal-${MPDECIMAL_VERSION}.tar.gz")" mpdecimal # Only needed by CPython versions that dropped the bundled copy; harmless # otherwise, and it keeps `decimal` on the fast C implementation either way. ./configure --prefix="$DEPS_PREFIX" --disable-shared --enable-static --disable-cxx make -j"$JOBS" make install}
build_zlibbuild_bzip2build_xzbuild_opensslbuild_libffibuild_sqlitebuild_ncursesbuild_readlinebuild_libuuidbuild_mpdecimal
log "static libraries in $DEPS_PREFIX/lib"ls -1 "$DEPS_PREFIX"/lib/*.a
# A stray .so here would silently become a runtime dependency of the interpreter.if compgen -G "$DEPS_PREFIX/lib/*.so*" > /dev/null; then echo "::error::shared objects were installed into $DEPS_PREFIX/lib — the build would not be self-contained:" >&2 ls -1 "$DEPS_PREFIX"/lib/*.so* >&2 exit 1fi
log "dependency digests"cat "$DEPS_DIGESTS"