Files
vnidrop/.codex/skills/compose-skill/references/ci-cd-distribution.md
Hammed Abass fd4fff2c86 docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory.

Co-authored-by: Junie <junie@jetbrains.com>
2026-07-07 16:55:43 +02:00

7.4 KiB

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

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

- 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

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

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 {
    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":

cd "$SRCROOT/.."
./gradlew :composeApp:embedAndSignAppleFrameworkForXcode

Swift Entry Point

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

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)

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 {
        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