├── .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 |
5 |
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 | 
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 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
--------------------------------------------------------------------------------