Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions homebrew/kandelo-homebrew/Formula/cpython.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require_relative "../Kandelo/formula_support/kandelo_package"

class Cpython < Formula
include KandeloPackageFormula
SOURCE_URL = "https://www.python.org/ftp/python/3.13.3/Python-3.13.3.tar.xz"
SOURCE_SHA256 = "40f868bcbdeb8149a3149580bb9bfd407b3321cd48f0be631af955ac92c0e041"

desc "Python interpreter for Kandelo (with standard library)"
homepage "https://www.python.org/"
url SOURCE_URL
sha256 SOURCE_SHA256
license "Python-2.0"

depends_on "automattic/kandelo-homebrew/zlib"

skip_clean "bin"
skip_clean "lib/python3.13"

def install
out_dir = kandelo_build_package("cpython", "build-cpython.sh", SOURCE_URL, SOURCE_SHA256,
script_env: {
"PYTHON_VERSION" => version.to_s,
"WASM_POSIX_DEP_ZLIB_DIR" => Formula["automattic/kandelo-homebrew/zlib"].opt_prefix,
})
kandelo_install_bin(out_dir, "python.wasm", "python")

# Ship the standard library so the runtime can import re/json/etc.; the bare
# python.wasm has no filesystem stdlib. PYTHONHOME (below) points here.
stdlib_stage = buildpath/"python-stdlib-stage"
system "unzip", "-q", out_dir/"python-stdlib.zip", "-d", stdlib_stage
(prefix/"lib").install Dir["#{stdlib_stage}/lib/*"]
end

test do
env = {
"PYTHONHOME" => prefix.to_s,
"HOME" => testpath.to_s,
"PYTHONDONTWRITEBYTECODE" => "1",
"PYTHONNOUSERSITE" => "1",
}
assert_match "python-ok", kandelo_run_wasm(bin/"python", ["-c", "print('python-ok')"], env: env)

# Representative stdlib imports must work (re + json were the reported gap).
prog = <<~PY
import re, json, zlib, base64
assert json.loads(json.dumps({"a": [1, 2, 3], "b": True})) == {"a": [1, 2, 3], "b": True}
assert re.match(r"(\\d+)-(\\w+)", "42-foo").group(2) == "foo"
print("stdlib-ok")
PY
assert_match "stdlib-ok", kandelo_run_wasm(bin/"python", ["-c", prog], env: env)
end
end
24 changes: 24 additions & 0 deletions packages/registry/cpython/build-cpython.sh
Original file line number Diff line number Diff line change
Expand Up @@ -583,10 +583,32 @@ fi
echo "==> CPython built successfully!"
ls -la "$SCRIPT_DIR/bin/python.wasm"

# --- Package the Python standard library for the shippable runtime ---
# The wasm build produces only python.wasm; without the stdlib on sys.path the
# shipped runtime cannot import re/json/etc. Ship lib/python3.13 as
# python-stdlib.zip (a declared package output) that the Homebrew formula
# installs and points PYTHONHOME at. Exclude the test suite and GUI-only
# packages that cannot run on Kandelo wasm32-posix.
echo "==> Packaging CPython standard library..."
STDLIB_STAGE="$SCRIPT_DIR/python-stdlib-stage"
rm -rf "$STDLIB_STAGE"
mkdir -p "$STDLIB_STAGE/lib/python3.13"
cp -R "$SRC_DIR/Lib/." "$STDLIB_STAGE/lib/python3.13/"
rm -rf "$STDLIB_STAGE/lib/python3.13/test" \
"$STDLIB_STAGE/lib/python3.13/idlelib" \
"$STDLIB_STAGE/lib/python3.13/tkinter" \
"$STDLIB_STAGE/lib/python3.13/turtledemo"
find "$STDLIB_STAGE" -type d -name '__pycache__' -prune -exec rm -rf {} + 2>/dev/null || true
STDLIB_ZIP="$SCRIPT_DIR/bin/python-stdlib.zip"
rm -f "$STDLIB_ZIP"
( cd "$STDLIB_STAGE" && zip -q -r -X "$STDLIB_ZIP" lib )
echo "==> Packaged stdlib: $STDLIB_ZIP ($(du -h "$STDLIB_ZIP" | cut -f1))"

# Install into local-binaries/ so the resolver picks the freshly-built
# binary over the fetched release.
source "$REPO_ROOT/scripts/install-local-binary.sh"
install_local_binary cpython "$SCRIPT_DIR/bin/python.wasm"
install_local_binary cpython "$STDLIB_ZIP"

# Manifest declares `wasm = "python.wasm"` (not cpython.wasm), so the
# resolver's $WASM_POSIX_DEP_OUT_DIR scratch needs the file under that
Expand All @@ -595,4 +617,6 @@ install_local_binary cpython "$SCRIPT_DIR/bin/python.wasm"
if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then
cp "$SCRIPT_DIR/bin/python.wasm" "$WASM_POSIX_DEP_OUT_DIR/python.wasm"
echo " installed $WASM_POSIX_DEP_OUT_DIR/python.wasm (manifest output name)"
cp "$STDLIB_ZIP" "$WASM_POSIX_DEP_OUT_DIR/python-stdlib.zip"
echo " installed $WASM_POSIX_DEP_OUT_DIR/python-stdlib.zip (stdlib output)"
fi
2 changes: 1 addition & 1 deletion packages/registry/cpython/build.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
script_path = "packages/registry/cpython/build-cpython.sh"
repo_url = "https://github.com/brandonpayton/kandelo.git"
commit = "8c53383229fab78f97b098c3207a655159c03041"
revision = 1
revision = 2

[binary]
index_url = "https://github.com/Automattic/kandelo/releases/download/binaries-abi-v{abi}/index.toml"
67 changes: 67 additions & 0 deletions packages/registry/cpython/demo/import-smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* import-smoke.ts — kd-mt3u CPython stdlib import smoke on Kandelo.
*
* Runs the built python.wasm under the Node kernel host (host-fs passthrough so
* PYTHONHOME resolves) and checks representative stdlib imports (re, json, zlib,
* ...) plus a json round-trip and a re match.
*
* Usage:
* bash build.sh && bash packages/registry/cpython/build-cpython.sh
* npx tsx packages/registry/cpython/demo/import-smoke.ts [PYTHONHOME]
*
* PYTHONHOME defaults to the built cpython-install; pass an arg to override
* (e.g. a packaged stdlib bundle) to smoke the shippable layout.
*/
import { existsSync } from "fs";
import { resolve, dirname } from "path";
import { runCentralizedProgram } from "../../../../host/test/centralized-test-helper";
import { NodePlatformIO } from "../../../../host/src/platform/node";

const scriptDir = dirname(new URL(import.meta.url).pathname);
const repoRoot = resolve(scriptDir, "../../../..");

const PY = [
"import sys",
"mods=['re','json','zlib','os','functools','enum','collections','datetime','base64','hashlib','textwrap','argparse']",
"res=[]",
"for m in mods:",
" try:",
" __import__(m); res.append(m+'=ok')",
" except Exception as e:",
" res.append(m+'=FAIL('+type(e).__name__+')')",
"print('PYVER='+sys.version.split()[0])",
"print('MODS='+','.join(res))",
"import json as j",
"assert j.loads(j.dumps({'a':[1,2,3],'b':True,'c':None}))=={'a':[1,2,3],'b':True,'c':None}, 'json roundtrip'",
"import re as r",
"assert r.match(r'(\\d+)-(\\w+)','42-foo').group(2)=='foo', 're match'",
"print('IMPORT_SMOKE_PASS' if all('=ok' in x for x in res) else 'IMPORT_SMOKE_PARTIAL')",
].join("\n");

async function main() {
const pythonWasm = resolve(repoRoot, "packages/registry/cpython/bin/python.wasm");
const pythonHome = process.argv[2] ||
resolve(repoRoot, "packages/registry/cpython/cpython-install");
if (!existsSync(pythonWasm)) {
console.error("python.wasm not found. Run: bash packages/registry/cpython/build-cpython.sh");
process.exit(1);
}

const result = await runCentralizedProgram({
programPath: pythonWasm,
argv: ["python", "-c", PY],
env: [`PYTHONHOME=${pythonHome}`, `HOME=/tmp`, `TMPDIR=/tmp`, `PYTHONDONTWRITEBYTECODE=1`],
io: new NodePlatformIO(),
timeout: 300_000,
});

process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
const ok = result.exitCode === 0 && result.stdout.includes("IMPORT_SMOKE_PASS");
process.exit(ok ? 0 : (result.exitCode || 1));
}

main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});
7 changes: 7 additions & 0 deletions packages/registry/cpython/package.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ script_path = "packages/registry/cpython/build-cpython.sh"
[[outputs]]
name = "cpython"
wasm = "python.wasm"

# Python standard library (lib/python3.13/*.py), staged so the shipped runtime
# can import re/json/etc. The Homebrew formula installs this and points
# PYTHONHOME at it. Without it the bare python.wasm has no filesystem stdlib.
[[outputs]]
name = "cpython-stdlib"
wasm = "python-stdlib.zip"
Loading