Skip to content

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:

./gradlew kiteSsotVerify

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:

  1. The switches that unlock the task. syncIos unlocks the Xcode tasks, and the app icon task needs propagateLogo as well.
  2. An explicitly named task that you run yourself.
  3. 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

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.