KiteSSOT¶
One place to declare a Kotlin Multiplatform app's identity: name, version, bundle ID, locales and SDK levels. KiteSSOT propagates it to the Android, Xcode and Kotlin files that each normally keep their own copy.
A typical Kotlin Multiplatform repo records its app name and version in four places:
- the Android module's
defaultConfig - the Xcode project's build settings
- the source
Info.plist - whatever constant the shared Kotlin code reads
Nothing keeps those four copies in agreement, so eventually they stop matching.
The shortest setup that does something¶
In the root build.gradle.kts:
plugins {
kotlin("multiplatform") version "2.4.10" apply false
id("com.android.application") version "9.2.1" apply false
id("io.github.yuroyami.kitessot") version "2.0.2"
}
kiteSsot {
appName = "Jetzy"
versionName = "1.4.0"
bundleIdBase = "com.example.jetzy"
android {
compileSdk = 36
minSdk = 26
targetSdk = 36
}
}
Then:
That prints the resolved model and writes nothing. Run it again whenever a value is not where you expected it.
Two required preconditions
The plugin goes on the root project. Applying it in a submodule throws
immediately, because it aggregates across allprojects from the root.
Add apply false to the Kotlin and Android plugin lines. KiteSSOT
integrates with typed classes from KGP (the Kotlin Gradle plugin) and AGP
(the Android Gradle plugin). Those integrations run only when KiteSSOT can
load the plugin classes from its own classloader. Declare
kotlin("multiplatform") only inside a subproject and Gradle loads KGP with
a different classloader. KiteSSOT cannot read the plugin classes from there.
Two tiers of switch¶
KiteSSOT splits its work into two tiers.
Gradle configuration is automatic and continuous. On every build, KiteSSOT
applies the Android identity and SDK levels, aligns the Java and Kotlin JVM
targets, and generates Kotlin under build/. This happens inside AGP's
finalizeDsl hook, which runs after a module's own android { } block. A value
set in kiteSsot { } therefore replaces the same value set in the module. Set
the value and set the switch. Nothing else is needed.
Edits to files you own are opt-in and manual. project.pbxproj,
Info.plist, Podfile, Swift imports and launcher icons are yours. Editing them
takes three things:
- The switches that unlock the task.
syncIosunlocks the Xcode tasks, and the app icon task needspropagateLogoas well. - An explicitly named task that you run yourself.
- A set of containment, ownership, checksum, backup and rollback checks, which must all pass first.
This surprises people. Setting propagateLogo = true installs nothing. It
unlocks kiteSsotSyncIosLogo only when you also set syncIos = true and
ios { deploymentTarget }, and you then run that task yourself. A plain
./gradlew build never writes outside build/, and CI asserts that on every
commit.
Run ./gradlew kiteSsotPlan before you run any mutating task. It lists which
mutations your current configuration authorizes, and the exact paths they would
change. Set dryRun = true to make the mutating tasks report without writing.
Where things live¶
README The full guide: DSL reference, the task table, compatibility, and the current limits.
API reference Every extension property and task type, generated from source.
FEATURES.md Behavior reference: what each switch does, and every default value.
Changelog Release history, including the 1.x to 2.x migration.
Compatibility¶
Gradle 8.5 and newer, AGP 8.5.2 through 9.2.x, KGP 2.4.x, on a JDK 17 or 21 daemon.
A dedicated agpCompatibilityTest builds real consumer projects on Gradle 8.5,
8.9 and 9.5.1, against AGP 8.5.2 and 9.2.1. CI runs on Linux with JDK 17 and 21,
on macOS with JDK 21, and on Windows with JDK 21. Each build runs twice. CI then
checks that the second run reuses the configuration cache entry, and that no
tracked file changed.