Files
firmware/tools/python/build.sh
T
Oleksandr ShyshatskyiandClaude Opus 5 813973620c The payload carries a CPython 3.13 with a working sqlite3
Nothing runs on it. FF_PYTHON still points at FlashForge's 3.8.2, and the
comment beside it now says at length why changing that line is a one-line way
to break the whole userspace: klippy, Moonraker and ff-startup.py import
third-party C extensions -- tornado, lmdb, cffi, greenlet, pillow, libnacl --
that exist on this printer only as mipsel objects built against 3.8. None has
been cross-built for 3.13 yet. The interpreter ships ahead of the switch for
the same reason s6 did: it puts the artefact on real printers where it can be
measured, and it leaves the switch a one-file change rather than a build
change and a boot change at once.

Why it exists at all: the Moonraker pin is frozen at commit 9d0d09d, December
2023, for exactly one reason -- this firmware's Python has no _sqlite3 and
there is no libsqlite3 anywhere on the box, while Moonraker v0.9.0 moved its
database off lmdb onto sqlite. That is a cost paid at every release. The gate
proves the premise rather than assuming it: the stock 3.8.2, given the library
path it needs to start, imports json and zlib fine and cannot import sqlite3.

Built by bin/patch.sh from eight pinned sources with the Ingenic toolchain
that already builds c_helper.so -- gcc 7.2, glibc 2.29, cross to
mipsel-linux-gnu. Not musl, and this is not a preference: a musl interpreter
cannot dlopen a glibc c_helper.so, which is exactly how klippy loads it.

Three things about the build are load-bearing and all three were found the
hard way:

  * -EL -mnan=2008 must reach the LINK lines, not only the compile lines.
    They are baked into PATH wrapper scripts around the gcc driver rather than
    passed as CFLAGS, because trusting eight separate build systems to forward
    flags to their linkers is how you get legacy-NaN objects that the printer
    refuses to load.
  * _sqlite3 disappears silently without LIBS="-latomic -lm". A static
    libsqlite3.a carries no DT_NEEDED on libm, so configure's
    sqlite3_bind_double probe fails to link and CPython records the module as
    "missing" -- a probe failure, so nothing in the build output says why. The
    build now hard-fails when _sqlite3 is absent rather than shipping an
    interpreter whose whole purpose is gone.
  * OpenSSL's linux-mips32 target hardcodes -mips2, which collides with this
    toolchain's -mfp64. -mips32r2 as a user cflag lands after the target's and
    wins.

Built --disable-shared, unlike FlashForge's own 3.8.2. Static libpython and
static dependencies mean the shipped tree contains no shared object of ours at
all: nothing to version-skew, nothing that can resolve to a /usr/prog copy by
accident, and no new mandatory LD_LIBRARY_PATH entry -- which matters here
specifically, because removing /usr/prog version strings from anvil-env.sh is
the point. The usual objection to static, that C extensions cannot find the
interpreter's symbols, was tested rather than assumed: an out-of-tree
extension cross-built against the staged include/ imports and runs on the
printer with no libpython mapped.

It installs into $MODDIR itself, --prefix=/usr/data/anvil, the same prefix
root s6 uses: bin/python3.13 beside bin/s6-svscan, stdlib in lib/python3.13
beside lib/. One install, not two trees.

That creates a hazard worth naming, because anvil-env.sh PREPENDS $MODDIR/bin
to PATH so s6-svscan can find s6-supervise. `make install` also writes
bin/python3, and a shipped bin/python3 would then sit in front of
FlashForge's for every process that sources anvil-env.sh -- silently handing
klippy and Moonraker an interpreter with none of their extensions. So the
build deletes it and asserts bin/ holds exactly python3.13, verify.sh fails a
package carrying bin/python3, and the gate asks the shell what `python3`
resolves to on anvil-env.sh's own PATH. It answers
/usr/prog/Python-3.8.2/bin/python3, while python3.13 answers ours.

About 28MB and 600-odd files, which takes the install manifest from 70 entries
to 728 -- case-upgrade.sh still passes, which is the check that the manifest
deletion loop scales.

The build image grows a host gcc, which looks redundant beside two
cross-toolchains and is not: cross-building CPython needs a build-python of
exactly the same version to freeze modules and byte-compile the stdlib, so
patch.sh builds an x86-64 3.13 from the same tarball first.

tools/python/ keeps the measurement build and what it cost, as
tools/supervisor/ does for s6. Two gaps recorded there rather than lost: the
printer has NO CA certificates -- /etc/ssl/certs exists and is empty, so TLS
negotiates and verifies nothing, which is true of the stock interpreter too --
and libatomic.so.1 is a new runtime dependency, present on the rootfs,
measured, and removable by linking it statically if that is ever wanted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 23:15:40 +03:00

398 lines
18 KiB
Bash
Executable File

#!/bin/bash
# ---------------------------------------------------------------------------
# Cross-build CPython 3.13 for the FlashForge Creator 5 Pro (Ingenic mipsel).
#
# THIS IS THE MEASUREMENT HARNESS, NOT THE BUILD. bin/patch.sh section 5c is
# what produces the interpreter a package ships: it compiles in place, inside
# the repo's own build image, from the tarballs pinned in versions.env, and
# caches the result in work/.py313. The two are the same build -- same flags,
# same wrappers, same gates -- but this one re-execs itself inside a
# throwaway debian:bookworm, which is how every number in README.md beside it
# was measured, and which is the fastest way to try a change to the recipe
# without touching the build lane. Exactly the relationship
# tools/supervisor/build.sh has to section 5b.
#
# Target ABI, measured, non-negotiable:
# e_flags = 0x70001405 = ELF32 / little-endian / NAN2008 / O32 /
# hard-float / mips32r2
# loader = /lib/ld-linux-mipsn8.so.1 , rootfs glibc 2.33
#
# Toolchain: the repo's Ingenic gcc 7.2 / glibc 2.29 cross compiler -- the SAME
# one bin/patch.sh uses for klippy's c_helper.so. musl is forbidden: a
# musl-linked interpreter cannot dlopen a glibc c_helper.so, which is exactly
# how klippy loads it. glibc 2.29 -> 2.33 is forward compatible.
#
# `-EL -mnan=2008` must reach BOTH the compile and the link of every object.
# Rather than trust each project's build system to forward CFLAGS to its link
# line (several do not), we install PATH wrappers that bake the two flags into
# the driver itself. That is the single most important trick in this file.
#
# Usage:
# ./tools/python/build.sh # the whole thing, in docker
# ./tools/python/build.sh --in-container # the actual build
#
# Artifacts land in tools/python/out/ (gitignored, like everything else this
# repo builds): py313.tgz is the trimmed tree the payload would carry,
# py313-full.tgz is everything `make install` produced.
# ---------------------------------------------------------------------------
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
REPO="${REPO:-$(cd "$HERE/../.." && pwd)}"
TOOLCHAIN_HOST="$REPO/work/.mips-toolchain/mips-gcc720-glibc229"
# --------------------------------------------------------------- versions ---
# Kept in step with versions.env by hand. They are duplicated rather than
# sourced because this script has to run against a source tree it is
# experimenting on -- bumping a version HERE first, seeing what breaks, and
# only then editing the pin -- which is the whole reason it still exists.
PY_VER="${PY_VER:-3.13.7}"
ZLIB_VER=1.3.1
OSSL_VER=3.0.15
FFI_VER=3.4.6
SQLITE_TAR=sqlite-autoconf-3460100 # 3.46.1
XZ_VER=5.4.7
BZIP2_VER=1.0.8
EXPAT_VER=2.6.4
EXPAT_TAG=R_2_6_4
# The mod's prefix root on the printer, and the same --prefix bin/patch.sh
# builds with: bin/python3.13 beside bin/s6-svscan, stdlib in
# lib/python3.13/. The one difference is that patch.sh deletes the `python3`
# symlink (and idle3/pydoc3/*-config) out of bin/ before staging, so that
# putting $MODDIR/bin on PATH cannot quietly change what `python3` means --
# see tools/python/README.md. This harness leaves the tree as `make install`
# produced it, because the sizes reported below are of that tree.
PREFIX=/usr/data/anvil
# ------------------------------------------------------- docker wrapper -----
if [ "${1:-}" != "--in-container" ]; then
[ -x "$TOOLCHAIN_HOST/bin/mips-linux-gnu-gcc" ] || {
echo "!! no toolchain at $TOOLCHAIN_HOST" >&2
echo " run ./bin/fetch-assets.sh, then any build (bin/patch.sh unpacks it)" >&2
exit 1; }
exec docker run --rm \
-v "$TOOLCHAIN_HOST":/toolchain:ro \
-v "$HERE":/work \
-e PY_VER="$PY_VER" -e PY_EXTRA_CONFIGURE="${PY_EXTRA_CONFIGURE:-}" \
-e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \
-w /work debian:bookworm \
bash /work/build.sh --in-container
fi
# =========================== everything below runs inside the container ======
START=$(date +%s)
export DEBIAN_FRONTEND=noninteractive
TC=/toolchain
W=/work
SRC=$W/src # tarball cache (survives between runs)
B=$W/build # scratch: unpacked trees
HOSTPY=$W/hostpy # the x86-64 build-Python 3.13 (--with-build-python)
DEP=$W/deproot # cross-built C libraries, STATIC. Not shipped.
STAGE=$W/stage # DESTDIR: what actually goes on the printer
OUT=$W/out
mkdir -p "$SRC" "$B" "$HOSTPY" "$DEP" "$OUT"
log() { echo; echo "===== $* ====="; }
log "apt: build dependencies"
apt-get update -qq
apt-get install -y -qq --no-install-recommends \
build-essential curl ca-certificates pkg-config perl file xz-utils \
zlib1g-dev libssl-dev libffi-dev >/dev/null
# ------------------------------------------------------- the wrappers -------
# -EL -mnan=2008 on every invocation, compile or link. Without -mnan=2008 the
# objects are legacy-NaN and the kernel refuses to load the result.
log "toolchain wrappers (-EL -mnan=2008 baked in)"
mkdir -p /opt/xw/bin
for t in gcc g++ cpp; do
cat > /opt/xw/bin/mips-linux-gnu-$t <<EOF
#!/bin/sh
exec $TC/bin/mips-linux-gnu-$t -EL -mnan=2008 "\$@"
EOF
chmod +x /opt/xw/bin/mips-linux-gnu-$t
done
for t in ar as ld nm objcopy objdump ranlib readelf strip strings size; do
ln -sf $TC/bin/mips-linux-gnu-$t /opt/xw/bin/mips-linux-gnu-$t
done
export PATH=/opt/xw/bin:$PATH
mips-linux-gnu-gcc --version | head -1
# gate the wrapper itself before building 300MB on top of it
echo 'int main(void){return 0;}' > /tmp/abi.c
mips-linux-gnu-gcc /tmp/abi.c -o /tmp/abi.out
FLAGS=$(mips-linux-gnu-readelf -h /tmp/abi.out | awk '/Flags:/{print $2}' | tr -d ,)
echo "wrapper produces e_flags=$FLAGS"
[ "$FLAGS" = "0x70001405" ] || { echo "!! wrong ABI from the wrapper"; exit 1; }
# ------------------------------------------------------------ fetch ---------
# No sha256 here on purpose: versions.env holds the pins and
# bin/fetch-assets.sh is what enforces them for anything that ships. This
# harness downloads whatever version you point it at, which is what makes it
# useful for deciding whether a bump is safe BEFORE it becomes a pin.
fetch() { # fetch <url> -> $SRC/<basename>, cached
local url=$1 f="$SRC/$(basename "$1")"
[ -s "$f" ] || { echo ">> $url"; curl -sSLf -o "$f.part" "$url" && mv "$f.part" "$f"; }
}
log "fetch sources"
fetch https://www.python.org/ftp/python/$PY_VER/Python-$PY_VER.tgz
fetch https://zlib.net/fossils/zlib-$ZLIB_VER.tar.gz
fetch https://github.com/openssl/openssl/releases/download/openssl-$OSSL_VER/openssl-$OSSL_VER.tar.gz
fetch https://github.com/libffi/libffi/releases/download/v$FFI_VER/libffi-$FFI_VER.tar.gz
fetch https://www.sqlite.org/2024/$SQLITE_TAR.tar.gz
fetch https://github.com/tukaani-project/xz/releases/download/v$XZ_VER/xz-$XZ_VER.tar.gz
fetch https://sourceware.org/pub/bzip2/bzip2-$BZIP2_VER.tar.gz
fetch https://github.com/libexpat/libexpat/releases/download/$EXPAT_TAG/expat-$EXPAT_VER.tar.gz
unpack() { rm -rf "$B/${2}"; tar -xf "$SRC/$1" -C "$B"; }
# ============================================ 1. host CPython 3.13 ==========
# Cross-building CPython needs a build-Python of the *same* version: the
# Makefile runs it to freeze modules, generate deepfreeze sources and byte
# compile the stdlib. A distro python3.11 will not do.
if [ ! -x "$HOSTPY/bin/python3.13" ]; then
log "host CPython $PY_VER (x86-64, for --with-build-python)"
unpack Python-$PY_VER.tgz Python-$PY_VER
cd "$B/Python-$PY_VER"
./configure --prefix="$HOSTPY" --without-ensurepip >/dev/null
make -j"$(nproc)" >/dev/null
make install >/dev/null
cd /
else
log "host CPython already built -- reusing $HOSTPY"
fi
"$HOSTPY/bin/python3.13" -VV
# ============================================ 2. cross C libraries ==========
# All STATIC (-fPIC) into $DEP. Rationale: linking them into the interpreter
# and its extension modules means the shipped tree has no .so of ours to find
# at runtime, so there is no LD_LIBRARY_PATH for dependencies, no chance of
# picking up FlashForge's /usr/prog copies, and nothing to version-skew. The
# only cost is a few MB of duplicated libcrypto between _ssl.so and _hashlib.so.
export CC=mips-linux-gnu-gcc
export CXX=mips-linux-gnu-g++
export AR=mips-linux-gnu-ar
export RANLIB=mips-linux-gnu-ranlib
export STRIP=mips-linux-gnu-strip
export CFLAGS="-O2 -fPIC -D_FILE_OFFSET_BITS=64"
export CPPFLAGS="-I$DEP/include"
export LDFLAGS="-L$DEP/lib"
# pkg-config must see ONLY our sysroot, never the container's /usr/lib.
export PKG_CONFIG_LIBDIR="$DEP/lib/pkgconfig"
export PKG_CONFIG_PATH="$DEP/lib/pkgconfig"
HOSTTRIPLE=mips-linux-gnu
BUILDTRIPLE=x86_64-linux-gnu
log "zlib $ZLIB_VER"
unpack zlib-$ZLIB_VER.tar.gz zlib-$ZLIB_VER
cd "$B/zlib-$ZLIB_VER"
CHOST=$HOSTTRIPLE ./configure --prefix="$DEP" --static >/dev/null
make -j"$(nproc)" >/dev/null && make install >/dev/null
log "openssl $OSSL_VER"
unpack openssl-$OSSL_VER.tar.gz openssl-$OSSL_VER
cd "$B/openssl-$OSSL_VER"
# linux-mips32 is the O32 target. openssldir points at where the interpreter
# will look for certs ON THE PRINTER, not at a build path.
#
# Two traps, both hit for real:
# * `no-docs` only exists from OpenSSL 3.1. On 3.0.15 it is an "Unsupported
# options" hard error, not a warning.
# * the linux-mips32 target hardcodes `-mips2` into its cflags. This
# toolchain defaults to -mfp64 (hard-float, fp64 registers), and gcc
# rejects `-mgp32 -mfp64` on anything below mips32r2:
# error: '-mgp32' and '-mfp64' can only be combined if the target
# supports the mfhc1 and mthc1 instructions
# User cflags land AFTER the target's on the command line, so appending
# -mips32r2 puts the ISA back where the printer actually is. If a future
# OpenSSL orders them the other way, linux-generic32 (portable C, no mips
# assembly) is the fallback and is exercised below.
OSSL_TARGET=linux-mips32
OSSL_ISA=-mips32r2
./Configure $OSSL_TARGET \
--prefix="$DEP" --libdir=lib --openssldir=$PREFIX/ssl \
no-shared no-tests \
-fPIC -O2 $OSSL_ISA -D_FILE_OFFSET_BITS=64 >/dev/null
if ! make -j"$(nproc)" >"$OUT/openssl.log" 2>&1; then
echo "!! $OSSL_TARGET failed; falling back to linux-generic32 (no mips asm)"
tail -20 "$OUT/openssl.log"
make distclean >/dev/null 2>&1 || true
./Configure linux-generic32 \
--prefix="$DEP" --libdir=lib --openssldir=$PREFIX/ssl \
no-shared no-tests no-asm \
-fPIC -O2 -D_FILE_OFFSET_BITS=64 >/dev/null
make -j"$(nproc)" >"$OUT/openssl.log" 2>&1
fi
make install_sw >/dev/null
log "libffi $FFI_VER"
unpack libffi-$FFI_VER.tar.gz libffi-$FFI_VER
cd "$B/libffi-$FFI_VER"
./configure --host=$HOSTTRIPLE --build=$BUILDTRIPLE --prefix="$DEP" \
--disable-shared --enable-static --with-pic --disable-docs >/dev/null
make -j"$(nproc)" >/dev/null && make install >/dev/null
log "sqlite ($SQLITE_TAR) -- THE reason this whole exercise exists"
unpack $SQLITE_TAR.tar.gz $SQLITE_TAR
cd "$B/$SQLITE_TAR"
./configure --host=$HOSTTRIPLE --build=$BUILDTRIPLE --prefix="$DEP" \
--disable-shared --enable-static --with-pic \
--disable-readline --disable-editline >/dev/null
make -j"$(nproc)" >/dev/null && make install >/dev/null
log "xz (liblzma) $XZ_VER"
unpack xz-$XZ_VER.tar.gz xz-$XZ_VER
cd "$B/xz-$XZ_VER"
./configure --host=$HOSTTRIPLE --build=$BUILDTRIPLE --prefix="$DEP" \
--disable-shared --enable-static --with-pic \
--disable-xz --disable-xzdec --disable-lzmadec --disable-lzmainfo \
--disable-scripts --disable-doc --disable-nls >/dev/null
make -j"$(nproc)" >/dev/null && make install >/dev/null
log "bzip2 $BZIP2_VER"
unpack bzip2-$BZIP2_VER.tar.gz bzip2-$BZIP2_VER
cd "$B/bzip2-$BZIP2_VER"
# bzip2 has no configure; drive its Makefile directly.
make -j"$(nproc)" libbz2.a CC="$CC" AR="$AR" RANLIB="$RANLIB" \
CFLAGS="-O2 -fPIC -D_FILE_OFFSET_BITS=64 -Wall -Winline" >/dev/null
install -m644 libbz2.a "$DEP/lib/"
install -m644 bzlib.h "$DEP/include/"
log "expat $EXPAT_VER"
unpack expat-$EXPAT_VER.tar.gz expat-$EXPAT_VER
cd "$B/expat-$EXPAT_VER"
./configure --host=$HOSTTRIPLE --build=$BUILDTRIPLE --prefix="$DEP" \
--disable-shared --enable-static --with-pic \
--without-docbook --without-examples --without-tests >/dev/null
make -j"$(nproc)" >/dev/null && make install >/dev/null
echo "-- $DEP/lib --"; ls "$DEP/lib"
# ============================================ 3. CPython, cross =============
log "CPython $PY_VER cross for $HOSTTRIPLE"
unpack Python-$PY_VER.tgz Python-$PY_VER
cd "$B/Python-$PY_VER"
# Answers configure cannot probe because it may not run target binaries.
cat > "$B/config.site" <<'EOF'
ac_cv_file__dev_ptmx=yes
ac_cv_file__dev_ptc=no
ac_cv_buggy_getaddrinfo=no
ac_cv_little_endian_double=yes
ac_cv_big_endian_double=no
ac_cv_mixed_endian_double=no
ac_cv_working_tzset=yes
ac_cv_have_long_long_format=yes
ac_cv_no_strict_aliasing=no
ac_cv_pthread_system_supported=yes
EOF
export CONFIG_SITE="$B/config.site"
export CPPFLAGS="-I$DEP/include"
export LDFLAGS="-L$DEP/lib"
# -latomic: 64-bit atomics on mips32 are out-of-line calls into libatomic, and
# CPython 3.13's _Py_atomic_* on 64-bit types needs them.
# -lm: because the sqlite/lzma/expat libraries here are STATIC. A shared
# libsqlite3.so carries its own DT_NEEDED on libm; a libsqlite3.a does not,
# so configure's `checking for sqlite3_bind_double in -lsqlite3` link probe
# fails with a wall of `undefined reference to floor/log/pow/...` and 3.13
# silently records _sqlite3 as "missing" -- which is the one module this
# whole build exists for. It is a probe failure, not a compile failure, so
# nothing in the build output says why.
export LIBS="-latomic -lm"
export CFLAGS="-O2 -D_FILE_OFFSET_BITS=64"
# Bypass pkg-config for sqlite and state the static link line outright, so the
# -lm above cannot be reordered out from under the probe.
export LIBSQLITE3_CFLAGS="-I$DEP/include"
export LIBSQLITE3_LIBS="-L$DEP/lib -lsqlite3 -lm"
./configure \
--host=$HOSTTRIPLE --build=$BUILDTRIPLE \
--with-build-python="$HOSTPY/bin/python3.13" \
--prefix=$PREFIX \
--disable-shared \
--without-ensurepip \
--disable-test-modules \
--with-openssl="$DEP" \
--with-system-expat \
${PY_EXTRA_CONFIGURE:-} 2>&1 | tee "$OUT/configure.log" | tail -60
echo "-- configure: modules it decided it could not build --"
grep -iE 'could not be built|missing|failed to build' -A6 "$OUT/configure.log" | tail -40 || true
make -j"$(nproc)" 2>&1 | tee "$OUT/make.log" | tail -40
echo "-- build: modules that failed --"
grep -iE 'necessary bits|could not be found|failed to build these' -A10 "$OUT/make.log" | tail -40 || true
rm -rf "$STAGE"
make install DESTDIR="$STAGE" 2>&1 | tail -20
# ============================================ 4. gates + package ============
log "ABI gate on every ELF in the stage"
# The measured target word is 0x70001405 for an EXECUTABLE. Shared objects
# additionally carry EF_MIPS_PIC (0x2) and so read 0x70001407 -- that is
# correct and unavoidable for a DYN, and klippy's own c_helper.so has it too.
# So gate on the ABI ATTRIBUTES, which is also what bin/patch.sh does
# (`readelf -h | grep -q nan2008`), and pin the whole word per ELF type.
BAD=0; N=0
while IFS= read -r f; do
case "$(file -b "$f")" in *ELF*) ;; *) continue;; esac
hdr=$(mips-linux-gnu-readelf -h "$f" 2>/dev/null)
fl=$(echo "$hdr" | awk '/Flags:/{print $2}' | tr -d ,)
typ=$(echo "$hdr" | awk '/^ Type:/{print $2}')
want=0x70001405; [ "$typ" = "DYN" ] && want=0x70001407
case "$hdr" in
*nan2008*o32*mips32r2*) ;;
*) echo "!! $f not nan2008/o32/mips32r2"; BAD=1; continue;;
esac
if [ "$fl" != "$want" ]; then
echo "!! $f type=$typ flags=$fl want=$want"; BAD=1
fi
N=$((N+1))
done < <(find "$STAGE" -type f)
[ $BAD -eq 0 ] && echo "$N ELF objects, all nan2008/o32/mips32r2 (EXEC 0x70001405, DYN 0x70001407)" || exit 1
# _sqlite3 is the reason this build exists. Its absence must be a hard error,
# not a line in a 400-line make log that nobody reads.
if ! ls "$STAGE$PREFIX/lib/python3.13/lib-dynload/"_sqlite3*.so >/dev/null 2>&1; then
echo "!! NO _sqlite3 MODULE WAS BUILT -- see $OUT/configure.log and config.log"
exit 1
fi
echo "_sqlite3 present: $(basename "$(ls "$STAGE$PREFIX/lib/python3.13/lib-dynload/"_sqlite3*.so)")"
echo "-- NEEDED of the interpreter --"
mips-linux-gnu-readelf -d "$STAGE$PREFIX/bin/python3.13" | grep -E 'NEEDED|RPATH|RUNPATH' || true
echo "-- lib-dynload --"
ls "$STAGE$PREFIX/lib/python3.13/lib-dynload/" | sed 's/\.cpython.*//' | sort | tr '\n' ' '; echo
log "sizes"
FULL=$(du -sm "$STAGE" | cut -f1)
# trimmed: what would actually ship
rm -rf "$W/stage-trim"
cp -a "$STAGE" "$W/stage-trim"
rm -rf "$W/stage-trim$PREFIX/lib/python3.13/test" \
"$W/stage-trim$PREFIX/lib/python3.13/idlelib" \
"$W/stage-trim$PREFIX/lib/python3.13/tkinter" \
"$W/stage-trim$PREFIX/lib/python3.13/turtledemo" \
"$W/stage-trim$PREFIX/lib/python3.13/config-"* \
"$W/stage-trim$PREFIX/include" \
"$W/stage-trim$PREFIX/share"
find "$W/stage-trim" -name '__pycache__' -prune -exec rm -rf {} + 2>/dev/null || true
find "$W/stage-trim" -name '*.a' -delete
mips-linux-gnu-strip "$W/stage-trim$PREFIX/bin/python3.13" 2>/dev/null || true
find "$W/stage-trim$PREFIX/lib/python3.13/lib-dynload" -name '*.so' \
-exec mips-linux-gnu-strip {} + 2>/dev/null || true
TRIM=$(du -sm "$W/stage-trim" | cut -f1)
echo "staged : ${FULL} MB, $(find "$STAGE" -type f | wc -l) files"
echo "trimmed : ${TRIM} MB, $(find "$W/stage-trim" -type f | wc -l) files"
log "tarballs"
# The tarball's root is 'usr/data/anvil/...' relative -- unpack with -C /
( cd "$STAGE" && tar -czf "$OUT/py313-full.tgz" usr )
( cd "$W/stage-trim" && tar -czf "$OUT/py313.tgz" usr )
ls -l "$OUT"/*.tgz
chown -R "${HOST_UID:-0}:${HOST_GID:-0}" "$W" 2>/dev/null || true
END=$(date +%s)
log "done in $(( (END-START)/60 ))m $(( (END-START)%60 ))s"