Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
77bc34d
Refactor: Restore usage-example/ (not sample/)
b-re-w Oct 3, 2026
b4274e8
Merge pull request #82 from thisisthepy/feat/usage-example
b-re-w Oct 3, 2026
7324646
CPython 3.15.0: PY3_15_0, 3.15t spelling, wheel tags follow compileSd…
b-re-w Oct 15, 2026
2bc0784
usage-example iOS phase: run install-python.sh from its new path in p…
b-re-w Oct 15, 2026
06b600e
Merge pull request #85 from thisisthepy/feat/python-3.15.0
b-re-w Oct 15, 2026
427da85
CI: Run the tests on pushes to develop, not on every pull request
b-re-w Oct 17, 2026
3b538b7
Merge pull request #87 from thisisthepy/feat/ci-on-develop
b-re-w Oct 17, 2026
035a9bb
Docs: toolchain-lite 0.1.0 is on PyPI (#73)
b-re-w Oct 17, 2026
2717dd6
Merge pull request #88 from thisisthepy/feat/pypi-released
b-re-w Oct 17, 2026
ffb8de1
compileSdk: Drop the GIL-only 3.14.7 and 3.13.0; refuse CPython befor…
b-re-w Oct 17, 2026
44acc1c
Merge pull request #89 from thisisthepy/feat/drop-gil-sdks
b-re-w Oct 17, 2026
0eee0da
typedpythonCheck: Take the gate from python-multiplatform-ksp's typed…
b-re-w Oct 17, 2026
fff7dbd
Merge pull request #91 from thisisthepy/feat/typedpython-gate-artifact
b-re-w Oct 17, 2026
8180f03
Fix: Delete directories without following symbolic links out of them …
b-re-w Oct 17, 2026
1fb172a
Merge pull request #94 from thisisthepy/feat/delete-no-follow
b-re-w Oct 17, 2026
9aa8f80
iOS e2e: Read the smoke report from a file in the app's container (#92)
b-re-w Oct 17, 2026
1e9af80
CI: Let a develop run in progress finish; a newer push replaces only …
b-re-w Oct 17, 2026
54ef5cd
Merge pull request #95 from thisisthepy/feat/ci-finish-runs
b-re-w Oct 17, 2026
37f811a
Merge pull request #96 from thisisthepy/feat/ios-e2e-flake
b-re-w Oct 17, 2026
4db268d
Publishing: Prepare the plugin for the Plugin Portal; take packpack f…
b-re-w Oct 17, 2026
c8bc543
Merge pull request #98 from thisisthepy/feat/plugin-portal-prep
b-re-w Oct 17, 2026
b27346b
Publishing: Release workflow for the Gradle plugin; coordinates in gr…
b-re-w Oct 17, 2026
f74d534
Merge pull request #100 from thisisthepy/feat/plugin-release
b-re-w Oct 17, 2026
fe52885
Docs: Design the live connection from a notebook to the running app (…
b-re-w Oct 17, 2026
f5cfcac
Merge pull request #102 from thisisthepy/feat/live-connection-design
b-re-w Oct 17, 2026
d93f4c4
Fix: hotReloadPython fails saying hot reload does not work yet (#103)
b-re-w Oct 17, 2026
bb01be0
Merge pull request #104 from thisisthepy/feat/hot-reload-not-yet
b-re-w Oct 17, 2026
b1a9df8
Live connection: stage the dev-agent marker for hot-reload builds; de…
b-re-w Oct 17, 2026
b59bc15
Merge pull request #106 from thisisthepy/feat/dev-agent-launch
b-re-w Oct 17, 2026
a07c6d2
Feat: switch Compose dependencies and plugins to org.thisisthepy.comp…
b-re-w Oct 25, 2026
dab7181
Merge pull request #108 from thisisthepy/feat/compose-extended
b-re-w Oct 25, 2026
69606b7
Feat: bump org.thisisthepy.compose Gradle plugin to 1.11.1-ext.3 (#109)
b-re-w Oct 25, 2026
852174a
Merge pull request #110 from thisisthepy/feat/compose-plugin-ext3
b-re-w Oct 25, 2026
30c10ef
Feat: switch python-multiplatform coordinates to org.thisisthepy:3.15…
b-re-w Oct 25, 2026
2cfb26b
Merge pull request #112 from thisisthepy/feat/pmp-coordinates
b-re-w Oct 25, 2026
9a3dce0
Fix: iOS phase runs the install-python.sh copy installPythonXcodeScri…
b-re-w Oct 25, 2026
c5a7fe6
Docs: temporary files go to the session scratchpad or .cache/, not .tmp/
b-re-w Oct 25, 2026
b32b565
Merge pull request #115 from thisisthepy/feat/temp-dir-rule
b-re-w Oct 25, 2026
8096060
Merge pull request #114 from thisisthepy/feat/ios-xcode-baked-install…
b-re-w Oct 25, 2026
ca241c6
Release: sync from 8096060541dd02e56fdaf201ea37735a817f28c6 8096060
github-actions[bot] Oct 25, 2026
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
93 changes: 93 additions & 0 deletions .github/scripts/maven/check_central_bundle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Check a Maven staging directory against Maven Central's requirements (pypackpack#99, toolchain#97).

check_central_bundle.py <staging dir> <group> <artifact> <version> [--plugin-id <id>] [--unsigned]

Central (https://central.sonatype.org/publish/requirements/) requires, for every artifact:
the jar, `-sources.jar` and `-javadoc.jar` beside the `.pom`; an `.asc` signature and `.md5`/`.sha1`
checksums for each file; and a POM with name, description, url, a license, a developer and SCM.
`--plugin-id` also requires the Gradle plugin marker (`<id>:<id>.gradle.plugin`). `--unsigned` skips the
`.asc` check, for a rehearsal without a key. Exit 0 when everything is there; otherwise every problem is
printed and the exit is 1.
"""
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

NS = {"m": "http://maven.apache.org/POM/4.0.0"}


def artifact_dir(root: Path, group: str, artifact: str, version: str) -> Path:
return root.joinpath(*group.split("."), artifact, version)


def check_files(directory: Path, artifact: str, version: str, kinds, signed: bool, problems: list):
base = f"{artifact}-{version}"
for kind in kinds:
name = base + kind
f = directory / name
if not f.is_file():
problems.append(f"missing {f}")
continue
suffixes = [".md5", ".sha1"] + ([".asc"] if signed else [])
for suffix in suffixes:
if not (directory / (name + suffix)).is_file():
problems.append(f"missing {name}{suffix}")


def text(pom, path):
node = pom.find(path, NS)
return (node.text or "").strip() if node is not None else ""


def check_pom(pom_file: Path, problems: list):
if not pom_file.is_file():
return
pom = ET.parse(pom_file).getroot()
for path, what in [
("m:name", "name"),
("m:description", "description"),
("m:url", "url"),
("m:licenses/m:license/m:name", "license name"),
("m:licenses/m:license/m:url", "license url"),
("m:developers/m:developer/m:name", "developer name"),
("m:scm/m:url", "scm url"),
("m:scm/m:connection", "scm connection"),
]:
if not text(pom, path):
problems.append(f"{pom_file.name}: no {what}")


def main(argv):
rest = list(argv[1:])
signed = "--unsigned" not in rest
plugin_id = None
if "--plugin-id" in rest:
i = rest.index("--plugin-id")
plugin_id = rest[i + 1]
del rest[i : i + 2]
args = [a for a in rest if not a.startswith("--")]
if len(args) != 4:
print(__doc__)
return 2
root, group, artifact, version = Path(args[0]), args[1], args[2], args[3]
problems: list = []

main_dir = artifact_dir(root, group, artifact, version)
check_files(main_dir, artifact, version, [".jar", "-sources.jar", "-javadoc.jar", ".pom"], signed, problems)
check_pom(main_dir / f"{artifact}-{version}.pom", problems)

if plugin_id:
marker = f"{plugin_id}.gradle.plugin"
marker_dir = artifact_dir(root, plugin_id, marker, version)
check_files(marker_dir, marker, version, [".pom"], signed, problems)
check_pom(marker_dir / f"{marker}-{version}.pom", problems)

for p in problems:
print(p)
print("OK" if not problems else f"{len(problems)} problem(s)")
return 0 if not problems else 1


if __name__ == "__main__":
sys.exit(main(sys.argv))
120 changes: 120 additions & 0 deletions .github/workflows/publish-gradle-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Publish the Gradle plugin

# Publishing a GitHub Release tagged `v<version>` (the plugin's tags, #71) stages and checks the plugin
# and its marker (.github/scripts/maven/check_central_bundle.py --plugin-id) and publishes them to the
# Gradle Plugin Portal (`publishPlugins`). A manual run does everything except the upload (#99).
#
# Nothing can upload until the user has created the `gradle-plugin-portal` environment (restricted to
# `v*` tags) with its secrets and published such a release; without a required secret the job stops
# and names it. The plugin depends on packpack, which the Portal resolves from Maven Central, so the
# job also stops while gradle.properties' packpackVersion is not on Central. docs/SPEC.md §1.19.
on:
release:
types: [published]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: publish-gradle-plugin-${{ github.ref }}
cancel-in-progress: false

jobs:
publish:
# Releases tagged toolchain-lite-v* are tcl's (publish-pypi.yml).
if: github.event_name != 'release' || (startsWith(github.event.release.tag_name, 'v'))
runs-on: ubuntu-latest
timeout-minutes: 30
environment: ${{ github.event_name == 'release' && 'gradle-plugin-portal' || '' }}
env:
IS_RELEASE: ${{ github.event_name == 'release' }}
TAG: ${{ github.event.release.tag_name }}
steps:
- uses: actions/checkout@v4

- name: The tag names the version; packpack is on Central; the version is new on the Portal
run: |
prop() { sed -n "s/^$1=//p" gradle.properties; }
group=$(prop mavenGroup); version=$(prop toolchainVersion); plugin=$(prop pluginId)
packpack=$(prop packpackModule); packpack_version=$(prop packpackVersion)
echo "plugin $plugin $version ($group:toolchain) packpack $packpack:$packpack_version tag: ${TAG:-none (dry run)}"
{ echo "GROUP=$group"; echo "VERSION=$version"; echo "PLUGIN_ID=$plugin"; } >> "$GITHUB_ENV"
fail() { if [ "$IS_RELEASE" = true ]; then echo "::error::$1"; exit 1; else echo "::warning::$1"; fi; }
if [ "$IS_RELEASE" = true ] && [ "$TAG" != "v$version" ]; then
echo "::error::release tag '$TAG' is not 'v$version' (gradle.properties toolchainVersion)"; exit 1
fi
pp_group=${packpack%%:*}; pp_artifact=${packpack##*:}
curl -fsS -o /dev/null "https://repo1.maven.org/maven2/${pp_group//.//}/$pp_artifact/$packpack_version/$pp_artifact-$packpack_version.pom" \
|| fail "$packpack:$packpack_version is not on Maven Central; publish packpack first (pypackpack publish-maven.yml)"
marker="https://plugins.gradle.org/m2/${plugin//.//}/$plugin.gradle.plugin/$version/$plugin.gradle.plugin-$version.pom"
if curl -fsS -o /dev/null "$marker"; then fail "$plugin $version is already on the Plugin Portal; bump toolchainVersion"; fi

- name: The publishing secrets are there
env:
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
run: |
missing=""
for name in GRADLE_PUBLISH_KEY GRADLE_PUBLISH_SECRET; do
[ -n "${!name}" ] || missing="$missing $name"
done
if [ -n "$missing" ]; then
if [ "$IS_RELEASE" = true ]; then
echo "::error::missing secrets in the gradle-plugin-portal environment:$missing (docs/SPEC.md §1.19)"; exit 1
fi
echo "::notice::dry run without:$missing; nothing is uploaded"
fi
# The Portal does not require signatures; with SIGNING_KEY (and SIGNING_KEY_PASSWORD) they are added.
[ -n "$SIGNING_KEY" ] || echo "::notice::no SIGNING_KEY: the plugin is published unsigned"

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"

- uses: gradle/actions/setup-gradle@v4

# The plugin builds against packpack; until packpack is on Maven Central (a dry run before its
# first release), build it from pypackpack's develop into mavenLocal, as test.yml does.
- name: packpack, from pypackpack, when Central does not have it yet
if: github.event_name != 'release'
run: |
prop() { sed -n "s/^$1=//p" gradle.properties; }
packpack=$(prop packpackModule); v=$(prop packpackVersion); g=${packpack%%:*}; a=${packpack##*:}
if ! curl -fsS -o /dev/null "https://repo1.maven.org/maven2/${g//.//}/$a/$v/$a-$v.pom"; then
git clone --depth 1 --branch develop https://github.com/thisisthepy/pypackpack.git "$RUNNER_TEMP/pypackpack"
(cd "$RUNNER_TEMP/pypackpack" && ./gradlew :packpack:publishToMavenLocal --console=plain)
fi

- name: Stage the plugin and its marker (signed when a key is there)
env:
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
run: |
if [ -z "$ORG_GRADLE_PROJECT_signingInMemoryKey" ]; then
unset ORG_GRADLE_PROJECT_signingInMemoryKey ORG_GRADLE_PROJECT_signingInMemoryKeyPassword
fi
./gradlew :toolchain:publishAllPublicationsToStagingRepository --console=plain

- name: The plugin and its marker meet the requirements
env:
SIGNED: ${{ secrets.SIGNING_KEY != '' }}
run: |
flag=""
[ "$SIGNED" = true ] || flag="--unsigned"
python3 .github/scripts/maven/check_central_bundle.py toolchain/build/staging-deploy "$GROUP" toolchain "$VERSION" --plugin-id "$PLUGIN_ID" $flag

- name: Publish to the Gradle Plugin Portal
if: github.event_name == 'release'
env:
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
run: |
if [ -z "$ORG_GRADLE_PROJECT_signingInMemoryKey" ]; then
unset ORG_GRADLE_PROJECT_signingInMemoryKey ORG_GRADLE_PROJECT_signingInMemoryKeyPassword
fi
./gradlew :toolchain:publishPlugins --console=plain
49 changes: 33 additions & 16 deletions .github/workflows/test-ios.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
name: iOS e2e

# toolchain#22, iOS: sample's app on an arm64 iOS simulator runs Python with the stdlib and
# toolchain#22, iOS: usage-example's app on an arm64 iOS simulator runs Python with the stdlib and
# Python.framework python-multiplatform puts into the .app, plus this plugin's python/ payload.
# macOS minutes cost ~10x, so this runs only for iOS wiring changes, on develop and by hand, and is
# informational (not a required check). Lead decision, 2026-10-03.
on:
# Checked on develop after merging, not on every pull request (user decision,
# 2026-10-17). A red develop is fixed forward by its own pull request.
push:
branches: [develop]
pull_request:
paths:
- "sample/src/iosMain/**"
- "sample/build.gradle.kts"
- "sample/gradle/**"
- "toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/bundle/**"
- ".github/workflows/test-ios.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ios-e2e-${{ github.ref }}
cancel-in-progress: true
cancel-in-progress: false

jobs:
ios-e2e:
Expand All @@ -48,7 +43,7 @@ jobs:
- uses: actions/cache@v4
with:
path: ~/.konan
key: konan-${{ runner.os }}-${{ hashFiles('sample/gradle/libs.versions.toml') }}
key: konan-${{ runner.os }}-${{ hashFiles('usage-example/gradle/libs.versions.toml') }}

- name: Publish packpack, the toolchain plugin and python-multiplatform (iOS simulator)
run: |
Expand All @@ -65,10 +60,10 @@ jobs:
run: |
set -euo pipefail
pmd="$GITHUB_WORKSPACE/.ci/python-multiplatform"
slice="$pmd/python-multiplatform/build/python-standalone/extracted/3.14.7/ios/Python.xcframework/ios-arm64_x86_64-simulator"
slice="$pmd/python-multiplatform/build/python-standalone/extracted/3.15.0/ios/Python.xcframework/ios-arm64_x86_64-simulator"
# TODO(python-multiplatform#90): PYTHON_MULTIPLATFORM_DIR and the -framework Python link flags
# are interim; the published plugin will stage Python.xcframework and its link options.
xcodebuild -project sample/src/iosMain/app.xcodeproj -target iosApp \
xcodebuild -project usage-example/src/iosMain/app.xcodeproj -target iosApp \
-sdk iphonesimulator -configuration Debug \
ARCHS=arm64 ONLY_ACTIVE_ARCH=YES CODE_SIGNING_ALLOWED=NO \
SYMROOT="$GITHUB_WORKSPACE/build-ios" \
Expand All @@ -90,17 +85,39 @@ jobs:
test -d "$app/Frameworks/Python.framework" || { echo "no Python.framework in the app"; exit 1; }

- name: Run the app's Python on a simulator
# The app writes its report to Documents/python-smoke.txt (PythonSmoke.ios.kt): reading the console
# with `launch --console-pty` lost it three times when the app had exited before the pty attached
# (toolchain#92). Wait until SpringBoard runs, launch without a pty, poll for the file, and relaunch
# once if it never appears.
run: |
set -euo pipefail
udid=$(xcrun simctl list devices available -j | python3 -c 'import json,sys; d=json.load(sys.stdin)["devices"]; print(next(x["udid"] for k,v in d.items() if "iOS" in k for x in v if x["name"].startswith("iPhone")))')
xcrun simctl boot "$udid" || true
xcrun simctl bootstatus "$udid" -b
for i in $(seq 1 60); do
xcrun simctl spawn "$udid" launchctl list 2>/dev/null | grep -q com.apple.SpringBoard && break
sleep 5
done
xcrun simctl install "$udid" "$APP"
bundle=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Info.plist")
out=$(xcrun simctl launch --console-pty --terminate-running-process "$udid" "$bundle" --python-smoke 2>&1) || true
echo "$out"
printf '%s\n' "$out" | grep -q 'PYTHON_SMOKE_OK ' || { echo "no PYTHON_SMOKE_OK line"; exit 1; }
stdlib=$(printf '%s\n' "$out" | sed -n 's/.*PYTHON_SMOKE_STDLIB //p' | tr -d '\r')
report=""
for attempt in 1 2; do
data=$(xcrun simctl get_app_container "$udid" "$bundle" data)
rm -f "$data/Documents/python-smoke.txt"
xcrun simctl launch --terminate-running-process "$udid" "$bundle" --python-smoke || true
for i in $(seq 1 60); do
[ -s "$data/Documents/python-smoke.txt" ] && break
sleep 5
done
if [ -s "$data/Documents/python-smoke.txt" ]; then
report=$(cat "$data/Documents/python-smoke.txt")
break
fi
echo "attempt $attempt: no report after 300 s"
done
echo "$report"
printf '%s\n' "$report" | grep -q 'PYTHON_SMOKE_OK ' || { echo "no PYTHON_SMOKE_OK line"; exit 1; }
stdlib=$(printf '%s\n' "$report" | sed -n 's/.*PYTHON_SMOKE_STDLIB //p' | tr -d '\r')
case "$stdlib" in
*.app/python-multiplatform-home/*) echo "stdlib from the app: $stdlib" ;;
*) echo "stdlib not from the app: '$stdlib'"; exit 1 ;;
Expand Down
Loading