diff --git a/tools/check_guide.py b/.github/scripts/check_guide.py similarity index 98% rename from tools/check_guide.py rename to .github/scripts/check_guide.py index b74217e38..69c2eff74 100644 --- a/tools/check_guide.py +++ b/.github/scripts/check_guide.py @@ -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; @@ -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) diff --git a/tools/release/README.md b/.github/scripts/release/README.md similarity index 81% rename from tools/release/README.md rename to .github/scripts/release/README.md index a4f6302d9..cfb4de357 100644 --- a/tools/release/README.md +++ b/.github/scripts/release/README.md @@ -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 `) 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. @@ -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. diff --git a/tools/release/sync-release.sh b/.github/scripts/release/sync-release.sh similarity index 100% rename from tools/release/sync-release.sh rename to .github/scripts/release/sync-release.sh diff --git a/tools/release/test-sync-release.sh b/.github/scripts/release/test-sync-release.sh similarity index 99% rename from tools/release/test-sync-release.sh rename to .github/scripts/release/test-sync-release.sh index cba1d3139..2ee99e3f9 100755 --- a/tools/release/test-sync-release.sh +++ b/.github/scripts/release/test-sync-release.sh @@ -2,7 +2,7 @@ # Tests for sync-release.sh. Builds a throwaway repo under /.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 diff --git a/tools/stubs/check-stubs.sh b/.github/scripts/stubs/check-stubs.sh similarity index 61% rename from tools/stubs/check-stubs.sh rename to .github/scripts/stubs/check-stubs.sh index b8c1c41d3..01cfe30a9 100755 --- a/tools/stubs/check-stubs.sh +++ b/.github/scripts/stubs/check-stubs.sh @@ -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 [venv-dir] +# .github/scripts/stubs/check-stubs.sh [venv-dir] # # is what `generatePythonStubs` wrote, e.g. # ksp-fixtures/compose/build/generated/pythonStubs/desktopMain # The venv defaults to /.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 [venv-dir]}" && pwd)" venv="${2:-$root/.tmp/stubs-venv}" @@ -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" diff --git a/tools/stubs/consumer.py b/.github/scripts/stubs/consumer.py similarity index 100% rename from tools/stubs/consumer.py rename to .github/scripts/stubs/consumer.py diff --git a/tools/stubs/mypy.ini b/.github/scripts/stubs/mypy.ini similarity index 100% rename from tools/stubs/mypy.ini rename to .github/scripts/stubs/mypy.ini diff --git a/.github/workflows/release-sync.yml b/.github/workflows/release-sync.yml index 746a0b9de..8914610b3 100644 --- a/.github/workflows/release-sync.yml +++ b/.github/workflows/release-sync.yml @@ -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: @@ -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" diff --git a/.github/workflows/stubs.yml b/.github/workflows/stubs.yml index b6a7a785d..f7c28305f 100644 --- a/.github/workflows/stubs.yml +++ b/.github/workflows/stubs.yml @@ -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: @@ -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: @@ -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: | @@ -86,7 +86,7 @@ jobs: Use: put this directory on the type checker's path (\`MYPYPATH=\`, 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) @@ -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 diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index a800e5dcd..8cebea898 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -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 diff --git a/.gitignore b/.gitignore index 670457d97..293cde73a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 0b0cd50de..d0b53d728 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ files, lives **inside this repository's root directory.** | Worktrees | `.worktrees/` (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 @@ -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 diff --git a/docs/SPEC.md b/docs/SPEC.md index aac6e82dd..b21edbb33 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -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 `/python-multiplatform-home/` and the consumer's payload (L-6) into `/python/`, and moves every `.so` under either into `Frameworks/.framework`, leaving a `.fwork` placeholder, as CPython's `AppleFrameworkLoader` and Apple's rule against loose @@ -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 @@ -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. diff --git a/docs/design/ecosystem.md b/docs/design/ecosystem.md index ba8caf2c2..acbf57f8a 100644 --- a/docs/design/ecosystem.md +++ b/docs/design/ecosystem.md @@ -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 diff --git a/docs/design/pyi-generation-design.md b/docs/design/pyi-generation-design.md index 0819abec4..13fd7405c 100644 --- a/docs/design/pyi-generation-design.md +++ b/docs/design/pyi-generation-design.md @@ -627,7 +627,7 @@ merges an overlap. ## 8. Checked with mypy, and published -- `tools/stubs/check-stubs.sh ` runs `mypy --strict` over `tools/stubs/consumer.py` (a +- `.github/scripts/stubs/check-stubs.sh ` 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 diff --git a/docs/platforms/ios-app-bundle.md b/docs/platforms/ios-app-bundle.md index 9a3c3fb37..1ddcaf218 100644 --- a/docs/platforms/ios-app-bundle.md +++ b/docs/platforms/ios-app-bundle.md @@ -33,7 +33,7 @@ PYTHON_DYLIB_INFO_TEMPLATE=/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 @@ -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 diff --git a/docs/platforms/python-version-acquisition.md b/docs/platforms/python-version-acquisition.md index ac7ab98b9..544c13d3f 100644 --- a/docs/platforms/python-version-acquisition.md +++ b/docs/platforms/python-version-acquisition.md @@ -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 diff --git a/docs/platforms/wasm-design.md b/docs/platforms/wasm-design.md index b9a9c29de..d0d385609 100644 --- a/docs/platforms/wasm-design.md +++ b/docs/platforms/wasm-design.md @@ -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 | @@ -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 | @@ -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). diff --git a/docs/roadmap/ROADMAP.md b/docs/roadmap/ROADMAP.md index 9b903ab0d..3a6261a14 100644 --- a/docs/roadmap/ROADMAP.md +++ b/docs/roadmap/ROADMAP.md @@ -187,7 +187,7 @@ cycles, interning and `Py_ssize_t` are closed (`PM/wasmJsTest/`: `WasmFinalizati `WasmCycleCollectionTest`, `WasmPySsizeTBoundaryTest`, `WasmBrowserRuntimeTest`, `WasmSelectorsImportTest`); the plugin stages the runtime for consumers (`GP/main/kotlin/python/multiplatform/gradle/WasmBrowserRuntimeStaging.kt`, -`GenerateWasmProxyExportsTask.kt`). The interpreter is built by `tools/wasm/build-cpython.sh` +`GenerateWasmProxyExportsTask.kt`). The interpreter is built by `python-multiplatform/scripts/wasm/build-cpython.sh` (`docs/platforms/wasm-design.md`). Old §10c/§10d (browser test, CI split) are closed; their open remainder is 10.1. @@ -212,7 +212,7 @@ remainder is 10.1. staged by the plugin; it was never run in a browser, and no production bundler other than the Kotlin Gradle webpack setup was tried (also `docs/design/ecosystem.md` §5 "Unverified"). - **10.5 The wasm CPython build lacks lzma, zstd and OpenSSL, open.** Listed as "NOT DONE" in - `tools/wasm/build-cpython.sh`'s header; ABI-sensitive per the Pyodide flag list, though they did not + `python-multiplatform/scripts/wasm/build-cpython.sh`'s header; ABI-sensitive per the Pyodide flag list, though they did not gate the one compiled wheel tested (`WasmCompiledWheelTest`). - **10.6 The sample's wasm upcall line (`presses x3 = 0`) was never cross-checked against desktop, unverified, minor.** @@ -264,7 +264,7 @@ trampolines exist. GraalVM native image upcalls are verified by hand - **13.1 The iOS app bundle carries no Python standard library, implemented, simulator proof open.** SPEC L-11, issue #59, `docs/platforms/ios-app-bundle.md`: per-slice staging - (`stageIosPythonHome_*`, `stageIosPythonHomeForXcode`), `tools/xcode/install-python.sh` (stdlib to + (`stageIosPythonHome_*`, `stageIosPythonHomeForXcode`), `python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh` (stdlib to `/python-multiplatform-home/`, payload to `/python/`, `.so` wrapped as frameworks with `.fwork` placeholders) replacing the two broken phases of the old root `iosApp/` (now `sample/src/iosMain/app.xcodeproj`, #107), and `IosPythonHome` + `Py_SetPythonHome` at run time. Left: the installed-app run on the simulator with no diff --git a/ksp-fixtures/compose/build.gradle.kts b/ksp-fixtures/compose/build.gradle.kts index c60b284a4..c7e52cd9f 100644 --- a/ksp-fixtures/compose/build.gradle.kts +++ b/ksp-fixtures/compose/build.gradle.kts @@ -117,7 +117,7 @@ pythonBindings { "python.multiplatform.compose.RememberSaveableKt", ), ) - // Stubs on: `generatePythonStubs` over the real Compose jars is what `tools/stubs/check-stubs.sh` (the mypy + // Stubs on: `generatePythonStubs` over the real Compose jars is what `.github/scripts/stubs/check-stubs.sh` (the mypy // check) and `.github/workflows/stubs.yml` (the published artifact) both run. Nothing depends on the // task, so the render tests do not pay for it. generateStubs.set(true) diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/IosPythonHome.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/IosPythonHome.kt index dce8d74a4..52c7b112c 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/IosPythonHome.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/IosPythonHome.kt @@ -33,7 +33,7 @@ import java.nio.file.StandardCopyOption * * `build/utils.sh`'s `install_stdlib` merges the shared tree with one `lib-$ARCHS` tree into the app; * this does the same merge at Gradle time, into a directory per slice, so the Xcode phase - * (`tools/xcode/install-python.sh`) copies a finished prefix and needs no path into the archive. + * (`python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh`) copies a finished prefix and needs no path into the archive. * Pure functions are separated out so the layout is pinned by tests without downloading anything. */ object IosPythonHomeLayout { @@ -100,7 +100,7 @@ object IosPythonHomeLayout { val platform = effectivePlatformName?.trim()?.removePrefix("-") require(!platform.isNullOrEmpty()) { "EFFECTIVE_PLATFORM_NAME is not set. This task picks the iOS slice from the settings Xcode " + - "exports to a Run Script phase; run it from one (tools/xcode/install-python.sh does)." + "exports to a Run Script phase; run it from one (python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh does)." } val archList = archs?.trim()?.split(Regex("\\s+"))?.filter { it.isNotEmpty() }.orEmpty() require(archList.isNotEmpty()) { "ARCHS is not set; run this from an Xcode Run Script phase." } diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/WasmBrowserRuntimeStaging.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/WasmBrowserRuntimeStaging.kt index b4d83da2f..8ba46eca6 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/WasmBrowserRuntimeStaging.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/WasmBrowserRuntimeStaging.kt @@ -25,7 +25,7 @@ import javax.inject.Inject * exists only inside this repository's checkout. An external consumer has no such directory and no * way to get one -- the CPython build it names is produced by hand-patching CPython's own source * (`Lib/sysconfig`, the generated `Makefile`) against a specific Emscripten SDK version, which is - * not something any upstream publishes yet. See `tools/wasm/build-cpython.sh`'s own header for why: PEP + * not something any upstream publishes yet. See `python-multiplatform/scripts/wasm/build-cpython.sh`'s own header for why: PEP * 783's `pyemscripten_2026_0` tag is real and accepted, but nothing ships a `python.wasm` whose * `EXPORTED_RUNTIME_METHODS` include `wasmExports`/`wasmMemory` -- that addition exists only so this * library's `@WasmImport` declarations have something to bind to, and no distributor has a reason to diff --git a/tools/xcode/install-python.sh b/python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh similarity index 100% rename from tools/xcode/install-python.sh rename to python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh diff --git a/python-multiplatform/build.gradle.kts b/python-multiplatform/build.gradle.kts index 62db64db1..119ff6769 100644 --- a/python-multiplatform/build.gradle.kts +++ b/python-multiplatform/build.gradle.kts @@ -1425,7 +1425,7 @@ tasks.withType().co logger.lifecycle( "No wheels in $wasmWheelsDir -- WasmCompiledWheelTest will " + (if (requireWasmRuntime) "FAIL (-PrequireWasmRuntime)" else "skip") + - ". Fetch them with tools/wasm/build-cpython.sh wheels (or :python-multiplatform:fetchWasmWheels)." + ". Fetch them with python-multiplatform/scripts/wasm/build-cpython.sh wheels (or :python-multiplatform:fetchWasmWheels)." ) } dir.resolve("cpython-config.mjs").appendText( diff --git a/tools/wasm/build-cpython.sh b/python-multiplatform/scripts/wasm/build-cpython.sh similarity index 95% rename from tools/wasm/build-cpython.sh rename to python-multiplatform/scripts/wasm/build-cpython.sh index 3f7ee669f..de8cd3da2 100755 --- a/tools/wasm/build-cpython.sh +++ b/python-multiplatform/scripts/wasm/build-cpython.sh @@ -2,12 +2,12 @@ # CPython 3.14.2 for wasm32-emscripten, ABI-matched to pyemscripten_2026_0 (PEP 783), and staged as # the runtime python-multiplatform's wasmJs target runs against. # -# tools/wasm/build-cpython.sh build + zip + verify + stage (needs network the first time) -# tools/wasm/build-cpython.sh zip rebuild only the stdlib zip of an existing build (no emsdk) -# tools/wasm/build-cpython.sh verify check an existing build and its zip carry the ABI claims -# tools/wasm/build-cpython.sh stage copy python.wasm, python.mjs and the zip to the runtime dir -# tools/wasm/build-cpython.sh wheels download the pinned compiled wheels the wasm suite loads -# tools/wasm/build-cpython.sh stock build a STOCK PEP 776 interpreter as a negative control +# python-multiplatform/scripts/wasm/build-cpython.sh build + zip + verify + stage (needs network the first time) +# python-multiplatform/scripts/wasm/build-cpython.sh zip rebuild only the stdlib zip of an existing build (no emsdk) +# python-multiplatform/scripts/wasm/build-cpython.sh verify check an existing build and its zip carry the ABI claims +# python-multiplatform/scripts/wasm/build-cpython.sh stage copy python.wasm, python.mjs and the zip to the runtime dir +# python-multiplatform/scripts/wasm/build-cpython.sh wheels download the pinned compiled wheels the wasm suite loads +# python-multiplatform/scripts/wasm/build-cpython.sh stock build a STOCK PEP 776 interpreter as a negative control # # Everything lands inside this repository, under the git-ignored `.caches/`: # @@ -53,7 +53,7 @@ # PYVERSION ?= 3.14.2). set -euo pipefail -ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" CACHES="$ROOT/.caches" CPYTHON_TAG=v3.14.2 diff --git a/python-multiplatform/src/iosMain/README.md b/python-multiplatform/src/iosMain/README.md index 1516054ea..fbc3c9853 100644 --- a/python-multiplatform/src/iosMain/README.md +++ b/python-multiplatform/src/iosMain/README.md @@ -33,7 +33,7 @@ than an installed app, and exactly the shape that hangs an app rather than start An installed **app** carries its own prefix inside its bundle (SPEC L-11, `docs/platforms/ios-app-bundle.md`): -- `stageIosPythonHome__` stages the stdlib for one slice; `tools/xcode/install-python.sh`, +- `stageIosPythonHome__` stages the stdlib for one slice; `python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh`, an Xcode Run Script phase, copies it to `/python-multiplatform-home/`, copies the consumer's payload to `/python/`, and wraps every `.so` as `Frameworks/.framework` with a `.fwork` placeholder (Apple loads no loose binaries; CPython's `AppleFrameworkLoader` follows the diff --git a/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PackagedPythonHome.ios.kt b/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PackagedPythonHome.ios.kt index 6398f4e82..402345076 100644 --- a/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PackagedPythonHome.ios.kt +++ b/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PackagedPythonHome.ios.kt @@ -16,7 +16,7 @@ import platform.Foundation.NSFileManager * (or `simctl launch`) with no `PYTHONHOME`. The simulator *test* task sets one, pointing at the * workspace (`SIMCTL_CHILD_PYTHONHOME`), but the same shape for an app parks it at 0% CPU when the * path is on an external volume (`iosMain/README.md`). So the app carries its prefix inside its own - * bundle -- `tools/xcode/install-python.sh` copies it to `/`[DIRECTORY_NAME] -- and it is handed + * bundle -- `python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh` copies it to `/`[DIRECTORY_NAME] -- and it is handed * to CPython in-process, before `Py_Initialize`, by [applyPackagedPythonHome]. * * The desktop counterpart is `PackagedPythonHome` (`jvmMain`, SPEC L-9); same order, same name. diff --git a/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PythonPayload.ios.kt b/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PythonPayload.ios.kt index 2446ace6f..c01d22bd6 100644 --- a/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PythonPayload.ios.kt +++ b/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PythonPayload.ios.kt @@ -7,7 +7,7 @@ import platform.Foundation.NSBundle * directory. * * `toolchain` produces the payload (`build/pythonStaging/ios/python`, or the app's own sources); - * `tools/xcode/install-python.sh`, an Xcode Run Script phase, copies it there and wraps any + * `python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh`, an Xcode Run Script phase, copies it there and wraps any * extension module in it as a framework (SPEC L-11, `docs/platforms/ios-app-bundle.md`). An app * built without that phase has no `python/`, and this returns an empty list. * diff --git a/python-multiplatform/src/wasmJsMain/README.md b/python-multiplatform/src/wasmJsMain/README.md index 0aa8edd73..360880eb5 100644 --- a/python-multiplatform/src/wasmJsMain/README.md +++ b/python-multiplatform/src/wasmJsMain/README.md @@ -5,7 +5,7 @@ Kotlin/Wasm reaching CPython 3.14 built for `wasm32-emscripten`, through `@WasmI the data path. Everything below is measured. The standalone `wasm-experiment/` that first took the measurements -was retired into this source set and `tools/wasm/` (it remains in git history: `git log -- +was retired into this source set and `python-multiplatform/scripts/wasm/` (it remains in git history: `git log -- wasm-experiment`); `docs/platforms/wasm-design.md` holds the current decisions and numbers, and `docs/archive/wasm-design-experiment-log.md` records how they were arrived at, including the several conclusions that were wrong before they were run. Everything the experiment proved that this file @@ -17,7 +17,7 @@ stands on is now a test in `wasmJsTest/.../emscripten/`, run against the real in | the shared memory survives CPython growing it | `WasmSharedMemoryGrowthTest` | | Kotlin-only work neither grows nor writes that memory | `WasmKotlinLeavesLinearMemoryAloneTest` | | the interpreter carries the `pyemscripten_2026_0` ABI | `WasmInterpreterAbiTest` | -| ... and a real compiled wheel loads and runs in it | `WasmCompiledWheelTest` (wheels: `tools/wasm/build-cpython.sh wheels`) | +| ... and a real compiled wheel loads and runs in it | `WasmCompiledWheelTest` (wheels: `python-multiplatform/scripts/wasm/build-cpython.sh wheels`) | | direct calls beat a JS frame; hoisting beats a shim; strings and bulk reads | `WasmCrossingOverheadTest` | | `Table.set` beats `addFunction` + a JS closure for upcalls | `WasmUpcallRouteOverheadTest` | | `PY_CALL_TRAMPOLINE`'s JS fallback is live; `pmp_invoke` survives the wasm one | `WasmCallTrampolineTest` | @@ -25,7 +25,7 @@ stands on is now a test in `wasmJsTest/.../emscripten/`, run against the real in The measurement tests print their rows and assert only orderings that held by a wide margin; the figures quoted below are the experiment's, and the tests' own output is the current reading. -`tools/wasm/build-cpython.sh stock` builds the unpatched PEP 776 interpreter (plus only the runtime +`python-multiplatform/scripts/wasm/build-cpython.sh stock` builds the unpatched PEP 776 interpreter (plus only the runtime methods the library binds through) as the negative control for the ABI tests: against it, `WasmInterpreterAbiTest` and `WasmCompiledWheelTest` must fail. @@ -456,7 +456,7 @@ karma, `webpackCopy` for the glue (the bundle's directory is a fresh temp path e proxy for the document-relative stdlib zip. Needs a CPython Emscripten build. The default location is `/.caches/wasm-runtime`, which -`./gradlew :python-multiplatform:buildWasmPython` (that is, `tools/wasm/build-cpython.sh`) fills; +`./gradlew :python-multiplatform:buildWasmPython` (that is, `python-multiplatform/scripts/wasm/build-cpython.sh`) fills; `-PwasmPythonDir=` or `PMP_PYTHON_DIR` override it. The task **skips with a message** rather than failing when it is absent. The script matches `pyemscripten_2026_0` (PEP 783) closely enough to load a compiled PyPI wheel, and its `verify` step checks the stdlib zip as well as `python.wasm` -- the diff --git a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/TestWheelConfig.kt b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/TestWheelConfig.kt index b90848d93..b53f2f7d5 100644 --- a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/TestWheelConfig.kt +++ b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/TestWheelConfig.kt @@ -4,7 +4,7 @@ package python.native.ffi.emscripten /** * Where the `KotlinJsTest` `doFirst` in `python-multiplatform/build.gradle.kts` extracted the - * pinned wheels `tools/wasm/build-cpython.sh wheels` downloads, or `null` when none were found. + * pinned wheels `python-multiplatform/scripts/wasm/build-cpython.sh wheels` downloads, or `null` when none were found. * Test-only: appended to the test bundle's `cpython-config.mjs`, never to a consumer's. */ external val PMP_TEST_SITE_PACKAGES: JsString? diff --git a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmCompiledWheelTest.kt b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmCompiledWheelTest.kt index 1c76fe96e..bb4da7064 100644 --- a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmCompiledWheelTest.kt +++ b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmCompiledWheelTest.kt @@ -16,12 +16,12 @@ import python.multiplatform.ffi.PythonTestFixture * PyPI) is `dlopen`ed as an Emscripten side module, links against `python.wasm`, and runs. * * Measured in the former `wasm-experiment/` (Test G) through CPython's own `python.sh`, with no - * Kotlin in the process. Against a stock PEP 776 build (`tools/wasm/build-cpython.sh stock`) the same + * Kotlin in the process. Against a stock PEP 776 build (`python-multiplatform/scripts/wasm/build-cpython.sh stock`) the same * import fails at load: * * LinkError: Import #204 "env" "__cpp_exception": tag import requires a WebAssembly.Tag * - * The wheels are not in the repository. `tools/wasm/build-cpython.sh wheels` downloads them by URL + * The wheels are not in the repository. `python-multiplatform/scripts/wasm/build-cpython.sh wheels` downloads them by URL * with pinned sha256 into `.caches/wasm-wheels`, and the test task extracts them next to the test * bundle. Without them this test skips with that instruction, and under `-PrequireWasmRuntime=true` * it fails instead -- the same contract every other wasm prerequisite has. @@ -39,7 +39,7 @@ class WasmCompiledWheelTest { val site = PMP_TEST_SITE_PACKAGES?.toString() if (site == null) { val message = "no pyemscripten_2026_0 wheels found in ${PMP_TEST_WHEELS_DIR?.toString()}. " + - "Run tools/wasm/build-cpython.sh wheels -- it downloads the pinned pydantic-core and " + + "Run python-multiplatform/scripts/wasm/build-cpython.sh wheels -- it downloads the pinned pydantic-core and " + "typing-extensions wheels there -- and re-run this task." if (required.toBoolean()) { fail("$message -PrequireWasmRuntime=true was passed, so this is a failure rather than a skip.") diff --git a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmInterpreterAbiTest.kt b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmInterpreterAbiTest.kt index d7a26057a..5c800f878 100644 --- a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmInterpreterAbiTest.kt +++ b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmInterpreterAbiTest.kt @@ -7,7 +7,7 @@ import python.multiplatform.ffi.Python3 import python.multiplatform.ffi.PythonTestFixture /** - * The interpreter under test is the one `tools/wasm/build-cpython.sh` produces: CPython matched to + * The interpreter under test is the one `python-multiplatform/scripts/wasm/build-cpython.sh` produces: CPython matched to * PEP 783's `pyemscripten_2026_0`, so that a compiled PyPI wheel with that tag can load. * * A stock PEP 776 build differs in exactly the two places asserted here, and both fail silently diff --git a/sample/src/iosMain/app.xcodeproj/project.pbxproj b/sample/src/iosMain/app.xcodeproj/project.pbxproj index 103864c7d..4117bea77 100644 --- a/sample/src/iosMain/app.xcodeproj/project.pbxproj +++ b/sample/src/iosMain/app.xcodeproj/project.pbxproj @@ -296,7 +296,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\n# SPEC L-10 / issue #59: the CPython stdlib and this app's python/ payload into the .app, with\n# extension modules wrapped as frameworks. See docs/platforms/ios-app-bundle.md.\ncd \"$SRCROOT/../../..\"\nexport PYTHON_PAYLOAD_DIR=\"$SRCROOT/../commonMain/python\"\n/bin/bash tools/xcode/install-python.sh\n"; + shellScript = "set -e\n# SPEC L-10 / issue #59: the CPython stdlib and this app's python/ payload into the .app, with\n# extension modules wrapped as frameworks. See docs/platforms/ios-app-bundle.md.\ncd \"$SRCROOT/../../..\"\nexport PYTHON_PAYLOAD_DIR=\"$SRCROOT/../commonMain/python\"\n/bin/bash python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/tools/loop-verify.sh b/tools/loop-verify.sh deleted file mode 100755 index bc1eaf31c..000000000 --- a/tools/loop-verify.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -# Verification gate for the paseo loop. Exits 0 only if every suite that the loop -# may have touched is green. Counts come from the XML, not from exit codes, and the -# results directories are cleared first so a crashed run cannot report stale numbers. -# -# tools/loop-verify.sh [checkout] defaults to the checkout this script lives in -# -# Each suite is its own Gradle invocation: one invocation can hide an ordering dependency -# between modules (it once hid 24 failures in :ksp-fixtures:artifact). -set -u -ROOT="${1:-$(cd "$(dirname "$0")/.." && pwd)}" -cd "$ROOT" || exit 1 -export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}" -LOGDIR="$ROOT/.tmp/loop-verify" -mkdir -p "$LOGDIR" - -rm -rf python-multiplatform/build/test-results \ - ksp-fixtures/app/build/test-results \ - ksp-fixtures/compose/build/test-results \ - ksp-fixtures/artifact/build/test-results \ - python-multiplatform-gradle-plugin/build/test-results - -rc=0 -for task in :python-multiplatform:desktopTest :ksp-fixtures:app:desktopTest \ - :ksp-fixtures:compose:desktopTest :ksp-fixtures:artifact:desktopTest \ - :python-multiplatform-gradle-plugin:test \ - :python-multiplatform:compileKotlinAndroidNativeArm64; do - log="$LOGDIR/$(echo "$task" | tr ':' '_').log" - ./gradlew "$task" --rerun --console=plain > "$log" 2>&1 - r=$? - echo "$task exit=$r compile-errors=$(grep -c '^e: ' "$log")" - [ "$r" -eq 0 ] || rc=1 -done - -python3 - <<'PY' -import glob, sys, xml.etree.ElementTree as ET -bad = 0 -for label, pat in [ - ("desktop", "python-multiplatform/build/test-results/desktopTest/*.xml"), - ("app", "ksp-fixtures/app/build/test-results/desktopTest/*.xml"), - ("compose", "ksp-fixtures/compose/build/test-results/desktopTest/*.xml"), - ("artifact", "ksp-fixtures/artifact/build/test-results/desktopTest/*.xml"), - ("plugin", "python-multiplatform-gradle-plugin/build/test-results/test/*.xml"), -]: - t = f = e = 0 - for p in glob.glob(pat): - r = ET.parse(p).getroot() - t += int(r.get('tests')); f += int(r.get('failures')); e += int(r.get('errors')) - print(f"{label}: {t}/{f}/{e}") - if t == 0 or f or e: - bad = 1 -sys.exit(bad) -PY -py=$? - -[ "$rc" -eq 0 ] && [ "$py" -eq 0 ]