Skip to content

Releases: ClayGendron/wasi-python

wasi-python 0.1.1

Choose a tag to compare

@github-actions github-actions released this 27 Oct 13:30
Immutable release. Only release title and notes can be modified.

Changed

  • The platform module is split in three (spec 131). _platform.py held every platform difference at 1,464 of its 1,500 lines. The Windows class, its handle structure, its constants and its three helpers are now _platform_windows.py; what the Windows and POSIX classes both run (the walk's result and its refusal, the steps taken by path, the remover's budget and its walk by path, the cache's kind checks) is _platform_common.py, below both, so the import graph stays acyclic; _platform.py keeps the interface, the POSIX hosts and the one choice, at 828 lines. Nothing an embedder sees changes.
  • The module cache is read through the descriptor it was checked by (spec 135). A compiled module is loaded through the descriptor it was checked by, so the file read is the file checked; a cache whose path passes through a folder another user can write is not used, with a warning; a set given through artifacts= is hashed and compiled from one read; a cache folder or compiled module with an access list (macOS, Linux) is not used; on Windows the file is held open from its check to its load; cache_dir='' is refused, and a relative cache_dir is made absolute at the call. Running the runner with another user's home is not supported.
  • The pin moves to wasmtime 50 (spec 127). The wheel requires wasmtime>=50,<51; wasmtime-py 50.0.1 bundles wasmtime 50.0.0, whose path_rename and fd_readdir carry the fixes of the two advisories the runner had applied itself since 0.1.0a4 (a rename of a symbolic link named with a trailing slash could move a host folder into the sandbox on macOS; directory listings carried up to three bytes of host memory per entry), and whose fuel counts the work of a C++ call that ends in a caught exception, so the judge's row for it now passes. The runner's two shadows are gone, and a call without mounts passes only the three calls the cap on open files counts through the runner; the two sandbox checks now run against wasmtime's own calls. A slash-named rename on a bad descriptor or a read-only pair answers in wasmtime's own order (EBADF or EPERM before ENOTDIR). Fuel is equal to the instruction on the snapshot and in the judge's plain-reactor checks; a guest that catches a C++ exception is now charged for the work before the catch, a few thousand instructions in the kiwisolver check. On macOS, ending a process that compiled or loaded the guest no longer costs about 5 s: wasi-python run of a one-line program takes about 0.4 s where it took 5.4 s; wasmtime 50's release notes attribute this to registering unwind information with the system in bulk. A guest that passes a file call an argument out of its type's range ends with wasmtime 50's words, which name the direction.
  • The build reads its pins by exact name, and the release checks every inner file (spec 136). The build selects pins by exact name, meson builds never download, every licence input is read through its pin, the release checks every file inside its tarball, and the verify command in the release notes follows the repository's name. scripts/steps/common.sh's pinned() and its new pin_of() find a pin by its name field compared as text, once, where a grep let . match any character; every meson configure passes --wrap-mode=nodownload (HarfBuzz keeps its own nofallback; contourpy, which has no wrap file to fetch and records its configure options in the shipped zip, is the one exemption); the download uses curl --proto '=https' --proto-redir '=https' --tlsv1.2 and names a present file that does not match with the fix; scripts/assemble_artifacts.py reads the wheels, the sympy wheel, the wasi-libc and LLVM notices and the Unicode terms through pinned_bytes(); the release's same-bytes job checks every file of the unpacked .tar.xz against its SHA256SUMS (no other file, no symlink, every published file listed), and the build job verifies the set again just before it packs it; release/version.py notes takes --repository, which the publish job sets to $GITHUB_REPOSITORY, and a unit check holds the documented command equal to the one the attest job runs. The zero-patch check now names a subprojects/ folder its tarball did not ship, and .gitignore names .env, .env.*, *.pem, *.key and .claude/settings.local.json. The three artifacts and LICENSES/ do not change; MANIFEST's recipe lines for the edited scripts do, and so does SHA256SUMS's line for MANIFEST.
  • Pull requests into dev run what their paths need (spec 138, step 2): the release workflow runs on every pull request into dev and main, on every merge into dev and weekly, besides the tag and a dispatch. One decision job, release/ci_plan.py, reads the changed paths with git and gates every later job with if:: quick (ruff, mypy, the unit tests, the rules) runs always; the judge's shards run from the cached set as the paths need (Linux and Windows for any code, macOS when the runner, the set or the harness changed, the upstream groups on Linux when the set, the suites or the harness changed); the gather is narrowed to the pairs that ran, lists a growth instead of failing on it, and leaves G7 to the release; ci-status is the one required check. A merge into dev runs every normal-tier shard on three systems, the build shard and verify on three. The judge jobs' matrices are generated from tests/release_shards.toml, every reader of the build's files runs one local action, the build shard and the arm64 rebuild share one compile, and release.yml is 1,316 lines, from 2,602.
  • The per-call comparison of matplotlib's data takes half the interpreter's time (spec 128). Every call compares the whole mpl-data tree, 213 entries, with its record before granting it; the comparison walked the tree with os.walk and built a path object per entry, 2.3 to 2.9 ms of interpreter time per call under the zip folder's lock. It now lists with os.scandir, stamps each entry with one os.lstat as the record does, and keeps plain strings: the same comparison, the same one stat per entry on every system, about 1 ms. On the research machine the eight-thread wall of eight hellos shortened by 14 to 33 % in three windows with a result file (their ratio to the sequence 1.5 to 1.9 before, 1.9 to 2.0 after; on a shared machine the wall is the steadier number), and a hello in sequence is one to two milliseconds shorter (benchmark facts, not checks). The memo behind it found no lock of the runner's held across a guest and none waited on: concurrent runs wait on the interpreter's lock over the runner's per-call checks, and that ceiling is named, not hidden. A check now pins that the home thread holds no process lock while its guest runs.
  • A guest's output is read from a pipe on macOS and Linux (spec 129). wasmtime hands a custom output its bytes 4 KiB at a time on a thread of its own, and the runner's callback there cost about 14 µs a piece, most of it the hand-off and a Python thread state made and destroyed per piece: 32 MiB of output took 160 to 240 ms, ten thousand printed lines 300 ms. wasmtime now writes each stream into a pipe the runner reads on one thread of its own per call: about 40 ms and 35 ms. The output cap holds to the byte, truncated names the same streams, stdout and stderr stay apart, and nothing in the API or its defaults changes. Windows keeps the callback for now.
  • Every merge into dev is accepted by CI (spec 138, step 3): the run after a merge judges the normal tier on three systems, and two jobs follow a green gather. accept-record runs uv run pytest --accept-from <record.json> --claims context/changes/ with no secret, and accept-push, in the accept environment, commits the two progress files alone and pushes them onto dev with a deploy key, only while dev is still the commit judged. A change's CHANGELOG line, package rows and claims are its news file under context/changes/, which release/version.py bump assembles at a release; ci-status refuses a pull request into dev that is not documentation only and has no news file, or no review file whose verdict begins "Lands", committed after its last code, by a reviewer other than its author. A documentation-only push onto a green head re-uses that head's run, and the weekly run opens an issue when it is red.
  • A release is a pull request from dev to main (spec 138, step 4): its run is the whole workflow on the commit main will receive (a set built on macOS and never reused, both rebuilds and same-bytes, every shard of both tiers on three systems, verify on three and quick), and its gather has every goal: no PLAN, G7 holding the newest acceptance to the run's own set, clean and whole, and each release-tier row that moved since the last release listed with the reason of a news file or of the release-tier ledger. Its own accept-record accepts the record of both tiers, and .github/workflows/accept-release.yml, started by that run's completion on dev's own context, pushes the acceptance onto dev while dev's tip is the head judged, red when it is not. When the acceptance lands, the release pull request's next run re-uses the green one. The release pull request is refused any commit that changes the accepted state and is not an acceptance, and its build job checks that the version's section is dated, its tag free and main's tip an ancestor. release/version.py date dates the section on dev before the release pull request, joins any news file merged after bump and moves the ledger's blocks under the version; release/version.py release commits nothing, tags main's tip and pushes the tag alone, so the commit tagged is the one judged plus its acceptance. bump, date and the release's two reviews land in one pull request into dev, the release's preparation, which the decision job judges as documentation, since its one change outside documentation is the version line and its change to...
Read more

wasi-python 0.1.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 14:21
Immutable release. Only release title and notes can be modified.
  • A coverage badge from CI (spec 124): the release workflow's new coverage job runs the fast tier serially under coverage.py (7.17.0, pinned in the dev group) against the set the build job made, and uploads the Cobertura report to Codecov with the repository's CODECOV_TOKEN; the README carries Codecov's badge, and test_readme_carries_the_codecov_badge holds that it is this repository's. uv run python tests/harness/coverage.py --measure takes the same measurement locally into build/coverage/. The -V sentence and numpy's integer width are now held on docs/reference/limitations.md, not the README.

  • The wheel's LICENSE carries every component licence (spec 125): before each wheel build the release workflow runs release/licences.py append, which rewrites the checkout's LICENSE as the Apache-2.0 text followed by every LICENSES/*.txt of the set in name order, as Pillow's wheels do; the build job checks the wheel it built, and the judge's wheel check compares .dist-info/licenses/LICENSE with the same text. FreeType's licence ships there, and the combined LICENSE's introduction gives FreeType's credit in words, so the README no longer carries it; release/licences.py restore puts LICENSE back after a wheel built by hand; test_the_wheel_carries_every_component_licence replaces the README credit check.

  • A short README and a clean tree (spec 122): the README is cut to about 170 lines (what it is, a five-minute quickstart, what is safe by default, what runs, hosts, the stability promise, verifying a download, building, licences), every link absolute for PyPI. The generated API reference moves to docs/reference/python-api.md (uv run python tests/harness/api.py --write writes it, --page names another); the command's options are generated from its parser into docs/reference/cli.md by tests/harness/cli_doc.py, and test_generated_pages_are_current holds both pages current. The 560 known gaps and suite entries whose closer was the README, and pandas' excluded marker, now name docs/reference/limitations.md, a new page of the platform's limits with a reason for each; six that named the questions file name the roadmap. The scans for document citations and embedder names read the README and every page under docs/. pyproject.toml says "Development Status :: 4 - Beta", names all seven compiled packages, gains a Documentation URL and a docs dependency group (MkDocs 1.6.1, Material 9.7.7); mkdocs.yml and .github/workflows/docs.yml build the site strictly on every pull request and push to main, and deploy it from a release tag. context/questions-for-the-owner.md is deleted; its last paragraph moves to context/decisions.md.

  • The documentation site (spec 123): a Diátaxis tree under docs/ (get started, how-to guides, reference, explanation, contributing), built with MkDocs and served at https://claygendron.github.io/wasi_python/ from each release tag.

  • The release runs the pure-Python packages' own tests (spec 067): each whole suite before a publication, a smoke group per package on every run, for the 42 pure-Python packages the product ships; 39 test sources (each package's sdist, or its release tag's archive where the sdist carries no tests, and pypdf's sample files) are pinned and named in MANIFEST, and the judge ties each to the bytes that ship. Four test-source patches are carried, each making one test-only import lazy or optional so the rest of its file runs: recipes/patches/pypdf-6.21.1/0001-tests-import-ssl-where-it-is-used.patch, recipes/patches/cloudpickle-3.1.2/0001-tests-import-psutil-where-it-is-used.patch, recipes/patches/formulaic-1.2.2/0001-tests-skip-the-pyarrow-check-without-pyarrow.patch, recipes/patches/jinja2-3.1.6/0001-tests-run-the-asyncio-cases-without-trio.patch. sympy's doctests run under sympy's own doctest runner, output checker and printer setup (the setup once per session, with the stdlib's finder: module globals, and doctest_depends_on not read); six sympy tests that do not end within 2.9 to 4.5 T of fuel, sympy's test_W25, and networkx's test_asadpour_integral_held_karp, whose path is not the same on Linux, are traps, named with their reason.

  • Two tiers of tests (spec 102). The slow tests run before a publication, never left out: numpy's and pandas' slow cases, contourpy's slow and random_big cases and CPython's cpu-gated tests form a release tier, judged on the tag, on the dry run on main and in the lane's release acceptance (--tier release); every other run judges the normal tier, about 4 T lighter. A row a run's tier did not run keeps what was accepted, a check that moves between tiers needs --accept-tier-move, and context/release-tier-claims.toml holds the claims between two releases.

  • The release runs scipy's, scikit-learn's and statsmodels' own tests (spec 095). Each suite runs whole in the release tier, 261 groups in 104 Linux release shards, about 86 T of guest fuel; a smoke group per package (two for scipy) runs in the normal tier at every check, 855 G in two Linux shards, chosen so every subpackage with compiled modules and every carried patch is touched, a rule the judge holds against the product's builtin modules. Every known failure is a class by test id with its closer; every file not run is named with its reason. The judge gains three keys for these families (the ancestors' __init__.py staged, test data scipy's build makes from its sdist made in the guest, a family's environment) and a clock step for a release group whose one test times a call.

  • One module touches wasmtime-py's private API (spec 103). src/wasi_python/_wasmtime_ffi.py is the engine seam: the runner and the mounts drive the engine through its named operations, it prefers wasmtime-py's public API wherever the installed release has it, and at the first run() it raises one ImportError naming the wasmtime-py version and every private name that moved. A rules check holds that no other .py under src/ or tests/ reaches the private API; a second holds the debt sheet context/upstream/34-wasmtime-py-public-api-for-the-runner/facts.md equal to the names the seam needs. No behaviour, fuel or errno changes.

  • The runner is a package (spec 104; the runner design review's H1, M5, L1). runner.py (6,241 lines) is eleven modules, one per concern (_limits, _platform, _artifacts, _engines, _private_dirs, _zip, _places, _home, _call, _sessions, and runner as the thin entry), the text moved verbatim, in a written import order with no cycle; __init__.py no longer imports runner last; a unit check finds every module-level lock and proves the fork hook re-makes it; a rule holds every module under src/ to 1,500 lines. Nothing the embedder sees changes: no public name, no message, no fuel count.

  • The runner's process state has one owner (spec 105): every lock, cache, counter and registry of the runner is an attribute of one _Process object, reset for a fork by introspection over its locks and released at exit by one method; the judge's seams are fields of that object, not module globals; two rules checks keep locks and mutables out of module level. No public name changes.

  • The runner's checked call is a frozen typed record, a session's and a home's life are transition tables the judge drives whole, and mypy --strict runs over src/wasi_python in the judge (spec 106).

  • The runner's mount table, platform choice, file stamps and comments explain themselves (spec 107; the design review's H4, M3, M4 and M2). mounts.py asks nine questions of a call-context protocol and imports nothing from the runner; one _platform.py holds every POSIX, macOS and Windows difference behind one interface chosen once at import; the digest and version caches in front of the artifact check key on the full file stamp, so a same-size, same-time replacement is hashed again; src/ names no specification, memo, review, finding ID or context/ path, every reason kept in words, held by one rules check (the owner's rule, "code speaks for itself"; tests/, scripts/ and release/ follow in spec 109); and mypy --strict is clean over the whole package. No public name, artifact byte or guest-visible errno changes.

  • A structural review is a stage (spec 108; the design review of 2026-10-20). A change that adds a module-level mutable or lock, a use of wasmtime-py's private API, a platform branch or a test seam outside the one place specs 103 to 107 give each, or takes a module past the size ceiling or a function past 80 lines, is reviewed before it is built; every release gets a structural review of src/ against the design review's eight-row rubric, beside its security review, which the judge requires of a released version; and two rules checks hold that the judge patches the runner only through its process object and that no function is over 80 lines, each with an allowance list that only shrinks. Nothing in the artifact set or the wheel changes.

  • Code speaks for itself outside src/ (spec 109). Comments and docstrings under tests/, scripts/, release/ and the release workflow give their reasons in words, with no specification number, review, finding identifier or context/ path; a rules check holds it, with the judge's allowance files, the guest's own sources and the src/ check's own documentation of its shapes named as the exceptions. No code, behaviour, fuel or artifact byte changes; MANIFEST's recipe hashes of scripts/ move with their comments.

  • numpy's linear algebra, dot and matmul run on the set's OpenBLAS (spec 100; questions 106 and 107). numpy was built with no BLAS and ran its bundled lapack_lite, an f2c LAPACK 3.2.2. It now finds the OpenBLAS 0.3.34 scipy uses, through numpy's own build options; a one-line recipe header (recipes/numpy_lapack_abi.h) and a file of shims (recipes/numpy_lapack_shims.c) bridge numpy's int declarations to Op...

Read more

wasi-python 0.1.0a7

wasi-python 0.1.0a7 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 19 Oct 13:21
  • Fixed: an async gate that raises GeneratorExit refuses the call (spec 075; the 0.1.0a6
    security review's F1). An async gate (an async def, a function returning an awaitable, or an
    object with an async __call__) that raised GeneratorExit or a subclass used to allow the
    call, and the tool ran; an async tool that raised it was answered ok with None. Now the
    gate's call is denied and the tool's tool_error, each with the fault in the record, as a
    sync gate's and a sync tool's already were. The result of an awaitable counts only if it
    returned. Also (spec 060's build review, N1): a tool list over the 16 MiB answer cap is refused
    from its length on each list frame, without its answer being built. No public name, default
    or answer on the wire changes.
  • Changed: the snapshot pre-imports no package (decision 34, spec 069). It starts with the
    interpreter and the stdlib's bootstrap modules (encodings, io, os, sys, json, csv,
    re, _colorize), for every package now and later; the judge refuses a snapshot that holds
    any other top-level module. A bare call costs 15.6M guest instructions instead of 83.5M; a call
    that imports pandas pays it, 4.60G in all instead of 83.5M (a small numpy job about 0.7G more
    than before), so a fuel= cap set for a pandas job on the snapshot needs that much more. The
    snapshot is 30.4 MB smaller (44.6 MB) and the wheel about 11 MB smaller. numpy, pandas and the
    other packages now import with the call's environment. A session's shutdown costs about 43M
    instructions when nothing beyond the stdlib was imported (about 306M before). snapshot=True
    is unchanged as an argument; no public name, signature or default changes. MANIFEST's
    x-preimport line is now read from the recipe. Known and unchanged: run on the snapshot
    loses an unclosed file's buffered bytes when __main__ defines a function (a gap until spec
    073; close your files before a call ends).
  • Bounded sessions (spec 055). Session keeps one instance of the runtime across turns:
    session.run(code) and await session.run_async(code) each run one turn in __main__, and
    variables, functions, classes and imports carry over. A turn echoes its final expression,
    reports an exception with <turn N> in its traceback, and reports sys.exit() as its exit
    status while the session goes on, as a Jupyter kernel does. Each turn has its own fuel,
    deadline, output cap, cancel, file-call and tool-call allowance; the session has one memory
    cap, an idle bound (idle_s, default 15 minutes) and a lifetime (lifetime_s, default 1
    hour), which caps every turn's deadline; max_open_files counts across a session's turns and
    its shutdown, since its store keeps what they open. A trap, os._exit, a bound, close() or a turn's
    exit() or quit() ends it; session.ended says which, and later turns raise
    SessionEnded. A session's close and expiry, and exit() or quit() in a turn, run
    Python's shutdown (atexit, flushes) within shutdown_s seconds and the session's fuel, as a
    Jupyter kernel's do; past that budget the session is stopped; sys.exit() keeps the session;
    session.shutdown_outcome says whether the shutdown finished, was stopped or did not run. Added:
    Session, SessionEnded, three Defaults fields (session_idle_s, session_lifetime_s,
    session_shutdown_s), stats()["sessions_open"], and two exports in both binaries, turn and
    shutdown, whose contract the README gives for any embedder. run is unchanged.
  • Deep recursion raises RecursionError (spec 071). A __repr__ that calls repr(self),
    lru_cache recursion, a recursive sort key or a numpy, pandas or lxml callback used to end the
    guest in a wasm trap; it now raises, on both binaries, at wasmtime's default 512 KiB stack.
    The guest narrows CPython's C-stack guard to 128 KiB with CPython's own
    PyUnstable_ThreadState_SetStackProtection, called from the reactor's recipe: no patch.
    Changed: such recursion stops at about half the depth it reached before trapping (214
    levels for the __repr__ case, 40 to 250 by shape), so a program that recursed that deep and
    finished now raises. Callbacks out of expat, recursion in C over very deep data, and freeing
    very deep structures still trap, with the limits the README lists. The runner now states its
    wasm stack, 512 KiB, instead of taking wasmtime's default, and the judge's guest driver makes
    the runner's engine (fuel, epoch interruption, that stack). python.wasm, the snapshot and
    MANIFEST change; the zip does not.
  • On Linux, a host's faulthandler can no longer turn a guest trap into the host's death
    (spec 071). If faulthandler was enabled or disabled after the runner's first call, a guest
    trap used to kill the host with SIGILL (and after a late signal.signal for SIGILL, hang
    it); run now refuses such a call with RuntimeError before the guest runs, says why, and
    names the remedy. Install faulthandler and crash reporters before the first call, or not at
    all.
  • The build runs only the pinned host Python (spec 079; the 0.1.0a6 security review's
    F2). FreeType's configuration headers, HarfBuzz's generated sources and matplotlib's
    matplotlibrc were written by whatever python3 the build host had first on PATH, and
    CPython's configure ran it once. Now step 10 puts a shim for the pinned CPython 3.15.0 first
    on every step's PATH. Behind it, scripts/build.sh puts a stand-in for every other Python
    name the host has, which fails the step if it is run. A build host needs no Python of its
    own. The artifacts are the same bytes; MANIFEST's recipe lines change.
  • The release judges in parallel shards (spec 063). The release runs numpy's and pandas' own
    test suites (eleven groups, 63,718 passing tests) on Linux, one group of them on macOS and on
    Windows, every product check and the runner's own tests on all three systems, and the rules and
    the judge's own tooling on macOS and Linux, in eighteen parallel jobs after the build. A
    gather job reads every shard's record by id and SHA-256 and publishes nothing unless each
    shard ran exactly its share on the release's bytes, nothing failed, the probe passed on three
    systems, and the lane's newest acceptance is on the same python.wasm. uv run pytest --shard=<name> runs one shard. Every package now names its own suite, or why not yet, in
    tests/upstream/packages.toml.
  • lxml, with libxml2 and libxslt built in (spec 046). lxml 6.1.3's lxml.etree and
    lxml.objectify are builtins, over libxml2 2.15.4 and libxslt 1.1.45 with libexslt: XPath,
    XSLT, XInclude, C14N, DTD, XML Schema, RELAX NG and Schematron validation, the HTML parser,
    iterparse, and files read and written by path inside a grant. lxml's Python package
    (lxml.html, lxml.builder, lxml.sax, ElementInclude, lxml.html.diff) is in the zip with
    its METADATA. pandas' read_html with no flavor now works (lxml is its first flavor), and so
    do read_xml and to_xml with their default parser. The judge's golden job lxml_xml equals
    native lxml at the same pins. What differs from native, as the README says:
    • Character encodings are wasi-libc's iconv, not GNU libiconv: windows-1252, ISO-8859-2
      and -15, cp1251, KOI8-R, TIS-620 and UTF-16 round-trip every character; Shift_JIS,
      ISO-2022-JP and GB2312 read a few characters as other characters; cp932, EUC-JP, Big5 and
      cp950 documents fail to parse (in HTML and recover=True their text ends at the first
      unreadable character, with no exception); most CJK writes raise LookupError. GB18030
      fails at its first non-ASCII byte, on purpose
      , because wasi-libc decodes 1,617 of its
      characters wrongly without an error: recipes/iconv_refusals.c, linked into the reactor
      with -Wl,--wrap=iconv_open, iconv and iconv_close, until a wasi-sdk carries musl's fix
      (context/upstream/15-wasi-libc-gb18030-decoder/). Strict XML raises, recover=True keeps
      no root, the HTML parser keeps only the ASCII text before it, read_html of a Chinese
      table raises; no character is read wrongly in any mode. But a table that reaches the first
      non-ASCII byte comes back cut there with no exception: its last row may be all NaN, a
      column may turn float, and the rows and tables after it are missing (check error_log).
      The README gives the working route: decode with Python's gb18030 codec and give lxml the
      text.
    • XSLT reads and writes files by default. A stylesheet without an access_control, or a
      document it loads with document(), can read any file in a grant (external entities
      included) and write into any writable grant; use etree.XSLTAccessControl.DENY_ALL for
      untrusted stylesheets. lxml's parser defaults stay the safe ones.
    • Left out: lxml.isoschematron (it reads its stylesheets by file path, and their terms
      are not approved; a stand-in raises ImportError; etree.Schematron works), zlib in
      libxml2 (XMLParser(decompress=True) raises; lxml compresses output through Python's
      gzip), cssselect and lxml_html_clean. A catalog is read only from a grant at /etc/xml.
    • One more carried patch:
      recipes/patches/libxml2-2.15.4/0001-xmlio-without-dup-on-wasi.patch lets libxml2's file
      input and output work without dup(), which WASI preview 1 does not have (without it
      libxml2 does not compile for wasm32-wasip1). Its facts sheet for the owner:
      context/upstream/patches/libxml2-2.15.4/0001-xmlio-without-dup-on-wasi.md.
    • The build: two new steps, 36 (libxml2 and libxslt, by their own configure, under an empty
      environment, with the pinned pkgconf and an empty search path) and 54 (lxml's two modules,
      from the Cython 3.2.9 C its sdist ships); three new pins. libxml2's default catalog is
      native Linux's /etc/xml/catalog, not a bui...
Read more

wasi-python 0.1.0a6

wasi-python 0.1.0a6 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 19 Oct 01:49
  • The x86_64 rebuild runs natively (spec 059). Every compile carries -ffp-contract=off,
    set once in the toolchain's own clang.cfg and clang++.cfg by step 10, so wasi-sdk's x86_64
    and aarch64 builds of clang emit the same bytes, and the x86_64 Linux rebuild compiles with its
    own clang instead of the arm64 clang under qemu (an hour less per release run). Run-time
    arithmetic does not change (wasm32 fuses nothing); a constant the compiler folds moves to the
    value the run time computes. python.wasm, the snapshot and MANIFEST change once; the zip
    stays. The judge refuses any compiler option that would override the file (-ffp-model=,
    -ffast-math, -Ofast, -funsafe-math-optimizations, --no-default-config, --config,
    another -ffp-contract=) in the recipe and in every build record, greps the source trees for
    a pragma that turns contraction on, and probes the compiler's own -cc1 line. Spec 040's
    emulated compiler stays in the tree, switched off by one line in scripts/line.conf, until
    the first alpha whose native x86_64 rebuild passed has shipped. The LLVM issue draft is
    narrowed to llvm.fmuladd on fp128.

  • Async tools and gates (spec 053). A tool or a gate may be an async def, or a function
    that returns an awaitable. Under run_async it is awaited on the event loop that awaits
    run_async, so an MCP client session or an async HTTP client works as it is; under run, on a
    loop of the call's own, on a thread started at the call's first awaitable and closed when the
    call ends. A call answered before its coroutine started never starts it (spec 056's fix,
    extended to async tools); every later end of the call (timeout_s, the deadline, a cancel,
    Ctrl-C) cancels it at its next await. A KeyboardInterrupt, SystemExit or
    CancelledError a tool raises, and a loop that has closed or stopped, are faults in the record,
    never exceptions in the embedder's loop. The guest, the protocol and the artifact set do not
    change. The README's MCP recipe is now an async def. An async generator is refused as a tool
    or a gate.

  • Changed: host= given an async def (or an async generator, a partial of one, an
    object with an async __call__) is refused with TypeError before any work. The raw door is
    synchronous; it used to answer every frame with a host fault and never await the coroutine.

  • A provider may raise OSError (spec 061). Its errno, matched by name (never by number,
    which differs by host), is the guest's answer, as the Errno of that name would be. With no
    errno at all, FileNotFoundError, FileExistsError, IsADirectoryError,
    NotADirectoryError, PermissionError and TimeoutError answer ENOENT, EEXIST,
    EISDIR, ENOTDIR, EACCES and ETIMEDOUT. An errno outside the closed set (EINTR,
    EAGAIN and the rest), whatever the class, and any other exception stay a fault, EIO. A
    mapped raise leaves no fault in the record. The guest never sees the exception's message, and
    the record keeps nothing of it.

  • The judge runs on several cores (spec 052). uv run pytest -n 6 gives the serial
    answer in about a third of the time: pytest-xdist 3.8.0 is pinned in the development group,
    each check's record is made on its worker and carried to the controller, which alone writes the
    run record and accepts, and an upstream group runs with its classes in one worker. The lane's
    accepting run stays serial; tests/harness/sameness.py compares two run records. Five checks
    no longer depend on the order or speed of the run, and the judge's zips folder is safe when
    several processes make it at once. Nothing in the artifact set or the wheel changes.

  • Security: a per-run budget of tool request bytes (spec 060; the 0.1.0a4 security review's
    F3; spec 049, Decision 8, exception 2). max_tool_request_bytes= on run and run_async, and
    Defaults.tool_request_bytes, default 16 MiB, counted from each frame's length before it is
    read, whatever its answer; a frame that would pass it is refused unread, and the guest gets
    ToolBudgetExceeded and runs on. Parsing a 1 MiB frame costs the host a few tenths of a second
    of CPU, so until now only the deadline bounded one call's total. The tool list is encoded once
    per call, not once per list frame. tool() finds a partial's bound keywords through a
    __call__ chain of any depth and through a metaclass's __call__ (the review's N8), and
    refuses a __call__ cycle with TypeError, where Python 3.14 raised RecursionError.

  • Changed: a new, tighter default (spec 060; spec 049, Decision 8, exception 1): a call that
    sends more than 16 MiB of tool requests passes a larger max_tool_request_bytes, or None.

  • matplotlib, the first milestone (spec 023). matplotlib 3.11.2 is built in: import matplotlib; matplotlib.use("agg"), plt.plot(...) and savefig to PNG, PDF, PS, EPS or SVG
    in a granted folder. A fixed line plot's RGBA buffer equals native matplotlib's at the same pins,
    and its PNG decodes to those pixels (the judge's golden job plot_line). Linked in: FreeType
    2.14.3, HarfBuzz 14.1.0, SheenBidi 3.0.0 and libraqm 0.10.5 (matplotlib's own wrap versions, for
    native pixel parity), matplotlib's ft2font, _path, _image, _backend_agg,
    _c_internal_utils and _tri, kiwisolver 1.5.1 and Pillow 13.0.0's _imaging, _imagingmath
    and _imagingmorph (zlib only: JPEG and other codecs raise OSError). In the zip: matplotlib,
    mpl_toolkits, PIL, kiwisolver, fontTools 4.67.0, cycler 0.12.1, pyparsing 3.3.3 and packaging
    26.3, each with its METADATA. Not yet: Delaunay triangulation (_qhull is left out of the link;
    explicit triangles work) and contour (contourpy), both the next milestone; the snapshot does not
    pre-import matplotlib (the owner's question 34).

    • matplotlib's data travels inside the zip as one member, matplotlib/mpl-data.zip. The
      runner extracts it once per process into its private zip folder, checks every file as it
      writes it, compares the whole tree with its record on every call, and grants it read-only at
      /usr/local/lib/python315.zip/matplotlib/mpl-data, the path matplotlib computes. The zip
      itself now sits in that folder's lib/, so /usr/local/lib still lists only the zip
      (spec 017's Decision 2, amended). An embedder without the runner extracts and preopens it
      itself (the README says how).
    • Three more carried patches.
      recipes/patches/matplotlib-3.11.2/0001-ft2font-enums-without-python.patch is upstream's
      merged commit 8e5591bc (pull request #30291, matplotlib 3.12): ft2font's enums no longer
      take the GIL during static initialization, which hung a statically linked reactor before Python
      started. recipes/patches/pillow-13.0.0/0001-arrow-capsule-signatures.patch and
      recipes/patches/kiwisolver-1.5.1/0001-callback-signatures.patch give two Pillow methods and
      21 kiwisolver methods and getters the signatures CPython calls them with: spec 051's callback
      scan found them in the first matplotlib link, and kiwisolver's trapped the guest at
      Solver.addConstraint. Facts sheets for the owner: context/upstream/17-… (nothing to send),
      18-pillow-arrow-capsule-signatures/ and 19-kiwisolver-callback-signatures/.
    • The build: two new steps, 37 (the plot libraries) and 55 (Pillow, kiwisolver and
      matplotlib); the pinned pkgconf wheel in step 10; scripts/wasi-cc drops -pthread, which
      would have compiled +atomics in; step 60 checks the static constructors and target features
      of every link against the judge's reviewed lists and stops on a new one. Eighteen new pins,
      among them the Unicode License V3 for HarfBuzz's tables, pinned at a commit of Unicode's own
      repository.
    • The judge: the milestone job and its PNG decoder (stdlib only); savefig(dpi=-1) and a
      kiwisolver UnsatisfiableConstraint; the data grant refuses every write; the static
      constructor list; the ten target features; fifteen source trees judged against their tarballs
      (the release workflow's count is 15); every C++ build's exception flags; the licence notices of
      every new component. The callback scan no longer reads a record whose function sits at a parser
      rule's table index as a method definition (FreeType's BDF property table). Two known gaps,
      closed by the research memo's M2.
  • Security: the release signs and publishes only what its build job made (spec 057; the
    0.1.0a4 security review's F4, N1 and N3's workflow side; it closes the one-release allowance the
    0.1.0a5 addendum gave F4, before this release). The build job hands on its upload's artifact id
    and the SHA-256 of its SHA256SUMS. Every job that checks, signs or publishes refuses an empty
    id, downloads by that id with digest-mismatch: error, and checks SHA256SUMS, every file, and
    that nothing else is there, before it acts; until now each fetched whatever artifact was named
    release-assets. The two Linux rebuilds are two jobs instead of a matrix, so one cannot hand on
    the other's ids. The download step fails, naming the file, when a fetched file has no pin.
    publish names the release environment, setup-uv's cache is off in every job, and every job
    states its own least permissions under a workflow-level permissions: {}. The judge checks all
    of it, runs the workflow's own check and guard steps, and runs the download step on fake pins.
    It also refuses any other download into release-assets or into a job that holds it, any
    continue-on-error:, and a pypi job that could run before publish or outside a pushed tag.
    python.wasm, the snapshot and the zip do not change. MANIFEST changes in one line, because
    it records the download script's hash, so the set's SHA256SUMS and the wheel change too.

Verify a download (gh logged in; see the README's Verifying a download):

gh...
Read more

wasi-python 0.1.0a5

wasi-python 0.1.0a5 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 18 Oct 19:38

The first release since 0.1.0a3. 0.1.0a4 was tagged but never published (see its section):
everything listed under 0.1.0a4 ships in this release too, including the security fixes of specs
048 and 056.

  • Read-only code on the import path (spec 054). Added: run(import_path=[…]) and
    run_async(import_path=[…]), and wasi-python run --import-path GUEST. Each entry, a folder
    or a zip file inside a read-only grant, goes on sys.path after the stdlib zip. The runner
    refuses, before any work, an entry the root, a writable grant or a mount serves, a writable
    grant or a mount under an entry, and a writable grant or root inside the grant serving one.
    No .pyc is written. A module the runtime has is always the runtime's; one it looks for and
    lacks (_hashlib, pytz, …) is taken from an entry, on the plain binary also for packages
    the snapshot pre-imported (README, The import path). Fixed (docs): the README no longer says
    a writable mount at / puts / on a -c program's sys.path; nothing writable is ever
    there.
  • A Windows grant renames a folder over a file (spec 058). The 0.1.0a4 release run stopped
    on Windows, where a writable grant lets os.rename replace a file with a folder; a mount
    answers ENOTDIR (54) there as on every host, and as a Linux or macOS grant does. The judge
    now asserts that row's grant answer, and the listing that follows from it, exactly per system,
    and the mount's answer everywhere; Linux's and macOS's assertions are kept, and each gains an
    exact value. The README names it, and a second Windows difference the same run showed: a
    Windows grant cannot remove or rename a file the guest holds open, so tempfile.TemporaryFile
    fails there. Nothing in the runtime or the artifact set changes.
  • The public API is frozen and documented (spec 049).
    • Added: defaults() and set_defaults(), which replace assigning
      wasi_python.runner.DEFAULTS (that still works, unpromised, until the internal name
      changes); Defaults and stats() at the top level; PROTOCOL in
      wasi_python.tools.__all__; py.typed, so a type checker reads the package's hints; tool's
      two overloads, so a function decorated with @tool keeps a type; and the README's "API
      reference", generated from the same render as tests/public_api.txt, the judge's snapshot of
      the surface.
    • Changed: the seventeen other wasi_python.tools constants (TOOL_REQUEST_BYTES and the
      rest) and wasi_python's ARTIFACT_DIR, ARTIFACTS_ENV and CHECKOUT_ARTIFACT_DIR are
      private, each with a leading _; the numbers stay in the README. Defaults' memory_bytes,
      deadline_s, output_bytes and file_calls are annotated … | None, host as returning
      bytes | bytearray | memoryview, and Mount.provider as a FileProvider, as the code
      already accepts. A call reads the defaults once, when it is made, so a set_defaults on
      another thread never gives it a mix. The README no longer names wasi_python.runner or
      DEFAULTS.
    • The promise, from 0.1.0, the first final release: a patch release changes nothing
      public; a minor release may add; a deprecated thing keeps working for at least two final
      minor releases (deprecated in 0.N, removable in 0.N+2); a tighter default, in a minor release,
      and a security fix, in any release, need no deprecation. The README's "API reference" says
      what is promised and what is not.

Verify a download (gh logged in; see the README's Verifying a download):

gh attestation verify <asset> --repo ClayGendron/wasi_python \
  --signer-workflow ClayGendron/wasi_python/.github/workflows/release.yml@refs/tags/v0.1.0a5 \
  --source-ref refs/tags/v0.1.0a5 --deny-self-hosted-runners

wasi-python 0.1.0a3

wasi-python 0.1.0a3 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Oct 21:19
  • Breaking: nothing is writable by default (the owner's rule, constitution §4.15; spec 025).
    A call given no root and no grants has no /, no /tmp and no HOME: it can import,
    compute, print, read the stdlib and call the door, and nothing else. Before, it got a fresh
    private writable /. To keep that, pass root=True (--private-root on the command line);
    or grant the folders the program needs. root=None now means no root, like False.
  • Security: a root is prepared only through the folder that was checked (the 0.1.0a3
    security review, S1). The runner made root/usr/local/lib and root/tmp by path after
    checking the root, so a guest of another call with a writable folder above it could swap that
    folder for a link in between, and the runner removed a file named usr, local, lib or
    tmp, or made folders, outside every grant. It now works through the walked folder's
    descriptor; the guest never saw the wrong folder before, and still does not. A writable grant
    at, inside or above the host Python's own stdlib or site-packages is now refused (S2). The
    patch applier refuses a source tree that holds .git (S3).
  • np.save and np.load of files work (the first carried patch,
    recipes/patches/numpy-2.5.2/0001-npy-files-without-dup.patch): they raised OSError 58,
    because numpy's tofile/fromfile need os.dup, which WASI lacks. The patch is the fix numpy
    is asked to take (its pull request is drafted for the owner) and goes when numpy releases it.
    ndarray.tofile, np.fromfile on real files and memory maps still fail. The next §5.6 security
    review reads this patch.
  • Carried patches (the owner's decision; constitution §4.1): a fix the project cannot wait
    for may now be carried as a patch to a pinned project's source, under recipes/patches/, with
    its upstream pull request drafted under context/upstream/patches/. Each is reviewed, hashed in
    MANIFEST, named here, and deleted once upstream releases the fix. None is carried yet; the
    release check is now "the source trees are their tarballs plus the recorded patches".
  • Typed grants: run(grants=[Grant(host, guest, writable=False), ...]) gives the guest
    more host folders, each read-only unless it says writable=True, nested in any order;
    root=False gives it no /, only its grants. Every grant and root is walked folder by folder
    and checked before any work: no link a guest could have made, no read-only grant inside a
    writable one, no writable grant at, inside or above the runtime's own folders. After wasmtime
    opens each folder, the runner checks it opened the one it checked. HOME is made in the folder
    the guest sees at /tmp, and there is none without one.
  • The stdlib zip is read-only and no longer copied into a root: every call sees it read-only
    at /usr/local/lib, from one folder per process that its exit removes (a killed process's is
    swept like its other folders). A python315.zip that 0.1.0a2 left in a reused root is never
    read again and may be deleted.
  • Some root= values are now refused (a change): a root at, inside or above the runtime's
    own folders, such as the home folder, a project folder holding .venv or build/artifacts,
    or the temporary folder itself, since its guest could change files later calls trust; a root
    holding wasi_python's or wasmtime-py's own code; a root still to be made where the module
    cache will be made; and a root reached through a link in a folder you can write. A missing
    root is still made. No grant, read-only either, may hold the temporary folder, which holds
    every call's private folders and stdin.
  • Grants on the command line: wasi-python run --grant GUEST=HOST, --grant-writable GUEST=HOST, --root HOST and --no-root, each one of run's arguments.
  • wasi-python run: a command that runs one program and prints its result as one line of
    JSON, so any language can run agent code by spawning it (python -m wasi_python run is the
    same). Each option is one of run's arguments; without options the call has run's defaults.
  • A killed process's files are cleaned up: a process killed mid-call left its private root
    (the guest's files) and its stdin folder (the caller's stdin) in the temporary folder for
    good. Each now has a locked lock file beside it naming the kernel that made it; the next
    wasi_python process on that kernel removes them at its first call, and never touches a live
    call's folders or another kernel's. On a temporary folder without file locks, calls go on
    and such folders stay, as before.
  • trapped names a trap by its kind (a change to what it says): a stack overflow now reads
    "stack overflow", not wasmtime's heading "error while executing at wasm backtrace:", and so
    do "unreachable", "memory out of bounds" and every other trap with a code. An error with no
    trap code keeps its first line.
  • The released wheel is the bytes a second host rebuilt: the release workflow rebuilds the
    wheel on arm64 Linux and publishes only a wheel equal to it, byte for byte, whose artifacts
    match the set's SHA256SUMS.
  • Run from async code: run_async runs a call on a worker thread, so an async server's
    event loop never waits for a guest. Cancelling the awaiting task stops the guest (checked
    every 50 ms while it computes, at once when it waits or calls the door, later when other
    threads hold the GIL) and frees its worker before CancelledError is raised. Calls share one
    pool per process of DEFAULTS.async_workers threads (the CPU count, at most 8).
    run(cancel=event) stops a guest from any thread (trapped == "cancelled"). A process forked
    after it ran a guest now refuses to run guests, instead of aborting (macOS) or losing its
    deadlines; at exit, run_async's guests are stopped and its queued calls dropped.
  • sqlite3, bz2 and lzma are built in, from three new pins (SQLite 3.54.1, bzip2 1.0.8,
    liblzma from xz 5.8.4), with no upstream patch. bz2, lzma and tarfile's bz2 and xz
    modes write the same bytes as native; sqlite3 gives native's results, except the last digits
    of its math functions (trigonometric, hyperbolic, atan2, log10, one-argument log), which
    follow the guest's C library, and what changed since native's SQLite 3.53.1, such as JSON's
    float text; it has the native feature set (FTS3/4/5, R-tree, Geopoly, math functions,
    dbstat, percentile). shelve and dbm now create dbm.sqlite3 files, as native does. -V
    now says "no ctypes, mmap, sockets, ssl or zstd": compression.zstd is the compression module
    still missing.
  • SQLite's limits in the guest (the README's sqlite3 in the guest): one connection holds
    the database, through a <db>.lock directory beside it, while it writes or has a statement
    open, so a second connection gets "database is locked" after its busy timeout (5 s by
    default); journal_mode=wal stays delete unless locking_mode=exclusive; a guest stopped
    inside a transaction (killed, out of fuel, past its deadline, or by os._exit) leaves the
    lock and a hot journal in a reused root for the embedder to remove; SQLite's own
    random(), randomblob() and temporary names are seeded from the clock's second, so use
    secrets, uuid.uuid4() or os.urandom for IDs; a database in a read-only directory opens
    only through a URI that takes no lock, such as file:<path>?immutable=1; no
    load_extension; sqlite3.threadsafety is 0.
  • Sizes: the three libraries add about 2 MB to python.wasm (SQLite is most of it); the zip
    is unchanged. import bz2 and import lzma cost nothing more on the snapshot, which already
    holds them; import sqlite3 is not pre-imported.

Verify a download (gh logged in; see the README's Verifying a download):

gh attestation verify <asset> --repo ClayGendron/wasi_python \
  --signer-workflow ClayGendron/wasi_python/.github/workflows/release.yml@refs/tags/v0.1.0a3 \
  --source-ref refs/tags/v0.1.0a3 --deny-self-hosted-runners

wasi-python 0.1.0a2

wasi-python 0.1.0a2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Oct 13:13

The second alpha moves the line to CPython 3.15.0 and wasi-sdk 33, so a C++ exception inside a
package becomes a Python exception instead of ending the guest.

  • The line: CPython 3.15.0, built with wasi-sdk 33; numpy 2.5.2, pandas 3.0.6 and the
    pure-Python packages are unchanged. The line's versions are named once, in
    scripts/line.conf.
  • C++ exceptions work: numpy and pandas' C++ is compiled for WebAssembly exception handling
    in its standard form (exnref). A C++ error such as pocketfft's failed allocation now raises
    MemoryError in the program, which goes on.
  • Hosts must support exception handling. wasmtime 47 or later runs it by default (as
    wasmtime-py does); wasmer, wazero and Node need a switch; WAMR and wasmi cannot run it. The
    README's Hosts lists them. For a host without it, use 0.1.0a1.
  • The stdlib zip's guest name is now /usr/local/lib/python315.zip. An embedder that lays
    out the root by hand must follow it; wasi_python.run does.
  • The snapshot is made by the pinned wasmtime's own wasmtime wizer (the standalone Wizer
    pin is gone), and pre-imports _colorize, which 3.15 loads lazily, so the first traceback in
    a run does not pay for it.
  • Measured: a snapshot call costs about 6.6% more guest instructions than on 3.14 (81.9M
    against 76.8M: CPython 3.15's own start-up and finalization); the stdlib zip is 6% larger.
  • Safe by default (run changes for existing callers):
    • with no root, a call runs in a fresh private directory, removed afterwards;
    • a call has a 60 s deadline, a 1 GiB memory cap and a 16 MiB output cap per stream unless it
      passes others (None means no limit); a longer job now ends with trapped == "deadline";
    • the guest's environment starts from TMPDIR=/tmp, PYTHONTZPATH= and a HOME made
      fresh for each call (under /tmp, removed after it), with env= laid over it, so
      os.environ is no longer exactly what env= gave;
    • a call's store is closed when the call ends, trapped or not, so a trapped call no longer
      holds its guest memory until Python's garbage collector runs;
    • Completed gains truncated, last, defaulting to ().
    • run refuses a limit of the wrong type (TypeError) or out of range (ValueError) before
      any work, where 0.1.0a1 passed it on: a negative fuel or memory_bytes, or a
      memory_bytes of 263 or more, meant no limit; True meant 1; a deadline of 0 or less ran
      one tick. Integer limits take any integer type (numpy's included) from 0 to 2
      63 - 1; a
      deadline is a positive real number of seconds (not Decimal), at most about 4.6e16.
    • run refuses an argv, env or stdin the guest would see differently, before any work: a
      NUL in an argument, key or value, or text that is not valid UTF-8 (ValueError); an = in a
      key, or an empty key (ValueError); a str as argv, a non-str item or entry, or an env
      that is not a Mapping (TypeError). stdin must be bytes-like: "", 0, False and
      [], which meant empty input, are now a TypeError, as any other str already was. Messages
      never quote a value.
  • Planted code never runs: only the stdlib zip is on sys.path and site does not run
    (sys.flags.no_site == 1), so a module, .pth or pyvenv.cfg one call leaves in a root is
    never run by the next. The plain reactor ran them before.
  • The door is bounded: a frame past guest memory, a response over 2 GiB or a failing host is
    a host fault (−2); run no longer raises for them.
  • Fuel counts the guest only: fuel_used and the fuel cap start after instantiation, so
    they no longer include wasmtime's copy of the memory image (42.35M instructions for the
    snapshot, 5.05M for the plain reactor, charged on macOS when compiled in the process and on
    Windows always). Figures from 0.1.0a1 are that much higher.
  • A sleeping guest keeps its deadline: time.sleep and select no longer hold a call past
    its deadline; the runner answers the guest's waits itself, stopping at the deadline. One
    poll_oneoff takes at most 4,096 subscriptions (more answer ENOMEM: select.poll() on more
    than 2,047 descriptors), and a bad pointer answers EFAULT.
  • Concurrent calls share nothing: calls from many threads no longer corrupt wasmtime-py's
    callback tables (after a few hundred concurrent calls, every later call raised, and a guest
    could reach another call's host or output). The host functions are defined once per engine,
    and outputs use the runner's own table. An exception a host raises, and Ctrl-C, now come back
    to the call that caused them; Ctrl-C while a guest computed could abort the whole process. A
    trap or exit inside the guest's allocator during a door call is that call's result, and its
    fuel_used is None. Concurrent first calls compile the module once.
  • A guest cannot make run raise by exhausting host file descriptors: the call's store,
    which holds every descriptor the guest opened, is now closed before anything else is cleaned
    up. Before, run raised OSError (Errno 24) and left the stdin scratch file behind.
  • Release provenance: from this release on, every asset is attested by the release workflow
    and verified before it is published; the notes say how to verify a download.
  • The snapshot honours extra preopens by name and in any order, as the plain reactor does;
    before, it ignored them or swapped them.
  • The same bytes from any build host: arm64 macOS and arm64 Linux rebuild the same artifact
    set from the pins, byte for byte, and the release workflow rebuilds it on an arm64 Linux runner
    and compares SHA256SUMS before anything is signed. x86_64 Linux is pinned and builds, but its
    python.wasm is not yet byte-identical (one numpy function compiles differently there).
    Every host links wasi-sdk's
    host-neutral sysroot and builtins, and the build sorts in the C locale, so python.wasm's hash
    changes. Inside the guest, sysconfig and numpy.show_config() name the build machine
    build. MANIFEST's pin lines now list every supported build host's inputs.

Verify a download (gh logged in; see the README's Verifying a download):

gh attestation verify <asset> --repo ClayGendron/wasi_python \
  --signer-workflow ClayGendron/wasi_python/.github/workflows/release.yml@refs/tags/v0.1.0a2 \
  --source-ref refs/tags/v0.1.0a2 --deny-self-hosted-runners

wasi-python 0.1.0a1

wasi-python 0.1.0a1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Oct 00:43

The first alpha: CPython 3.14.8 for wasm32-wasip1 with numpy 2.5.2 and pandas 3.0.6 compiled
in, built from pinned sources with no upstream patches.

  • The artifact set: python.wasm (a WASI reactor), python-snapshot.wasm (the same binary
    with numpy and pandas pre-imported by Wizer), the bytecode-only stdlib zip (with
    python-dateutil, tzdata and six), MANIFEST, SHA256SUMS and the licence notices. Each
    rebuilds to the same bytes on one host.
  • The guest: two imports only, WASI preview1 and one host door (host::call); run follows
    CPython's command line (-c, -m, a script, stdin, -u, -V).
  • The runner: wasi_python.run(argv, root=...) runs one program in one call, with a host
    function for the door, fuel, a deadline, a memory cap and a private compiled-module cache
    (the run extra).
  • Known limits: numpy's default integer is 32-bit; no threads, sockets, ssl, sqlite3, bz2 or
    lzma; a C++ exception aborts the guest (wasi-sdk 24); compute is about 1.3 to 2.3 times slower
    than native. Built and judged on arm64 macOS; the release workflow also checks it on Linux
    and Windows.