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
52 changes: 52 additions & 0 deletions .github/scripts/release/assemble-desktop-release.sh
Original file line number Diff line number Diff line change
@@ -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 <out-dir>
# Produces in <out-dir>:
# python-multiplatform-maven-repo-<version>.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-<pyversion>-py3-none-any.whl the TypedPython compiler (pure Python + its C runtime header)
# python-multiplatform-ksp-<version>-typedpython-gate.zip
# python-multiplatform-examples-<version>.zip docs/release/desktop (the examples below, verified per release)
# SHA256SUMS
set -euo pipefail
out="$(mkdir -p "${1:?usage: assemble-desktop-release.sh <out-dir>}" && 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$'
90 changes: 90 additions & 0 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
@@ -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
62 changes: 62 additions & 0 deletions docs/release/desktop/README.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 20 additions & 0 deletions docs/release/desktop/kotlin-hello/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
}
20 changes: 20 additions & 0 deletions docs/release/desktop/kotlin-hello/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -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"
17 changes: 17 additions & 0 deletions docs/release/desktop/kotlin-hello/src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -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))
}
Binary file not shown.
34 changes: 34 additions & 0 deletions docs/release/desktop/typedpython/kernels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# typedpython: compiled
import sys


def dot(n: int) -> 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()