Skip to main content

Landmines

The recurring foot-guns. Read once; the pattern-match wakes up when you see it again.

sys.exit(n) in -c module — fixed upstream in composectl 80f82012

Symptom (until recently): a smoke that ends with sys.exit(0 if passed == total else 1) would return the correct exit code but drop the last flush of stdout. Every "N/N PASS" summary was invisible; only failures showed up.

Root cause: composectl's wasmtime host tore the guest instance down before the WASI stdio streams flushed. Fixed in ~/git/webassembly-component-orchestration commit 80f82012 ("fix(hosts/wasmtime): preserve stdout/stderr across guest trap paths").

If you see mysteriously-empty smoke output, confirm pylon-launch is built against a composectl newer than that commit.

Lean vs fat builds — smoke gates

The feat/pylon-lean-arrow-endpoint branch commented numpy's C extensions out of Modules/Setup.local to shrink pylon.wasm for the Arrow-endpoint use case. Any smoke that transitively depends on numpy needs to guard against its absence.

The canonical gate is at the top of every smoke (see tests/wave11_shim_smoke.py:23-65):

import os
def _probe(mod: str) -> bool:
try:
__import__(mod); return True
except BaseException:
return False

_HAVE_NUMPY = _probe("numpy")
_REQUIRE_FAT = os.environ.get("PYLON_REQUIRE_FAT") == "1"
_NUMPY = ("numpy", _HAVE_NUMPY)

def check(label, thunk, requires=()):
missing = [r for r in requires if not r[1]]
if missing and not _REQUIRE_FAT:
# SKIP — counts toward passed on lean builds
return
...

Release-cut runs set PYLON_REQUIRE_FAT=1, which promotes SKIPs to FAILs. Lean CI runs leave it unset, so the same smoke stays green on both variants.

PyCapsule_SetPointer(cap, NULL) triggers ValueError on 3.14

PyCapsule_SetPointer silently rejects NULL on CPython 3.14, so the classic "mark this capsule closed" idiom (SetPointer(cap, NULL); SetDestructor(cap, NULL)) sets the ValueError instead of doing nothing. This bit us across sqlite, duckdb, and several other caps and was swept in commit 76b2f93e.

Use the SetDestructor(NULL) + SetName(..., ".closed") pattern instead. Reference at cpython-ext/_sqlite_capability/_sqlite_capability_module.c:302-303:

PyCapsule_SetDestructor(cap_obj, NULL);
PyCapsule_SetName(cap_obj, CONN_CAPSULE_NAME ".closed");

Extraction helpers do a name check that fails after the rename, so double-close becomes a clean error and the capsule GC does not double-drop.

Never expose duckdb_dynlink, crc32c_dynlink, or any other name containing _dynlink to user code. One user-facing name per cap. Linkage mode is an implementation detail. The private _<name>_dynlink_cap C ext stays available for probes; the user-facing import <name> must not care whether the underlying provider is compose-static or runtime-linked.

Retirement of this class of leak: commits 28d2c4f3 and fb396eb3.

duckdb_value_varchar returns NULL for UUID cells

DuckDB's C API's duckdb_value_varchar returns NULL for UUID column values (task #1799 open upstream). Workaround: cast to VARCHAR in SQL:

SELECT id::VARCHAR FROM t WHERE ... -- returns "550e8400-e29b-...", not NULL

See tests/duckdb_cap_smoke.py and commit c1b4bffd for the smoke that documents this.

Artifact rename: python.composed.wasmpylon.wasm

Landed in commit feee530e. Older scripts, blog posts, and comments still reference the old name; new code and docs should use pylon.wasm. The Makefile emits a symlink for the transition period.