Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
fb5b531
Bench: Add Cython (pure and typed) and numba columns to the TypedPyth…
b-re-w Oct 3, 2026
b159f27
TypedPython: decode cl.exe output in the console code page
b-re-w Oct 29, 2026
460a267
Merge pull request #351 from thisisthepy/feat/typedpython-msvc-output…
b-re-w Oct 29, 2026
2a75e57
TypedPython: publish the package to PyPI by trusted publishing
b-re-w Oct 29, 2026
6eb83b1
Merge pull request #355 from thisisthepy/feat/typedpython-pypi
b-re-w Oct 29, 2026
a59096f
TypedPython: @compiled is the only mark; drop the '# typedpython: com…
b-re-w Oct 29, 2026
f702f48
TypedPython: no PyPI publishing; the wheel ships with the GitHub Rele…
b-re-w Oct 29, 2026
66a1410
Merge pull request #356 from thisisthepy/feat/typedpython-no-pypi
b-re-w Oct 29, 2026
cca2ef5
TypedPython tests: child processes install the compiled builtin thems…
b-re-w Oct 29, 2026
cc14724
Merge pull request #357 from thisisthepy/feat/typedpython-drop-module…
b-re-w Oct 29, 2026
0e54f8e
Feat: replace # typedpython: parallel comment with @compiled(parallel…
b-re-w Oct 31, 2026
cf96f26
Merge pull request #359 from thisisthepy/feat/typedpython-compiled-pa…
b-re-w Oct 31, 2026
8824385
Feat: add per-function @compiled(fastmath=True) opt-in (#332)
b-re-w Oct 31, 2026
1463c75
Merge pull request #360 from thisisthepy/feat/typedpython-fastmath
b-re-w Oct 31, 2026
2b1bac3
TypedPython: TSan test ignores races against new_threadstate reusing …
b-re-w Oct 31, 2026
5d81106
Merge pull request #361 from thisisthepy/feat/tsan-new-threadstate
b-re-w Oct 31, 2026
371e967
Test: Make mid() loop heavier in test_eval_breaker to avoid CI timing…
b-re-w Nov 1, 2026
185965c
Merge pull request #362 from thisisthepy/feat/tp-eval-breaker-flake
b-re-w Nov 1, 2026
c14d264
Feat: Switch notebook-e2e native-image CI to Maven Central and stage …
b-re-w Nov 1, 2026
c92c58b
Merge pull request #363 from thisisthepy/feat/phase-a-native-image-ci
b-re-w Nov 1, 2026
afec35c
Fix: Point Gradle toolchain resolution at GRAALVM_HOME in native-imag…
b-re-w Nov 4, 2026
83a7d0b
Merge pull request #364 from thisisthepy/feat/fix-native-image-ci-too…
b-re-w Nov 4, 2026
5b7ac9d
TypedPython: cache generated C for the pypackpack compile slot (#113)
b-re-w Nov 4, 2026
c50b53d
Merge pull request #365 from thisisthepy/feat/tp-c-cache
b-re-w Nov 4, 2026
831174c
Fix: Disable Gradle toolchain auto-detect in native-image CI so Liber…
b-re-w Nov 4, 2026
5464619
Merge pull request #366 from thisisthepy/feat/disable-toolchain-autod…
b-re-w Nov 4, 2026
693252c
Fix: Require BellSoft vendor for JDK 25 native-image toolchain (#251)
b-re-w Nov 4, 2026
7add3ea
Merge pull request #367 from thisisthepy/feat/bellsoft-vendor-native-…
b-re-w Nov 4, 2026
986c736
TypedPython: narrow GCC fastmath to reassociation and FMA contract (#…
b-re-w Nov 5, 2026
decc90e
Feat: add macosArm64 and linuxX64 Kotlin/Native desktop targets (#191)
b-re-w Nov 5, 2026
9394936
Merge pull request #370 from thisisthepy/feat/kn-desktop-targets
b-re-w Nov 5, 2026
5d93fe9
TypedPython test: ignore a probe that succeeds only after the call re…
b-re-w Nov 5, 2026
8ccfda4
TypedPython: fill a fresh New's slots by direct release stores (#125)
b-re-w Nov 5, 2026
6f7b669
TypedPython: MSVC fastmath enables FMA contraction only (#368)
b-re-w Nov 5, 2026
eb17d99
TypedPython: emit-c CLI and manifest for pypackpack's compile slot (#…
b-re-w Nov 5, 2026
c44a7d9
Merge pull request #371 from thisisthepy/feat/tp-new-fixed-direct
b-re-w Nov 5, 2026
23262de
Merge pull request #369 from thisisthepy/feat/tp-gcc-fastmath
b-re-w Nov 5, 2026
9975a41
Merge pull request #372 from thisisthepy/feat/tp-emit-c
b-re-w Nov 5, 2026
8f27c26
Feat: add TypedPythonEmitCTask and manifest v1 mapper in Gradle plugi…
b-re-w Nov 7, 2026
c8ad08c
Merge pull request #373 from thisisthepy/feat/tp-emit-c-gradle
b-re-w Nov 7, 2026
3c2ed3d
Feat(gradle-plugin): register emitTypedPythonC from pythonBindings DS…
b-re-w Nov 7, 2026
a148c07
Merge pull request #374 from thisisthepy/feat/tp-emit-c-plugin-wiring
b-re-w Nov 7, 2026
e01d422
Publish the library, KSP processor and Gradle plugin to Maven Central…
b-re-w Nov 7, 2026
8fa5a33
maven-central: install uv for the typedpython-gate bundle (#375)
b-re-w Nov 7, 2026
71e4598
Merge pull request #376 from thisisthepy/feat/maven-central-publish
b-re-w Nov 7, 2026
cf90930
Release: bump to 3.15.0-alpha03, the first Maven Central release (#377)
b-re-w Nov 7, 2026
88bf6a1
Merge pull request #378 from thisisthepy/feat/release-alpha03
b-re-w Nov 7, 2026
ac5e0dd
Publish python-multiplatform-compose to Maven Central; drop stale And…
b-re-w Nov 7, 2026
684d3f1
Merge pull request #380 from thisisthepy/feat/publish-compose-central
b-re-w Nov 7, 2026
aade0f8
AGENTS 7: what counts as the owner's decision (shared rule)
b-re-w Nov 7, 2026
6cf78d2
Merge pull request #381 from thisisthepy/feat/owner-decision-rule
b-re-w Nov 7, 2026
9e0718b
AGENTS 3, 10: clean up on merge; agents only check compilation
b-re-w Nov 7, 2026
b48ac32
Merge pull request #382 from thisisthepy/feat/agent-build-rule
b-re-w Nov 7, 2026
7638d75
Merge remote-tracking branch 'origin/develop' into feat/bench-cython-…
b-re-w Nov 7, 2026
9507e6d
Merge pull request #384 from thisisthepy/feat/bench-cython-numba
b-re-w Nov 7, 2026
6eae2c4
TypedPython: record benchmark output identity in CI and index the dif…
b-re-w Nov 12, 2026
2815a65
Merge pull request #385 from thisisthepy/feat/tp-bench-outputs
b-re-w Nov 12, 2026
e066013
Docs: the last pip install example becomes uv pip install (#136)
b-re-w Nov 12, 2026
e308e7e
Merge pull request #388 from thisisthepy/feat/docs-rules-cleanup
b-re-w Nov 12, 2026
f64c1bc
Python3: public finalize() and flush(), one-argument eval(expression)…
b-re-w Nov 12, 2026
f16e5f2
Compose host: add macosArm64 and linuxX64 Kotlin/Native desktop targe…
b-re-w Nov 12, 2026
bffb9bf
build-ft-mobile: build free-threaded CPython for x86_64 Android too (…
b-re-w Nov 12, 2026
58ba011
CI: build free-threaded CPython 3.15t for wasm32-emscripten with pthr…
b-re-w Nov 12, 2026
d4c8f64
Merge pull request #397 from thisisthepy/feat/wasm-ft-build
b-re-w Nov 12, 2026
2f9267d
Merge pull request #396 from thisisthepy/feat/android-ft-x86_64
b-re-w Nov 12, 2026
4963a4f
Merge pull request #393 from thisisthepy/feat/compose-kn-desktop-targets
b-re-w Nov 12, 2026
92e35d3
Merge pull request #392 from thisisthepy/feat/interpreter-shutdown
b-re-w Nov 12, 2026
44d4be6
Release: sync from 92e35d348ec6e96ed7b1a8b4c4cc2b97a6d06be5 92e35d3
github-actions[bot] Nov 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .github/scripts/ft-mobile/build-android.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
#!/usr/bin/env bash
# Build free-threaded CPython (--disable-gil) for aarch64-linux-android with
# CPython's own tooling (Platforms/Android). Usage: build-android.sh [VERSION]
# CPython's own tooling (Platforms/Android). Usage: build-android.sh [VERSION] [HOST]
# HOST is aarch64-linux-android (default) or x86_64-linux-android.
# Needs ANDROID_HOME (SDK with cmdline-tools/latest) and a JDK 17, 21 or 25.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=common.sh
source "$here/common.sh"

PY_VERSION="${1:-3.15.0}"
HOST=aarch64-linux-android
HOST="${2:-aarch64-linux-android}"
case "$HOST" in
aarch64-linux-android) elf_arch="ARM aarch64" ;;
x86_64-linux-android) elf_arch="x86-64" ;;
*) echo "unsupported HOST: $HOST" >&2; exit 2 ;;
esac
: "${ANDROID_HOME:?ANDROID_HOME must point at the Android SDK}"

src="$(fetch_source "$PY_VERSION")"
Expand Down Expand Up @@ -55,9 +61,9 @@ require_gil_disabled "$check/prefix/include/python${minor}t/pyconfig.h"
lib="$(ls "$check"/prefix/lib/libpython${minor}t*.so* 2>/dev/null | head -n 1 || true)"
[ -n "$lib" ] || die "no libpython${minor}t*.so in the package"
log "ok: $lib"
file "$lib" | tee /dev/stderr | grep -q 'ARM aarch64' || die "$lib is not an aarch64 ELF"
file "$lib" | tee /dev/stderr | grep -q "$elf_arch" || die "$lib is not an $elf_arch ELF"
[ -d "$check/prefix/lib/python${minor}t" ] || die "no lib/python${minor}t in the package"

cp "$archive" "$FT_OUT/"
echo "Android archive:" >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
echo "Android archive ($HOST):" >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
record_sha256 "$FT_OUT/$(basename "$archive")"
249 changes: 249 additions & 0 deletions .github/scripts/ft-wasm/build-wasm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
#!/usr/bin/env bash
# Build free-threaded CPython (--disable-gil) with pthreads for wasm32-emscripten,
# with CPython's own driver (Platforms/emscripten). Step one of #175: a build
# experiment. Usage: build-wasm.sh [VERSION]
#
# Environment:
# FT_WORK scratch directory (default .tmp/ft-wasm)
# FT_OUT receives the archive the workflow uploads (default .tmp/ft-wasm-out)
# FT_WASM_EMSDK_VERSION Emscripten to install; empty = the version CPython itself pins
# (Platforms/emscripten/config.toml, 4.0.19 in 3.15.0)
# FT_WASM_TRAMPOLINE_PATCH apply (default) | skip; see patch_trampoline below
#
# Needs: a C toolchain for the native build Python, git, curl, node on PATH
# (CPython 3.15.0 pins node 24 in config.toml), python3 >= 3.11 to run the driver.
#
# This is a separate script, not a mode of python-multiplatform/scripts/wasm/build-cpython.sh:
# that script is pinned to CPython 3.14.2 / Emscripten 5.0.3 / pyemscripten_2026_0, clones a
# pinned git commit, writes into .caches/ and stages the runtime the wasmJs suite runs against.
# This job builds a different CPython (3.15.0 tarball) with a different Emscripten and must not
# touch that runtime. The flags below are the #175 investigation's proposal, not a decision.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"

export FT_WORK="${FT_WORK:-$PWD/.tmp/ft-wasm}"
export FT_OUT="${FT_OUT:-$PWD/.tmp/ft-wasm-out}"
# fetch_source, require_gil_disabled, record_sha256, py_minor
# shellcheck source-path=SCRIPTDIR source=../ft-mobile/common.sh
source "$here/../ft-mobile/common.sh"
log() { echo "[ft-wasm] $*" >&2; }
die() { echo "[ft-wasm] ERROR: $*" >&2; exit 1; }

PY_VERSION="${1:-3.15.0}"
TRAMPOLINE_PATCH="${FT_WASM_TRAMPOLINE_PATCH:-apply}"
minor="$(py_minor "$PY_VERSION")"
HOST=wasm32-emscripten

# Compile and link flags (agent proposal from the #175 investigation, not confirmed).
# The driver hardcodes CFLAGS=-DPY_CALL_TRAMPOLINE -sUSE_BZIP2 and appends user args after
# it; autoconf keeps the last assignment, so the full string is repeated here.
# -fwasm-exceptions -sSUPPORT_LONGJMP=wasm same unwinding ABI as build-cpython.sh
# -pthread atomics + shared memory (configure also adds it
# to CFLAGS_NODIST for --enable-wasm-pthreads)
# -sPTHREAD_POOL_SIZE=8 pre-created Workers: Thread.start() blocks until
# the thread runs, and a Worker cannot start while
# the JS thread that asked for it is blocked
# -sMAXIMUM_MEMORY=2GB shared memory needs a declared maximum
ABI_FLAGS="-fwasm-exceptions -sSUPPORT_LONGJMP=wasm"
HOST_CFLAGS="-DPY_CALL_TRAMPOLINE -sUSE_BZIP2 $ABI_FLAGS -pthread"
HOST_LDFLAGS="$ABI_FLAGS -pthread -sPTHREAD_POOL_SIZE=8 -sMAXIMUM_MEMORY=2GB"

src="$(fetch_source "$PY_VERSION")"
cd "$src"
[ -d Platforms/emscripten ] || die "no Platforms/emscripten in CPython $PY_VERSION (3.15+ expected)"

# ---- Emscripten -----------------------------------------------------------
# One emsdk under FT_WORK. The driver is then run without --emsdk-cache, so it uses the
# activated environment ("Build will use EMSDK from current environment").
pinned="$(sed -n 's/^emscripten-version = "\(.*\)"$/\1/p' Platforms/emscripten/config.toml)"
[ -n "$pinned" ] || die "no emscripten-version in Platforms/emscripten/config.toml"
EMSDK_VERSION="${FT_WASM_EMSDK_VERSION:-$pinned}"
log "Emscripten pinned by CPython $PY_VERSION: $pinned; using: $EMSDK_VERSION"
emsdk="$FT_WORK/emsdk"
if [ ! -x "$emsdk/emsdk" ]; then
git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$emsdk"
fi
"$emsdk/emsdk" install "$EMSDK_VERSION"
"$emsdk/emsdk" activate "$EMSDK_VERSION"
# shellcheck disable=SC1091
EMSDK_QUIET=1 source "$emsdk/emsdk_env.sh"
emcc --version > "$FT_WORK/emcc-version.txt"
emcc_line="$(head -n 1 "$FT_WORK/emcc-version.txt")"
log "$emcc_line"
case "$emcc_line" in *" $EMSDK_VERSION "*) ;; *) die "emcc is not $EMSDK_VERSION" ;; esac
node --version

# ---- patch: call trampoline under shared memory ---------------------------
# Python/emscripten_trampoline.c instantiates a wasm-gc trampoline module in pre-run. That
# module is compiled with -Wl,--import-memory and no -pthread (Makefile.pre.in), so it imports
# a NON-shared env.memory; giving it the shared wasmMemory of a pthread build is a link error,
# and the `new WebAssembly.Instance` call is outside the try block, so startup throws.
# Pyodide #237 also reports that the addFunction'ed pointer exists only in the main thread's
# table ("table index is out of bounds" on the first call from a spawned thread).
# Minimal fix: when memory is shared, return 0, which selects the existing JS trampoline
# (_PyEM_TrampolineCall_JS, wasmTable.get(func) on the calling thread). The edit is asserted,
# so an upstream change fails the build instead of silently skipping the patch.
# FT_WASM_TRAMPOLINE_PATCH=skip leaves the source alone, to observe the unpatched failure.
patch_trampoline() {
python3 - Python/emscripten_trampoline.c <<'PATCH'
import sys
from pathlib import Path
p = Path(sys.argv[1]); t = p.read_text()
marker = "pmp-175: shared memory"
if marker not in t:
old = "function getPyEMTrampolinePtr() {\n"
new = (old +
" // " + marker + ": the wasm-gc trampoline imports non-shared memory and its\n"
" // addFunction pointer is valid only on this thread; use the JS trampoline.\n"
" if (typeof SharedArrayBuffer !== \"undefined\" && wasmMemory.buffer instanceof SharedArrayBuffer) {\n"
" return 0;\n"
" }\n")
assert t.count(old) == 1, "getPyEMTrampolinePtr changed shape; re-check the patch"
p.write_text(t.replace(old, new))
print("patched Python/emscripten_trampoline.c (shared memory -> JS trampoline)")
PATCH
}
case "$TRAMPOLINE_PATCH" in
apply) patch_trampoline ;;
skip) log "trampoline patch skipped (FT_WASM_TRAMPOLINE_PATCH=skip)" ;;
*) die "FT_WASM_TRAMPOLINE_PATCH must be apply or skip, got '$TRAMPOLINE_PATCH'" ;;
esac

# ---- native build Python (free-threaded too, as in ft-mobile) -------------
python3 --version
python3 Platforms/emscripten configure-build-python -- --disable-gil
python3 Platforms/emscripten make-build-python
python3 Platforms/emscripten pythoninfo-build
cross-build/build/python -c 'import sys, sysconfig; assert sysconfig.get_config_var("Py_GIL_DISABLED") == 1; print("build python free-threaded:", sys.version)'

# ---- libffi and mpdec with -pthread ---------------------------------------
# The driver builds them with CFLAGS="-O2 -fPIC -DWASM_BIGINT" (make_libffi.sh) and
# CFLAGS=-fPIC (mpdec). emcc appends EMCC_CFLAGS to every invocation, so -pthread reaches
# both without editing the driver. Checked below by the objects' target_features section.
EMCC_CFLAGS="-pthread" python3 Platforms/emscripten make-libffi
EMCC_CFLAGS="-pthread" python3 Platforms/emscripten make-mpdec

prefix="cross-build/$HOST/prefix"
for lib in "$prefix/lib/libffi.a" "$prefix/lib/libmpdec.a"; do
[ -f "$lib" ] || die "no $lib"
# wasm target_features entries are <prefix byte><LEB length><name>: "+", 7, "atomics".
python3 - "$lib" <<'CHECK' || die "$lib was not compiled with atomics (-pthread)"
import sys
data = open(sys.argv[1], "rb").read()
sys.exit(0 if b"+\x07atomics" in data else 1)
CHECK
log "ok: $lib has +atomics"
done

# ---- host configure -------------------------------------------------------
python3 Platforms/emscripten configure-host --clean --host-runner node -- \
"CFLAGS=$HOST_CFLAGS" \
"LDFLAGS=$HOST_LDFLAGS" \
--disable-gil --enable-wasm-pthreads

hostdir="cross-build/$HOST/build/python"
require_gil_disabled "$hostdir/pyconfig.h"
# configure adds -sUSE_PTHREADS to LDFLAGS_NODIST (CONFIGURE_LDFLAGS_NODIST in the
# Makefile) only when --enable-wasm-pthreads took effect.
grep -Eq '^CONFIGURE_LDFLAGS_NODIST=.*-sUSE_PTHREADS' "$hostdir/Makefile" \
|| die "configure did not enable wasm pthreads (no -sUSE_PTHREADS in LDFLAGS_NODIST)"
grep -Eq '^ABIFLAGS=[[:space:]]*t$' "$hostdir/Makefile" || die "host Makefile ABIFLAGS is not t"
log "ok: host configured with --disable-gil --enable-wasm-pthreads, ABIFLAGS=t"

# ---- host build -----------------------------------------------------------
# make-host also runs `python.sh --version`, so a startup failure (for example the trampoline
# instantiation above, with FT_WASM_TRAMPOLINE_PATCH=skip) fails this step.
python3 Platforms/emscripten make-host

# ---- verification ---------------------------------------------------------
# python.wasm must use shared memory with a declared maximum.
node - "$hostdir/python.wasm" <<'NODE' || die "python.wasm has no shared memory"
const b = require("fs").readFileSync(process.argv[2]);
const leb = (i) => { let r = 0, s = 0; for (;;) { const x = b[i++]; r += (x & 0x7f) * 2 ** s; s += 7; if (!(x & 0x80)) return [r, i]; } };
const str = (i) => { let n; [n, i] = leb(i); return [b.slice(i, i + n).toString(), i + n]; };
const limits = (i) => { const flags = b[i++]; let min, max = null; [min, i] = leb(i); if (flags & 1) [max, i] = leb(i); return [{ flags, min, max }, i]; };
let i = 8, mem = null;
while (i < b.length && !mem) {
const id = b[i++]; let size; [size, i] = leb(i); const end = i + size;
if (id === 2) { let n, j; [n, j] = leb(i);
for (let k = 0; k < n && !mem; k++) { let m, f; [m, j] = str(j); [f, j] = str(j); const kind = b[j++];
if (kind === 0) { [, j] = leb(j); }
else if (kind === 1) { j++; [, j] = limits(j); }
else if (kind === 2) { let l; [l, j] = limits(j); mem = { where: `import ${m}.${f}`, ...l }; }
else if (kind === 3) { j += 2; }
else if (kind === 4) { j++; [, j] = leb(j); }
else throw new Error(`unknown import kind ${kind}`); } }
if (id === 5) { let j; [, j] = leb(i); const [l] = limits(j); mem = { where: "defined", ...l }; }
i = end;
}
if (!mem) { console.error("no memory found"); process.exit(1); }
const shared = (mem.flags & 2) !== 0;
console.log(`python.wasm memory: ${mem.where} shared=${shared} min=${mem.min} max=${mem.max} pages`);
process.exit(shared && mem.max !== null ? 0 : 1);
NODE

# The interpreter itself: GIL off at run time, four real threads.
run_smoke() {
local label="$1"; shift
local out="$FT_WORK/smoke-$label.log" rc
set +e
timeout 600 "$@" "$here/smoke.py" > "$out" 2>&1
rc=$?
set -e
cat "$out"
[ "$rc" -eq 0 ] || die "smoke test ($label) exited $rc (124 = timed out, a thread start may have deadlocked)"
grep -q '^FT-WASM-SMOKE-OK ' "$out" || die "smoke test ($label) did not print FT-WASM-SMOKE-OK"
log "ok: smoke test ($label)"
}
run_smoke build-tree "$PWD/$hostdir/python.sh"

# ---- archive --------------------------------------------------------------
# The driver has no install or package step for Emscripten, so the archive is assembled here.
# The stdlib directory is named by the built interpreter's own sysconfig, so a non-FT build
# (python3.15) fails the name check instead of being renamed.
stdlib_name="$(timeout 300 "$PWD/$hostdir/python.sh" -c 'import os, sysconfig; print(os.path.basename(sysconfig.get_path("stdlib")))' | tail -n 1)"
[ "$stdlib_name" = "python${minor}t" ] || die "interpreter's stdlib dir is '$stdlib_name', expected python${minor}t"
log "ok: interpreter reports lib/$stdlib_name"

name="python-${PY_VERSION}t-$HOST"
stage="$FT_WORK/stage/$name"
rm -rf "$FT_WORK/stage"; mkdir -p "$stage/lib/$stdlib_name/lib-dynload" "$stage/include/$stdlib_name"
cp "$hostdir/python.mjs" "$hostdir/python.wasm" "$stage/"
cp "$hostdir/node_entry.mjs" "$hostdir/streams.mjs" "$stage/"
# Everything but the test suite; the zip-and-omit selection is left to whoever stages it.
tar -C Lib --exclude='./test' --exclude='__pycache__' -cf - . | tar -C "$stage/lib/$stdlib_name" -xf -
cp "$hostdir/$(cat "$hostdir/pybuilddir.txt")"/_sysconfigdata_*.py "$stage/lib/$stdlib_name/"
touch "$stage/lib/$stdlib_name/lib-dynload/.empty"
cp -R Include/. "$stage/include/$stdlib_name/"
cp "$hostdir/pyconfig.h" "$stage/include/$stdlib_name/"
{
echo "CPython $PY_VERSION, --disable-gil --enable-wasm-pthreads, $HOST"
echo "emcc: $emcc_line"
echo "CFLAGS=$HOST_CFLAGS"
echo "LDFLAGS=$HOST_LDFLAGS"
echo "trampoline patch: $TRAMPOLINE_PATCH"
echo "built from https://www.python.org/ftp/python/ (Python-$PY_VERSION.tgz)"
} > "$stage/BUILD-INFO.txt"

archive="$FT_OUT/$name.tar.gz"
mkdir -p "$FT_OUT"
tar -C "$FT_WORK/stage" -czf "$archive" "$name"

# Re-check the archive itself, unpacked somewhere else, and run it with PYTHONHOME pointing
# at it (node_entry.mjs copies process.env into the Emscripten ENV, and NODEFS mounts /).
check="$FT_WORK/wasm-check"
rm -rf "$check"; mkdir -p "$check"
tar -xzf "$archive" -C "$check"
root="$check/$name"
require_gil_disabled "$root/include/python${minor}t/pyconfig.h"
[ -d "$root/lib/python${minor}t" ] || die "no lib/python${minor}t in the archive"
[ -f "$root/lib/python${minor}t/os.py" ] || die "no lib/python${minor}t/os.py in the archive"
ls "$root/lib/python${minor}t"/_sysconfigdata_*.py > /dev/null || die "no _sysconfigdata in the archive"
nodeflags=""
[ "$(node -p 'process.versions.node.split(".")[0]')" = "24" ] && nodeflags="--experimental-wasm-jspi"
# shellcheck disable=SC2086
run_smoke archive env PYTHONHOME="$root" node $nodeflags "$root/node_entry.mjs" \
--this-program="$root/bin/python${minor}t"

echo "wasm32-emscripten archive (Emscripten $EMSDK_VERSION, trampoline patch: $TRAMPOLINE_PATCH):" >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
record_sha256 "$archive"
42 changes: 42 additions & 0 deletions .github/scripts/ft-wasm/smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Smoke test for the free-threaded wasm32-emscripten build (#175), run by build-wasm.sh
# under node. Prints one line starting with FT-WASM-SMOKE-OK, which the script greps for;
# any failed assertion exits non-zero instead.
import sys
import sysconfig
import threading

assert sys.platform == "emscripten", sys.platform
assert sysconfig.get_config_var("Py_GIL_DISABLED") == 1, "not a free-threaded build"
assert sys._is_gil_enabled() is False, "the GIL is enabled at run time"

N_THREADS = 4
N_ITER = 200_000
results = [None] * N_THREADS


def work(i):
total = 0
for k in range(N_ITER):
total += k * (i + 1)
results[i] = (threading.get_ident(), total)


threads = [threading.Thread(target=work, args=(i,)) for i in range(N_THREADS)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=120)
alive = [t.name for t in threads if t.is_alive()]
assert not alive, f"threads still running after join: {alive}"

expected = [(i + 1) * N_ITER * (N_ITER - 1) // 2 for i in range(N_THREADS)]
got = [r[1] if r else None for r in results]
assert got == expected, f"results {got} != {expected}"
idents = {r[0] for r in results}
assert len(idents) == N_THREADS, f"expected {N_THREADS} distinct thread idents, got {idents}"
assert threading.get_ident() not in idents, "a worker ran on the main thread"
# Importing threading must not have re-enabled the GIL.
assert sys._is_gil_enabled() is False, "the GIL was re-enabled during the run"

print(f"FT-WASM-SMOKE-OK gil={sys._is_gil_enabled()} threads={N_THREADS} "
f"abiflags={sys.abiflags} version={sys.version.split()[0]}")
Loading