Skip to content

Konan Plugin 2.0 - #18

Merged
lemcoder merged 57 commits into
mainfrom
feature/konan-dsl
Nov 3, 2026
Merged

lemcoder merged 57 commits into
mainfrom
feature/konan-dsl

Conversation

@lemcoder

@lemcoder lemcoder commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Summary

Konan Plugin 2.0: the feature/konan-dsl branch, published as 2.0.0-alpha01 through 2.0.0-alpha04.

  • One typed DSL, konan { }: libraries (default clang build, CMake, Make, Cargo, Zig, prebuilt, external), bindings and machine paths (config { }) in one extension. It replaces 1.x's DSL; the README's migration notes cover the move.
  • Bindings generated from cinterop's own NativeIndex, inside the Kotlin/Native distribution, for every Kotlin/Native release from 2.0.0 to 2.4.21:
    • Kotlin/Native through KGP's cinterops { };
    • JNI (Kotlin externals and C stubs): strings as real UTF-8, primitive buffers, structs by value, typed handles for pointers to records;
    • FFM through jextract, on the JDK and on Android through PanamaPort.
  • Android libraries built with the Android NDK, libc++ linked in; JVM jars and Android packages bundle only their own targets' libraries.
  • Kotlin Multiplatform: JVM, Android and native targets wired from one declaration.
  • CI on Linux x64.

See CHANGELOG.md for alpha03 and alpha04.

Not released yet: typed handles for pointers to records (#17) landed after 2.0.0-alpha04 and are breaking for code that passed those pointers as Long.

Fixes #14
Fixes #16

🤖 Generated with Claude Code

lemcoder and others added 30 commits September 28, 2026 23:15
- kotlin-native-utils 2.2.10 -> 2.4.20; examples on KGP 2.4.20.
- Gradle wrapper 8.11.1 -> 9.8.0 (KGP 2.5 needs >= 8.14.4), plugin-publish 1.3.0 -> 2.2.1.
- Since 2.4 the cinterop generator locates jni.h itself via java.home, then JAVA_HOME. The java on
  PATH is often a JBR without include/, so pass the JDK already resolved for the stub as JAVA_HOME.
- Gradle 9 validatePlugins fails on tasks without a caching declaration: mark the toolchain-dependent
  tasks @DisableCachingByDefault and give stubCFile a path sensitivity.
- JVM example reads resolvedLibraryDirectory instead of reconstructing the jniLibs path, which went
  stale when host libraries moved to lib/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
third_party/kotlin pins JetBrains/kotlin at the v2.4.20 release, checked out sparsely to
kotlin-native/Interop: the cinterop indexer (NativeIndex), StubIr and the stub generator, for
reading and testing against the same version as the distribution the plugin runs.

A plain `git submodule update --init` would check out the whole repository, since sparse-checkout
is not recorded; scripts/init-kotlin-submodule.sh does the sparse, blob-less fetch instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Components of the planned rewrite (DSL, target model, toolchain, library provider, binding spec,
generators, glue builder, packaging), what the Kotlin/Native distribution ships, and the open design
questions. Sections A-D are outlines still to be designed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion

cinterop builds a C-level model of the headers a .def names (NativeIndex) before it generates any
Kotlin. Reuse that model instead of parsing cinterop's generated JVM bindings:

- An exporter (src/indexExporter) indexes the headers with cinterop's own buildNativeIndex and writes
  the result as JSON. Structs, enums and typedefs go into tables referenced by id, since the model is a
  graph: a struct can point to itself.
- ExportNativeIndexTask runs it with javaexec, on a classpath of only the distribution's compiler jar and
  the exporter jar (nested in the plugin jar). Not in the daemon, where libclang may already be loaded by
  KGP's cinterop, and not in a Gradle worker, which would put Gradle's Kotlin stdlib and the plugin's
  classes next to the compiler jar's own.
- NativeIndexReader reads the file back into upstream's model classes, vendored unmodified from
  JetBrains/kotlin into src/main with a three-symbol shim for what they borrow from the libclang-bound
  half of the indexer.
- kotlinNativeVersion in gradle.properties is the one version for kotlin-native-utils, the vendored
  model, the exporter's compile-only compiler jar and the distribution it runs in.
  updateNativeIndexModel re-vendors the model; verifyNativeIndexModel (part of check) fails while the
  stamps disagree. The wiring moves to a buildSrc convention plugin.

Round-trip tests export a sample header covering unions, bit-fields, anonymous members, enums, typedef
chains, function pointers, flexible arrays, varargs and macro constants, and compare the exporter's dump
of the index it built with the dump of the one read back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The NativeIndex model is now vendored into src/main and refreshed with updateNativeIndexModel, so
nothing builds from third_party/kotlin any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The exporter reimplemented cinterop's internal buildNativeLibrary to turn a .def into a NativeLibrary,
missing clang modules, -header and the header-filter search prefixes. It now takes a cinterop command
line after `--` and follows processCLib's own steps: CInteropArguments, prepareTool, DefFile,
buildNativeLibrary, buildNativeIndex. The index for the sample header is unchanged.

prepareTool and buildNativeLibrary are internal to the compiler jar, so the exporter compiles with that
jar as a friend module; a signature change fails compilation at the kotlinNativeVersion bump. Java is not
an alternative: `native` is a Java keyword, so Java cannot import any org.jetbrains.kotlin.native class.

Dependency klibs (-library) are rejected rather than silently ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plugin no longer reads the NativeIndex back: generators run in the exporter's process, next to the
index cinterop built, and only their output files leave it. This drops everything that carried the index
across the process boundary: the vendored NativeIndex model and its shim, the JSON writer and reader,
the updateNativeIndexModel/verifyNativeIndexModel tasks and the shared dump source set.

- BindingGenerator is the exporter's generator interface; generators are chosen by name on its command
  line (--generator <name> --output <dir> -- <cinterop command line>).
- The dump becomes the first generator, writing index.txt.
- ExportNativeIndexTask becomes GenerateFromNativeIndexTask, taking a generator name and an output
  directory.

The tests run the dump generator through TestKit and check signatures, a self-referential struct and
macro constants, and that an unknown generator fails naming the known ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NativeIndexer no longer indexes anything: it holds the Kotlin/Native version the exporter was compiled
against and finds that distribution. Rename it PinnedKotlinNative (version, home()) and its resource to
kotlin-native.properties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The JNI path ran cinterop's JVM flavor, whose bridges pass every pointer as a raw address, and rewrote
its output with regexes so strings and buffers crossed as Kotlin types. A jni generator in the index
exporter now writes both sides of each bridge from the C declarations:

- each C function becomes an `external fun` under its own name (`my_add`, not `kniBridge0`), with its C
  signature as a doc comment;
- integers, char, bool, floating point and enums cross as the same-size primitive; `const char*` as
  String? both ways; pointers to integers or floating point as primitive arrays, written back unless the
  pointee is const; structs by value as their bytes, size-checked; other pointers as addresses;
- functions it cannot bind (variadic, long double) are listed with the reason.

GenerateJvmInteropTask keeps its outputs, so linking and packaging are unchanged. Generators get a
GenerationContext (index, package, headers, the .def's preamble, --option pairs), and runIndexExporter is
the one launcher for the exporter.

Removed: JniMarshalling, CInteropFormatter and their tests, the empty ExampleTest, DefFile.render and the
fields only it read, and the unused detectKonanHome. The JVM example gains string and buffer functions
and calls the bindings by their C names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a com.android.base module both wireIntoAndroid and wireJniLibsIntoAndroid added the per-ABI jniLibs
directory to every variant, and mergeDebugJniLibFolders failed with "Duplicate resources" for each
library. wireJniLibsIntoAndroid already covers com.android.base, so wireIntoAndroid now adds only the
generated Kotlin sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A jextract generator in the index exporter writes java.lang.foreign bindings into java/<package>/.
jextract parses the headers with its own libclang, so the work splits: cinterop's index decides what is
bound — every function, struct, union, typedef, constant and global that passed the .def's header filter
becomes a jextract --include-* filter, so the FFM and JNI bindings cover the same declarations — and
jextract decides how. Only the -I/-D/-U compiler options reach jextract; the bindings load the library
with System.loadLibrary.

The two parsers model typedef'd anonymous records differently: cinterop folds
`typedef union { … } bits_t` into a record named bits_t, jextract keeps it a typedef and binds it only
under --include-typedef. Records and enums spelled without a struct/union/enum keyword are passed as
typedefs.

GenerationContext now carries the clang arguments cinterop parsed with. The test runs when JEXTRACT points
at a jextract executable and is skipped otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A klib is a compiler artifact, so Kotlin/Native bindings come from KGP rather than the index exporter:
the klib must match the consumer's Kotlin version, not the pinned one the exporter runs in.

kotlinNativeInterop(name) { defFile; includeDirs; compilerOptions; packageName; targets } is an adapter:
it declares KGP's own cinterops {} entry on the main compilation of every Kotlin/Native target, so KGP
builds the klib with the project's Kotlin version and handles the per-target tasks, -Xmodule-name,
commonization, the IDE and publication. Where konanConfig builds the library for a target, cinterop gets
-libraryPath to its archive directory and runs after runKonanClang. The KGP code is reached only inside
plugins.withId("org.jetbrains.kotlin.multiplatform"); the plugin compiles against KGP compileOnly. The
native example uses the adapter instead of a hand-written cinterops {} block.

Running cinterop's own pipeline inside the exporter (Interop().interop("native", …)) was tried: it works
and yields a klib matching KGP's apart from the header hash and unique_name, but is pinned to
kotlinNativeVersion. What it left behind is kept: the index and library facts in GenerationContext are
built on first use, so a generator that needs neither skips parsing the headers, and
GenerateFromNativeIndexTask takes further cinterop options (extraCinteropArgs).

TestKit loads the plugin under test where a separately applied KGP cannot see it, so the KMP test passes
KGP on the plugin-under-test classpath itself; the other tests keep proving the plugin loads without KGP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring the interop architecture doc up to date with what was built and verified: cinterop's NativeIndex
as the binding spec, the index exporter and why it runs in its own process, the two kinds of generator
(exporter generators jni, jextract and dump; the KGP cinterop adapter), what the jextract and cinterop
spikes taught (two parsers need translation rules; a klib must come from the consumer's Kotlin), what
each consumer needs for packaging and which of it is still done by hand, and the open questions that
remain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One project extension, `konan { libraries { }; bindings { } }`, replaces
konanConfig, jvmInterops and kotlinNativeInterop. A binding references
its library as an object, and consumers (Kotlin/Native via KGP's
cinterop, JNI, FFM, Android) are wired from the project's plugins.

- Library providers: `LibraryProvider` with Kotlin/Native's clang as the
  default and Zig (`zig { }`, the project's own build.zig) as a second
  build system, both behind `compileNativeLibrary<Name>` and the
  `NativeArtifacts` layout contract.
- Typed exporter protocol: a sealed `GeneratorRequest` and
  `ExporterInvocation`, compiled into both the plugin and the index
  exporter, replace the string-keyed generator options.
- Enums and sealed classes in place of string choices: ZigOptimize,
  GeneratedVisibility, LibraryProviderKind, GradlePluginId, KonanTarget-only
  targets; tool executables are file properties.
- Sources reorganised into dsl, library, jni, link, toolchain, index and
  wiring packages; README, examples and design doc updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consumers: each way a project uses a binding is a BindingConsumer
(KotlinNativeConsumer, JniConsumer, FfmConsumer), prepared eagerly and
registered after evaluation when the project's plugins call for it,
replacing the branching in wiring/Bindings.kt.

JNI stub builds: a sealed JniStubBuild, either the plugin's own
KonanLink or an External build. External builds share one task type,
ExternalBuildTask, which runs a list of commands per target with the
plugin's contract (KONAN_JNI_STUB_DIR, KONAN_JNI_LIB_NAME,
KONAN_JNI_OUTPUT_DIR, KONAN_TARGET, KONAN_JNI_ABI, JAVA_HOME) as
environment, and can copy a library the build leaves elsewhere into
place.

- jni { external { step { } } } runs any build system.
- jni { cmake { } } is now a preset over it: configure and build in one
  task, per-target settings keyed by KonanTarget, `buildTargets` for
  CMake's --target.
- Programs are a sealed Executable (File, OnPath), resolved when the
  task runs; CMake and Zig use it.
- The contract is also on JniSpec as providers for arguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LibraryProvider becomes a sealed hierarchy matching JniStubBuild:
KonanClang, or External with Generic (`external { step { } }`, any build
system) and Zig as a preset. External library builds share
ExternalBuildTask with the JNI stub builds: one task per target, the
ExternalLibraryVariable contract (KONAN_LIBRARY_NAME,
KONAN_LIBRARY_OUTPUT_DIR, KONAN_LIBRARY_SOURCE_DIR, KONAN_TARGET) as
environment, the produced archive copied to lib<name>.a.

- ZigBuildTask is gone; Zig runs per target as zigBuild<Name><Target>
  and is up to date while its project is unchanged.
- The provider kind enum, registry and conventions hook are replaced by
  the selected sealed value and DSL-constructor conventions.
- The plugin's JNI link decides onlyIf from its own staticLibrary, so
  the configuration cache no longer captures the library's TaskProvider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`libraries { create(…) { prebuilt { archive(target, file) } } }` takes an
archive per target built elsewhere and places it as <target>/lib<name>.a,
so consumers cannot tell it from a built one and a Windows name.lib works
unchanged. Targets default to the ones with an archive; a declared target
without one fails configuration, and a missing file fails
prebuilt<Name> naming it (a Copy task would skip it as NO-SOURCE).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- model/: the domain without tasks or KGP. NativeTarget is the plugin's
  own target type, one entry per Kotlin/Native target, with family,
  architecture, Android ABI and library naming; plus HostOs, task
  naming, NativeArtifacts, the external-build contract, Executable,
  DefFile and selectOne.
- kotlin/: the only package importing org.jetbrains.kotlin.
  NativeTarget.of(konanTarget), KonanDistribution (home, pinned
  version, run_konan clang/llvm-ar, NDK), KotlinGradlePlugin (the KGP
  adapter) and the index exporter client. kotlin-native-utils becomes
  compileOnly; NativeTargetTest fails when a Kotlin version adds or
  retires a target.
- toolchain/: Jdk, Xcode, executable lookup and Zig triples, replacing
  NativeToolchain.
- dsl/: KonanExtension split into one file per spec. FFM gets its own
  linkerOptions, and jextract is an Executable.
- Duplicates merged: one BindingVisibility shared with the exporter
  protocol, one ExternalBuildContext, one registerExternalBuilds, one
  taskName/capitalized, one selectOne, one HostOs.

BREAKING: targets are NativeTarget (KonanTarget.X -> NativeTarget.X,
hostKonanTarget() -> NativeTarget.host), Executable moved to model, and
jniHome() is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`ffm()` in an Android module now replaces JNI there as it does in
kotlin("jvm"): jextract's bindings are moved from java.lang.foreign onto
PanamaPort's com.v7878.foreign (and its VarHandle), PanamaPort is added
as a dependency (ffm.panamaPortVersion, default v0.1.5), and the library
is linked as lib<name>.so per 64-bit ABI into jniLibs with no stub.

- GeneratorRequest.Jextract carries an FfmApi (JAVA, PANAMA_PORT); the
  exporter rewrites the packages for PANAMA_PORT.
- FFM links are per target (linkFfm<Name><Target>) under a linkFfm<Name>
  lifecycle task.
- AgpWiring handles Kotlin or Java sources, and its registrations take an
  enabled check evaluated inside onVariants, so JNI's Android wiring
  steps aside when FFM replaces it.
- examples/android-ffm runs end to end on an emulator. Apps need
  compileSdk 37 and minSdk 26 (PanamaPort) and Java 21 source (jextract).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared library FFM bindings load becomes a sealed FfmLibraryBuild,
shaped like JniStubBuild: KonanLink (the default, the library's archive
linked whole), Prebuilt (`ffm { prebuilt { sharedLibrary(target, file) } }`)
or External.Generic (`ffm { external { step { } } }`, run per target by
ExternalBuildTask with the ExternalFfmVariable contract:
KONAN_FFM_LIB_NAME, KONAN_FFM_OUTPUT_DIR, KONAN_TARGET, KONAN_FFM_ABI).

- ffm.libraryName (default: the library's name, else the binding's) is
  what the bindings load, so a vendor library can keep its own name.
- ffm.targets defaults to the host on a JVM and 64-bit ABIs on Android,
  narrowed to the library's; neither external nor prebuilt needs a
  library(…), and the default link without one now warns.
- Library and FFM prebuilts share one PlacePrebuiltFilesTask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`ffm { cmake { } }` builds the library FFM bindings load with the
project's CMakeLists, per target: the contract as cache entries and
CMAKE_LIBRARY_OUTPUT_DIRECTORY set to where the bindings load it, so
add_library(${KONAN_FFM_LIB_NAME} SHARED …) is all the CMakeLists needs.
It builds for its target(…) blocks, else for ffm.targets; on Android the
NDK toolchain is passed per ABI.

The CMake commands move to external/CMakeCommands.kt, shared by the JNI
and FFM presets. JNI's CMake build now also receives the whole contract
as cache entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ries in the jar

Toolchain export: konanToolchain<Target> writes cc/c++ wrappers around
the clang run_konan would run (captured once, no JVM per compile), an
ar, and a CMake toolchain file. External builds choose with
ExternalToolchain (AUTO: Kotlin/Native's when cross-compiling; KONAN;
SYSTEM) and then get CC, CXX, AR and KONAN_CMAKE_TOOLCHAIN_FILE; the
CMake preset passes the toolchain file unless it uses a preset. Make and
CMake default to AUTO, generic external steps to SYSTEM.

- Per-target link flags live in KonanDistribution.linkArguments. Adds
  -fuse-ld=lld for Linux, so the plugin's own links for Linux now work
  from a macOS host.
- `make { }` preset for libraries, jni and ffm: make -C <dir> -j<n>
  CC=… CXX=… AR=… <contract> per target.
- JNI external build tasks are named *BuildJni<Name><Target>, so they no
  longer clash with a library of the same name.

Libraries in the jar: in a kotlin("jvm") project or a multiplatform jvm()
target, each JVM target's JNI library and the FFM library go into the jar
under konan/natives/<target>/ (bundleJni<Name>, bundleFfm<Name>;
bundleInJar to opt out). A loader generated into the bindings tries
System.loadLibrary, then extracts the platform's library from the jar;
jextract's loadLibrary is rerouted to it. The JVM example needs no
java.library.path any more.

Also gives examples/native the daemon memory the Kotlin/Native compiler
needs (repeated links ran out of Metaspace).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `libraries { create(…) { cargo { } } }` builds a Rust crate
  (crate-type = ["staticlib"]) per target with `cargo build --lib
  --target <rust triple>`; its archive becomes the library's, so every
  consumer binds Rust like C. Crates with C code get Kotlin/Native's
  compilers as CC_<triple>/CXX_<triple>/AR_<triple>, the cc crate's
  target-specific names; the build context now carries the toolchain
  files (ToolchainFiles) and only some builds export the standard names.
- In a multiplatform project ffm() serves the jvm() targets: Java into
  the <target>Main source set, the library into the jar. Android targets
  keep JNI. JNI's sources leave jvm() lazily, with the compile's
  dependency on generation spelled out.
- KonanDistribution.ensureDependencies fetches a target's sysroot when
  missing (run_konan never does) by linking a trivial program; the
  compile, link and toolchain tasks call it first.
- Zig's linux_arm64 triple is pinned to glibc 2.25, Kotlin/Native's.
- Tests for androidTargets(), linkerOptionsFor, both argumentsFor
  overloads, multiplatform FFM and JNI ordering, and Cargo.

Verified: one jar built on macOS runs on Linux arm64 and x86-64; Rust
through JNI on the JVM and on an Android arm64 emulator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…0.0–2.4.21

Machine paths come from one place: konan { config { } }, then the Gradle
property konan.config.<field>, then the environment, with strict errors and
a konanConfig report. Nothing is searched for; jni.h ships with the plugin,
so no JDK is needed.

The index exporter is compiled once per Kotlin/Native patch release from
2.0.0 to 2.4.21, against that release's compiler jar, and the plugin runs
the one matching konanHome's version; any other version is an error. What
differs between releases lives in src/indexExporter/compat, chosen by
version thresholds. kotlinNativeDistribution() follows the project's Kotlin
plugin version, and multiplatform jvm() targets get withJava() before
Kotlin 2.1.20 so FFM's Java is compiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
examples/kmp: one binding behind an expect/actual API in a multiplatform
library — KGP's cinterop for iosArm64, iosSimulatorArm64, macosArm64 and
linuxX64, generated JNI for the Android target — and an Android app over
it. Its common tests run on the iOS simulator and macOS; the Linux binaries
are cross-linked on macOS.

Documents what a multiplatform build needs from the user: cinterop
commonization for a shared native source set calling the binding, and that
JS and Wasm targets get no bindings (with notes in CLAUDE.md on what binding
them would take).

The generated JNI loader reads os.name and os.arch null-safely, which
Android's nullable System.getProperty warned about.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted as the DSL

Build systems: a library's archive, a binding's JNI library and the library
FFM loads were three sealed hierarchies (LibraryProvider, JniStubBuild,
FfmLibraryBuild) with the same External branch each. They are now one
ExternalBuild per build system (CMake, Make, Cargo, Zig, generic steps),
registered from an ExternalBuildOutput that carries the only per-owner
differences: targets, output directory, file name, extra variables.
Every block's settings implement the sealed BuildSettings; calling a block
selects its settings, and owners dispatch on PrebuiltSettings or
ExternalBuildSettings.

External builds read one contract (ExternalVariable): KONAN_LIB_NAME,
KONAN_OUTPUT_DIR, KONAN_TARGET, KONAN_ABI for all, KONAN_SOURCE_DIR for
libraries, KONAN_JNI_STUB_DIR and KONAN_JNI_INCLUDE_DIR for JNI. This
replaces KONAN_LIBRARY_*, KONAN_JNI_LIB_NAME/OUTPUT_DIR/ABI and KONAN_FFM_*.

JNI is generated by GenerateFromNativeIndexTask like FFM; the JNI-only task
is gone, and the generator's layout is GeneratorRequest.Jni's constants.
JNI and FFM share BindingLibrarySpec and BindingLibraryWiring: directories,
the link<Side> task, target defaulting, the chosen build, the jar bundle.

DSL changes: jni.archive is removed (a prebuilt library replaces it); both
prebuilt blocks take library(target, file); jni { cmake { libraryDirectory } }
is copied from, as for libraries and FFM; cmake target(...) blocks choose a
binding library's targets when targets is unset; ffm gains
linkerOptionsFor; the build blocks' types (ExternalToolchain, ZigOptimize,
CargoProfile) move to io.github.lemcoder.dsl.builds.

Packages follow the DSL: dsl, dsl.config, dsl.libraries, dsl.bindings
(.jni, .ffm) and dsl.builds; the implementation in libraries and bindings
(.jni, .ffm); wiring keeps AGP, plugin ids and the config report.

A test now links the JNI library for the host and Android for real: a link
reading the stub through a provider without its producer passes a dry run,
and Gradle rejects it only when several links execute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dsl.libraries now also holds a library's registration and builds, and
dsl.bindings (with .jni and .ffm) a binding's registration, consumers,
shared wiring and links — one package per DSL block, its implementation
internal. The top-level libraries and bindings packages are gone; the
bundled jni.h moves to io/github/lemcoder/dsl/bindings/jni/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The remaining shared packages are split by owner:
- dsl.config: ConfigField, ConfigSource, ConfigValue and the konanConfig report
- dsl.libraries: NativeArtifacts, the layout a library publishes
- dsl.bindings: DefFile; dsl.bindings.jni: stubLibraryBaseName (JniNaming)
- dsl.builds: the external builds (ExternalBuild, ExternalBuildTask, each
  build system's commands), their contract, selectOne, Zig and Rust triples
- kotlin: Xcode, used only by the distribution's links

external/ and toolchain/ are gone; model keeps the shared domain
(NativeTarget, HostOs, Executable, taskName) and wiring AGP and plugin ids.
Tests follow their classes. ConfigField, ConfigSource and ConfigValue are
public and now live in io.github.lemcoder.dsl.config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e over C, Rust and C++

Android gives apps neither libc++ nor an unwinder, and the plugin's Android
links left both undefined: a C++ library (operator new, std::string,
exceptions) or a Rust staticlib (std's _Unwind_*) linked fine and failed on
the device with "cannot locate symbol". Kotlin/Native's NDK has all three —
libc++_static.a and libc++abi.a in its sysroot, the unwinder in
lib/gcc/<triple>/4.9.x/libgcc_real.a — so LinkSharedLibraryTask now ends
every Android link with them (KonanDistribution.androidRuntime) and links
with -Wl,--no-undefined, so a missing symbol fails the build instead. An
archive only contributes what is used: C libraries link as before.

examples/android replaces examples/android and examples/android-ffm: one app
over three libraries — C (clang) and Rust (cargo) through JNI, C++ (clang)
through FFM on PanamaPort — built from this checkout with includeBuild.
It runs on an arm64 emulator and a phone. scripts/run-android.sh follows it.

A test links a C++ library using new, std::vector and an exception for
android_arm64, which fails without the runtime archives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l has

feature/konan-dsl still said 1.2.0-alpha05, already on the Plugin Portal
as the previous plugin, so publishToMavenLocal overwrote that version in
~/.m2 with the new DSL: every project pinning 1.2.0-alpha05 with
mavenLocal() first stopped configuring, while CI kept building.

The DSL changed without a compatibility shim, so the branch is now
2.0.0-alpha01, which the Portal does not have. checkVersionUnpublished
runs before publishToMavenLocal and publishPlugins and fails when the
version's marker POM is already on the Portal; it skips offline or when
the Portal cannot be reached.

Fixes #8

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lemcoder and others added 27 commits October 26, 2026 12:12
With a preset, configure ran `cmake --preset <p>` with no -B, so it
configured into the preset's binaryDir, while the build step ran
`cmake --build <buildDirectory>`: the build failed, or configure wrote its
cache entries into a directory another build owned (koinference's library
and JNI builds shared one through a preset). -B overrides a preset's
binaryDir, so a set buildDirectory — the target block's, else the block's —
is now passed with the preset too, and configure and build agree.

A target block's unset buildDirectory now falls back to the block's; it
used to shadow it, because the property exists either way.

Fixes #4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bundleJni<Name> (and bundleFfm<Name>) put only the JVM targets' libraries
in the jar but depended on link<Side><Name>, which builds every target's:
jvmTest built the Android JNI libraries too — a full NDK build per ABI,
and a failure on a machine without one.

BindingLibraryWiring now records each target's build — the plugin's link,
an external build's per-target task, or the prebuilt placement — and the
bundle depends on the JVM targets' only.

Fixes #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A library must embed one archive per target, but a CMake project built with
CPM or add_subdirectory leaves one per add_library — whisper.cpp leaves
libwhisper.a, libggml.a, libggml-base.a, libggml-cpu.a and more — and every
consumer had to merge them by hand, per platform.

A library's external build now writes into a staging directory, which is
what KONAN_OUTPUT_DIR and CMake's CMAKE_ARCHIVE_OUTPUT_DIRECTORY name,
emptied before each run. ExternalBuildTask then copies the one archive it
finds, or merges several into lib<name>.a with Kotlin/Native's llvm-ar
(quick-append of each archive's members, then an index; darwin format for
Apple targets), the build's own lib<name>.a first. Consumers — cinterop's
-staticLibrary, the JNI and FFM links — are unchanged.

A test builds two archives, one needing the other, and links a JNI library
from the merged one.

Fixes #7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An Android cross-build through cmake { }, make { }, cargo { } or
external { } with no toolchain of its own compiled with Kotlin/Native's
bundled Android toolchain — NDK r20, clang 8 — and nothing said so. Modern
C++ needs a current NDK, and archives must match the libc++ they link
against.

konan.config.androidNdk, read like every other machine path (build script,
Gradle property, ANDROID_NDK_HOME), gives Android targets on the AUTO
toolchain that NDK instead: its per-API clang drivers, llvm-ar and
llvm-ranlib as CC, CXX, AR and RANLIB; for CMake its android.toolchain.cmake
with ANDROID_ABI and ANDROID_PLATFORM from androidApiLevel; and
ANDROID_NDK_HOME in the environment for presets that read it. Its files are
checked when the build reads them. Without one, such a build warns that it
uses Kotlin/Native's toolchain; toolchain = KONAN keeps it without the
warning. The plugin's own links keep Kotlin/Native's toolchain.

Fixes #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…quire

The plugin built Android libraries with Kotlin/Native's toolchain: its
clang 21, but NDK r20's sysroot, libc++ and LLD 8, with sysroots only up
to API 29. An archive built with a current NDK — what llama.cpp and
whisper.cpp need — then linked against an older libc++ than it was
compiled for.

konan.config.androidNdk (or ANDROID_NDK_HOME) is now required for Android
targets and used by every Android build: the default library build
compiles and archives with the NDK's <triple><api>-clang and llvm-ar; the
JNI and FFM links compile the stub with its C driver and link with its C++
driver, -static-libstdc++ (libc++, libc++abi and libunwind inside each
library, no libc++_shared) and -Wl,--no-undefined; external builds on AUTO
get its compilers, failing rather than falling back. Only an external
build with toolchain = KONAN still uses Kotlin/Native's. androidApiLevel
reaches every API level the NDK has. The tasks read the NDK as a config
value checked when they run, and track its path and revision as an input.

Kotlin/Native's Android runtime archives (androidRuntime) and sysroot
override (androidSysroot) are gone. A test compiles C++ needing a newer
libc++ than r20's and C calling an API 30 function, and checks the
library was linked by the NDK's LLD without libc++_shared; a build without
an NDK fails saying how to set one. scripts/run-android.sh uses the SDK's
newest NDK when ANDROID_NDK_HOME is unset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in uses

Android targets require an NDK, but an Android module already names one:
android { ndkVersion }, else AGP's default. konan.config.androidNdk now
falls back to it, after the build script, the Gradle property and
ANDROID_NDK_HOME, so an Android module needs nothing set. It is the
project's own configuration, read through AndroidComponents.sdkComponents
.ndkDirectory, not a search.

AgpWiring sets it, for both the application/library plugins and the
multiplatform Android library plugin. AGP throws when that NDK is not
installed; the value then reads as unset, so the plugin's own error says
what to set. konanConfig reports the source as the Android Gradle plugin's
NDK. scripts/run-android.sh no longer exports the SDK's newest NDK, which
as an environment variable would have overridden ndkVersion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The features list predated the build presets (CMake, Make, Cargo), archive
merging and Android's NDK; installation gains a step for Android's NDK,
which the Android Gradle plugin's usually provides; the migration notes
cover the NDK requirement and merged archives; run-android.sh no longer
publishes the plugin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A binding whose .def had no `package =` line, and that set no
packageName, failed with Gradle's generic "property 'request' doesn't
have a configured value", naming neither the binding nor the fix.
cinterop accepts such a .def and names the package after the file, so a
.def that worked with plain cinterop failed here.

The package now falls back, as cinterop's does, to the .def's file name,
made a valid package name (koinference-llama.def → koinference_llama).

Fixes #11

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every CMake build ran `cmake --build --parallel` with no job count, which
CMake leaves to the generator: with Unix Makefiles, CMake's default on
macOS and Linux, that is an unbounded `make -j`, one compiler per ready
source. Gradle then ran several such builds at once (#9), and five
llama.cpp builds exhausted 32 GB and froze the machine.

cmake { } gains `jobs`, defaulting to the processor count as make { }'s
does, passed as --parallel <jobs>. And every external build — CMake, Make,
Cargo, Zig, generic steps — now uses one shared build service with
maxParallelUsages 1, so builds that each use every core never stack; the
Gradle property konan.externalBuilds.maxParallel raises it.

Tests: the build step's arguments carry the count; two builds that each
record when they ran do not overlap by default, and do with the limit
raised to 2.

Fixes #10

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cinterop<Binding><Target>, the JNI link and the FFM library for one target
all depended on compileNativeLibrary<Name>, which builds every target: a
macosArm64Test built the iOS, macOS x64 and Linux archives too, all at
once, and a machine that could not build one target could run no native
test. With #10's unbounded make -j, five llama.cpp builds side by side
froze a 32 GB machine.

Each library now records its build per target (NativeLibrarySpec
.buildsByTarget), and NativeArtifacts.builtBy(target) gives a consumer that
target's task: the external build's per-target task, the prebuilt
placement, or — the default build being split for this — one
konanClang<Name><Target> per target, whose archive directory is now a
declared output. It is read lazily, when the graph is built, so libraries
and bindings may be declared in either order. compileNativeLibrary<Name>
stays the task that builds them all.

Fixes #9

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AgpWiring hung the jniLibs merge and preBuild off link<Side><Name>, which
builds every target's library: assembling an Android variant also built
the host's JNI library, which no APK or AAR holds — a minute of llama.cpp,
and a failure on a machine that cannot build the host.

BindingLibraryWiring.androidBuilds gives packaging the Android targets'
builds only. preBuild and the multiplatform Android compile now depend on
generation alone: for FFM that is generateFfmAndroid, rather than
linkFfm<Name>, which also generated the JDK bindings and built the host.

Fixes #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cmake { } had no sources, unlike make { } and external { }, so every
cmakeBuild task ran on every invocation: a configure and a no-op build per
target, and nothing after it could be up to date.

sources feeds ExternalBuildTask.sources, as for make { }. It has no
default: a CMakeLists that reads outside its own directory
(add_subdirectory(../llama.cpp)) would stay up to date after an edit there.

Fixes #13

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ative

The first workflow: the test suite on ubuntu-24.04 with JetBrains' Kotlin/Native
2.4.20, and on ubuntu-24.04-arm with voize-gmbh's unofficial 2.4.21 build for
Linux arm64 hosts (pinned by checksum), for which JetBrains ships none.

The arm64 job may fail without failing the workflow until it is known to pass.
Tests skip rather than fail when a tool is missing, so the job summary lists
every skipped and failed test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
targets(NativeTarget.host, …, NativeTarget.LINUX_ARM64) names linux_arm64 twice
on a Linux arm64 host, and the plugin registered its tasks twice: "Cannot add
task 'konanClangMymathLinux_arm64' as a task with that name already exists".
A build script naming its host explicitly breaks the same way on any host.

Libraries and bindings register from distinctTargets, the declared targets
without repeats.

Tests that need Kotlin to know the build host now skip where it does not:
Kotlin/Native has no Linux arm64 host, and KGP and kotlin-native-utils from
Maven throw "Unknown host target" there, though the plugin runs on an
unofficial distribution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
download.jetbrains.com returns 404 for the 2.4.20 prebuilt distribution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ad it

ensureDependencies found missing dependencies in the clang command run_konan
echoes, but read it through clangCommand, which requires clang to run. On a
fresh machine LLVM itself is missing, so run_konan failed with "Cannot run
program .../llvm-21-x86_64-linux-essentials-116/bin/clang" before anything
was downloaded. run_konan echoes the command before running it, so the echo
is now read whatever the outcome.

The Android NDK test's API 30 source is C++: NDK 27's android/thermal.h names
its enums and structs without their tags, which C rejects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GetStringUTFChars and NewStringUTF speak modified UTF-8: a character
outside the BMP reached C as two encoded surrogates on HotSpot, and
NewStringUTF given a 4-byte sequence is undefined, which CheckJNI aborts.

A const char* now crosses as a NUL-terminated UTF-8 ByteArray. The public
binding keeps its String? signature and wraps a private external named
<name>$kni; '$' cannot occur in a C identifier, so it clashes with no
binding.

Fixes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
addKmpJvmSources added the generated directory through a provider of its own,
which carries no producer, and patched only the compile tasks with dependsOn.
Any other reader of the source set raced generation: a published Android or
jvm() sources jar failed Gradle's implicit-dependency check. The directory is
now a file collection built by the generation task, so every reader depends on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
konanHome.set(kotlinNativeDistribution()) names the directory the Kotlin
plugin installs in downloadKotlinNativeDistribution (KGP 2.3 and newer),
but nothing ordered the plugin's tasks after it: on a machine without that
distribution, generateJni<Name> could run first and fail.

Every UsesKonanDistribution task now depends on that task when it exists
and konanHome is that directory. The dependency is a project.provider:
given konanHome.map, Gradle asks the mapped property for its producer,
finds none, and never evaluates the mapping.

Fixes #16

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It ran on voize-gmbh's unofficial Kotlin/Native build, where KGP knows no
host; CI now tests Linux x64 on JetBrains' distribution only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pointer to a named struct or union was a bare Long: any Long compiled
where a tsf* was expected, and a NULL result was 0L that the next call
dereferenced. Each such record now gets
@JvmInline value class <Tag>Pointer(val address: Long), named by the
record's tag (or an anonymous record's typedef), so node_t* and
struct node* are one NodePointer.

Parameters take the handle non-null; results are nullable, NULL being
null. The external keeps the Long behind the <name>$kni wrapper strings
already use: Kotlin mangles the JVM name of a function with a value-class
parameter, which JNI could not resolve.

Typedef'd void*, function pointers and pointers to pointers are unchanged.
Bindings that passed these pointers as Long must use the handle type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds a C counter, passes its handle back, and gets NULL as null: a
wrapper whose $kni external did not resolve would fail the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lemcoder
lemcoder merged commit 7f86b67 into main Nov 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant