Skip to content

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

./gradlew :shared-core:kmpTargetsInfo -q
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 the defaultSelection / built-in fallbacks. Values from -Dorg.gradle.project.kmptargets.targets and ~/.gradle/gradle.properties report as the fused gradle.properties (...) layer.
  • Supported — whether the module declared supports { } and what it expanded to.
  • Registered — the intersection that registered. Every empty case states its reason: no supports { } call, a selection narrowed to nothing, or a disjoint selection ∩ supported.
  • Apple framework — present when the module declares appleFramework: the declared name, the Apple leaves it attached to (registered ∩ on), its effective build types (and, when a lane narrowed them, the winning kmptargets.framework.buildTypes origin layer plus the declared set), and whether XCFramework assembly is on. An empty attached is 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

./gradlew :feature-mobile:kmpTargetsDoctor -q

A healthy module:

kmp-targets doctor — :jvm-tools

✓ clean bill of health — registered: jvm

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 external jvm-only libraries; co-selecting jvm with android (the rule) remains the fix.
  • The android→jvm fallback is approximate. Doctor assumes "an androidTarget consumer is satisfied by a jvm-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).