From 472f775f912143f4efe456e224ffa30dc4d831a2 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 6 May 2026 09:45:06 -0700 Subject: [PATCH 01/44] Create a `_shared` module --- Platforms/WASI/_build.py | 60 ++++++++++++--------------------------- Platforms/WASI/_shared.py | 29 +++++++++++++++++++ 2 files changed, 47 insertions(+), 42 deletions(-) create mode 100644 Platforms/WASI/_shared.py diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index 76d2853163baa9e..73dc4f9bb99cba6 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -18,23 +18,12 @@ except ImportError: from os import cpu_count +import _shared -CHECKOUT = HERE = pathlib.Path(__file__).parent - -while CHECKOUT != CHECKOUT.parent: - if (CHECKOUT / "configure").is_file(): - break - CHECKOUT = CHECKOUT.parent -else: - raise FileNotFoundError( - "Unable to find the root of the CPython checkout by looking for 'configure'" - ) - -CROSS_BUILD_DIR = CHECKOUT / "cross-build" # Build platform can also be found via `config.guess`. -BUILD_DIR = CROSS_BUILD_DIR / sysconfig.get_config_var("BUILD_GNU_TYPE") +BUILD_DIR = _shared.CROSS_BUILD_DIR / sysconfig.get_config_var("BUILD_GNU_TYPE") -LOCAL_SETUP = CHECKOUT / "Modules" / "Setup.local" +LOCAL_SETUP = _shared.CHECKOUT / "Modules" / "Setup.local" LOCAL_SETUP_MARKER = ( b"# Generated by Platforms/WASI .\n" b"# Required to statically build extension modules." @@ -193,7 +182,7 @@ def configure_build_python(context, working_dir): log("๐Ÿ“", f"Creating {LOCAL_SETUP} ...") LOCAL_SETUP.write_bytes(LOCAL_SETUP_MARKER) - configure = [os.path.relpath(CHECKOUT / "configure", working_dir)] + configure = [os.path.relpath(_shared.CHECKOUT / "configure", working_dir)] if context.args: configure.extend(context.args) @@ -229,7 +218,7 @@ def wasi_sdk(context): ) return wasi_sdk_path - with (HERE / "config.toml").open("rb") as file: + with (_shared.HERE / "config.toml").open("rb") as file: config = tomllib.load(file) wasi_sdk_version = config["targets"]["wasi-sdk"] @@ -317,31 +306,18 @@ def wasi_sdk_env(context): return env -def host_triple(context): - """Determine the target triple for the WASI host build.""" - if context.host_triple: - return context.host_triple - - with (HERE / "config.toml").open("rb") as file: - config = tomllib.load(file) - - # Cache the result. - context.host_triple = config["targets"]["host-triple"] - return context.host_triple - - -@subdir(lambda context: CROSS_BUILD_DIR / host_triple(context), clean_ok=True) +@subdir(lambda context: _shared.CROSS_BUILD_DIR / _shared.host_triple(context), clean_ok=True) def configure_wasi_python(context, working_dir): """Configure the WASI/host build.""" - config_site = os.fsdecode(HERE / "config.site-wasm32-wasi") + config_site = os.fsdecode(_shared.HERE / "config.site-wasm32-wasi") - wasi_build_dir = working_dir.relative_to(CHECKOUT) + wasi_build_dir = working_dir.relative_to(_shared.CHECKOUT) args = { "WASMTIME": "wasmtime", "ARGV0": f"/{wasi_build_dir}/python.wasm", - "CHECKOUT": os.fsdecode(CHECKOUT), - "WASMTIME_CONFIG_PATH": os.fsdecode(HERE / "wasmtime.toml"), + "_shared.CHECKOUT": os.fsdecode(_shared.CHECKOUT), + "WASMTIME_CONFIG_PATH": os.fsdecode(_shared.HERE / "wasmtime.toml"), } # Check dynamically for wasmtime in case it was specified manually via # `--host-runner`. @@ -358,10 +334,10 @@ def configure_wasi_python(context, working_dir): build_python = os.fsdecode(build_python_path()) # The path to `configure` MUST be relative, else `python.wasm` is unable # to find the stdlib due to Python not recognizing that it's being - # executed from within a checkout. + # executed from within a _shared.CHECKOUT. configure = [ - os.path.relpath(CHECKOUT / "configure", working_dir), - f"--host={host_triple(context)}", + os.path.relpath(_shared.CHECKOUT / "configure", working_dir), + f"--host={_shared.host_triple(context)}", f"--build={BUILD_DIR.name}", f"--with-build-python={build_python}", ] @@ -384,7 +360,7 @@ def configure_wasi_python(context, working_dir): sys.stdout.flush() -@subdir(lambda context: CROSS_BUILD_DIR / host_triple(context)) +@subdir(lambda context: _shared.CROSS_BUILD_DIR / _shared.host_triple(context)) def make_wasi_python(context, working_dir): """Run `make` for the WASI/host build.""" call( @@ -394,7 +370,7 @@ def make_wasi_python(context, working_dir): ) exec_script = working_dir / "python.sh" - call([exec_script, "--version"], quiet=False) + call([exec_script, "-c", "import sys; print(sys.version)"], quiet=False) log( "๐ŸŽ‰", f"Use `{exec_script.relative_to(pathlib.Path().absolute())}` " @@ -404,9 +380,9 @@ def make_wasi_python(context, working_dir): def clean_contents(context): """Delete all files created by this script.""" - if CROSS_BUILD_DIR.exists(): - log("๐Ÿงน", f"Deleting {CROSS_BUILD_DIR} ...") - shutil.rmtree(CROSS_BUILD_DIR) + if _shared.CROSS_BUILD_DIR.exists(): + log("๐Ÿงน", f"Deleting {_shared.CROSS_BUILD_DIR} ...") + shutil.rmtree(_shared.CROSS_BUILD_DIR) if LOCAL_SETUP.exists(): if LOCAL_SETUP.read_bytes() == LOCAL_SETUP_MARKER: diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py new file mode 100644 index 000000000000000..c5e1b7055fe006d --- /dev/null +++ b/Platforms/WASI/_shared.py @@ -0,0 +1,29 @@ +import pathlib +import tomllib + + +CHECKOUT = HERE = pathlib.Path(__file__).parent + +while CHECKOUT != CHECKOUT.parent: + if (CHECKOUT / "configure").is_file(): + break + CHECKOUT = CHECKOUT.parent +else: + raise FileNotFoundError( + "Unable to find the root of the CPython checkout by looking for 'configure'" + ) + +CROSS_BUILD_DIR = CHECKOUT / "cross-build" + + +def host_triple(context): + """Determine the target triple for the WASI host build.""" + if context.host_triple: + return context.host_triple + + with (HERE / "config.toml").open("rb") as file: + config = tomllib.load(file) + + # Cache the result. + context.host_triple = config["targets"]["host-triple"] + return context.host_triple From 15503a3a56bd644dd412a072523d484acb5dbdc1 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 3 Jun 2026 14:16:13 -0700 Subject: [PATCH 02/44] Attempt at getting `lib/` working --- Platforms/WASI/__main__.py | 8 ++- Platforms/WASI/_build.py | 14 ++-- Platforms/WASI/_package.py | 142 +++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 Platforms/WASI/_package.py diff --git a/Platforms/WASI/__main__.py b/Platforms/WASI/__main__.py index b8513a004f18e5f..0a1c6ff02f757c0 100644 --- a/Platforms/WASI/__main__.py +++ b/Platforms/WASI/__main__.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 -__lazy_modules__ = ["_build"] +__lazy_modules__ = ["_build", "_package"] import argparse import os import pathlib import _build +import _package HERE = pathlib.Path(__file__).parent @@ -53,6 +54,9 @@ def main(): build_host = subcommands.add_parser( "build-host", help="Build the host/WASI Python" ) + package = subcommands.add_parser( + "package", help="Package the host/WASI Python into an archive" + ) subcommands.add_parser( "clean", help="Delete files and directories created by this script" ) @@ -151,6 +155,8 @@ def main(): _build.make_wasi_python(context) case "clean": _build.clean_contents(context) + case "package": + _package.package(context) case _: raise ValueError(f"Unknown subcommand {context.subcommand!r}") diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index 73dc4f9bb99cba6..53edaca87d28ba5 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -21,7 +21,9 @@ import _shared # Build platform can also be found via `config.guess`. -BUILD_DIR = _shared.CROSS_BUILD_DIR / sysconfig.get_config_var("BUILD_GNU_TYPE") +BUILD_DIR = _shared.CROSS_BUILD_DIR / sysconfig.get_config_var( + "BUILD_GNU_TYPE" +) LOCAL_SETUP = _shared.CHECKOUT / "Modules" / "Setup.local" LOCAL_SETUP_MARKER = ( @@ -147,6 +149,7 @@ def call(command, *, context=None, quiet=False, **kwargs): subprocess.check_call(command, **kwargs, stdout=stdout, stderr=stderr) +@functools.cache def build_python_path(): """The path to the build Python binary.""" binary = BUILD_DIR / "python" @@ -160,6 +163,7 @@ def build_python_path(): return binary +@functools.cache def build_python_is_pydebug(): """Find out if the build Python is a pydebug build.""" test = "import sys, test.support; sys.exit(test.support.Py_DEBUG)" @@ -206,6 +210,7 @@ def make_build_python(context, working_dir): log("๐ŸŽ‰", f"{binary} {version}") +@functools.cache def wasi_sdk(context): """Find the path to the WASI SDK.""" if wasi_sdk_path := context.wasi_sdk_path: @@ -267,6 +272,7 @@ def wasi_sdk(context): return wasi_sdk_path +@functools.cache def wasi_sdk_env(context): """Calculate environment variables for building with wasi-sdk.""" wasi_sdk_path = wasi_sdk(context) @@ -306,7 +312,7 @@ def wasi_sdk_env(context): return env -@subdir(lambda context: _shared.CROSS_BUILD_DIR / _shared.host_triple(context), clean_ok=True) +@subdir(lambda context: _shared.wasi_build_path(context), clean_ok=True) def configure_wasi_python(context, working_dir): """Configure the WASI/host build.""" config_site = os.fsdecode(_shared.HERE / "config.site-wasm32-wasi") @@ -334,7 +340,7 @@ def configure_wasi_python(context, working_dir): build_python = os.fsdecode(build_python_path()) # The path to `configure` MUST be relative, else `python.wasm` is unable # to find the stdlib due to Python not recognizing that it's being - # executed from within a _shared.CHECKOUT. + # executed from within a _shared.checkout. configure = [ os.path.relpath(_shared.CHECKOUT / "configure", working_dir), f"--host={_shared.host_triple(context)}", @@ -360,7 +366,7 @@ def configure_wasi_python(context, working_dir): sys.stdout.flush() -@subdir(lambda context: _shared.CROSS_BUILD_DIR / _shared.host_triple(context)) +@subdir(lambda context: _shared.wasi_build_path(context)) def make_wasi_python(context, working_dir): """Run `make` for the WASI/host build.""" call( diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py new file mode 100644 index 000000000000000..0281ac68f33b303 --- /dev/null +++ b/Platforms/WASI/_package.py @@ -0,0 +1,142 @@ +import functools +import json +import pathlib + +import _shared + +# https://reproducible-builds.org/docs/archives/ +# https://sethmlarson.dev/security-developer-in-residence-weekly-report-14 +# - mtime +# - uid +# - gid +# - uname +# - gname +# https://docs.python.org/3/library/tarfile.html#writing-examples + +# โ˜ bin +# โ˜ pythonN.M.wasmtime +# โŒ idleN.M +# โŒ pydocN.M +# โ˜ include/pythonN.Md? +# โ˜ pyconfig.h +# โ˜ .h files +# โœ… lib +# โœ… pkgconfig (from Misc/) +# โœ… pythonN.M +# โŒ lib-dynload +# โœ… LICENSE.txt (license_file()) +# โœ… Stuff from build/lib.* (build_dir_files()) +# โœ… Lib/ +# โŒ share/man/man1/ +# โ˜ python.wasm + + +@functools.cache +def build_dir(context): + """The path to the build directory pointed to by pybuilddir.txt.""" + relative_dir = ( + (_shared.wasi_build_path(context) / "pybuilddir.txt") + .read_text() + .strip() + ) + return _shared.wasi_build_path(context) / relative_dir + + +@functools.cache +def build_details(context): + """Get the JSON contents of build-details.json.""" + with (build_dir(context) / "build-details.json").open() as f: + return json.load(f) + + +def pythonXY(context, support_debug=False): + """Calculate the "pythonX.Y" part of a path. + + If *support_debug* is True, then "d" is appended as appropriate. + """ + details = build_details(context) + major = details["language"]["version_info"]["major"] + minor = details["language"]["version_info"]["minor"] + name = f"python{major}.{minor}" + if support_debug and "d" in details["abi"]["flags"]: + name += "d" + return name + + +@functools.cache +def lib_python(context): + pathlib.PurePath("lib") / pythonXY(context) + + +def license_file(context): + """Have /LICENSE end up as lib/pythonXY/LICENSE.txt.""" + return (lib_python(context) / "LICENSE.txt", _shared.CHECKOUT / "LICENSE") + + +def build_dir_files(context): + """Have build/lib.* files end up in lib/pythonXY. + + Symlinks are skipped as those files are covered by handling + /Modules. + """ + return [ + (lib_python(context) / path.name, path) + for path in build_dir(context).iterdir() + if path.is_file(follow_symlinks=False) + ] + + +def stdlib_files(context): + """Have /Lib files end up in lib/pythonXY.""" + lib_dir = _shared.CHECKOUT / "Lib" + lib_files = [] + for root, dirs, files in lib_dir: + try: + dirs.remove("__pycache__") + except ValueError: + pass + + for file in files: + file_path = pathlib.Path(root) / file + details = ( + lib_python(context) / file_path.relative_to(lib_dir), + file_path + ) + lib_files.append(details) + return lib_files + + +def pkgconfig_files(context): + """Have /Misc/python*.pc end up in lib/pkgconfig. + + Each file ends up being listed under `python3` and `python-3.N`. + """ + misc_dir = _shared.wasi_build_path(context) / "Misc" + details = build_details(context) + major = details["language"]["version_info"]["major"] + minor = details["language"]["version_info"]["minor"] + pkgconfig = lib_python(context) / "pkgconfig" + return [ + (pkgconfig / f"python{major}.pc", misc_dir / "python.pc"), + (pkgconfig / f"python-{major}.{minor}.pc", misc_dir / "python.pc"), + (pkgconfig / f"python3-embed.pc", misc_dir / "python-embed.pc"), + (pkgconfig / f"python-{major}.{minor}-embed.pc", misc_dir / "python-embed.pc"), + ] + + +def filename_stem(context): + """Calculate the stem of the archive file name.""" + # XXX File name: python-3.15.0a8-wasm32-wasip1.tar.xz + # Include/patchlevel.h: `#define PY_VERSION "3.15.0a8+"` + # `build-details.json` for either/both version and triple + + +def package(context): + dist = _shared.CHECKOUT / "dist" + files = [] + files.append(license_file(context)) + files.extend(build_dir_files(context)) + files.extend(stdlib_files(context)) + files.extend(pkgconfig_files(context)) + for dest, src in files: + src.copy(dist / dest) From db0bff7407c9141f98e9bcd0f0da9c119eb1a713 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 3 Jun 2026 14:33:39 -0700 Subject: [PATCH 03/44] Ignore "/dist/" --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 118eb5ee76e8051..dbe62c670127d1a 100644 --- a/.gitignore +++ b/.gitignore @@ -139,6 +139,7 @@ Tools/unicode/data/ /config.status.lineno /.ccache /cross-build*/ +/dist/ /jit_stencils*.h /jit_unwind_info*.h .jit-stamp From b97d99722b9842f814feb3593c96a98397c8fcf8 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 3 Jun 2026 14:34:00 -0700 Subject: [PATCH 04/44] Get `lib/` working --- Platforms/WASI/_package.py | 17 +++++++++-------- Platforms/WASI/_shared.py | 7 ++++++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 0281ac68f33b303..bc201aa77cc1cfc 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -1,6 +1,6 @@ -import functools import json import pathlib +import shutil import _shared @@ -31,7 +31,6 @@ # โ˜ python.wasm -@functools.cache def build_dir(context): """The path to the build directory pointed to by pybuilddir.txt.""" relative_dir = ( @@ -42,7 +41,6 @@ def build_dir(context): return _shared.wasi_build_path(context) / relative_dir -@functools.cache def build_details(context): """Get the JSON contents of build-details.json.""" with (build_dir(context) / "build-details.json").open() as f: @@ -63,9 +61,8 @@ def pythonXY(context, support_debug=False): return name -@functools.cache def lib_python(context): - pathlib.PurePath("lib") / pythonXY(context) + return pathlib.PurePath("lib") / pythonXY(context) def license_file(context): @@ -90,7 +87,7 @@ def stdlib_files(context): """Have /Lib files end up in lib/pythonXY.""" lib_dir = _shared.CHECKOUT / "Lib" lib_files = [] - for root, dirs, files in lib_dir: + for root, dirs, files in lib_dir.walk(): try: dirs.remove("__pycache__") except ValueError: @@ -115,7 +112,7 @@ def pkgconfig_files(context): details = build_details(context) major = details["language"]["version_info"]["major"] minor = details["language"]["version_info"]["minor"] - pkgconfig = lib_python(context) / "pkgconfig" + pkgconfig = pathlib.PurePath("lib") / "pkgconfig" return [ (pkgconfig / f"python{major}.pc", misc_dir / "python.pc"), (pkgconfig / f"python-{major}.{minor}.pc", misc_dir / "python.pc"), @@ -133,10 +130,14 @@ def filename_stem(context): def package(context): dist = _shared.CHECKOUT / "dist" + if dist.exists(): + shutil.rmtree(dist) files = [] files.append(license_file(context)) files.extend(build_dir_files(context)) files.extend(stdlib_files(context)) files.extend(pkgconfig_files(context)) for dest, src in files: - src.copy(dist / dest) + target = dist / dest + target.parent.mkdir(parents=True, exist_ok=True) + src.copy(target) diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index c5e1b7055fe006d..b498416f9064743 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -18,7 +18,7 @@ def host_triple(context): """Determine the target triple for the WASI host build.""" - if context.host_triple: + if getattr(context, "host_triple", None): return context.host_triple with (HERE / "config.toml").open("rb") as file: @@ -27,3 +27,8 @@ def host_triple(context): # Cache the result. context.host_triple = config["targets"]["host-triple"] return context.host_triple + + +def wasi_build_path(context): + """Determine the path to the WASI build directory.""" + return CROSS_BUILD_DIR / host_triple(context) From 080851636df8508a7fcb5a012b3e96aa935e8731 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 3 Jun 2026 16:12:46 -0700 Subject: [PATCH 05/44] Add some caching --- Platforms/WASI/_build.py | 8 ++++---- Platforms/WASI/_package.py | 3 +++ Platforms/WASI/_shared.py | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index 53edaca87d28ba5..88bde6920713498 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -149,7 +149,7 @@ def call(command, *, context=None, quiet=False, **kwargs): subprocess.check_call(command, **kwargs, stdout=stdout, stderr=stderr) -@functools.cache +@_shared.forced_cache def build_python_path(): """The path to the build Python binary.""" binary = BUILD_DIR / "python" @@ -163,7 +163,7 @@ def build_python_path(): return binary -@functools.cache +@_shared.forced_cache def build_python_is_pydebug(): """Find out if the build Python is a pydebug build.""" test = "import sys, test.support; sys.exit(test.support.Py_DEBUG)" @@ -210,7 +210,7 @@ def make_build_python(context, working_dir): log("๐ŸŽ‰", f"{binary} {version}") -@functools.cache +@_shared.forced_cache def wasi_sdk(context): """Find the path to the WASI SDK.""" if wasi_sdk_path := context.wasi_sdk_path: @@ -272,7 +272,7 @@ def wasi_sdk(context): return wasi_sdk_path -@functools.cache +@_shared.forced_cache def wasi_sdk_env(context): """Calculate environment variables for building with wasi-sdk.""" wasi_sdk_path = wasi_sdk(context) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index bc201aa77cc1cfc..d7520c3a352d3f3 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -31,6 +31,7 @@ # โ˜ python.wasm +@_shared.forced_cache def build_dir(context): """The path to the build directory pointed to by pybuilddir.txt.""" relative_dir = ( @@ -41,12 +42,14 @@ def build_dir(context): return _shared.wasi_build_path(context) / relative_dir +@_shared.forced_cache def build_details(context): """Get the JSON contents of build-details.json.""" with (build_dir(context) / "build-details.json").open() as f: return json.load(f) + def pythonXY(context, support_debug=False): """Calculate the "pythonX.Y" part of a path. diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index b498416f9064743..8fe9b244a0caa5d 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -1,3 +1,4 @@ +import functools import pathlib import tomllib @@ -16,6 +17,26 @@ CROSS_BUILD_DIR = CHECKOUT / "cross-build" +_NO_CACHE = object() + +def forced_cache(func): + """Cache the result of a function no matter what. + + Useful for functions that will only ever be called with the same arguments. + """ + cache = _NO_CACHE + + @functools.wraps(func) + def wrapper(*args, **kwargs): + nonlocal cache + if cache is _NO_CACHE: + cache = func(*args, **kwargs) + return cache + + return wrapper + + +@forced_cache def host_triple(context): """Determine the target triple for the WASI host build.""" if getattr(context, "host_triple", None): From 353cf3c54fd6650035a31fabd0e7317ed23db6ab Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 4 Jun 2026 10:57:32 -0700 Subject: [PATCH 06/44] Log what's going on --- Platforms/WASI/_build.py | 39 ++++++++++++++------------------------ Platforms/WASI/_package.py | 30 ++++++++++++++++++++--------- Platforms/WASI/_shared.py | 11 +++++++++++ 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index 88bde6920713498..0d398ff13854958 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -48,17 +48,6 @@ def separator(): print("โŽฏ" * terminal_width) -def log(emoji, message, *, spacing=None): - """Print a notification with an emoji. - - If 'spacing' is None, calculate the spacing based on the number of code points - in the emoji as terminals "eat" a space when the emoji has multiple code points. - """ - if spacing is None: - spacing = " " if len(emoji) == 1 else " " - print("".join([emoji, spacing, message])) - - def updated_env(updates={}): """Create a new dict representing the environment to use. @@ -85,7 +74,7 @@ def updated_env(updates={}): env_vars = [ f"\n {key}={item}" for key, item in sorted(env_diff.items()) ] - log("๐ŸŒŽ", f"Environment changes:{''.join(env_vars)}") + _shared.log("๐ŸŒŽ", f"Environment changes:{''.join(env_vars)}") return environment @@ -101,13 +90,13 @@ def wrapper(context): if callable(working_dir): working_dir = working_dir(context) separator() - log("๐Ÿ“", os.fsdecode(working_dir)) + _shared.log("๐Ÿ“", os.fsdecode(working_dir)) if ( clean_ok and getattr(context, "clean", False) and working_dir.exists() ): - log("๐Ÿšฎ", "Deleting directory (--clean)...") + _shared.log("๐Ÿšฎ", "Deleting directory (--clean)...") shutil.rmtree(working_dir) working_dir.mkdir(parents=True, exist_ok=True) @@ -128,7 +117,7 @@ def call(command, *, context=None, quiet=False, **kwargs): if context is not None: quiet = context.quiet - log("โฏ", " ".join(map(str, command)), spacing=" ") + _shared.log("โฏ", " ".join(map(str, command)), spacing=" ") if not quiet: stdout = None stderr = None @@ -144,7 +133,7 @@ def call(command, *, context=None, quiet=False, **kwargs): suffix=".log", ) stderr = subprocess.STDOUT - log("๐Ÿ“", f"Logging output to {stdout.name} (--quiet)...") + _shared.log("๐Ÿ“", f"Logging output to {stdout.name} (--quiet)...") subprocess.check_call(command, **kwargs, stdout=stdout, stderr=stderr) @@ -179,11 +168,11 @@ def configure_build_python(context, working_dir): """Configure the build/host Python.""" if LOCAL_SETUP.exists(): if LOCAL_SETUP.read_bytes() == LOCAL_SETUP_MARKER: - log("๐Ÿ‘", f"{LOCAL_SETUP} exists ...") + _shared.log("๐Ÿ‘", f"{LOCAL_SETUP} exists ...") else: - log("โš ๏ธ", f"{LOCAL_SETUP} exists, but has unexpected contents") + _shared.log("โš ๏ธ", f"{LOCAL_SETUP} exists, but has unexpected contents") else: - log("๐Ÿ“", f"Creating {LOCAL_SETUP} ...") + _shared.log("๐Ÿ“", f"Creating {LOCAL_SETUP} ...") LOCAL_SETUP.write_bytes(LOCAL_SETUP_MARKER) configure = [os.path.relpath(_shared.CHECKOUT / "configure", working_dir)] @@ -207,7 +196,7 @@ def make_build_python(context, working_dir): ] version = subprocess.check_output(cmd, encoding="utf-8").strip() - log("๐ŸŽ‰", f"{binary} {version}") + _shared.log("๐ŸŽ‰", f"{binary} {version}") @_shared.forced_cache @@ -261,7 +250,7 @@ def wasi_sdk(context): # supported version is a prefix of the found version (e.g. `25` and `2567`). if not found_version.startswith(f"{wasi_sdk_version}."): major_version = found_version.partition(".")[0] - log( + _shared.log( "โš ๏ธ", f" Found WASI SDK {major_version}, " f"but WASI SDK {wasi_sdk_version} is the supported version", @@ -362,7 +351,7 @@ def configure_wasi_python(context, working_dir): with exec_script.open("w", encoding="utf-8") as file: file.write(f'#!/bin/sh\nexec {host_runner} {python_wasm} "$@"\n') exec_script.chmod(0o755) - log("๐Ÿƒ", f"Created {exec_script} (--host-runner)... ") + _shared.log("๐Ÿƒ", f"Created {exec_script} (--host-runner)... ") sys.stdout.flush() @@ -377,7 +366,7 @@ def make_wasi_python(context, working_dir): exec_script = working_dir / "python.sh" call([exec_script, "-c", "import sys; print(sys.version)"], quiet=False) - log( + _shared.log( "๐ŸŽ‰", f"Use `{exec_script.relative_to(pathlib.Path().absolute())}` " "to run CPython w/ the WASI host specified by --host-runner", @@ -387,9 +376,9 @@ def make_wasi_python(context, working_dir): def clean_contents(context): """Delete all files created by this script.""" if _shared.CROSS_BUILD_DIR.exists(): - log("๐Ÿงน", f"Deleting {_shared.CROSS_BUILD_DIR} ...") + _shared.log("๐Ÿงน", f"Deleting {_shared.CROSS_BUILD_DIR} ...") shutil.rmtree(_shared.CROSS_BUILD_DIR) if LOCAL_SETUP.exists(): if LOCAL_SETUP.read_bytes() == LOCAL_SETUP_MARKER: - log("๐Ÿงน", f"Deleting generated {LOCAL_SETUP} ...") + _shared.log("๐Ÿงน", f"Deleting generated {LOCAL_SETUP} ...") diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index d7520c3a352d3f3..f9e766148956bc5 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -131,16 +131,28 @@ def filename_stem(context): # `build-details.json` for either/both version and triple +def copy_files(files, base): + for dest, src in files: + target = base / dest + target.parent.mkdir(parents=True, exist_ok=True) + src.copy(target) + def package(context): dist = _shared.CHECKOUT / "dist" if dist.exists(): + _shared.log("๐Ÿงน", f"Deleting {dist} ...") shutil.rmtree(dist) - files = [] - files.append(license_file(context)) - files.extend(build_dir_files(context)) - files.extend(stdlib_files(context)) - files.extend(pkgconfig_files(context)) - for dest, src in files: - target = dist / dest - target.parent.mkdir(parents=True, exist_ok=True) - src.copy(target) + + indent = " " + _shared.log("๐Ÿ“", f"Copying files to {dist} ...") + _shared.log("๐Ÿ“", "lib/", spacing=indent * 2) + _shared.log("๐Ÿ“", "pythonN.M/", spacing=indent * 3) + _shared.log("๐Ÿ“„", "LICENSE.txt", spacing=indent * 4) + copy_files([license_file(context)], dist) + _shared.log("๐Ÿ“„", "files in pybuilddir.txt", spacing=indent * 4) + copy_files(build_dir_files(context), dist) + _shared.log("๐Ÿ“„", "**/*.py", spacing=indent * 4) + copy_files(stdlib_files(context), dist) + _shared.log("๐Ÿ“", "pkgconfig/", spacing=indent * 3) + _shared.log("๐Ÿ“„", "python*.pc", spacing=indent * 4) + copy_files(pkgconfig_files(context), dist) diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index 8fe9b244a0caa5d..0adaebe115654be 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -53,3 +53,14 @@ def host_triple(context): def wasi_build_path(context): """Determine the path to the WASI build directory.""" return CROSS_BUILD_DIR / host_triple(context) + + +def log(emoji, message, *, spacing=None): + """Print a notification with an emoji. + + If 'spacing' is None, calculate the spacing based on the number of code points + in the emoji as terminals "eat" a space when the emoji has multiple code points. + """ + if spacing is None: + spacing = " " if len(emoji) == 1 else " " + print("".join([emoji, spacing, message])) From 99a8eb5350a6dfdbb0ae41982e3b9902caed50f1 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Tue, 9 Jun 2026 13:28:35 -0700 Subject: [PATCH 07/44] Fix an over-zealous substitution --- Platforms/WASI/_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index 0d398ff13854958..847e0764836bfae 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -311,7 +311,7 @@ def configure_wasi_python(context, working_dir): args = { "WASMTIME": "wasmtime", "ARGV0": f"/{wasi_build_dir}/python.wasm", - "_shared.CHECKOUT": os.fsdecode(_shared.CHECKOUT), + "CHECKOUT": os.fsdecode(_shared.CHECKOUT), "WASMTIME_CONFIG_PATH": os.fsdecode(_shared.HERE / "wasmtime.toml"), } # Check dynamically for wasmtime in case it was specified manually via From 02870c27e2ef84e4e49eb722ebd5b77f2daa51be Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Tue, 9 Jun 2026 16:06:16 -0700 Subject: [PATCH 08/44] Centralize context in an object --- Platforms/WASI/__main__.py | 17 ++-- Platforms/WASI/_build.py | 173 +++++++------------------------- Platforms/WASI/_package.py | 34 ++----- Platforms/WASI/_shared.py | 198 +++++++++++++++++++++++++++---------- 4 files changed, 204 insertions(+), 218 deletions(-) diff --git a/Platforms/WASI/__main__.py b/Platforms/WASI/__main__.py index 0a1c6ff02f757c0..cd94d62f876a611 100644 --- a/Platforms/WASI/__main__.py +++ b/Platforms/WASI/__main__.py @@ -8,8 +8,7 @@ import _build import _package - -HERE = pathlib.Path(__file__).parent +import _shared def main(): @@ -28,6 +27,7 @@ def main(): # may want to use them. "--config {WASMTIME_CONFIG_PATH}" ) + context = _shared.Context() parser = argparse.ArgumentParser() subcommands = parser.add_subparsers(dest="subcommand") @@ -80,6 +80,8 @@ def main(): "--logdir", type=pathlib.Path, default=None, + dest="_log_path", + metavar="LOG-DIR", help="Directory to store log files", ) for subcommand in ( @@ -109,8 +111,9 @@ def main(): subcommand.add_argument( "--wasi-sdk", type=pathlib.Path, - dest="wasi_sdk_path", + dest="_wasi_sdk_path", default=None, + metavar="WASI-SDK-PATH", help="Path to the WASI SDK; defaults to WASI_SDK_PATH environment variable " "or the appropriate version found in /opt", ) @@ -122,16 +125,18 @@ def main(): help="Command template for running the WASI host; defaults to " f"`{default_host_runner}`", ) - for subcommand in build, configure_host, make_host, build_host: + for subcommand in build, configure_host, make_host, build_host, package: subcommand.add_argument( "--host-triple", action="store", default=None, + dest="_host_triple", + metavar="WASI-TRIPLE", help="The target triple for the WASI host build; " - f"defaults to the value found in {os.fsdecode(HERE / 'config.toml')}", + f"defaults to the value found in {os.fsdecode(context.here / 'config.toml')}", ) - context = parser.parse_args() + parser.parse_args(namespace=context) match context.subcommand: case "configure-build-python": diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index 847e0764836bfae..4f472f875e40413 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -__lazy_modules__ = ["shutil", "sys", "tempfile", "tomllib"] +__lazy_modules__ = ["shutil", "sys", "tempfile"] import contextlib import functools @@ -9,9 +9,7 @@ import shutil import subprocess import sys -import sysconfig import tempfile -import tomllib try: from os import process_cpu_count as cpu_count @@ -20,12 +18,6 @@ import _shared -# Build platform can also be found via `config.guess`. -BUILD_DIR = _shared.CROSS_BUILD_DIR / sysconfig.get_config_var( - "BUILD_GNU_TYPE" -) - -LOCAL_SETUP = _shared.CHECKOUT / "Modules" / "Setup.local" LOCAL_SETUP_MARKER = ( b"# Generated by Platforms/WASI .\n" b"# Required to statically build extension modules." @@ -79,21 +71,20 @@ def updated_env(updates={}): return environment -def subdir(working_dir, *, clean_ok=False): +def subdir(context_attr, *, clean_ok=False): """Decorator to change to a working directory.""" def decorator(func): @functools.wraps(func) def wrapper(context): - nonlocal working_dir + nonlocal context_attr - if callable(working_dir): - working_dir = working_dir(context) + working_dir = getattr(context, context_attr) separator() _shared.log("๐Ÿ“", os.fsdecode(working_dir)) if ( clean_ok - and getattr(context, "clean", False) + and context.clean and working_dir.exists() ): _shared.log("๐Ÿšฎ", "Deleting directory (--clean)...") @@ -122,13 +113,13 @@ def call(command, *, context=None, quiet=False, **kwargs): stdout = None stderr = None else: - if (logdir := getattr(context, "logdir", None)) is None: - logdir = pathlib.Path(tempfile.gettempdir()) + if (log_path := getattr(context, "log_path", None)) is None: + log_path = pathlib.Path(tempfile.gettempdir()) stdout = tempfile.NamedTemporaryFile( "w", encoding="utf-8", delete=False, - dir=logdir, + dir=log_path, prefix="cpython-wasi-", suffix=".log", ) @@ -138,56 +129,31 @@ def call(command, *, context=None, quiet=False, **kwargs): subprocess.check_call(command, **kwargs, stdout=stdout, stderr=stderr) -@_shared.forced_cache -def build_python_path(): - """The path to the build Python binary.""" - binary = BUILD_DIR / "python" - if not binary.is_file(): - binary = binary.with_suffix(".exe") - if not binary.is_file(): - raise FileNotFoundError( - f"Unable to find `python(.exe)` in {BUILD_DIR}" - ) - - return binary - - -@_shared.forced_cache -def build_python_is_pydebug(): - """Find out if the build Python is a pydebug build.""" - test = "import sys, test.support; sys.exit(test.support.Py_DEBUG)" - result = subprocess.run( - [build_python_path(), "-c", test], - capture_output=True, - ) - return bool(result.returncode) - - -@subdir(BUILD_DIR, clean_ok=True) +@subdir("build_python_path", clean_ok=True) def configure_build_python(context, working_dir): """Configure the build/host Python.""" - if LOCAL_SETUP.exists(): - if LOCAL_SETUP.read_bytes() == LOCAL_SETUP_MARKER: - _shared.log("๐Ÿ‘", f"{LOCAL_SETUP} exists ...") + if context.setup_local_path.exists(): + if context.setup_local_path.read_bytes() == LOCAL_SETUP_MARKER: + _shared.log("๐Ÿ‘", f"{context.setup_local_path} exists ...") else: - _shared.log("โš ๏ธ", f"{LOCAL_SETUP} exists, but has unexpected contents") + _shared.log("โš ๏ธ", f"{context.setup_local_path} exists, but has unexpected contents") else: - _shared.log("๐Ÿ“", f"Creating {LOCAL_SETUP} ...") - LOCAL_SETUP.write_bytes(LOCAL_SETUP_MARKER) + _shared.log("๐Ÿ“", f"Creating {context.setup_local_path} ...") + context.setup_local_path.write_bytes(LOCAL_SETUP_MARKER) - configure = [os.path.relpath(_shared.CHECKOUT / "configure", working_dir)] + configure = [os.path.relpath(context.checkout / "configure", working_dir)] if context.args: configure.extend(context.args) call(configure, context=context) -@subdir(BUILD_DIR) -def make_build_python(context, working_dir): +@subdir("build_python_path") +def make_build_python(context, _working_dir): """Make/build the build Python.""" call(["make", "--jobs", str(cpu_count()), "all"], context=context) - binary = build_python_path() + binary = context.build_python_interpreter() cmd = [ binary, "-c", @@ -199,72 +165,9 @@ def make_build_python(context, working_dir): _shared.log("๐ŸŽ‰", f"{binary} {version}") -@_shared.forced_cache -def wasi_sdk(context): - """Find the path to the WASI SDK.""" - if wasi_sdk_path := context.wasi_sdk_path: - if not wasi_sdk_path.exists(): - raise ValueError( - "WASI SDK not found; " - "download from " - "https://github.com/WebAssembly/wasi-sdk and/or " - "specify via $WASI_SDK_PATH or --wasi-sdk" - ) - return wasi_sdk_path - - with (_shared.HERE / "config.toml").open("rb") as file: - config = tomllib.load(file) - wasi_sdk_version = config["targets"]["wasi-sdk"] - - if wasi_sdk_path_env_var := os.environ.get("WASI_SDK_PATH"): - wasi_sdk_path = pathlib.Path(wasi_sdk_path_env_var) - if not wasi_sdk_path.exists(): - raise ValueError( - f"WASI SDK not found at $WASI_SDK_PATH ({wasi_sdk_path})" - ) - else: - opt_path = pathlib.Path("/opt") - # WASI SDK versions have a ``.0`` suffix, but it's a constant; the WASI SDK team - # has said they don't plan to ever do a point release and all of their Git tags - # lack the ``.0`` suffix. - # Starting with WASI SDK 23, the tarballs went from containing a directory named - # ``wasi-sdk-{WASI_SDK_VERSION}.0`` to e.g. - # ``wasi-sdk-{WASI_SDK_VERSION}.0-x86_64-linux``. - potential_sdks = [ - path - for path in opt_path.glob(f"wasi-sdk-{wasi_sdk_version}.0*") - if path.is_dir() - ] - if len(potential_sdks) == 1: - wasi_sdk_path = potential_sdks[0] - elif (default_path := opt_path / "wasi-sdk").is_dir(): - wasi_sdk_path = default_path - - # Starting with WASI SDK 25, a VERSION file is included in the root - # of the SDK directory that we can read to warn folks when they are using - # an unsupported version. - if wasi_sdk_path and (version_file := wasi_sdk_path / "VERSION").is_file(): - version_details = version_file.read_text(encoding="utf-8") - found_version = version_details.splitlines()[0] - # Make sure there's a trailing dot to avoid false positives if somehow the - # supported version is a prefix of the found version (e.g. `25` and `2567`). - if not found_version.startswith(f"{wasi_sdk_version}."): - major_version = found_version.partition(".")[0] - _shared.log( - "โš ๏ธ", - f" Found WASI SDK {major_version}, " - f"but WASI SDK {wasi_sdk_version} is the supported version", - ) - - # Cache the result. - context.wasi_sdk_path = wasi_sdk_path - return wasi_sdk_path - - -@_shared.forced_cache def wasi_sdk_env(context): """Calculate environment variables for building with wasi-sdk.""" - wasi_sdk_path = wasi_sdk(context) + wasi_sdk_path = context.wasi_sdk_path sysroot = wasi_sdk_path / "share" / "wasi-sysroot" env = { "CC": "clang", @@ -301,18 +204,18 @@ def wasi_sdk_env(context): return env -@subdir(lambda context: _shared.wasi_build_path(context), clean_ok=True) +@subdir("wasi_build_path", clean_ok=True) def configure_wasi_python(context, working_dir): """Configure the WASI/host build.""" - config_site = os.fsdecode(_shared.HERE / "config.site-wasm32-wasi") + config_site = os.fsdecode(context.here / "config.site-wasm32-wasi") - wasi_build_dir = working_dir.relative_to(_shared.CHECKOUT) + wasi_build_dir = working_dir.relative_to(context.checkout) args = { "WASMTIME": "wasmtime", "ARGV0": f"/{wasi_build_dir}/python.wasm", - "CHECKOUT": os.fsdecode(_shared.CHECKOUT), - "WASMTIME_CONFIG_PATH": os.fsdecode(_shared.HERE / "wasmtime.toml"), + "CHECKOUT": os.fsdecode(context.checkout), + "WASMTIME_CONFIG_PATH": os.fsdecode(context.here / "wasmtime.toml"), } # Check dynamically for wasmtime in case it was specified manually via # `--host-runner`. @@ -326,17 +229,17 @@ def configure_wasi_python(context, working_dir): ) host_runner = context.host_runner.format_map(args) env_additions = {"CONFIG_SITE": config_site, "HOSTRUNNER": host_runner} - build_python = os.fsdecode(build_python_path()) + build_python = os.fsdecode(context.build_python_path) # The path to `configure` MUST be relative, else `python.wasm` is unable # to find the stdlib due to Python not recognizing that it's being - # executed from within a _shared.checkout. + # executed from within context.checkout. configure = [ - os.path.relpath(_shared.CHECKOUT / "configure", working_dir), - f"--host={_shared.host_triple(context)}", - f"--build={BUILD_DIR.name}", + os.path.relpath(context.checkout / "configure", working_dir), + f"--host={context.host_triple}", + f"--build={context.build_python_path.name}", f"--with-build-python={build_python}", ] - if build_python_is_pydebug(): + if context.is_debug: configure.append("--with-pydebug") if context.args: configure.extend(context.args) @@ -355,7 +258,7 @@ def configure_wasi_python(context, working_dir): sys.stdout.flush() -@subdir(lambda context: _shared.wasi_build_path(context)) +@subdir("wasi_build_path") def make_wasi_python(context, working_dir): """Run `make` for the WASI/host build.""" call( @@ -375,10 +278,10 @@ def make_wasi_python(context, working_dir): def clean_contents(context): """Delete all files created by this script.""" - if _shared.CROSS_BUILD_DIR.exists(): - _shared.log("๐Ÿงน", f"Deleting {_shared.CROSS_BUILD_DIR} ...") - shutil.rmtree(_shared.CROSS_BUILD_DIR) + if context.cross_build_path.exists(): + _shared.log("๐Ÿงน", f"Deleting {context.cross_build_path} ...") + shutil.rmtree(context.cross_build_path) - if LOCAL_SETUP.exists(): - if LOCAL_SETUP.read_bytes() == LOCAL_SETUP_MARKER: - _shared.log("๐Ÿงน", f"Deleting generated {LOCAL_SETUP} ...") + if context.setup_local_path.exists(): + if context.setup_local_path.read_bytes() == LOCAL_SETUP_MARKER: + _shared.log("๐Ÿงน", f"Deleting generated {context.setup_local_path} ...") diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index f9e766148956bc5..a545dcbd3f0e357 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -30,32 +30,12 @@ # โŒ share/man/man1/ # โ˜ python.wasm - -@_shared.forced_cache -def build_dir(context): - """The path to the build directory pointed to by pybuilddir.txt.""" - relative_dir = ( - (_shared.wasi_build_path(context) / "pybuilddir.txt") - .read_text() - .strip() - ) - return _shared.wasi_build_path(context) / relative_dir - - -@_shared.forced_cache -def build_details(context): - """Get the JSON contents of build-details.json.""" - with (build_dir(context) / "build-details.json").open() as f: - return json.load(f) - - - def pythonXY(context, support_debug=False): """Calculate the "pythonX.Y" part of a path. If *support_debug* is True, then "d" is appended as appropriate. """ - details = build_details(context) + details = context.wasi_build_details major = details["language"]["version_info"]["major"] minor = details["language"]["version_info"]["minor"] name = f"python{major}.{minor}" @@ -70,7 +50,7 @@ def lib_python(context): def license_file(context): """Have /LICENSE end up as lib/pythonXY/LICENSE.txt.""" - return (lib_python(context) / "LICENSE.txt", _shared.CHECKOUT / "LICENSE") + return (lib_python(context) / "LICENSE.txt", context.checkout / "LICENSE") def build_dir_files(context): @@ -81,14 +61,14 @@ def build_dir_files(context): """ return [ (lib_python(context) / path.name, path) - for path in build_dir(context).iterdir() + for path in context.wasi_pybuilddir.iterdir() if path.is_file(follow_symlinks=False) ] def stdlib_files(context): """Have /Lib files end up in lib/pythonXY.""" - lib_dir = _shared.CHECKOUT / "Lib" + lib_dir = context.checkout / "Lib" lib_files = [] for root, dirs, files in lib_dir.walk(): try: @@ -111,8 +91,8 @@ def pkgconfig_files(context): Each file ends up being listed under `python3` and `python-3.N`. """ - misc_dir = _shared.wasi_build_path(context) / "Misc" - details = build_details(context) + misc_dir = context.wasi_build_path / "Misc" + details = context.wasi_build_details major = details["language"]["version_info"]["major"] minor = details["language"]["version_info"]["minor"] pkgconfig = pathlib.PurePath("lib") / "pkgconfig" @@ -138,7 +118,7 @@ def copy_files(files, base): src.copy(target) def package(context): - dist = _shared.CHECKOUT / "dist" + dist = context.checkout / "dist" if dist.exists(): _shared.log("๐Ÿงน", f"Deleting {dist} ...") shutil.rmtree(dist) diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index 0adaebe115654be..6074d4e98ea9d9e 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -1,58 +1,156 @@ +__lazy_modules__ = ["json", "os", "subprocess", "sysconfig", "tempfile", "tomllib"] import functools +import json +import os import pathlib +import subprocess +import sysconfig +import tempfile import tomllib -CHECKOUT = HERE = pathlib.Path(__file__).parent - -while CHECKOUT != CHECKOUT.parent: - if (CHECKOUT / "configure").is_file(): - break - CHECKOUT = CHECKOUT.parent -else: - raise FileNotFoundError( - "Unable to find the root of the CPython checkout by looking for 'configure'" - ) - -CROSS_BUILD_DIR = CHECKOUT / "cross-build" - - -_NO_CACHE = object() - -def forced_cache(func): - """Cache the result of a function no matter what. - - Useful for functions that will only ever be called with the same arguments. - """ - cache = _NO_CACHE - - @functools.wraps(func) - def wrapper(*args, **kwargs): - nonlocal cache - if cache is _NO_CACHE: - cache = func(*args, **kwargs) - return cache - - return wrapper - - -@forced_cache -def host_triple(context): - """Determine the target triple for the WASI host build.""" - if getattr(context, "host_triple", None): - return context.host_triple - - with (HERE / "config.toml").open("rb") as file: - config = tomllib.load(file) - - # Cache the result. - context.host_triple = config["targets"]["host-triple"] - return context.host_triple - - -def wasi_build_path(context): - """Determine the path to the WASI build directory.""" - return CROSS_BUILD_DIR / host_triple(context) +class Context: + + def __init__(self): + self.here = pathlib.Path(__file__).parent + + @functools.cached_property + def checkout(self): + checkout = self.here + while checkout != checkout.parent: + if (checkout / "configure").is_file(): + return checkout + checkout = checkout.parent + raise FileNotFoundError( + "Unable to find the root of the CPython checkout by looking for 'configure'" + ) + + @functools.cached_property + def setup_local_path(self): + return self.checkout / "Modules" / "Setup.local" + + @functools.cached_property + def host_triple(self): + if self._host_triple: + return self._host_triple + + with (self.here / "config.toml").open("rb") as file: + config = tomllib.load(file) + + return config["targets"]["host-triple"] + + @functools.cached_property + def cross_build_path(self): + return self.checkout / "cross-build" + + @functools.cached_property + def build_python_path(self): + # Build platform can also be found via `config.guess`. + return self.cross_build_path / sysconfig.get_config_var("BUILD_GNU_TYPE") + + @functools.cached_property + def build_python_interpreter(self): + binary = self.build_python_path / "python" + if not binary.is_file(): + binary = binary.with_suffix(".exe") + if not binary.is_file(): + raise FileNotFoundError( + f"Unable to find `python(.exe)` in {self.build_python_path}" + ) + + return binary + + @functools.cached_property + def is_debug(self): + test = "import sys, test.support; sys.exit(test.support.Py_DEBUG)" + result = subprocess.run( + [self.build_python_interpreter, "-c", test], + capture_output=True, + ) + return bool(result.returncode) + + @functools.cached_property + def wasi_build_path(self): + return self.cross_build_path / self.host_triple + + @functools.cached_property + def wasi_pybuilddir(self): + relative_dir = ( + (self.wasi_build_path / "pybuilddir.txt") + .read_text() + .strip() + ) + return self.wasi_build_path / relative_dir + + @functools.cached_property + def wasi_build_details(self): + with (self.wasi_pybuilddir / "build-details.json").open() as f: + return json.load(f) + + @functools.cached_property + def wasi_sdk_path(self): + if wasi_sdk_path := self._wasi_sdk_path: + if not wasi_sdk_path.exists(): + raise ValueError( + "WASI SDK not found; " + "download from " + "https://github.com/WebAssembly/wasi-sdk and/or " + "specify via $WASI_SDK_PATH or --wasi-sdk" + ) + return wasi_sdk_path + + with (self.here / "config.toml").open("rb") as file: + config = tomllib.load(file) + wasi_sdk_version = config["targets"]["wasi-sdk"] + + if wasi_sdk_path_env_var := os.environ.get("WASI_SDK_PATH"): + wasi_sdk_path = pathlib.Path(wasi_sdk_path_env_var) + if not wasi_sdk_path.exists(): + raise ValueError( + f"WASI SDK not found at $WASI_SDK_PATH ({wasi_sdk_path})" + ) + else: + opt_path = pathlib.Path("/opt") + # WASI SDK versions have a ``.0`` suffix, but it's a constant; the WASI SDK team + # has said they don't plan to ever do a point release and all of their Git tags + # lack the ``.0`` suffix. + # Starting with WASI SDK 23, the tarballs went from containing a directory named + # ``wasi-sdk-{WASI_SDK_VERSION}.0`` to e.g. + # ``wasi-sdk-{WASI_SDK_VERSION}.0-x86_64-linux``. + potential_sdks = [ + path + for path in opt_path.glob(f"wasi-sdk-{wasi_sdk_version}.0*") + if path.is_dir() + ] + if len(potential_sdks) == 1: + wasi_sdk_path = potential_sdks[0] + elif (default_path := opt_path / "wasi-sdk").is_dir(): + wasi_sdk_path = default_path + + # Starting with WASI SDK 25, a VERSION file is included in the root + # of the SDK directory that we can read to warn folks when they are using + # an unsupported version. + if wasi_sdk_path and (version_file := wasi_sdk_path / "VERSION").is_file(): + version_details = version_file.read_text(encoding="utf-8") + found_version = version_details.splitlines()[0] + # Make sure there's a trailing dot to avoid false positives if somehow the + # supported version is a prefix of the found version (e.g. `25` and `2567`). + if not found_version.startswith(f"{wasi_sdk_version}."): + major_version = found_version.partition(".")[0] + log( + "โš ๏ธ", + f" Found WASI SDK {major_version}, " + f"but WASI SDK {wasi_sdk_version} is the supported version", + ) + + return wasi_sdk_path + + @functools.cached_property + def log_path(self): + if self._log_path is not None: + return self._log_path + + return pathlib.Path(tempfile.gettempdir()) def log(emoji, message, *, spacing=None): From b91761f032f24baf60ddd537c72f648f80680c9b Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Tue, 9 Jun 2026 16:13:51 -0700 Subject: [PATCH 09/44] Make calculating if a debug build was used easier --- Platforms/WASI/_package.py | 2 +- Platforms/WASI/_shared.py | 28 ++++++++++++++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index a545dcbd3f0e357..b1611def39e59fa 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -39,7 +39,7 @@ def pythonXY(context, support_debug=False): major = details["language"]["version_info"]["major"] minor = details["language"]["version_info"]["minor"] name = f"python{major}.{minor}" - if support_debug and "d" in details["abi"]["flags"]: + if support_debug and context.is_debug: name += "d" return name diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index 6074d4e98ea9d9e..72ab70eaf401f81 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -25,6 +25,10 @@ def checkout(self): "Unable to find the root of the CPython checkout by looking for 'configure'" ) + def _pybuilddir(self, build_path): + relative_dir = (build_path / "pybuilddir.txt").read_text().strip() + return build_path / relative_dir + @functools.cached_property def setup_local_path(self): return self.checkout / "Modules" / "Setup.local" @@ -62,12 +66,17 @@ def build_python_interpreter(self): @functools.cached_property def is_debug(self): - test = "import sys, test.support; sys.exit(test.support.Py_DEBUG)" - result = subprocess.run( - [self.build_python_interpreter, "-c", test], - capture_output=True, - ) - return bool(result.returncode) + pybuilddir = self._pybuilddir(self.build_python_path) + if pybuilddir.is_file(): + build_details = json.loads(pybuilddir.read_text()) + return "d" in build_details["abi"]["flags"] + else: + test = "import sys, test.support; sys.exit(test.support.Py_DEBUG)" + result = subprocess.run( + [self.build_python_interpreter, "-c", test], + capture_output=True, + ) + return bool(result.returncode) @functools.cached_property def wasi_build_path(self): @@ -75,12 +84,7 @@ def wasi_build_path(self): @functools.cached_property def wasi_pybuilddir(self): - relative_dir = ( - (self.wasi_build_path / "pybuilddir.txt") - .read_text() - .strip() - ) - return self.wasi_build_path / relative_dir + return self._pybuilddir(self.wasi_build_path) @functools.cached_property def wasi_build_details(self): From 1cf98360b8ba34fe56e33c69852d9eece69f0849 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Tue, 9 Jun 2026 16:14:02 -0700 Subject: [PATCH 10/44] Run `ruff format` --- Platforms/WASI/_build.py | 15 ++++++++------- Platforms/WASI/_package.py | 15 ++++++++++----- Platforms/WASI/_shared.py | 35 +++++++++++++++++++++++------------ 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index 4f472f875e40413..a5764e104193d62 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -82,11 +82,7 @@ def wrapper(context): working_dir = getattr(context, context_attr) separator() _shared.log("๐Ÿ“", os.fsdecode(working_dir)) - if ( - clean_ok - and context.clean - and working_dir.exists() - ): + if clean_ok and context.clean and working_dir.exists(): _shared.log("๐Ÿšฎ", "Deleting directory (--clean)...") shutil.rmtree(working_dir) @@ -136,7 +132,10 @@ def configure_build_python(context, working_dir): if context.setup_local_path.read_bytes() == LOCAL_SETUP_MARKER: _shared.log("๐Ÿ‘", f"{context.setup_local_path} exists ...") else: - _shared.log("โš ๏ธ", f"{context.setup_local_path} exists, but has unexpected contents") + _shared.log( + "โš ๏ธ", + f"{context.setup_local_path} exists, but has unexpected contents", + ) else: _shared.log("๐Ÿ“", f"Creating {context.setup_local_path} ...") context.setup_local_path.write_bytes(LOCAL_SETUP_MARKER) @@ -284,4 +283,6 @@ def clean_contents(context): if context.setup_local_path.exists(): if context.setup_local_path.read_bytes() == LOCAL_SETUP_MARKER: - _shared.log("๐Ÿงน", f"Deleting generated {context.setup_local_path} ...") + _shared.log( + "๐Ÿงน", f"Deleting generated {context.setup_local_path} ..." + ) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index b1611def39e59fa..c6a85af522b4d96 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -30,6 +30,7 @@ # โŒ share/man/man1/ # โ˜ python.wasm + def pythonXY(context, support_debug=False): """Calculate the "pythonX.Y" part of a path. @@ -80,7 +81,7 @@ def stdlib_files(context): file_path = pathlib.Path(root) / file details = ( lib_python(context) / file_path.relative_to(lib_dir), - file_path + file_path, ) lib_files.append(details) return lib_files @@ -100,7 +101,10 @@ def pkgconfig_files(context): (pkgconfig / f"python{major}.pc", misc_dir / "python.pc"), (pkgconfig / f"python-{major}.{minor}.pc", misc_dir / "python.pc"), (pkgconfig / f"python3-embed.pc", misc_dir / "python-embed.pc"), - (pkgconfig / f"python-{major}.{minor}-embed.pc", misc_dir / "python-embed.pc"), + ( + pkgconfig / f"python-{major}.{minor}-embed.pc", + misc_dir / "python-embed.pc", + ), ] @@ -113,9 +117,10 @@ def filename_stem(context): def copy_files(files, base): for dest, src in files: - target = base / dest - target.parent.mkdir(parents=True, exist_ok=True) - src.copy(target) + target = base / dest + target.parent.mkdir(parents=True, exist_ok=True) + src.copy(target) + def package(context): dist = context.checkout / "dist" diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index 72ab70eaf401f81..2bf4e37fa3f0851 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -1,4 +1,11 @@ -__lazy_modules__ = ["json", "os", "subprocess", "sysconfig", "tempfile", "tomllib"] +__lazy_modules__ = [ + "json", + "os", + "subprocess", + "sysconfig", + "tempfile", + "tomllib", +] import functools import json import os @@ -10,7 +17,6 @@ class Context: - def __init__(self): self.here = pathlib.Path(__file__).parent @@ -50,7 +56,9 @@ def cross_build_path(self): @functools.cached_property def build_python_path(self): # Build platform can also be found via `config.guess`. - return self.cross_build_path / sysconfig.get_config_var("BUILD_GNU_TYPE") + return self.cross_build_path / sysconfig.get_config_var( + "BUILD_GNU_TYPE" + ) @functools.cached_property def build_python_interpreter(self): @@ -94,14 +102,14 @@ def wasi_build_details(self): @functools.cached_property def wasi_sdk_path(self): if wasi_sdk_path := self._wasi_sdk_path: - if not wasi_sdk_path.exists(): - raise ValueError( - "WASI SDK not found; " - "download from " - "https://github.com/WebAssembly/wasi-sdk and/or " - "specify via $WASI_SDK_PATH or --wasi-sdk" - ) - return wasi_sdk_path + if not wasi_sdk_path.exists(): + raise ValueError( + "WASI SDK not found; " + "download from " + "https://github.com/WebAssembly/wasi-sdk and/or " + "specify via $WASI_SDK_PATH or --wasi-sdk" + ) + return wasi_sdk_path with (self.here / "config.toml").open("rb") as file: config = tomllib.load(file) @@ -134,7 +142,10 @@ def wasi_sdk_path(self): # Starting with WASI SDK 25, a VERSION file is included in the root # of the SDK directory that we can read to warn folks when they are using # an unsupported version. - if wasi_sdk_path and (version_file := wasi_sdk_path / "VERSION").is_file(): + if ( + wasi_sdk_path + and (version_file := wasi_sdk_path / "VERSION").is_file() + ): version_details = version_file.read_text(encoding="utf-8") found_version = version_details.splitlines()[0] # Make sure there's a trailing dot to avoid false positives if somehow the From 347e9500c316cfc3e80dcea36e3a764d1051ccda Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Tue, 9 Jun 2026 16:16:19 -0700 Subject: [PATCH 11/44] Add comment to clarify debug check for Python 3.13 and older --- Platforms/WASI/_shared.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index 2bf4e37fa3f0851..9c0e013506418fb 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -79,6 +79,7 @@ def is_debug(self): build_details = json.loads(pybuilddir.read_text()) return "d" in build_details["abi"]["flags"] else: + # Python 3.13 and older. test = "import sys, test.support; sys.exit(test.support.Py_DEBUG)" result = subprocess.run( [self.build_python_interpreter, "-c", test], From 6748280b63bf299405ad250c8e2409cb19c05a1e Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 10 Jun 2026 15:47:30 -0700 Subject: [PATCH 12/44] Put the files into a subdirectory that records the Python version and architecture --- Platforms/WASI/_package.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index c6a85af522b4d96..9523d1696444f75 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -110,9 +110,14 @@ def pkgconfig_files(context): def filename_stem(context): """Calculate the stem of the archive file name.""" - # XXX File name: python-3.15.0a8-wasm32-wasip1.tar.xz - # Include/patchlevel.h: `#define PY_VERSION "3.15.0a8+"` - # `build-details.json` for either/both version and triple + version_info = context.wasi_build_details["language"]["version_info"] + version = f"python-{version_info['major']}.{version_info['minor']}.{version_info['micro']}" + if version_info["releaselevel"] != "final": + version += version_info["releaselevel"][0] + str( + version_info["serial"] + ) + + return f"{version}-{context.host_triple}" def copy_files(files, base): @@ -129,15 +134,16 @@ def package(context): shutil.rmtree(dist) indent = " " - _shared.log("๐Ÿ“", f"Copying files to {dist} ...") + base = dist / filename_stem(context) + _shared.log("๐Ÿ“", f"Copying files to {base} ...") _shared.log("๐Ÿ“", "lib/", spacing=indent * 2) _shared.log("๐Ÿ“", "pythonN.M/", spacing=indent * 3) _shared.log("๐Ÿ“„", "LICENSE.txt", spacing=indent * 4) - copy_files([license_file(context)], dist) + copy_files([license_file(context)], base) _shared.log("๐Ÿ“„", "files in pybuilddir.txt", spacing=indent * 4) - copy_files(build_dir_files(context), dist) + copy_files(build_dir_files(context), base) _shared.log("๐Ÿ“„", "**/*.py", spacing=indent * 4) - copy_files(stdlib_files(context), dist) + copy_files(stdlib_files(context), base) _shared.log("๐Ÿ“", "pkgconfig/", spacing=indent * 3) _shared.log("๐Ÿ“„", "python*.pc", spacing=indent * 4) - copy_files(pkgconfig_files(context), dist) + copy_files(pkgconfig_files(context), base) From de141e831da3b85f92d9f990e8c7d68a21f40ee0 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 10 Jun 2026 16:13:36 -0700 Subject: [PATCH 13/44] Add the man page --- Platforms/WASI/_package.py | 63 +++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 9523d1696444f75..43d1e4eef19456c 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -17,9 +17,9 @@ # โ˜ pythonN.M.wasmtime # โŒ idleN.M # โŒ pydocN.M -# โ˜ include/pythonN.Md? -# โ˜ pyconfig.h -# โ˜ .h files +# โœ… include/pythonN.Md? +# โœ… pyconfig.h +# โœ… .h files # โœ… lib # โœ… pkgconfig (from Misc/) # โœ… pythonN.M @@ -27,7 +27,7 @@ # โœ… LICENSE.txt (license_file()) # โœ… Stuff from build/lib.* (build_dir_files()) # โœ… Lib/ -# โŒ share/man/man1/ +# โœ… share/man/man1/ # โ˜ python.wasm @@ -100,7 +100,7 @@ def pkgconfig_files(context): return [ (pkgconfig / f"python{major}.pc", misc_dir / "python.pc"), (pkgconfig / f"python-{major}.{minor}.pc", misc_dir / "python.pc"), - (pkgconfig / f"python3-embed.pc", misc_dir / "python-embed.pc"), + (pkgconfig / f"python{major}-embed.pc", misc_dir / "python-embed.pc"), ( pkgconfig / f"python-{major}.{minor}-embed.pc", misc_dir / "python-embed.pc", @@ -108,6 +108,48 @@ def pkgconfig_files(context): ] +def pyconfig_file(context): + """Have /pyconfig.h end up in include/pythonXYd?/pyconfig.h.""" + return ( + pathlib.PurePath("include") + / pythonXY(context, support_debug=True) + / "pyconfig.h", + context.wasi_build_path / "pyconfig.h", + ) + + +def header_files(context): + """Have /Include/*.h end up in include/pythonXYd?/.""" + include_dir = context.checkout / "Include" + files = [] + for root, dirs, filenames in include_dir.walk(): + for filename in filenames: + file_path = pathlib.Path(root) / filename + details = ( + pathlib.PurePath("include") + / pythonXY(context, support_debug=True) + / file_path.relative_to(include_dir), + file_path, + ) + files.append(details) + return files + + +def man_files(context): + """Have Misc/python.man end up in share/man/man1/.""" + version_info = context.wasi_build_details["language"]["version_info"] + man_dir = pathlib.PurePath("share", "man", "man1") + man_file = context.checkout / "Misc" / "python.man" + return [ + (man_dir / f"python{version_info['major']}.1", man_file), + ( + man_dir + / f"python{version_info['major']}.{version_info['minor']}.1", + man_file, + ), + ] + + def filename_stem(context): """Calculate the stem of the archive file name.""" version_info = context.wasi_build_details["language"]["version_info"] @@ -136,6 +178,12 @@ def package(context): indent = " " base = dist / filename_stem(context) _shared.log("๐Ÿ“", f"Copying files to {base} ...") + _shared.log("๐Ÿ“", "include/", spacing=indent * 2) + _shared.log("๐Ÿ“", "pythonN.Md?/", spacing=indent * 3) + _shared.log("๐Ÿ“„", "pyconfig.h", spacing=indent * 4) + copy_files([pyconfig_file(context)], base) + _shared.log("๐Ÿ“„", "**/*.h", spacing=indent * 4) + copy_files(header_files(context), base) _shared.log("๐Ÿ“", "lib/", spacing=indent * 2) _shared.log("๐Ÿ“", "pythonN.M/", spacing=indent * 3) _shared.log("๐Ÿ“„", "LICENSE.txt", spacing=indent * 4) @@ -147,3 +195,8 @@ def package(context): _shared.log("๐Ÿ“", "pkgconfig/", spacing=indent * 3) _shared.log("๐Ÿ“„", "python*.pc", spacing=indent * 4) copy_files(pkgconfig_files(context), base) + _shared.log("๐Ÿ“", "share", spacing=indent * 2) + _shared.log("๐Ÿ“", "man", spacing=indent * 3) + _shared.log("๐Ÿ“", "man1", spacing=indent * 4) + _shared.log("๐Ÿ“„", "python*.1", spacing=indent * 5) + copy_files(man_files(context), base) From 4c53d56738bd16958a2fe2818c7d632b430f8545 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 11 Jun 2026 11:48:00 -0700 Subject: [PATCH 14/44] Symlink man page --- Platforms/WASI/_package.py | 66 ++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 43d1e4eef19456c..51cddfec1800c83 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -21,32 +21,32 @@ # โœ… pyconfig.h # โœ… .h files # โœ… lib -# โœ… pkgconfig (from Misc/) +# โ˜ pkgconfig (from Misc/) # โœ… pythonN.M # โŒ lib-dynload # โœ… LICENSE.txt (license_file()) # โœ… Stuff from build/lib.* (build_dir_files()) # โœ… Lib/ -# โœ… share/man/man1/ +# โ˜ share/man/man1/ # โ˜ python.wasm -def pythonXY(context, support_debug=False): - """Calculate the "pythonX.Y" part of a path. +def python_version(context, debug_ok=False): + """Calculate the M.N part of Python's version. - If *support_debug* is True, then "d" is appended as appropriate. + If *debug_ok* is True, then "d" is appended as appropriate. """ details = context.wasi_build_details major = details["language"]["version_info"]["major"] minor = details["language"]["version_info"]["minor"] - name = f"python{major}.{minor}" - if support_debug and context.is_debug: - name += "d" - return name + version = f"{major}.{minor}" + if debug_ok and context.is_debug: + version += "d" + return version def lib_python(context): - return pathlib.PurePath("lib") / pythonXY(context) + return pathlib.PurePath("lib") / f"python{python_version(context)}" def license_file(context): @@ -95,14 +95,15 @@ def pkgconfig_files(context): misc_dir = context.wasi_build_path / "Misc" details = context.wasi_build_details major = details["language"]["version_info"]["major"] - minor = details["language"]["version_info"]["minor"] pkgconfig = pathlib.PurePath("lib") / "pkgconfig" return [ - (pkgconfig / f"python{major}.pc", misc_dir / "python.pc"), - (pkgconfig / f"python-{major}.{minor}.pc", misc_dir / "python.pc"), - (pkgconfig / f"python{major}-embed.pc", misc_dir / "python-embed.pc"), ( - pkgconfig / f"python-{major}.{minor}-embed.pc", + pkgconfig / f"python-{python_version(context, debug_ok=True)}.pc", + misc_dir / "python.pc", + ), + ( + pkgconfig + / f"python-{python_version(context, debug_ok=True)}-embed.pc", misc_dir / "python-embed.pc", ), ] @@ -112,7 +113,7 @@ def pyconfig_file(context): """Have /pyconfig.h end up in include/pythonXYd?/pyconfig.h.""" return ( pathlib.PurePath("include") - / pythonXY(context, support_debug=True) + / f"python{python_version(context, debug_ok=True)}" / "pyconfig.h", context.wasi_build_path / "pyconfig.h", ) @@ -127,7 +128,7 @@ def header_files(context): file_path = pathlib.Path(root) / filename details = ( pathlib.PurePath("include") - / pythonXY(context, support_debug=True) + / f"python{python_version(context, debug_ok=True)}" / file_path.relative_to(include_dir), file_path, ) @@ -135,19 +136,20 @@ def header_files(context): return files -def man_files(context): +def man_file(context): """Have Misc/python.man end up in share/man/man1/.""" - version_info = context.wasi_build_details["language"]["version_info"] man_dir = pathlib.PurePath("share", "man", "man1") man_file = context.checkout / "Misc" / "python.man" - return [ - (man_dir / f"python{version_info['major']}.1", man_file), - ( - man_dir - / f"python{version_info['major']}.{version_info['minor']}.1", - man_file, - ), - ] + return ( + man_dir / f"python{python_version(context)}.1", + man_file, + ) + + +def man_symlink(man_path, context): + """Symlink pythonN.M.1 to pythonN.1.""" + major = context.wasi_build_details["language"]["version_info"]["major"] + return (man_path.parent / f"python{major}.1", man_path) def filename_stem(context): @@ -169,6 +171,11 @@ def copy_files(files, base): src.copy(target) +def symlink_files(files, base): + for target, source in files: + (base / target).symlink_to(base / source) + + def package(context): dist = context.checkout / "dist" if dist.exists(): @@ -195,8 +202,11 @@ def package(context): _shared.log("๐Ÿ“", "pkgconfig/", spacing=indent * 3) _shared.log("๐Ÿ“„", "python*.pc", spacing=indent * 4) copy_files(pkgconfig_files(context), base) + # XXX symlinks _shared.log("๐Ÿ“", "share", spacing=indent * 2) _shared.log("๐Ÿ“", "man", spacing=indent * 3) _shared.log("๐Ÿ“", "man1", spacing=indent * 4) _shared.log("๐Ÿ“„", "python*.1", spacing=indent * 5) - copy_files(man_files(context), base) + man_path = man_file(context) + copy_files([man_path], base) + symlink_files([man_symlink(man_path[0], context)], base) From c5f1a655c7b8df6ae6de468870f84d773b5f1de7 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 11 Jun 2026 15:24:42 -0700 Subject: [PATCH 15/44] Get symlinks working --- Platforms/WASI/_package.py | 39 ++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 51cddfec1800c83..aa9a4fee1db7058 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -21,13 +21,13 @@ # โœ… pyconfig.h # โœ… .h files # โœ… lib -# โ˜ pkgconfig (from Misc/) +# โœ… pkgconfig (from Misc/) # โœ… pythonN.M # โŒ lib-dynload # โœ… LICENSE.txt (license_file()) # โœ… Stuff from build/lib.* (build_dir_files()) # โœ… Lib/ -# โ˜ share/man/man1/ +# โœ… share/man/man1/ # โ˜ python.wasm @@ -109,6 +109,33 @@ def pkgconfig_files(context): ] +def pkgconfig_symlinks(pkgconfig_files, context): + assert len(pkgconfig_files) == 2 + embed, plain = ( + (0, 1) if pkgconfig_files[0].name.endswith("-embed.pc") else (1, 0) + ) + embed_path, plain_path = pkgconfig_files[embed], pkgconfig_files[plain] + major = context.wasi_build_details["language"]["version_info"]["major"] + paths = [ + (embed_path.parent / f"python{major}-embed.pc", embed_path), + (plain_path.parent / f"python{major}.pc", plain_path), + ] + if context.is_debug: + paths += [ + ( + embed_path.parent + / f"python-{python_version(context)}-embed.pc", + embed_path, + ), + ( + plain_path.parent / f"python-{python_version(context)}.pc", + plain_path, + ), + ] + + return paths + + def pyconfig_file(context): """Have /pyconfig.h end up in include/pythonXYd?/pyconfig.h.""" return ( @@ -201,8 +228,12 @@ def package(context): copy_files(stdlib_files(context), base) _shared.log("๐Ÿ“", "pkgconfig/", spacing=indent * 3) _shared.log("๐Ÿ“„", "python*.pc", spacing=indent * 4) - copy_files(pkgconfig_files(context), base) - # XXX symlinks + pkgconfig_paths = pkgconfig_files(context) + copy_files(pkgconfig_paths, base) + symlink_files( + pkgconfig_symlinks([path for path, _ in pkgconfig_paths], context), + base, + ) _shared.log("๐Ÿ“", "share", spacing=indent * 2) _shared.log("๐Ÿ“", "man", spacing=indent * 3) _shared.log("๐Ÿ“", "man1", spacing=indent * 4) From 6048670b6e23e303a2373fe99ebaa571af519b9a Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 9 Jul 2026 15:21:56 -0700 Subject: [PATCH 16/44] Fix the `clean` and `build` commands --- Platforms/WASI/_build.py | 5 +++-- Platforms/WASI/_shared.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index a5764e104193d62..825474a1f1442d3 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -152,7 +152,7 @@ def make_build_python(context, _working_dir): """Make/build the build Python.""" call(["make", "--jobs", str(cpu_count()), "all"], context=context) - binary = context.build_python_interpreter() + binary = context.build_python_interpreter cmd = [ binary, "-c", @@ -228,7 +228,7 @@ def configure_wasi_python(context, working_dir): ) host_runner = context.host_runner.format_map(args) env_additions = {"CONFIG_SITE": config_site, "HOSTRUNNER": host_runner} - build_python = os.fsdecode(context.build_python_path) + build_python = os.fsdecode(context.build_python_interpreter) # The path to `configure` MUST be relative, else `python.wasm` is unable # to find the stdlib due to Python not recognizing that it's being # executed from within context.checkout. @@ -277,6 +277,7 @@ def make_wasi_python(context, working_dir): def clean_contents(context): """Delete all files created by this script.""" + context.clean = True if context.cross_build_path.exists(): _shared.log("๐Ÿงน", f"Deleting {context.cross_build_path} ...") shutil.rmtree(context.cross_build_path) diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index 9c0e013506418fb..de3386e75627cf0 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -17,6 +17,9 @@ class Context: + + clean = False + def __init__(self): self.here = pathlib.Path(__file__).parent From 19899cdb81329518171604e5ef7427c8c8b45384 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 9 Jul 2026 15:23:33 -0700 Subject: [PATCH 17/44] Get `wasmtime.toml` copied over --- Platforms/WASI/_package.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index aa9a4fee1db7058..4755e7ed4af25a0 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -179,6 +179,37 @@ def man_symlink(man_path, context): return (man_path.parent / f"python{major}.1", man_path) +def wasmtime_config_file(context): + """Have wasmtime.toml end up in etc/pythonXY/.""" + config = context.checkout / "Platforms" / "WASI" / "wasmtime.toml" + return ( + pathlib.PurePath("etc") + / f"python{python_version(context)}" + / "wasmtime.toml", + config, + ) + + +def wasm_file(context): + """Have python.wasm end up at lib/pythonXY/lib-wasm/pythonX.Yd?.wasm.""" + XXX + + +def python_wasmtime_script(context): + """Create bin/pythonN.Md?.wasmtime.""" + XXX + + +def python_wasmtime_symlink(context): + """Symlink bin/pythonN.Md?.wasmtime. + + - bin/pythonN.M.wasmtime (if debug build) + - bin/pythonNd?.wasmtime + - bin/pythonN.wasmtime (if debug build) + """ + # XXX + + def filename_stem(context): """Calculate the stem of the archive file name.""" version_info = context.wasi_build_details["language"]["version_info"] @@ -241,3 +272,8 @@ def package(context): man_path = man_file(context) copy_files([man_path], base) symlink_files([man_symlink(man_path[0], context)], base) + + _shared.log("๐Ÿ“", "etc/", spacing=indent * 2) + _shared.log("๐Ÿ“", "pythonN.M/", spacing=indent * 3) + _shared.log("๐Ÿ“„", "wasmtime.toml", spacing=indent * 4) + copy_files([wasmtime_config_file(context)], base) From 1b5b8e73784b6abaec0bc68684383d5a3bedabc5 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 9 Jul 2026 16:04:02 -0700 Subject: [PATCH 18/44] Copy `python.wasm` --- Platforms/WASI/_package.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 4755e7ed4af25a0..a17c73df96751f8 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -15,6 +15,7 @@ # โ˜ bin # โ˜ pythonN.M.wasmtime +# โ˜ pythonN.M.config # โŒ idleN.M # โŒ pydocN.M # โœ… include/pythonN.Md? @@ -28,7 +29,7 @@ # โœ… Stuff from build/lib.* (build_dir_files()) # โœ… Lib/ # โœ… share/man/man1/ -# โ˜ python.wasm +# โœ… python.wasm def python_version(context, debug_ok=False): @@ -192,12 +193,15 @@ def wasmtime_config_file(context): def wasm_file(context): """Have python.wasm end up at lib/pythonXY/lib-wasm/pythonX.Yd?.wasm.""" - XXX + return ( + lib_python(context) / "lib-wasm" / f"python{python_version(context, debug_ok=True)}.wasm", + context.wasi_build_path / "python.wasm", + ) def python_wasmtime_script(context): """Create bin/pythonN.Md?.wasmtime.""" - XXX + # XXX set executable bit def python_wasmtime_symlink(context): @@ -257,6 +261,10 @@ def package(context): copy_files(build_dir_files(context), base) _shared.log("๐Ÿ“„", "**/*.py", spacing=indent * 4) copy_files(stdlib_files(context), base) + _shared.log("๐Ÿ“", "lib-wasm", spacing=indent * 4) + _shared.log("๐Ÿ“„", "pythonN.Md?.wasm", spacing=indent * 5) + copy_files([wasm_file(context)], base) + _shared.log("๐Ÿ“", "pkgconfig/", spacing=indent * 3) _shared.log("๐Ÿ“„", "python*.pc", spacing=indent * 4) pkgconfig_paths = pkgconfig_files(context) @@ -265,9 +273,9 @@ def package(context): pkgconfig_symlinks([path for path, _ in pkgconfig_paths], context), base, ) - _shared.log("๐Ÿ“", "share", spacing=indent * 2) - _shared.log("๐Ÿ“", "man", spacing=indent * 3) - _shared.log("๐Ÿ“", "man1", spacing=indent * 4) + _shared.log("๐Ÿ“", "share/", spacing=indent * 2) + _shared.log("๐Ÿ“", "man/", spacing=indent * 3) + _shared.log("๐Ÿ“", "man1/", spacing=indent * 4) _shared.log("๐Ÿ“„", "python*.1", spacing=indent * 5) man_path = man_file(context) copy_files([man_path], base) From feef36d8923ec7610242ced5c474796e7f4cf0dc Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 9 Jul 2026 16:18:06 -0700 Subject: [PATCH 19/44] Copy `pythonN.Md-config` --- Platforms/WASI/_package.py | 42 ++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index a17c73df96751f8..15c56454f7bdad0 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -214,6 +214,24 @@ def python_wasmtime_symlink(context): # XXX +def config_file(context): + """Have Misc/config.sh end up at bin/pythonN.Md?.config.""" + return ( + pathlib.PurePath("bin") / f"python{python_version(context, debug_ok=True)}-config", + context.wasi_build_path / "Misc" / "python-config.sh", + ) + + +def config_symlink(context): + """Symlink bin/pythonN.Md?.config. + + - bin/pythonN.M.config (if debug build) + - bin/pythonNd?.config + - bin/pythonN.config (if debug build) + """ + # XXX + + def filename_stem(context): """Calculate the stem of the archive file name.""" version_info = context.wasi_build_details["language"]["version_info"] @@ -247,23 +265,34 @@ def package(context): indent = " " base = dist / filename_stem(context) _shared.log("๐Ÿ“", f"Copying files to {base} ...") + + _shared.log("๐Ÿ“", "bin/", spacing=indent * 2) + _shared.log("๐Ÿ“„", "pythonN.Md?-config", spacing=indent * 3) + copy_files([config_file(context)], base) + + _shared.log("๐Ÿ“", "etc/", spacing=indent * 2) + _shared.log("๐Ÿ“", "pythonN.M/", spacing=indent * 3) + _shared.log("๐Ÿ“„", "wasmtime.toml", spacing=indent * 4) + copy_files([wasmtime_config_file(context)], base) + _shared.log("๐Ÿ“", "include/", spacing=indent * 2) _shared.log("๐Ÿ“", "pythonN.Md?/", spacing=indent * 3) _shared.log("๐Ÿ“„", "pyconfig.h", spacing=indent * 4) copy_files([pyconfig_file(context)], base) _shared.log("๐Ÿ“„", "**/*.h", spacing=indent * 4) copy_files(header_files(context), base) + _shared.log("๐Ÿ“", "lib/", spacing=indent * 2) _shared.log("๐Ÿ“", "pythonN.M/", spacing=indent * 3) + _shared.log("๐Ÿ“", "lib-wasm", spacing=indent * 4) + _shared.log("๐Ÿ“„", "pythonN.Md?.wasm", spacing=indent * 5) + copy_files([wasm_file(context)], base) _shared.log("๐Ÿ“„", "LICENSE.txt", spacing=indent * 4) copy_files([license_file(context)], base) - _shared.log("๐Ÿ“„", "files in pybuilddir.txt", spacing=indent * 4) + _shared.log("๐Ÿ“„", "files in `cat pybuilddir.txt`", spacing=indent * 4) copy_files(build_dir_files(context), base) _shared.log("๐Ÿ“„", "**/*.py", spacing=indent * 4) copy_files(stdlib_files(context), base) - _shared.log("๐Ÿ“", "lib-wasm", spacing=indent * 4) - _shared.log("๐Ÿ“„", "pythonN.Md?.wasm", spacing=indent * 5) - copy_files([wasm_file(context)], base) _shared.log("๐Ÿ“", "pkgconfig/", spacing=indent * 3) _shared.log("๐Ÿ“„", "python*.pc", spacing=indent * 4) @@ -280,8 +309,3 @@ def package(context): man_path = man_file(context) copy_files([man_path], base) symlink_files([man_symlink(man_path[0], context)], base) - - _shared.log("๐Ÿ“", "etc/", spacing=indent * 2) - _shared.log("๐Ÿ“", "pythonN.M/", spacing=indent * 3) - _shared.log("๐Ÿ“„", "wasmtime.toml", spacing=indent * 4) - copy_files([wasmtime_config_file(context)], base) From 32f2bd058f58ea088d7986147372066f334ac355 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 10 Jul 2026 16:01:21 -0700 Subject: [PATCH 20/44] Get the wasmtime script working --- Platforms/WASI/_package.py | 80 ++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 15c56454f7bdad0..0f01ca3926db739 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -1,4 +1,3 @@ -import json import pathlib import shutil @@ -13,23 +12,22 @@ # - gname # https://docs.python.org/3/library/tarfile.html#writing-examples -# โ˜ bin -# โ˜ pythonN.M.wasmtime -# โ˜ pythonN.M.config -# โŒ idleN.M -# โŒ pydocN.M -# โœ… include/pythonN.Md? -# โœ… pyconfig.h -# โœ… .h files -# โœ… lib -# โœ… pkgconfig (from Misc/) -# โœ… pythonN.M -# โŒ lib-dynload -# โœ… LICENSE.txt (license_file()) -# โœ… Stuff from build/lib.* (build_dir_files()) -# โœ… Lib/ -# โœ… share/man/man1/ -# โœ… python.wasm +def wasmtime_script(context): + return f"""\ +#!/bin/sh + +set -eu + +script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd -P) +root=$(CDPATH= cd "$script_dir/.." && pwd -P) +wasm_file="$root/lib/python{python_version(context)}/lib-wasm/python{python_version(context, debug_ok=True)}.wasm" + +exec wasmtime run \ + --argv0 "$wasm_file" \ + --config "$root/etc/python{python_version(context)}/wasmtime.toml" \ + --dir "/" \ + "$wasm_file" "$@" +""" def python_version(context, debug_ok=False): @@ -199,19 +197,30 @@ def wasm_file(context): ) -def python_wasmtime_script(context): +def python_wasmtime_script(base, context): """Create bin/pythonN.Md?.wasmtime.""" - # XXX set executable bit + script = wasmtime_script(context) + path = base / f"bin/python{python_version(context, debug_ok=True)}.wasmtime" + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "w") as f: + f.write(script) + path.chmod(0o755) + return path -def python_wasmtime_symlink(context): + +def python_wasmtime_symlink(path, context): """Symlink bin/pythonN.Md?.wasmtime. - bin/pythonN.M.wasmtime (if debug build) - - bin/pythonNd?.wasmtime - bin/pythonN.wasmtime (if debug build) """ - # XXX + symlinks = [path.parent / f"python{context.wasi_build_details["language"]["version_info"]["major"]}.wasmtime"] + if context.is_debug: + # The file already has the debug name, so the missing symlink is the non-debug name. + symlinks.append(path.parent / f"python{python_version(context)}.wasmtime") + + return [(symlink, path) for symlink in symlinks] def config_file(context): @@ -222,14 +231,19 @@ def config_file(context): ) -def config_symlink(context): - """Symlink bin/pythonN.Md?.config. +def config_symlink(config_path, context): + """Symlink bin/pythonN.Md?-config. - - bin/pythonN.M.config (if debug build) - - bin/pythonNd?.config - - bin/pythonN.config (if debug build) + - bin/pythonN.M-config (if debug build) + - bin/pythonNd?-config + - bin/pythonN-config (if debug build) """ - # XXX + symlinks = [config_path.parent / f"python{context.wasi_build_details['language']['version_info']['major']}-config"] + if context.is_debug: + # The file already has the debug name, so the missing symlink is the non-debug name. + symlinks.append(config_path.parent / f"python{python_version(context)}-config") + + return [(symlink, config_path) for symlink in symlinks] def filename_stem(context): @@ -268,7 +282,13 @@ def package(context): _shared.log("๐Ÿ“", "bin/", spacing=indent * 2) _shared.log("๐Ÿ“„", "pythonN.Md?-config", spacing=indent * 3) - copy_files([config_file(context)], base) + config_location = config_file(context) + copy_files([config_location], base) + symlink_files(config_symlink(config_location[0], context), base) + + _shared.log("๐Ÿ“„", "pythonN.Md?.wasmtime", spacing=indent * 3) + script_path = python_wasmtime_script(base, context) + symlink_files(python_wasmtime_symlink(script_path, context), base) _shared.log("๐Ÿ“", "etc/", spacing=indent * 2) _shared.log("๐Ÿ“", "pythonN.M/", spacing=indent * 3) From fbf38fe63a88cfb21fb12d5ed46cad64d5e5955f Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 10 Jul 2026 16:09:03 -0700 Subject: [PATCH 21/44] `ruff format` --- Platforms/WASI/_package.py | 30 +++++++++++++++++++++++------- Platforms/WASI/_shared.py | 1 - 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 0f01ca3926db739..2882dc178e4f781 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -12,6 +12,7 @@ # - gname # https://docs.python.org/3/library/tarfile.html#writing-examples + def wasmtime_script(context): return f"""\ #!/bin/sh @@ -192,7 +193,9 @@ def wasmtime_config_file(context): def wasm_file(context): """Have python.wasm end up at lib/pythonXY/lib-wasm/pythonX.Yd?.wasm.""" return ( - lib_python(context) / "lib-wasm" / f"python{python_version(context, debug_ok=True)}.wasm", + lib_python(context) + / "lib-wasm" + / f"python{python_version(context, debug_ok=True)}.wasm", context.wasi_build_path / "python.wasm", ) @@ -200,7 +203,9 @@ def wasm_file(context): def python_wasmtime_script(base, context): """Create bin/pythonN.Md?.wasmtime.""" script = wasmtime_script(context) - path = base / f"bin/python{python_version(context, debug_ok=True)}.wasmtime" + path = ( + base / f"bin/python{python_version(context, debug_ok=True)}.wasmtime" + ) path.parent.mkdir(parents=True, exist_ok=True) with open(path, "w") as f: f.write(script) @@ -215,10 +220,15 @@ def python_wasmtime_symlink(path, context): - bin/pythonN.M.wasmtime (if debug build) - bin/pythonN.wasmtime (if debug build) """ - symlinks = [path.parent / f"python{context.wasi_build_details["language"]["version_info"]["major"]}.wasmtime"] + symlinks = [ + path.parent + / f"python{context.wasi_build_details['language']['version_info']['major']}.wasmtime" + ] if context.is_debug: # The file already has the debug name, so the missing symlink is the non-debug name. - symlinks.append(path.parent / f"python{python_version(context)}.wasmtime") + symlinks.append( + path.parent / f"python{python_version(context)}.wasmtime" + ) return [(symlink, path) for symlink in symlinks] @@ -226,7 +236,8 @@ def python_wasmtime_symlink(path, context): def config_file(context): """Have Misc/config.sh end up at bin/pythonN.Md?.config.""" return ( - pathlib.PurePath("bin") / f"python{python_version(context, debug_ok=True)}-config", + pathlib.PurePath("bin") + / f"python{python_version(context, debug_ok=True)}-config", context.wasi_build_path / "Misc" / "python-config.sh", ) @@ -238,10 +249,15 @@ def config_symlink(config_path, context): - bin/pythonNd?-config - bin/pythonN-config (if debug build) """ - symlinks = [config_path.parent / f"python{context.wasi_build_details['language']['version_info']['major']}-config"] + symlinks = [ + config_path.parent + / f"python{context.wasi_build_details['language']['version_info']['major']}-config" + ] if context.is_debug: # The file already has the debug name, so the missing symlink is the non-debug name. - symlinks.append(config_path.parent / f"python{python_version(context)}-config") + symlinks.append( + config_path.parent / f"python{python_version(context)}-config" + ) return [(symlink, config_path) for symlink in symlinks] diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index de3386e75627cf0..32f3e8832d66355 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -17,7 +17,6 @@ class Context: - clean = False def __init__(self): From 4f69a2afb146ed6cd4be8a7af53067b4538ec9c0 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 10 Jul 2026 16:12:09 -0700 Subject: [PATCH 22/44] Get at the WASI build version easily Pragmatically I was using it a lot. --- Platforms/WASI/_package.py | 5 ++--- Platforms/WASI/_shared.py | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 2882dc178e4f781..28ba980341da621 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -221,8 +221,7 @@ def python_wasmtime_symlink(path, context): - bin/pythonN.wasmtime (if debug build) """ symlinks = [ - path.parent - / f"python{context.wasi_build_details['language']['version_info']['major']}.wasmtime" + path.parent / f"python{context.wasi_build_version['major']}.wasmtime" ] if context.is_debug: # The file already has the debug name, so the missing symlink is the non-debug name. @@ -251,7 +250,7 @@ def config_symlink(config_path, context): """ symlinks = [ config_path.parent - / f"python{context.wasi_build_details['language']['version_info']['major']}-config" + / f"python{context.wasi_build_version['major']}-config" ] if context.is_debug: # The file already has the debug name, so the missing symlink is the non-debug name. diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index 32f3e8832d66355..ef2a0fa5bc484a4 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -102,6 +102,10 @@ def wasi_build_details(self): with (self.wasi_pybuilddir / "build-details.json").open() as f: return json.load(f) + @functools.cached_property + def wasi_build_version(self): + return self.wasi_build_details["language"]["version_info"] + @functools.cached_property def wasi_sdk_path(self): if wasi_sdk_path := self._wasi_sdk_path: From fd0464eaf7cae18b7295da4d0445f5c6f43fa091 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 10 Jul 2026 16:25:59 -0700 Subject: [PATCH 23/44] Remove reproducible build comments from _package.py --- Platforms/WASI/_package.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 28ba980341da621..27f0897c6fead22 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -3,15 +3,6 @@ import _shared -# https://reproducible-builds.org/docs/archives/ -# https://sethmlarson.dev/security-developer-in-residence-weekly-report-14 -# - mtime -# - uid -# - gid -# - uname -# - gname -# https://docs.python.org/3/library/tarfile.html#writing-examples - def wasmtime_script(context): return f"""\ From b7c618f28f2f6b44c72239b7daf620f5e76e4de6 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 15 Jul 2026 15:38:19 -0700 Subject: [PATCH 24/44] Add archiving --- Platforms/WASI/__main__.py | 3 ++- Platforms/WASI/_package.py | 51 +++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Platforms/WASI/__main__.py b/Platforms/WASI/__main__.py index cd94d62f876a611..8ba5700e4daf225 100644 --- a/Platforms/WASI/__main__.py +++ b/Platforms/WASI/__main__.py @@ -161,7 +161,8 @@ def main(): case "clean": _build.clean_contents(context) case "package": - _package.package(context) + _package.gather(context) + _package.archive(context) case _: raise ValueError(f"Unknown subcommand {context.subcommand!r}") diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 27f0897c6fead22..d40f52485de1ba6 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -1,5 +1,7 @@ +import os import pathlib import shutil +import subprocess import _shared @@ -276,7 +278,7 @@ def symlink_files(files, base): (base / target).symlink_to(base / source) -def package(context): +def gather(context): dist = context.checkout / "dist" if dist.exists(): _shared.log("๐Ÿงน", f"Deleting {dist} ...") @@ -335,3 +337,50 @@ def package(context): man_path = man_file(context) copy_files([man_path], base) symlink_files([man_symlink(man_path[0], context)], base) + + return base + + +def archive(context): + file_name = f"{filename_stem(context)}.tar.xz" + file_path = context.checkout / "dist" / file_name + if file_path.exists(): + _shared.log("๐Ÿงน", f"Deleting {file_path} ...") + file_path.unlink() + to_compress = context.checkout / "dist" / filename_stem(context) + _shared.log("๐Ÿ—œ๏ธ", f"Compressing to {file_path} ...") + mtime = subprocess.run( + [ + "git", + "log", + "-1", + "--format=tformat:%cd", + "--date=format:%Y-%m-%dT%H:%M:%SZ", + os.fsdecode(context.checkout), + ], + env={"TZ": "UTC0"}, + capture_output=True, + text=True, + check=True, + ).stdout.strip() + + subprocess.run( + [ + "tar", + "-c", + "-f", os.fsdecode(file_path), + "--sort=name", + f"--mtime", "{mtime}", + "--clamp-mtime", + "--owner=0", + "--group=0", + "--numeric-owner", + "--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime", + "--mode=go+u,go-w", + "--use-compress-program", "xz -T 0", + os.fsdecode(to_compress), + ], + capture_output=True, + text=True, + check=True, + ) From 5ff44ba55ee7cea848efc5a6a001351274d66df6 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 15 Jul 2026 15:40:15 -0700 Subject: [PATCH 25/44] Ignore dist/ --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 118eb5ee76e8051..dbe62c670127d1a 100644 --- a/.gitignore +++ b/.gitignore @@ -139,6 +139,7 @@ Tools/unicode/data/ /config.status.lineno /.ccache /cross-build*/ +/dist/ /jit_stencils*.h /jit_unwind_info*.h .jit-stamp From fe5c6403e9b5cbd45e7c3360ca96e96e4ceb96a7 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 15 Jul 2026 16:09:47 -0700 Subject: [PATCH 26/44] Add quiet mode for pythoninfo commands in main function --- Platforms/WASI/__main__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Platforms/WASI/__main__.py b/Platforms/WASI/__main__.py index 2c3f72409638f6c..2f7812130fdb31b 100644 --- a/Platforms/WASI/__main__.py +++ b/Platforms/WASI/__main__.py @@ -176,12 +176,14 @@ def main(): # Configure and build the build Python _build.configure_build_python(context) _build.make_build_python(context) - _build.pythoninfo_build_python(context) + if not context.quiet: + _build.pythoninfo_build_python(context) # Configure and build the host/WASI Python _build.configure_wasi_python(context) _build.make_wasi_python(context) - _build.pythoninfo_wasi_python(context) + if not context.quiet: + _build.pythoninfo_wasi_python(context) case "clean": _build.clean_contents(context) case "package": From d2b5418965fa59c3901da71bc03bf62d57f75edf Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 15 Jul 2026 16:10:13 -0700 Subject: [PATCH 27/44] Fix archive function to correctly store mtime argument --- Platforms/WASI/_package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index d40f52485de1ba6..af336de9cbe7298 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -370,7 +370,7 @@ def archive(context): "-c", "-f", os.fsdecode(file_path), "--sort=name", - f"--mtime", "{mtime}", + f"--mtime", mtime, "--clamp-mtime", "--owner=0", "--group=0", From 3742dcb26b14dc09ba171945a6224b69cabc501e Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 15 Jul 2026 16:10:23 -0700 Subject: [PATCH 28/44] Update gather function to use dynamic Python versioning in logs --- Platforms/WASI/_package.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index af336de9cbe7298..5d15c151d7acf82 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -279,6 +279,9 @@ def symlink_files(files, base): def gather(context): + py_version = python_version(context) + py_d_version = python_version(context, debug_ok=True) + dist = context.checkout / "dist" if dist.exists(): _shared.log("๐Ÿงน", f"Deleting {dist} ...") @@ -289,31 +292,31 @@ def gather(context): _shared.log("๐Ÿ“", f"Copying files to {base} ...") _shared.log("๐Ÿ“", "bin/", spacing=indent * 2) - _shared.log("๐Ÿ“„", "pythonN.Md?-config", spacing=indent * 3) + _shared.log("๐Ÿ“„", f"python{py_d_version}-config", spacing=indent * 3) config_location = config_file(context) copy_files([config_location], base) symlink_files(config_symlink(config_location[0], context), base) - _shared.log("๐Ÿ“„", "pythonN.Md?.wasmtime", spacing=indent * 3) + _shared.log("๐Ÿ“„", f"python{py_d_version}.wasmtime", spacing=indent * 3) script_path = python_wasmtime_script(base, context) symlink_files(python_wasmtime_symlink(script_path, context), base) _shared.log("๐Ÿ“", "etc/", spacing=indent * 2) - _shared.log("๐Ÿ“", "pythonN.M/", spacing=indent * 3) + _shared.log("๐Ÿ“", f"python{py_version}/", spacing=indent * 3) _shared.log("๐Ÿ“„", "wasmtime.toml", spacing=indent * 4) copy_files([wasmtime_config_file(context)], base) _shared.log("๐Ÿ“", "include/", spacing=indent * 2) - _shared.log("๐Ÿ“", "pythonN.Md?/", spacing=indent * 3) + _shared.log("๐Ÿ“", f"python{py_version}/", spacing=indent * 3) _shared.log("๐Ÿ“„", "pyconfig.h", spacing=indent * 4) copy_files([pyconfig_file(context)], base) _shared.log("๐Ÿ“„", "**/*.h", spacing=indent * 4) copy_files(header_files(context), base) _shared.log("๐Ÿ“", "lib/", spacing=indent * 2) - _shared.log("๐Ÿ“", "pythonN.M/", spacing=indent * 3) + _shared.log("๐Ÿ“", f"python{py_version}/", spacing=indent * 3) _shared.log("๐Ÿ“", "lib-wasm", spacing=indent * 4) - _shared.log("๐Ÿ“„", "pythonN.Md?.wasm", spacing=indent * 5) + _shared.log("๐Ÿ“„", f"python{py_d_version}.wasm", spacing=indent * 5) copy_files([wasm_file(context)], base) _shared.log("๐Ÿ“„", "LICENSE.txt", spacing=indent * 4) copy_files([license_file(context)], base) From 740616082eb2576a0ead12eeb32463009b1991f0 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 15 Jul 2026 16:17:44 -0700 Subject: [PATCH 29/44] Format wasmtime_script for better readability --- Platforms/WASI/_package.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 5d15c151d7acf82..792fe83c1c1cbd5 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -16,11 +16,11 @@ def wasmtime_script(context): root=$(CDPATH= cd "$script_dir/.." && pwd -P) wasm_file="$root/lib/python{python_version(context)}/lib-wasm/python{python_version(context, debug_ok=True)}.wasm" -exec wasmtime run \ - --argv0 "$wasm_file" \ - --config "$root/etc/python{python_version(context)}/wasmtime.toml" \ - --dir "/" \ - "$wasm_file" "$@" +exec wasmtime run \\ + --argv0 "$wasm_file" \\ + --config "$root/etc/python{python_version(context)}/wasmtime.toml" \\ + --dir "/" \\ + "$wasm_file" "$@" """ From 1cf47698e9ece4eba63586b7b91e31747ef03d69 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 15 Jul 2026 16:18:12 -0700 Subject: [PATCH 30/44] Update archive function log message for clarity --- Platforms/WASI/_package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 792fe83c1c1cbd5..6ac6fb0dd2b0ada 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -351,7 +351,7 @@ def archive(context): _shared.log("๐Ÿงน", f"Deleting {file_path} ...") file_path.unlink() to_compress = context.checkout / "dist" / filename_stem(context) - _shared.log("๐Ÿ—œ๏ธ", f"Compressing to {file_path} ...") + _shared.log("๐Ÿ—œ๏ธ", f"Archiving to {file_path} ...") mtime = subprocess.run( [ "git", From cb0f034b06baee77a9b33def7d0feda4b2754cd1 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 16 Jul 2026 15:52:09 -0700 Subject: [PATCH 31/44] Add comment to clarify use of `xz -T` --- Platforms/WASI/_package.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 6ac6fb0dd2b0ada..9545719d73ab986 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -380,6 +380,9 @@ def archive(context): "--numeric-owner", "--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime", "--mode=go+u,go-w", + # Explicitly using `-T` because if you don't compress with threads you can't + # uncompress with them and the size difference is negligible when using + # single-threaded compression. "--use-compress-program", "xz -T 0", os.fsdecode(to_compress), ], From 1c28ad8be71e41b896a5add8b5bc31e235025c15 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 12:55:35 -0700 Subject: [PATCH 32/44] Reformat --- Platforms/WASI/_package.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 9545719d73ab986..0d1daee755434db 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -371,9 +371,11 @@ def archive(context): [ "tar", "-c", - "-f", os.fsdecode(file_path), + "-f", + os.fsdecode(file_path), "--sort=name", - f"--mtime", mtime, + f"--mtime", + mtime, "--clamp-mtime", "--owner=0", "--group=0", @@ -383,7 +385,8 @@ def archive(context): # Explicitly using `-T` because if you don't compress with threads you can't # uncompress with them and the size difference is negligible when using # single-threaded compression. - "--use-compress-program", "xz -T 0", + "--use-compress-program", + "xz -T 0", os.fsdecode(to_compress), ], capture_output=True, From 612174b1f8e5cb68a7e115a90476aed5d9ce3006 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 13:01:34 -0700 Subject: [PATCH 33/44] Make all imports lazy --- Platforms/WASI/__main__.py | 4 +--- Platforms/WASI/_build.py | 13 ++++++++++--- Platforms/WASI/_package.py | 2 ++ Platforms/WASI/_shared.py | 3 +++ 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Platforms/WASI/__main__.py b/Platforms/WASI/__main__.py index 2f7812130fdb31b..5236ae7e0409fcf 100644 --- a/Platforms/WASI/__main__.py +++ b/Platforms/WASI/__main__.py @@ -1,6 +1,4 @@ -#!/usr/bin/env python3 - -__lazy_modules__ = ["_build", "_package"] +__lazy_modules__ = ["argparse", "os", "pathlib", "_build", "_package", "_shared"] import argparse import os diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index 609a90914c07148..11072ed384b36d4 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -1,6 +1,13 @@ -#!/usr/bin/env python3 - -__lazy_modules__ = ["shutil", "sys", "tempfile"] +__lazy_modules__ = [ + "contextlib", + "functools", + "os", + "pathlib", + "shutil", + "subprocess", + "sys", + "tempfile", +] import contextlib import functools diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 0d1daee755434db..60d71023c24bec4 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -1,3 +1,5 @@ +__lazy_modules__ = ["os", "pathlib", "shutil", "subprocess", "_shared"] + import os import pathlib import shutil diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index ef2a0fa5bc484a4..7bfd5d361e21ec1 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -1,11 +1,14 @@ __lazy_modules__ = [ + "functools", "json", "os", + "pathlib", "subprocess", "sysconfig", "tempfile", "tomllib", ] + import functools import json import os From 7b475a5c586fda0e497963cdd3c441fb97fea400 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 13:01:49 -0700 Subject: [PATCH 34/44] `ruff check` --- Platforms/WASI/_package.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 60d71023c24bec4..754eebb40f45955 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -88,8 +88,6 @@ def pkgconfig_files(context): Each file ends up being listed under `python3` and `python-3.N`. """ misc_dir = context.wasi_build_path / "Misc" - details = context.wasi_build_details - major = details["language"]["version_info"]["major"] pkgconfig = pathlib.PurePath("lib") / "pkgconfig" return [ ( @@ -376,7 +374,7 @@ def archive(context): "-f", os.fsdecode(file_path), "--sort=name", - f"--mtime", + "--mtime", mtime, "--clamp-mtime", "--owner=0", From f89df22d3a2dc42405a1fbe023e4c6d4bafbd006 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 13:54:59 -0700 Subject: [PATCH 35/44] Plug a hole where the lack of a WASI SDK didn't raise an exception --- Platforms/WASI/_shared.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index 7bfd5d361e21ec1..6f1843861986a06 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -144,7 +144,9 @@ def wasi_sdk_path(self): for path in opt_path.glob(f"wasi-sdk-{wasi_sdk_version}.0*") if path.is_dir() ] - if len(potential_sdks) == 1: + if not potential_sdks: + raise ValueError(f"WASI SDK {wasi_sdk_version} not found in {opt_path}") + elif len(potential_sdks) == 1: wasi_sdk_path = potential_sdks[0] elif (default_path := opt_path / "wasi-sdk").is_dir(): wasi_sdk_path = default_path From abc9ea43e9e6c00bc7c5f38a7d303585ece1b97d Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 13:55:38 -0700 Subject: [PATCH 36/44] Actually delete the generated `Setup.local` --- Platforms/WASI/_build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index 11072ed384b36d4..66e2ae64e181d70 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -294,6 +294,7 @@ def clean_contents(context): _shared.log( "๐Ÿงน", f"Deleting generated {context.setup_local_path} ..." ) + context.setup_local_path.unlink() @subdir("build_python_path") From 3789b64310c7b8cd42407e96f867b4cfcdc51a0f Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 15:03:56 -0700 Subject: [PATCH 37/44] Fix archive function to use the correct file name and set working directory --- Platforms/WASI/_package.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 754eebb40f45955..921aba256ef51e9 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -387,8 +387,9 @@ def archive(context): # single-threaded compression. "--use-compress-program", "xz -T 0", - os.fsdecode(to_compress), + to_compress.name, ], + cwd=to_compress.parent, capture_output=True, text=True, check=True, From d97c44f0b9535fab7206f7d385029dcc50837f2b Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 15:04:13 -0700 Subject: [PATCH 38/44] Make symlinks relative --- Platforms/WASI/_package.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 921aba256ef51e9..be2da369858ab98 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -275,7 +275,11 @@ def copy_files(files, base): def symlink_files(files, base): for target, source in files: - (base / target).symlink_to(base / source) + target_path = base / target + source_path = base / source + target_path.symlink_to( + os.path.relpath(source_path, start=target_path.parent) + ) def gather(context): From d9cd40921235c8735e473d25ff30982d1759ae3c Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 15:04:36 -0700 Subject: [PATCH 39/44] Add error handling for multiple or missing WASI SDKs --- Platforms/WASI/_shared.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index 6f1843861986a06..aed0d6a4d63f7cd 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -150,6 +150,12 @@ def wasi_sdk_path(self): wasi_sdk_path = potential_sdks[0] elif (default_path := opt_path / "wasi-sdk").is_dir(): wasi_sdk_path = default_path + elif potential_sdks: + raise ValueError( + f"Multiple WASI SDKs found in {opt_path} w/o knowing which to use" + ) + else: + raise ValueError(f"WASI SDK not found in {opt_path}") # Starting with WASI SDK 25, a VERSION file is included in the root # of the SDK directory that we can read to warn folks when they are using From f2214f941eecc7998a3751d257f3b7f977cee397 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 15:04:42 -0700 Subject: [PATCH 40/44] Fix build interpreter to read build details from the correct file path --- Platforms/WASI/_shared.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index aed0d6a4d63f7cd..b1494b894c44020 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -80,8 +80,9 @@ def build_python_interpreter(self): @functools.cached_property def is_debug(self): pybuilddir = self._pybuilddir(self.build_python_path) - if pybuilddir.is_file(): - build_details = json.loads(pybuilddir.read_text()) + if pybuilddir.exists(): + build_details_path = pybuilddir / "build-details.json" + build_details = json.loads(build_details_path.read_text()) return "d" in build_details["abi"]["flags"] else: # Python 3.13 and older. From 8222bf42efbef74d9dadfbd3ad79203f8ce108c2 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 15:18:42 -0700 Subject: [PATCH 41/44] Enhance archive function to support SOURCE_DATE_EPOCH --- Platforms/WASI/_package.py | 46 +++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index be2da369858ab98..5f41eefad20814a 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -1,5 +1,13 @@ -__lazy_modules__ = ["os", "pathlib", "shutil", "subprocess", "_shared"] - +__lazy_modules__ = [ + "datetime", + "os", + "pathlib", + "shutil", + "subprocess", + "_shared", +] + +import datetime import os import pathlib import shutil @@ -356,20 +364,26 @@ def archive(context): file_path.unlink() to_compress = context.checkout / "dist" / filename_stem(context) _shared.log("๐Ÿ—œ๏ธ", f"Archiving to {file_path} ...") - mtime = subprocess.run( - [ - "git", - "log", - "-1", - "--format=tformat:%cd", - "--date=format:%Y-%m-%dT%H:%M:%SZ", - os.fsdecode(context.checkout), - ], - env={"TZ": "UTC0"}, - capture_output=True, - text=True, - check=True, - ).stdout.strip() + mtime_format = "%Y-%m-%dT%H:%M:%SZ" + if source_date_epoch := os.environ.get("SOURCE_DATE_EPOCH"): + mtime = datetime.datetime.fromtimestamp( + int(source_date_epoch), datetime.UTC + ).strftime(mtime_format) + else: + mtime = subprocess.run( + [ + "git", + "log", + "-1", + "--format=tformat:%cd", + f"--date=format:{mtime_format}", + os.fsdecode(context.checkout), + ], + env={"TZ": "UTC0"}, + capture_output=True, + text=True, + check=True, + ).stdout.strip() subprocess.run( [ From 8c771798469d2943871014495c9d0145fd77b6b8 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 15:20:16 -0700 Subject: [PATCH 42/44] `ruff format` --- Platforms/WASI/__main__.py | 9 ++++++++- Platforms/WASI/_shared.py | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Platforms/WASI/__main__.py b/Platforms/WASI/__main__.py index 5236ae7e0409fcf..58cbc44834dd284 100644 --- a/Platforms/WASI/__main__.py +++ b/Platforms/WASI/__main__.py @@ -1,4 +1,11 @@ -__lazy_modules__ = ["argparse", "os", "pathlib", "_build", "_package", "_shared"] +__lazy_modules__ = [ + "argparse", + "os", + "pathlib", + "_build", + "_package", + "_shared", +] import argparse import os diff --git a/Platforms/WASI/_shared.py b/Platforms/WASI/_shared.py index b1494b894c44020..ee1a65859a1a4d1 100644 --- a/Platforms/WASI/_shared.py +++ b/Platforms/WASI/_shared.py @@ -146,7 +146,9 @@ def wasi_sdk_path(self): if path.is_dir() ] if not potential_sdks: - raise ValueError(f"WASI SDK {wasi_sdk_version} not found in {opt_path}") + raise ValueError( + f"WASI SDK {wasi_sdk_version} not found in {opt_path}" + ) elif len(potential_sdks) == 1: wasi_sdk_path = potential_sdks[0] elif (default_path := opt_path / "wasi-sdk").is_dir(): From 78836118de238fb43ed8efa2d31d068a8ec304c2 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 15:43:15 -0700 Subject: [PATCH 43/44] Add `lib-dynload` to get rid of a warning when launching the REPL --- Platforms/WASI/_package.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 5f41eefad20814a..7f5c3d92ccb56db 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -327,7 +327,11 @@ def gather(context): _shared.log("๐Ÿ“", "lib/", spacing=indent * 2) _shared.log("๐Ÿ“", f"python{py_version}/", spacing=indent * 3) - _shared.log("๐Ÿ“", "lib-wasm", spacing=indent * 4) + _shared.log("๐Ÿ“", "lib-dynload/", spacing=indent * 4) + (base / lib_python(context) / "lib-dynload").mkdir(parents=True, exist_ok=True) + _shared.log("๐Ÿ“„", "", spacing=indent * 5) + + _shared.log("๐Ÿ“", "lib-wasm/", spacing=indent * 4) _shared.log("๐Ÿ“„", f"python{py_d_version}.wasm", spacing=indent * 5) copy_files([wasm_file(context)], base) _shared.log("๐Ÿ“„", "LICENSE.txt", spacing=indent * 4) From 28126f490d6c1ccb1bea0471ac840f809546764e Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 22 Jul 2026 16:00:28 -0700 Subject: [PATCH 44/44] `ruff format` --- Platforms/WASI/_package.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 7f5c3d92ccb56db..37119799c233a23 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -328,7 +328,9 @@ def gather(context): _shared.log("๐Ÿ“", "lib/", spacing=indent * 2) _shared.log("๐Ÿ“", f"python{py_version}/", spacing=indent * 3) _shared.log("๐Ÿ“", "lib-dynload/", spacing=indent * 4) - (base / lib_python(context) / "lib-dynload").mkdir(parents=True, exist_ok=True) + (base / lib_python(context) / "lib-dynload").mkdir( + parents=True, exist_ok=True + ) _shared.log("๐Ÿ“„", "", spacing=indent * 5) _shared.log("๐Ÿ“", "lib-wasm/", spacing=indent * 4)