Diagnostics¶
Two read-only tasks (group help) on every module the plugin is applied to: kmpTargetsInfo reports state — what resolved, what registered, and why. kmpTargetsDoctor reports findings — what's wrong, the consequence, and the fix.
kmpTargetsInfo¶
kmp-targets — :shared-core
Selection (what to build now)
targets: iosArm64, iosSimulatorArm64, jvm
source: kmp-targets.properties
Supported (what this module can build)
declared: yes
targets: androidNativeArm32, ..., watchosX64
Registered (selection ∩ supported)
targets: iosArm64, iosSimulatorArm64, jvm
Apple framework
name: KotlinShared
attached: iosArm64, iosSimulatorArm64
buildTypes: DEBUG, RELEASE
xcframework: off
Vocabulary
presets: all, native, appleMobile, appleDesktop, appleWatch, appleTv, apple, linux, mingw, windows, androidNative, web, jvmFamily, mobile
leaves: androidNativeArm32, ..., macosX64 (deprecated), ..., watchosX64 (deprecated) (25)
Reading the report¶
Selection— the resolved global selection and its winning layer by name:command line (-Pkmptargets.targets),kmp-targets.local.properties,local.properties, or thedefaultSelection/ built-in fallbacks. Values from-Dorg.gradle.project.kmptargets.targetsand~/.gradle/gradle.propertiesreport as the fusedgradle.properties (...)layer.Supported— whether the module declaredsupports { }and what it expanded to.Registered— the intersection that registered. Every empty case states its reason: nosupports { }call, a selection narrowed to nothing, or a disjointselection ∩ supported.Apple framework— present when the module declaresappleFramework: the declared name, the Apple leaves it attached to (registered ∩ on), its effective build types (and, when a lane narrowed them, the winningkmptargets.framework.buildTypesorigin layer plus the declared set), and whether XCFramework assembly is on. An emptyattachedis the framework-without-an-Apple-target state — the framework never builds.Vocabulary— the parser's full preset and leaf list; a copy-paste surface for valid tokens.
Per-leaf annotations¶
| Annotation | Meaning |
|---|---|
iosArm64 (not compilable on this host: LINUX_X64) |
registered, but this host can't compile it — see the host advisory |
macosX64 (deprecated) |
Kotlin marks the leaf deprecated — see the deprecated advisory |
androidTarget (skipped: no Android plugin applied) |
the AGP guard skipped it |
jvm (registered as: desktop) |
the leaf registered under a custom name |
inert: line |
the module registered zero targets — the inert-module condition |
kmpTargetsDoctor¶
A healthy module:
A module that hit a trap gets one [!] block per finding:
kmp-targets doctor — :jvm-tools
[!] selection matches nothing supported
why: the selection iosArm64 matches none of the supported set jvm
effect: this module registers no targets
fix: widen the selection or this module's supports { } so they overlap
[!] inert module
why: declared supports { } but registered zero targets
effect: KGP still materializes the commonMain metadata compilation, which fails with no platform targets
fix: gate on registered().isEmpty() in build-logic
Findings¶
| Finding | Advisory it explains |
|---|---|
selection matches nothing supported |
empty overlap |
inert module |
inert modules |
JVM-less commonMain |
native-only metadata |
framework declared but unattached |
framework without an Apple target |
framework build types disjoint |
framework build types disjoint |
androidTarget skipped |
android without AGP |
not compilable on this host |
host compatibility |
deprecated targets registered |
deprecated targets |
single-target KSP |
the commonMain-KSP trap — doctor-only, no build-time advisory |
note: jvm (registered as: …) |
a renamed target — context, not a problem |
single-target KSP is doctor-only: it fires when a KSP plugin is applied and exactly one target is registered, so the commonMain metadata route (kspCommonMainKotlinMetadata, which needs ≥2 targets) is absent. Whether the module actually generates into commonMain is unobservable, so the finding flags the risk rather than a certainty — and there is no build-time advisory or strict-mode escalation for it.
Doctor also renders the same Apple framework facts block as kmpTargetsInfo (declared name, attached leaves, buildTypes, XCFramework on/off), from the same providers so the two surfaces can't drift. It renders even for a module that declared a framework but never called supports { } — so a declared-but-unattached framework is always visible, even where there is no advisory.
Project-edge closure¶
Doctor also flags a project(...) dependency that registers none of the leaves this module needs (the failure in Selection vs the dependency graph):
kmp-targets doctor — :app-mobile
✓ clean bill of health — registered: iosArm64, jvm
Project-edge closure (project dependencies only)
[!] :app-mobile → :lib-jvm
missing: iosArm64 — the dependency registers none of these leaves this module needs
fix: widen :lib-jvm's supports { } (or the selection) to register them
limits: external (non-project) dependencies are invisible; the android→jvm fallback is approximate
The check crosses the project boundary by file, never by reading a peer project's model: each module emits its primitives via kmpTargetsDoctorData and the dependent resolves those files through a lenient artifact view. It is configuration-cache safe and runs unchanged under Isolated Projects.
Two permanent limits:
- No external-dependency coverage. Only
project(...)edges are visible. The android→jvm-fallback pain usually comes from externaljvm-only libraries; co-selectingjvmwithandroid(the rule) remains the fix. - The android→jvm fallback is approximate. Doctor assumes "an
androidTargetconsumer is satisfied by ajvm-only producer"; it does not reproduce Gradle's real attribute matching.
Task properties¶
Both tasks are read-only (they never register targets), configuration-cache compatible (the second run is a cache hit), and per-module (:feature-mobile:kmpTargetsDoctor). Doctor runs even when the module is inert. Findings key off the same decisions the advisories use (Design).