From c4a8c554fb5273678e37b11c7ba58b21299da8e1 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:45:33 +0900 Subject: [PATCH] CI: desktop-only GitHub Release workflow with verified examples On a v* tag (or by hand for an existing tag) desktop-release.yml assembles, on macOS, a Maven repository zip (library desktop artifact with every desktop libpython, KSP processor and gate zip, Gradle plugin and marker; built with -Dmaven.repo.local, never ~/.m2), the typedpython wheel, the gate zip, an examples zip and SHA256SUMS; runs both examples from those assets alone (a Kotlin app calling Python, and typedpython demo); and only then creates or updates the pre-release. Nothing goes to Maven Central. The tag must equal v-. --- .../release/assemble-desktop-release.sh | 52 ++++++++++ .github/workflows/desktop-release.yml | 90 ++++++++++++++++++ docs/release/desktop/README.md | 62 ++++++++++++ .../desktop/kotlin-hello/build.gradle.kts | 20 ++++ .../desktop/kotlin-hello/settings.gradle.kts | 20 ++++ .../kotlin-hello/src/main/kotlin/Main.kt | 17 ++++ .../__pycache__/kernels.cpython-315.pyc | Bin 0 -> 1947 bytes docs/release/desktop/typedpython/kernels.py | 34 +++++++ 8 files changed, 295 insertions(+) create mode 100755 .github/scripts/release/assemble-desktop-release.sh create mode 100644 .github/workflows/desktop-release.yml create mode 100644 docs/release/desktop/README.md create mode 100644 docs/release/desktop/kotlin-hello/build.gradle.kts create mode 100644 docs/release/desktop/kotlin-hello/settings.gradle.kts create mode 100644 docs/release/desktop/kotlin-hello/src/main/kotlin/Main.kt create mode 100644 docs/release/desktop/typedpython/__pycache__/kernels.cpython-315.pyc create mode 100644 docs/release/desktop/typedpython/kernels.py diff --git a/.github/scripts/release/assemble-desktop-release.sh b/.github/scripts/release/assemble-desktop-release.sh new file mode 100755 index 000000000..175190edd --- /dev/null +++ b/.github/scripts/release/assemble-desktop-release.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Assembles the assets of a desktop-only GitHub Release (no Maven publishing): run from the repository root. +# .github/scripts/release/assemble-desktop-release.sh +# Produces in : +# python-multiplatform-maven-repo-.zip a Maven repository (org/thisisthepy/...): the library's +# desktop variant and KMP root, the KSP processor with its +# typedpython-gate zip, the Gradle plugin and its marker +# typedpython--py3-none-any.whl the TypedPython compiler (pure Python + its C runtime header) +# python-multiplatform-ksp--typedpython-gate.zip +# python-multiplatform-examples-.zip docs/release/desktop (the examples below, verified per release) +# SHA256SUMS +set -euo pipefail +out="$(mkdir -p "${1:?usage: assemble-desktop-release.sh }" && cd "$1" && pwd)" +root="$(pwd)" +version="$(sed -nE 's/^pythonVersion=(.*)$/\1/p' gradle.properties)-$(sed -nE 's/^libraryVersionSuffix=(.*)$/\1/p' gradle.properties)" +repo="$out/maven-repo" +rm -rf "$repo" +mkdir -p "$repo" + +# The desktop variant and the KMP root metadata of the library, the KSP processor, and the Gradle plugin +# (an included build: its own invocation), all into one repository directory, never ~/.m2. +./gradlew --console=plain -Dmaven.repo.local="$repo" \ + :python-multiplatform:publishDesktopPublicationToMavenLocal \ + :python-multiplatform:publishKotlinMultiplatformPublicationToMavenLocal \ + :python-multiplatform-ksp:publishToMavenLocal +./gradlew --console=plain -p python-multiplatform-gradle-plugin -Dmaven.repo.local="$repo" publishToMavenLocal + +test -f "$repo/org/thisisthepy/python-multiplatform-desktop/$version/python-multiplatform-desktop-$version.jar" +test -f "$repo/org/thisisthepy/python-multiplatform-ksp/$version/python-multiplatform-ksp-$version-typedpython-gate.zip" +test -d "$repo/org/thisisthepy/python/multiplatform/bindings/org.thisisthepy.python.multiplatform.bindings.gradle.plugin" +if find "$repo" -path '*io/github/thisisthepy*' | grep -q .; then + echo "error: an io.github.thisisthepy coordinate was published (#95)" >&2; exit 1 +fi +(cd "$out" && rm -f "python-multiplatform-maven-repo-$version.zip" && zip -qr "python-multiplatform-maven-repo-$version.zip" maven-repo) +cp "$repo/org/thisisthepy/python-multiplatform-ksp/$version/python-multiplatform-ksp-$version-typedpython-gate.zip" "$out/" + +# The TypedPython wheel. +rm -f "$out"/typedpython-*.whl +uv build --wheel --out-dir "$out" python-multiplatform-ksp >/dev/null + +# The examples, with the release version written into them. +ex="$out/examples" +rm -rf "$ex" +cp -R docs/release/desktop "$ex" +find "$ex" -type f \( -name '*.kts' -o -name '*.md' -o -name '*.properties' \) -exec sed -i.bak "s/@VERSION@/$version/g" {} + +find "$ex" -name '*.bak' -delete +(cd "$out" && rm -f "python-multiplatform-examples-$version.zip" && zip -qr "python-multiplatform-examples-$version.zip" examples) + +(cd "$out" && rm -f SHA256SUMS && shasum -a 256 python-multiplatform-maven-repo-*.zip typedpython-*.whl \ + python-multiplatform-ksp-*-typedpython-gate.zip python-multiplatform-examples-*.zip > SHA256SUMS) +echo "version=$version" +ls -1 "$out" | grep -v '^maven-repo$\|^examples$' diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml new file mode 100644 index 000000000..90cd6432f --- /dev/null +++ b/.github/workflows/desktop-release.yml @@ -0,0 +1,90 @@ +name: Desktop release + +# A desktop-only preview release: the assets are files on a GitHub Release, nothing is published to +# Maven Central. Triggered by pushing a version tag (the coordinator tags after release -> main), or +# by hand for an existing tag. The assets are assembled by .github/scripts/release/assemble-desktop-release.sh, +# and both examples of docs/release/desktop run against those assets before anything is uploaded. + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Existing tag to build and attach the assets to (e.g. v3.15.0-alpha01)' + required: true + +permissions: + contents: write + +jobs: + desktop-release: + name: Assemble, check and attach (macOS) + runs-on: macos-latest + env: + TAG: ${{ github.event.inputs.tag || github.ref_name }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.tag || github.ref }} + + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'zulu' + + - name: Setup uv + uses: astral-sh/setup-uv@v6 + + - name: Assemble the assets + run: .github/scripts/release/assemble-desktop-release.sh build/desktop-release + + - name: The tag names this build's version + run: | + set -euo pipefail + version="$(sed -nE 's/^pythonVersion=(.*)$/\1/p' gradle.properties)-$(sed -nE 's/^libraryVersionSuffix=(.*)$/\1/p' gradle.properties)" + test "$TAG" = "v$version" || { echo "error: tag $TAG does not match version $version" >&2; exit 1; } + echo "VERSION=$version" >> "$GITHUB_ENV" + + - name: Example 1, Kotlin desktop app, from the assets only + run: | + set -euo pipefail + u="$RUNNER_TEMP/user" + rm -rf "$u" && mkdir -p "$u" && cd "$u" + unzip -q "$GITHUB_WORKSPACE/build/desktop-release/python-multiplatform-maven-repo-$VERSION.zip" + unzip -q "$GITHUB_WORKSPACE/build/desktop-release/python-multiplatform-examples-$VERSION.zip" + cd examples/kotlin-hello + "$GITHUB_WORKSPACE/gradlew" -q run -PpmpRepo="$u/maven-repo" -Dmaven.repo.local="$u/empty-m2" --console=plain | tee "$u/run.log" + grep -q '^Python 3\.' "$u/run.log" + grep -q '^{"sum": 45}$' "$u/run.log" + grep -q '^Kotlin sees: 42$' "$u/run.log" + + - name: Example 2, TypedPython, from the wheel only + run: | + set -euo pipefail + u="$RUNNER_TEMP/user" + cd "$u" + uv venv -q -p 3.15t tpenv + uv pip install -q --python tpenv/bin/python "$GITHUB_WORKSPACE"/build/desktop-release/typedpython-*.whl + tpenv/bin/typedpython demo --out build examples/typedpython/kernels.py 400000 | tee tp.log + grep -q '^compiled functions: .*dot' tp.log + grep -q '^identical output' tp.log + + - name: Attach to the GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + d=build/desktop-release + sed "s/@VERSION@/$VERSION/g" docs/release/desktop/README.md > "$d/NOTES.md" + assets=("$d"/python-multiplatform-maven-repo-*.zip "$d"/typedpython-*.whl \ + "$d"/python-multiplatform-ksp-*-typedpython-gate.zip "$d"/python-multiplatform-examples-*.zip "$d"/SHA256SUMS) + if gh release view "$TAG" >/dev/null 2>&1; then + gh release upload "$TAG" "${assets[@]}" --clobber + else + gh release create "$TAG" --prerelease --title "python-multiplatform $VERSION (desktop preview)" \ + --notes-file "$d/NOTES.md" "${assets[@]}" + fi diff --git a/docs/release/desktop/README.md b/docs/release/desktop/README.md new file mode 100644 index 000000000..e2fbd55b0 --- /dev/null +++ b/docs/release/desktop/README.md @@ -0,0 +1,62 @@ +# python-multiplatform @VERSION@: desktop preview + +A desktop-only preview, published as files on this GitHub Release rather than to Maven Central. +Android, iOS and wasm are not part of it. + +## What is attached + +| File | What it is | +|---|---| +| `python-multiplatform-maven-repo-@VERSION@.zip` | A Maven repository (`maven-repo/org/thisisthepy/...`): the library's desktop artifact (it carries `libpython` for macOS arm64/x86_64, Linux x86_64 and Windows x86_64), the KSP processor with its `typedpython-gate` zip, and the Gradle plugin `org.thisisthepy.python.multiplatform.bindings` | +| `typedpython-0.1.0-py3-none-any.whl` | TypedPython, the compiler from annotated Python to C extension modules | +| `python-multiplatform-ksp-@VERSION@-typedpython-gate.zip` | The TypedPython gate bundle on its own (also inside the repository zip) | +| `python-multiplatform-examples-@VERSION@.zip` | The two examples below | +| `SHA256SUMS` | Checksums of all of the above | + +Check the downloads first: + +```bash +shasum -a 256 -c SHA256SUMS +``` + +## Example 1: call Python from a Kotlin desktop app + +Unpack the repository and the examples side by side: + +```bash +unzip python-multiplatform-maven-repo-@VERSION@.zip +unzip python-multiplatform-examples-@VERSION@.zip +cd examples/kotlin-hello +gradle run -PpmpRepo=../../maven-repo +``` + +Use Gradle 8.11 or newer with JDK 21 or newer. `-PpmpRepo` points at the unpacked `maven-repo` +directory (the default is `../maven-repo`). The first `run` downloads the CPython build this release +was made against and sets `PYTHONHOME` for it. Expected output: + +```text +Python 3.15.0 from Kotlin +{"sum": 45} +Kotlin sees: 42 +``` + +## Example 2: compile Python with TypedPython + +TypedPython needs CPython 3.15t (free-threaded) and a C compiler (Xcode command line tools on macOS, +`gcc` on Linux). + +```bash +uv venv -p 3.15t .venv +uv pip install --python .venv/bin/python typedpython-0.1.0-py3-none-any.whl +.venv/bin/typedpython demo examples/typedpython/kernels.py 400000 +``` + +`demo` compiles the functions it can prove safe, runs `main()` interpreted and compiled, and checks +that both print the same thing. It lists the compiled functions (`dot`, `collatz_steps`) and the +reason any function stayed interpreted. + +## Not covered by this preview + +- Android, iOS and wasm artifacts, and publishing to Maven Central. +- The wasm runtime zip (it needs a CPython Emscripten build). +- Linux arm64 and Windows arm64 desktops have no `libpython` in the desktop artifact. diff --git a/docs/release/desktop/kotlin-hello/build.gradle.kts b/docs/release/desktop/kotlin-hello/build.gradle.kts new file mode 100644 index 000000000..22ed027d8 --- /dev/null +++ b/docs/release/desktop/kotlin-hello/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + kotlin("jvm") version "2.4.20-Beta2" + id("com.google.devtools.ksp") version "2.3.11" + id("org.thisisthepy.python.multiplatform.bindings") version "@VERSION@" + application +} + +dependencies { + implementation("org.thisisthepy:python-multiplatform:@VERSION@") +} + +kotlin { + jvmToolchain(21) +} + +application { + mainClass.set("MainKt") + // python-multiplatform calls CPython through the JDK's Foreign Function API + applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED") +} diff --git a/docs/release/desktop/kotlin-hello/settings.gradle.kts b/docs/release/desktop/kotlin-hello/settings.gradle.kts new file mode 100644 index 000000000..3932d444a --- /dev/null +++ b/docs/release/desktop/kotlin-hello/settings.gradle.kts @@ -0,0 +1,20 @@ +// The release's Maven repository zip, unpacked beside this project (see ../README.md). +val releaseRepo = file(providers.gradleProperty("pmpRepo").getOrElse("../maven-repo")) + +pluginManagement { + repositories { + maven { url = uri(file(providers.gradleProperty("pmpRepo").getOrElse("../maven-repo"))) } + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + maven { url = uri(releaseRepo) } + mavenCentral() + google() + } +} + +rootProject.name = "kotlin-hello" diff --git a/docs/release/desktop/kotlin-hello/src/main/kotlin/Main.kt b/docs/release/desktop/kotlin-hello/src/main/kotlin/Main.kt new file mode 100644 index 000000000..ca8a0ec6c --- /dev/null +++ b/docs/release/desktop/kotlin-hello/src/main/kotlin/Main.kt @@ -0,0 +1,17 @@ +import python.multiplatform.ffi.Python3 + +fun main() { + // `./gradlew run` stages the CPython this release was built against and sets PYTHONHOME for it. + Python3.initialize() + Python3.exec( + """ + import sys, json + print("Python", sys.version.split()[0], "from Kotlin") + print(json.dumps({"sum": sum(range(10))})) + sys.stdout.flush() # Python buffers stdout; the JVM does not flush it for us on exit + """.trimIndent(), + ) + // eval(expression, Py_eval_input = 258, globals, locals): the result is a Python object + val globals = Python3.import("__main__").getAttr("__dict__") + println("Kotlin sees: " + Python3.eval("6 * 7", 258, globals, globals)) +} diff --git a/docs/release/desktop/typedpython/__pycache__/kernels.cpython-315.pyc b/docs/release/desktop/typedpython/__pycache__/kernels.cpython-315.pyc new file mode 100644 index 0000000000000000000000000000000000000000..773b058cc283c194092cd06038a5ed35d2f91820 GIT binary patch literal 1947 zcmbtV-D?|15Z}EINwy_hvSZVz`LG)rA{9$@LQNh*(?IQ}CZSeZg@7W&)jHc(pYD|1 zRZ|-Z3Jpbd?M4Bc_@NJZXdeRoJNgGSp}31F0YBxILM<2hNe=x~RNMZ3D3on01-zIU_c3a9E?0&iwB_kFe`mJcamc3&B=LczN|1 zl_s;i1v_Q15bR3IC3PhIhGVkNkB*MAK2+=Q!E6L1^`Y;B@PKSb`s&wqVuQ_zM|T=@ zI~r^1>z{xS>1ig`^Xt8F* z_JozwMv?NLMe$^E&UXPRD=oO`(%6Xu#&)r zej{E48%MKVr0L))%l;`0Es^dlQac@+m$94788;#y#|2TvDmRu zxT0c){Rf}`8J^lnj5P0Uz5C>B!`zN000{j9&557pw+G_QiS^nRU0c~cF}#)7oPJDS zf{dK-ho=I);fZphZRheHT)To^~j(1aJ2Qx&juhqHYr@9KrztcOrcRP=SR2tnGh+ z6m{wk&yZAD>aOicoiB4~SZv{IRb$euGHBhZCnz*Plo7wG=sHcqzyP!NK&t}#5eV2d z63{imy=#{qUH;+nci%Sey$HXx6CK#lpF|!<03LnMqo*2l$LLu**RXy(zcK!lKE3s8 z`1#<4KaG^if{c_(Xu4ENnd|~s?RueJ!+p=HK%-{oa2GpIyNyMzRQz(ywLfGT&>^xo sy4TVvrMsl}AO!9DAPzNukc7 float: + s = 0.0 + for i in range(n): + x = i * 0.5 + s += x * x + return s + + +def collatz_steps(limit: int) -> int: + total = 0 + for start in range(1, limit): + k = start + while k != 1: + if k % 2 == 0: + k = k // 2 + else: + k = 3 * k + 1 + total += 1 + return total + + +def main() -> None: + n = 2_000_000 + if len(sys.argv) > 1: + n = int(sys.argv[1]) + print(dot(n), collatz_steps(n // 20)) + + +if __name__ == "__main__": + main()