├── .github ├── CODEOWNERS ├── FUNDING.yml ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.md └── workflows │ └── prepare-release.yml ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── cli ├── src │ ├── jvmMain │ │ ├── resources │ │ │ └── project │ │ │ │ ├── iosApp │ │ │ │ ├── iosApp │ │ │ │ │ ├── Assets.xcassets │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ │ │ ├── app-icon-1024.png │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── AccentColor.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Preview Content │ │ │ │ │ │ └── Preview Assets.xcassets │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── iOSApp.swift │ │ │ │ │ ├── Info.plist │ │ │ │ │ └── ContentView.swift │ │ │ │ ├── Configuration │ │ │ │ │ └── Config.xcconfig │ │ │ │ └── iosApp.xcodeproj │ │ │ │ │ └── project.pbxproj │ │ │ │ ├── composeApp │ │ │ │ ├── src │ │ │ │ │ ├── androidMain │ │ │ │ │ │ ├── res │ │ │ │ │ │ │ ├── values │ │ │ │ │ │ │ │ └── strings.xml │ │ │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ │ │ │ ├── drawable-v24 │ │ │ │ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ │ │ │ │ └── drawable │ │ │ │ │ │ │ │ └── ic_launcher_background.xml │ │ │ │ │ │ ├── kotlin │ │ │ │ │ │ │ └── org │ │ │ │ │ │ │ │ └── example │ │ │ │ │ │ │ │ └── project │ │ │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ │ ├── webMain │ │ │ │ │ │ ├── resources │ │ │ │ │ │ │ ├── styles.css │ │ │ │ │ │ │ └── index.html │ │ │ │ │ │ └── kotlin │ │ │ │ │ │ │ └── org │ │ │ │ │ │ │ └── example │ │ │ │ │ │ │ └── main.kt │ │ │ │ │ ├── jvmMain │ │ │ │ │ │ └── kotlin │ │ │ │ │ │ │ └── org │ │ │ │ │ │ │ └── example │ │ │ │ │ │ │ └── main.kt │ │ │ │ │ ├── iosMain │ │ │ │ │ │ └── kotlin │ │ │ │ │ │ │ └── org │ │ │ │ │ │ │ └── example │ │ │ │ │ │ │ └── project │ │ │ │ │ │ │ └── MainViewController.kt │ │ │ │ │ └── commonMain │ │ │ │ │ │ └── kotlin │ │ │ │ │ │ └── org │ │ │ │ │ │ └── example │ │ │ │ │ │ └── project │ │ │ │ │ │ └── App.kt │ │ │ │ ├── build.gradle.kts │ │ │ │ └── webpack.config.d │ │ │ │ │ └── watch.js │ │ │ │ ├── gradle │ │ │ │ ├── wrapper │ │ │ │ │ ├── gradle-wrapper.jarX │ │ │ │ │ └── gradle-wrapper.properties │ │ │ │ └── libs.versions.toml │ │ │ │ ├── gradle.properties │ │ │ │ ├── .gitignore │ │ │ │ ├── build.gradle.kts │ │ │ │ ├── settings.gradle.kts │ │ │ │ ├── gradlew.bat │ │ │ │ └── gradlew │ │ └── kotlin │ │ │ └── Cli.kt │ └── jvmTest │ │ └── kotlin │ │ └── com │ │ └── composables │ │ └── cli │ │ └── CliTest.kt └── build.gradle.kts ├── .idea ├── vcs.xml ├── compiler.xml ├── AndroidProjectSystem.xml ├── .gitignore ├── icon.svg ├── jsLibraryMappings.xml ├── gradle.xml └── misc.xml ├── gradle.properties ├── .gitignore ├── README.md ├── LICENSE ├── settings.gradle.kts ├── CHANGELOG.md ├── scripts └── add_to_path.kts ├── gradlew.bat └── gradlew /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @alexstyl -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://composables.com/sponsor'] 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | {{app_name}} 3 | 4 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | {{imports}} 2 | {{plugins}} 3 | 4 | kotlin { 5 | {{kotlin_targets}} 6 | {{sourcesets}} 7 | } 8 | 9 | {{configuration_blocks}} 10 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/webMain/resources/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | overflow: hidden; 7 | } -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/gradle/wrapper/gradle-wrapper.jarX: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/cli/src/jvmMain/resources/project/gradle/wrapper/gradle-wrapper.jarX -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | 3 | PRODUCT_NAME={{app_name}} 4 | PRODUCT_BUNDLE_IDENTIFIER={{namespace}}$(TEAM_ID) 5 | 6 | CURRENT_PROJECT_VERSION=1 7 | MARKETING_VERSION=1.0 8 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/jvmMain/kotlin/org/example/main.kt: -------------------------------------------------------------------------------- 1 | package {{namespace}} 2 | 3 | import androidx.compose.ui.window.singleWindowApplication 4 | 5 | fun main() = singleWindowApplication { 6 | App() 7 | } 8 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.idea/AndroidProjectSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt: -------------------------------------------------------------------------------- 1 | package {{namespace}} 2 | 3 | import androidx.compose.ui.window.ComposeUIViewController 4 | 5 | fun MainViewController() = ComposeUIViewController { App() } 6 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/composables-cli/HEAD/cli/src/jvmMain/resources/project/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/gradle.properties: -------------------------------------------------------------------------------- 1 | #Kotlin 2 | kotlin.code.style=official 3 | kotlin.daemon.jvmargs=-Xmx3072M 4 | 5 | #Gradle 6 | org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 7 | org.gradle.configuration-cache=true 8 | org.gradle.caching=true 9 | 10 | {{android_properties}} -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | composables-cli = "0.4.5" 3 | kotlin = "2.2.21" 4 | shadow = "8.3.8" 5 | 6 | [libraries] 7 | 8 | [plugins] 9 | multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 10 | shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } 11 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Environment-dependent path to Maven home directory 7 | /mavenHomeManager.xml 8 | # Datasource local storage ignored files 9 | /dataSources/ 10 | /dataSources.local.xml 11 | /artifacts/ 12 | modules.xml 13 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/webMain/kotlin/org/example/main.kt: -------------------------------------------------------------------------------- 1 | package {{namespace}} 2 | 3 | import androidx.compose.ui.ExperimentalComposeUiApi 4 | import androidx.compose.ui.window.ComposeViewport 5 | 6 | @OptIn(ExperimentalComposeUiApi::class) 7 | fun main() { 8 | ComposeViewport { 9 | App() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.idea/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .kotlin/ 4 | /local.properties 5 | /.idea 6 | .DS_Store 7 | kotlin-js-store/ 8 | build/ 9 | /captures 10 | .externalNativeBuild 11 | .cxx 12 | iosApp/Podfile.lock 13 | iosApp/Pods/* 14 | iosApp/iosApp.xcworkspace/* 15 | iosApp/iosApp.xcodeproj/* 16 | !iosApp/iosApp.xcodeproj/project.pbxproj 17 | shared/shared.podspec -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | 2 | #Gradle 3 | org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=2048m -XX:+HeapDumpOnOutOfMemoryError 4 | 5 | kotlin.code.style=official 6 | 7 | #Compose 8 | org.jetbrains.compose.experimental.wasm.enabled=true 9 | org.jetbrains.compose.experimental.jscanvas.enabled=true 10 | 11 | #Android 12 | android.useAndroidX=true 13 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | 5 | ## Test Plan 6 | 7 | 8 | - [ ] Tests added/updated 9 | - [ ] Manual testing performed 10 | 11 | ## Related Issues 12 | 13 | 14 | 15 | ## Screenshots/Videos 16 | 17 | 18 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // this is necessary to avoid the plugins to be loaded multiple times 3 | // in each subproject's classloader 4 | alias(libs.plugins.jetbrains.kotlin.multiplatform) apply false 5 | alias(libs.plugins.jetbrains.compose.hotreload) apply false 6 | alias(libs.plugins.jetbrains.compose) apply false 7 | alias(libs.plugins.jetbrains.compose.compiler) apply false 8 | {{android_plugin}}} 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Get Help 4 | url: https://github.com/composablehorizons/composables-cli/discussions/new?category=help 5 | about: If you can't get something to work the way you expect, open a question in our discussion forums. 6 | - name: Feature Request 7 | url: https://github.com/composablehorizons/composables-cli/discussions/new?category=ideas 8 | about: 'Suggest any ideas you have using our discussion forums.' 9 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/webMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{app_name}} 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug or unexpected behavior 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe your issue** 10 | 11 | Describe your issue here. Steps to reproduce and what the expected behavior should be. 12 | 13 | **What version of Composables CLI are you using?** 14 | 15 | For example: 0.4.0 16 | 17 | **Which platforms can you reproduce the bug on?** 18 | 19 | Select one or multiple: 20 | - [ ] Mac OS 21 | - [ ] Windows 22 | - [ ] Linux 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | **/build/ 4 | xcuserdata 5 | !src/**/build/ 6 | local.properties 7 | kotlin-js-store/ 8 | .DS_Store 9 | captures 10 | .externalNativeBuild 11 | .cxx 12 | *.xcodeproj/* 13 | !*.xcodeproj/project.pbxproj 14 | !*.xcodeproj/xcshareddata/ 15 | !*.xcodeproj/project.xcworkspace/ 16 | !*.xcworkspace/contents.xcworkspacedata 17 | **/xcshareddata/WorkspaceSettings.xcsettings 18 | venv/ 19 | .cache/ 20 | .kotlin/ 21 | 22 | # CMake 23 | cmake-build-*/ 24 | 25 | # File-based project format 26 | *.iws 27 | 28 | # IntelliJ 29 | out/ 30 | temp/ 31 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package {{namespace}} 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | 8 | class MainActivity : ComponentActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | 12 | enableEdgeToEdge() 13 | setContent { 14 | App() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import {{ios_binary_name}} 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea() 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Composables CLI 2 | 3 | Set up new Compose Multiplatform apps with a single command. 4 | 5 | ![Demo](https://github.com/user-attachments/assets/c60cfcf9-1fec-4364-953f-1a8a2ce3d9c6) 6 | 7 | 8 | ## Installation 9 | 10 | ```shell 11 | curl -fsSL https://composables.com/get-composables.sh | bash 12 | ``` 13 | 14 | > [!WARNING] 15 | > The CLI tool has only be tested on Mac, but it should work on other platforms. If you face any issues with it, kindly open an issue. 16 | 17 | 18 | ## Quick Usage 19 | 20 | ```shell 21 | composables init composeApp 22 | cd composeApp 23 | ./gradlew run 24 | ``` 25 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # Compose 3 | compose = "1.9.1" 4 | composeHotReload = "1.0.0" 5 | 6 | # Kotlin 7 | kotlin = "2.2.21" 8 | 9 | {{android_versions}} 10 | [libraries] 11 | {{android_libraries}} 12 | [plugins] 13 | {{android_plugins}} 14 | jetbrains-compose-hotreload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" } 15 | jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose" } 16 | jetbrains-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 17 | jetbrains-kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/webpack.config.d/watch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Temporary workaround for [KT-80582](https://youtrack.jetbrains.com/issue/KT-80582) 3 | * 4 | * This file should be safe to be removed once the ticket is closed and the project is updated to Kotlin version which solves that issue. 5 | */ 6 | config.watchOptions = config.watchOptions || { 7 | ignored: ["**/*.kt", "**/node_modules"] 8 | } 9 | 10 | if (config.devServer) { 11 | config.devServer.static = config.devServer.static.map(file => { 12 | if (typeof file === "string") { 13 | return { 14 | directory: file, 15 | watch: false, 16 | } 17 | } else { 18 | return file 19 | } 20 | }) 21 | } -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "idiom" : "universal", 17 | "platform" : "ios", 18 | "size" : "1024x1024" 19 | }, 20 | { 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "tinted" 25 | } 26 | ], 27 | "idiom" : "universal", 28 | "platform" : "ios", 29 | "size" : "1024x1024" 30 | } 31 | ], 32 | "info" : { 33 | "author" : "xcode", 34 | "version" : 1 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | pluginManagement { 4 | repositories { 5 | google { 6 | mavenContent { 7 | includeGroupAndSubgroups("androidx") 8 | includeGroupAndSubgroups("com.android") 9 | includeGroupAndSubgroups("com.google") 10 | } 11 | } 12 | mavenCentral() 13 | gradlePluginPortal() 14 | mavenLocal() 15 | } 16 | } 17 | 18 | dependencyResolutionManagement { 19 | repositories { 20 | google { 21 | mavenContent { 22 | includeGroupAndSubgroups("androidx") 23 | includeGroupAndSubgroups("com.android") 24 | includeGroupAndSubgroups("com.google") 25 | } 26 | } 27 | mavenCentral() 28 | mavenLocal() 29 | } 30 | } 31 | 32 | plugins { 33 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 34 | } 35 | 36 | include(":{{module_name}}") 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Composable Horizons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /cli/src/jvmTest/kotlin/com/composables/cli/CliTest.kt: -------------------------------------------------------------------------------- 1 | package com.composables.cli 2 | 3 | import java.io.File 4 | import kotlin.test.BeforeTest 5 | import kotlin.test.Test 6 | 7 | class CliTest { 8 | val targetDir = "/Users/alexstyl/projects/composables-cli/temp" 9 | 10 | @BeforeTest 11 | fun cleanTargetDirectory() { 12 | val targetFile = File(targetDir) 13 | if (targetFile.exists()) { 14 | targetFile.deleteRecursively() 15 | } 16 | } 17 | 18 | @Test 19 | fun `test constants are defined correctly`() { 20 | cloneGradleProject( 21 | targetDir = targetDir, 22 | dirName = "newApp", 23 | packageName = "com.composables", 24 | moduleName = "composeApp", 25 | appName = "The App", 26 | targets = setOf( 27 | ANDROID, 28 | ) 29 | ) 30 | 31 | // Assert that composeApp directory exists 32 | val appDir = File(targetDir, "newApp") 33 | assert(appDir.exists()) { "newApp directory should exist in $targetDir" } 34 | assert(appDir.isDirectory) { "newApp should be a directory" } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | pluginManagement { 4 | repositories { 5 | google() 6 | google { 7 | mavenContent { 8 | includeGroupAndSubgroups("androidx") 9 | includeGroupAndSubgroups("com.android") 10 | includeGroupAndSubgroups("com.google") 11 | } 12 | } 13 | mavenCentral() 14 | gradlePluginPortal() 15 | } 16 | } 17 | plugins { 18 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" 19 | } 20 | 21 | dependencyResolutionManagement { 22 | repositories { 23 | google() 24 | google { 25 | mavenContent { 26 | includeGroupAndSubgroups("androidx") 27 | includeGroupAndSubgroups("com.android") 28 | includeGroupAndSubgroups("com.google") 29 | } 30 | } 31 | mavenCentral() 32 | mavenLocal() 33 | maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental") 34 | } 35 | versionCatalogs { 36 | create("kotlinWrappers") { 37 | val wrappersVersion = "2025.3.17" 38 | from("org.jetbrains.kotlin-wrappers:kotlin-wrappers-catalog:$wrappersVersion") 39 | } 40 | } 41 | } 42 | 43 | include(":cli") 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | # [0.4.5] - 2025-12-10 11 | 12 | ### Fixed 13 | - Fixed a bug where the JVM and Web targets would have no sourceset generated. 14 | - Fixed a crash after using the `update` command. 15 | - Fixed an issue where entering a prompt would be slower than normal. 16 | - Fixed the displayed output directory when creating apps or modules. 17 | - Fixed some blank new lines in the CLI output. 18 | 19 | ## [0.4.1] - 2025-12-9 20 | 21 | ### Fixed 22 | - Added Kotlin 2.2.21 as a requirement when adding modules to existing Gradle projects, as it is required by Hot Reload. 23 | - Fix a problem where Android projects would not generate the iOS binary 24 | 25 | ## [0.4.0] - 2025-12-9 26 | 27 | ### Added 28 | - `init` can now add modules in existing Gradle Projects. 29 | 30 | ## [0.3.0] - 2025-12-8 31 | 32 | ### Added 33 | - NEW `target` command. You can now add new targets in existing Compose Multiplatform projects. 34 | 35 | ### Changed 36 | - All `init` options are now optional when creating a new app. 37 | 38 | ## [0.2.0] - 2025-12-7 39 | 40 | ### Added 41 | - You can now select the platforms when creating a new app. 42 | 43 | ## [0.1.1] - 2025-12-6 44 | 45 | ### Fixed 46 | - Fix a bug where gradle-wrapper.jar is not copied to the new app 47 | 48 | ## [0.1.0] - 2025-12-6 49 | 50 | ### Added 51 | - Add the option to create compose apps for all targets using `composables init composeApp` 52 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /.github/workflows/prepare-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | prepare: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - run: git fetch --tags -f 22 | 23 | - name: Resolve version 24 | id: vars 25 | run: | 26 | TAG_NAME=$(git describe --tags --abbrev=0) 27 | VERSION=${TAG_NAME#v} 28 | echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV 29 | echo "VERSION=$VERSION" >> $GITHUB_ENV 30 | echo "Version: $VERSION" 31 | 32 | - name: Generate CHANGELOG content 33 | run: | 34 | # Extract the changelog content for this version, excluding the version header 35 | CHANGELOG_CONTENT=$(sed -n "/## \[$VERSION\]/,/## \[/p" CHANGELOG.md | sed '$ d' | tail -n +2) 36 | 37 | # Store in environment file for the release step 38 | echo "CHANGELOG_CONTENT<> $GITHUB_ENV 39 | echo "$CHANGELOG_CONTENT" >> $GITHUB_ENV 40 | echo "EOF" >> $GITHUB_ENV 41 | 42 | - name: Set up JDK 17 43 | uses: actions/setup-java@v4 44 | with: 45 | java-version: '17' 46 | distribution: 'jetbrains' 47 | 48 | - name: Cache Gradle packages 49 | uses: actions/cache@v4 50 | with: 51 | path: | 52 | ~/.gradle/caches 53 | ~/.gradle/wrapper 54 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 55 | restore-keys: | 56 | ${{ runner.os }}-gradle- 57 | 58 | - name: Build Shadow JAR 59 | run: ./gradlew jvmShadowJar --no-daemon 60 | 61 | - name: Create GitHub Release 62 | uses: softprops/action-gh-release@v2 63 | with: 64 | draft: true 65 | tag_name: ${{ env.TAG_NAME }} 66 | body: ${{ env.CHANGELOG_CONTENT }} 67 | files: | 68 | cli/build/libs/composables.jar 69 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/commonMain/kotlin/org/example/project/App.kt: -------------------------------------------------------------------------------- 1 | package {{namespace}} 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.safeDrawingPadding 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Scaffold 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.text.style.TextAlign 16 | import androidx.compose.ui.unit.dp 17 | import org.jetbrains.compose.ui.tooling.preview.Preview 18 | 19 | @Composable 20 | fun App() { 21 | MaterialTheme { 22 | Scaffold { 23 | Box( 24 | modifier = Modifier 25 | .safeDrawingPadding() 26 | .fillMaxSize() 27 | .padding(16.dp), 28 | contentAlignment = Alignment.Center 29 | ) { 30 | Column( 31 | horizontalAlignment = Alignment.CenterHorizontally, 32 | verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically) 33 | ) { 34 | Text( 35 | text = "Hello Beautiful World!", 36 | style = MaterialTheme.typography.displayLarge, 37 | textAlign = TextAlign.Center 38 | ) 39 | Text( 40 | text = "Go to App.kt to edit your app", 41 | style = MaterialTheme.typography.displayMedium, 42 | textAlign = TextAlign.Center 43 | ) 44 | Text( 45 | text = "Pro tip: Use the `dev` configuration in your IDE to auto-reload your app when you edit your code", 46 | textAlign = TextAlign.Center 47 | ) 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | @Preview 55 | @Composable 56 | fun AppPreview() { 57 | App() 58 | } 59 | -------------------------------------------------------------------------------- /scripts/add_to_path.kts: -------------------------------------------------------------------------------- 1 | fun runBash(command: String) { 2 | val process = ProcessBuilder("bash", "-c", command).inheritIO().start() 3 | process.waitFor() 4 | } 5 | 6 | val jarPath = "cli/build/libs/composables.jar" 7 | val projectRoot = "/Users/alexstyl/projects/composables-cli" 8 | val installDir = "${System.getenv("HOME")}/.composables/bin" 9 | val jarName = "composables.jar" 10 | val wrapperName = "composables" 11 | 12 | println("Installing Composables CLI from local build...") 13 | 14 | runBash("if ! command -v java &> /dev/null; then echo 'Error: Java is required but not installed'; exit 1; fi") 15 | 16 | println("Building shadowjar...") 17 | runBash("cd $projectRoot && ./gradlew jvmShadowJar") 18 | 19 | runBash("mkdir -p $installDir") 20 | 21 | println("Installing local JAR...") 22 | runBash("cp $projectRoot/$jarPath $installDir/$jarName") 23 | 24 | println("Creating wrapper script...") 25 | val homeDir = System.getenv("HOME") 26 | val wrapperScript = """#!/bin/bash 27 | exec java -jar "$homeDir/.composables/bin/$jarName" "$@" 28 | """ 29 | 30 | runBash("echo '$wrapperScript' > $installDir/$wrapperName") 31 | runBash("chmod +x $installDir/$wrapperName") 32 | 33 | val currentShell = System.getenv("SHELL")?.substringAfterLast("/") ?: "bash" 34 | val shellRc = 35 | if (currentShell == "zsh") "${System.getenv("HOME")}/.zshrc" 36 | else if (currentShell == "bash") { 37 | if (System.getProperty("os.name").contains("Mac")) "${System.getenv("HOME")}/.bash_profile" 38 | else "${System.getenv("HOME")}/.bashrc" 39 | } else "${System.getenv("HOME")}/.bash_profile" 40 | 41 | runBash("if ! grep -q '.composables/bin' $shellRc 2>/dev/null; then echo '' >> $shellRc; echo '# Composables CLI' >> $shellRc; echo 'export PATH=\"\$HOME/.composables/bin:\$PATH\"' >> $shellRc; echo 'Added Composables CLI to PATH in $shellRc'; else echo 'Composables CLI already in PATH'; fi") 42 | 43 | runBash("export PATH=\"$installDir:\$PATH\"") 44 | 45 | println("Testing installation...") 46 | runBash("if command -v composables &> /dev/null; then echo '✓ Composables CLI installed successfully!'; echo ''; echo 'Usage:'; echo ' composables --help - Show all available commands'; echo ''; echo \"Note: Restart your terminal or run 'source $shellRc' to use composables from anywhere\"; else echo 'Error: Installation verification failed'; exit 1; fi") 47 | -------------------------------------------------------------------------------- /cli/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar 4 | import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget 5 | 6 | 7 | plugins { 8 | alias(libs.plugins.multiplatform) 9 | alias(libs.plugins.shadow) 10 | id("com.github.gmazzo.buildconfig") version "6.0.6" 11 | } 12 | val mainClassName = "com.composables.cli.CliKt" 13 | val cliName = "composables" 14 | 15 | group = "com.composables" 16 | version = libs.versions.composables.cli.get() 17 | 18 | buildConfig { 19 | buildConfigField("Version", libs.versions.composables.cli.get()) 20 | } 21 | 22 | val organization = "composablehorizons" 23 | 24 | val projectName = "composables-cli" 25 | val githubUrl = "github.com/$organization/$projectName" 26 | 27 | java { 28 | toolchain { 29 | vendor = JvmVendorSpec.JETBRAINS 30 | languageVersion = JavaLanguageVersion.of(17) 31 | } 32 | } 33 | 34 | kotlin { 35 | jvmToolchain { 36 | vendor = JvmVendorSpec.JETBRAINS 37 | languageVersion = JavaLanguageVersion.of(17) 38 | } 39 | 40 | jvm { 41 | binaries { 42 | executable { 43 | mainClass.set(mainClassName) 44 | } 45 | } 46 | } 47 | 48 | sourceSets { 49 | val commonMain by getting { 50 | dependencies { 51 | implementation("com.alexstyl:debugln:1.0.3") 52 | implementation("com.github.ajalt.clikt:clikt:5.0.3") 53 | } 54 | } 55 | 56 | val jvmTest by getting { 57 | dependencies { 58 | implementation(kotlin("test")) 59 | } 60 | } 61 | } 62 | } 63 | 64 | 65 | // see: https://stackoverflow.com/questions/63426211/kotlin-multiplatform-shadowjar-gradle-plugin-creates-empty-jar 66 | 67 | fun registerShadowJar(targetName: String) { 68 | kotlin.targets.named(targetName) { 69 | compilations.named("main") { 70 | tasks { 71 | val shadowJar = register("${targetName}ShadowJar") { 72 | group = "build" 73 | from(output) 74 | from("src/jvmMain/resources/project") { 75 | into("project") 76 | } 77 | configurations = listOf(runtimeDependencyFiles) 78 | 79 | archiveFileName.set("$cliName.jar") 80 | 81 | manifest { 82 | attributes("Main-Class" to mainClassName) 83 | } 84 | mergeServiceFiles() 85 | } 86 | getByName("${targetName}Jar") { 87 | finalizedBy(shadowJar) 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | registerShadowJar("jvm") 95 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /cli/src/jvmMain/resources/project/iosApp/iosApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXFileReference section */ 10 | 5A86DD708273B2C3A29BCC2F /* {{target_name}} */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = {{target_name}}.app; sourceTree = BUILT_PRODUCTS_DIR; }; 11 | /* End PBXFileReference section */ 12 | 13 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 14 | 581D2DE7874258B6195B8CA1 /* Exceptions for "iosApp" folder in "iosApp" target */ = { 15 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 16 | membershipExceptions = ( 17 | Info.plist, 18 | ); 19 | target = FBA3E83EF5F81DF361705AB2 /* iosApp */; 20 | }; 21 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 22 | 23 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 24 | 98529B1B44558D434CA79376 /* iosApp */ = { 25 | isa = PBXFileSystemSynchronizedRootGroup; 26 | exceptions = ( 27 | 581D2DE7874258B6195B8CA1 /* Exceptions for "iosApp" folder in "iosApp" target */, 28 | ); 29 | path = iosApp; 30 | sourceTree = ""; 31 | }; 32 | B3C1C90FC9CDA286B07172AF /* Configuration */ = { 33 | isa = PBXFileSystemSynchronizedRootGroup; 34 | path = Configuration; 35 | sourceTree = ""; 36 | }; 37 | /* End PBXFileSystemSynchronizedRootGroup section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | DBD8CEDAC0F872C4C9F9F5B8 /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXFrameworksBuildPhase section */ 48 | 49 | /* Begin PBXGroup section */ 50 | B5C18064B82E43FA6E0DD05B = { 51 | isa = PBXGroup; 52 | children = ( 53 | B3C1C90FC9CDA286B07172AF /* Configuration */, 54 | 98529B1B44558D434CA79376 /* iosApp */, 55 | ADD1D5B2552AA3A7B434A2F4 /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | ADD1D5B2552AA3A7B434A2F4 /* Products */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 5A86DD708273B2C3A29BCC2F /* {{target_name}} */, 63 | ); 64 | name = Products; 65 | sourceTree = ""; 66 | }; 67 | /* End PBXGroup section */ 68 | 69 | /* Begin PBXNativeTarget section */ 70 | FBA3E83EF5F81DF361705AB2 /* iosApp */ = { 71 | isa = PBXNativeTarget; 72 | buildConfigurationList = EDD6F11B4287E6B910E47D29 /* Build configuration list for PBXNativeTarget "iosApp" */; 73 | buildPhases = ( 74 | D48850EE4DF1A743461735BE /* Compile Kotlin Framework */, 75 | 6AD98EFC3C40784458C1E4BE /* Sources */, 76 | DBD8CEDAC0F872C4C9F9F5B8 /* Frameworks */, 77 | BF98CB9C015B0BEFCB0A0B13 /* Resources */, 78 | ); 79 | buildRules = ( 80 | ); 81 | dependencies = ( 82 | ); 83 | fileSystemSynchronizedGroups = ( 84 | 98529B1B44558D434CA79376 /* iosApp */, 85 | ); 86 | name = iosApp; 87 | packageProductDependencies = ( 88 | ); 89 | productName = iosApp; 90 | productReference = 5A86DD708273B2C3A29BCC2F /* {{target_name}} */; 91 | productType = "com.apple.product-type.application"; 92 | }; 93 | /* End PBXNativeTarget section */ 94 | 95 | /* Begin PBXProject section */ 96 | 96936317EB474FAF82FDF5DE /* Project object */ = { 97 | isa = PBXProject; 98 | attributes = { 99 | BuildIndependentTargetsInParallel = 1; 100 | LastSwiftUpdateCheck = 1620; 101 | LastUpgradeCheck = 1620; 102 | TargetAttributes = { 103 | FBA3E83EF5F81DF361705AB2 = { 104 | CreatedOnToolsVersion = 16.2; 105 | }; 106 | }; 107 | }; 108 | buildConfigurationList = CFBB86E4FD9CAB09BD31F8B9 /* Build configuration list for PBXProject "iosApp" */; 109 | developmentRegion = en; 110 | hasScannedForEncodings = 0; 111 | knownRegions = ( 112 | en, 113 | Base, 114 | ); 115 | mainGroup = B5C18064B82E43FA6E0DD05B; 116 | minimizedProjectReferenceProxies = 1; 117 | preferredProjectObjectVersion = 77; 118 | productRefGroup = ADD1D5B2552AA3A7B434A2F4 /* Products */; 119 | projectDirPath = ""; 120 | projectRoot = ""; 121 | targets = ( 122 | FBA3E83EF5F81DF361705AB2 /* iosApp */, 123 | ); 124 | }; 125 | /* End PBXProject section */ 126 | 127 | /* Begin PBXResourcesBuildPhase section */ 128 | BF98CB9C015B0BEFCB0A0B13 /* Resources */ = { 129 | isa = PBXResourcesBuildPhase; 130 | buildActionMask = 2147483647; 131 | files = ( 132 | ); 133 | runOnlyForDeploymentPostprocessing = 0; 134 | }; 135 | /* End PBXResourcesBuildPhase section */ 136 | 137 | /* Begin PBXShellScriptBuildPhase section */ 138 | D48850EE4DF1A743461735BE /* Compile Kotlin Framework */ = { 139 | isa = PBXShellScriptBuildPhase; 140 | alwaysOutOfDate = 1; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | ); 144 | inputFileListPaths = ( 145 | ); 146 | inputPaths = ( 147 | ); 148 | name = "Compile Kotlin Framework"; 149 | outputFileListPaths = ( 150 | ); 151 | outputPaths = ( 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | shellPath = /bin/sh; 155 | shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :{{module_name}}:embedAndSignAppleFrameworkForXcode\n"; 156 | }; 157 | /* End PBXShellScriptBuildPhase section */ 158 | 159 | /* Begin PBXSourcesBuildPhase section */ 160 | 6AD98EFC3C40784458C1E4BE /* Sources */ = { 161 | isa = PBXSourcesBuildPhase; 162 | buildActionMask = 2147483647; 163 | files = ( 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXSourcesBuildPhase section */ 168 | 169 | /* Begin XCBuildConfiguration section */ 170 | 228AA6470793404B602654F8 /* Debug */ = { 171 | isa = XCBuildConfiguration; 172 | baseConfigurationReferenceAnchor = B3C1C90FC9CDA286B07172AF /* Configuration */; 173 | baseConfigurationReferenceRelativePath = Config.xcconfig; 174 | buildSettings = { 175 | ALWAYS_SEARCH_USER_PATHS = NO; 176 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 177 | CLANG_ANALYZER_NONNULL = YES; 178 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 179 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 180 | CLANG_ENABLE_MODULES = YES; 181 | CLANG_ENABLE_OBJC_ARC = YES; 182 | CLANG_ENABLE_OBJC_WEAK = YES; 183 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 184 | CLANG_WARN_BOOL_CONVERSION = YES; 185 | CLANG_WARN_COMMA = YES; 186 | CLANG_WARN_CONSTANT_CONVERSION = YES; 187 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 188 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 189 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 190 | CLANG_WARN_EMPTY_BODY = YES; 191 | CLANG_WARN_ENUM_CONVERSION = YES; 192 | CLANG_WARN_INFINITE_RECURSION = YES; 193 | CLANG_WARN_INT_CONVERSION = YES; 194 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 195 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 196 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 197 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 198 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 199 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 200 | CLANG_WARN_STRICT_PROTOTYPES = YES; 201 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 202 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 203 | CLANG_WARN_UNREACHABLE_CODE = YES; 204 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 205 | COPY_PHASE_STRIP = NO; 206 | DEBUG_INFORMATION_FORMAT = dwarf; 207 | ENABLE_STRICT_OBJC_MSGSEND = YES; 208 | ENABLE_TESTABILITY = YES; 209 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 210 | GCC_C_LANGUAGE_STANDARD = gnu17; 211 | GCC_DYNAMIC_NO_PIC = NO; 212 | GCC_NO_COMMON_BLOCKS = YES; 213 | GCC_OPTIMIZATION_LEVEL = 0; 214 | GCC_PREPROCESSOR_DEFINITIONS = ( 215 | "DEBUG=1", 216 | "$(inherited)", 217 | ); 218 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 219 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 220 | GCC_WARN_UNDECLARED_SELECTOR = YES; 221 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 222 | GCC_WARN_UNUSED_FUNCTION = YES; 223 | GCC_WARN_UNUSED_VARIABLE = YES; 224 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 225 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 226 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 227 | MTL_FAST_MATH = YES; 228 | ONLY_ACTIVE_ARCH = YES; 229 | SDKROOT = iphoneos; 230 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 231 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 232 | }; 233 | name = Debug; 234 | }; 235 | 16C81EECEC608FE09565E711 /* Release */ = { 236 | isa = XCBuildConfiguration; 237 | baseConfigurationReferenceAnchor = B3C1C90FC9CDA286B07172AF /* Configuration */; 238 | baseConfigurationReferenceRelativePath = Config.xcconfig; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 242 | CLANG_ANALYZER_NONNULL = YES; 243 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 244 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 245 | CLANG_ENABLE_MODULES = YES; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_ENABLE_OBJC_WEAK = YES; 248 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 249 | CLANG_WARN_BOOL_CONVERSION = YES; 250 | CLANG_WARN_COMMA = YES; 251 | CLANG_WARN_CONSTANT_CONVERSION = YES; 252 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 253 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 254 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 255 | CLANG_WARN_EMPTY_BODY = YES; 256 | CLANG_WARN_ENUM_CONVERSION = YES; 257 | CLANG_WARN_INFINITE_RECURSION = YES; 258 | CLANG_WARN_INT_CONVERSION = YES; 259 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 261 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 263 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 264 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 265 | CLANG_WARN_STRICT_PROTOTYPES = YES; 266 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 267 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 268 | CLANG_WARN_UNREACHABLE_CODE = YES; 269 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 270 | COPY_PHASE_STRIP = NO; 271 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 272 | ENABLE_NS_ASSERTIONS = NO; 273 | ENABLE_STRICT_OBJC_MSGSEND = YES; 274 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 275 | GCC_C_LANGUAGE_STANDARD = gnu17; 276 | GCC_NO_COMMON_BLOCKS = YES; 277 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 278 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 279 | GCC_WARN_UNDECLARED_SELECTOR = YES; 280 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 281 | GCC_WARN_UNUSED_FUNCTION = YES; 282 | GCC_WARN_UNUSED_VARIABLE = YES; 283 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 284 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 285 | MTL_ENABLE_DEBUG_INFO = NO; 286 | MTL_FAST_MATH = YES; 287 | SDKROOT = iphoneos; 288 | SWIFT_COMPILATION_MODE = wholemodule; 289 | VALIDATE_PRODUCT = YES; 290 | }; 291 | name = Release; 292 | }; 293 | FF8BC89969EB5899AF2AC2D3 /* Debug */ = { 294 | isa = XCBuildConfiguration; 295 | buildSettings = { 296 | ARCHS = arm64; 297 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 298 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 299 | CODE_SIGN_IDENTITY = "Apple Development"; 300 | CODE_SIGN_STYLE = Automatic; 301 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 302 | DEVELOPMENT_TEAM = "${TEAM_ID}"; 303 | ENABLE_PREVIEWS = YES; 304 | GENERATE_INFOPLIST_FILE = YES; 305 | INFOPLIST_FILE = iosApp/Info.plist; 306 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 307 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 308 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 309 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 310 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 311 | LD_RUNPATH_SEARCH_PATHS = ( 312 | "$(inherited)", 313 | "@executable_path/Frameworks", 314 | ); 315 | SWIFT_EMIT_LOC_STRINGS = YES; 316 | SWIFT_VERSION = 5.0; 317 | TARGETED_DEVICE_FAMILY = "1,2"; 318 | }; 319 | name = Debug; 320 | }; 321 | AB907B91D5394CB78BC88058 /* Release */ = { 322 | isa = XCBuildConfiguration; 323 | buildSettings = { 324 | ARCHS = arm64; 325 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 326 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 327 | CODE_SIGN_IDENTITY = "Apple Development"; 328 | CODE_SIGN_STYLE = Automatic; 329 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 330 | DEVELOPMENT_TEAM = "${TEAM_ID}"; 331 | ENABLE_PREVIEWS = YES; 332 | GENERATE_INFOPLIST_FILE = YES; 333 | INFOPLIST_FILE = iosApp/Info.plist; 334 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 335 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 336 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 337 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 338 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 339 | LD_RUNPATH_SEARCH_PATHS = ( 340 | "$(inherited)", 341 | "@executable_path/Frameworks", 342 | ); 343 | SWIFT_EMIT_LOC_STRINGS = YES; 344 | SWIFT_VERSION = 5.0; 345 | TARGETED_DEVICE_FAMILY = "1,2"; 346 | }; 347 | name = Release; 348 | }; 349 | /* End XCBuildConfiguration section */ 350 | 351 | /* Begin XCConfigurationList section */ 352 | CFBB86E4FD9CAB09BD31F8B9 /* Build configuration list for PBXProject "iosApp" */ = { 353 | isa = XCConfigurationList; 354 | buildConfigurations = ( 355 | 228AA6470793404B602654F8 /* Debug */, 356 | 16C81EECEC608FE09565E711 /* Release */, 357 | ); 358 | defaultConfigurationIsVisible = 0; 359 | defaultConfigurationName = Release; 360 | }; 361 | EDD6F11B4287E6B910E47D29 /* Build configuration list for PBXNativeTarget "iosApp" */ = { 362 | isa = XCConfigurationList; 363 | buildConfigurations = ( 364 | FF8BC89969EB5899AF2AC2D3 /* Debug */, 365 | AB907B91D5394CB78BC88058 /* Release */, 366 | ); 367 | defaultConfigurationIsVisible = 0; 368 | defaultConfigurationName = Release; 369 | }; 370 | /* End XCConfigurationList section */ 371 | }; 372 | rootObject = 96936317EB474FAF82FDF5DE /* Project object */; 373 | } 374 | -------------------------------------------------------------------------------- /cli/src/jvmMain/kotlin/Cli.kt: -------------------------------------------------------------------------------- 1 | package com.composables.cli 2 | 3 | import com.alexstyl.debugln.debugln 4 | import com.alexstyl.debugln.infoln 5 | import com.alexstyl.debugln.warnln 6 | import com.github.ajalt.clikt.core.CliktCommand 7 | import com.github.ajalt.clikt.core.Context 8 | import com.github.ajalt.clikt.core.main 9 | import com.github.ajalt.clikt.core.subcommands 10 | import com.github.ajalt.clikt.parameters.arguments.argument 11 | import com.github.ajalt.clikt.parameters.arguments.optional 12 | import com.github.ajalt.clikt.parameters.options.versionOption 13 | import java.io.File 14 | import java.io.InputStream 15 | import java.util.jar.JarFile 16 | 17 | val ANDROID = "android" 18 | val JVM = "jvm" 19 | val IOS = "ios" 20 | val WEB = "web" 21 | 22 | suspend fun main(args: Array) { 23 | ComposablesCli() 24 | .subcommands(Init(), Update(), Target()) 25 | .main(args) 26 | } 27 | 28 | class ComposablesCli : CliktCommand(name = "composables") { 29 | init { 30 | versionOption( 31 | version = BuildConfig.Version, 32 | names = setOf("-v", "--version"), 33 | message = { BuildConfig.Version } 34 | ) 35 | } 36 | 37 | override fun run() { 38 | } 39 | 40 | override fun help(context: Context) = """ 41 | If you have any problems or need help, do not hesitate to ask for help at: 42 | https://github.com/composablehorizons/composables-cli 43 | """.trimIndent() 44 | } 45 | 46 | class Update : CliktCommand("update") { 47 | 48 | override fun help(context: Context): String = """ 49 | Updates the CLI tool with the latest version 50 | """.trimIndent() 51 | 52 | override fun run() { 53 | try { 54 | echo("Updating Composables CLI...") 55 | 56 | // Get current JAR path 57 | val currentJarPath = this::class.java.protectionDomain.codeSource.location.path 58 | val installDir = File(currentJarPath).parent 59 | 60 | // Download to temporary location first 61 | val tempJar = File(installDir, "composables.jar.tmp") 62 | 63 | echo("Fetching latest version...") 64 | val latestVersion = ProcessBuilder( 65 | "bash", 66 | "-c", 67 | "curl -s https://api.github.com/repos/composablehorizons/composables-cli/releases/latest | grep '\"tag_name\":' | sed -E 's/.*\"([^\"]+)\".*/\\1/'" 68 | ) 69 | .redirectErrorStream(true) 70 | .start() 71 | .inputStream.bufferedReader() 72 | .readText() 73 | .trim() 74 | 75 | if (latestVersion.isEmpty()) { 76 | echo("Failed to fetch latest version", err = true) 77 | return 78 | } 79 | 80 | echo("Latest version: $latestVersion") 81 | echo("Downloading...") 82 | 83 | val downloadProcess = ProcessBuilder( 84 | "curl", "-fSL", 85 | "https://github.com/composablehorizons/composables-cli/releases/download/$latestVersion/composables.jar", 86 | "-o", tempJar.absolutePath 87 | ).inheritIO().start() 88 | 89 | val downloadExitCode = downloadProcess.waitFor() 90 | if (downloadExitCode != 0) { 91 | echo("Failed to download new version", err = true) 92 | tempJar.delete() 93 | return 94 | } 95 | 96 | echo("✓ Downloaded new version") 97 | 98 | // Replace the JAR file 99 | val currentJar = File(currentJarPath) 100 | if (currentJar.exists()) { 101 | if (!currentJar.delete()) { 102 | echo("Failed to remove old JAR file", err = true) 103 | tempJar.delete() 104 | return 105 | } 106 | } 107 | 108 | if (!tempJar.renameTo(currentJar)) { 109 | echo("Failed to install new JAR file", err = true) 110 | tempJar.delete() 111 | return 112 | } 113 | 114 | echo("✓ Update completed successfully!") 115 | echo("Note: Restart your terminal to use the new version") 116 | 117 | } catch (e: Exception) { 118 | echo("Failed to run update: ${e.message}", err = true) 119 | } 120 | } 121 | } 122 | 123 | class Init : CliktCommand("init") { 124 | override fun help(context: Context): String = """ 125 | Initializes a new Compose Multiplatform module to the specified path. 126 | """.trimIndent() 127 | 128 | private val directory by argument("directory", help = "The directory path to create the new module in").optional() 129 | 130 | override fun run() { 131 | val workingDir = System.getProperty("user.dir") 132 | val projectName = directory.orEmpty() 133 | if (projectName.isBlank()) { 134 | debugln { "Please specify the project directory:" } 135 | infoln { "composables init " } 136 | debugln { "" } 137 | debugln { "For example:" } 138 | infoln { "composables init app" } 139 | return 140 | } 141 | val target = if (projectName == ".") File(workingDir) else File(workingDir).resolve(projectName) 142 | 143 | // Check if we can create the directory first 144 | if (target.exists()) { 145 | if (target.listFiles()?.isEmpty() == true) { 146 | target.deleteRecursively() 147 | } else { 148 | // Check if it's a Gradle project 149 | val isGradleProject = File(target, "build.gradle.kts").exists() || 150 | File(target, "build.gradle").exists() || 151 | File(target, "settings.gradle.kts").exists() || 152 | File(target, "settings.gradle").exists() 153 | 154 | if (isGradleProject) { 155 | echo("Gradle project detected. This will add a new module to your existing project. Is this what you want? y/n ", trailingNewline = false) 156 | val response = readln().trim().lowercase() 157 | if (response != "y" && response != "yes") { 158 | echo("Operation cancelled.") 159 | return 160 | } 161 | 162 | // Check Kotlin version 163 | val kotlinVersion = getKotlinVersion(target) 164 | if (kotlinVersion != null && !isKotlinVersionSupported(kotlinVersion)) { 165 | echo("Kotlin version $kotlinVersion is not supported. At least version 2.2.21 is required.") 166 | echo("Please update your Kotlin version and try again.") 167 | return 168 | } 169 | 170 | val moduleName = readUniqueModuleName(target) 171 | val appName = readAppName() 172 | val namespace = readNamespace() 173 | val targets = readTargets() 174 | 175 | // Create only the module directory and files 176 | createModuleOnly( 177 | targetDir = target.absolutePath, 178 | moduleName = moduleName, 179 | packageName = namespace, 180 | appName = appName, 181 | targets = targets 182 | ) 183 | 184 | // Add module to settings.gradle.kts 185 | addModuleToSettings(target.absolutePath, moduleName) 186 | 187 | // Update version catalog if needed 188 | updateVersionCatalog(target.absolutePath, targets) 189 | 190 | // Update root build.gradle.kts with required plugins 191 | updateRootBuildFile(target.absolutePath, targets) 192 | 193 | // Create iOS app directory if iOS target is selected 194 | if (targets.contains("ios")) { 195 | createIosAppDirectory(target.absolutePath, moduleName) 196 | } 197 | return 198 | } else { 199 | val dirName = if (projectName == ".") "The current directory" else "The directory $projectName" 200 | echo("$dirName is not empty and does not contain a Gradle project.") 201 | echo("Try a new directory path or delete the existing one before trying to initialize a new module.") 202 | return 203 | } 204 | } 205 | } 206 | 207 | val moduleName = readModuleName(projectName) 208 | val appName = readAppName() 209 | val namespace = readNamespace() 210 | val targets = readTargets() 211 | 212 | if (!target.mkdirs()) { 213 | echo("Failed to create directory $projectName") 214 | return 215 | } 216 | 217 | cloneGradleProjectAndPrint( 218 | targetDir = workingDir, 219 | dirName = projectName, 220 | packageName = namespace, 221 | appName = appName, 222 | targets = targets, 223 | moduleName = moduleName 224 | ) 225 | } 226 | 227 | private fun readNamespace(): String { 228 | while (true) { 229 | echo("Enter package name (default: com.example.app): ", trailingNewline = false) 230 | val namespace = readln().trim() 231 | if (namespace.isEmpty()) { 232 | return "com.example.app" 233 | } 234 | 235 | if (!isValidPackageName(namespace)) { 236 | echo("Invalid package name. Must be a valid Java package name (e.g., com.example.app)") 237 | continue 238 | } 239 | 240 | return namespace 241 | } 242 | } 243 | 244 | private fun readAppName(): String { 245 | while (true) { 246 | echo("Enter app name (default: My App): ", trailingNewline = false) 247 | val appName = readln().trim() 248 | 249 | if (appName.isEmpty()) { 250 | return "My App" 251 | } 252 | 253 | if (!isValidAppName(appName)) { 254 | echo("Invalid app name. Must start with a letter and contain only letters, digits, or underscores") 255 | continue 256 | } 257 | 258 | return appName 259 | } 260 | } 261 | 262 | private fun readTargets(): Set { 263 | while (true) { 264 | val targets = mutableSetOf() 265 | 266 | echo("Which platforms would you like your app to run on?") 267 | 268 | while (true) { 269 | echo("Android (y/n, default: y): ", trailingNewline = false) 270 | val android = readln().trim().lowercase() 271 | if (android.isEmpty() || android == "y" || android == "yes") { 272 | targets.add(ANDROID) 273 | } 274 | break 275 | } 276 | 277 | while (true) { 278 | echo("JVM (Desktop) (y/n, default: y): ", trailingNewline = false) 279 | val jvm = readln().trim().lowercase() 280 | if (jvm.isEmpty() || jvm == "y" || jvm == "yes") { 281 | targets.add(JVM) 282 | } 283 | break 284 | } 285 | 286 | while (true) { 287 | echo("iOS (y/n, default: y): ", trailingNewline = false) 288 | val ios = readln().trim().lowercase() 289 | if (ios.isEmpty() || ios == "y" || ios == "yes") { 290 | targets.add(IOS) 291 | } 292 | break 293 | } 294 | 295 | while (true) { 296 | echo("Web (y/n, default: y): ", trailingNewline = false) 297 | val web = readln().trim().lowercase() 298 | if (web.isEmpty() || web == "y" || web == "yes") { 299 | targets.add(WEB) 300 | } 301 | break 302 | } 303 | 304 | if (targets.isNotEmpty()) { 305 | return targets 306 | } else { 307 | echo("At least one platform is required...") 308 | } 309 | } 310 | } 311 | 312 | private fun isValidAppName(appName: String): Boolean { 313 | if (appName.isEmpty()) return false 314 | 315 | // Check if it contains at least one letter or digit 316 | return appName.any { char -> char.isLetterOrDigit() } 317 | } 318 | 319 | private fun readModuleName(projectName: String): String { 320 | while (true) { 321 | echo("Enter module name (default: composeApp): ", trailingNewline = false) 322 | val moduleName = readln().trim() 323 | 324 | if (moduleName.isEmpty()) { 325 | return "composeApp" 326 | } 327 | 328 | if (moduleName == projectName) { 329 | echo("Module name cannot be the same as the project name \"$projectName\". Try specifying a different name.") 330 | continue 331 | } 332 | 333 | if (!isValidModuleName(moduleName)) { 334 | echo("Invalid module name. Must start with a letter and contain only letters, digits, hyphens, or underscores") 335 | continue 336 | } 337 | 338 | return moduleName 339 | } 340 | } 341 | 342 | private fun readUniqueModuleName(targetDir: File): String { 343 | while (true) { 344 | echo("Enter module name (default: composeApp): ", trailingNewline = false) 345 | val moduleName = readln().trim() 346 | 347 | if (moduleName.isEmpty()) { 348 | if (File(targetDir, "composeApp").exists()) { 349 | echo("Module name 'composeApp' already exists. Please choose a different name.") 350 | continue 351 | } 352 | return "composeApp" 353 | } 354 | 355 | if (!isValidModuleName(moduleName)) { 356 | echo("Invalid module name. Must start with a letter and contain only letters, digits, hyphens, or underscores") 357 | continue 358 | } 359 | 360 | val moduleDir = File(targetDir, moduleName) 361 | if (moduleDir.exists()) { 362 | echo("Module '$moduleName' already exists. Please choose a different name.") 363 | continue 364 | } 365 | 366 | return moduleName 367 | } 368 | } 369 | 370 | private fun isValidModuleName(moduleName: String): Boolean { 371 | if (moduleName.isEmpty()) return false 372 | 373 | // Check if it contains at least one letter or digit 374 | return moduleName.any { char -> char.isLetterOrDigit() } && 375 | moduleName.all { char -> char.isLetterOrDigit() || char == '-' || char == '_' } 376 | } 377 | 378 | private fun isValidPackageName(packageName: String): Boolean { 379 | if (packageName.isEmpty()) return false 380 | 381 | val parts = packageName.split(".") 382 | if (parts.size < 2) return false 383 | 384 | // Check each part is a valid Java identifier 385 | return parts.all { part -> 386 | part.isNotEmpty() && 387 | part[0].isLetter() && 388 | part.all { char -> char.isLetterOrDigit() || char == '_' } 389 | } 390 | } 391 | } 392 | 393 | private fun toCamelCase(input: String): String { 394 | return input.split(Regex("[-_]")) 395 | .mapIndexed { index, part -> 396 | if (index == 0) { 397 | part.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } 398 | } else { 399 | part.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } 400 | } 401 | } 402 | .joinToString("") 403 | } 404 | 405 | class Target : CliktCommand("target") { 406 | override fun help(context: Context): String = """ 407 | Adds a new Kotlin target to the current Compose Multiplatform project (options: android, jvm, ios, web). 408 | """.trimIndent() 409 | 410 | private val targetName by argument(name = "target") 411 | 412 | override fun run() { 413 | val validTargets = setOf("android", "jvm", "ios", "web") 414 | 415 | if (targetName !in validTargets) { 416 | echo("Unknown target '$targetName'") 417 | echo("Available targets: android, jvm, ios, web") 418 | echo("Usage: composables target ") 419 | return 420 | } 421 | 422 | val workingDir = System.getProperty("user.dir") 423 | 424 | if (!isValidComposeAppDirectory(workingDir)) { 425 | echo("This doesn't appear to be a Compose Multiplatform project.") 426 | echo("To create a new Compose app, run:") 427 | echo(" composables init app") 428 | return 429 | } 430 | 431 | val composeModuleBuildFile = findComposeModuleBuildFile(workingDir) 432 | if (composeModuleBuildFile == null) { 433 | echo("Could not find a Compose Multiplatform module in this project.") 434 | return 435 | } 436 | 437 | when (targetName) { 438 | "android" -> { 439 | if (hasAndroidTarget(composeModuleBuildFile)) { 440 | echo("Android target is already configured in this project.") 441 | return 442 | } 443 | try { 444 | addAndroidTarget(workingDir, composeModuleBuildFile) 445 | echo("Android target added successfully!") 446 | echo("Run '$gradleScript build' to verify the configuration.") 447 | } catch (e: Exception) { 448 | echo("Failed to add Android target: ${e.message}", err = true) 449 | } 450 | } 451 | 452 | "jvm" -> { 453 | if (hasJvmTarget(composeModuleBuildFile)) { 454 | echo("JVM target is already configured in this project.") 455 | return 456 | } 457 | try { 458 | addJvmTarget(workingDir, composeModuleBuildFile) 459 | echo("JVM target added successfully!") 460 | echo("Run '$gradleScript build' to verify the configuration.") 461 | } catch (e: Exception) { 462 | echo("Failed to add JVM target: ${e.message}", err = true) 463 | } 464 | } 465 | 466 | "ios" -> { 467 | if (hasIosTarget(composeModuleBuildFile)) { 468 | echo("iOS target is already configured in this project.") 469 | return 470 | } 471 | try { 472 | addIosTarget(workingDir, composeModuleBuildFile) 473 | echo("iOS target added successfully!") 474 | echo("Run '$gradleScript build' to verify the configuration.") 475 | } catch (e: Exception) { 476 | echo("Failed to add iOS target: ${e.message}", err = true) 477 | } 478 | } 479 | 480 | "web" -> { 481 | if (hasWebTarget(composeModuleBuildFile)) { 482 | echo("Web target is already configured in this project.") 483 | return 484 | } 485 | try { 486 | addWebTarget(workingDir, composeModuleBuildFile) 487 | echo("Web target added successfully!") 488 | echo("Run '$gradleScript build' to verify the configuration.") 489 | } catch (e: Exception) { 490 | echo("Failed to add web target: ${e.message}", err = true) 491 | } 492 | } 493 | } 494 | } 495 | 496 | private fun isValidComposeAppDirectory(directory: String): Boolean { 497 | val dir = File(directory) 498 | 499 | // Check for root build.gradle.kts 500 | val rootBuildFile = File(dir, "build.gradle.kts") 501 | if (!rootBuildFile.exists()) { 502 | return false 503 | } 504 | 505 | // Look for any subdirectory with build.gradle.kts that has Compose dependencies 506 | val subDirs = dir.listFiles()?.filter { subDir -> 507 | subDir.isDirectory && File(subDir, "build.gradle.kts").exists() 508 | } ?: return false 509 | 510 | return subDirs.any { subDir -> 511 | val buildFile = File(subDir, "build.gradle.kts") 512 | val content = buildFile.readText() 513 | hasComposeDependencies(content) 514 | } 515 | } 516 | 517 | private fun hasComposeDependencies(content: String): Boolean { 518 | val composeDependencies = listOf( 519 | "compose.components.resources", 520 | "compose.components.uiToolingPreview", 521 | "compose.material3", 522 | "compose.desktop.currentOs", 523 | "compose.preview", 524 | "compose.runtime" 525 | ) 526 | 527 | return composeDependencies.any { dependency -> 528 | content.contains(dependency) 529 | } 530 | } 531 | 532 | private fun findComposeModuleBuildFile(workingDir: String): File? { 533 | val dir = File(workingDir) 534 | 535 | val composeModules = dir.listFiles()?.filter { subDir -> 536 | subDir.isDirectory && File(subDir, "build.gradle.kts").exists() 537 | }?.filter { subDir -> 538 | val buildFile = File(subDir, "build.gradle.kts") 539 | val content = buildFile.readText() 540 | hasComposeDependencies(content) 541 | } ?: return null 542 | 543 | when { 544 | composeModules.isEmpty() -> return null 545 | composeModules.size == 1 -> return File(composeModules.first(), "build.gradle.kts") 546 | else -> { 547 | return selectComposeModule(composeModules) 548 | } 549 | } 550 | } 551 | 552 | private fun selectComposeModule(composeModules: List): File? { 553 | val sortedModules = composeModules.sortedBy { it.name } 554 | echo("Multiple Compose modules detected:") 555 | sortedModules.forEachIndexed { index, module -> 556 | echo(" ${index + 1}. ${module.name}") 557 | } 558 | 559 | while (true) { 560 | echo("Select a module (1-${sortedModules.size}): ", trailingNewline = false) 561 | val input = readln().trim() 562 | 563 | val selection = input.toIntOrNull() 564 | if (selection != null && selection in 1..sortedModules.size) { 565 | val selectedModule = sortedModules[selection - 1] 566 | echo("Selected module: ${selectedModule.name}") 567 | return File(selectedModule, "build.gradle.kts") 568 | } else { 569 | echo("Invalid selection. Please enter a number between 1 and ${sortedModules.size}") 570 | } 571 | } 572 | } 573 | 574 | private fun hasAndroidTarget(buildFile: File): Boolean { 575 | val content = buildFile.readText() 576 | return content.contains("androidTarget {") || content.contains("android {") 577 | } 578 | 579 | private fun hasJvmTarget(buildFile: File): Boolean { 580 | val content = buildFile.readText() 581 | return content.contains("jvm()") 582 | } 583 | 584 | private fun hasIosTarget(buildFile: File): Boolean { 585 | val content = buildFile.readText() 586 | return content.contains("iosArm64()") || content.contains("iosSimulatorArm64()") 587 | } 588 | 589 | private fun hasWebTarget(buildFile: File): Boolean { 590 | val content = buildFile.readText() 591 | return content.contains("js(") || content.contains("wasmJs(") 592 | } 593 | 594 | private fun addAndroidTarget(workingDir: String, buildFile: File) { 595 | var content = buildFile.readText() 596 | val lines = content.lines().toMutableList() 597 | 598 | // Add import if needed 599 | if (!content.contains("import org.jetbrains.kotlin.gradle.dsl.JvmTarget")) { 600 | val importLine = "import org.jetbrains.kotlin.gradle.dsl.JvmTarget" 601 | // Find the last import line and add after it 602 | val lastImportIndex = lines.indexOfLast { it.startsWith("import ") } 603 | if (lastImportIndex >= 0) { 604 | lines.add(lastImportIndex + 1, importLine) 605 | } else { 606 | // Add before the first non-empty, non-comment line 607 | val firstCodeLine = lines.indexOfFirst { !it.trim().isEmpty() && !it.trim().startsWith("//") } 608 | if (firstCodeLine >= 0) { 609 | lines.add(firstCodeLine, importLine) 610 | } 611 | } 612 | } 613 | 614 | // Append to plugins block 615 | val pluginsCloseIndex = findPluginsBlockEnd(lines) 616 | if (pluginsCloseIndex >= 0) { 617 | lines.add(pluginsCloseIndex, " alias(libs.plugins.android.application)") 618 | } 619 | 620 | // Append to kotlin block 621 | val kotlinCloseIndex = findKotlinBlockEnd(lines) 622 | if (kotlinCloseIndex >= 0) { 623 | val androidTargetLines = listOf( 624 | "", 625 | " androidTarget {", 626 | " compilerOptions {", 627 | " jvmTarget.set(JvmTarget.JVM_11)", 628 | " }", 629 | " }" 630 | ) 631 | androidTargetLines.reversed().forEach { line -> 632 | lines.add(kotlinCloseIndex, line) 633 | } 634 | } 635 | 636 | // Append to sourceSets block 637 | val sourceSetsCloseIndex = findSourceSetsBlockEnd(lines) 638 | if (sourceSetsCloseIndex >= 0) { 639 | val androidMainLines = listOf( 640 | "", 641 | " androidMain.dependencies {", 642 | " implementation(compose.preview)", 643 | " implementation(compose.material3)", 644 | " implementation(libs.androidx.activity.compose)", 645 | " }" 646 | ) 647 | androidMainLines.reversed().forEach { line -> 648 | lines.add(sourceSetsCloseIndex, line) 649 | } 650 | } 651 | 652 | // Add android block at the end 653 | val namespace = "com.example.app" // Default namespace for target command 654 | val androidBlock = listOf( 655 | "", 656 | "android {", 657 | " namespace = \"$namespace\"", 658 | " compileSdk = 36", 659 | "", 660 | " defaultConfig {", 661 | " applicationId = \"$namespace\"", 662 | " minSdk = 24", 663 | " targetSdk = 36", 664 | " versionCode = 1", 665 | " versionName = \"1.0\"", 666 | " }", 667 | " packaging {", 668 | " resources {", 669 | " excludes += \"/META-INF/{AL2.0,LGPL2.1}\"", 670 | " }", 671 | " }", 672 | " buildTypes {", 673 | " getByName(\"release\") {", 674 | " isMinifyEnabled = false", 675 | " }", 676 | " }", 677 | " compileOptions {", 678 | " sourceCompatibility = JavaVersion.VERSION_11", 679 | " targetCompatibility = JavaVersion.VERSION_11", 680 | " }", 681 | "}" 682 | ) 683 | lines.addAll(androidBlock) 684 | 685 | // Write updated content 686 | buildFile.writeText(lines.joinToString("\n")) 687 | 688 | // Create androidMain source set and MainActivity 689 | val moduleDir = buildFile.parentFile 690 | createAndroidSourceSet(moduleDir, namespace) 691 | 692 | // Copy Android resources 693 | copyAndroidResources(moduleDir, namespace) 694 | 695 | // Update root build.gradle.kts 696 | updateRootBuildFile(workingDir) 697 | 698 | // Update gradle.properties 699 | updateGradleProperties(workingDir) 700 | 701 | // Update libs.versions.toml 702 | updateVersionsFile(workingDir) 703 | } 704 | 705 | private fun findPluginsBlockEnd(lines: List): Int { 706 | var depth = 0 707 | for (i in lines.indices) { 708 | val line = lines[i].trim() 709 | if (line.startsWith("plugins {")) { 710 | depth = 1 711 | for (j in i + 1 until lines.size) { 712 | val currentLine = lines[j].trim() 713 | if (currentLine.contains("{")) depth++ 714 | if (currentLine.contains("}")) depth-- 715 | if (depth == 0) return j 716 | } 717 | } 718 | } 719 | return -1 720 | } 721 | 722 | private fun findKotlinBlockEnd(lines: List): Int { 723 | var depth = 0 724 | for (i in lines.indices) { 725 | val line = lines[i].trim() 726 | if (line.startsWith("kotlin {")) { 727 | depth = 1 728 | for (j in i + 1 until lines.size) { 729 | val currentLine = lines[j].trim() 730 | if (currentLine.contains("{")) depth++ 731 | if (currentLine.contains("}")) depth-- 732 | if (depth == 0) return j 733 | } 734 | } 735 | } 736 | return -1 737 | } 738 | 739 | private fun findSourceSetsBlockEnd(lines: List): Int { 740 | var depth = 0 741 | for (i in lines.indices) { 742 | val line = lines[i].trim() 743 | if (line.contains("sourceSets") && line.contains("{")) { 744 | depth = 1 745 | for (j in i + 1 until lines.size) { 746 | val currentLine = lines[j].trim() 747 | if (currentLine.contains("{")) depth++ 748 | if (currentLine.contains("}")) depth-- 749 | if (depth == 0) return j 750 | } 751 | } 752 | } 753 | return -1 754 | } 755 | 756 | private fun extractNamespace(lines: List): String { 757 | // Try to find existing namespace from android block or use a default 758 | for (line in lines) { 759 | if (line.trim().startsWith("namespace =")) { 760 | return line.trim().substringAfter("namespace =").trim().removeSurrounding("\"") 761 | } 762 | } 763 | return "com.example.app" // fallback 764 | } 765 | 766 | private fun updateRootBuildFile(workingDir: String) { 767 | val rootBuildFile = File(workingDir, "build.gradle.kts") 768 | if (!rootBuildFile.exists()) return 769 | 770 | var content = rootBuildFile.readText() 771 | if (!content.contains("android-application")) { 772 | // Find the plugins block and add android plugin 773 | val lines = content.lines().toMutableList() 774 | val pluginsCloseIndex = findPluginsBlockEnd(lines) 775 | if (pluginsCloseIndex >= 0) { 776 | lines.add(pluginsCloseIndex, " alias(libs.plugins.android.application) apply false") 777 | rootBuildFile.writeText(lines.joinToString("\n")) 778 | } 779 | } 780 | } 781 | 782 | private fun updateVersionsFile(workingDir: String) { 783 | val versionsFile = File(workingDir, "gradle/libs.versions.toml") 784 | if (!versionsFile.exists()) return 785 | 786 | var content = versionsFile.readText() 787 | 788 | // Add Android versions if not present 789 | if (!content.contains("agp =")) { 790 | content = content.replace( 791 | "[versions]", 792 | """[versions] 793 | # Android 794 | agp = "8.11.2" 795 | android-compileSdk = "36" 796 | android-minSdk = "24" 797 | android-targetSdk = "36" 798 | activityCompose = "1.11.0" 799 | """ 800 | ) 801 | } 802 | 803 | // Add Android libraries if not present 804 | if (!content.contains("androidx-activity-compose")) { 805 | content = content.replace( 806 | "[libraries]", 807 | """[libraries] 808 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } 809 | """ 810 | ) 811 | } 812 | 813 | // Add Android plugins if not present 814 | if (!content.contains("android-application")) { 815 | content = content.replace( 816 | "[plugins]", 817 | """[plugins] 818 | android-application = { id = "com.android.application", version.ref = "agp" } 819 | """ 820 | ) 821 | } 822 | 823 | versionsFile.writeText(content) 824 | } 825 | 826 | private fun updateGradleProperties(workingDir: String) { 827 | val gradlePropertiesFile = File(workingDir, "gradle.properties") 828 | if (!gradlePropertiesFile.exists()) return 829 | 830 | var content = gradlePropertiesFile.readText() 831 | 832 | // Add Android properties if not present 833 | if (!content.contains("android.useAndroidX")) { 834 | content += "\n\n#Android\nandroid.useAndroidX=true\nandroid.nonTransitiveRClass=true\n" 835 | } 836 | 837 | gradlePropertiesFile.writeText(content) 838 | } 839 | 840 | private fun createAndroidSourceSet(moduleDir: File, namespace: String) { 841 | val androidMainDir = File(moduleDir, "src/androidMain/kotlin") 842 | val packageDir = File(androidMainDir, namespace.replace(".", "/")) 843 | 844 | // Create directories 845 | packageDir.mkdirs() 846 | 847 | // Create MainActivity.kt 848 | val mainActivityFile = File(packageDir, "MainActivity.kt") 849 | val mainActivityContent = """package $namespace 850 | 851 | import android.os.Bundle 852 | import androidx.activity.ComponentActivity 853 | import androidx.activity.compose.setContent 854 | import androidx.compose.foundation.layout.Arrangement 855 | import androidx.compose.foundation.layout.Box 856 | import androidx.compose.foundation.layout.Column 857 | import androidx.compose.foundation.layout.fillMaxSize 858 | import androidx.compose.foundation.layout.padding 859 | import androidx.compose.foundation.layout.safeDrawingPadding 860 | import androidx.compose.material3.MaterialTheme 861 | import androidx.compose.material3.Scaffold 862 | import androidx.compose.material3.Text 863 | import androidx.compose.runtime.Composable 864 | import androidx.compose.ui.Alignment 865 | import androidx.compose.ui.Modifier 866 | import androidx.compose.ui.text.style.TextAlign 867 | import androidx.compose.ui.unit.dp 868 | import androidx.compose.ui.tooling.preview.Preview 869 | 870 | class MainActivity : ComponentActivity() { 871 | override fun onCreate(savedInstanceState: Bundle?) { 872 | super.onCreate(savedInstanceState) 873 | setContent { 874 | AndroidApp() 875 | } 876 | } 877 | } 878 | 879 | @Composable 880 | fun AndroidApp() { 881 | MaterialTheme { 882 | Scaffold { 883 | Box( 884 | modifier = Modifier 885 | .safeDrawingPadding() 886 | .fillMaxSize() 887 | .padding(16.dp), 888 | contentAlignment = Alignment.Center 889 | ) { 890 | Column( 891 | horizontalAlignment = Alignment.CenterHorizontally, 892 | verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically) 893 | ) { 894 | Text( 895 | text = "Hello Beautiful World!", 896 | style = MaterialTheme.typography.displayLarge, 897 | textAlign = TextAlign.Center 898 | ) 899 | Text( 900 | text = "Go to MainActivity.kt to edit your app", 901 | style = MaterialTheme.typography.displayMedium, 902 | textAlign = TextAlign.Center 903 | ) 904 | } 905 | } 906 | } 907 | } 908 | } 909 | 910 | @Preview 911 | @Composable 912 | fun DefaultPreview() { 913 | AndroidApp() 914 | } 915 | """ 916 | mainActivityFile.writeText(mainActivityContent) 917 | } 918 | 919 | private fun copyAndroidResources(moduleDir: File, namespace: String) { 920 | fun copyResource(resourcePath: String, targetFile: File) { 921 | val inputStream: InputStream? = object {}.javaClass.getResourceAsStream(resourcePath) 922 | if (inputStream != null) { 923 | targetFile.parentFile?.mkdirs() 924 | inputStream.use { input -> 925 | targetFile.outputStream().use { output -> 926 | input.copyTo(output) 927 | } 928 | } 929 | } 930 | } 931 | 932 | fun listResources(path: String): List { 933 | val resources = mutableListOf() 934 | val resourceUrl = object {}.javaClass.getResource(path) 935 | 936 | if (resourceUrl != null) { 937 | when (resourceUrl.protocol) { 938 | "file" -> { 939 | val dir = File(resourceUrl.toURI()) 940 | dir.walkTopDown().forEach { file -> 941 | if (file.isFile) { 942 | val relativePath = file.relativeTo(dir) 943 | resources.add("$path/${relativePath.path}") 944 | } 945 | } 946 | } 947 | 948 | "jar" -> { 949 | val jarPath = resourceUrl.path.substringBefore("!") 950 | val jarFile = JarFile(File(jarPath.substringAfter("file:"))) 951 | val entries = jarFile.entries() 952 | 953 | while (entries.hasMoreElements()) { 954 | val entry = entries.nextElement() 955 | if (entry.name.startsWith(path.substring(1)) && !entry.isDirectory) { 956 | resources.add("/${entry.name}") 957 | } 958 | } 959 | jarFile.close() 960 | } 961 | } 962 | } 963 | 964 | return resources 965 | } 966 | 967 | val resources = listResources("/project/composeApp/src/androidMain") 968 | resources.forEach { resourcePath -> 969 | val targetPath = resourcePath.removePrefix("/project/composeApp/src/androidMain/") 970 | 971 | // Skip MainActivity.kt template since we create it programmatically 972 | if (targetPath.endsWith("MainActivity.kt")) { 973 | return@forEach 974 | } 975 | 976 | val targetFile = File(moduleDir, "src/androidMain/$targetPath") 977 | copyResource(resourcePath, targetFile) 978 | 979 | // Replace placeholders in text files 980 | if (targetFile.name.endsWith(".kt")) { 981 | try { 982 | val content = targetFile.readText() 983 | var updatedContent = content.replace("{{namespace}}", namespace) 984 | updatedContent = updatedContent.replace("{{app_name}}", "My App") 985 | if (content != updatedContent) { 986 | targetFile.writeText(updatedContent) 987 | } 988 | } catch (e: Exception) { 989 | // Skip binary files 990 | } 991 | } 992 | 993 | // Replace placeholders in strings.xml 994 | if (targetFile.name == "strings.xml") { 995 | try { 996 | val content = targetFile.readText() 997 | var updatedContent = content.replace("{{app_name}}", "My App") 998 | if (content != updatedContent) { 999 | targetFile.writeText(updatedContent) 1000 | } 1001 | } catch (e: Exception) { 1002 | // Skip binary files 1003 | } 1004 | } 1005 | } 1006 | } 1007 | 1008 | private fun addJvmTarget(workingDir: String, buildFile: File) { 1009 | var content = buildFile.readText() 1010 | val lines = content.lines().toMutableList() 1011 | 1012 | // Add import if needed 1013 | if (!content.contains("import org.jetbrains.compose.desktop.application.dsl.TargetFormat")) { 1014 | val importLine = "import org.jetbrains.compose.desktop.application.dsl.TargetFormat" 1015 | // Find the last import line and add after it 1016 | val lastImportIndex = lines.indexOfLast { it.startsWith("import ") } 1017 | if (lastImportIndex >= 0) { 1018 | lines.add(lastImportIndex + 1, importLine) 1019 | } else { 1020 | // Add before the first non-empty, non-comment line 1021 | val firstCodeLine = lines.indexOfFirst { !it.trim().isEmpty() && !it.trim().startsWith("//") } 1022 | if (firstCodeLine >= 0) { 1023 | lines.add(firstCodeLine, importLine) 1024 | } 1025 | } 1026 | } 1027 | 1028 | // Append to kotlin block 1029 | val kotlinCloseIndex = findKotlinBlockEnd(lines) 1030 | if (kotlinCloseIndex >= 0) { 1031 | lines.add(kotlinCloseIndex, " jvm()") 1032 | } 1033 | 1034 | // Append to sourceSets block 1035 | val sourceSetsCloseIndex = findSourceSetsBlockEnd(lines) 1036 | if (sourceSetsCloseIndex >= 0) { 1037 | val jvmMainLines = listOf( 1038 | "", 1039 | " jvmMain.dependencies {", 1040 | " implementation(compose.desktop.currentOs)", 1041 | " implementation(compose.material3)", 1042 | " }" 1043 | ) 1044 | jvmMainLines.reversed().forEach { line -> 1045 | lines.add(sourceSetsCloseIndex, line) 1046 | } 1047 | } 1048 | 1049 | // Add compose.desktop block at the end 1050 | val namespace = extractNamespace(lines) 1051 | val desktopBlock = listOf( 1052 | "", 1053 | "compose.desktop {", 1054 | " application {", 1055 | " mainClass = \"${namespace}.MainKt\"", 1056 | "", 1057 | " nativeDistributions {", 1058 | " targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)", 1059 | " packageName = \"${namespace}\"", 1060 | " packageVersion = \"1.0.0\"", 1061 | " }", 1062 | " }", 1063 | "}" 1064 | ) 1065 | lines.addAll(desktopBlock) 1066 | 1067 | // Write updated content 1068 | buildFile.writeText(lines.joinToString("\n")) 1069 | 1070 | // Create jvmMain source set and main function 1071 | val moduleDir = buildFile.parentFile 1072 | createJvmSourceSet(moduleDir, namespace) 1073 | } 1074 | 1075 | private fun createJvmSourceSet(moduleDir: File, namespace: String) { 1076 | val jvmMainDir = File(moduleDir, "src/jvmMain/kotlin") 1077 | val packageDir = File(jvmMainDir, namespace.replace(".", "/")) 1078 | 1079 | // Create directories 1080 | packageDir.mkdirs() 1081 | 1082 | // Create main.desktop.kt 1083 | val mainFile = File(packageDir, "main.desktop.kt") 1084 | val mainContent = """@file:JvmName("MainKt") 1085 | package $namespace 1086 | 1087 | import androidx.compose.foundation.layout.Arrangement 1088 | import androidx.compose.foundation.layout.Box 1089 | import androidx.compose.foundation.layout.Column 1090 | import androidx.compose.foundation.layout.fillMaxSize 1091 | import androidx.compose.foundation.layout.padding 1092 | import androidx.compose.foundation.layout.safeDrawingPadding 1093 | import androidx.compose.material3.MaterialTheme 1094 | import androidx.compose.material3.Scaffold 1095 | import androidx.compose.material3.Text 1096 | import androidx.compose.runtime.Composable 1097 | import androidx.compose.ui.Alignment 1098 | import androidx.compose.ui.Modifier 1099 | import androidx.compose.ui.text.style.TextAlign 1100 | import androidx.compose.ui.unit.dp 1101 | import androidx.compose.ui.window.singleWindowApplication 1102 | import org.jetbrains.compose.ui.tooling.preview.Preview 1103 | 1104 | fun main() = singleWindowApplication { 1105 | DesktopApp() 1106 | } 1107 | 1108 | @Composable 1109 | fun DesktopApp() { 1110 | MaterialTheme { 1111 | Scaffold { 1112 | Box( 1113 | modifier = Modifier 1114 | .safeDrawingPadding() 1115 | .fillMaxSize() 1116 | .padding(16.dp), 1117 | contentAlignment = Alignment.Center 1118 | ) { 1119 | Column( 1120 | horizontalAlignment = Alignment.CenterHorizontally, 1121 | verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically) 1122 | ) { 1123 | Text( 1124 | text = "Hello Beautiful World!", 1125 | style = MaterialTheme.typography.displayLarge, 1126 | textAlign = TextAlign.Center 1127 | ) 1128 | Text( 1129 | text = "Go to main.desktop.kt to edit your app", 1130 | style = MaterialTheme.typography.displayMedium, 1131 | textAlign = TextAlign.Center 1132 | ) 1133 | } 1134 | } 1135 | } 1136 | } 1137 | } 1138 | 1139 | @Preview 1140 | @Composable 1141 | fun DesktopAppPreview() { 1142 | DesktopApp() 1143 | } 1144 | """ 1145 | mainFile.writeText(mainContent) 1146 | } 1147 | 1148 | private fun addIosTarget(workingDir: String, buildFile: File) { 1149 | var content = buildFile.readText() 1150 | val lines = content.lines().toMutableList() 1151 | 1152 | // Append to kotlin block 1153 | val kotlinCloseIndex = findKotlinBlockEnd(lines) 1154 | if (kotlinCloseIndex >= 0) { 1155 | val moduleName = buildFile.parentFile.name 1156 | val baseName = toCamelCase(moduleName) 1157 | val iosTargetLines = listOf( 1158 | "", 1159 | " listOf(", 1160 | " iosArm64(),", 1161 | " iosSimulatorArm64()", 1162 | " ).forEach { iosTarget ->", 1163 | " iosTarget.binaries.framework {", 1164 | " baseName = \"$baseName\"", 1165 | " isStatic = true", 1166 | " }", 1167 | " }" 1168 | ) 1169 | iosTargetLines.reversed().forEach { line -> 1170 | lines.add(kotlinCloseIndex, line) 1171 | } 1172 | } 1173 | 1174 | // Append to sourceSets block 1175 | val sourceSetsCloseIndex = findSourceSetsBlockEnd(lines) 1176 | if (sourceSetsCloseIndex >= 0) { 1177 | val iosMainLines = listOf( 1178 | "", 1179 | " iosMain.dependencies {", 1180 | " implementation(compose.material3)", 1181 | " }" 1182 | ) 1183 | iosMainLines.reversed().forEach { line -> 1184 | lines.add(sourceSetsCloseIndex, line) 1185 | } 1186 | } 1187 | 1188 | // Write updated content 1189 | buildFile.writeText(lines.joinToString("\n")) 1190 | 1191 | // Create iosMain source set 1192 | val moduleDir = buildFile.parentFile 1193 | createIosSourceSet(moduleDir, extractNamespace(lines)) 1194 | 1195 | // Copy iOS app directory 1196 | copyIosAppDirectory(workingDir, moduleDir.name) // iOS app is still at root level 1197 | 1198 | // Link iOS project for IDE 1199 | try { 1200 | debugln { "Preparing iOS target..." } 1201 | val process = ProcessBuilder(gradleScript, "compileIosMainKotlinMetadata", "--quiet") 1202 | .directory(File(workingDir)) 1203 | .inheritIO() 1204 | .start() 1205 | val exitCode = process.waitFor() 1206 | if (exitCode == 0) { 1207 | echo("iOS target is now ready to run from the IDE") 1208 | } else { 1209 | echo("Warning: Failed to link iOS project for IDE. You may need to run '$gradleScript compileIosMainKotlinMetadata' manually.") 1210 | } 1211 | } catch (e: Exception) { 1212 | echo("Warning: Failed to link iOS project for IDE: ${e.message}") 1213 | } 1214 | } 1215 | 1216 | private fun createIosSourceSet(moduleDir: File, namespace: String) { 1217 | val iosMainDir = File(moduleDir, "src/iosMain/kotlin") 1218 | val packageDir = iosMainDir 1219 | 1220 | // Create directories 1221 | packageDir.mkdirs() 1222 | 1223 | // Create IosApp.kt 1224 | val mainFile = File(packageDir, "MainViewController.kt") 1225 | val mainContent = """import androidx.compose.foundation.layout.Arrangement 1226 | import androidx.compose.foundation.layout.Box 1227 | import androidx.compose.foundation.layout.Column 1228 | import androidx.compose.foundation.layout.fillMaxSize 1229 | import androidx.compose.foundation.layout.padding 1230 | import androidx.compose.foundation.layout.safeDrawingPadding 1231 | import androidx.compose.material3.MaterialTheme 1232 | import androidx.compose.material3.Scaffold 1233 | import androidx.compose.material3.Text 1234 | import androidx.compose.runtime.Composable 1235 | import androidx.compose.ui.Alignment 1236 | import androidx.compose.ui.Modifier 1237 | import androidx.compose.ui.text.style.TextAlign 1238 | import androidx.compose.ui.unit.dp 1239 | import org.jetbrains.compose.ui.tooling.preview.Preview 1240 | import androidx.compose.ui.window.ComposeUIViewController 1241 | 1242 | fun MainViewController() = ComposeUIViewController { IosApp() } 1243 | 1244 | @Composable 1245 | fun IosApp() { 1246 | MaterialTheme { 1247 | Scaffold { 1248 | Box( 1249 | modifier = Modifier 1250 | .safeDrawingPadding() 1251 | .fillMaxSize() 1252 | .padding(16.dp), 1253 | contentAlignment = Alignment.Center 1254 | ) { 1255 | Column( 1256 | horizontalAlignment = Alignment.CenterHorizontally, 1257 | verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically) 1258 | ) { 1259 | Text( 1260 | text = "Hello Beautiful World!", 1261 | style = MaterialTheme.typography.displayLarge, 1262 | textAlign = TextAlign.Center 1263 | ) 1264 | Text( 1265 | text = "Go to MainViewController.kt to edit your app", 1266 | style = MaterialTheme.typography.displayMedium, 1267 | textAlign = TextAlign.Center 1268 | ) 1269 | } 1270 | } 1271 | } 1272 | } 1273 | } 1274 | 1275 | @Preview 1276 | @Composable 1277 | fun IosAppPreview() { 1278 | IosApp() 1279 | } 1280 | """ 1281 | mainFile.writeText(mainContent) 1282 | } 1283 | 1284 | private fun addWebTarget(workingDir: String, buildFile: File) { 1285 | var content = buildFile.readText() 1286 | val lines = content.lines().toMutableList() 1287 | 1288 | // Add imports if needed 1289 | if (!content.contains("import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl")) { 1290 | val importLine = "import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl" 1291 | val lastImportIndex = lines.indexOfLast { it.startsWith("import ") } 1292 | if (lastImportIndex >= 0) { 1293 | lines.add(lastImportIndex + 1, importLine) 1294 | } else { 1295 | val firstCodeLine = lines.indexOfFirst { !it.trim().isEmpty() && !it.trim().startsWith("//") } 1296 | if (firstCodeLine >= 0) { 1297 | lines.add(firstCodeLine, importLine) 1298 | } 1299 | } 1300 | } 1301 | 1302 | if (!content.contains("import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig")) { 1303 | val importLine = "import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig" 1304 | val lastImportIndex = lines.indexOfLast { it.startsWith("import ") } 1305 | if (lastImportIndex >= 0) { 1306 | lines.add(lastImportIndex + 1, importLine) 1307 | } else { 1308 | val firstCodeLine = lines.indexOfFirst { !it.trim().isEmpty() && !it.trim().startsWith("//") } 1309 | if (firstCodeLine >= 0) { 1310 | lines.add(firstCodeLine, importLine) 1311 | } 1312 | } 1313 | } 1314 | 1315 | // Append to kotlin block 1316 | val kotlinCloseIndex = findKotlinBlockEnd(lines) 1317 | if (kotlinCloseIndex >= 0) { 1318 | val webTargetLines = listOf( 1319 | "", 1320 | " js {", 1321 | " browser {", 1322 | " val rootDirPath = project.rootDir.path", 1323 | " val projectDirPath = project.projectDir.path", 1324 | " commonWebpackConfig {", 1325 | " outputFileName = \"composeApp.js\"", 1326 | " devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {", 1327 | " static = (static ?: mutableListOf()).apply {", 1328 | " add(rootDirPath)", 1329 | " add(projectDirPath)", 1330 | " }", 1331 | " }", 1332 | " }", 1333 | " }", 1334 | " binaries.executable()", 1335 | " }", 1336 | "", 1337 | " @OptIn(ExperimentalWasmDsl::class)", 1338 | " wasmJs {", 1339 | " browser {", 1340 | " val rootDirPath = project.rootDir.path", 1341 | " val projectDirPath = project.projectDir.path", 1342 | " commonWebpackConfig {", 1343 | " outputFileName = \"composeApp.js\"", 1344 | " devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {", 1345 | " static = (static ?: mutableListOf()).apply {", 1346 | " add(rootDirPath)", 1347 | " add(projectDirPath)", 1348 | " }", 1349 | " }", 1350 | " }", 1351 | " }", 1352 | " binaries.executable()", 1353 | " }" 1354 | ) 1355 | webTargetLines.reversed().forEach { line -> 1356 | lines.add(kotlinCloseIndex, line) 1357 | } 1358 | } 1359 | 1360 | // Append to sourceSets block 1361 | val sourceSetsCloseIndex = findSourceSetsBlockEnd(lines) 1362 | if (sourceSetsCloseIndex >= 0) { 1363 | val webMainLines = listOf( 1364 | "", 1365 | " jsMain.dependencies {", 1366 | " implementation(compose.material3)", 1367 | " }" 1368 | ) 1369 | webMainLines.reversed().forEach { line -> 1370 | lines.add(sourceSetsCloseIndex, line) 1371 | } 1372 | } 1373 | 1374 | // Write updated content 1375 | buildFile.writeText(lines.joinToString("\n")) 1376 | 1377 | // Create web source sets 1378 | val moduleDir = buildFile.parentFile 1379 | createWebSourceSets(moduleDir, extractNamespace(lines)) 1380 | 1381 | // Copy webpack.config.d directory 1382 | copyWebpackConfigDirectory(moduleDir) 1383 | 1384 | // Copy resources directory 1385 | copyResourcesDirectory(moduleDir) 1386 | } 1387 | 1388 | private fun createWebSourceSets(moduleDir: File, namespace: String) { 1389 | // Create webMain source set 1390 | val webMainDir = File(moduleDir, "src/webMain/kotlin") 1391 | val webPackageDir = webMainDir 1392 | webPackageDir.mkdirs() 1393 | 1394 | val webMainFile = File(webPackageDir, "main.web.kt") 1395 | val webMainContent = """import androidx.compose.foundation.layout.Arrangement 1396 | import androidx.compose.foundation.layout.Box 1397 | import androidx.compose.foundation.layout.Column 1398 | import androidx.compose.foundation.layout.fillMaxSize 1399 | import androidx.compose.foundation.layout.padding 1400 | import androidx.compose.foundation.layout.safeDrawingPadding 1401 | import androidx.compose.material3.MaterialTheme 1402 | import androidx.compose.material3.Scaffold 1403 | import androidx.compose.material3.Text 1404 | import androidx.compose.runtime.Composable 1405 | import androidx.compose.ui.Alignment 1406 | import androidx.compose.ui.Modifier 1407 | import androidx.compose.ui.text.style.TextAlign 1408 | import androidx.compose.ui.unit.dp 1409 | import androidx.compose.ui.ExperimentalComposeUiApi 1410 | import androidx.compose.ui.window.ComposeViewport 1411 | import org.jetbrains.compose.ui.tooling.preview.Preview 1412 | 1413 | @OptIn(ExperimentalComposeUiApi::class) 1414 | fun main() { 1415 | ComposeViewport { 1416 | WebApp() 1417 | } 1418 | } 1419 | 1420 | @Composable 1421 | fun WebApp() { 1422 | MaterialTheme { 1423 | Scaffold { 1424 | Box( 1425 | modifier = Modifier 1426 | .safeDrawingPadding() 1427 | .fillMaxSize() 1428 | .padding(16.dp), 1429 | contentAlignment = Alignment.Center 1430 | ) { 1431 | Column( 1432 | horizontalAlignment = Alignment.CenterHorizontally, 1433 | verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically) 1434 | ) { 1435 | Text( 1436 | text = "Hello Beautiful World!", 1437 | style = MaterialTheme.typography.displayLarge, 1438 | textAlign = TextAlign.Center 1439 | ) 1440 | Text( 1441 | text = "Go to main.web.kt to edit your app", 1442 | style = MaterialTheme.typography.displayMedium, 1443 | textAlign = TextAlign.Center 1444 | ) 1445 | } 1446 | } 1447 | } 1448 | } 1449 | } 1450 | 1451 | @Preview 1452 | @Composable 1453 | fun WebAppPreview() { 1454 | WebApp() 1455 | } 1456 | """ 1457 | webMainFile.writeText(webMainContent) 1458 | } 1459 | 1460 | private fun copyWebpackConfigDirectory(moduleDir: File) { 1461 | val targetDir = File(moduleDir, "webpack.config.d") 1462 | 1463 | fun copyResource(resourcePath: String, targetFile: File) { 1464 | val inputStream: InputStream? = object {}.javaClass.getResourceAsStream(resourcePath) 1465 | if (inputStream != null) { 1466 | targetFile.parentFile?.mkdirs() 1467 | inputStream.use { input -> 1468 | targetFile.outputStream().use { output -> 1469 | input.copyTo(output) 1470 | } 1471 | } 1472 | } 1473 | } 1474 | 1475 | fun listResources(path: String): List { 1476 | val resources = mutableListOf() 1477 | val resourceUrl = object {}.javaClass.getResource(path) 1478 | 1479 | if (resourceUrl != null) { 1480 | when (resourceUrl.protocol) { 1481 | "file" -> { 1482 | val dir = File(resourceUrl.toURI()) 1483 | dir.walkTopDown().forEach { file -> 1484 | if (file.isFile) { 1485 | val relativePath = file.relativeTo(dir) 1486 | resources.add("$path/${relativePath.path}") 1487 | } 1488 | } 1489 | } 1490 | 1491 | "jar" -> { 1492 | val jarPath = resourceUrl.path.substringBefore("!") 1493 | val jarFile = JarFile(File(jarPath.substringAfter("file:"))) 1494 | val entries = jarFile.entries() 1495 | 1496 | while (entries.hasMoreElements()) { 1497 | val entry = entries.nextElement() 1498 | if (entry.name.startsWith(path.substring(1)) && !entry.isDirectory) { 1499 | resources.add("/${entry.name}") 1500 | } 1501 | } 1502 | jarFile.close() 1503 | } 1504 | } 1505 | } 1506 | 1507 | return resources 1508 | } 1509 | 1510 | val resources = listResources("/project/composeApp/webpack.config.d") 1511 | resources.forEach { resourcePath -> 1512 | val targetPath = resourcePath.removePrefix("/project/composeApp/webpack.config.d/") 1513 | val targetFile = targetDir.resolve(targetPath) 1514 | copyResource(resourcePath, targetFile) 1515 | } 1516 | } 1517 | 1518 | private fun copyResourcesDirectory(moduleDir: File) { 1519 | val targetDir = File(moduleDir, "src/webMain/resources") 1520 | 1521 | fun copyResource(resourcePath: String, targetFile: File) { 1522 | val inputStream: InputStream? = object {}.javaClass.getResourceAsStream(resourcePath) 1523 | if (inputStream != null) { 1524 | targetFile.parentFile?.mkdirs() 1525 | inputStream.use { input -> 1526 | targetFile.outputStream().use { output -> 1527 | input.copyTo(output) 1528 | } 1529 | } 1530 | } 1531 | } 1532 | 1533 | fun listResources(path: String): List { 1534 | val resources = mutableListOf() 1535 | val resourceUrl = object {}.javaClass.getResource(path) 1536 | 1537 | if (resourceUrl != null) { 1538 | when (resourceUrl.protocol) { 1539 | "file" -> { 1540 | val dir = File(resourceUrl.toURI()) 1541 | dir.walkTopDown().forEach { file -> 1542 | if (file.isFile) { 1543 | val relativePath = file.relativeTo(dir) 1544 | resources.add("$path/${relativePath.path}") 1545 | } 1546 | } 1547 | } 1548 | 1549 | "jar" -> { 1550 | val jarPath = resourceUrl.path.substringBefore("!") 1551 | val jarFile = JarFile(File(jarPath.substringAfter("file:"))) 1552 | val entries = jarFile.entries() 1553 | 1554 | while (entries.hasMoreElements()) { 1555 | val entry = entries.nextElement() 1556 | if (entry.name.startsWith(path.substring(1)) && !entry.isDirectory) { 1557 | resources.add("/${entry.name}") 1558 | } 1559 | } 1560 | jarFile.close() 1561 | } 1562 | } 1563 | } 1564 | 1565 | return resources 1566 | } 1567 | 1568 | val resources = listResources("/project/composeApp/src/webMain/resources") 1569 | resources.forEach { resourcePath -> 1570 | val targetPath = resourcePath.removePrefix("/project/composeApp/src/webMain/resources/") 1571 | val targetFile = targetDir.resolve(targetPath) 1572 | copyResource(resourcePath, targetFile) 1573 | 1574 | // Replace placeholders in text files 1575 | if (targetFile.name.endsWith(".html") || targetFile.name.endsWith(".css") || targetFile.name.endsWith(".js")) { 1576 | try { 1577 | val content = targetFile.readText() 1578 | var updatedContent = content.replace("{{app_name}}", "ComposeApp") 1579 | if (content != updatedContent) { 1580 | targetFile.writeText(updatedContent) 1581 | } 1582 | } catch (e: Exception) { 1583 | // Skip binary files 1584 | } 1585 | } 1586 | } 1587 | } 1588 | 1589 | private fun copyIosAppDirectory(workingDir: String, moduleName: String) { 1590 | val iosAppName = "ios${toCamelCase(moduleName)}" // Dynamic iOS app name based on module 1591 | val targetDir = File(workingDir, iosAppName) // iOS app directory name based on module 1592 | 1593 | fun copyResource(resourcePath: String, targetFile: File) { 1594 | val inputStream: InputStream? = object {}.javaClass.getResourceAsStream(resourcePath) 1595 | if (inputStream != null) { 1596 | targetFile.parentFile?.mkdirs() 1597 | inputStream.use { input -> 1598 | targetFile.outputStream().use { output -> 1599 | input.copyTo(output) 1600 | } 1601 | } 1602 | } 1603 | } 1604 | 1605 | fun listResources(path: String): List { 1606 | val resources = mutableListOf() 1607 | val resourceUrl = object {}.javaClass.getResource(path) 1608 | 1609 | if (resourceUrl != null) { 1610 | when (resourceUrl.protocol) { 1611 | "file" -> { 1612 | val dir = File(resourceUrl.toURI()) 1613 | dir.walkTopDown().forEach { file -> 1614 | if (file.isFile) { 1615 | val relativePath = file.relativeTo(dir) 1616 | resources.add("$path/${relativePath.path}") 1617 | } 1618 | } 1619 | } 1620 | 1621 | "jar" -> { 1622 | val jarPath = resourceUrl.path.substringBefore("!") 1623 | val jarFile = JarFile(File(jarPath.substringAfter("file:"))) 1624 | val entries = jarFile.entries() 1625 | 1626 | while (entries.hasMoreElements()) { 1627 | val entry = entries.nextElement() 1628 | if (entry.name.startsWith(path.substring(1)) && !entry.isDirectory) { 1629 | resources.add("/${entry.name}") 1630 | } 1631 | } 1632 | jarFile.close() 1633 | } 1634 | } 1635 | } 1636 | 1637 | return resources 1638 | } 1639 | 1640 | val resources = listResources("/project/iosApp") 1641 | resources.forEach { resourcePath -> 1642 | val targetPath = resourcePath.removePrefix("/project/iosApp/") 1643 | val targetFile = targetDir.resolve(targetPath) 1644 | copyResource(resourcePath, targetFile) 1645 | 1646 | // Replace placeholders in text files 1647 | if (targetFile.name.endsWith(".swift") || targetFile.name.endsWith(".h") || targetFile.name.endsWith(".m") || targetFile.name.endsWith( 1648 | ".pbxproj" 1649 | ) || targetFile.name.endsWith(".xcconfig") 1650 | ) { 1651 | try { 1652 | val content = targetFile.readText() 1653 | var updatedContent = content.replace("{{module_name}}", moduleName) 1654 | updatedContent = updatedContent.replace("{{ios_binary_name}}", toCamelCase(moduleName)) 1655 | updatedContent = updatedContent.replace("{{target_name}}", "${toCamelCase(moduleName)}.app") 1656 | // For target command, use hardcoded defaults since appName/namespace aren't in scope 1657 | updatedContent = updatedContent.replace("{{app_name}}", "My App") 1658 | updatedContent = updatedContent.replace("{{namespace}}", "com.example.app") 1659 | if (content != updatedContent) { 1660 | targetFile.writeText(updatedContent) 1661 | } 1662 | } catch (e: Exception) { 1663 | // Skip binary files 1664 | } 1665 | } 1666 | } 1667 | } 1668 | } 1669 | 1670 | val gradleScript: String 1671 | get() { 1672 | return if (System.getProperty("os.name").lowercase().contains("win")) 1673 | "gradlew.bat" 1674 | else 1675 | "./gradlew" 1676 | } 1677 | 1678 | fun cloneGradleProjectAndPrint( 1679 | targetDir: String, 1680 | dirName: String, 1681 | packageName: String, 1682 | appName: String, 1683 | targets: Set, 1684 | moduleName: String 1685 | ) { 1686 | cloneGradleProject( 1687 | targetDir, 1688 | dirName, 1689 | packageName, 1690 | appName, 1691 | targets, 1692 | moduleName 1693 | ) 1694 | // Log project configuration summary 1695 | infoln { "" } 1696 | infoln { "Project Configuration:" } 1697 | infoln { "\tApp Name: $appName" } 1698 | infoln { "\tPackage: $packageName" } 1699 | infoln { "\tCompose Module: $moduleName" } 1700 | infoln { "\tTargets: ${targets.joinToString(", ")}" } 1701 | infoln { "" } 1702 | 1703 | debugln { "Success! Your new Compose app is ready at ${File(targetDir).resolve(dirName).absolutePath}" } 1704 | debugln { "Start by typing:" } 1705 | infoln { "" } 1706 | infoln { "\tcd $dirName" } 1707 | infoln { "\t$gradleScript run" } 1708 | infoln { "" } 1709 | debugln { "Happy coding!" } 1710 | } 1711 | 1712 | 1713 | fun cloneGradleProject( 1714 | targetDir: String, 1715 | dirName: String, 1716 | packageName: String, 1717 | appName: String, 1718 | targets: Set, 1719 | moduleName: String 1720 | ) { 1721 | val target = File(targetDir).resolve(dirName) 1722 | 1723 | fun copyResource(resourcePath: String, targetFile: File) { 1724 | val inputStream: InputStream? = object {}.javaClass.getResourceAsStream(resourcePath) 1725 | if (inputStream != null) { 1726 | targetFile.parentFile?.mkdirs() 1727 | 1728 | // Handle gradle-wrapper.jarX -> gradle-wrapper.jar rename 1729 | val actualTargetFile = if (targetFile.name.endsWith(".jarX")) { 1730 | File(targetFile.parent, targetFile.nameWithoutExtension + ".jar") 1731 | } else { 1732 | targetFile 1733 | } 1734 | 1735 | inputStream.use { input -> 1736 | actualTargetFile.outputStream().use { output -> 1737 | input.copyTo(output) 1738 | } 1739 | } 1740 | 1741 | // Set executable permissions for scripts 1742 | if (actualTargetFile.name == "gradlew") { 1743 | actualTargetFile.setExecutable(true) 1744 | } 1745 | } else { 1746 | error("Resource not found: $resourcePath") 1747 | debugln { "Resource not found: $resourcePath" } 1748 | } 1749 | } 1750 | 1751 | fun listResources(path: String): List { 1752 | val resources = mutableListOf() 1753 | val resourceUrl = object {}.javaClass.getResource(path) 1754 | 1755 | if (resourceUrl != null) { 1756 | when (resourceUrl.protocol) { 1757 | "file" -> { 1758 | // Development mode - read from filesystem 1759 | val dir = File(resourceUrl.toURI()) 1760 | dir.walkTopDown().forEach { file -> 1761 | if (file.isFile) { // Only include files, not directories 1762 | val relativePath = file.relativeTo(dir) 1763 | resources.add("$path/${relativePath.path}") 1764 | } 1765 | } 1766 | } 1767 | 1768 | "jar" -> { 1769 | // Production mode - read from JAR 1770 | val jarPath = resourceUrl.path.substringBefore("!") 1771 | val jarFile = JarFile(File(jarPath.substringAfter("file:"))) 1772 | val entries = jarFile.entries() 1773 | 1774 | while (entries.hasMoreElements()) { 1775 | val entry = entries.nextElement() 1776 | if (entry.name.startsWith(path.substring(1)) && !entry.isDirectory) { 1777 | resources.add("/${entry.name}") 1778 | } 1779 | } 1780 | jarFile.close() 1781 | } 1782 | } 1783 | } 1784 | 1785 | return resources 1786 | } 1787 | 1788 | val resources = listResources("/project") 1789 | resources.forEach { resourcePath -> 1790 | var targetPath = resourcePath.removePrefix("/project/") 1791 | 1792 | // Skip iOS directory if iOS target is not selected 1793 | val iosAppName = "ios${toCamelCase(moduleName)}" 1794 | if (!targets.contains("ios") && targetPath.startsWith("iosApp/")) { 1795 | return@forEach 1796 | } 1797 | 1798 | // Skip source set directories if corresponding target is not selected 1799 | val isInsideAKotlinSourceSet = targetPath.startsWith("composeApp/src/") 1800 | if (isInsideAKotlinSourceSet) { 1801 | val sourceSetType = targetPath.substringAfter("composeApp/src/").substringBefore("/") 1802 | 1803 | when (sourceSetType) { 1804 | "androidMain" -> if (!targets.contains("android")) return@forEach 1805 | "iosMain" -> if (!targets.contains("ios")) return@forEach 1806 | "jvmMain" -> if (!targets.contains("jvm")) return@forEach 1807 | "jsMain" -> if (!targets.contains("web")) return@forEach 1808 | "wasmJsMain" -> if (!targets.contains("web")) return@forEach 1809 | "webMain" -> if (!targets.contains("web")) return@forEach 1810 | "commonMain" -> Unit 1811 | else -> error("Unknown target: $targetPath") 1812 | } 1813 | } 1814 | 1815 | // Skip webpack.config.d directory if web target is not selected 1816 | if (!targets.contains("web") && targetPath.startsWith("$moduleName/webpack.config.d/")) { 1817 | return@forEach 1818 | } 1819 | 1820 | // Replace org.example.project with the actual namespace in file paths 1821 | targetPath = targetPath.replace("org/example/project", packageName.replace(".", "/")) 1822 | 1823 | // Replace composeApp with the actual module name in file paths 1824 | targetPath = targetPath.replace("composeApp", moduleName) 1825 | 1826 | // Replace only the top-level iosApp directory with the dynamic iOS app name 1827 | if (targetPath.startsWith("iosApp/")) { 1828 | val newPath = iosAppName + "/" + targetPath.removePrefix("iosApp/") 1829 | // .replace("iosApp/", iosAppName + "/") 1830 | targetPath = newPath 1831 | } 1832 | 1833 | val targetFile = target.resolve(targetPath) 1834 | copyResource(resourcePath, targetFile) 1835 | } 1836 | 1837 | // Replace placeholders in text files only (skip binary files) 1838 | target.walkTopDown().forEach { file -> 1839 | if (file.isFile) { 1840 | // Skip binary files and known non-text files 1841 | if (file.name.endsWith(".jar") || 1842 | file.name.endsWith(".png") || 1843 | file.name.endsWith(".jpg") || 1844 | file.name.endsWith(".jpeg") || 1845 | file.name.endsWith(".ico") || 1846 | file.name.endsWith(".icns") || 1847 | file.name.endsWith(".class") 1848 | ) { 1849 | return@forEach 1850 | } 1851 | 1852 | try { 1853 | val content = file.readText() 1854 | var updatedContent = content.replace("{{app_name}}", appName) 1855 | 1856 | val androidVersions = if (targets.contains("android")) """# Android 1857 | agp = "8.11.2" 1858 | android-compileSdk = "36" 1859 | android-minSdk = "24" 1860 | android-targetSdk = "36" 1861 | activityCompose = "1.11.0" 1862 | 1863 | """ else "" 1864 | 1865 | val androidLibraries = 1866 | if (targets.contains("android")) """androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } 1867 | 1868 | """ else "" 1869 | 1870 | val androidPlugins = 1871 | if (targets.contains("android")) """android-application = { id = "com.android.application", version.ref = "agp" }""" else "" 1872 | 1873 | val androidPlugin = 1874 | if (targets.contains("android")) """ alias(libs.plugins.android.application) apply false 1875 | """ else "" 1876 | 1877 | val androidProperties = if (targets.contains("android")) """#Android 1878 | android.nonTransitiveRClass=true 1879 | android.useAndroidX=true 1880 | """ else "" 1881 | 1882 | // Build imports block 1883 | val imports = mutableListOf() 1884 | if (targets.contains("jvm")) { 1885 | imports.add("import org.jetbrains.compose.desktop.application.dsl.TargetFormat") 1886 | } 1887 | if (targets.contains("web")) { 1888 | imports.add("import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl") 1889 | imports.add("import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig") 1890 | } 1891 | if (targets.contains("android")) { 1892 | imports.add("import org.jetbrains.kotlin.gradle.dsl.JvmTarget") 1893 | } 1894 | val importsBlock = if (imports.isNotEmpty()) imports.joinToString("\n") + "\n" else "" 1895 | 1896 | // Build plugins block 1897 | val plugins = mutableListOf() 1898 | plugins.add(" alias(libs.plugins.jetbrains.kotlin.multiplatform)") 1899 | plugins.add(" alias(libs.plugins.jetbrains.compose)") 1900 | // Only add compose compiler if kotlin compose plugin is not already present 1901 | if (!content.contains("libs.plugins.kotlin.compose")) { 1902 | plugins.add(" alias(libs.plugins.jetbrains.compose.compiler)") 1903 | } 1904 | plugins.add(" alias(libs.plugins.jetbrains.compose.hotreload)") 1905 | if (targets.contains("android")) { 1906 | plugins.add(" alias(libs.plugins.android.application)") 1907 | } 1908 | val pluginsBlock = "plugins {\n" + plugins.joinToString("\n") + "\n}" 1909 | 1910 | // Build kotlin targets block 1911 | val kotlinTargets = mutableListOf() 1912 | if (targets.contains("android")) { 1913 | kotlinTargets.add( 1914 | """ androidTarget { 1915 | compilerOptions { 1916 | jvmTarget.set(JvmTarget.JVM_11) 1917 | } 1918 | }""" 1919 | ) 1920 | } 1921 | if (targets.contains("ios")) { 1922 | val baseName = toCamelCase(moduleName) 1923 | kotlinTargets.add( 1924 | """ listOf( 1925 | iosArm64(), 1926 | iosSimulatorArm64() 1927 | ).forEach { iosTarget -> 1928 | iosTarget.binaries.framework { 1929 | baseName = "$baseName" 1930 | isStatic = true 1931 | } 1932 | }""" 1933 | ) 1934 | } 1935 | if (targets.contains("jvm")) { 1936 | kotlinTargets.add(" jvm()") 1937 | } 1938 | if (targets.contains("web")) { 1939 | kotlinTargets.add( 1940 | """ js { 1941 | browser { 1942 | val rootDirPath = project.rootDir.path 1943 | val projectDirPath = project.projectDir.path 1944 | commonWebpackConfig { 1945 | outputFileName = "composeApp.js" 1946 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { 1947 | static = (static ?: mutableListOf()).apply { 1948 | add(rootDirPath) 1949 | add(projectDirPath) 1950 | } 1951 | } 1952 | } 1953 | } 1954 | binaries.executable() 1955 | }""" 1956 | ) 1957 | kotlinTargets.add( 1958 | """ @OptIn(ExperimentalWasmDsl::class) 1959 | wasmJs { 1960 | browser { 1961 | val rootDirPath = project.rootDir.path 1962 | val projectDirPath = project.projectDir.path 1963 | commonWebpackConfig { 1964 | outputFileName = "composeApp.js" 1965 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { 1966 | static = (static ?: mutableListOf()).apply { 1967 | // Serve sources to debug inside browser 1968 | add(rootDirPath) 1969 | add(projectDirPath) 1970 | } 1971 | } 1972 | } 1973 | } 1974 | binaries.executable() 1975 | }""" 1976 | ) 1977 | } 1978 | val kotlinTargetsBlock = 1979 | if (kotlinTargets.isNotEmpty()) kotlinTargets.joinToString("\n\n") + "\n" else "" 1980 | 1981 | // Build sourcesets block 1982 | val sourcesets = mutableListOf() 1983 | sourcesets.add( 1984 | """ sourceSets { 1985 | commonMain.dependencies { 1986 | implementation(compose.components.uiToolingPreview) 1987 | implementation(compose.material3) 1988 | }""" 1989 | ) 1990 | 1991 | if (targets.contains("jvm")) { 1992 | sourcesets.add( 1993 | """ jvmMain.dependencies { 1994 | implementation(compose.desktop.currentOs) 1995 | }""" 1996 | ) 1997 | } 1998 | if (targets.contains("android")) { 1999 | sourcesets.add( 2000 | """ androidMain.dependencies { 2001 | implementation(compose.preview) 2002 | implementation(libs.androidx.activity.compose) 2003 | }""" 2004 | ) 2005 | } 2006 | sourcesets.add(" }") 2007 | val sourcesetsBlock = sourcesets.joinToString("\n") 2008 | 2009 | // Build configuration blocks 2010 | val configurations = mutableListOf() 2011 | if (targets.contains("android")) { 2012 | configurations.add( 2013 | """android { 2014 | namespace = "{{namespace}}" 2015 | compileSdk = libs.versions.android.compileSdk.get().toInt() 2016 | 2017 | defaultConfig { 2018 | applicationId = "{{namespace}}" 2019 | minSdk = libs.versions.android.minSdk.get().toInt() 2020 | targetSdk = libs.versions.android.targetSdk.get().toInt() 2021 | versionCode = 1 2022 | versionName = "1.0" 2023 | } 2024 | packaging { 2025 | resources { 2026 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 2027 | } 2028 | } 2029 | buildTypes { 2030 | getByName("release") { 2031 | isMinifyEnabled = false 2032 | } 2033 | } 2034 | compileOptions { 2035 | sourceCompatibility = JavaVersion.VERSION_11 2036 | targetCompatibility = JavaVersion.VERSION_11 2037 | } 2038 | }""" 2039 | ) 2040 | } 2041 | if (targets.contains("jvm")) { 2042 | configurations.add( 2043 | """compose.desktop { 2044 | application { 2045 | mainClass = "{{namespace}}.MainKt" 2046 | 2047 | nativeDistributions { 2048 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 2049 | packageName = "{{namespace}}" 2050 | packageVersion = "1.0.0" 2051 | } 2052 | } 2053 | }""" 2054 | ) 2055 | } 2056 | val configurationBlocksBlock = 2057 | if (configurations.isNotEmpty()) configurations.joinToString("\n\n") else "" 2058 | 2059 | val composeDesktop = if (targets.contains("jvm")) """compose.desktop { 2060 | application { 2061 | mainClass = "{{namespace}}.MainDesktopKt" 2062 | 2063 | nativeDistributions { 2064 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 2065 | packageName = "{{namespace}}" 2066 | packageVersion = "1.0.0" 2067 | } 2068 | } 2069 | }""" else "" 2070 | 2071 | val androidMainDependencies = if (targets.contains("android")) """ androidMain.dependencies { 2072 | implementation(compose.preview) 2073 | implementation(libs.androidx.activity.compose) 2074 | }""" else "" 2075 | 2076 | val androidBlock = if (targets.contains("android")) """android { 2077 | namespace = "{{namespace}}" 2078 | compileSdk = libs.versions.android.compileSdk.get().toInt() 2079 | 2080 | defaultConfig { 2081 | applicationId = "{{namespace}}" 2082 | minSdk = libs.versions.android.minSdk.get().toInt() 2083 | targetSdk = libs.versions.android.targetSdk.get().toInt() 2084 | versionCode = 1 2085 | versionName = "1.0" 2086 | } 2087 | packaging { 2088 | resources { 2089 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 2090 | } 2091 | } 2092 | buildTypes { 2093 | getByName("release") { 2094 | isMinifyEnabled = false 2095 | } 2096 | } 2097 | compileOptions { 2098 | sourceCompatibility = JavaVersion.VERSION_11 2099 | targetCompatibility = JavaVersion.VERSION_11 2100 | } 2101 | } 2102 | 2103 | """ else "" 2104 | 2105 | updatedContent = updatedContent.replace("{{android_versions}}", androidVersions) 2106 | updatedContent = updatedContent.replace("{{android_libraries}}", androidLibraries) 2107 | updatedContent = updatedContent.replace("{{android_plugins}}", androidPlugins) 2108 | updatedContent = updatedContent.replace("{{android_plugin}}", androidPlugin) 2109 | updatedContent = updatedContent.replace("{{android_properties}}", androidProperties) 2110 | 2111 | // Replace composeApp build.gradle.kts blocks 2112 | updatedContent = updatedContent.replace("{{imports}}", importsBlock) 2113 | updatedContent = updatedContent.replace("{{plugins}}", pluginsBlock) 2114 | updatedContent = updatedContent.replace("{{kotlin_targets}}", kotlinTargetsBlock) 2115 | updatedContent = updatedContent.replace("{{sourcesets}}", sourcesetsBlock) 2116 | updatedContent = updatedContent.replace("{{configuration_blocks}}", configurationBlocksBlock) 2117 | 2118 | // Replace remaining placeholders after blocks are built 2119 | updatedContent = updatedContent.replace("{{namespace}}", packageName) 2120 | updatedContent = updatedContent.replace("{{module_name}}", moduleName) 2121 | updatedContent = updatedContent.replace("{{app_name}}", appName) 2122 | updatedContent = updatedContent.replace("{{ios_binary_name}}", toCamelCase(moduleName)) 2123 | updatedContent = updatedContent.replace("{{target_name}}", toCamelCase(moduleName) + ".app") 2124 | if (content != updatedContent) { 2125 | file.writeText(updatedContent.trim() + "\n") 2126 | } 2127 | } catch (e: Exception) { 2128 | // If we can't read as text, skip this file 2129 | debugln { "Skipping binary file: ${file.name}" } 2130 | } 2131 | } 2132 | } 2133 | 2134 | // Link iOS project for IDE if iOS target was included 2135 | if (targets.contains("ios")) { 2136 | try { 2137 | debugln { "Preparing iOS target..." } 2138 | val process = ProcessBuilder(gradleScript, "compileIosMainKotlinMetadata", "--quiet") 2139 | .directory(target) 2140 | .inheritIO() 2141 | .start() 2142 | val exitCode = process.waitFor() 2143 | if (exitCode == 0) { 2144 | debugln { "iOS target is now ready to run from the IDE" } 2145 | } else { 2146 | warnln { "Warning: Failed to link iOS project for IDE. You may need to run '$gradleScript compileIosMainKotlinMetadata' manually." } 2147 | } 2148 | } catch (e: Exception) { 2149 | warnln { "Warning: Failed to link iOS project for IDE: ${e.message}" } 2150 | } 2151 | } 2152 | 2153 | } 2154 | 2155 | fun updateRootBuildFile( 2156 | targetDir: String, 2157 | targets: Set 2158 | ) { 2159 | val buildFile = File(targetDir, "build.gradle.kts") 2160 | if (!buildFile.exists()) { 2161 | warnln { "build.gradle.kts not found in $targetDir" } 2162 | return 2163 | } 2164 | 2165 | var content = buildFile.readText() 2166 | var modified = false 2167 | 2168 | // Find plugins block or create one 2169 | val lines = content.lines().toMutableList() 2170 | val pluginsBlockIndex = lines.indexOfFirst { it.trim().startsWith("plugins {") } 2171 | 2172 | if (pluginsBlockIndex >= 0) { 2173 | // Find end of plugins block 2174 | var pluginsEndIndex = pluginsBlockIndex + 1 2175 | var depth = 1 2176 | while (pluginsEndIndex < lines.size && depth > 0) { 2177 | val line = lines[pluginsEndIndex].trim() 2178 | if (line.contains("{")) depth++ 2179 | if (line.contains("}")) depth-- 2180 | pluginsEndIndex++ 2181 | } 2182 | 2183 | // Extract plugins content for checking 2184 | val pluginsContent = lines.subList(pluginsBlockIndex, pluginsEndIndex).joinToString("\n") 2185 | val requiredPlugins = mutableListOf() 2186 | 2187 | // Check for exact plugin references, not partial matches 2188 | if (!pluginsContent.contains("libs.plugins.jetbrains.kotlin.multiplatform")) { 2189 | requiredPlugins.add(" alias(libs.plugins.jetbrains.kotlin.multiplatform) apply false") 2190 | } 2191 | if (!pluginsContent.contains("libs.plugins.jetbrains.compose") && !pluginsContent.contains("libs.plugins.kotlin.compose")) { 2192 | requiredPlugins.add(" alias(libs.plugins.jetbrains.compose) apply false") 2193 | } 2194 | if (!pluginsContent.contains("libs.plugins.jetbrains.compose.compiler")) { 2195 | // Only add compose compiler at root level if kotlin compose plugin is not already present 2196 | if (!pluginsContent.contains("libs.plugins.kotlin.compose")) { 2197 | requiredPlugins.add(" alias(libs.plugins.jetbrains.compose.compiler) apply false") 2198 | } 2199 | } 2200 | if (!pluginsContent.contains("libs.plugins.jetbrains.compose.hotreload")) { 2201 | requiredPlugins.add(" alias(libs.plugins.jetbrains.compose.hotreload) apply false") 2202 | } 2203 | if (targets.contains("android") && !pluginsContent.contains("libs.plugins.android.application")) { 2204 | requiredPlugins.add(" alias(libs.plugins.android.application) apply false") 2205 | } 2206 | 2207 | if (requiredPlugins.isNotEmpty()) { 2208 | // Add missing plugins before closing brace 2209 | requiredPlugins.reversed().forEach { plugin -> 2210 | lines.add(pluginsEndIndex - 1, plugin) 2211 | } 2212 | modified = true 2213 | } 2214 | } else { 2215 | // Create plugins block at the beginning 2216 | val requiredPlugins = mutableListOf() 2217 | requiredPlugins.add("plugins {") 2218 | requiredPlugins.add(" alias(libs.plugins.jetbrains.kotlin.multiplatform) apply false") 2219 | requiredPlugins.add(" alias(libs.plugins.jetbrains.compose) apply false") 2220 | requiredPlugins.add(" alias(libs.plugins.jetbrains.compose.compiler) apply false") 2221 | requiredPlugins.add(" alias(libs.plugins.jetbrains.compose.hotreload) apply false") 2222 | if (targets.contains("android")) { 2223 | requiredPlugins.add(" alias(libs.plugins.android.application) apply false") 2224 | } 2225 | requiredPlugins.add("}") 2226 | 2227 | // Add at the beginning of file 2228 | requiredPlugins.reversed().forEach { line -> 2229 | lines.add(0, line) 2230 | } 2231 | modified = true 2232 | } 2233 | 2234 | if (modified) { 2235 | buildFile.writeText(lines.joinToString("\n")) 2236 | } 2237 | } 2238 | 2239 | fun updateVersionCatalog( 2240 | targetDir: String, 2241 | targets: Set 2242 | ) { 2243 | val versionsFile = File(targetDir, "gradle/libs.versions.toml") 2244 | if (!versionsFile.exists()) { 2245 | warnln { "libs.versions.toml not found in $targetDir/gradle/" } 2246 | return 2247 | } 2248 | 2249 | var content = versionsFile.readText() 2250 | var modified = false 2251 | 2252 | // Parse existing sections 2253 | val versionsSection = extractSection(content, "versions") 2254 | val librariesSection = extractSection(content, "libraries") 2255 | val pluginsSection = extractSection(content, "plugins") 2256 | 2257 | // Add required versions if not present 2258 | val newVersions = mutableListOf() 2259 | if (!hasVersionVariable(versionsSection, "kotlin")) { 2260 | newVersions.add("kotlin = \"2.2.20\"") 2261 | } 2262 | if (!hasVersionVariable(versionsSection, "compose")) { 2263 | newVersions.add("compose = \"1.9.1\"") 2264 | } 2265 | if (!hasVersionVariable(versionsSection, "composeHotReload")) { 2266 | newVersions.add("composeHotReload = \"1.0.0\"") 2267 | } 2268 | 2269 | // Add Android versions if android target is selected 2270 | if (targets.contains("android")) { 2271 | if (!hasVersionVariable(versionsSection, "agp")) newVersions.add("agp = \"8.11.2\"") 2272 | if (!hasVersionVariable(versionsSection, "android-compileSdk")) newVersions.add("android-compileSdk = \"36\"") 2273 | if (!hasVersionVariable(versionsSection, "android-minSdk")) newVersions.add("android-minSdk = \"24\"") 2274 | if (!hasVersionVariable(versionsSection, "android-targetSdk")) newVersions.add("android-targetSdk = \"36\"") 2275 | if (!hasVersionVariable(versionsSection, "activityCompose")) newVersions.add("activityCompose = \"1.11.0\"") 2276 | } 2277 | 2278 | // Add required libraries if not present 2279 | val newLibraries = mutableListOf() 2280 | if (targets.contains("android") && !hasLibraryVariable(librariesSection, "androidx-activity-compose")) { 2281 | newLibraries.add("androidx-activity-compose = { group = \"androidx.activity\", name = \"activity-compose\", version.ref = \"activityCompose\" }") 2282 | } 2283 | 2284 | // Add required plugins if not present 2285 | val newPlugins = mutableListOf() 2286 | if (!hasPluginVariable(pluginsSection, "jetbrains-kotlin-multiplatform")) { 2287 | newPlugins.add("jetbrains-kotlin-multiplatform = { id = \"org.jetbrains.kotlin.multiplatform\", version.ref = \"kotlin\" }") 2288 | } 2289 | if (!hasPluginVariable(pluginsSection, "jetbrains-compose")) { 2290 | newPlugins.add("jetbrains-compose = { id = \"org.jetbrains.compose\", version.ref = \"compose\" }") 2291 | } 2292 | if (!hasPluginVariable(pluginsSection, "jetbrains-compose-compiler")) { 2293 | newPlugins.add("jetbrains-compose-compiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }") 2294 | } 2295 | if (!hasPluginVariable(pluginsSection, "jetbrains-compose-hotreload")) { 2296 | newPlugins.add("jetbrains-compose-hotreload = { id = \"org.jetbrains.compose.hot-reload\", version.ref = \"composeHotReload\" }") 2297 | } 2298 | if (targets.contains("android") && !hasPluginVariable(pluginsSection, "android-application")) { 2299 | newPlugins.add("android-application = { id = \"com.android.application\", version.ref = \"agp\" }") 2300 | } 2301 | 2302 | // Build updated content 2303 | if (newVersions.isNotEmpty() || newLibraries.isNotEmpty() || newPlugins.isNotEmpty()) { 2304 | modified = true 2305 | 2306 | // Update versions section 2307 | if (newVersions.isNotEmpty()) { 2308 | content = updateSection(content, "versions", newVersions) 2309 | } 2310 | 2311 | // Update libraries section 2312 | if (newLibraries.isNotEmpty()) { 2313 | content = updateSection(content, "libraries", newLibraries) 2314 | } 2315 | 2316 | // Update plugins section 2317 | if (newPlugins.isNotEmpty()) { 2318 | content = updateSection(content, "plugins", newPlugins) 2319 | } 2320 | } 2321 | 2322 | if (modified) { 2323 | versionsFile.writeText(content) 2324 | } 2325 | } 2326 | 2327 | private fun extractSection(content: String, sectionName: String): String { 2328 | val startPattern = Regex("""\[$sectionName\]""") 2329 | val startMatch = startPattern.find(content) 2330 | if (startMatch == null) return "" 2331 | 2332 | val startIndex = startMatch.range.last + 1 2333 | val nextSectionPattern = Regex("""\[[^\]]+\]""") 2334 | val nextMatch = nextSectionPattern.find(content, startIndex) 2335 | 2336 | val endIndex = if (nextMatch != null) nextMatch.range.first else content.length 2337 | return content.substring(startIndex, endIndex) 2338 | } 2339 | 2340 | private fun hasVersionVariable(sectionContent: String, variableName: String): Boolean { 2341 | // Check for exact version variable match: variableName = "version" 2342 | val pattern = Regex("""^\s*$variableName\s*=""", RegexOption.MULTILINE) 2343 | return pattern.containsMatchIn(sectionContent) 2344 | } 2345 | 2346 | private fun hasLibraryVariable(sectionContent: String, variableName: String): Boolean { 2347 | // Check for exact library variable match: variableName = { ... } 2348 | val pattern = Regex("""^\s*$variableName\s*=""", RegexOption.MULTILINE) 2349 | return pattern.containsMatchIn(sectionContent) 2350 | } 2351 | 2352 | private fun hasPluginVariable(sectionContent: String, variableName: String): Boolean { 2353 | // Check for exact plugin variable match: variableName = { ... } 2354 | val pattern = Regex("""^\s*$variableName\s*=""", RegexOption.MULTILINE) 2355 | return pattern.containsMatchIn(sectionContent) 2356 | } 2357 | 2358 | private fun updateSection(content: String, sectionName: String, newEntries: List): String { 2359 | val lines = content.lines().toMutableList() 2360 | val sectionIndex = lines.indexOfFirst { it.trim() == "[$sectionName]" } 2361 | 2362 | if (sectionIndex >= 0) { 2363 | // Add new entries after section header 2364 | newEntries.reversed().forEach { entry -> 2365 | lines.add(sectionIndex + 1, entry) 2366 | } 2367 | } else { 2368 | // Create new section at end 2369 | lines.add("") 2370 | lines.add("[$sectionName]") 2371 | newEntries.forEach { entry -> 2372 | lines.add(entry) 2373 | } 2374 | } 2375 | 2376 | return lines.joinToString("\n") 2377 | } 2378 | 2379 | fun addModuleToSettings( 2380 | targetDir: String, 2381 | moduleName: String 2382 | ) { 2383 | val settingsFile = File(targetDir, "settings.gradle.kts") 2384 | if (!settingsFile.exists()) { 2385 | warnln { "settings.gradle.kts not found in $targetDir" } 2386 | return 2387 | } 2388 | 2389 | val content = settingsFile.readText() 2390 | val includePattern = Regex("""include\s*\(\s*["']([^"']+)["']\s*\)""") 2391 | val existingModules = includePattern.findAll(content).map { it.groupValues[1] }.toSet() 2392 | 2393 | if (existingModules.contains(":$moduleName")) { 2394 | warnln { "Module ':$moduleName' is already included in settings.gradle.kts" } 2395 | return 2396 | } 2397 | 2398 | val lines = content.lines().toMutableList() 2399 | 2400 | // Add new include statement at the end 2401 | lines.add("") 2402 | lines.add("include(\":$moduleName\")") 2403 | 2404 | settingsFile.writeText(lines.joinToString("\n")) 2405 | } 2406 | 2407 | fun createModuleOnly( 2408 | targetDir: String, 2409 | moduleName: String, 2410 | packageName: String, 2411 | appName: String, 2412 | targets: Set 2413 | ) { 2414 | val moduleDir = File(targetDir, moduleName) 2415 | 2416 | fun copyResource(resourcePath: String, targetFile: File) { 2417 | val inputStream: InputStream? = object {}.javaClass.getResourceAsStream(resourcePath) 2418 | if (inputStream != null) { 2419 | targetFile.parentFile?.mkdirs() 2420 | inputStream.use { input -> 2421 | targetFile.outputStream().use { output -> 2422 | input.copyTo(output) 2423 | } 2424 | } 2425 | } else { 2426 | error("Resource not found: $resourcePath") 2427 | debugln { "Resource not found: $resourcePath" } 2428 | } 2429 | } 2430 | 2431 | fun listResources(path: String): List { 2432 | val resources = mutableListOf() 2433 | val resourceUrl = object {}.javaClass.getResource(path) 2434 | 2435 | if (resourceUrl != null) { 2436 | when (resourceUrl.protocol) { 2437 | "file" -> { 2438 | val dir = File(resourceUrl.toURI()) 2439 | dir.walkTopDown().forEach { file -> 2440 | if (file.isFile) { 2441 | val relativePath = file.relativeTo(dir) 2442 | resources.add("$path/${relativePath.path}") 2443 | } 2444 | } 2445 | } 2446 | 2447 | "jar" -> { 2448 | val jarPath = resourceUrl.path.substringBefore("!") 2449 | val jarFile = JarFile(File(jarPath.substringAfter("file:"))) 2450 | val entries = jarFile.entries() 2451 | 2452 | while (entries.hasMoreElements()) { 2453 | val entry = entries.nextElement() 2454 | if (entry.name.startsWith(path.substring(1)) && !entry.isDirectory) { 2455 | resources.add("/${entry.name}") 2456 | } 2457 | } 2458 | jarFile.close() 2459 | } 2460 | } 2461 | } 2462 | 2463 | return resources 2464 | } 2465 | 2466 | // Copy only the module contents (not the entire project) 2467 | val resources = listResources("/project/composeApp") 2468 | resources.forEach { resourcePath -> 2469 | var targetPath = resourcePath.removePrefix("/project/composeApp/") 2470 | 2471 | // Skip source set directories if corresponding target is not selected 2472 | val isInsideAKotlinSourceSet = targetPath.startsWith("src/") 2473 | if (isInsideAKotlinSourceSet) { 2474 | val sourceSetType = targetPath.substringAfter("src/").substringBefore("/") 2475 | when (sourceSetType) { 2476 | "androidMain" -> if (!targets.contains("android")) return@forEach 2477 | "iosMain" -> if (!targets.contains("ios")) return@forEach 2478 | "jvmMain" -> if (!targets.contains("jvm")) return@forEach 2479 | "jsMain" -> if (!targets.contains("web")) return@forEach 2480 | "wasmJsMain" -> if (!targets.contains("web")) return@forEach 2481 | "webMain" -> if (!targets.contains("web")) return@forEach 2482 | "commonMain" -> Unit 2483 | else -> error("Unknown target: $targetPath") 2484 | } 2485 | } 2486 | 2487 | // Skip webpack.config.d directory if web target is not selected 2488 | if (!targets.contains("web") && targetPath.startsWith("webpack.config.d/")) { 2489 | return@forEach 2490 | } 2491 | 2492 | // Replace org.example.project with the actual namespace in file paths 2493 | targetPath = targetPath.replace("org/example/project", packageName.replace(".", "/")) 2494 | 2495 | val targetFile = moduleDir.resolve(targetPath) 2496 | copyResource(resourcePath, targetFile) 2497 | } 2498 | 2499 | // Replace placeholders in text files only (skip binary files) 2500 | moduleDir.walkTopDown().forEach { file -> 2501 | if (file.isFile) { 2502 | // Skip binary files and known non-text files 2503 | if (file.name.endsWith(".jar") || 2504 | file.name.endsWith(".png") || 2505 | file.name.endsWith(".jpg") || 2506 | file.name.endsWith(".jpeg") || 2507 | file.name.endsWith(".ico") || 2508 | file.name.endsWith(".icns") || 2509 | file.name.endsWith(".class") 2510 | ) { 2511 | return@forEach 2512 | } 2513 | 2514 | try { 2515 | val content = file.readText() 2516 | var updatedContent = content.replace("{{app_name}}", appName) 2517 | 2518 | // Build imports block 2519 | val imports = mutableListOf() 2520 | if (targets.contains("jvm")) { 2521 | imports.add("import org.jetbrains.compose.desktop.application.dsl.TargetFormat") 2522 | } 2523 | if (targets.contains("web")) { 2524 | imports.add("import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl") 2525 | imports.add("import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig") 2526 | } 2527 | if (targets.contains("android")) { 2528 | imports.add("import org.jetbrains.kotlin.gradle.dsl.JvmTarget") 2529 | } 2530 | val importsBlock = if (imports.isNotEmpty()) imports.joinToString("\n") + "\n" else "" 2531 | 2532 | // Build plugins block 2533 | val plugins = mutableListOf() 2534 | plugins.add(" alias(libs.plugins.jetbrains.kotlin.multiplatform)") 2535 | plugins.add(" alias(libs.plugins.jetbrains.compose)") 2536 | // Only add compose compiler if kotlin compose plugin is not already present 2537 | if (!content.contains("libs.plugins.kotlin.compose")) { 2538 | plugins.add(" alias(libs.plugins.jetbrains.compose.compiler)") 2539 | } 2540 | plugins.add(" alias(libs.plugins.jetbrains.compose.hotreload)") 2541 | if (targets.contains("android")) { 2542 | plugins.add(" alias(libs.plugins.android.application)") 2543 | } 2544 | val pluginsBlock = "plugins {\n" + plugins.joinToString("\n") + "\n}" 2545 | 2546 | // Build kotlin targets block 2547 | val kotlinTargets = mutableListOf() 2548 | if (targets.contains("android")) { 2549 | kotlinTargets.add( 2550 | """ androidTarget { 2551 | compilerOptions { 2552 | jvmTarget.set(JvmTarget.JVM_11) 2553 | } 2554 | }""" 2555 | ) 2556 | } 2557 | if (targets.contains("ios")) { 2558 | val baseName = toCamelCase(moduleName) 2559 | kotlinTargets.add( 2560 | """ listOf( 2561 | iosArm64(), 2562 | iosSimulatorArm64() 2563 | ).forEach { iosTarget -> 2564 | iosTarget.binaries.framework { 2565 | baseName = "$baseName" 2566 | isStatic = true 2567 | } 2568 | }""" 2569 | ) 2570 | } 2571 | if (targets.contains("jvm")) { 2572 | kotlinTargets.add(" jvm()") 2573 | } 2574 | if (targets.contains("web")) { 2575 | kotlinTargets.add( 2576 | """ js { 2577 | browser { 2578 | val rootDirPath = project.rootDir.path 2579 | val projectDirPath = project.projectDir.path 2580 | commonWebpackConfig { 2581 | outputFileName = "composeApp.js" 2582 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { 2583 | static = (static ?: mutableListOf()).apply { 2584 | add(rootDirPath) 2585 | add(projectDirPath) 2586 | } 2587 | } 2588 | } 2589 | } 2590 | binaries.executable() 2591 | }""" 2592 | ) 2593 | kotlinTargets.add( 2594 | """ @OptIn(ExperimentalWasmDsl::class) 2595 | wasmJs { 2596 | browser { 2597 | val rootDirPath = project.rootDir.path 2598 | val projectDirPath = project.projectDir.path 2599 | commonWebpackConfig { 2600 | outputFileName = "composeApp.js" 2601 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { 2602 | static = (static ?: mutableListOf()).apply { 2603 | // Serve sources to debug inside browser 2604 | add(rootDirPath) 2605 | add(projectDirPath) 2606 | } 2607 | } 2608 | } 2609 | } 2610 | binaries.executable() 2611 | }""" 2612 | ) 2613 | } 2614 | val kotlinTargetsBlock = 2615 | if (kotlinTargets.isNotEmpty()) kotlinTargets.joinToString("\n\n") + "\n" else "" 2616 | 2617 | // Build sourcesets block 2618 | val sourcesets = mutableListOf() 2619 | sourcesets.add( 2620 | """ sourceSets { 2621 | commonMain.dependencies { 2622 | implementation(compose.components.uiToolingPreview) 2623 | implementation(compose.material3) 2624 | }""" 2625 | ) 2626 | 2627 | if (targets.contains("jvm")) { 2628 | sourcesets.add( 2629 | """ jvmMain.dependencies { 2630 | implementation(compose.desktop.currentOs) 2631 | }""" 2632 | ) 2633 | } 2634 | if (targets.contains("android")) { 2635 | sourcesets.add( 2636 | """ androidMain.dependencies { 2637 | implementation(compose.preview) 2638 | implementation(libs.androidx.activity.compose) 2639 | }""" 2640 | ) 2641 | } 2642 | sourcesets.add(" }") 2643 | val sourcesetsBlock = sourcesets.joinToString("\n") 2644 | 2645 | // Build configuration blocks 2646 | val configurations = mutableListOf() 2647 | if (targets.contains("android")) { 2648 | configurations.add( 2649 | """android { 2650 | namespace = "{{namespace}}" 2651 | compileSdk = libs.versions.android.compileSdk.get().toInt() 2652 | 2653 | defaultConfig { 2654 | applicationId = "{{namespace}}" 2655 | minSdk = libs.versions.android.minSdk.get().toInt() 2656 | targetSdk = libs.versions.android.targetSdk.get().toInt() 2657 | versionCode = 1 2658 | versionName = "1.0" 2659 | } 2660 | packaging { 2661 | resources { 2662 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 2663 | } 2664 | } 2665 | buildTypes { 2666 | getByName("release") { 2667 | isMinifyEnabled = false 2668 | } 2669 | } 2670 | compileOptions { 2671 | sourceCompatibility = JavaVersion.VERSION_11 2672 | targetCompatibility = JavaVersion.VERSION_11 2673 | } 2674 | }""" 2675 | ) 2676 | } 2677 | if (targets.contains("jvm")) { 2678 | configurations.add( 2679 | """compose.desktop { 2680 | application { 2681 | mainClass = "{{namespace}}.MainKt" 2682 | 2683 | nativeDistributions { 2684 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 2685 | packageName = "{{namespace}}" 2686 | packageVersion = "1.0.0" 2687 | } 2688 | } 2689 | }""" 2690 | ) 2691 | } 2692 | val configurationBlocksBlock = 2693 | if (configurations.isNotEmpty()) configurations.joinToString("\n\n") else "" 2694 | 2695 | // Replace composeApp build.gradle.kts blocks 2696 | updatedContent = updatedContent.replace("{{imports}}", importsBlock) 2697 | updatedContent = updatedContent.replace("{{plugins}}", pluginsBlock) 2698 | updatedContent = updatedContent.replace("{{kotlin_targets}}", kotlinTargetsBlock) 2699 | updatedContent = updatedContent.replace("{{sourcesets}}", sourcesetsBlock) 2700 | updatedContent = updatedContent.replace("{{configuration_blocks}}", configurationBlocksBlock) 2701 | 2702 | // Replace remaining placeholders after blocks are built 2703 | updatedContent = updatedContent.replace("{{namespace}}", packageName) 2704 | updatedContent = updatedContent.replace("{{module_name}}", moduleName) 2705 | updatedContent = updatedContent.replace("{{app_name}}", appName) 2706 | updatedContent = updatedContent.replace("{{ios_binary_name}}", toCamelCase(moduleName)) 2707 | updatedContent = updatedContent.replace("{{target_name}}", "${toCamelCase(moduleName)}.app") 2708 | if (content != updatedContent) { 2709 | file.writeText(updatedContent.trim() + "\n") 2710 | } 2711 | } catch (e: Exception) { 2712 | // If we can't read as text, skip this file 2713 | debugln { "Skipping binary file: ${file.name}" } 2714 | } 2715 | } 2716 | } 2717 | 2718 | // Log module creation summary 2719 | infoln { "" } 2720 | infoln { "Module Configuration:" } 2721 | infoln { "\tApp Name: $appName" } 2722 | infoln { "\tPackage: $packageName" } 2723 | infoln { "\tModule: $moduleName" } 2724 | infoln { "\tTargets: ${targets.joinToString(", ")}" } 2725 | infoln { "" } 2726 | 2727 | debugln { "Success! Your new Compose Multiplatform module is ready at ${moduleDir.absolutePath}" } 2728 | } 2729 | 2730 | private fun createIosAppDirectory( 2731 | targetDir: String, 2732 | moduleName: String 2733 | ) { 2734 | val iosAppName = "ios${toCamelCase(moduleName)}" 2735 | val targetDir = File(targetDir, iosAppName) 2736 | 2737 | fun copyResource(resourcePath: String, targetFile: File) { 2738 | val inputStream: InputStream? = object {}.javaClass.getResourceAsStream(resourcePath) 2739 | if (inputStream != null) { 2740 | targetFile.parentFile?.mkdirs() 2741 | inputStream.use { input -> 2742 | targetFile.outputStream().use { output -> 2743 | input.copyTo(output) 2744 | } 2745 | } 2746 | } 2747 | } 2748 | 2749 | fun listResources(path: String): List { 2750 | val resources = mutableListOf() 2751 | val resourceUrl = object {}.javaClass.getResource(path) 2752 | 2753 | if (resourceUrl != null) { 2754 | when (resourceUrl.protocol) { 2755 | "file" -> { 2756 | val dir = File(resourceUrl.toURI()) 2757 | dir.walkTopDown().forEach { file -> 2758 | if (file.isFile) { 2759 | val relativePath = file.relativeTo(dir) 2760 | resources.add("$path/${relativePath.path}") 2761 | } 2762 | } 2763 | } 2764 | 2765 | "jar" -> { 2766 | val jarPath = resourceUrl.path.substringBefore("!") 2767 | val jarFile = JarFile(File(jarPath.substringAfter("file:"))) 2768 | val entries = jarFile.entries() 2769 | 2770 | while (entries.hasMoreElements()) { 2771 | val entry = entries.nextElement() 2772 | if (entry.name.startsWith(path.substring(1)) && !entry.isDirectory) { 2773 | resources.add("/${entry.name}") 2774 | } 2775 | } 2776 | jarFile.close() 2777 | } 2778 | } 2779 | } 2780 | 2781 | return resources 2782 | } 2783 | 2784 | val resources = listResources("/project/iosApp") 2785 | resources.forEach { resourcePath -> 2786 | val targetPath = resourcePath.removePrefix("/project/iosApp/") 2787 | val targetFile = targetDir.resolve(targetPath) 2788 | copyResource(resourcePath, targetFile) 2789 | 2790 | // Replace placeholders in text files 2791 | if (targetFile.name.endsWith(".swift") || targetFile.name.endsWith(".h") || targetFile.name.endsWith(".m") || 2792 | targetFile.name.endsWith(".pbxproj") || targetFile.name.endsWith(".xcconfig") 2793 | ) { 2794 | try { 2795 | val content = targetFile.readText() 2796 | var updatedContent = content.replace("{{module_name}}", moduleName) 2797 | updatedContent = updatedContent.replace("{{ios_binary_name}}", toCamelCase(moduleName)) 2798 | updatedContent = updatedContent.replace("{{target_name}}", "${toCamelCase(moduleName)}.app") 2799 | // Use defaults for module addition since we don't have app name/namespace in scope 2800 | updatedContent = updatedContent.replace("{{app_name}}", "My App") 2801 | updatedContent = updatedContent.replace("{{namespace}}", "com.example.app") 2802 | if (content != updatedContent) { 2803 | targetFile.writeText(updatedContent) 2804 | } 2805 | } catch (e: Exception) { 2806 | // Skip binary files 2807 | } 2808 | } 2809 | } 2810 | } 2811 | 2812 | private fun getKotlinVersion(projectDir: File): String? { 2813 | try { 2814 | // Try to get Kotlin version from gradle.properties 2815 | val gradleProperties = File(projectDir, "gradle.properties") 2816 | if (gradleProperties.exists()) { 2817 | val content = gradleProperties.readText() 2818 | val kotlinVersionMatch = Regex("kotlin\\.version\\s*=\\s*([^\n\r]+)").find(content) 2819 | if (kotlinVersionMatch != null) { 2820 | return kotlinVersionMatch.groupValues[1].trim() 2821 | } 2822 | } 2823 | 2824 | // Try to get from libs.versions.toml 2825 | val versionsToml = File(projectDir, "gradle/libs.versions.toml") 2826 | if (versionsToml.exists()) { 2827 | val content = versionsToml.readText() 2828 | val kotlinVersionMatch = Regex("kotlin\\s*=\\s*\"?([^\"]+)\"?").find(content) 2829 | if (kotlinVersionMatch != null) { 2830 | return kotlinVersionMatch.groupValues[1].trim() 2831 | } 2832 | } 2833 | 2834 | // Try to run gradle and get the version 2835 | val process = ProcessBuilder("./gradlew", "properties", "-q", "--no-daemon") 2836 | .directory(projectDir) 2837 | .redirectErrorStream(true) 2838 | .start() 2839 | 2840 | val output = process.inputStream.bufferedReader().readText() 2841 | process.waitFor() 2842 | 2843 | val versionMatch = Regex("kotlin\\.version\\s*=\\s*([^\n\r]+)").find(output) 2844 | if (versionMatch != null) { 2845 | return versionMatch.groupValues[1].trim() 2846 | } 2847 | 2848 | } catch (e: Exception) { 2849 | // Failed to get version, return null 2850 | } 2851 | 2852 | return null 2853 | } 2854 | 2855 | private fun isKotlinVersionSupported(version: String): Boolean { 2856 | return try { 2857 | val parts = version.split(".") 2858 | if (parts.size >= 3) { 2859 | val major = parts[0].toInt() 2860 | val minor = parts[1].toInt() 2861 | val patch = parts[2].toInt() 2862 | 2863 | // Check if version is at least 2.2.21 2864 | if (major > 2) return true 2865 | if (major < 2) return false 2866 | if (minor > 2) return true 2867 | if (minor < 2) return false 2868 | if (patch >= 21) return true 2869 | return false 2870 | } else { 2871 | // For versions like "2.2" or "2.2.0", assume they're too old 2872 | false 2873 | } 2874 | } catch (e: Exception) { 2875 | // Failed to parse version, assume it's not supported 2876 | false 2877 | } 2878 | } 2879 | --------------------------------------------------------------------------------