Skip to content
Merged
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
4 changes: 2 additions & 2 deletions tools/check_guide.py → .github/scripts/check_guide.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Checks the bilingual GitHub Pages guide in docs/guide/.

python3 tools/check_guide.py [guide-dir]
python3 .github/scripts/check_guide.py [guide-dir]

Fails (exit 1) when:
1. an HTML file does not parse into balanced elements;
Expand Down Expand Up @@ -153,7 +153,7 @@ def check(guide):

def main():
here = os.path.dirname(os.path.abspath(__file__))
guide = sys.argv[1] if len(sys.argv) > 1 else os.path.join(here, "..", "docs", "guide")
guide = sys.argv[1] if len(sys.argv) > 1 else os.path.join(here, "..", "..", "docs", "guide")
failures = check(guide)
for f in failures:
print("FAIL", f)
Expand Down
10 changes: 5 additions & 5 deletions tools/release/README.md → .github/scripts/release/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ between runs, and any existing `release` (e.g. a stale one from an older workflo
ignored and overwritten.

1. Every push to `develop` triggers `.github/workflows/release-sync.yml`, which runs
`tools/release/sync-release.sh --push`. It rebuilds `release` from scratch as exactly
`.github/scripts/release/sync-release.sh --push`. It rebuilds `release` from scratch as exactly
one commit (`Release: sync from develop <sha>`) whose parent is that develop commit and
whose tree is the develop tree minus the files below. This happens even when nothing
is dropped. It is a no-op only if `release` already has that tree and that parent.
Expand All @@ -31,10 +31,10 @@ Only Markdown is ever dropped.

## Local use

tools/release/sync-release.sh --dry-run # list paths that would be dropped
tools/release/sync-release.sh # regenerate local release branch
tools/release/sync-release.sh --push # also push to origin
tools/release/test-sync-release.sh # tests (throwaway repo in .tmp/)
.github/scripts/release/sync-release.sh --dry-run # list paths that would be dropped
.github/scripts/release/sync-release.sh # regenerate local release branch
.github/scripts/release/sync-release.sh --push # also push to origin
.github/scripts/release/test-sync-release.sh # tests (throwaway repo in .tmp/)

The script uses plumbing and a private index, so your working tree, index and HEAD are untouched.
It refuses to run while `release` is checked out.
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Tests for sync-release.sh. Builds a throwaway repo under <repo>/.tmp/.
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
ROOT="$(cd "$HERE/../../.." && pwd)"
SYNC="$HERE/sync-release.sh"
SCRATCH="$ROOT/.tmp/release-sync-test.$$"
FAILS=0; PASSES=0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#!/usr/bin/env bash
# Type-checks tools/stubs/consumer.py against a directory of generated Kotlin-name stubs (issue #31).
# Type-checks .github/scripts/stubs/consumer.py against a directory of generated Kotlin-name stubs (issue #31).
#
# tools/stubs/check-stubs.sh <stub-dir> [venv-dir]
# .github/scripts/stubs/check-stubs.sh <stub-dir> [venv-dir]
#
# <stub-dir> is what `generatePythonStubs` wrote, e.g.
# ksp-fixtures/compose/build/generated/pythonStubs/desktopMain
# The venv defaults to <repo>/.tmp/stubs-venv (git-ignored) and is created on first use.
set -euo pipefail

root="$(cd "$(dirname "$0")/../.." && pwd)"
root="$(cd "$(dirname "$0")/../../.." && pwd)"
stubs="$(cd "${1:?usage: check-stubs.sh <stub-dir> [venv-dir]}" && pwd)"
venv="${2:-$root/.tmp/stubs-venv}"

Expand All @@ -17,10 +17,10 @@ if [ ! -x "$venv/bin/mypy" ]; then
"$venv/bin/pip" install --quiet mypy
fi

# tools/stubs/mypy.ini: `strict`, and `warn_unused_ignores` for the consumer so that the lines it
# .github/scripts/stubs/mypy.ini: `strict`, and `warn_unused_ignores` for the consumer so that the lines it
# expects to be rejected fail the check when they stop being. Errors inside the stubs themselves are
# reported too: MYPYPATH modules are checked, not silenced.
MYPYPATH="$stubs" "$venv/bin/mypy" \
--config-file "$root/tools/stubs/mypy.ini" \
--config-file "$root/.github/scripts/stubs/mypy.ini" \
--cache-dir "$root/.tmp/mypy-cache" \
"$root/tools/stubs/consumer.py"
"$root/.github/scripts/stubs/consumer.py"
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions .github/workflows/release-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Not used as a base (release is regenerated); the script reads the
# remote sha itself for --force-with-lease.
bash tools/release/sync-release.sh --source "${GITHUB_SHA}" --push
bash .github/scripts/release/sync-release.sh --source "${GITHUB_SHA}" --push

- name: Open or update PR release -> main
env:
Expand All @@ -43,7 +43,7 @@ jobs:
echo "release already equals main; nothing to PR"; exit 0
fi
title="Release: merge release into main"
body="Automated sync of \`develop\` (${GITHUB_SHA::7}) into \`release\`. Markdown outside README.md and docs/ subfolders is removed by tools/release/sync-release.sh."
body="Automated sync of \`develop\` (${GITHUB_SHA::7}) into \`release\`. Markdown outside README.md and docs/ subfolders is removed by .github/scripts/release/sync-release.sh."
existing="$(gh pr list --head release --base main --state open --json number --jq '.[0].number // empty')"
if [ -n "$existing" ]; then
gh pr edit "$existing" --title "$title" --body "$body"
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/stubs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: Kotlin-name stubs
# build resolves (`compose-plugin` in gradle/libs.versions.toml) and publishes the result.
#
# - every push to develop and every pull request: the stubs are generated, type-checked against
# tools/stubs/consumer.py with mypy (issue #31), and uploaded as the workflow artifact `kotlin-stubs`;
# .github/scripts/stubs/consumer.py with mypy (issue #31), and uploaded as the workflow artifact `kotlin-stubs`;
# - every `v*` tag: the same stubs are also attached to the tag's GitHub release as `kotlin-stubs.zip`.

on:
Expand All @@ -17,7 +17,7 @@ on:
paths:
- "python-multiplatform-gradle-plugin/**"
- "ksp-fixtures/compose/**"
- "tools/stubs/**"
- ".github/scripts/stubs/**"
- "gradle/libs.versions.toml"
- ".github/workflows/stubs.yml"
workflow_dispatch:
Expand Down Expand Up @@ -55,7 +55,7 @@ jobs:
run: ./gradlew :ksp-fixtures:compose:generatePythonStubs --console=plain

- name: Type-check a consumer against them (mypy)
run: tools/stubs/check-stubs.sh ksp-fixtures/compose/build/generated/pythonStubs/desktopMain
run: .github/scripts/stubs/check-stubs.sh ksp-fixtures/compose/build/generated/pythonStubs/desktopMain

- name: Assemble the artifact
run: |
Expand Down Expand Up @@ -86,7 +86,7 @@ jobs:

Use: put this directory on the type checker's path (\`MYPYPATH=<this directory>\`, or the
IDE's "stub path" / interpreter paths). It is checked with mypy against
\`tools/stubs/consumer.py\` in the commit above.
\`.github/scripts/stubs/consumer.py\` in the commit above.
README
(cd build && zip -qr kotlin-stubs.zip kotlin-stubs)

Expand Down Expand Up @@ -146,4 +146,4 @@ jobs:
PY

- name: Type-check a consumer against them (mypy)
run: tools/stubs/check-stubs.sh ksp-fixtures/compose/build/generated/pythonStubs/desktopMain
run: .github/scripts/stubs/check-stubs.sh ksp-fixtures/compose/build/generated/pythonStubs/desktopMain
2 changes: 1 addition & 1 deletion .github/workflows/wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ jobs:
# `-PrequireWasmRuntime` a missing wheel is a failure, not a skip, so fetch them first: by URL,
# each accepted only against the sha256 the script pins.
- name: Fetch the pinned wasm wheels
run: bash tools/wasm/build-cpython.sh wheels
run: bash python-multiplatform/scripts/wasm/build-cpython.sh wheels

# `-PrequireWasmRuntime` is the second half of the guard, and it is what makes this job unable
# to repeat the original defect: if either task would skip -- no interpreter, or no
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ google-services.json
# subagent worktrees live inside the repo
.worktrees/

# repo-local caches: emsdk, the CPython wasm build and its staged runtime (tools/wasm/build-cpython.sh)
# repo-local caches: emsdk, the CPython wasm build and its staged runtime (python-multiplatform/scripts/wasm/build-cpython.sh)
.caches/

# scratch space for agents and scripts; delete when done
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ files, lives **inside this repository's root directory.**
| Worktrees | `.worktrees/<name>` (git-ignored) |
| Temporary files | `.tmp/` (git-ignored); delete when done |
| Benchmarks | `benchmarks/` |
| Developer tooling | `tools/` |
| Developer tooling | `.github/scripts/` (CI-only), or beside the module it serves |

Before writing a file, check that its absolute path starts with this repository's root. If it does
not, stop. The only exceptions are a path the user names explicitly, and caches that build tools
Expand Down Expand Up @@ -70,7 +70,7 @@ landed or reported, not left. Branches named `release-*` are preserved snapshots

`main` carries a reduced layout: of the Markdown files, only `README.md` stays at the repository
root, and `docs/` keeps only its subdirectories (no Markdown files directly under `docs/`).
CI runs `tools/release/sync-release.sh` (`.github/workflows/release-sync.yml`) to produce that layout; do not hand-edit `release` or `main`.
CI runs `.github/scripts/release/sync-release.sh` (`.github/workflows/release-sync.yml`) to produce that layout; do not hand-edit `release` or `main`.

### Issues and pull requests

Expand Down
6 changes: 3 additions & 3 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ fail, then implement.
`libpython*.dylib`, or the top-level packages `test`, `idlelib`, `tkinter`, `turtledemo` and
`ensurepip`; `stageIosPythonHomeForXcode` picks the slice from Xcode's `EFFECTIVE_PLATFORM_NAME`
and `ARCHS` and prints `PYTHON_HOME_DIR=` and `PYTHON_DYLIB_INFO_TEMPLATE=`. An Xcode Run Script
phase (`tools/xcode/install-python.sh`, after Copy Bundle Resources and before Embed Frameworks)
phase (`python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh`, after Copy Bundle Resources and before Embed Frameworks)
copies that prefix into `<app>/python-multiplatform-home/` and the consumer's payload (L-6) into
`<app>/python/`, and moves every `.so` under either into `Frameworks/<dotted.name>.framework`,
leaving a `.fwork` placeholder, as CPython's `AppleFrameworkLoader` and Apple's rule against loose
Expand Down Expand Up @@ -427,7 +427,7 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
module is stubbed in its parent package as one attribute whose type has `__call__` (the function or
its `@overload`s) and the module's constants and functions as members, never as a bare `def`, which
would hide `TextRange.Zero` from a checker),
`tools/stubs/check-stubs.sh` (mypy over the Compose stubs). A class whose name is also a function in
`.github/scripts/stubs/check-stubs.sh` (mypy over the Compose stubs). A class whose name is also a function in
its module is `Any`, except that a class's own bound constructor is that class's `__init__`
(`@overload`ed for several), so the class keeps its stub, members and properties; a single one's
docstring carries its table key after `Kotlin constructor: `; its `__`-suffixed table-key spellings
Expand All @@ -444,7 +444,7 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
(`Arrangement.HorizontalOrVertical(Horizontal, Vertical)`, so a `Vertical` slot accepts it): `Any`
and `java.lang.*` are dropped, and a base already implied by another listed base's known ancestry
is dropped, so the order is a valid Python MRO (#71, `GP/stubs/SupertypeStubTest.kt`, and the
`Column`/`Row` `SpaceBetween` lines of `tools/stubs/consumer.py`).
`Column`/`Row` `SpaceBetween` lines of `.github/scripts/stubs/consumer.py`).
- **B-8** CI generates the stubs over the Compose version the build resolves and publishes them
(`.github/workflows/stubs.yml`): workflow artifact `kotlin-stubs` on every push to `develop`, with a
README naming the Compose version and the commit, and `kotlin-stubs.zip` on every `v*` tag's release.
Expand Down
2 changes: 1 addition & 1 deletion docs/design/ecosystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ This section documents the current state, language/build system/distribution art
- `PythonMultiplatform` has no dependencies on other repos in the ecosystem.
- `pythonx-compose` and consumer applications depend on `PythonMultiplatform` runtime and its Gradle/KSP bindings plugin.
- **Unverified**:
- Behavior of WASM browser runtime under production web bundlers. (*2026-10-03:* the former `wasm-experiment/` is folded into the library, `wasmJsMain`/`wasmJsTest`, CPython built by `tools/wasm/build-cpython.sh`, commit `6e54f617`; the bundler question is unchanged.)
- Behavior of WASM browser runtime under production web bundlers. (*2026-10-03:* the former `wasm-experiment/` is folded into the library, `wasmJsMain`/`wasmJsTest`, CPython built by `python-multiplatform/scripts/wasm/build-cpython.sh`, commit `6e54f617`; the bundler question is unchanged.)

**Version check (2026-08-17): "3.14 runtime vs. 3.13 header, never checked", could not find the
sentence, checked the claim anyway.** The literal sentence was searched for across every `.md` file
Expand Down
2 changes: 1 addition & 1 deletion docs/design/pyi-generation-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,7 @@ merges an overlap.

## 8. Checked with mypy, and published

- `tools/stubs/check-stubs.sh <stub-dir>` runs `mypy --strict` over `tools/stubs/consumer.py` (a
- `.github/scripts/stubs/check-stubs.sh <stub-dir>` runs `mypy --strict` over `.github/scripts/stubs/consumer.py` (a
`Modifier` chain from the class and from an instance, `Checkbox`, `Text`, `Column` with its keyword
`content`, and four lines that must be rejected) against stubs generated for the real Compose jars
(`:ksp-fixtures:compose:generatePythonStubs`, which `ksp-fixtures/compose` now enables). Errors inside
Expand Down
4 changes: 2 additions & 2 deletions docs/platforms/ios-app-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ PYTHON_DYLIB_INFO_TEMPLATE=<abs>/python-multiplatform/build/python-ios-home/ipho
A universal `ARCHS` (`arm64 x86_64`) is refused with the fix (`ONLY_ACTIVE_ARCH=YES` or `ARCHS=arm64`):
one prefix holds one `lib-dynload`. Upstream's `utils.sh` has the same limit without saying so.

**Xcode** (`tools/xcode/install-python.sh`, a Run Script phase after Copy Bundle Resources and before
**Xcode** (`python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh`, a Run Script phase after Copy Bundle Resources and before
Embed Frameworks; `ENABLE_USER_SCRIPT_SANDBOXING = NO`).

1. Runs Gradle as above. The output is captured, then parsed, then checked to be a directory, a
Expand Down Expand Up @@ -97,7 +97,7 @@ The sample's project is `sample/src/iosMain/app.xcodeproj` (target `app`, produc
set -e
cd "$SRCROOT/../../.."
export PYTHON_PAYLOAD_DIR="$SRCROOT/../commonMain/python"
/bin/bash tools/xcode/install-python.sh
/bin/bash python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh
```

It replaced two phases: "Install Target Specific Python Standard Library", which rsynced the slice's
Expand Down
2 changes: 1 addition & 1 deletion docs/platforms/python-version-acquisition.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ The project currently uses `python-build-standalone` (maintained by Astral). For
## 6. WebAssembly

No distributor ships a `python.wasm` that exports `wasmExports` and `wasmMemory` to JavaScript, which the
`wasmJs` target needs. CPython is therefore **built**, not downloaded: `tools/wasm/build-cpython.sh` builds
`wasmJs` target needs. CPython is therefore **built**, not downloaded: `python-multiplatform/scripts/wasm/build-cpython.sh` builds
CPython 3.14.2 for `wasm32-emscripten` (Emscripten 5.0.3, matched to the `pyemscripten_2026_0` platform of
PEP 783) under the git-ignored `.caches/`, and `python-multiplatform-wasm-runtime` is the published zip of
the result. This is deliberately a different patch release from the native `pythonVersion`; see
Expand Down
8 changes: 4 additions & 4 deletions docs/platforms/wasm-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ has a browser wasm target. `wasmWasi` is not a target (when last assessed it was
|---|---|---|
| How Kotlin reaches CPython | Raw Stable ABI symbols as `@WasmImport`s against the Emscripten module's own wasm exports, **no JS in the call path** | Preserves the exact object model (`NativePointer`, `PyObject`) of every other target; a high-level Pyodide-style JS API (`pyodide.runPython`, `PyProxy`) would replace the whole object model with `JsAny` wrappers and covers almost none of the ~330 C functions |
| Memory | **One shared linear memory.** Kotlin imports Emscripten's `Module.wasmMemory` as its `intrinsics.memory` | Needs Kotlin ≥ 2.4.20-Beta2, whose `wasmJs` modules **import** `intrinsics.memory` (`min=0, max=none`). Releases up to 2.4.10 exported an unbounded memory, which Emscripten cannot import, the earlier plan to patch the `.wasm` memory section (and a YouTrack issue) is obsolete ([`../archive/wasm-youtrack-issue.md`](../archive/wasm-youtrack-issue.md)) |
| Where CPython comes from | **Our own Emscripten build**, `tools/wasm/build-cpython.sh`: CPython **3.14.2**, Emscripten **5.0.3**, matched to the `pyemscripten_2026_0` platform of PEP 783 | No distributor ships a `python.wasm` exporting `wasmExports,wasmMemory`. The version is deliberately not the 3.14.x pinned for native targets (`pythonVersion` in `gradle.properties`); see `docs/design/ecosystem.md` |
| Where CPython comes from | **Our own Emscripten build**, `python-multiplatform/scripts/wasm/build-cpython.sh`: CPython **3.14.2**, Emscripten **5.0.3**, matched to the `pyemscripten_2026_0` platform of PEP 783 | No distributor ships a `python.wasm` exporting `wasmExports,wasmMemory`. The version is deliberately not the 3.14.x pinned for native targets (`pythonVersion` in `gradle.properties`); see `docs/design/ecosystem.md` |
| C shim / composition | **None** | The hops that composition amortised are gone; measured gain ≤ 2.9 ns per operation (below). It also removes a second build pipeline |
| `EXPORTED_FUNCTIONS` list | **Not needed** | `-sMAIN_MODULE` is `LINKABLE`, so the whole Stable ABI is exported (all 310 C symbols `EmbedAPI.kt` declared at the time were found in the build's 8287 exports). The build adds `wasmExports,wasmMemory` to `-sEXPORTED_RUNTIME_METHODS`, which is not ABI-sensitive |
| Upcalls (Python → Kotlin) | `@WasmExport` trampolines placed in CPython's `__indirect_function_table` with `WebAssembly.Table.set`; the table index is the C function pointer. Routing is by data (a handle in `self`/`closure`), through the generated table, as on every platform | A funcref is a funcref: `call_indirect` reaches the Kotlin export directly. Measured 3.1 ns vs 10.9 ns for the `addFunction` + JS-closure route |
Expand Down Expand Up @@ -86,7 +86,7 @@ Corrections that came out of measuring (they changed the design):

## The interpreter build and the platform tag

`tools/wasm/build-cpython.sh` builds CPython 3.14.2 with CPython's own `Tools/wasm/emscripten` driver
`python-multiplatform/scripts/wasm/build-cpython.sh` builds CPython 3.14.2 with CPython's own `Tools/wasm/emscripten` driver
plus the changes that make it claim `pyemscripten_2026_0` (PEP 783):

| | stock Tier 3 build | this build |
Expand All @@ -100,8 +100,8 @@ Evidence: a real PyPI wheel, `pydantic_core-2.48.0-cp314-cp314-pyemscripten_2026
and runs on this build (`validate_python(42) -> 42`, a `ValidationError` raised and caught, the
exception exercises the unwinding ABI), and fails with `tag import requires a WebAssembly.Tag` on the
stock build. This is one wheel and one code path; it does not show that every compiled extension
loads. `WasmCompiledWheelTest` runs it (`tools/wasm/build-cpython.sh wheels` fetches the pinned wheels);
`WasmInterpreterAbiTest` checks the claims, with `tools/wasm/build-cpython.sh stock` as the negative
loads. `WasmCompiledWheelTest` runs it (`python-multiplatform/scripts/wasm/build-cpython.sh wheels` fetches the pinned wheels);
`WasmInterpreterAbiTest` checks the claims, with `python-multiplatform/scripts/wasm/build-cpython.sh stock` as the negative
control. Emscripten is pinned to 5.0.3 because that is what the platform specifies, and installing it
replaces `~/emsdk/upstream` in place (a global setting).

Expand Down
Loading
Loading