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
68 changes: 39 additions & 29 deletions .github/workflows/publish-gradle-plugin.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
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).
# Publishing a GitHub Release tagged `v<version>` (the plugin's tags, #71) signs and stages the plugin
# (`org.thisisthepy.python.multiplatform:toolchain`) and its marker
# (`org.thisisthepy.python.multiplatform:org.thisisthepy.python.multiplatform.gradle.plugin`), checks
# the bundle against Maven Central's requirements (.github/scripts/maven/check_central_bundle.py --plugin-id)
# and uploads it to the Sonatype Central Portal as USER_MANAGED: the Portal validates it and waits, and
# a person presses Publish there. A manual run does everything except the upload (#99, #116).
#
# 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
# Nothing can upload until the user has created the `maven-central` environment (restricted to
# `v*` tags) with its four secrets and published such a release; without a secret the job stops
# and names it. The plugin depends on packpack, which consumers resolve from Maven Central, so the
# job also stops while gradle.properties' packpackVersion is not on Central. docs/SPEC.md §1.19.
on:
release:
Expand All @@ -26,14 +29,14 @@ jobs:
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' || '' }}
environment: ${{ github.event_name == 'release' && 'maven-central' || '' }}
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
- name: The tag names the version; packpack is on Central; the version is new on Central
run: |
prop() { sed -n "s/^$1=//p" gradle.properties; }
group=$(prop mavenGroup); version=$(prop toolchainVersion); plugin=$(prop pluginId)
Expand All @@ -47,27 +50,28 @@ jobs:
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
pom="https://repo1.maven.org/maven2/${group//.//}/toolchain/$version/toolchain-$version.pom"
if curl -fsS -o /dev/null "$pom"; then fail "$group:toolchain:$version is already on Maven Central; bump toolchainVersion"; fi
marker="https://repo1.maven.org/maven2/${plugin//.//}/$plugin.gradle.plugin/$version/$plugin.gradle.plugin-$version.pom"
if curl -fsS -o /dev/null "$marker"; then fail "$plugin $version marker is already on Maven Central; 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 }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
CENTRAL_PORTAL_USERNAME: ${{ secrets.CENTRAL_PORTAL_USERNAME }}
CENTRAL_PORTAL_PASSWORD: ${{ secrets.CENTRAL_PORTAL_PASSWORD }}
run: |
missing=""
for name in GRADLE_PUBLISH_KEY GRADLE_PUBLISH_SECRET; do
for name in SIGNING_KEY SIGNING_KEY_PASSWORD CENTRAL_PORTAL_USERNAME CENTRAL_PORTAL_PASSWORD; 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
echo "::error::missing secrets in the maven-central environment:$missing (docs/SPEC.md §1.19)"; exit 1
fi
echo "::notice::dry run without:$missing; nothing is uploaded"
echo "::notice::dry run without:$missing; the bundle is built unsigned and 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:
Expand All @@ -88,33 +92,39 @@ jobs:
(cd "$RUNNER_TEMP/pypackpack" && ./gradlew :packpack:publishToMavenLocal --console=plain)
fi

- name: Stage the plugin and its marker (signed when a key is there)
- name: Stage the bundle (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
./gradlew :toolchain:centralBundle --console=plain

- name: The plugin and its marker meet the requirements
- name: The plugin and its marker meet Central's 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
- uses: actions/upload-artifact@v4
with:
name: central-bundle-${{ env.VERSION }}
path: toolchain/build/central/central-bundle-${{ env.VERSION }}.zip

- name: Upload to the Central Portal (USER_MANAGED; press Publish in the 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 }}
CENTRAL_PORTAL_USERNAME: ${{ secrets.CENTRAL_PORTAL_USERNAME }}
CENTRAL_PORTAL_PASSWORD: ${{ secrets.CENTRAL_PORTAL_PASSWORD }}
run: |
if [ -z "$ORG_GRADLE_PROJECT_signingInMemoryKey" ]; then
unset ORG_GRADLE_PROJECT_signingInMemoryKey ORG_GRADLE_PROJECT_signingInMemoryKeyPassword
fi
./gradlew :toolchain:publishPlugins --console=plain
token=$(printf '%s:%s' "$CENTRAL_PORTAL_USERNAME" "$CENTRAL_PORTAL_PASSWORD" | base64 -w0)
id=$(curl --fail-with-body -sS -X POST \
-H "Authorization: Bearer $token" \
-F "bundle=@toolchain/build/central/central-bundle-$VERSION.zip" \
"https://central.sonatype.com/api/v1/publisher/upload?publishingType=USER_MANAGED&name=toolchain-$VERSION")
echo "Central Portal deployment: $id"
echo "::notice::uploaded as deployment $id; check it at https://central.sonatype.com/publishing/deployments and press Publish"
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,29 @@ owns the Gradle vocabulary in between.

## 🚀 Quick start

> `toolchain` is pre-release and published to Maven Local only. Publish `pypackpack`'s `packpack`
> artifact and this plugin locally first, and have `uv` on your `PATH`.
> `toolchain` is published to Maven Central (with its Gradle plugin marker, so add `mavenCentral()` to
> `pluginManagement.repositories` in `settings.gradle.kts`). While pre-release, publish `pypackpack`'s
> `packpack` artifact and this plugin to `mavenLocal()` first, and have `uv` on your `PATH`.

```shell
# in pypackpack: ./gradlew :packpack:publishToMavenLocal
./gradlew :toolchain:publishToMavenLocal
```

```kotlin
// settings.gradle.kts
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
mavenLocal() // while using an unreleased local build
}
}
```

| Coordinates | What |
|---|---|
| plugin id `org.thisisthepy.python.multiplatform` | the Gradle plugin; apply it by id (unchanged) |
| plugin id `org.thisisthepy.python.multiplatform` | the Gradle plugin; resolved via its marker on Maven Central |
| `org.thisisthepy.python.multiplatform:toolchain` | the plugin's implementation artifact |
| PyPI `toolchain-lite` | `tcl`, the command line for Python users (`uv tool install toolchain-lite`) |

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/getting-started.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ <h2><span data-lang="en">1. Prerequisites</span><span data-lang="ko">1. 준비

# in this repository
./gradlew :toolchain:publishToMavenLocal</code></pre>
<div class="note"><p data-lang="en">The plugin is pre-release and resolved from <code>mavenLocal()</code>. Add <code>mavenLocal()</code> to <code>pluginManagement.repositories</code> in your <code>settings.gradle.kts</code>.</p><p data-lang="ko">플러그인은 출시 전이며 <code>mavenLocal()</code> 에서 받습니다. <code>settings.gradle.kts</code> 의 <code>pluginManagement.repositories</code> 에 <code>mavenLocal()</code> 을 추가하세요.</p></div>
<div class="note"><p data-lang="en">The plugin and its marker are distributed through Maven Central (with <code>mavenLocal()</code> as the fallback while pre-release). Put <code>mavenCentral()</code> in <code>pluginManagement.repositories</code> in your <code>settings.gradle.kts</code> (plus <code>mavenLocal()</code> for local builds).</p><p data-lang="ko">플러그인과 마커는 Maven Central 로 배포됩니다(출시 전에는 <code>mavenLocal()</code> 로 대체). <code>settings.gradle.kts</code> 의 <code>pluginManagement.repositories</code> 에 <code>mavenCentral()</code> 을 두세요(로컬 빌드를 쓸 때는 <code>mavenLocal()</code> 도 함께).</p></div>
</section>

<section>
Expand Down
18 changes: 15 additions & 3 deletions docs/locale/README_ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,29 @@ Python 을 내장한 Kotlin Multiplatform 앱에는 맞물려 돌아가야 하

## 🚀 빠른 시작

> `toolchain` 은 정식 출시 전이며 Maven Local 에만 배포됩니다. `pypackpack` 의 `packpack`
> 아티팩트와 이 플러그인을 먼저 로컬에 배포하고, `PATH` 에 `uv` 가 있어야 합니다.
> `toolchain` 은 Gradle 플러그인 마커와 함께 Maven Central 에 배포됩니다(`settings.gradle.kts` 의
> `pluginManagement.repositories` 에 `mavenCentral()` 을 두세요). 정식 출시 전에는 `pypackpack` 의
> `packpack` 아티팩트와 이 플러그인을 먼저 `mavenLocal()` 에 배포하고, `PATH` 에 `uv` 가 있어야 합니다.

```shell
# pypackpack 에서: ./gradlew :packpack:publishToMavenLocal
./gradlew :toolchain:publishToMavenLocal
```

```kotlin
// settings.gradle.kts
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
mavenLocal() // 출시 전 로컬 빌드를 쓸 때
}
}
```

| 좌표 | 무엇 |
|---|---|
| 플러그인 id `org.thisisthepy.python.multiplatform` | Gradle 플러그인; id 로 적용 (변경 없음) |
| 플러그인 id `org.thisisthepy.python.multiplatform` | Gradle 플러그인; Maven Central 의 마커로 해석 |
| `org.thisisthepy.python.multiplatform:toolchain` | 플러그인 구현 아티팩트 |
| PyPI `toolchain-lite` | Python 사용자를 위한 명령줄 도구 `tcl` (`uv tool install toolchain-lite`) |

Expand Down
32 changes: 22 additions & 10 deletions toolchain/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ plugins {
`maven-publish`
`java-gradle-plugin`
signing
// The Gradle Plugin Portal's publishing (`publishPlugins`); it also adds the sources and javadoc
// jars the portal and Maven Central require (toolchain#97). Nothing runs it here.
id("com.gradle.plugin-publish") version "1.3.1"
// The native `tcl` (toolchain-lite) that the PyPI wheels carry; see the cli source sets below.
id("org.graalvm.buildtools.native") version "0.10.6"
}

// Maven Central requires `-sources.jar` and `-javadoc.jar` beside the main jar (toolchain#97, #116).
java {
withSourcesJar()
withJavadocJar()
}

// packpack's coordinate, from gradle.properties (#99).
val packpack = "${providers.gradleProperty("packpackModule").get()}:${providers.gradleProperty("packpackVersion").get()}"

Expand Down Expand Up @@ -64,8 +67,6 @@ dependencies {
}

gradlePlugin {
website.set("https://thisisthepy.github.io/toolchain/")
vcsUrl.set("https://github.com/thisisthepy/toolchain")
plugins {
create("toolchain") {
// The id comes from gradle.properties (#99); the class is a Kotlin package, not a coordinate.
Expand All @@ -75,7 +76,6 @@ gradlePlugin {
description =
"Python in Kotlin Multiplatform apps: dependencies per target, bundling and staging of the " +
"app's Python, through pypackpack."
tags.set(listOf("python", "kotlin-multiplatform", "android", "ios", "packaging"))
}
}
}
Expand Down Expand Up @@ -114,8 +114,8 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach
// plugin id (resolved via the marker) and get the jar through `pluginMaven` -- so it was pure
// duplication, not an intentional second artifact. Removed rather than re-coordinated: there was
// no second artifact to keep.
// Maven Central and the Plugin Portal read the same POM fields (toolchain#97); both publications, the
// plugin's jar (`pluginMaven`) and its marker, carry them.
// Both publications, the plugin's jar (`pluginMaven`) and its marker (`<id>:<id>.gradle.plugin`),
// carry the POM fields Maven Central requires (toolchain#97, #116).
publishing {
publications.withType<MavenPublication>().configureEach {
pom {
Expand Down Expand Up @@ -153,11 +153,23 @@ publishing {
}
}

// The upload format of the Central Portal (https://central.sonatype.org/publish/publish-portal-upload/):
// the staged repository layout (both the plugin artifact and its `.gradle.plugin` marker), zipped,
// without Gradle's maven-metadata files. Built, never uploaded here.
tasks.register<Zip>("centralBundle") {
group = "publishing"
description = "Zips build/staging-deploy into the bundle the Sonatype Central Portal accepts."
dependsOn("publishAllPublicationsToStagingRepository")
from(layout.buildDirectory.dir("staging-deploy")) { exclude("**/maven-metadata*") }
archiveFileName.set("central-bundle-${project.version}.zip")
destinationDirectory.set(layout.buildDirectory.dir("central"))
}

// Signs only when a key is given (`-PsigningInMemoryKey`, `-PsigningInMemoryKeyPassword`, or the
// ORG_GRADLE_PROJECT_ environment variables of the same names), as pypackpack does.
// ORG_GRADLE_PROJECT_ environment variables of the same names), as pypackpack does, so a local
// `publishToMavenLocal` works without one.
signing {
val key = providers.gradleProperty("signingInMemoryKey").orNull
// plugin-publish signs every publication once `signing` is applied; without a key, skip instead of failing.
isRequired = key != null
if (key != null) {
useInMemoryPgpKeys(key, providers.gradleProperty("signingInMemoryKeyPassword").orNull)
Expand Down
28 changes: 15 additions & 13 deletions usage-example/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
// usage-example/ is its own Gradle build (toolchain#22, #62), run with the repository's wrapper:
// ./gradlew -p usage-example …. It consumes python-multiplatform, which is built with Kotlin 2.4, so it uses
// that toolchain (gradle/libs.versions.toml here); the plugin in ../toolchain stays on
// its own Kotlin for its consumers. Both plugins and the
// python-multiplatform library come from mavenLocal: publish them first (see AGENTS.md §16).
// its own Kotlin for its consumers. Plugins and libraries resolve from Maven Central once published,
// and fall back to mavenLocal while unreleased (publish them locally first; see AGENTS.md §16).
pluginManagement {
repositories {
mavenLocal {
content {
excludeGroupAndSubgroups("org.thisisthepy.compose")
excludeGroupAndSubgroups("org.thisisthepy.kotlin")
}
}
google {
mavenContent {
includeGroupAndSubgroups("androidx")
Expand All @@ -28,17 +22,18 @@ pluginManagement {
}
}
gradlePluginPortal()
}
}

dependencyResolutionManagement {
repositories {
mavenLocal {
content {
includeGroupAndSubgroups("org.thisisthepy")
excludeGroupAndSubgroups("org.thisisthepy.compose")
excludeGroupAndSubgroups("org.thisisthepy.kotlin")
}
}
}
}

dependencyResolutionManagement {
repositories {
google {
mavenContent {
includeGroupAndSubgroups("androidx")
Expand All @@ -55,6 +50,13 @@ dependencyResolutionManagement {
mavenPom()
}
}
mavenLocal {
content {
includeGroupAndSubgroups("org.thisisthepy")
excludeGroupAndSubgroups("org.thisisthepy.compose")
excludeGroupAndSubgroups("org.thisisthepy.kotlin")
}
}
}
}

Expand Down