Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/workflows/samples-minimal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Guard for #195: samples/minimal is a from-published consumer (it resolves the
# released kplusplus plugin from mavenCentral/gradlePluginPortal, not the in-tree
# generator — see samples/minimal/README.md), so it cannot catch a generated-shape
# change at the moment the generator changes. The only moment it CAN catch one is
# when this sample itself next builds against a plugin version — which is exactly
# what silently broke here: the context-parameter migration (#145/#146) changed
# `origin` from a property to `origin()`/`_origin(v)` accessors, and nothing ran
# `samples/minimal:nativeTest` to notice until the 0.3.4 self-host bump (#193).
#
# This workflow closes that gap: any PR/push that touches samples/minimal (most
# notably a plugin-version bump in samples/minimal/build.gradle.kts, which is
# precisely the trigger that caused #195) runs `:nativeTest`, which forces
# src/nativeMain to compile AND exercises the generated bindings' behavior
# (src/nativeTest/kotlin/GeometryTest.kt). A shape drift fails the compile step
# loudly, the same way it did when this was reproduced by hand for #195.
#
# Deliberately NOT wired into the root build / release.yml: samples/minimal is
# explicitly not part of the root settings.gradle.kts (it's a standalone,
# from-published consumer proving the published artifact, not the in-tree one —
# see docs/ARCHITECTURE.md), and this workflow doesn't need the in-tree LLVM-linked
# :krapper build at all, so it's cheap and independent of the main build's slower
# self-hosting path.
name: samples/minimal

on:
push:
branches: [main]
paths:
- 'samples/minimal/**'
- '.github/workflows/samples-minimal.yml'
pull_request:
paths:
- 'samples/minimal/**'
- '.github/workflows/samples-minimal.yml'
workflow_dispatch:

jobs:
native-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

# The published krapper tool binary is LLVM-linked and needs clang++ on PATH at
# RUNTIME to parse cpp/geometry.h and compile it (see samples/minimal/README.md
# "Requirements") — no in-tree LLVM build required, unlike release.yml.
- name: Install LLVM/Clang 22 (apt.llvm.org)
run: |
set -euxo pipefail
sudo apt-get update
sudo apt-get install -y wget lsb-release gnupg ca-certificates software-properties-common
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
chmod +x /tmp/llvm.sh
sudo /tmp/llvm.sh 22 all
sudo ln -sf "$(command -v clang++-22)" /usr/local/bin/clang++
sudo ln -sf "$(command -v clang-22)" /usr/local/bin/clang
sudo ln -sf "$(command -v llvm-config-22)" /usr/local/bin/llvm-config
echo "clang++: $(clang++ --version | head -1); llvm-config: $(llvm-config --version)"

- name: samples/minimal :nativeTest
working-directory: samples/minimal
run: ./gradlew nativeTest --no-daemon
3 changes: 3 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ export JAVA_HOME=/usr/lib/jvm/java-21-openjdk
-Pkpp.bundleTools.krapper="$PWD/krapper/build/bin/klinker/krapperRelease/krapper"
# 2. Prove a from-published consumer resolves the bundled tool + runs from mavenLocal:
(cd samples/minimal && ./gradlew runReleaseExecutableKlinker)
# 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)
```

`publishToMavenLocal` does **not** sign (no key in the environment) — signing is exercised only
Expand Down
15 changes: 15 additions & 0 deletions samples/minimal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Expected output:

```
Rect area: 6.0
Rect origin: (0.0, 0.0)
distance (0,0)->(3,4): 5.0
center: (1.0, 1.5)
```
Expand All @@ -67,6 +68,20 @@ parses the header and compiles `cpp/geometry.cc`. JDK 21 is required to run the
The first build is slow (it builds the self-hosted front-end, generates the bindings, then
compiles and links the generated C++ wrapper); later runs are incremental.

## Keeping this sample honest

Because this sample resolves the **published** plugin rather than the in-tree generator, it
can't catch a generated-shape change (e.g. #145/#146's context-parameter migration) the moment
the generator changes — only the next time this sample itself builds against a new plugin
version. `src/nativeTest/kotlin/GeometryTest.kt` exercises every accessor kind the generator
emits for `cpp/geometry.h` (scalar `var` properties, `context(scope: MemScope)` get/set-style
accessors for object-valued fields, and a plain const-ref method), so `./gradlew nativeTest`
fails to compile — loudly — the moment the generated shape drifts from what `Main.kt`/the test
assume, exactly the way `rect.origin = …` did in #195. This is wired into CI at
[`.github/workflows/samples-minimal.yml`](../../.github/workflows/samples-minimal.yml) (runs on
any change under `samples/minimal/**`, most notably a plugin-version bump) and into the release
[local dry run](../../docs/releasing.md#local-dry-run-no-outward-upload).

## See also

- The repository [README](../../README.md) — quick start and the `kplusplus { }` DSL.
Expand Down
12 changes: 12 additions & 0 deletions samples/minimal/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ kotlin {
}
}
}

// `:nativeTest` (src/nativeTest/kotlin) asserts the generated bindings actually
// behave as documented, and — just as importantly — forces src/nativeMain to
// compile as part of an ordinary `check`/test run. This is the guard for #195:
// samples/minimal is a from-published consumer (resolves the released plugin, not
// the in-tree generator), so it can't catch a generated-shape change at the moment
// the generator changes — only at the moment this sample next builds against a new
// plugin version. Wiring nativeTest in means that moment fails loudly instead of
// silently, the way the `rect.origin = …` break in this issue did.
sourceSets["nativeTest"].dependencies {
implementation(kotlin("test"))
}
}

// The bindings can only be generated after libgeometry.a exists: kplusplusSync compiles
Expand Down
12 changes: 10 additions & 2 deletions samples/minimal/src/nativeMain/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,20 @@ fun main(args: Array<String>) {
// when it ends. `Point()` / `Rect()` are MemScope extension factories (see
// the generated companion objects), in scope thanks to the imports above.
memScoped {
// A 2x3 rectangle at the origin.
// A 2x3 rectangle at the origin. `origin` is a Point-VALUED field, so its
// accessors are `origin()` (get, needs a MemScope to allocate the returned
// Point into) / `_origin(value)` (set, no allocation needed) -- NOT a Kotlin
// `var` property. A getter that allocates can't be an ordinary property
// getter (no way to thread a MemScope through `rect.origin`), so the
// generator falls back to plain functions for object-valued fields. Scalar
// fields (width, height below; x, y on Point) need no allocation and DO stay
// ordinary `var` properties.
val rect = Rect()
rect.origin = Point().apply { x = 0.0; y = 0.0 }
rect._origin(Point().apply { x = 0.0; y = 0.0 })
rect.width = 2.0
rect.height = 3.0
println("Rect area: ${rect.area()}")
println("Rect origin: (${rect.origin()!!.x}, ${rect.origin()!!.y})")

// distanceTo takes the other Point by const-ref in C++ -> a plain Kotlin
// parameter here; the classic 3-4-5 right triangle gives 5.0.
Expand Down
68 changes: 68 additions & 0 deletions samples/minimal/src/nativeTest/kotlin/GeometryTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.
*/
import geo.Point
import geo.Point.Companion.Point
import geo.Rect
import geo.Rect.Companion.Rect
import kotlinx.cinterop.memScoped
import kotlin.test.Test
import kotlin.test.assertEquals

// The guard for #195: samples/minimal is a from-published consumer, so it only ever
// sees a generated-shape change (e.g. #145/#146's context-parameter migration) at the
// point this sample next resolves a new plugin version -- there is no in-tree signal.
// This test (and Main.kt, which it deliberately mirrors) exercises every accessor kind
// the generator emits for this header: scalar `var` properties (x/y/width/height),
// object-valued get/set pairs that need a MemScope (`origin()` / `_origin(v)`), a
// by-value-returning method that needs a MemScope (`center()`), and a plain const-ref
// method that doesn't (`distanceTo`). If the generated shape drifts, this fails to
// COMPILE (the same way `rect.origin = …` did) before it can fail to assert.
class GeometryTest {
@Test
fun rectExposesOriginAsAccessorsNotAProperty() =
memScoped {
val rect = Rect()
rect._origin(Point().apply { x = 1.0; y = 2.0 })

val origin = rect.origin()!!
assertEquals(1.0, origin.x)
assertEquals(2.0, origin.y)
}

@Test
fun rectAreaAndCenterMatchTheGeometry() =
memScoped {
val rect = Rect()
rect._origin(Point().apply { x = 0.0; y = 0.0 })
rect.width = 2.0
rect.height = 3.0

assertEquals(6.0, rect.area())

val center = rect.center()
assertEquals(1.0, center.x)
assertEquals(1.5, center.y)
}

@Test
fun pointDistanceToIsAPlainConstRefMethod() =
memScoped {
val a = Point().apply { x = 0.0; y = 0.0 }
val b = Point().apply { x = 3.0; y = 4.0 }

assertEquals(5.0, a.distanceTo(b))
}
}
Loading