CI¶
Each runner builds only the targets it can host. The matrix passes a selection per job — the same kmptargets.targets strings used locally in kmp-targets.local.properties or -P. Why per-host selection pays (macOS pricing): Design.
The matrix¶
name: Build
on:
push:
branches: [main]
pull_request:
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
targets: jvm,android,web,linuxX64,linuxArm64,androidNative
- os: macos-latest
targets: apple
- os: windows-latest
targets: windows
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '23'
- uses: gradle/actions/setup-gradle@v6
# Quoted on purpose: PowerShell (windows-latest) splits unquoted commas into an array.
- run: ./gradlew build "-Pkmptargets.targets=${{ matrix.targets }}"
Host → target mapping¶
| Runner | kmptargets.targets |
Rationale |
|---|---|---|
ubuntu-latest |
jvm,android,js,wasmJs,wasmWasi,linuxX64,linuxArm64,androidNative |
every non-Apple, non-MinGW target cross-compiles here (android requires AGP in your build; web is the js,wasmJs,wasmWasi preset) |
macos-latest |
apple (or appleMobile on PRs) |
the only host that can compile/link Apple targets |
windows-latest |
mingwX64 (alias windows) |
the only host for MinGW |
Notes¶
- Same vocabulary. Matrix values are plain selection grammar strings — presets, leaves, and
-exclusions all work. What CI builds is whatkmpTargetsInfoprints locally for the same value. - Env form.
ORG_GRADLE_PROJECT_kmptargets.targetsas a jobenv:is equivalent;-Pis the canonical recommendation. Both sit above the committedkmp-targets.propertiesin precedence. - Cost tier. Restrict the macOS job to
appleMobileonpull_requestand run the fullapplepreset only onpushtomain. - Strict mode. Set
kmptargets.strict=trueon jobs whose selection the host can fully compile — a misconfigured module fails the job instead of silently building nothing. - Configuration cache. Each distinct selection is its own configuration-cache key; matrix jobs with different selections don't share an entry. See Design.
Umbrella tasks¶
kmpCompileAll and kmpTestAll are opt-in lifecycle tasks that depend on exactly the registered intersection — one stable task name per job, correct in every lane and under a renamed jvm target.
Hardcoded task lists fail two ways under a variable selection: a name absent from the lane (compileReleaseKotlinAndroid on an android-less selection) fails with Task '…' not found; under a renamed jvm target a literal compileKotlinJvm matches zero tasks and the job passes without compiling.
kmpCompileAll— compiles every registered target's main compilation.kmpTestAll— runs every registered target's tests (targets without a test task, like device-onlyiosArm64, are skipped).
Opt in — the tasks add dependency edges to every registered compile/test task, and most builds only want them for CI:
The flag reads through the same precedence chain as every kmptargets. key.
Semantics:
- Registered in every module the plugin is applied to, even without
supports { }. An unqualified./gradlew kmpCompileAllfrom the root fans out to every project that has the task — no root aggregator. A module with nothing registered is a no-op, never a 404. - They depend on registered platform compilations only, never on
*KotlinMetadatacompilations — so they cannot re-introduce the inert-module failures those gates disable.
In the matrix, replace build for compile-only jobs:
The desktop-named sample asserts the rename-proofing: its jvm leaf is renamed to desktop, and kmpCompileAll wires the real compileKotlinDesktop.
The living example¶
This repo runs the pattern: sample-matrix.yml builds the hello-world sample per host on every push.