/Volumes/macMini/thisisthepy holds several repositories that are meant to compose into one
product. Until now none of them referenced the others in writing — this repository's docs/roadmap/ROADMAP.md
mentions pypackpack, toolchain, pythonx and chaquopy exactly zero times — so this file
records the intended relationships, what each repository actually contains today, and what has to
be built before the parts can meet.
Everything here was read from the working copies on 2026-08-14. Claims that were inferred rather than observed are marked as such.
pypackpack (ppp) the features: Python acquisition, dependency resolution, cross-compilation,
bundling. A Kotlin/JVM library that also ships as a GraalVM native CLI.
toolchain the Gradle interface to ppp, and nothing more than that.
A developer applies one plugin; toolchain translates the Gradle DSL into
ppp middleware calls.
PythonMultiplatform the binder: CPython embedded in Kotlin Multiplatform, the object model,
the FFI layer, upcalls, downcalls, the generated function table.
pythonx-compose Compose bound into Python, so a UI can be written in Python.
PyREPL reference implementation for turning a Kotlin namespace into `.pyi` stubs,
and — see below — the actual origin of `toolchain`.
The division of labour that matters: ppp owns the work, toolchain owns the Gradle vocabulary, this repository owns the language boundary. A feature that acquires a Python distribution or resolves a dependency belongs in ppp even if a Gradle task is what triggers it.
Roughly 4,900 lines of Kotlin, a clikt CLI compiled to a GraalVM native image, with real uv
integration. docs/SPEC.md (483 lines, added in the most recent commit) marks every feature
Implemented / Partially implemented / Not yet implemented.
Working: init, python use/list/find/install/uninstall, package add/remove/sync/tree,
target list/add/remove, per-package dependency operations with platform markers, and build
driving Meson for C/C++.
Placeholder — verified by line count, all two to four lines: the whole bundle/ tree, the whole
deploy/ tree, every compile backend except Meson (Clang, MSVC, NDK, XCode, Emscripten, Cargo),
and all of compile/middleware/{external,transcompile,minification}.
It is already designed to be called from Gradle. dependency/frontend/BaseInterface.kt declares
enum FrontendType { CLI, GRADLE } and Gradle.kt returns the same middleware the CLI uses. The
stable surface is dependency/middleware/BaseInterface.kt — initProject, addDependencies,
syncDependencies, addTargets, installPythonVersion, and so on — plus
compile/middleware/BaseInterface.kt's compile.
But packpack/build.gradle.kts applies no maven-publish and declares no publication. Nothing
outside that build can resolve it. Until that changes, "toolchain is the Gradle interface to ppp"
cannot be implemented at all.
A Kotlin Gradle plugin, id org.thisisthepy.python.multiplatform, about 1,240 lines. It registers
a python extension and three tasks. What the tasks do today: copy a local folder into
src/main/assets/python, copy build/pythonLibraries into build/pythonBundle/libs, zip that,
and shell out uv install -r — which is not a valid uv command.
toolchain shares git history with PyREPL. Commits f806656, 5f3f932, 1b32d03,
654ef7c exist in both with identical hashes; dba17dd extracted PyREPL's meta-generation out of
app/build.gradle.kts into toolchain/.../meta/createMetaClass.kt. So the pyi generator to study
is already here, in a mutated form — see §4.
The target API is (플러그인예시)build.gradle.kts at the repo root: 276 lines describing
compileSdk, per-platform declarations, packaging { embedLevel, hotReload, codePush },
buildTypes { compileLevel }, projectFlavors, buildFeatures { metaclass, compose }, and source
sets carrying both implementation("pyzmq") and integration("pycomposeui") — the latter being a
Kotlin-dependent Python package that needs a KLIBDEPENS file in its wheel.
Of that spec the DSL data classes exist for roughly 60%, and none of hotReload, codePush,
buildTypes, projectFlavors, buildFeatures, metaDirs, libDirs or integration() is read by
any task. PythonPlugin.kt consumes only sourceSets.*.dependencies.implementations.
dsl/PythonConfiguration.kt is dead code — its extension is never registered.
toolchain/usage-example/ does not apply the plugin, has no python { } block, and is commented
out of settings.gradle.kts. There is currently no executable definition of the target API.
toolchain-legacy is a different lineage: a fork of kivy/kivy-ios in pure Python that
cross-compiles CPython and native wheels for android/ios/macos/linux/mingw, last worked on in
February 2024. Nothing moved from it into toolchain; its job is what ppp is rebuilding in Kotlin
with uv, crossenv and meson instead of kivy recipes.
Two generations coexist, and the one the notebook demonstrates is in neither of them.
pythonx/compose/ is the chaquopy generation: a Composable decorator class, ComposeApp, and
material3 wrappers — 37 files of which 28 are empty. It binds through jclass(...) and
from java import jclass, and it reaches AndroidX directly:
from androidx.compose.material3 import TextKt
try: self.__kotlin_composable = TextKt.Text
except:
for name, obj in TextKt.__dict__.items():
if name.startswith("Text-"):
self.__kotlin_composable = obj; break
That trailing - is Kotlin's value-class mangling suffix (Text-fLXpl1I), which chaquopy exposes
verbatim. There are 26 such scans across 8 files. It then calls the function passing Compose's
synthetic $composer and $changed by hand.
pythonx/compose/lite/ is a desktop JVM generation using JPype and hand-written Kotlin wrappers.
test/ still depends on chaquopy 15.0.0 and contains a stale in-repo prototype of this
repository's binder.
The Kotlin-to-pyi converter lives inline in app/build.gradle.kts, 627 lines. It walks each Kotlin
source set's resolved artifacts, reads the jars with ASM (not kotlinx-metadata), and writes one
__init__.pyi per Java package under src/<sourceSet>/generated/meta/, with ... bodies and a
JNI-ish type map. It drops private members, <init>, Companion, and any member whose name
contains - — that is, it discards the mangled ones rather than scanning for them. Collisions
between commonMain and a platform source set are resolved by renaming both and re-exporting.
It is stubs only; the runtime underneath is chaquopy jclass. Its purpose is IDE completion.
toolchain's createMetaClass.kt is the same code mutated into a runtime generator: it emits
.py with from java import *, __metaclass__ = jclass(__name__), real delegating bodies, and a
Kotlin-to-dunder name map. For a chaquopy-free future, PyREPL's stub-only version is the one to
keep.
Gemstone belongs to a different organisation and does not depend on any of these; its README says
it is scheduled to adopt Python Multiplatform. reference/ and cpython/ are not repositories —
vendored material and prebuilt CPython artefacts.
UI.ipynb imports pythonx.compose.runtime, pythonx.compose.material3, pythonx.compose.ui
and pythonx.compose.layout, and documents signatures like Text(text, color, font_size),
Button(onclick, …) — quoted as the notebook writes it; the 2024 library spells that parameter
on_click (docs/archive/pythonx-adapter-design.md §3 measured both). Which Python spelling
pythonx uses is pythonx-compose's decision: this repository's binder exposes the Kotlin name
onClick and renames nothing (docs/INTENT.md §2.2) — plus Column/Row/Spacer/TextField. It also
requires main.App as a live object, App.messages.getValue()/setValue(), and
main.App.update(NewComposable) for hot-swapping the UI from a Jupyter cell.
main.App, App.update, DefaultIcons, Column, Row, Spacer and TextField do not exist
in pythonx-compose. They exist in PyREPL, under app/src/androidMain/python/, in a simpler
design that does no name matching: it binds hand-written Kotlin wrappers by exact @JvmName
(Material3Kt.SimpleTextWidget) and passes the composer explicitly.
So if the goal is "the notebook runs on our stack", the port target is PyREPL's app/, and
pythonx-compose is the more ambitious, largely unfinished generalisation of it.
Ordered so that each unblocks the next.
1. @Composable cannot be an exposed callable. The generator emits
callable = { args -> Owner.fn(args[0] as X) } and ExposedCallable.callable is typed
(Array<Any?>) -> Any?. A @Composable function can only be called from a @Composable context,
so a generated entry for a widget would not compile, and BindingPolicy has no @Composable
handling at all. Every widget in pycomposeui is @Composable. Nothing in pythonx-compose can
leave chaquopy until this is designed. (read from source; not runtime-verified.) This is the
smallest and most blocking item, and it is entirely inside this repository.
Closed — see
docs/archive/pythonx-adapter-design.md§5.6.Text('hi')written in Python now draws through the realandroidx.compose.material3.Text(:ksp-fixtures:compose'sComposableRenderTest: 71 non-background pixels against 0 for an empty body). The paragraph above is right that a generated Kotlin entry for a widget cannot compile, and that is not the route taken: an artefact composable's call site is emitted as bytecode, with$composer,$changedand$defaultexposed as ordinary slots, and the binding layer (python_multiplatform.binding,PythonxAdapter.kt) computes the mask.BindingPolicydoes now have@Composablehandling and what it does is decline — a composable in the consumer's own source has no compiled signature to call yet, which is a different problem from one in a jar.
2. packpack must be publishable. One file. Without it the stated architecture is
unimplementable.
3. A Python-facing upcall module. Today the only Python-side surface is the generated proxy
module plus, in a demo, raw ctypes pointers. pythonx-compose needs Kotlin namespaces to be
importable, so _material3 = jclass("...Material3Kt") becomes an ordinary import. Most of
this exists: generated modules are injected into sys.modules under their Kotlin fully-qualified
name. What is missing is a sys.meta_path finder so a name resolves on demand rather than only
after an eager install(), the pythonx prefix, and generated .pyi beside it.
Current (2026-10-03): the finder exists — the binding layer
python_multiplatform.bindinginstalls asys.meta_pathfinder that makes Kotlin-named modules importable on demand (_Finder/_LoaderinPythonxAdapter.kt; SPEC U-8). Thepythonxprefix is not this repository's to provide: the binder creates nopythonxmodule, andpythonxis a real package in pythonx-compose (AGENTS.md§12.2). The plugin's.pyiare emitted under Kotlin module paths only (SPEC B-7).
4. One CPython acquisition path, not two. This repository's stagePythonHome downloads from
python-build-standalone into a Gradle cache; ppp's python install downloads from this
repository's own GitHub releases into ~/.pypackpack/python/<version>. They disagree on source and
destination, and ppp's own list/find disagree with its install about the destination.
Per the stated architecture the surviving one is ppp's.
5. ppp's bundle stage, at least bundle-type resource, which its SPEC defines as the handoff
format to python-multiplatform and toolchain. It is the missing seam between the two halves of the
system, and all four bundlers are three-line placeholders.
6. Port PyREPL's pyi generator into toolchain, replacing the chaquopy-coupled variant, and fix
its hardcoded \\ path separators, which break on macOS and Linux.
7. A usage-example that applies the plugin, so the target API stops being only a document.
This section documents the current state, language/build system/distribution artifact, ecosystem goals, gaps to goal, and dependency direction for each of the 5 repositories in /Volumes/macMini/thisisthepy/, based on direct inspection of working trees on 2026-08-14.
- Observed Current State:
- Embedded CPython FFI binder (
python-multiplatform/src/commonMain/kotlin/.../EmbedAPI.kt, platform implementations inEmbedAPI.desktop.kt,bindings.kt, JNI/Panama/cinterop). Correction, 2026-08-17: this line used to say "CPython 3.13" — stale.gradle.properties'spythonVersion(the single source of truth, read intoVersions.currentVersionatpython-multiplatform/src/commonMain/kotlin/python/multiplatform/Versions.kt) is3.14.7, and every platform's observed runtime agrees: desktop printsruntime : 3.14.7(4ed98143:docs/roadmap/ROADMAP.md:2063), Android prints3.14.7on bothpmp_api36andpmp_api26(4ed98143:docs/roadmap/ROADMAP.md:2073), wasmJs prints3.14.2— its own, separately-pinned Emscripten build, not this line's concern (docs/platforms/wasm-design.md). See the version-mismatch check below. - Kotlin object model hierarchy (
python-multiplatform/src/commonMain/kotlin/.../PyObject.ktand wrappers). - Code generators: KSP processor (
python-multiplatform-ksp/) producingFunctionTableFragments, and Gradle plugin (python-multiplatform-gradle-plugin/src/main/kotlin/.../artifact/ArtifactScanner.kt,PythonArtifactBindingsTask.kt; corrected 2026-10-03 — there is noArtifactBindingGenerator.kt) scanning resolved dependency jars (ASM +kotlin-metadata-jvm) and klibs forArtifactTablefragments, plus.pyistubs under Kotlin module paths (stubs/PythonStubsTask.kt). - Multiplatform target support: Desktop JVM/Panama, Android JNI, iOS Native Cinterop, androidNativeArm64 (
:python-multiplatform:compileKotlinAndroidNativeArm64). - CPython binary download and SHA-256 lockfile / Sigstore verification (
python-checksums.properties,python-multiplatform/build.gradle.kts).
- Embedded CPython FFI binder (
- Language / Build System / Distribution:
- Kotlin Multiplatform (Kotlin
2.4.20-Beta2pinned ingradle/libs.versions.tomlas of 2026-10-03), Compose Multiplatform 1.11.1 for the sample and fixtures, Java 21, C/C++ FFI. - Gradle (
build.gradle.kts,settings.gradle.kts, included buildpython-multiplatform-gradle-plugin). - Maven artifact publication (
id("maven-publish")inpython-multiplatform/build.gradle.kts, published under groupio.github.thisisthepy:python-multiplatform).
- Kotlin Multiplatform (Kotlin
- Goal in Ecosystem:
- Core language boundary and binder between Kotlin Multiplatform and CPython.
- Manages low-level FFI, object reference handles, upcall/downcall function tables, GIL lifecycle, and proxy injection into CPython
sys.modules.
- Gap to Goal (status notes added 2026-10-03, read from code and SPEC, not re-run):
@Composable-capable callable shape:ExposedCallabletyped(Array<Any?>) -> Any?cannot invoke@Composablefunctions taking synthetic$composer/$changedparameters. Needs dedicatedCallableKindor opt-in annotation handling. — Closed on desktop by bytecode thunks (ComposableThunks.kt), SPEC B-6; Android, iOS and wasm planned (SPEC N-2).- Python-side
sys.meta_pathfinder for lazy import resolution of Kotlin namespaces(upon import. — Exists for Kotlin FQNs inpythonx.*or FQCNs)python_multiplatform.binding(SPEC U-8).pythonx.*is not the binder's to resolve (AGENTS.md§12.2). - Opaque Kotlin object round-tripping for Compose
Composer(HandleTableandObjectReferenceexist, but runtime hand-off to Python needs verification). — Done on desktop: one hand-written@Composablepushes the live composer as a handle onto the binding layer's stack (push_composer), proven by theksp-fixtures/composerender tests. - Lifetime and storage of Python callables passed into Kotlin across Compose recompositions (
content=lambda: ...,onClick=...). — Partly addressed:PythonCallableScope(PythonCallables.kt) holds the callables a composition hands to Kotlin and is closed by a Compose-sideRememberObserverinonForgotten; several hand-written fixture wrappers (pythonDraggable,pythonComposed, the swipe/anchor wrappers) still leak their callbacks by design, pinned byDraggableLeakTest. Not verified here beyond reading the sources. - Retire
stagePythonHomein favor ofpypackpack's Python distribution management. - Hand
stageWasmBrowserRuntimelogic totoolchain.
- Dependency Direction:
PythonMultiplatformhas no dependencies on other repos in the ecosystem.pythonx-composeand consumer applications depend onPythonMultiplatformruntime and its Gradle/KSP bindings plugin.
- Unverified:
- Behavior of WASM browser runtime under production web bundlers. (2026-10-03: the former
wasm-experiment/is folded into the library —wasmJsMain/wasmJsTest, CPython built bytools/wasm/build-cpython.sh— commit6e54f617; the bundler question is unchanged.)
- Behavior of WASM browser runtime under production web bundlers. (2026-10-03: the former
Version check (2026-08-17): "3.14 runtime vs. 3.13 header, never checked" — could not find the
sentence, checked the claim anyway. The literal sentence was searched for across every .md file
in this repository (grep -rn for Korean and English phrasings of "nobody/no one checked", "never
verified", "mismatch", combined with "header"/"헤더" and "runtime"/"런타임") and in the git history
of this file; it does not appear anywhere, verbatim or paraphrased. The nearest things on record are
this section's own now-corrected "CPython 3.13" line above (stale since the version became
configurable) and 4ed98143:docs/roadmap/ROADMAP.md:2033, which already fixed a test fixture that hardcoded the string
"CPython 3.13" while pythonVersion had moved to 3.14.7. Neither is the sentence quoted, and neither
left the underlying question — do headers and runtime actually agree, per platform — checked. That
question was checked here, freshly, platform by platform:
| platform | header source | runtime source | agree? | how checked |
|---|---|---|---|---|
| desktop | none — Panama binds by symbol name at runtime (java.lang.foreign.Linker/SymbolLookup), no C header is compiled against |
libpython3.14.dylib/.so/.dll extracted from the python-build-standalone archive pinned to pythonVersion (python-checksums.properties: macos-aarch64-3.14.7-20260807, etc.) |
n/a — no header exists to disagree with the runtime | read build.gradle.kts's desktop jvm() block; no cinterop/headers() call anywhere in it; 4ed98143:docs/roadmap/ROADMAP.md:1034-1038 records the same conclusion independently |
iOS / androidNative (Kotlin/Native cinterop) |
$targetExtractDir/include/python$libVersion/Python.h (Android) or the extracted Python.xcframework/.../Headers (iOS) — read from the same extraction the runtime library comes from, not from any vendored copy |
same archive, same extraction, libVersion/targetExtractDir computed from the one pythonVersion/libVersion variables |
yes, by construction — both paths are derived from the same Gradle variable, so they cannot independently drift for a given build | read python-multiplatform/build.gradle.kts:1093-1128 (targetIncludePath, the cinterops.create("python") block); confirmed no defFile/hardcoded header path overrides it |
Android JNI (artMain, jni_onload.def) |
none — hand-written extern prototypes in jni_onload.def, no #include <Python.h>, pointers passed as jlong |
libpython$libVersion.so linked with -lpython$libVersion from the same extraction tree |
n/a for headers; symbol-level agreement already checked by earlier work (nm/objdump against the shipped .so, cited in docs/roadmap/ROADMAP.md around the 3.15 migration) |
read python-multiplatform/src/artMain/cinterop/jni_onload.def and its build.gradle.kts cinterop block (no headers()/includeDirs() call) |
| wasmJs | Emscripten's own CPython 3.14.2 build, entirely separate toolchain (docs/platforms/wasm-design.md) |
same 3.14.2 build (Embedded CPython version: 3.14.2, docs/archive/wasm-design-experiment-log.md "Building CPython 3.14.2 for Emscripten"; runtime : 3.14.2 sample output, 4ed98143:docs/roadmap/ROADMAP.md:1488) |
yes, and deliberately a different minor-patch than the 3.14.7 native default — documented and reasoned about at length in docs/platforms/wasm-design.md, not an oversight |
What is genuinely stale, and does not affect any of the above: two artefacts in this repository
still carry CPython 3.13 and are not read by the default build. python-multiplatform/src/nativeInterop/cinterop/include/patchlevel.h
is a vendored header tree whose PY_VERSION reads "3.13.0+" — git log shows it landed with
b184cba5 (pre-dates pythonVersion becoming a Gradle property) and build.gradle.kts never
references nativeInterop/cinterop/include at all; cinterop reads headers from the version-keyed
extraction tree instead (table above). binary/ holds five committed CPython 3.13.0 archives from
python-build-standalone release 20241008 (binary/download.md); they are consumed only by the
special-cased branch if (configuredPythonVersion == "3.13.0" && !pythonFreeThreaded) in the
desktopJar task (build.gradle.kts:1049), i.e. only if a consumer explicitly builds with
-PpythonVersion=3.13.0. That legacy path has no Py_LIMITED_API/header dependency either (desktop
never compiles against headers), so even under -PpythonVersion=3.13.0 there is no header/runtime
split — but python-checksums.properties has no 3.13.0 entries for Android or iOS, so that flag
would fail checksum verification on those two targets rather than silently mixing versions; this was
not run to confirm the failure mode, only the absence of pinned checksums.
Correction to the paragraph above, made while verifying it: the 3.13 headers are not inert. They
are not a compiler input — that part holds, build.gradle.kts never puts
nativeInterop/cinterop/include on an include path. But two test suites read that directory as their
source of truth about the C API: EmbedApiSurfaceTest.kt:454 and
JniCallConventionClassificationTest.kt:628 both open src/nativeInterop/cinterop/include and
derive, respectively, the deprecation schedule the expect surface is checked against and the JNI
call-convention classification. So the one place in this repository where a 3.13 header meets a 3.14
runtime is the evidence layer, not the build: a function whose deprecation or signature changed in
3.14 would be classified from 3.13's text and the tests would agree with themselves. This is the
mismatch the check set out to look for, in the one shape nobody had looked at, and it is recorded
rather than fixed here — refreshing the header tree is a separate change with its own verification.
That separate change was made (2026-08-17), and it answered the open question: nothing this
project uses moved between 3.13 and 3.14. The vendored include/ tree was replaced with the
3.14.7 headers out of the build's own checksum-verified extraction
(build/python-standalone/extracted/3.14.7/android-aarch64/prefix/include/python3.14/; the four
pyconfig-<platform>.h files and the hand-written pyconfig.h dispatcher were re-derived from the
matching per-platform extractions, since CPython ships one pyconfig.h per build and this tree
serves four). The evidence base the two suites read grew from 1129 PyAPI_FUNC prototypes to 1207
and from 81 Py_DEPRECATED symbols to 101, and every conclusion drawn from it was unchanged:
the JNI classification reported byte-identical totals before and after
(LEAF_PROVEN=5, RE_ENTRANT_BY_JNI_UPCALL=3, RE_ENTRANT_BY_SIGNATURE=293, UNDECIDED=65; 34
GC-blocking declarations, 22 promoted, 12 quarantined), and desktopTest went 467/0/1 → 469/0/1,
the two added tests being the new guard below.
Why nothing moved, stated as a set intersection rather than as an impression: of the 6 prototypes
3.14 drops (PyUnstable_InterpreterState_GetMainModule, _PyBytes_Join, _PyTrash_begin,
_PyTrash_end, _Py_InitializeMain, _Py_fopen_obj), the 1 whose signature changed
(_PyLong_NumBits, size_t → int64_t), and the 23 newly marked Py_DEPRECATED (led by
PyThread_exit_thread, _PyThreadState_UncheckedGet, _Py_HashPointer, and the whole
_PyUnicodeWriter_* family), none appears in the 312-name expect surface of EmbedAPI.kt, and
none appears among the 319 CPython prototypes jni_onload.def calls. Every one of the 30 is a
private (_Py/_PyUnstable) symbol this project's abi3 self-rule already forbids. So the
3.13-vs-3.14 evidence gap was real but benign, and it is now closed rather than argued about.
The guard that keeps it closed is VendoredHeaderVersionTest (src/desktopTest/.../ VendoredHeaderVersionTest.kt): it compares patchlevel.h's PY_MAJOR/MINOR/MICRO_VERSION against
gradle.properties's pythonVersion and fails if they diverge, with a canary test so that a
regex that stops matching fails loudly instead of making the comparison vacuous. It was watched
failing on the pre-refresh tree (expected:<3.1[4.7]> but was:<3.1[3.0]>, sole failure in a
469-test run) before it was trusted. It compares versions, not contents — deliberately, since a
content comparison would need the extraction present and so would be unavailable exactly when the
extraction has not been run.
The rest of that tree is genuinely unreferenced and large: git ls-files counts 15,842 tracked files
under src/nativeInterop/cinterop (679 MB in lib/, 2.6 MB in include/), and of lib/ only
lib/desktop is read, by the -PpythonVersion=3.13.0 branch above. The Android CPython 3.13 standard
library trees under lib/android/<abi>/python3.13/ are referenced by no build file, no Kotlin source
and no test; the runtime stdlib comes from the version-keyed extraction instead. Whether to remove
them is a repository-size decision, not a correctness one, and is left open.
Py_LIMITED_API status, checked directly: grep -rn "Py_LIMITED_API" across .kts/.def build
files finds it only inside vendored CPython header guards (#if defined(Py_LIMITED_API) ...) — it is
never passed as a compiler define anywhere (no -DPy_LIMITED_API, no compilerOpts setting it). This
independently confirms 4ed98143:docs/roadmap/ROADMAP.md:1031-1038's own finding, reached from the free-threading
investigation rather than this one: the project does not build against the Limited API / Stable
ABI in the Py_LIMITED_API-macro sense. "abi3" as used elsewhere in this repository's docs means a
self-imposed rule about which C API functions this project chooses to call (ones stable across
versions), not a Py_LIMITED_API compilation mode. So the "Stable ABI permits a header/runtime minor
mismatch" escape hatch the task brief raised does not apply here — but it is not needed, either,
because the table above shows headers and runtime are never independently sourced in the first place.
Residual risk, marked as such because it was not run: the table above is a build-configuration
argument (headers and runtime derive from one variable), corroborated by observed sys.version-
equivalent output on desktop and Android and by an existing wasmJs run — not by a from-scratch build
of compileKotlinIosSimulatorArm64/compileKotlinAndroidNativeArm64 performed in this pass (this
check reused docs/roadmap/ROADMAP.md's existing, dated build/test evidence rather than re-running Gradle). If a
future change hardcodes a header path or a library name outside the libVersion/targetIncludePath
variables audited above, this guarantee would silently stop holding; nothing currently tests for that
kind of regression directly (the closest is the version-agnostic assertion in Python3Test.kt's
versionReportsTheConfiguredRelease, which catches a runtime drift but not a header-only one, since
no target here has a runtime check of the header's declared version — the headers have no
executable presence to assert against).
Issues (checked 2026-08-16): the only open issue against this repository is
python-multiplatform#4, "Add Python/C API
expect declaration reclassification and actual definitions" (deadline noted in the issue as 25-07-24, i.e.
already past). It has no checklist, just two prose tasks: (1) reclassify the expect declarations in
EmbedAPI.kt, excluding deprecated/soon-to-be-deprecated APIs and ones judged unnecessary, and (2) add
actual definitions for androidMain, desktopMain, nativeMain.
Cross-checked against code rather than assumed:
- The "335/336/336/315" difference claim was incorrect. Commit
4ac2eca6proved thatgrepcounted the numbers wrong: Kotlin block comments nest, and each file carries superseded pre-migration drafts inside comments, sogrepscanned them as live code. When counted with comment-aware scanning, the difference betweenexpectandactualmembers is zero on every target — it was empty before the commit and remained empty after. The miscount pattern (grep unable to see block nesting) has recurred three times in this repository (EmbedAPIinitial draft, two parser canaries); documenting this as a pattern is valuable to prevent it again.EmbedAPI.wasmJs.ktexists too (not one of the three the issue named). - The "reclassify to exclude deprecated APIs" half of the issue was resolved differently than specified.
Rather than removing deprecated CPython APIs, the repository kept them and marked them
@Deprecatedin Kotlin:PyEval_InitThreadswas removed (empty in CPython 3.9+), four functions kept marked@DeprecatedwithoutReplaceWith(because exact replacements exist but blind swaps would be wrong in ways the compiler cannot catch — three return borrowed vs. new references, one takes an unnormalized triple), and three that CPython marks as "discouraged" (not deprecated) were left alone. Commit4ac2eca6adds a test that validates all three targets against a canonical set; the test passes at 466 cases across all five targets. - Issue #4's
actual-definition half was already complete before4ac2eca6; the commit's real work was reclassification, dead-code cleanup (three draft blocks removed from comments), and adding validation tests.
- Observed Current State:
- Plugin project structure (
toolchain/src/main/kotlin/.../PythonPlugin.kt) registeringpythonextension and 3 tasks (InstallDependenciesTask,BuildPythonArtifactTask,AssemblePythonPackageTask). - Naive, self-contained task implementations in
bundle/(AssemblePythonPackageTask.kt,BuildPythonArtifactTask.kt): copying local directory tosrc/main/assets/python, zipping tobuild/pythonBundle/libs, and executinguv install -r(which is an invaliduvcommand). - DSL definitions in
dsl/(DSLBuild.kt,DSLCore.kt,DSLPackaging.kt,DSLPlatforms.kt,PythonConfiguration.kt) covering ~60% of target spec(플러그인예시)build.gradle.kts. - Mutated PyREPL generator
toolchain/src/main/kotlin/.../dependency/lang/createMetaClass.ktproducing runtime.pyfiles usingfrom java import *.
- Plugin project structure (
- Language / Build System / Distribution:
- Kotlin (
kotlin-dsl,java-gradle-plugin). - Gradle (
build.gradle.kts,settings.gradle.kts). - Gradle Plugin published to Maven (
org.thisisthepy.python.multiplatformplugin ID).
- Kotlin (
- Goal in Ecosystem:
- Developer-facing Gradle DSL vocabulary (
python { ... }) and nothing else. - Translates Gradle DSL configuration into
pypackpackmiddleware API calls. - Owns Kotlin-to-
.pyistub generation (porting PyREPL's ASM-based stub generator). - Handles
integration()dependency types withKLIBDEPENS.
- Developer-facing Gradle DSL vocabulary (
- Gap to Goal:
- Delegate to
pypackpack: Currently delegates nothing topypackpackbecausepypackpackis not published to Maven. All tasks must be rewritten to invokepypackpackmiddleware. - Delete redundant tasks: Remove internal naive tasks (
InstallDependenciesTask,BuildPythonArtifactTask,AssemblePythonPackageTask, and asset copy). - Wire up DSL: DSL blocks in
dsl/(hotReload,codePush,buildTypes,projectFlavors,buildFeatures,metaDirs,libDirs,integration()) are not read by any task.PythonConfiguration.ktextension is dead code (never registered). - Replace stub generator: Replace runtime
createMetaClass.ktwith PyREPL's ASM-based build-time.pyigenerator, fixing hardcoded Windows path separators (\\). - Executable specification:
usage-example/does not apply the plugin and is commented out insettings.gradle.kts.
- Delegate to
- Dependency Direction:
toolchain->pypackpack(callspypackpackJVM library API).- Applied by end-user Kotlin Multiplatform projects.
- Unverified:
- Behavior of
integration()configuration withKLIBDEPENSwheel metadata (only documented in DSL spec).
- Behavior of
Issues (checked 2026-08-16): two open issues,
toolchain#2 ("[Todo] Kotlin Gradle Plugin and Build
Tools" — a checklist, sub-issue pypackpack#2) and
toolchain#1 ("[Todo] Toolchain-lite for python-only
users", parent pypackpack#2, one line: tcl install pythonx-compose). Full checklist-vs-code table is in
§5c. Three corrections to the bullets above, found by reading the current tree (git log --oneline -12
runs through fb1dba7, 30a064c, f7008eb) rather than by re-deriving them:
- The
createMetaClass.ktgenerator this file described above no longer exists.git logshows it deleted in30a064c("Feat: Open the packagePython chain, delegate dependency install, and delete the meta generator");find . -iname "createMetaClass*"in the working tree returns nothing. The "Observed Current State" bullet above describing it is stale as of that commit — left as written per this repository's convention of not erasing superseded claims, corrected here instead. - "Delete redundant tasks" (this file's own gap item) did not happen by deletion.
InstallDependenciesTask,BuildPythonArtifactTaskandAssemblePythonPackageTaskall still exist, under the same names. What changed is their bodies:InstallDependenciesTaskno longer shellsuv install -r(not a realuvverb) and instead callspypackpack'sdependency.backend.BaseInterfacedirectly (confirmed by readingInstallDependenciesTask.kt's current source, which now importsorg.thisisthepy.python.multiplatform.packpack.dependency.backend.BaseInterface). The naive copy-to-src/main/assets/python-then-zip step that lived inline inPythonPlugin.kt'safterEvaluatewas deleted (that part of the gap item is accurate), but the task classes were rewired, not removed. - "Wire up DSL" is now partially wrong.
integration()andbuildTypes(debug/releaseselection via-Ppython.buildType) are wired as offb1dba7and an earlier commit respectively — confirmed by readingPythonPlugin.kt'scollectInstallDependencies(foldsimplementations + integrations) andresolveActiveBuildType.hotReload,codePush,buildFeatures(metaclass/compose),metaDirs,libDirsand per-variantplatforms(e.g. Android min-SDK) remain unwired — butfb1dba7's commit message states this was tested, not assumed: registeringPythonConfiguration.kt's extension was tried and confirmed to compile and do nothing (usage-examplenever imports the DSL package that would reach it), and the rest have no corresponding concept inpypackpackto bind to yet.PythonConfiguration.ktdead-code claim re-confirmed by this agent:grep -rn PythonConfiguration toolchain/src/main/kotlinoutside its own file returns nothing.
- Observed Current State:
- ~4,900 lines of Kotlin implementing CLI subcommands in
packpack/src/main/kotlin/.../cli/:init,python use/list/find/install/uninstall,package add/remove/sync/tree,target list/add/remove,add/remove/sync/treewith target markers (platform_system,platform_machine). uvintegration (dependency/backend/external/UV.kt) for downloading uv and managingDevEnv/CrossEnvvenvs.buildsubcommand driving Meson CLI (compile/backend/external/Meson.kt) for C/C++ extension modules (py.extension_module()) and Python sources (py.install_sources()).- Architecture ready for dual usage (
dependency/frontend/BaseInterface.kthasenum FrontendType { CLI, GRADLE }andGradle.kt). - Specification in
docs/SPEC.mdmarking feature statuses.
- ~4,900 lines of Kotlin implementing CLI subcommands in
- Language / Build System / Distribution:
- Kotlin JVM (Kotlin 2.3.0 / Java 21), Clikt CLI, Ktor client, Ktoml, zstd-jni.
- Gradle (
packpack/build.gradle.kts, GraalVM Native Image pluginorg.graalvm.buildtools.native). - GraalVM native binary executable (
pypackpack), intended to also publish as a Kotlin/JVM Maven library (org.thisisthepy.python.multiplatform:packpack).
- Goal in Ecosystem:
- Owns all heavy lifting for Python packaging: Python distribution acquisition/management, dependency resolution (
uv), cross-compilation environment (crossenv), C/C++/Rust extension compilation, bundling into resource/wheel formats. - Serves as both a standalone CLI and an internal library for
toolchain.
- Owns all heavy lifting for Python packaging: Python distribution acquisition/management, dependency resolution (
- Gap to Goal:
- Publish to Maven:
packpack/build.gradle.ktslacksmaven-publishplugin and publication block. Nothing outside can resolvepypackpackuntil published. - Implement
bundlestage: All bundlers inbundle/(BinaryBundler.kt,FatWheelBundler.kt,SingleWheelBundler.kt,WheelPatchBundler.kt) are 2-4 line empty placeholders. Bundle typeresource(needed byPythonMultiplatformandtoolchain) must be implemented. - Fix destination disagreement:
python installdownloads to<project>/.venvor<project>/<target>, whilepython list/find/uninstallsearch~/.pypackpack/python/<version>. - Auto-install build tools:
Meson.installMeson()exists but is never invoked before build execution. - CLI pass-through flags:
add/remove/sync/treehardcodeextraArgs = null. - Non-Meson compiler backends (Clang, MSVC, NDK, XCode, Emscripten, Cargo) and compilation/minification middleware (Nuitka, Cython, Lpython) are empty placeholders.
- Publish to Maven:
- Dependency Direction:
pypackpackhas no dependencies ontoolchainorpythonx-compose.toolchaindepends onpypackpackas a Maven library.
- Unverified:
- Wheel patch generation (
WheelPatchBundler.kt) and incremental upload logic (placeholder files only).
- Wheel patch generation (
Issues (checked 2026-08-16): four open issues —
pypackpack#2 ("[Todo] PyPackPack Initial
Development", parent toolchain#2, subs pypackpack#1/toolchain#1),
pypackpack#1 ("[Todo] Python Dependency
Management", parent pypackpack#2, sub pypackpack#5),
pypackpack#5 ("Managing Multi-Platform Dependencies
in uv with Platform Markers", help wanted, a how-to guide rather than a checklist), and
pypackpack#12 ("Ambiguous file name due to
duplicated name on entirely codebase", a naming-convention proposal). Full checklist-vs-code table in §5c.
Several "Gap to Goal" items above are now stale — this repository moved fast in the last two days
(git log --oneline -12 runs 488bac0 back through 08353c7, all dated 2026-08-15/16) — and their commit
messages are unusually explicit about what they do and do not close, so this agent read each rather than
inferring from diff stats:
- "Publish to Maven" is done.
d10ab76("Build: Publish packpack, so toolchain can finally depend on it") addedmaven-publishtopackpack/build.gradle.kts; the commit message statespublishToMavenLocalnow producesorg.thisisthepy.python.multiplatform:packpack:0.1.0. The one caveat the same commit records — CLI dependencies (Clikt, Ktor, zstd) leaking onto a library consumer's classpath because the CLI lived in the same module — was itself closed one commit later,488bac0("Build: Split the CLI into its own module"): confirmed byfind . -iname BuildCommand.kt, which now resolves undercli/src/main/kotlin/.../cli/BuildCommand.kt, not underpackpack/. - "Implement
bundlestage" is partially done, more precisely than "all four bundlers are placeholders."8d7b4b4("Feat: Implement the resource bundle...") implementedResourceBundler.kt(310 lines, its own 328-line test file) — the one bundle typepython-multiplatformandtoolchainneed per the commit message.BinaryBundler.kt,FatWheelBundler.kt,SingleWheelBundler.ktandWheelPatchBundler.ktare still placeholders (re-confirmed by this agent, unchanged). ButResourceBundleris not yet reachable from the CLI:grep -n "ResourceBundler\|resource" cli/.../BuildCommand.ktfinds nothing, anddocs/SPEC.md(pypackpack's own spec, current as of these commits) says outright "there is nosource/resourcebundle-type subcommand yet". So the class exists and is tested; the CLI seam to it does not. - The "destination disagreement" gap item is mostly fixed, with a named residual.
5de8656("Fix: Close the three known defects...") added aregistry.propertiesfile bridginginstall's project-relative writes andfind/list/uninstall's~/.pypackpack/python/<version>reads.docs/SPEC.mdconfirms this in itspython installsection. The same section still lists "Final placement after download/extraction is incomplete (markedTODOin code)" as an open limitation — the registry closes the lookup mismatch, not this separate placement TODO. - "Auto-install build tools" is done.
isMesonInstalled()used to be a stub returningtrue(per8d7b4b4's commit message);5de8656made it probemeson --version/ninja --versionfor real and callMeson.installMeson()when the probe fails. - "CLI pass-through flags" is done, not just started.
5de8656's message statesadd/remove/sync/treeforward unrecognized flags in all three command shapes, verified through the middleware to the per-targetuvcall —docs/SPEC.md's dependency-management section documents the same allowlist (parsePassthroughArgsincli/CommandExtension.kt) and a known ordering gotcha (--target's greedy vararg swallows a passthrough flag placed after it). - Non-Meson compile backends and bundlers remain placeholders, re-confirmed.
Clang.kt,MSVC.kt,NDK.kt,XCode.kt,Emscripten.kt,Cargo.kt,Nuitka.ktare all still exactly 4 lines each (wc -l, checked by this agent, unchanged from what this file already said). pypackpack#12's file-naming complaint is unresolved. The suggested rename (BaseInterface.kt/DefaultInterface.ktduplicated acrossdependency/,compile/backend/,compile/middleware/,bundle/,deploy/→FrontendInterface.kt/MiddlewareInterface.kt/BackendInterface.kt/DefaultMiddleware.kt/DefaultBackend.kt) has not been applied —find . -iname "*Interface*.kt"still returns the ambiguous names the issue complains about, unchanged.pypackpack#5's platform-marker guide describes something that already mostly works.MarkerPolicy(dependency/middleware/DefaultInterface.kt) buildsplatform_system == '...' and platform_machine == '...'markers per target and--python-platformis threaded throughtree/sync/add/remove. The one place it does not hold:docs/KNOWN_ISSUES.mdrecords that the markeruv addactually persists can come out asplatform_machine/sys_platforminstead ofplatform_system/platform_machine, which can makeremove --targetfail to find a dependency added with that same target. So the guide's approach is largely implemented, with one recorded, unfixed matching bug rather than being unbuilt.pypackpack#1's "Middleware Refactoring" item ("areDevEnv/CrossEnvredundant wrappers?") is still genuinely open, not stale: readingDevEnv.ktnow, its methods still follow "resolve project root → delegate to backend → print success/failure", i.e. still the shape the issue calls a simple wrapper.CrossEnv.kt(552 lines) has grown well past that shape, though — asymmetric, unresolved by this agent's reading, matches the issue's unchecked box.pypackpack#1's "settings.gradle.kts — [ ] uv" item could not be resolved to a concrete claim. The issue text is a single unexplained bullet with no elaboration.settings.gradle.ktstoday has no mention ofuv(checked directly); whether that is the intended scope of the checkbox, this agent could not determine from the issue alone — left open rather than guessed.
- Observed Current State:
- Intended Python UI framework wrapping Kotlin Compose Multiplatform.
- Two unmerged legacy generations:
- Chaquopy branch (
pythonx/compose/): 37 files (28 empty placeholders), usingfrom java import jclassand scanning mangled AndroidX Kotlin symbols (TextKt.Text-fLXpl1I) via__dict__reflection. - Desktop JVM branch (
pythonx/compose/lite/): uses JPype with hand-written Kotlin wrappers.
- Chaquopy branch (
- Demo notebook
UI.ipynbtarget API works against PyREPL'sapp/src/androidMain/python/implementation (main.App,App.update,DefaultIcons,Column,Row,Spacer,TextField), NOTpythonx-compose's current codebase.
- Language / Build System / Distribution:
- Python (3.8+).
setuptools(pyproject.tomlbuilding packagepycomposeuiversion 0.0.1).- PyPI / wheel package (
pycomposeui).
- Goal in Ecosystem:
- Provide Pythonic Compose wrappers (
pythonx.compose.*) for writing UIs in Python. - Adapt Kotlin Compose bindings generically via
__getattr__and caching (mapping named arguments, threading composer, adaptingcontent=lambdas) rather than hand-writing wrapper functions per widget.
- Provide Pythonic Compose wrappers (
- Gap to Goal:
- Delete raw-AndroidX dynamic reflection branch (
TextKt.Text-fLXpl1I) and JPype dependency. - Migrate runtime to
PythonMultiplatform's static upcall table and Python proxy injection (sys.modules), replacingjclass. - Port PyREPL's widget and runtime model (
app/src/androidMain/python/) intopythonx-compose. - Implement generic Python adapter (
__getattr__onpythonx.compose.material3resolving underlyingandroidx.compose.material3bindings lazily). - Replace string-based type name parsing in
remember_saveablewithPythonMultiplatform'sPyValuetype tags.
- Delete raw-AndroidX dynamic reflection branch (
- Dependency Direction:
pythonx-compose->PythonMultiplatform(depends onPythonMultiplatform's FFI binding surface and Python proxy module at runtime).UsesCorrected 2026-10-03:.pyistubs emitted bytoolchain/PythonMultiplatformplugin for editor autocompletion.PythonMultiplatform's plugin emits stubs for the Kotlin-named modules only (SPEC B-7). The Pythonicpythonx.*stubs, and thepythonx-map.tomlmanifest that would drive them, arepythonx-compose's own product; it builds its API frominspect.signatureandpython_multiplatform.describe(KotlinSurface.kt).
- Unverified:
- Full hot-reload integration with Jupyter notebook server outside
UI.ipynbstatic cells.
- Full hot-reload integration with Jupyter notebook server outside
Issues (checked 2026-08-16): gh issue list --repo thisisthepy/pythonx-compose --state open returns
nothing. There is no issue tracker source for this repository's gap list — everything in "Gap to Goal"
above is this document's own inference from reading the code, not sourced from an issue. Flagged here so
it is not mistaken for the issue-backed items in the other three repos.
- Observed Current State:
- Legacy build toolchain source tree (
toolchain/directory) derived from Kivy (kivy-iosandpython-for-android), last worked on in February 2024. - Python CLI entry points in
setup.py:toolchain,toolchain_targetver,toolchain_targetos. - Cross-compiles CPython 3.11 and native recipes (NumPy, OpenSSL, Kivy) for Android, iOS, macOS, Linux, and Windows (mingw).
- Depends on external forks:
thisisthepy/toolchain-iosandthisisthepy/toolchain-android.
- Legacy build toolchain source tree (
- Language / Build System / Distribution:
- Pure Python (3.9+).
setup.py(package nametoolchain, version3.11.0.1).- PyPI package / Python CLI tool (
pip install git+https://...).
- Goal in Ecosystem:
- Legacy reference implementation only.
- Kept for historical reference to understand pre-2024 CPython cross-compilation and recipe management.
- Replaced entirely by
pypackpack(cross-compilation and packaging in Kotlin viauv/crossenv/meson) andtoolchain(Gradle plugin interface).
- Gap to Goal:
- Deprecated/superseded. No active development or gap to close — retained for historical reference only.
- Dependency Direction:
- None. Independent legacy Python CLI tool.
- Unverified:
- Modern Xcode 15+ / NDK 26+ compatibility of legacy recipes (untested).
Four questions were open about what Python code should look like. They are settled.
A Kotlin fully-qualified name means the original Kotlin. import androidx.compose.material3
reaches real AndroidX, not a wrapper of ours wearing its name.
This is reachable, and the mechanism is the one PyREPL already uses: the Gradle plugin reads the
resolved dependency artefacts. For each Kotlin source set it makes a resolvable copy of the
implementation and api configurations and walks the resulting jars with ASM. AndroidX is an
ordinary jar in that set, so it is covered like anything else.
The two producers are therefore split by what they look at, not by whether something is reachable:
KSP the consumer's own source — declarations it can see being compiled
artefact walker everything the build resolves — third-party jars, AndroidX included
Both run at build time under the same applied plugin. An earlier draft of this file claimed AndroidX could never be reached because KSP does not see it; that conflated one producer's limit with the system's.
What genuinely differs per platform is what a jar means. On JVM and Android the artefacts are jars and the walker applies directly. On iOS, androidNative and wasm the artefacts are klibs, and whether the same walk is possible there — and what a Kotlin declaration from a klib can be bound to at runtime with no JVM underneath — is the open question, not AndroidX.
Update — the walker now exists on one path, and both of those questions have answers.
python-multiplatform-gradle-plugin gained generatePythonArtifactBindings, and
ksp-fixtures/artifact carries a resolved junit:junit:4.13.2 through ASM, a generated
FunctionTableFragment, UpcallTable and PythonProxySource to
from junit.runner.Version import id answering "4.13.2". ROADMAP §16 records it in full; three
things in it change what this section says:
- The walker's fragments are a second aggregator (
ArtifactTable), not additions to KSP'sFunctionTable. OneUpcallTable, one fragment interface, one Python namespace — butFunctionTablekeeps meaning "every module in this graph compiled with the processor", which is whatksp-fixtures/appasserts and what a consumer's own table should not silently outgrow. Install site:UpcallTable.install(FunctionTable.fragments + ArtifactTable.fragments). - ASM alone does not reach
androidx.compose.material3. It reaches Java statics and Kotlin@JvmStatics. A Kotlin top-level function compiles onto a package-private multi-file part (kotlin/text/StringsKt__IndentKt) behind a facade Kotlin cannot name, and its extension receiver is indistinguishable from an ordinary parameter in bytecode. The@Metadatakind is readable with ASM; the payload that carries the Kotlin names isd1/d2and needskotlin-metadata-jvm. So AndroidX is reachable, as this section says — one library short of it, not one design short. - klib is the easier half, not the harder one.
org.jetbrains.kotlin.library.abi.LibraryAbiReader, already in thekotlin-compiler-embeddablethis build pins, readpython-multiplatform-iosX64Main.kliband returned 441 top-level declarations under their Kotlin qualified names, withisSuspend,hasExtensionReceiverParameter, value parameters and Kotlin (unerased) types. There is no facade problem because a klib records the Kotlin declaration; and since a generated fragment is Kotlin source compiled into the consumer's own binary with the klib on its compile classpath, the call is an ordinary Kotlin call with no reflection. ~~The open question that remains is not feasibility but cost:@ExperimentalLibraryAbiReader, and a ~60 MB compiler artefact on the plugin classpath versioned against the consumer's Kotlin rather than the plugin's.~~ The cost question is answered too, and not by shrinking it — by moving it off the plugin's classpath. ROADMAP §16e/16f:KlibScannerruns inside an isolated Gradle worker (WorkerExecutor.scanKlibIsolated) whose classpath is supplied per build rather than baked into the plugin jar, because calling it from the plugin's own classloader throwsNoSuchMethodErroragainst a real consumer'skotlin-util-klib— a correctness fix that also happens to keep the ~60 MB artefact off a build that never walks a klib.PythonArtifactBindingsTaskis wired to it and covered byKlibScannerTestagainst two real klibs.
Dynamic binding is a removed option, not a missing one. The 2024 design document
(PyComposeUI, the open-source contest report) specifies a dynamic binder — Java/Kotlin
Reflection on the JVM target, ctypes and pyobjc on Kotlin/Native — with a generated meta
package existing only so an IDE can infer types against it. That is history. The binder was built
statically instead, and the reason is the same one §7 records for choosing a build-time table:
a GraalVM native image is a closed world and Kotlin/Native has no reflection, so a name that only
exists at runtime cannot be reached on the targets this library has to support.
So there is one axis, not two. Bindings are produced at build time, by KSP for the consumer's own source and by an artefact walk for everything the build resolves. Anyone reading the 2024 document will find a dynamic path described as the plan; it was dropped deliberately, and the meta package that document treats as a typing aid is, here, generated beside bindings that are themselves static.
pythonx.* is ours. Whatever we wrap or add lives under that prefix. Kotlin fully-qualified
names point at the original; pythonx points at our Pythonic layer. The two namespaces do not mix.
(2026-10-03: "ours" means the ecosystem's — pythonx is a real package in pythonx-compose; this
repository's binder creates no pythonx module and renames nothing, AGENTS.md §12.1–12.2.)
The wrapping happens in Python, not in Kotlin. pythonx.compose.material3 is Python code that
uses the generated androidx.compose.material3 bindings and presents a Pythonic API over them.
There is no hand-written Kotlin wrapper module in between — that is what the existing
io.github.thisisthepy.pycomposeui RuntimeKt and Runtime_androidKt are, and they are the shape
being moved away from.
This settles a question that otherwise looks hard. A @Composable function is, after compilation,
an ordinary function taking $composer and $changed; a binding generated from the artefact
exposes that signature as it is, and the Python wrapper passes the composer as a value — which is
what pythonx-compose's Composable class already does. So nothing on the Kotlin side needs a
@Composable callable type, and the upcall trampolines do not have to preserve a @Composable
context.
pythonx adapts generically; it does not wrap function by function. Writing a Python wrapper
for every Composable is the cost that made the existing pythonx-compose 37 files of which 28 are
empty. The conversions are rules, not per-function decisions: a Pythonic name maps to the original,
the composer and change flags are threaded the same way every time, and a content= callable is
adapted the same way every time. So the layer should resolve on demand — a module __getattr__
that finds the corresponding binding, adapts it once and caches it — rather than enumerate.
That trades away IDE completion, which is exactly why stubs are generated from the same metadata.
(Corrected 2026-10-03:) this repository's plugin generates stubs for the Kotlin-named modules,
under Kotlin names (SPEC B-7); the stubs that carry the Pythonic names and signatures belong to
pythonx itself, in pythonx-compose
(docs/archive/pyi-generation-pythonic-stubs.md is the
design that was removed from here).
JClass/JavaClass, KClass/KotlinClass, ObjcClass work only where the platform has the
thing they name. No stubs, no substitutes, no forced uniformity across targets — a target that
has no JVM has no JClass, and says so.
.pyi generation belongs to the Gradle plugin, the way PyREPL did it: read the resolved
artefacts at build time and emit stubs per source set. It is not a sibling of PythonProxySource,
which fills sys.modules at runtime; these are different products for different consumers, one for
the interpreter and one for the IDE. Long term that generator belongs to toolchain — see §5.
§5 was originally written by reading working copies, and it never opened an issue tracker — this
repository's own docs/roadmap/ROADMAP.md mentions the other repos zero times, and the same was true of GitHub issues.
That is the gap this section closes. Issues read in full, 2026-08-16:
gh issue list --repo thisisthepy/toolchain --state open
gh issue list --repo thisisthepy/pypackpack --state open
gh issue list --repo thisisthepy/python-multiplatform --state open
gh issue list --repo thisisthepy/pythonx-compose --state open
pythonx-compose has none open. Seven issues came back across the other three repos, each read with
gh issue view <n> --repo <repo>, then checked against the working copy (not against the issue's own
description of itself). "✅ done" below means this agent found the corresponding code and, where the issue
or a commit message made a narrower claim, checked that narrower claim too — not just that a file with a
plausible name exists.
| Checklist item | Issue's checkbox | Actual state (checked against code) |
|---|---|---|
| Create the basic structure of the plugin | ☑ checked | ✅ matches — PythonPlugin.kt registers the python extension and three tasks |
| Python version setup → Version Enum (alpha, rc, normal) | ☐ unchecked | ❌ matches — compileSdk is a plain String in DSLCore.kt; no enum anywhere |
| Build target platform setup → platform-specific min-SDK setting | ☐ unchecked | 🟡 partial — AndroidPlatformExtension.androidSdk: Int exists in DSLPlatforms.kt but no task reads it (per fb1dba7's own commit message: "platforms has a partial hook... mapping them silently would be wrong") |
| Build target platform setup → check Kotlin-side enabled build target | ☐ unchecked | ❌ matches — no code found that inspects which KMP targets are enabled |
| Hot reload / Code Push → expose a direct run button | ☐ unchecked | ❌ matches — HotReloadExtension/CodePushExtension (DSLPackaging.kt) are pure data holders; fb1dba7: "hotReload and codePush have no backend concept in packpack" |
SourceSet setup → implementation |
☐ unchecked | ✅ stale checkbox — wired since before fb1dba7; collectInstallDependencies reads implementations |
SourceSet setup → integration |
☐ unchecked | ✅ stale checkbox — fb1dba7 ("Feat: Make integration() install...") folds integrations into the same install list; it is treated identically to implementation, no KLIBDEPENS distinction (deliberately, per the same commit — nothing in pypackpack has that concept) |
| SourceSet setup → etcs (src/resource path, sourceset naming) | ☐ unchecked | ❌ matches — SourceSetConfig.srcDirs/metaDirs/libDirs (DSLBuild.kt) accept values via DSL but nothing reads them (fb1dba7: "metaDirs and libDirs have nothing in the resource bundler to bind to") |
| CompileLevel setup (debug/release, project flavors) | ☐ unchecked | 🟡 partial — debug/release selection via resolveActiveBuildType/-Ppython.buildType is wired; project flavors (buildFeatures) are not (fb1dba7: "buildFeatures has no consumer at all") |
| Automate the build process and integrate with KMP | ☐ unchecked | 🟡 largely done, not fully end-to-end verified by this agent — buildTask→installTask→packageTask chain runs and delegates real work to pypackpack's ResourceBundler (30a064c); this agent did not run usage-example to confirm a full build |
One line, no checklist: tcl install pythonx-compose. ❌ Not started — find . -iname "*tcl*" -o -iname "*lite*" in the toolchain working copy (excluding .git/build) returns nothing.
| Checklist item | Issue's checkbox | Actual state (checked against code) |
|---|---|---|
| Create the basic structure of the plugin | ☐ unchecked | (parent bullet, not independently checkable — see sub-items) |
| Python version setup → Version Enum | ☐ unchecked | ❌ same as toolchain#2 — no enum found in either repo |
| Build target platform setup → platform-specific min-SDK | ☐ unchecked | ❌ not found in pypackpack |
| Build target platform setup → check Kotlin-side enabled build target | ☑ checked | 🟡 not independently verified by this agent — no corroborating code found in the time available; recorded as unverified rather than disputed |
| Hot reload / Code Push → run button | ☐ unchecked | ❌ matches |
| SourceSet setup → implementation (python-only) | ☐ unchecked | 🟡 the underlying dependency-add path exists (DependencyBackend.addDependencies), but this is toolchain's DSL concept, not pypackpack's — checkbox sits on the wrong side of the boundary the ecosystem doc draws in §1 |
| SourceSet setup → integration (python+kotlin mixed) | ☐ unchecked | ❌ matches — no KLIBDEPENS handling anywhere in pypackpack (grepped, zero hits) |
| External tool detection → Nuitka | ☐ unchecked | ❌ matches — Nuitka.kt is 4 lines |
| External tool detection → Host Python | ☑ checked | ✅ matches — python install downloads a prebuilt CPython from python-multiplatform's GitHub releases (docs/SPEC.md, confirmed) |
| External tool detection → MSVC, Clang | ☐ unchecked | ❌ matches — both 4-line placeholders |
| External tool detection → Poetry or UV | ☑ checked | ✅ matches — dependency/backend/external/UV.kt is a real, exercised integration |
| External tool detection → Crossenv or equivalent | ☐ unchecked | 🟡 partial — CrossEnv.kt exists at 552 lines and is exercised by add/sync/tree per-target commands, but "creating a dedicated venv per target" is explicitly listed as not-yet-implemented in docs/SPEC.md |
| Compilation modes → instant (pure python) | ☑ checked | 🟡 not verified as a selectable mode — docs/SPEC.md states the --level CLI flag is "accepted but ignored" for build, i.e. there's exactly one behavior today, which happens to look like instant, not a selection mechanism |
| Compilation modes → bytecode/mixed/native | ☐ unchecked | ❌ matches — docs/SPEC.md lists all three as "Not yet implemented (target)" |
| Build Tools → configure Nuitka | ☐ unchecked | ❌ matches |
| Build Tools → compatibility testing with Android | ☐ unchecked | ❌ not found |
| Build Tools → minification | ☐ unchecked | ❌ matches — compile/middleware/minification/ has only the base interface |
| Checklist item | Issue's checkbox | Actual state (checked against code) |
|---|---|---|
Handle pyproject.toml → lossless modification |
☑ checked | ✅ matches — utils/toml/TomlEditor.kt + TomlValue.kt exist and are exercised (SPEC.md's target add/remove, package remove sections describe editing specific keys in place) |
Fix CrossEnv.kt abstraction → encapsulate UV logic in backend |
☑ checked | ✅ plausible, not fully re-derived — CrossEnv.kt's target-marker logic (MarkerPolicy) and per-target uv calls live in the middleware/backend layers this item describes; this agent did not diff against a pre-fix version to confirm the decoupling directly |
Middleware Refactoring → is DevEnv/CrossEnv still a redundant wrapper? |
☐ unchecked | ✅ matches, genuinely still open — DevEnv.kt (240 lines) still follows "resolve project root → delegate to backend → print result" for every method, the shape the issue questions. CrossEnv.kt (552 lines) has grown well past a simple wrapper, which is itself an argument the two are no longer symmetric — unresolved either way |
| Target Platforms → research a cleaner architecture for platform-specific deps via UV | ☐ unchecked | 🟡 partial — MarkerPolicy/Platforms.describeTarget (utils/Platforms.kt) already implement a platform_system/platform_machine marker scheme and thread --python-platform through tree/sync, i.e. a pattern exists; whether it counts as the "research" this item asks for is a judgment call this agent did not make either way |
settings.gradle.kts → uv |
☐ unchecked | ❓ could not resolve — the issue gives no elaboration beyond the single word "uv"; settings.gradle.kts today has no uv reference at all, but this agent could not determine what behavior the checkbox is asking for, so this is left open rather than guessed |
pypackpack#5 — "Managing Multi-Platform Dependencies in uv with Platform Markers" (help wanted, guide not checklist)
Not a checklist — a how-to document proposing platform_system/platform_machine PEP 508 markers per
target. 🟡 largely already matches the shipped design: MarkerPolicy.markerForTarget in
dependency/middleware/DefaultInterface.kt builds exactly platform_system == '...' and platform_machine == '...', and --python-platform is passed to uv tree/uv add per target. The one place code and guide
diverge in practice: docs/KNOWN_ISSUES.md records that the marker text uv add actually persists can
come back as platform_machine/sys_platform rather than platform_system/platform_machine, which
breaks remove --target's re-derived-marker matching for some dependencies. So the guide's approach is
implemented, with one recorded, unfixed bug in marker-round-tripping.
pypackpack#12 — "Ambiguous file name due to duplicated name on entirely codebase" (question, not checklist)
❌ Not addressed. The issue proposes renaming BaseInterface.kt/DefaultInterface.kt (duplicated
across dependency/, compile/backend/, compile/middleware/, bundle/, deploy/) to
FrontendInterface.kt/MiddlewareInterface.kt/BackendInterface.kt/DefaultMiddleware.kt/
DefaultBackend.kt. find . -iname "*Interface*.kt" in the current tree still shows BaseInterface.kt
and DefaultInterface.kt repeated in all five domains named in the issue, unchanged.
python-multiplatform#4 — "Add Python/C API expect declaration reclassification and actual definitions"
No checklist, two prose tasks. ✅ Closed by commit 4ac2eca6 ("Fix: Remove the drafts stranded in
comments, and reclassify what CPython deprecated"). The "20-member gap" this document had reported
(335 expect vs 315 actual on desktop) was a grep counting artifact: Kotlin block comments nest, and
old drafts inside comments were counted as live code. Comment-aware scanning shows the difference is
zero on all targets. The actual-definition half was already complete; the commit's work was
reclassification (removing PyEval_InitThreads, annotating four replacements as @Deprecated without
ReplaceWith because exact replacements exist but blind swaps would be incorrect, leaving three CPython-discouraged
functions alone), deleting three dead draft blocks from comments, and adding a test validating all three
targets against a canonical surface (466 test cases, all passing).
Per the task that produced this section: items in §5's existing "Gap to Goal" lists that also appear in an issue above are issue-sourced, and are now cross-referenced from within §5 itself (see the "Issues" note in each repository's block). Two kinds of mismatch, kept separate rather than merged:
- In an issue, missing from §5's original gap lists:
pypackpack#12's file-naming complaint;pypackpack#1'sDevEnv/CrossEnvredundancy question and its unexplainedsettings.gradle.kts/uvitem;toolchain#2's Python-version-enum and Kotlin-target-detection sub-items;pypackpack#2's compile-mode checklist (instant/bytecode/mixed/native) and Android-build-compatibility item. These were not in this document before this pass — they are what "renders §5 incomplete" in the sense the task described, now folded into each repository's "Issues" note above rather than duplicated again here. - In §5's original gap lists, absent from any issue:
PythonMultiplatform's@Composable-callable-shape gap (already closed per §4 item 1, and never was apython-multiplatformissue — it ispythonx-compose's problem surfacing in this repo's code), itssys.meta_pathlazy-import gap, and all ofpythonx-compose's "Gap to Goal" list. These are this document's own judgment calls, not sourced from a tracker — flagged in thepythonx-compose"Issues" note above since that repository has zero open issues to source anything from.
It describes five repositories and sits in one of them, because that is the only one with an active working copy and a documentation habit. If the organisation grows a place for cross-repo documents, this belongs there, and what stays here is the last section's first entry.