From ac5e0ddc1253680f12bdfee636ab2bb5205de651 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 7 Nov 2026 11:55:13 +0900 Subject: [PATCH] Publish python-multiplatform-compose to Maven Central; drop stale Android libs; runnable getting-started (#379) - python-multiplatform-compose applies central-publishing (common, android, desktop) and joins publishAll* tasks. - copyAndroidPythonBinaries is a Sync; the link task removes stale libmultiplatform_python*.so; central-bundle.sh fails on an AAR that carries a libpython for another major.minor. - getting-started section 04: Kotlin version, desktop target, pythonBindings role app, upcall bootstrap, compose dependency; Central instead of mavenLocal; .tmp/ -> .cache/. --- .github/scripts/release/central-bundle.sh | 8 +++ build.gradle.kts | 15 +++--- docs/guide/getting-started.html | 50 ++++++++++++++++--- gradle/central-publishing.gradle.kts | 4 +- python-multiplatform-compose/build.gradle.kts | 9 ++++ python-multiplatform/build.gradle.kts | 13 ++++- 6 files changed, 83 insertions(+), 16 deletions(-) diff --git a/.github/scripts/release/central-bundle.sh b/.github/scripts/release/central-bundle.sh index 55e8f8f2a..84e0b9585 100755 --- a/.github/scripts/release/central-bundle.sh +++ b/.github/scripts/release/central-bundle.sh @@ -48,6 +48,14 @@ while IFS= read -r pom; do [[ -e "$dir/$base-sources.jar" ]] || { echo "error: $art has no sources jar" >&2; fail=1; } fi done < <(find "$staging" -name '*.pom') +# An AAR must carry the native libraries of this pythonVersion only. A Copy-staged jniLibs tree once +# kept libpython3.14.so beside libpython3.15.so after a version bump. libpython3.so (the stable-ABI +# stub) carries no minor version and is allowed. +py_mm="$(sed -nE 's/^pythonVersion=([0-9]+\.[0-9]+).*$/\1/p' gradle.properties)" +while IFS= read -r aar; do + stale="$(unzip -Z1 "$aar" | grep -E '(^|/)lib(multiplatform_)?python[0-9]+\.[0-9]+[^/]*\.so$' | grep -vE "(^|/)lib(multiplatform_)?python${py_mm//./\\.}(t)?\.so$" || true)" + [[ -z "$stale" ]] || { echo "error: $aar carries native libraries for another python than $py_mm:" >&2; echo "$stale" >&2; fail=1; } +done < <(find "$staging" -name '*.aar') if $signed; then while IFS= read -r f; do case "$f" in *.asc|*.md5|*.sha1|*.sha256|*.sha512) continue ;; esac diff --git a/build.gradle.kts b/build.gradle.kts index cad457f0d..c1a07f833 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,27 +34,30 @@ gradle.projectsEvaluated { // (see `pluginManagement { includeBuild(...) }` in settings.gradle.kts), so its tasks never join // the root task graph -- `./gradlew tasks --all` from here lists none of them, and a coordinator // publishing only what the root build shows would silently skip it. This task does not change how -// any of the three components publish (each already has its own `maven-publish` config); it only -// gives the root build one entry point that reaches all three. +// any component publishes (each already has its own `maven-publish` config); it only gives the root +// build one entry point that reaches all of them (the Compose host included). tasks.register("publishAllToMavenLocal") { group = "publishing" - description = "Publishes python-multiplatform, python-multiplatform-ksp, and " + + description = "Publishes python-multiplatform, python-multiplatform-ksp, python-multiplatform-compose and " + "python-multiplatform-gradle-plugin (including its plugin-marker publication) to mavenLocal()." dependsOn(":python-multiplatform:publishToMavenLocal") dependsOn(":python-multiplatform-ksp:publishToMavenLocal") + dependsOn(":python-multiplatform-compose:publishToMavenLocal") dependsOn(gradle.includedBuild("python-multiplatform-gradle-plugin").task(":publishToMavenLocal")) } -// Maven Central (ROADMAP 15.1): one entry point that fills build/central-staging with all three -// components. Signed when MAVEN_GPG_PRIVATE_KEY is set (gradle/central-publishing.gradle.kts). +// Maven Central (ROADMAP 15.1): one entry point that fills build/central-staging with every +// component (library, KSP processor, Compose host, Gradle plugin). Signed when MAVEN_GPG_PRIVATE_KEY +// is set (gradle/central-publishing.gradle.kts). // It does not clean the directory (the plugin build is another build, so ordering cannot reach // it): .github/scripts/release/central-bundle.sh empties it first. tasks.register("publishAllToCentralStaging") { group = "publishing" - description = "Publishes python-multiplatform, python-multiplatform-ksp and " + + description = "Publishes python-multiplatform, python-multiplatform-ksp, python-multiplatform-compose and " + "python-multiplatform-gradle-plugin (with its plugin marker) to build/central-staging." dependsOn(":python-multiplatform:publishAllToCentralStaging") dependsOn(":python-multiplatform-ksp:publishAllToCentralStaging") + dependsOn(":python-multiplatform-compose:publishAllToCentralStaging") dependsOn(gradle.includedBuild("python-multiplatform-gradle-plugin").task(":publishAllToCentralStaging")) } diff --git a/docs/guide/getting-started.html b/docs/guide/getting-started.html index 47a73045c..976e6e60b 100644 --- a/docs/guide/getting-started.html +++ b/docs/guide/getting-started.html @@ -34,7 +34,7 @@
Getting started시작하기

From clone to a running sample클론부터 샘플 실행까지

-

No release is published yet, so you build from source. The sample app is the shortest path to seeing both directions work.아직 배포된 릴리스가 없으므로 소스에서 빌드한다. 두 방향이 동작하는 것을 보는 가장 짧은 길은 샘플 앱이다.

+

Releases are on Maven Central (section 04). To work on the library itself, build from source: the sample app is the shortest path to seeing both directions work.릴리스는 Maven Central 에 있다(04 절). 라이브러리 자체를 고치려면 소스에서 빌드한다. 두 방향이 동작하는 것을 보는 가장 짧은 길은 샘플 앱이다.

01Requirements준비물

@@ -70,25 +70,61 @@

03Your first call

There is no close() in that code, and none is missing: every wrapper releases its Python reference when the Kotlin object is collected.위 코드에는 close() 가 없고, 빠진 것도 아니다. 모든 래퍼는 Kotlin 객체가 수거될 때 Python 참조를 놓는다.

04Use it from Maven CentralMaven Central 에서 쓰기

+

Consumers must build with Kotlin 2.4.20-Beta2, the version the artifacts are compiled with. A desktop app that Python calls back into:소비자는 아티팩트가 컴파일된 Kotlin 2.4.20-Beta2 로 빌드해야 한다. Python 이 다시 호출하는 데스크톱 앱:

// settings.gradle.kts
 pluginManagement { repositories { mavenCentral(); gradlePluginPortal() } }
-dependencyResolutionManagement { repositories { mavenCentral() } }
+dependencyResolutionManagement { repositories { mavenCentral(); google() } }
 
-// build.gradle.kts of a Kotlin Multiplatform module
+// build.gradle.kts of a Kotlin Multiplatform app
 plugins {
-    kotlin("multiplatform")
+    kotlin("multiplatform") version "2.4.20-Beta2"
     id("org.thisisthepy.python.multiplatform.bindings") version "3.15.0-alpha03"
 }
 kotlin {
+    jvm("desktop") {
+        @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
+        binaries { executable { mainClass.set("app.MainKt") } }
+    }
     sourceSets.commonMain.dependencies {
         implementation("org.thisisthepy:python-multiplatform:3.15.0-alpha03")
+        // Compose hosts (PythonAppView, PythonWidget, PythonLauncher):
+        // implementation("org.thisisthepy:python-multiplatform-compose:3.15.0-alpha03")
     }
+}
+// A KMP executable is otherwise treated as a library and gets no FunctionTable.
+pythonBindings { role.set("app") }
+
// src/desktopMain/kotlin/app/Main.kt
+package app
+
+import python.multiplatform.ffi.Python3
+import python.multiplatform.ffi.upcall.PythonProxySource
+import python.native.ffi.UpcallStub
+
+class Greeter(val subject: String) {
+    fun greet(times: Long): String = List(times.toInt()) { "hello $subject" }.joinToString(", ")
+}
+
+fun main() {
+    Python3.initialize()
+    // Upcalls: install the generated table, bind the entry points, install the proxies.
+    python.multiplatform.generated.FunctionTable.installInto()
+    Python3.exec(
+        """
+        import ctypes
+        _pm_resolve = ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_char_p)(${UpcallStub.resolveHandleStubAddr})
+        _pm_invoke = ctypes.CFUNCTYPE(ctypes.py_object, ctypes.c_long, ctypes.py_object)(${UpcallStub.invokeWithArgsStubAddr})
+        _pm_release = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_long)(${UpcallStub.releaseObjectStubAddr})
+        _pm_cancel = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_long)(${UpcallStub.cancelCallStubAddr})
+        """.trimIndent(),
+    )
+    PythonProxySource.install()
+    Python3.exec("from app import Greeter\nprint(Greeter('Kotlin').greet(2), flush=True)")
 }
-

The library, the KSP processor and the Gradle plugin share one version, $pythonVersion-$libraryVersionSuffix. A version tag publishes them in one deployment through .github/workflows/maven-central.yml; until a release has been published there, resolve from mavenLocal() after ./gradlew publishAllToMavenLocal.라이브러리, KSP 프로세서, Gradle 플러그인은 $pythonVersion-$libraryVersionSuffix 라는 한 버전을 쓴다. 버전 태그를 달면 .github/workflows/maven-central.yml 이 셋을 한 번의 배포로 올린다. 릴리스가 올라가기 전에는 ./gradlew publishAllToMavenLocal 뒤에 mavenLocal() 에서 가져온다.

+

The library, the KSP processor, the Compose host and the Gradle plugin share one version, $pythonVersion-$libraryVersionSuffix, and are published together on Maven Central; the current release is 3.15.0-alpha03. google() is needed for Android targets.라이브러리, KSP 프로세서, Compose 호스트, Gradle 플러그인은 $pythonVersion-$libraryVersionSuffix 라는 한 버전을 쓰고 Maven Central 에 함께 올라간다. 현재 릴리스는 3.15.0-alpha03 이다. Android 타깃에는 google() 이 필요하다.

05Verify a change변경 검증하기

-
./gradlew :python-multiplatform:compileKotlinAndroidNativeArm64 --console=plain > .tmp/build.log 2>&1; echo "EXIT=$?"
-./gradlew :python-multiplatform:desktopTest --rerun --console=plain > .tmp/test.log 2>&1; echo "EXIT=$?"
+  
./gradlew :python-multiplatform:compileKotlinAndroidNativeArm64 --console=plain > .cache/build.log 2>&1; echo "EXIT=$?"
+./gradlew :python-multiplatform:desktopTest --rerun --console=plain > .cache/test.log 2>&1; echo "EXIT=$?"
 ./gradlew :python-multiplatform-ksp:fixtures:app:desktopTest --rerun
 ./gradlew :python-multiplatform-compose:fixtures:compose:desktopTest --rerun
 ./gradlew :python-multiplatform-gradle-plugin-fixtures:artifact:desktopTest --rerun
diff --git a/gradle/central-publishing.gradle.kts b/gradle/central-publishing.gradle.kts index 59b6db55d..c108f2bb9 100644 --- a/gradle/central-publishing.gradle.kts +++ b/gradle/central-publishing.gradle.kts @@ -1,5 +1,5 @@ -// Maven Central publishing, applied by the three published components (the library, the KSP -// processor and the Gradle plugin, which is an included build): +// Maven Central publishing, applied by the published components (the library, the KSP processor, +// the Compose host and the Gradle plugin, which is an included build): // // apply(from = rootProject.file("gradle/central-publishing.gradle.kts")) // root build // apply(from = rootDir.resolve("../gradle/central-publishing.gradle.kts")) // plugin build diff --git a/python-multiplatform-compose/build.gradle.kts b/python-multiplatform-compose/build.gradle.kts index 011f95798..353384d43 100644 --- a/python-multiplatform-compose/build.gradle.kts +++ b/python-multiplatform-compose/build.gradle.kts @@ -15,10 +15,14 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.jetpack.compose) alias(libs.plugins.compose.compiler) + id("maven-publish") } kotlin { androidTarget { + // Without a named variant KMP creates no Android publication (ROADMAP 15d, same as the library). + publishLibraryVariants("release") + @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { jvmTarget.set(JvmTarget.JVM_17) @@ -52,3 +56,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } } + +// Published to Maven Central beside the library as org.thisisthepy:python-multiplatform-compose, at the +// root's one version (#70): POM fields, sources/javadoc jars, centralStaging repository and in-memory +// signing come from the shared script (ROADMAP 15.1). +apply(from = rootProject.file("gradle/central-publishing.gradle.kts")) diff --git a/python-multiplatform/build.gradle.kts b/python-multiplatform/build.gradle.kts index 30a722feb..e9f99602c 100644 --- a/python-multiplatform/build.gradle.kts +++ b/python-multiplatform/build.gradle.kts @@ -1067,7 +1067,11 @@ kotlin { } afterEvaluate { val abiList = listOf("arm64-v8a", "x86_64") - val copyAndroidPythonBinaries by tasks.creating(Copy::class) { + // A Sync, not a Copy: a Copy never removes what it stopped copying, so a jniLibs tree + // staged under an older pythonVersion (libpython3.14.so beside libpython3.15.so) kept + // shipping in the AAR. Nothing else writes this directory (the link task stages into + // `$androidBuildDir//jniLibs`), so the Sync owns all of it. + val copyAndroidPythonBinaries by tasks.creating(Sync::class) { dependsOn( tasks.named("linkAndroidNativeArm64"), tasks.named("linkAndroidNativeX64"), @@ -1301,7 +1305,14 @@ kotlin { // outputFile. That is why a changed .def or source produced an APK with // a stale library and an UnsatisfiedLinkError that looked like a code // bug -- the fix had to be built twice for the second run to pick it up. + // A library linked under an older pythonVersion + // (libmultiplatform_python3.14.so) is removed first, or it keeps + // shipping in the AAR beside the current one. doLast { + val current = outputFile.get().name + dest.listFiles { f -> + f.name.startsWith("libmultiplatform_python") && f.name.endsWith(".so") && f.name != current + }?.forEach { it.delete() } copy { from(outputFile) into(dest)