From 73246466fe76ce6e408f6fb7c06523188558834a Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:19:22 +0900 Subject: [PATCH 1/2] CPython 3.15.0: PY3_15_0, 3.15t spelling, wheel tags follow compileSdk (#84) --- .github/workflows/test-ios.yml | 2 +- .github/workflows/test.yml | 8 +-- AGENTS.md | 2 +- PROJECT.md | 2 +- docs/SPEC.md | 22 +++++-- docs/guide/guide-variants.html | 2 +- .../bundle/BuildPythonArtifactTask.kt | 57 ++++++++++++++----- .../multiplatform/toolchain/dsl/DSLCore.kt | 3 +- .../multiplatform/toolchain/dsl/PythonSdk.kt | 7 ++- .../toolchain/dsl/PythonVersion.kt | 20 +++++-- .../bundle/BuildPythonArtifactTaskTest.kt | 20 +++++++ .../toolchain/dsl/PythonSdkTest.kt | 30 ++++++++++ .../toolchain/dsl/PythonVersionTest.kt | 14 ++++- usage-example/build.gradle.kts | 2 +- usage-example/gradle/libs.versions.toml | 2 +- .../src/iosMain/install-python-phase.sh | 2 +- 16 files changed, 157 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test-ios.yml b/.github/workflows/test-ios.yml index 0162026..d42e4ec 100644 --- a/.github/workflows/test-ios.yml +++ b/.github/workflows/test-ios.yml @@ -65,7 +65,7 @@ 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 usage-example/src/iosMain/app.xcodeproj -target iosApp \ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c09c7c..2e43e01 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -117,12 +117,12 @@ jobs: - uses: gradle/actions/setup-gradle@v4 - uses: actions/setup-python@v5 with: - python-version: "3.14" + python-version: "3.15" - uses: astral-sh/setup-uv@v6 # usage-example applies three things from mavenLocal: packpack (through the plugin), the # toolchain plugin, and python-multiplatform's library and bindings plugin. Versions pinned in - # usage-example/gradle/libs.versions.toml (library 3.14.7-alpha01, plugin 3.13.0 until + # usage-example/gradle/libs.versions.toml (library 3.15.0-alpha01, plugin 3.13.0 until # python-multiplatform#70). - name: Publish packpack, the toolchain plugin and python-multiplatform to mavenLocal run: | @@ -178,7 +178,7 @@ jobs: - uses: gradle/actions/setup-gradle@v4 - uses: actions/setup-python@v5 with: - python-version: "3.14" + python-version: "3.15" - uses: astral-sh/setup-uv@v6 - name: Publish packpack, the toolchain plugin and python-multiplatform to mavenLocal @@ -219,7 +219,7 @@ jobs: # python-multiplatform ships and unpacks (PythonBootstrap) plus this plugin's assets/python/ # (example_py and iniconfig installed for x86_64-linux-android). The smoke result goes to logcat. # Green since python-multiplatform#72 (PR #75): the emulator logs PYTHON_SMOKE_OK with the stdlib - # unpacked under /data/data//files/lib/python3.14/. + # unpacked under /data/data//files/lib/python3.15/. android-e2e: runs-on: ubuntu-latest timeout-minutes: 75 diff --git a/AGENTS.md b/AGENTS.md index dc6db5c..ee30c31 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -246,7 +246,7 @@ Prerequisites: `org.thisisthepy.python.multiplatform:packpack:0.1.0` in `~/.m2` `pypackpack` repository), `uv` on `PATH`, and network access: `InstallDependenciesTaskTest` and `InstallerTest` run a real `uv add` against PyPI. `usage-example` additionally needs an Android SDK (`ANDROID_HOME`) and python-multiplatform in `~/.m2`: from a python-multiplatform checkout, -`./gradlew publishAllToMavenLocal` (library `3.14.7-alpha01`, bindings plugin `3.13.0` until its #70). +`./gradlew publishAllToMavenLocal` (library `3.15.0-alpha01`, bindings plugin `3.13.0` until its #70). Run each module separately (rule 8), with output to a file under `.tmp/`: diff --git a/PROJECT.md b/PROJECT.md index 34d4d56..3ff5650 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -16,7 +16,7 @@ Kotlin Multiplatform 앱의 Python 부분을 Gradle `python { }` 블록으로 ## 현재 상태 (2026-10-03, 코드와 테스트를 읽고 판정) **구현 (테스트 있음)** -- `compileSdk` 버전 문자열 파싱 (alpha / rc / normal 채널), 그리고 이름 상수 `PY3_14_7`·`PY3_13_0` +- `compileSdk` 버전 문자열 파싱 (alpha / rc / normal 채널), 그리고 이름 상수 `PY3_15_0`·`PY3_14_7`·`PY3_13_0` (python-multiplatform 이 런타임을 제공하는 버전만). 제공되지 않는 버전 문자열은 `buildPython…` 에서 이유와 함께 실패 - `python { }` 최상위의 `android("android") { }` / `ios { }` / `desktop()` 과 변형 호출 (`androidArm64()` 등) → `pypackpack` 타깃 트리플 매핑, Kotlin 타깃 대조 경고, 최소 SDK 전달. diff --git a/docs/SPEC.md b/docs/SPEC.md index b09ffc6..67d06bc 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -44,16 +44,30 @@ form `pypackpack` accepts. A blank value is a silent skip; a malformed one fails **Status: implemented**. `plugin/dsl/PythonVersion.kt`; `ptest/dsl/PythonVersionTest.kt`. `compileSdk` takes either form the example build file writes, both with `=` (Gradle's Kotlin -assignment overloading on `PythonSdk`): a string, `compileSdk = "3.14.7"`, or a named constant, -`compileSdk = PY3_14_7`. Constants exist only for versions python-multiplatform provides a runtime -for (its pinned `pythonVersion`, 3.14.7, and the older 3.13.0 archive), so the example's +assignment overloading on `PythonSdk`): a string, `compileSdk = "3.15.0"`, or a named constant, +`compileSdk = PY3_15_0`. Constants exist only for versions python-multiplatform provides a runtime +for (its pinned `pythonVersion`, 3.15.0; 3.14.7 and the older 3.13.0 archive are kept), so the example's `PY3_11_9_ALPHA` does not compile (INTENT §4.1). A string resolves to a provided version (`X.Y` takes the newest `X.Y.*`). A string naming a version that is not provided would be built automatically (INTENT §4.1), which is not available yet: the reason fails `buildPython…` (only when there is a package to bundle), not the configuration. +A trailing `t` names the free-threaded build (`compileSdk = "3.15t"`, `"3.15.0t"`); it resolves +against the same published list (python-multiplatform ships both builds of a pinned version) and +there is no free-threaded constant. The `t` never reaches `toReleaseString()`, so the agreement +check with python-multiplatform's `pythonVersion` (§1.12) compares `X.Y.Z` only. **Not checked:** +whether python-multiplatform was built free-threaded (its `freeThreaded` flag is a separate +attribute that toolchain does not read). + +The resolved version is also forwarded to every bundle as pypackpack's +`BundleRequest.pythonVersion` (`3.15.0`, or `3.15.0t`; absent without `compileSdk`), which fixes the +wheel tags (`cp315-cp315`, `cp315-cp315t`). **Status: implemented**. +`ptest/bundle/BuildPythonArtifactTaskTest.kt`, `ptest/dsl/PythonSdkTest.kt`, +`ptest/dsl/PythonVersionTest.kt`. Needs a `packpack` in `~/.m2` built from pypackpack#77 or later +(the same `0.1.0` coordinates; CI publishes it from pypackpack `develop`). + **Status: implemented**. `plugin/dsl/PythonSdk.kt`; `ptest/dsl/PythonSdkTest.kt`, -`ptest/bundle/BuildPythonSdkRejectionTest.kt`; `usage-example` uses `compileSdk = PY3_14_7`. +`ptest/bundle/BuildPythonSdkRejectionTest.kt`; `usage-example` uses `compileSdk = PY3_15_0`. **What `compileSdk` selects (#42).** `compileSdk` selects the Python version of the wheels that go into `python/`: it is the `--python-version` of every per-target install (§1.10, #16). It does diff --git a/docs/guide/guide-variants.html b/docs/guide/guide-variants.html index 0a84b83..99b3ba1 100644 --- a/docs/guide/guide-variants.html +++ b/docs/guide/guide-variants.html @@ -57,7 +57,7 @@

Build types빌드 타입

Only debug and release exist. useCodeMinifier, excludeMetaclass and enableCodePush are release-only: setting them on debug throws.

debug 와 release 만 있습니다. useCodeMinifier, excludeMetaclass, enableCodePush 는 release 전용이며 debug 에 설정하면 예외가 납니다.

compileLevelcompileLevelMeaning의미Today지금
instant (or blank)(또는 빈 값).py only.py 만implemented구현
bytecodedebug: .py + .pyc; release: .pyc onlydebug: .py + .pyc, release: .pyc 만implemented구현
nativenative code only네이티브 코드만planned (pypackpack#19)계획 (pypackpack#19)
mixedyour code as bytecode, libraries native내 코드는 바이트코드, 라이브러리는 네이티브planned (pypackpack#19)계획 (pypackpack#19)
-

bytecode compiles with the Python in a .venv at or above your package directory. installPythonDependencies leaves one there (uv add creates <package>/.venv) once you declare a dependency; otherwise run uv venv --python 3.14 in the package directory. Its minor version must match compileSdk, because a .pyc only loads on the CPython minor version that wrote it; a mismatch fails the variant. Without a declared compileSdk, or a pyvenv.cfg to read, the versions are not compared.

bytecode 는 패키지 디렉터리나 그 위의 .venv 에 있는 Python 으로 컴파일합니다. 의존성을 하나라도 선언하면 installPythonDependencies 가 그것을 만들어 둡니다(uv add 가 <package>/.venv 를 만듭니다). 그렇지 않으면 패키지 디렉터리에서 uv venv --python 3.14 를 실행하세요. 그 마이너 버전은 compileSdk 와 같아야 합니다. .pyc 는 그것을 쓴 CPython 마이너 버전에서만 로드되기 때문이며, 다르면 그 변형이 실패합니다. compileSdk 를 선언하지 않았거나 읽을 pyvenv.cfg 가 없으면 버전을 비교하지 않습니다.

+

bytecode compiles with the Python in a .venv at or above your package directory. installPythonDependencies leaves one there (uv add creates <package>/.venv) once you declare a dependency; otherwise run uv venv --python 3.15 in the package directory. Its minor version must match compileSdk, because a .pyc only loads on the CPython minor version that wrote it; a mismatch fails the variant. Without a declared compileSdk, or a pyvenv.cfg to read, the versions are not compared.

bytecode 는 패키지 디렉터리나 그 위의 .venv 에 있는 Python 으로 컴파일합니다. 의존성을 하나라도 선언하면 installPythonDependencies 가 그것을 만들어 둡니다(uv add 가 <package>/.venv 를 만듭니다). 그렇지 않으면 패키지 디렉터리에서 uv venv --python 3.15 를 실행하세요. 그 마이너 버전은 compileSdk 와 같아야 합니다. .pyc 는 그것을 쓴 CPython 마이너 버전에서만 로드되기 때문이며, 다르면 그 변형이 실패합니다. compileSdk 를 선언하지 않았거나 읽을 pyvenv.cfg 가 없으면 버전을 비교하지 않습니다.

A planned level fails only its own variant's buildPython… task, so ./gradlew packagePython --continue still builds every other variant.

계획 단계의 레벨은 그 변형의 buildPython… 태스크만 실패시키므로, ./gradlew packagePython --continue 는 나머지 변형을 모두 빌드합니다.

diff --git a/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/bundle/BuildPythonArtifactTask.kt b/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/bundle/BuildPythonArtifactTask.kt index f16565c..4a31276 100644 --- a/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/bundle/BuildPythonArtifactTask.kt +++ b/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/bundle/BuildPythonArtifactTask.kt @@ -191,7 +191,11 @@ open class BuildPythonArtifactTask : DefaultTask() { ?.let { throw GradleException(it) } } - val result = bundleWithPackpack(source, target, buildType, bundleDir, resolvedBuildLevel, minSdk, metaDirs, libDirs, versionName, versionCode) + val result = + bundleWithPackpack( + source, target, buildType, bundleDir, resolvedBuildLevel, minSdk, metaDirs, libDirs, + versionName, versionCode, compileSdkVersion?.let { wheelVersionOf(it) }, + ) logger.lifecycle( "Bundled ${result.fileCount} file(s) for Python '$pythonVersion' via packpack's " + "'${result.bundleType.id}' bundler into ${result.outputDir} " + @@ -202,6 +206,35 @@ open class BuildPythonArtifactTask : DefaultTask() { } } +/** The [BundleRequest] [bundleWithPackpack] hands pypackpack; separate so a test can read what is forwarded. */ +fun bundleRequestFor( + packageDir: File, + target: String, + buildType: String, + outputDir: File, + buildLevel: String = "instant", + minSdk: Int? = null, + metaDirs: List = emptyList(), + libDirs: List = emptyList(), + versionName: String? = null, + versionCode: Int? = null, + pythonVersion: String? = null, +): BundleRequest = + BundleRequest( + packageDir = packageDir, + target = target, + buildType = buildType, + buildLevel = buildLevel, + outputDir = outputDir, + overwrite = true, + minSdk = minSdk, + metaDirs = metaDirs, + libDirs = libDirs, + versionName = versionName, + versionCode = versionCode, + pythonVersion = pythonVersion, + ) + /** * The actual delegation to `pypackpack`, factored out of [BuildPythonArtifactTask.buildPython] so * it can be exercised without a Gradle [org.gradle.api.Project] -- see @@ -217,6 +250,9 @@ open class BuildPythonArtifactTask : DefaultTask() { * caller that declares no `python { sourceSets { commonMain { metaDirs(...) } } }`. * @param libDirs forwarded to [BundleRequest.libDirs] verbatim. Empty by default, matching every * caller that declares no `python { sourceSets { commonMain { libDirs(...) } } }`. + * @param pythonVersion forwarded to [BundleRequest.pythonVersion] verbatim (`3.15.0`, `3.15.0t`): the + * app's interpreter, which fixes the wheel tags of the wheel bundlers. `null` (no `compileSdk`) + * leaves pypackpack's own default. */ fun bundleWithPackpack( packageDir: File, @@ -229,20 +265,12 @@ fun bundleWithPackpack( libDirs: List = emptyList(), versionName: String? = null, versionCode: Int? = null, + pythonVersion: String? = null, ): BundleResult { val request = - BundleRequest( - packageDir = packageDir, - target = target, - buildType = buildType, - buildLevel = buildLevel, - outputDir = outputDir, - overwrite = true, - minSdk = minSdk, - metaDirs = metaDirs, - libDirs = libDirs, - versionName = versionName, - versionCode = versionCode, + bundleRequestFor( + packageDir, target, buildType, outputDir, buildLevel, minSdk, metaDirs, libDirs, + versionName, versionCode, pythonVersion, ) return BundlerInterface.create(BundleType.RESOURCE) .bundle(request) @@ -330,3 +358,6 @@ fun bytecodeInterpreterRejection( "Recreate that .venv with Python ${compileSdk.major}.${compileSdk.minor} " + "(`uv venv --python ${compileSdk.major}.${compileSdk.minor}`)." } + +/** [PythonVersion.toWheelVersion] of a resolved `compileSdk` label (`PythonVersion.toString()`). */ +fun wheelVersionOf(compileSdkLabel: String): String = PythonVersion.parse(compileSdkLabel).toWheelVersion() diff --git a/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/DSLCore.kt b/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/DSLCore.kt index 52e0c47..060256f 100644 --- a/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/DSLCore.kt +++ b/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/DSLCore.kt @@ -5,8 +5,9 @@ import javax.inject.Inject open class PythonExtension @Inject constructor(objects: ObjectFactory) { - /** `compileSdk = "3.14.7"` or `compileSdk = PY3_14_7`; see [PythonSdk]. */ + /** `compileSdk = "3.15.0"` or `compileSdk = PY3_15_0` (`"3.15t"` for the free-threaded build); see [PythonSdk]. */ val compileSdk: PythonSdk = PythonSdk() + val PY3_15_0: PythonSdkVersion = PythonSdkVersion.PY3_15_0 val PY3_14_7: PythonSdkVersion = PythonSdkVersion.PY3_14_7 val PY3_13_0: PythonSdkVersion = PythonSdkVersion.PY3_13_0 var localLibraryPath: String? = null diff --git a/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonSdk.kt b/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonSdk.kt index 304c22c..8531e71 100644 --- a/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonSdk.kt +++ b/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonSdk.kt @@ -4,18 +4,19 @@ import org.gradle.api.SupportsKotlinAssignmentOverloading /** * The CPython versions python-multiplatform actually provides a runtime for: its - * `gradle.properties` `pythonVersion` (3.14.7 on 2026-10-03), acquired as described in its + * `gradle.properties` `pythonVersion` (3.15.0 since python-multiplatform#197; 3.14.7 is kept), acquired as described in its * `docs/platforms/python-version-acquisition.md` (python-build-standalone for desktop, python.org * for Android, BeeWare's Python-Apple-support for iOS), plus 3.13.0, the older archive on its * `release` branch's `binary/` that pypackpack's `DefaultBackend.installPython` still downloads. * Add a version here and a constant to [PythonSdkVersion] when python-multiplatform pins one. */ object PythonReleaseServer { - val PUBLISHED: List = listOf("3.14.7", "3.13.0") + val PUBLISHED: List = listOf("3.15.0", "3.14.7", "3.13.0") } /** `compileSdk = PY3_14_7`: a version python-multiplatform provides. Only those exist as constants. */ enum class PythonSdkVersion(val version: String) { + PY3_15_0("3.15.0"), PY3_14_7("3.14.7"), PY3_13_0("3.13.0"), } @@ -76,7 +77,7 @@ fun resolvePythonSdk(sdk: PythonSdk): ResolvedPythonSdk? { .maxByOrNull { it.micro } return if (published != null) { - ResolvedPythonSdk(version = published, fromConstant = sdk.constant != null, rejection = null) + ResolvedPythonSdk(version = published.copy(freeThreaded = parsed.freeThreaded), fromConstant = sdk.constant != null, rejection = null) } else { ResolvedPythonSdk( version = parsed, diff --git a/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonVersion.kt b/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonVersion.kt index dd45d31..fbbddaa 100644 --- a/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonVersion.kt +++ b/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonVersion.kt @@ -24,7 +24,8 @@ enum class PythonReleaseChannel { /** * A parsed CPython version string, in the format `(플러그인예시)build.gradle.kts` actually writes - * (`compileSdk = "3.11.9-alpha"`): `X.Y`, `X.Y.Z`, `X.Y.Z-alpha[N]`, or `X.Y.Z-rc[N]`. + * (`compileSdk = "3.11.9-alpha"`): `X.Y`, `X.Y.Z`, `X.Y.Z-alpha[N]`, or `X.Y.Z-rc[N]`, each with an + * optional trailing `t` for the free-threaded build (`3.15t`). * * Deliberately a different grammar from `pypackpack`'s own `validatePythonVersion` * (`cli/.../CommandExtension.kt`'s `^\d+\.\d+(\.\d+)?$`), which has no channel suffix at all -- @@ -37,10 +38,18 @@ data class PythonVersion( val micro: Int, val channel: PythonReleaseChannel, val serial: Int? = null, + /** The free-threaded build (PEP 703), spelled with a trailing `t`: `3.15t`, `3.15.0t`. */ + val freeThreaded: Boolean = false, ) { /** The `X.Y.Z` form `pypackpack`'s `validatePythonVersion` accepts, channel suffix stripped. */ fun toReleaseString(): String = "$major.$minor.$micro" + /** + * The `X.Y.Z` or `X.Y.Zt` form `pypackpack`'s `BundleRequest.pythonVersion` takes, which fixes + * the wheel tags (`3.15.0` is `cp315-cp315`, `3.15.0t` is `cp315-cp315t`). + */ + fun toWheelVersion(): String = toReleaseString() + if (freeThreaded) "t" else "" + override fun toString(): String = buildString { append(toReleaseString()) @@ -49,10 +58,11 @@ data class PythonVersion( PythonReleaseChannel.RC -> append("-rc${serial ?: ""}") PythonReleaseChannel.NORMAL -> Unit } + if (freeThreaded) append("t") } companion object { - private val PATTERN = Regex("""^(\d+)\.(\d+)(?:\.(\d+))?(?:-(alpha|rc)(\d+)?)?$""") + private val PATTERN = Regex("""^(\d+)\.(\d+)(?:\.(\d+))?(?:-(alpha|rc)(\d+)?)?(t)?$""") /** Throws [IllegalArgumentException] on anything that does not match the format above. */ fun parse(raw: String): PythonVersion { @@ -60,10 +70,10 @@ data class PythonVersion( val match = PATTERN.matchEntire(trimmed) ?: throw IllegalArgumentException( "Invalid Python version '$raw'. Expected 'X.Y', 'X.Y.Z', 'X.Y.Z-alpha[N]', " + - "or 'X.Y.Z-rc[N]'.", + "or 'X.Y.Z-rc[N]', each optionally followed by 't' (free-threaded).", ) - val (majorStr, minorStr, microStr, channelWord, serialStr) = match.destructured + val (majorStr, minorStr, microStr, channelWord, serialStr, threadedStr) = match.destructured val major = majorStr.toInt() val minor = minorStr.toInt() val micro = microStr.toIntOrNull() ?: 0 @@ -76,7 +86,7 @@ data class PythonVersion( else -> PythonReleaseChannel.NORMAL } - return PythonVersion(major, minor, micro, channel, serial) + return PythonVersion(major, minor, micro, channel, serial, threadedStr == "t") } } } diff --git a/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/bundle/BuildPythonArtifactTaskTest.kt b/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/bundle/BuildPythonArtifactTaskTest.kt index 2ded432..b0ee13a 100644 --- a/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/bundle/BuildPythonArtifactTaskTest.kt +++ b/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/bundle/BuildPythonArtifactTaskTest.kt @@ -163,4 +163,24 @@ class BuildPythonArtifactTaskTest { result.outputDir.resolve("python/vendor_pkg/module.py").readText(), ) } + + /** + * `compileSdk` must reach `BundleRequest.pythonVersion` so wheel tags follow the app's + * interpreter (pypackpack#77: 3.15.0 is cp315-cp315, 3.15.0t is cp315-cp315t). Fails to compile + * before `bundleRequestFor` and `wheelVersionOf` exist. + */ + @Test + fun `the compileSdk version is forwarded to packpack's BundleRequest pythonVersion`() { + val request = bundleRequestFor( + packageDir = fixturePackageDir(), + target = "aarch64-apple-darwin", + buildType = "debug", + outputDir = kotlin.io.path.createTempDirectory("packpack-bundle-out-pyver").toFile(), + pythonVersion = wheelVersionOf("3.15.0"), + ) + assertEquals("3.15.0", request.pythonVersion) + assertEquals("3.15.0t", wheelVersionOf("3.15.0t")) + assertEquals("3.14.7", wheelVersionOf("3.14.7")) + assertEquals(null, bundleRequestFor(fixturePackageDir(), "aarch64-apple-darwin", "debug", java.io.File(".")).pythonVersion) + } } diff --git a/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonSdkTest.kt b/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonSdkTest.kt index 06aa7a5..522d340 100644 --- a/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonSdkTest.kt +++ b/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonSdkTest.kt @@ -49,6 +49,36 @@ class PythonSdkTest { assertNull(resolved.rejection) } + @Test + fun `3_15_0 is a published constant and X dot Y resolves to it`() { + val byConstant = resolvePythonSdk(PythonSdk().apply { assign(PythonSdkVersion.PY3_15_0) })!! + assertEquals("3.15.0", byConstant.version.toString()) + assertNull(byConstant.rejection) + assertEquals(true, byConstant.fromConstant) + + val byMinor = resolvePythonSdk(PythonSdk().apply { assign("3.15") })!! + assertEquals("3.15.0", byMinor.version.toReleaseString()) + assertNull(byMinor.rejection) + } + + @Test + fun `3_14_7 stays published beside 3_15_0`() { + assertTrue("3.14.7" in PythonReleaseServer.PUBLISHED && "3.15.0" in PythonReleaseServer.PUBLISHED) + } + + @Test + fun `the free-threaded spelling resolves to a published version and keeps the t`() { + listOf("3.15t", "3.15.0t").forEach { raw -> + val resolved = resolvePythonSdk(PythonSdk().apply { assign(raw) })!! + + assertNull(resolved.rejection, raw) + assertEquals(true, resolved.version.freeThreaded, raw) + assertEquals("3.15.0", resolved.version.toReleaseString(), raw) + assertEquals("3.15.0t", resolved.version.toWheelVersion(), raw) + } + assertEquals("3.15.0", resolvePythonSdk(PythonSdk().apply { assign("3.15") })!!.version.toWheelVersion()) + } + @Test fun `a string naming a published version is accepted, X dot Y included`() { listOf("3.13.0", "3.13").forEach { raw -> diff --git a/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonVersionTest.kt b/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonVersionTest.kt index 9151423..720f59c 100644 --- a/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonVersionTest.kt +++ b/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/dsl/PythonVersionTest.kt @@ -82,7 +82,7 @@ class PythonVersionTest { val error = assertFailsWith { PythonVersion.parse("not-a-version") } assertEquals( "Invalid Python version 'not-a-version'. Expected 'X.Y', 'X.Y.Z', 'X.Y.Z-alpha[N]', " + - "or 'X.Y.Z-rc[N]'.", + "or 'X.Y.Z-rc[N]', each optionally followed by 't' (free-threaded).", error.message, ) } @@ -91,4 +91,16 @@ class PythonVersionTest { fun `an unknown channel word is rejected loudly`() { assertFailsWith { PythonVersion.parse("3.13.0-beta1") } } + + @Test + fun `a trailing t marks the free-threaded build and the wheel version keeps it`() { + val threaded = PythonVersion.parse("3.15t") + + assertEquals(true, threaded.freeThreaded) + assertEquals("3.15.0", threaded.toReleaseString()) + assertEquals("3.15.0t", threaded.toWheelVersion()) + assertEquals("3.15.0t", threaded.toString()) + assertEquals(false, PythonVersion.parse("3.15.0").freeThreaded) + assertEquals("3.15.0", PythonVersion.parse("3.15.0-rc1").toWheelVersion()) + } } diff --git a/usage-example/build.gradle.kts b/usage-example/build.gradle.kts index 9168cdb..0e70ed2 100644 --- a/usage-example/build.gradle.kts +++ b/usage-example/build.gradle.kts @@ -27,7 +27,7 @@ plugins { // `integration()`. The rest of the DSL surface (`buildFeatures`) is declared but not wired to any // task yet -- see the plugin's own README/report for what is scaffolding versus live. python { - compileSdk = PY3_14_7 + compileSdk = PY3_15_0 // A real `pypackpack` package (`pyproject.toml` + `src/main/`), so the chain has a payload // to carry. Without it `buildPython` skips `pypackpack` entirely and every step downstream of it // -- the zip, and now the staging tasks -- correctly produces nothing, which makes "the artifact diff --git a/usage-example/gradle/libs.versions.toml b/usage-example/gradle/libs.versions.toml index 5751dec..b85d045 100644 --- a/usage-example/gradle/libs.versions.toml +++ b/usage-example/gradle/libs.versions.toml @@ -12,7 +12,7 @@ compose-multiplatform = "1.11.1" kotlin = "2.4.20-Beta2" kotlinx-coroutines = "1.10.1" # python-multiplatform's coordinates today (its #70 will unify the plugin's version with the library's). -python-multiplatform = "3.14.7-alpha01" +python-multiplatform = "3.15.0-alpha01" python-multiplatform-plugin = "3.13.0" # The toolchain plugin, from ../toolchain via mavenLocal (`./gradlew :toolchain:publishToMavenLocal`). toolchain = "1.0.0-alpha" diff --git a/usage-example/src/iosMain/install-python-phase.sh b/usage-example/src/iosMain/install-python-phase.sh index 9a237c1..785bbb8 100755 --- a/usage-example/src/iosMain/install-python-phase.sh +++ b/usage-example/src/iosMain/install-python-phase.sh @@ -25,7 +25,7 @@ export PYTHON_PAYLOAD_DIR="$payload" # 2. Python.framework (libpython) into /Frameworks, for the simulator or the device slice. # TODO(python-multiplatform#90): from the plugin's staged Python.xcframework, not the checkout's build/. -xcframework="$PYTHON_MULTIPLATFORM_DIR/python-multiplatform/build/python-standalone/extracted/${PYTHON_VERSION:-3.14.7}/ios/Python.xcframework" +xcframework="$PYTHON_MULTIPLATFORM_DIR/python-multiplatform/build/python-standalone/extracted/${PYTHON_VERSION:-3.15.0}/ios/Python.xcframework" case "${EFFECTIVE_PLATFORM_NAME:-}" in -iphonesimulator) slice="ios-arm64_x86_64-simulator" ;; -iphoneos) slice="ios-arm64" ;; From 2bc0784ea14c6f8a7fc89d16314601892a3ffa20 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:41:40 +0900 Subject: [PATCH 2/2] usage-example iOS phase: run install-python.sh from its new path in python-multiplatform (#85) --- usage-example/src/iosMain/install-python-phase.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usage-example/src/iosMain/install-python-phase.sh b/usage-example/src/iosMain/install-python-phase.sh index 785bbb8..cb7484a 100755 --- a/usage-example/src/iosMain/install-python-phase.sh +++ b/usage-example/src/iosMain/install-python-phase.sh @@ -42,4 +42,4 @@ fi # 3. The stdlib prefix and the payload, by python-multiplatform's own phase script. # TODO(python-multiplatform#90): run from this project's Gradle root, not the checkout. cd "$PYTHON_MULTIPLATFORM_DIR" -/bin/bash tools/xcode/install-python.sh +/bin/bash python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh