# CI/CD & Distribution CI/CD and native distribution for Compose Multiplatform: Android, Desktop (JVM), and iOS. ## 1. Distribution Overview | Platform | Output | Gradle Task | Notes | |----------|--------|-------------|-------| | Android | APK/AAB | `assembleRelease`/`bundleRelease` | Standard distribution | | Desktop macOS | DMG | `packageDmg` | Needs signing for Gatekeeper | | Desktop Windows | MSI | `packageMsi` | Optional signing | | Desktop Linux | DEB | `packageDeb` | Package manager format | | iOS | .app/.ipa | Xcode Archive | Gradle builds framework only | ## 2. GitHub Actions — Android ```yaml name: Android Build on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' - uses: gradle/actions/setup-gradle@v4 - run: ./gradlew :androidApp:assembleRelease - uses: actions/upload-artifact@v4 with: name: android-apk path: androidApp/build/outputs/apk/release/*.apk ``` ### With Signing ```yaml - name: Decode Keystore run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > release.keystore - run: ./gradlew :androidApp:assembleRelease env: KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} ``` ## 3. GitHub Actions — Desktop Multi-Platform ```yaml name: Desktop Build on: workflow_dispatch: inputs: build_macos: { type: boolean, default: true } build_windows: { type: boolean, default: true } build_linux: { type: boolean, default: true } jobs: build-macos: if: ${{ inputs.build_macos }} runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: { java-version: '21', distribution: 'temurin' } - uses: gradle/actions/setup-gradle@v4 - run: ./gradlew :desktopApp:packageDmg - uses: actions/upload-artifact@v4 with: name: macos-dmg path: desktopApp/build/compose/binaries/main/dmg/*.dmg build-windows: if: ${{ inputs.build_windows }} runs-on: windows-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: { java-version: '21', distribution: 'temurin' } - uses: gradle/actions/setup-gradle@v4 - run: ./gradlew :desktopApp:packageMsi - uses: actions/upload-artifact@v4 with: name: windows-msi path: desktopApp/build/compose/binaries/main/msi/*.msi build-linux: if: ${{ inputs.build_linux }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: { java-version: '21', distribution: 'temurin' } - uses: gradle/actions/setup-gradle@v4 - run: ./gradlew :desktopApp:packageDeb - uses: actions/upload-artifact@v4 with: name: linux-deb path: desktopApp/build/compose/binaries/main/deb/*.deb ``` ## 4. Desktop App Module ```kotlin import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.compose.multiplatform) alias(libs.plugins.compose.compiler) } kotlin { jvm() sourceSets { jvmMain.dependencies { implementation(compose.desktop.currentOs) implementation(projects.composeApp) } } } compose.desktop { application { mainClass = "com.example.MainKt" // Required for DataStore/serialization jvmArgs += listOf( "--add-opens", "java.base/java.lang=ALL-UNNAMED", "--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED" ) nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "MyApp" packageVersion = "1.0.0" modules("jdk.unsupported") macOS { bundleID = "com.example.myapp" iconFile.set(project.file("icons/icon.icns")) // signing { sign.set(true); identity.set("Developer ID Application: ...") } } windows { iconFile.set(project.file("icons/icon.ico")) upgradeUuid = "YOUR-UUID" // Keep constant across versions } linux { iconFile.set(project.file("icons/icon.png")) } } } } ``` ## 5. iOS Xcode Integration iOS uses Xcode, not Gradle. Gradle builds the shared framework; Xcode embeds it. ### Framework in `composeApp` ```kotlin kotlin { listOf(iosArm64(), iosSimulatorArm64()).forEach { it.binaries.framework { baseName = "ComposeApp" isStatic = true // Required for App Store } } } ``` ### Xcode Build Phase Script Add "Run Script" before "Compile Sources": ```bash cd "$SRCROOT/.." ./gradlew :composeApp:embedAndSignAppleFrameworkForXcode ``` ### Swift Entry Point ```swift import SwiftUI import ComposeApp @main struct iOSApp: App { init() { AppKt.doInitKoin() } var body: some Scene { WindowGroup { ComposeViewControllerRepresentable().ignoresSafeArea() } } } struct ComposeViewControllerRepresentable: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { MainViewControllerKt.MainViewController() } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} } ``` ## 6. Signing ### Android ```kotlin android { signingConfigs { create("release") { storeFile = file("release.keystore") storePassword = System.getenv("KEYSTORE_PASSWORD") keyAlias = System.getenv("KEY_ALIAS") keyPassword = System.getenv("KEY_PASSWORD") } } buildTypes { release { signingConfig = signingConfigs.getByName("release") } } } ``` ### macOS (Direct Distribution) ```kotlin macOS { signing { sign.set(true) identity.set("Developer ID Application: Your Name (TEAM_ID)") } notarization { appleID.set("your-email@example.com") password.set("@keychain:AC_PASSWORD") teamID.set("YOUR_TEAM_ID") } } ``` ### iOS Handled by Xcode via `CODE_SIGN_STYLE = Automatic` and `DEVELOPMENT_TEAM`. ## 7. Adding Desktop to Existing CMP Project 1. Add `jvm()` target in `composeApp`: ```kotlin kotlin { jvm() sourceSets { jvmMain.dependencies { /* desktop deps */ } } } ``` 2. Add KSP for JVM: `add("kspJvm", libs.room.compiler)` 3. Create `desktopApp` module with `compose.desktop {}` config 4. Add `include(":desktopApp")` to `settings.gradle.kts` ## 8. Gradle Tasks | Platform | Build | Package | Run | |----------|-------|---------|-----| | Android | `assembleRelease` | `bundleRelease` | — | | Desktop | `jvmJar` | `packageDmg`/`packageMsi`/`packageDeb` | `run` | | iOS | `compileKotlinIosArm64` | Xcode Archive | Xcode | ## 9. Troubleshooting | Issue | Solution | |-------|----------| | `InaccessibleObjectException` | Add `--add-opens` JVM args | | "App is damaged" on macOS | Enable code signing | | Framework not found in Xcode | Check `FRAMEWORK_SEARCH_PATHS` | | Windows MSI won't upgrade | Keep `upgradeUuid` constant |