Skip to content

Commit b207906

Browse files
authored
chore(build): enforce explicit API + klib ABI (#87)
1 parent 6efca93 commit b207906

10 files changed

Lines changed: 969 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,17 @@ jobs:
143143
with:
144144
cache_read_only: ${{ github.event_name == 'pull_request' }}
145145

146+
# apiCheck now validates the klib/native ABI too, so it compiles native
147+
# klibs and needs the Kotlin/Native toolchain. Cache ~/.konan to avoid
148+
# re-downloading it every run.
149+
- name: Cache Konan (Kotlin/Native toolchain)
150+
uses: actions/cache@v6
151+
with:
152+
path: ~/.konan
153+
key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties') }}
154+
restore-keys: |
155+
konan-${{ runner.os }}-
156+
146157
- name: Run API compatibility check
147158
run: ./gradlew apiCheck --stacktrace
148159

build-logic/convention/src/main/kotlin/MqttKmpLibraryConventionPlugin.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ class MqttKmpLibraryConventionPlugin : Plugin<Project> {
4848
.toInt(),
4949
)
5050

51+
// Strict explicit API mode: every public/protected declaration must
52+
// state its visibility and return type explicitly. Compiler-enforced
53+
// companion to the Konsist allowlist (ADR-0008) and BCV apiCheck.
54+
explicitApi()
55+
5156
jvm()
5257
iosArm64()
5358
iosSimulatorArm64()

core/api/core.klib.api

Lines changed: 857 additions & 0 deletions
Large diffs are not rendered by default.

core/build.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ kotlin {
5959
}
6060
}
6161

62+
apiValidation {
63+
// Validate the full cross-platform ABI (klib/native + common), not JVM only.
64+
// With klib enabled, `apiDump` writes a merged <module>.klib.api baseline next
65+
// to the JVM dump under api/, and `apiCheck` validates both. Targets a CI host
66+
// can't build are skipped by BCV and trusted from the committed dump —
67+
// regenerate the full dump on a macOS host via `./gradlew apiDump`.
68+
@OptIn(kotlinx.validation.ExperimentalBCVApi::class)
69+
klib {
70+
enabled = true
71+
}
72+
}
73+
6274
dokka {
6375
dokkaSourceSets.named("commonMain") {
6476
includes.from(layout.projectDirectory.file("Module.md"))

core/src/commonMain/kotlin/org/meshtastic/mqtt/MqttTransport.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,19 @@ package org.meshtastic.mqtt
3131
*/
3232
public interface MqttTransport {
3333
/** Establish a connection to the broker. */
34-
suspend fun connect(endpoint: MqttEndpoint)
34+
public suspend fun connect(endpoint: MqttEndpoint)
3535

3636
/** Send raw bytes over the transport. */
37-
suspend fun send(bytes: ByteArray)
37+
public suspend fun send(bytes: ByteArray)
3838

3939
/** Receive one complete MQTT packet as raw bytes. */
40-
suspend fun receive(): ByteArray
40+
public suspend fun receive(): ByteArray
4141

4242
/** Close the transport connection. */
43-
suspend fun close()
43+
public suspend fun close()
4444

4545
/** Whether the transport is currently connected. */
46-
val isConnected: Boolean
46+
public val isConnected: Boolean
4747
}
4848

4949
/**

core/src/commonMain/kotlin/org/meshtastic/mqtt/packet/VariableByteInt.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ public data class VbiResult(
3434
* Maximum 4 bytes, maximum value 268,435,455.
3535
*/
3636
public object VariableByteInt {
37-
const val MAX_VALUE: Int = 268_435_455
37+
public const val MAX_VALUE: Int = 268_435_455
3838

3939
/**
4040
* Encode an integer as a Variable Byte Integer.
4141
* @throws IllegalArgumentException if [value] exceeds [MAX_VALUE] or is negative.
4242
*/
43-
fun encode(value: Int): ByteArray {
43+
public fun encode(value: Int): ByteArray {
4444
require(value in 0..MAX_VALUE) { "Value $value out of VBI range 0..$MAX_VALUE" }
4545

4646
val buf = ByteArray(4)
@@ -61,7 +61,7 @@ public object VariableByteInt {
6161
* @return A [VbiResult] containing the decoded value and number of bytes consumed.
6262
* @throws IllegalArgumentException if the encoding is malformed.
6363
*/
64-
fun decode(
64+
public fun decode(
6565
bytes: ByteArray,
6666
offset: Int = 0,
6767
): VbiResult {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Klib ABI Dump
2+
// Targets: [iosArm64, iosSimulatorArm64, linuxArm64, linuxX64, macosArm64, mingwX64]
3+
// Rendering settings:
4+
// - Signature version: 2
5+
// - Show manifest properties: true
6+
// - Show declarations: true
7+
8+
// Library unique name: <MQTTastic-Client-KMP:transport-tcp>
9+
final class org.meshtastic.mqtt.transport.tcp/TcpTransport : org.meshtastic.mqtt/MqttTransport { // org.meshtastic.mqtt.transport.tcp/TcpTransport|null[0]
10+
constructor <init>() // org.meshtastic.mqtt.transport.tcp/TcpTransport.<init>|<init>(){}[0]
11+
12+
final val isConnected // org.meshtastic.mqtt.transport.tcp/TcpTransport.isConnected|{}isConnected[0]
13+
final fun <get-isConnected>(): kotlin/Boolean // org.meshtastic.mqtt.transport.tcp/TcpTransport.isConnected.<get-isConnected>|<get-isConnected>(){}[0]
14+
15+
final suspend fun close() // org.meshtastic.mqtt.transport.tcp/TcpTransport.close|close(){}[0]
16+
final suspend fun connect(org.meshtastic.mqtt/MqttEndpoint) // org.meshtastic.mqtt.transport.tcp/TcpTransport.connect|connect(org.meshtastic.mqtt.MqttEndpoint){}[0]
17+
final suspend fun receive(): kotlin/ByteArray // org.meshtastic.mqtt.transport.tcp/TcpTransport.receive|receive(){}[0]
18+
final suspend fun send(kotlin/ByteArray) // org.meshtastic.mqtt.transport.tcp/TcpTransport.send|send(kotlin.ByteArray){}[0]
19+
}
20+
21+
final class org.meshtastic.mqtt.transport.tcp/TcpTransportFactory : org.meshtastic.mqtt/MqttTransportFactory { // org.meshtastic.mqtt.transport.tcp/TcpTransportFactory|null[0]
22+
constructor <init>() // org.meshtastic.mqtt.transport.tcp/TcpTransportFactory.<init>|<init>(){}[0]
23+
24+
final fun create(org.meshtastic.mqtt/MqttEndpoint): org.meshtastic.mqtt/MqttTransport // org.meshtastic.mqtt.transport.tcp/TcpTransportFactory.create|create(org.meshtastic.mqtt.MqttEndpoint){}[0]
25+
final fun supports(org.meshtastic.mqtt/MqttEndpoint): kotlin/Boolean // org.meshtastic.mqtt.transport.tcp/TcpTransportFactory.supports|supports(org.meshtastic.mqtt.MqttEndpoint){}[0]
26+
}

transport-tcp/build.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ kotlin {
4949
}
5050
}
5151

52+
apiValidation {
53+
// Validate the full cross-platform ABI (klib/native + common), not JVM only.
54+
// With klib enabled, `apiDump` writes a merged <module>.klib.api baseline next
55+
// to the JVM dump under api/, and `apiCheck` validates both. Targets a CI host
56+
// can't build are skipped by BCV and trusted from the committed dump —
57+
// regenerate the full dump on a macOS host via `./gradlew apiDump`.
58+
@OptIn(kotlinx.validation.ExperimentalBCVApi::class)
59+
klib {
60+
enabled = true
61+
}
62+
}
63+
5264
dokka {
5365
dokkaSourceSets.named("commonMain") {
5466
includes.from(layout.projectDirectory.file("Module.md"))
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Klib ABI Dump
2+
// Targets: [iosArm64, iosSimulatorArm64, linuxArm64, linuxX64, macosArm64, mingwX64, wasmJs]
3+
// Rendering settings:
4+
// - Signature version: 2
5+
// - Show manifest properties: true
6+
// - Show declarations: true
7+
8+
// Library unique name: <MQTTastic-Client-KMP:transport-ws>
9+
final class org.meshtastic.mqtt.transport.ws/WebSocketTransport : org.meshtastic.mqtt/MqttTransport { // org.meshtastic.mqtt.transport.ws/WebSocketTransport|null[0]
10+
constructor <init>() // org.meshtastic.mqtt.transport.ws/WebSocketTransport.<init>|<init>(){}[0]
11+
12+
final val isConnected // org.meshtastic.mqtt.transport.ws/WebSocketTransport.isConnected|{}isConnected[0]
13+
final fun <get-isConnected>(): kotlin/Boolean // org.meshtastic.mqtt.transport.ws/WebSocketTransport.isConnected.<get-isConnected>|<get-isConnected>(){}[0]
14+
15+
final suspend fun close() // org.meshtastic.mqtt.transport.ws/WebSocketTransport.close|close(){}[0]
16+
final suspend fun connect(org.meshtastic.mqtt/MqttEndpoint) // org.meshtastic.mqtt.transport.ws/WebSocketTransport.connect|connect(org.meshtastic.mqtt.MqttEndpoint){}[0]
17+
final suspend fun receive(): kotlin/ByteArray // org.meshtastic.mqtt.transport.ws/WebSocketTransport.receive|receive(){}[0]
18+
final suspend fun send(kotlin/ByteArray) // org.meshtastic.mqtt.transport.ws/WebSocketTransport.send|send(kotlin.ByteArray){}[0]
19+
}
20+
21+
final class org.meshtastic.mqtt.transport.ws/WebSocketTransportFactory : org.meshtastic.mqtt/MqttTransportFactory { // org.meshtastic.mqtt.transport.ws/WebSocketTransportFactory|null[0]
22+
constructor <init>() // org.meshtastic.mqtt.transport.ws/WebSocketTransportFactory.<init>|<init>(){}[0]
23+
24+
final fun create(org.meshtastic.mqtt/MqttEndpoint): org.meshtastic.mqtt/MqttTransport // org.meshtastic.mqtt.transport.ws/WebSocketTransportFactory.create|create(org.meshtastic.mqtt.MqttEndpoint){}[0]
25+
final fun supports(org.meshtastic.mqtt/MqttEndpoint): kotlin/Boolean // org.meshtastic.mqtt.transport.ws/WebSocketTransportFactory.supports|supports(org.meshtastic.mqtt.MqttEndpoint){}[0]
26+
}

transport-ws/build.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ kotlin {
7474
}
7575
}
7676

77+
apiValidation {
78+
// Validate the full cross-platform ABI (klib/native + common), not JVM only.
79+
// With klib enabled, `apiDump` writes a merged <module>.klib.api baseline next
80+
// to the JVM dump under api/, and `apiCheck` validates both. Targets a CI host
81+
// can't build are skipped by BCV and trusted from the committed dump —
82+
// regenerate the full dump on a macOS host via `./gradlew apiDump`.
83+
@OptIn(kotlinx.validation.ExperimentalBCVApi::class)
84+
klib {
85+
enabled = true
86+
}
87+
}
88+
7789
dokka {
7890
dokkaSourceSets.named("commonMain") {
7991
includes.from(layout.projectDirectory.file("Module.md"))

0 commit comments

Comments
 (0)