01Requirements준비물
@@ -70,25 +70,61 @@03Your first callThere 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 참조를 놓는다.
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 59b6db55..c108f2bb 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 011f9579..353384d4 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 30a722fe..e9f99602 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)