Skip to content

wasi-python 0.1.0a2

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Oct 13:13
· 1252 commits to main since this release

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