Apple Framework¶
Modules that ship a Kotlin framework to an iOS/macOS app usually hand-roll the same block: pick native targets from ad-hoc properties, loop them, call binaries.framework { baseName = …; export(…) }. The plugin already owns that loop's input — the registration log (onRegistered) — so it can attach the framework for you.
It does that without copying KGP's Framework API. The plugin owns only the loop, the base name, the build types, and the Apple-subset filter; the configure block is the real org.jetbrains.kotlin.gradle.plugin.mpp.Framework. Nothing here is reviewed on a Kotlin release — a new framework option shows up in your block for free.
appleFramework¶
Declare the framework once; it attaches to every registered Apple leaf:
import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
kmpTargets {
supports { appleMobile }
appleFramework("KotlinShared") { // this: Framework (real KGP)
isStatic = false // KGP
export(libs.shared.core) // KGP export(Any): catalog Provider / project / "g:a:v"
binaryOption("bundleId", "com.example.shared")
}
}
baseNameis the first argument; KGP'snamePrefixstays"", so link-task names (linkDebugFrameworkIosSimulatorArm64) and theembedAndSign…contract match plain KGP byte-for-byte.on(defaultapple) narrows which Apple leaves attach, using the same target algebra assupports:appleFramework("KotlinShared", on = KmpTargetSet.appleMobile) { … }. Anonnot ⊆applefails at declaration, naming the offending ids.buildTypes(default KGP'sNativeBuildType.DEFAULT_BUILD_TYPES= DEBUG + RELEASE) is passed straight to KGP'sbinaries.framework(buildTypes = …)factory. The plugin never silently narrows a KGP default — the declaration is the ceiling. A lane can narrow it globally withkmptargets.framework.buildTypes(effective = property ∩ declared).- Ordering-immune.
appleFrameworkdeclared before or aftersupports {}behaves identically (it rides theonRegisteredreplay); a latersupports {}union attaches only the delta. - Exports stay lazy. A version-catalog
Provideris realized by KGP at attach time, not at declaration. - One per module in v1 — a second call, a blank name, or
on ⊄ applefails fast. (Multiple frameworks need anamePrefixstrategy; a follow-up.) - Attaches to nothing? You're warned. If the current selection registers no Apple target in the framework's
onscope (a jvm-only lane, or anonnarrower than what registered), the framework silently never builds and an Xcode consumer fails downstream — so the plugin emits the framework-without-an-Apple-target advisory (a configuration failure underkmptargets.strict).kmpTargetsInfoandkmpTargetsDoctorrender the declared name and the leaves it attached to.
XCFramework assembly¶
Set xcframework = true to also bundle the registered slices into one .xcframework (the artifact an Xcode app links):
kmpTargets {
supports { appleMobile }
appleFramework("KotlinShared", xcframework = true) { isStatic = false }
}
KGP registers assembleKotlinSharedDebugXCFramework, assembleKotlinSharedReleaseXCFramework, and the parent assembleKotlinSharedXCFramework.
- Opt-in, matching the umbrella-task precedent — assembly tasks add edges, so they're off by default.
- Lazy: the
XCFrameworkConfigis created on the first Apple attach, so a selection that registers no Apple leaf (e.g. a jvm-only lane) wires zeroassemble…XCFrameworktasks. - Invariant-safe by construction: KGP requires every framework in an XCFramework to share a
baseNameand match onbuildType— both come from the single declaration, so there's nothing to maintain by hand.buildTypesflows through (buildTypes = listOf(NativeBuildType.DEBUG)produces only the Debug assemble variant). - macOS to run, host-blind to register: the assemble tasks shell out to
xcodebuild, so they only execute on a macOS host. Registration is host-independent (same as the link tasks), so your selection stays host-blind.
Build types per lane¶
Build type is lane-shaped, not module-shaped: local dev wants DEBUG-only links (fast), the release lane wants RELEASE. So which NativeBuildTypes a framework links is a global, layered input — kmptargets.framework.buildTypes — resolved through the same selection-sources chain as kmptargets.targets (CLI -P → ORG_GRADLE_PROJECT_* env → kmp-targets.local.properties → kmp-targets.properties → gradle.properties), validated against the key registry, with the same did-you-mean on a typo.
# the release lane overrides it, exactly like the targets key
./gradlew assembleKotlinSharedXCFramework -Pkmptargets.framework.buildTypes=release
- Effective = property ∩ declared — the
selection ∩ supportedshape on the build-type axis. The property only ever narrows what the module declared; it can never link a build type the declaration didn't list. Absent property → the declared value wins (no silent narrowing of a KGP default). - Grammar: a comma-separated, case-insensitive list of
debug/release(KGP's closedNativeBuildTypeenum). A junk value (relese) fails the build with a did-you-mean. - Drives both the per-buildType framework link tasks and the XCFramework assemble variants:
=debugleaves onlylinkDebugFramework…andassemble…DebugXCFramework. - Disjoint → nothing links. A module that declares
buildTypes = listOf(NativeBuildType.DEBUG)under a=releaselane has an empty effective set: no binary (or XCFramework slice) links, and the plugin emits the framework-build-types-disjoint advisory (a configuration failure underkmptargets.strict). - Surfaced.
kmpTargetsInfoprints the effective build types, the winning origin layer, and the declared set when a lane narrowed it.
onAppleTarget — the no-magic primitive¶
appleFramework is thin sugar over onAppleTarget, which hands you the live KGP KotlinNativeTarget for every registered Apple leaf. Reach for it for anything that isn't a single framework — multiple binaries, cinterops, linker options:
kmpTargets {
supports { appleMobile }
onAppleTarget { // this: KotlinNativeTarget (real KGP)
binaries.framework("KotlinShared") { isStatic = false }
compilations.getByName("main").cinterops.create("analytics")
}
}
Same replay/ordering guarantees as onRegistered; fires only when KGP is applied; runs at configuration time only (don't hold the target from a task action).
From build-logic¶
Both are plain extension calls — no type-safe receiver to bypass — so a convention plugin uses them directly, and on already takes a raw KmpTargetSet:
import com.rsicarelli.kmptargets.KmpTargetsExtension
import com.rsicarelli.kmptargets.model.KmpTargetSet
extensions.configure<KmpTargetsExtension> {
appleFramework("KotlinShared", on = KmpTargetSet.appleMobile) { isStatic = false }
supports(KmpTargetSet.appleMobile)
}
Out of scope¶
Per-module build-type property variants and custom (non-DEBUG/RELEASE) build types are out of scope (KGP's enum is closed). Multiple frameworks per module (and aggregating several into one XCFramework), and non-Apple binaries (sharedLib/staticLib/executable) are deliberate follow-ups.
Letting Xcode's own SDK_NAME/ARCHS/CONFIGURATION drive selection — so the Xcode build phase needs no -P — is the opt-in Xcode Environment source.