From dd8515eb9b81e4194f05ed559606efc36cc8c210 Mon Sep 17 00:00:00 2001 From: monkopedia-coder Date: Wed, 29 Jul 2026 14:29:08 -0400 Subject: [PATCH] fix(plugin): shade the plugin's ksrpc/ktor/coroutines runtime into the jar (#194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Any MULTI-PROJECT consumer of 0.3.4 died on its first `kplusplusSync` with `NoSuchMethodError: Job.invokeOnCompletion$default`. Gradle gives each project's buildscript its own classloader whose parent is the root's, and loading is parent-first: a build that declares the Kotlin plugin only at the root has kotlin-compiler-runner's kotlinx-coroutines 1.8.0 on that PARENT, so a subproject applying kplusplus resolves coroutines 1.11.0 into its child loader and never sees it — ktor-io then calls a method 1.9+ moved onto the `Job` interface and 1.8.0 keeps in `Job$DefaultImpls`. Reachable since #185 put ksrpc (hence ktor + coroutines) on the plugin's runtime classpath for the first time. Option 1 of the issue: SHADE. The plugin jar now bundles ksrpc, ktor-io, kotlinx coroutines/serialization/io and jnanoid with their packages relocated under `com.monkopedia.kplusplus.compiler.shaded`, so nothing a parent classloader owns can shadow them and the plugin runs on the versions it was compiled against. Nothing left to relocate is published: the POM now declares only the Kotlin runtime pieces Gradle/KGP already supply (stdlib, gradle-plugin-api, reflect), which also retires the org.jetbrains:annotations `strictly 13.0` exclusions. Also adds samples/multiproject — the canary this class of failure needed. Every other sample is single-project, where both plugins land in one `plugins { }` block and one classloader, which is exactly why this shipped. Wired into the release local dry run (step 4). The root `plugins { id(...) apply false }` workaround from #193 STAYS for now, with its comment rewritten to say why: this repo self-hosts one release behind, on the already-published (unshaded) 0.3.4, so the workaround is load-bearing until the pluginManagement pin moves to 0.3.5. Verified removable by publishing this build to mavenLocal and running the whole gate without it. --- build.gradle.kts | 23 ++-- compiler/gradle/build.gradle.kts | 124 +++++++++++++++--- docs/getting-started.md | 11 ++ docs/releasing.md | 4 + gradle/libs.versions.toml | 2 + samples/multiproject/README.md | 54 ++++++++ .../multiproject/bindings/build.gradle.kts | 49 +++++++ samples/multiproject/bindings/cpp/counter.h | 30 +++++ samples/multiproject/build.gradle.kts | 25 ++++ samples/multiproject/gradle.properties | 8 ++ samples/multiproject/settings.gradle.kts | 35 +++++ 11 files changed, 333 insertions(+), 32 deletions(-) create mode 100644 samples/multiproject/README.md create mode 100644 samples/multiproject/bindings/build.gradle.kts create mode 100644 samples/multiproject/bindings/cpp/counter.h create mode 100644 samples/multiproject/build.gradle.kts create mode 100644 samples/multiproject/gradle.properties create mode 100644 samples/multiproject/settings.gradle.kts diff --git a/build.gradle.kts b/build.gradle.kts index 8f71c4d..ba584be 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,21 +1,14 @@ plugins { alias(libs.plugins.kotlin.multiplatform) apply false + // 0.3.4-ONLY WORKAROUND (#194) — DELETE THIS LINE with the pluginManagement bump to 0.3.5. // Declared here (never applied) purely to put the kplusplus plugin on the ROOT buildscript - // classpath, next to the Kotlin plugin. Its version comes from settings.gradle.kts's - // pluginManagement pin, exactly as in the modules that really apply it. - // - // WHY this is load-bearing, not tidiness: since 0.3.4 the plugin drives krapper over ksrpc, - // so its runtime needs kotlinx-coroutines 1.11 / ktor-io 3.5 in the Gradle daemon. Gradle - // gives each project's buildscript its OWN classloader whose PARENT is the root's, and - // class loading is parent-first. With the Kotlin plugin declared only here, the root - // classloader owns kotlin-compiler-runner's kotlinx-coroutines 1.8.0; a module that also - // applies the kplusplus plugin resolves coroutines 1.11.0 into its CHILD classloader, but - // `kotlinx.coroutines.Job` still loads from the parent at 1.8.0. ktor-io then calls - // `Job.invokeOnCompletion$default`, which 1.9+ moved onto the interface itself - // (-Xjvm-default=all) and 1.8.0 keeps in `Job$DefaultImpls`, so kplusplusSync dies with a - // NoSuchMethodError before krapper is even started. Declaring both plugins in the SAME - // (root) block puts them in ONE classpath, where Gradle's conflict resolution picks - // coroutines 1.11.0 and the parent-first lookup finds the right class. + // classpath next to the Kotlin plugin, so both resolve as ONE classpath. 0.3.4's plugin + // carried its ksrpc/ktor/coroutines runtime as ordinary dependencies, which a multi-project + // consumer resolves into a CHILD classloader while parent-first loading keeps serving the + // ROOT's older kotlinx-coroutines — `NoSuchMethodError: Job.invokeOnCompletion$default` + // before krapper is even started. 0.3.5 SHADES that runtime into the plugin jar (see + // compiler/gradle/build.gradle.kts), which is the actual fix; this line is only what keeps + // THIS repo building while it self-hosts one release behind, on 0.3.4. id("com.monkopedia.kplusplus.compiler") apply false alias(libs.plugins.ktlint) } diff --git a/compiler/gradle/build.gradle.kts b/compiler/gradle/build.gradle.kts index 98db7a6..fac66d9 100644 --- a/compiler/gradle/build.gradle.kts +++ b/compiler/gradle/build.gradle.kts @@ -24,32 +24,79 @@ plugins { // owns the Portal side. `publishToMavenLocal` still yields the same coordinates R1 did. alias(libs.plugins.vannik.publish) alias(libs.plugins.gradle.plugin.publish) + // #194: SHADE the plugin's runtime dependencies into the jar (see the `shaded` + // configuration + `shadowJar` block below for the full WHY). + alias(libs.plugins.shadow) } group = "com.monkopedia.kplusplus" version = "0.3.4" +// #194: the plugin's runtime dependencies are SHADED — bundled INTO the plugin jar with +// their packages relocated — instead of being declared for the consumer to resolve. +// +// WHY. Gradle gives every project's buildscript its own classloader whose PARENT is the root +// build's, and class loading is PARENT-FIRST. A multi-project build normally declares the +// Kotlin Gradle Plugin only at the ROOT, so the root classloader owns kotlin-compiler-runner's +// kotlinx-coroutines 1.8.0. A module that applies THIS plugin resolves coroutines 1.11.0 +// correctly into its CHILD classloader — and never sees it, because `kotlinx.coroutines.Job` +// keeps loading from the parent at 1.8.0. ktor-io then calls `Job.invokeOnCompletion$default`, +// which coroutines 1.9+ moved onto the interface itself (-Xjvm-default=all) while 1.8.0 keeps +// it in `Job$DefaultImpls`: NoSuchMethodError on the first `kplusplusSync` of every +// multi-project consumer (#194 — reachable since #185 put ksrpc, hence ktor + coroutines, on +// the plugin's runtime classpath). Relocating those packages under `shadedPrefix` makes their +// names unique, so there is nothing for a parent classloader to shadow them with, and the +// plugin runs on the exact versions it was compiled against. +// +// These are `shaded` -> `compileOnly`: compiled against the real coordinates, shipped as +// relocated bytecode, and published as NO dependency at all. +val shaded = configurations.create("shaded") { + isCanBeConsumed = false + isCanBeResolved = true +} + +val shadedPrefix = "com.monkopedia.kplusplus.compiler.shaded" + +// Every third-party package that lands in the jar. Anything bundled but NOT listed here keeps +// its original name and can still collide, so this must cover the whole `shaded` resolution: +// ksrpc (+ the jnanoid id generator it uses), kotlinx coroutines/serialization/io, ktor-io. +val shadedPackages = listOf( + "com.monkopedia.ksrpc", + "com.aventrix.jnanoid", + "kotlinx.coroutines", + // kotlinx-coroutines' stack-trace-recovery marker package — real classes with real + // methods, so a 1.8.0 copy on a parent classloader is the very hazard this fixes. + "_COROUTINE", + "kotlinx.serialization", + "kotlinx.io", + "io.ktor" +) +configurations.compileOnly { extendsFrom(shaded) } +// The tests exercise the session against the REAL (unrelocated) classes the main source set +// was compiled against, so they need those — and the un-bundled `shadow` set, which is on the +// compile classpath but on no test one — on their runtime classpath. +configurations.testImplementation { extendsFrom(shaded, configurations.shadow.get()) } + dependencies { - implementation(kotlin("stdlib")) - implementation(kotlin("gradle-plugin-api")) + // `shadow` is shadow's bucket for "compile against it, do NOT bundle it, DO declare it for + // the consumer": it is the only configuration whose dependencies reach the published POM + // once the shadow jar is the artifact (everything else is expected to be inside the jar). + // These three are the Kotlin runtime pieces the Gradle daemon / KGP already supply on the + // parent classloader — relocating them is not even meaningful. + shadow(kotlin("stdlib")) + shadow(kotlin("gradle-plugin-api")) + // ksrpc-api reaches for kotlin-reflect at runtime; it used to arrive transitively. + shadow(kotlin("reflect")) // For KotlinNativeCompilation (MPP-specific). compileOnly because the consumer's // build always has kotlin-gradle-plugin on the classpath. compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.0") // #185: the typed channel to krapper. ksrpc-sockets supplies the JVM // `ProcessBuilder.asConnection` that speaks the protocol over the subprocess's stdio // pipe; ksrpc-core + serialization + coroutines are what the generated stubs run on. - // - // Each one EXCLUDES org.jetbrains:annotations: a Gradle buildscript classpath pins that - // module to `strictly 13.0` (Gradle's embedded Kotlin), while coroutines asks for 23.0.0 - // — an unsatisfiable constraint that fails the consumer's build at configuration time. - // The excludes are declared per-dependency (not on the configuration) so they are carried - // in the published POM and protect a from-published consumer too. Dropping the module is - // safe: its contents are CLASS-retention annotations, and Gradle's own kotlin-stdlib - // supplies the 13.0 set anyway. - implementation(libs.ksrpc.core) { excludeJetbrainsAnnotations() } - implementation(libs.ksrpc.sockets) { excludeJetbrainsAnnotations() } - implementation(libs.serialization.json) { excludeJetbrainsAnnotations() } - implementation(libs.coroutines.core) { excludeJetbrainsAnnotations() } + shaded(libs.ksrpc.core) + shaded(libs.ksrpc.sockets) + shaded(libs.serialization.json) + shaded(libs.coroutines.core) // The subprocess-lifecycle tests fault-inject a stand-in `krapper` (see // KrapperSessionFailureTest); `java-gradle-plugin` already puts gradleApi() on the @@ -57,9 +104,6 @@ dependencies { testImplementation(kotlin("test-junit")) } -fun ExternalModuleDependency.excludeJetbrainsAnnotations() = - exclude(group = "org.jetbrains", module = "annotations") - // #185: ONE definition of the krapper service protocol, compiled for BOTH sides. // // The interfaces, the config/filter/fixup payloads and the resolved schema live in @@ -79,6 +123,52 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } +// #194: the PUBLISHED jar is the shadow jar — this module's own classes plus the `shaded` +// configuration, every bundled package moved under `shadedPrefix` (see the `shaded` block +// above for why). Only third-party runtime packages move; `com.monkopedia.kplusplus.**` (the +// plugin) and `com.monkopedia.krapper.**` (the wire protocol, compiled from shared source) +// keep their names — they are what the plugin IS. +tasks.shadowJar { + // Take the main jar's name: this IS the plugin artifact, not a fat side-car. + archiveClassifier.set("") + // Transformers (below) only see entries the copy spec did not already drop, and the Jar + // default (EXCLUDE) drops same-path duplicates BEFORE they reach one. + duplicatesStrategy = DuplicatesStrategy.INCLUDE + configurations.set(listOf(shaded)) + for (pkg in shadedPackages) { + relocate(pkg, "$shadedPrefix.$pkg") + } + // ServiceLoader registrations are resource FILES, so relocating the bytecode alone would + // leave them pointing at (and named after) classes that no longer exist — + // kotlinx-coroutines' CoroutineExceptionHandler / MainDispatcherFactory providers among + // them. mergeServiceFiles() rewrites both the entry names and their contents. + mergeServiceFiles() + // Never BUNDLE what the Gradle runtime already provides on the parent classloader, and + // what relocating would break anyway: the Kotlin runtime (stdlib + reflect), slf4j-api + // (Gradle's own logging API — a relocated copy would find no provider), and + // org.jetbrains:annotations (CLASS-retention only). Filtered here rather than as + // configuration-level excludes because `compileOnly` extends `shaded`, and exclude rules + // are inherited down that chain — they would strip the Kotlin API off our OWN compile + // classpath. + dependencies { + exclude(dependency("org.jetbrains.kotlin:.*:.*")) + exclude(dependency("org.jetbrains:annotations:.*")) + exclude(dependency("org.slf4j:.*:.*")) + } + // Dependency-jar metadata that must not be republished: signatures (invalid once the jar + // is rebuilt) and module-info descriptors (they name the ORIGINAL modules/packages). + exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") + exclude("**/module-info.class") +} + +// The plain `jar` would write the same archive name as the shadow jar above, and there is +// nothing left for it to produce: the shadow plugin already makes its jar the artifact of the +// `java` component, so the shadow jar is what gets published, what the Plugin Portal uploads, +// and what an `includeBuild` consumer substitutes to. +tasks.jar { + enabled = false +} + gradlePlugin { website.set("https://github.com/Monkopedia/kplusplus") vcsUrl.set("https://github.com/Monkopedia/kplusplus.git") diff --git a/docs/getting-started.md b/docs/getting-started.md index 2139c58..dda101b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -46,6 +46,15 @@ repositories { } ``` +In a **multi-project** build, apply this in whichever project owns the bindings; the plugin +does not need to be declared at the root alongside the Kotlin plugin. (Under **0.3.4 only** it +did — the plugin's own runtime classes were shadowed by whatever the root buildscript +classloader owned, and the first `kplusplusSync` failed with +`NoSuchMethodError: Job.invokeOnCompletion$default`. Adding +`id("com.monkopedia.kplusplus.compiler") apply false` to the root `plugins { }` block works +around it on 0.3.4; upgrading is the real fix. See +[#194](https://github.com/Monkopedia/kplusplus/issues/194).) + ## 2. Configure the binding Add a `kplusplus { }` block pointing at the header(s) and library you want to bind: @@ -109,6 +118,8 @@ cryptic `file not found` deep in the parse. When `llvm-config` reports a default ## See also - [`samples/minimal`](../samples/minimal) — the smallest end-to-end binding, a working copy of this guide. +- [`samples/multiproject`](../samples/multiproject) — the same thing as two Gradle projects, with the + Kotlin plugin declared at the root. - [`samples/v8`](../samples/v8) — the heavyweight demo (binds and runs V8). - [README](../README.md) — the `kplusplus { }` DSL reference and `fixup { }` escape hatch. - [docs/ARCHITECTURE.md](ARCHITECTURE.md) — how the self-hosted front-end and generator fit together. diff --git a/docs/releasing.md b/docs/releasing.md index abbd18a..a5b6483 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -99,6 +99,10 @@ export JAVA_HOME=/usr/lib/jvm/java-21-openjdk # 3. ...and that the generated bindings still match what the sample expects (the # `samples/minimal:nativeTest` guard for #195 — see samples/minimal/README.md): (cd samples/minimal && ./gradlew nativeTest) +# 4. ...and that a MULTI-PROJECT consumer can drive the plugin at all (the #194 guard: the +# plugin's runtime is shaded into its jar precisely so a subproject's classloader can't be +# shadowed by the root's — see samples/multiproject/README.md): +(cd samples/multiproject && ./gradlew :bindings:build) ``` `publishToMavenLocal` does **not** sign (no key in the environment) — signing is exercised only diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 38b9b97..15bbdfd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ klinker = "0.2.0" gradle-plugin-publish = "2.1.1" vannik = "0.37.0" binary-compatibility-validator = "0.18.0" +shadow = "9.6.1" [libraries] coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } @@ -32,3 +33,4 @@ klinker = { id = "com.monkopedia.klinker.plugin", version.ref = "klinker" } gradle-plugin-publish = { id = "com.gradle.plugin-publish", version.ref = "gradle-plugin-publish" } vannik-publish = { id = "com.vanniktech.maven.publish", version.ref = "vannik" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } diff --git a/samples/multiproject/README.md b/samples/multiproject/README.md new file mode 100644 index 0000000..0bf76a4 --- /dev/null +++ b/samples/multiproject/README.md @@ -0,0 +1,54 @@ +# samples/multiproject + +The **multi-project canary**. Two Gradle projects: a root that declares the Kotlin Gradle +Plugin, and a `:bindings` subproject that applies kplusplus to one header-only C++ struct. +It is not a showcase — `samples/minimal` is the sample to read first. This one exists to keep +a specific failure mode visible. + +## Why it exists + +Gradle gives every project's buildscript its own classloader whose **parent** is the root +build's, and class loading is **parent-first**. A multi-project build normally declares the +Kotlin plugin once, at the root — which puts `kotlin-compiler-runner`'s own +`kotlinx-coroutines` on that *parent* classloader. A subproject applying kplusplus resolves +the coroutines version the plugin actually needs into its *child* classloader and never sees +it, because the parent answers first. + +That is [#194](https://github.com/Monkopedia/kplusplus/issues/194): kplusplus 0.3.4 drove +krapper over ksrpc (#185), ktor-io called `Job.invokeOnCompletion$default` — a method +coroutines 1.9+ moved onto the interface and 1.8.0 keeps in `Job$DefaultImpls` — and every +multi-project consumer died with `NoSuchMethodError` on its first `kplusplusSync`. The fix +shades the plugin's runtime dependencies into the plugin jar (see +`compiler/gradle/build.gradle.kts`). It shipped broken because **every other sample is +single-project**, where the two plugins land in one `plugins { }` block and one classloader, +and nothing can shadow anything. + +So: keep this sample two projects, and keep the Kotlin plugin declared **only at the root**. +That is the entire test. What it binds does not matter. + +## Run it + +The plugin is resolved by coordinate (`mavenLocal()` first), so publish the build under test +first — the release local dry run, from the repo root: + +```sh +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk +./gradlew :krapper:linkReleaseExecutableKlinker +./gradlew -p compiler publishToMavenLocal \ + -Pkpp.bundleTools.krapper="$PWD/krapper/build/bin/klinker/krapperRelease/krapper" +``` + +Then, from this directory: + +```sh +../../gradlew :bindings:kplusplusSync +``` + +`kplusplusSync` is the canary: the classloader failure happens when the plugin opens the +service channel to krapper, before any binding is generated. Compiling the generated Kotlin +(`../../gradlew :bindings:build`) exercises the rest, but is not what this sample is for. + +`:bindings:build` is step 4 of the release **local dry run** ([docs/releasing.md](../../docs/releasing.md)), +which is where this sample is exercised. It has no CI workflow of its own — unlike +`samples/minimal` it cannot run against the *released* plugin, since the plugin under test is +the point. diff --git a/samples/multiproject/bindings/build.gradle.kts b/samples/multiproject/bindings/build.gradle.kts new file mode 100644 index 0000000..15bb179 --- /dev/null +++ b/samples/multiproject/bindings/build.gradle.kts @@ -0,0 +1,49 @@ +/* + * Copyright 2026 Jason Monk + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// kplusplus is applied HERE, in a subproject, while the Kotlin plugin comes from the root's +// `plugins { }` block (see ../build.gradle.kts and ../settings.gradle.kts for why that +// arrangement is the whole point of this sample). The version pin matches samples/minimal's: +// this is a from-published consumer. +plugins { + kotlin("multiplatform") + id("com.monkopedia.kplusplus.compiler") version "0.3.4" +} + +repositories { + mavenLocal() + mavenCentral() +} + +kotlin { + linuxX64("native") { + compilations.all { + compileTaskProvider.configure { + compilerOptions { + optIn.add("kotlinx.cinterop.ExperimentalForeignApi") + // The generated bindings carry `context(scope: MemScope)` on their + // allocating members, which needs Kotlin 2.4 context parameters. + freeCompilerArgs.add("-Xcontext-parameters") + } + } + } + } +} + +// One header, no library, no fixups — see cpp/counter.h. +kplusplus { + header("cpp/counter.h") +} diff --git a/samples/multiproject/bindings/cpp/counter.h b/samples/multiproject/bindings/cpp/counter.h new file mode 100644 index 0000000..42e8a6f --- /dev/null +++ b/samples/multiproject/bindings/cpp/counter.h @@ -0,0 +1,30 @@ +/* + * Copyright 2026 Jason Monk + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +// The whole C++ surface of the multi-project canary: header-only (every method is defined +// inline, so there is no library to build or link) and std-free, so a sync here exercises the +// PLUGIN, not the binding generator. Deliberately boring — what is under test is whether the +// plugin can talk to krapper at all from a subproject's classloader (#194). +struct Counter { + int value; + + Counter() : value(0) {} + explicit Counter(int start) : value(start) {} + + int next() { return ++value; } + int peek() const { return value; } +}; diff --git a/samples/multiproject/build.gradle.kts b/samples/multiproject/build.gradle.kts new file mode 100644 index 0000000..e85e04f --- /dev/null +++ b/samples/multiproject/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright 2026 Jason Monk + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// The Kotlin plugin is declared HERE, at the root, and applied in :bindings — the ordinary +// multi-project convention (one version pin for the whole build), and the one that made #194 +// reachable: this block is what puts kotlin-compiler-runner's own kotlinx-coroutines on the +// ROOT buildscript classloader, the PARENT of the classloader :bindings' kplusplus plugin +// loads from. kplusplus is deliberately NOT declared here — a consumer should not have to +// know about any of this. +plugins { + kotlin("multiplatform") version "2.4.0" apply false +} diff --git a/samples/multiproject/gradle.properties b/samples/multiproject/gradle.properties new file mode 100644 index 0000000..c53a52b --- /dev/null +++ b/samples/multiproject/gradle.properties @@ -0,0 +1,8 @@ +org.gradle.jvmargs=-Xmx4096m +# The single custom native target is named "native", which clashes with the source sets the +# default hierarchy template creates; this sample doesn't use them (mirrors samples/minimal). +kotlin.mpp.applyDefaultHierarchyTemplate=false +# Generate the bindings through the cpp front-end. The key is the PROJECT name — `bindings`, +# the subproject that applies the plugin, not the root. The krapper tool binary comes from the +# bundled plugin jar; it is LLVM-linked, so an LLVM/Clang toolchain must be on PATH at runtime. +kpp.frontend.bindings=cpp diff --git a/samples/multiproject/settings.gradle.kts b/samples/multiproject/settings.gradle.kts new file mode 100644 index 0000000..88379a6 --- /dev/null +++ b/samples/multiproject/settings.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Jason Monk + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +rootProject.name = "multiproject" +pluginManagement { + repositories { + // A from-published consumer, exactly like samples/minimal: the plugin (and the + // `krapper` binary bundled inside it) is resolved by coordinate, with mavenLocal + // first so a locally published build under test wins over the released one. + mavenLocal() + gradlePluginPortal() + mavenCentral() + } +} + +// THE POINT OF THIS SAMPLE (#194): a consumer with more than one project. Everything else in +// samples/ is single-project, where the Kotlin plugin and the kplusplus plugin are declared in +// the same `plugins { }` block and share one buildscript classloader — the arrangement in which +// the plugin's own runtime classpath can never be shadowed. Here the Kotlin plugin is declared +// at the ROOT and kplusplus is applied in a SUBPROJECT, so the plugin runs in a CHILD +// classloader whose parent already owns a (different, older) kotlinx-coroutines. That is the +// shape that shipped broken in 0.3.4. +include(":bindings")