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
8 changes: 8 additions & 0 deletions .github/scripts/release/central-bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 9 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}

Expand Down
50 changes: 43 additions & 7 deletions docs/guide/getting-started.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<div class="page-head"><div class="wrap">
<div class="eyebrow"><span data-lang="en">Getting started</span><span data-lang="ko">시작하기</span></div>
<h1><span data-lang="en">From clone to a running sample</span><span data-lang="ko">클론부터 샘플 실행까지</span></h1>
<p><span data-lang="en">No release is published yet, so you build from source. The sample app is the shortest path to seeing both directions work.</span><span data-lang="ko">아직 배포된 릴리스가 없으므로 소스에서 빌드한다. 두 방향이 동작하는 것을 보는 가장 짧은 길은 샘플 앱이다.</span></p>
<p><span data-lang="en">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.</span><span data-lang="ko">릴리스는 Maven Central 에 있다(04 절). 라이브러리 자체를 고치려면 소스에서 빌드한다. 두 방향이 동작하는 것을 보는 가장 짧은 길은 샘플 앱이다.</span></p>
</div></div>
<main><div class="wrap"><div class="prose">
<h2><span class="num">01</span><span data-lang="en">Requirements</span><span data-lang="ko">준비물</span></h2>
Expand Down Expand Up @@ -70,25 +70,61 @@ <h2><span class="num">03</span><span data-lang="en">Your first call</span><span
<div class="note"><p><span data-lang="en">There is no <code>close()</code> in that code, and none is missing: every wrapper releases its Python reference when the Kotlin object is collected.</span><span data-lang="ko">위 코드에는 <code>close()</code> 가 없고, 빠진 것도 아니다. 모든 래퍼는 Kotlin 객체가 수거될 때 Python 참조를 놓는다.</span></p></div>

<h2><span class="num">04</span><span data-lang="en">Use it from Maven Central</span><span data-lang="ko">Maven Central 에서 쓰기</span></h2>
<p><span data-lang="en">Consumers must build with Kotlin <code>2.4.20-Beta2</code>, the version the artifacts are compiled with. A desktop app that Python calls back into:</span><span data-lang="ko">소비자는 아티팩트가 컴파일된 Kotlin <code>2.4.20-Beta2</code> 로 빌드해야 한다. Python 이 다시 호출하는 데스크톱 앱:</span></p>
<pre translate="no"><code class="language-kotlin">// 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(&quot;multiplatform&quot;)
kotlin(&quot;multiplatform&quot;) version &quot;2.4.20-Beta2&quot;
id(&quot;org.thisisthepy.python.multiplatform.bindings&quot;) version &quot;3.15.0-alpha03&quot;
}
kotlin {
jvm(&quot;desktop&quot;) {
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
binaries { executable { mainClass.set(&quot;app.MainKt&quot;) } }
}
sourceSets.commonMain.dependencies {
implementation(&quot;org.thisisthepy:python-multiplatform:3.15.0-alpha03&quot;)
// Compose hosts (PythonAppView, PythonWidget, PythonLauncher):
// implementation(&quot;org.thisisthepy:python-multiplatform-compose:3.15.0-alpha03&quot;)
}
}
// A KMP executable is otherwise treated as a library and gets no FunctionTable.
pythonBindings { role.set(&quot;app&quot;) }</code></pre>
<pre translate="no"><code class="language-kotlin">// 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()) { &quot;hello $subject&quot; }.joinToString(&quot;, &quot;)
}

fun main() {
Python3.initialize()
// Upcalls: install the generated table, bind the entry points, install the proxies.
python.multiplatform.generated.FunctionTable.installInto()
Python3.exec(
&quot;&quot;&quot;
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})
&quot;&quot;&quot;.trimIndent(),
)
PythonProxySource.install()
Python3.exec(&quot;from app import Greeter\nprint(Greeter('Kotlin').greet(2), flush=True)&quot;)
}</code></pre>
<p><span data-lang="en">The library, the KSP processor and the Gradle plugin share one version, <code>$pythonVersion-$libraryVersionSuffix</code>. A version tag publishes them in one deployment through <code>.github/workflows/maven-central.yml</code>; until a release has been published there, resolve from <code>mavenLocal()</code> after <code>./gradlew publishAllToMavenLocal</code>.</span><span data-lang="ko">라이브러리, KSP 프로세서, Gradle 플러그인은 <code>$pythonVersion-$libraryVersionSuffix</code> 라는 한 버전을 쓴다. 버전 태그를 달면 <code>.github/workflows/maven-central.yml</code> 이 셋을 한 번의 배포로 올린다. 릴리스가 올라가기 전에는 <code>./gradlew publishAllToMavenLocal</code> 뒤에 <code>mavenLocal()</code> 에서 가져온다.</span></p>
<p><span data-lang="en">The library, the KSP processor, the Compose host and the Gradle plugin share one version, <code>$pythonVersion-$libraryVersionSuffix</code>, and are published together on Maven Central; the current release is <code>3.15.0-alpha03</code>. <code>google()</code> is needed for Android targets.</span><span data-lang="ko">라이브러리, KSP 프로세서, Compose 호스트, Gradle 플러그인은 <code>$pythonVersion-$libraryVersionSuffix</code> 라는 한 버전을 쓰고 Maven Central 에 함께 올라간다. 현재 릴리스는 <code>3.15.0-alpha03</code> 이다. Android 타깃에는 <code>google()</code> 이 필요하다.</span></p>

<h2><span class="num">05</span><span data-lang="en">Verify a change</span><span data-lang="ko">변경 검증하기</span></h2>
<pre translate="no"><code class="language-bash">./gradlew :python-multiplatform:compileKotlinAndroidNativeArm64 --console=plain &gt; .tmp/build.log 2&gt;&amp;1; echo &quot;EXIT=$?&quot;
./gradlew :python-multiplatform:desktopTest --rerun --console=plain &gt; .tmp/test.log 2&gt;&amp;1; echo &quot;EXIT=$?&quot;
<pre translate="no"><code class="language-bash">./gradlew :python-multiplatform:compileKotlinAndroidNativeArm64 --console=plain &gt; .cache/build.log 2&gt;&amp;1; echo &quot;EXIT=$?&quot;
./gradlew :python-multiplatform:desktopTest --rerun --console=plain &gt; .cache/test.log 2&gt;&amp;1; echo &quot;EXIT=$?&quot;
./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</code></pre>
Expand Down
4 changes: 2 additions & 2 deletions gradle/central-publishing.gradle.kts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions python-multiplatform-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"))
13 changes: 12 additions & 1 deletion python-multiplatform/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<type>/jniLibs`), so the Sync owns all of it.
val copyAndroidPythonBinaries by tasks.creating(Sync::class) {
dependsOn(
tasks.named("linkAndroidNativeArm64"),
tasks.named("linkAndroidNativeX64"),
Expand Down Expand Up @@ -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)
Expand Down
Loading