diff --git a/.github/compilation-check-source.yml b/.github/compilation-check-source.yml index f804e23fa..0b2c6e82b 100644 --- a/.github/compilation-check-source.yml +++ b/.github/compilation-check-source.yml @@ -290,6 +290,49 @@ jobs: - *publish_test_report - *upload_reports + check-kotlin-2-dynamic-sample: + runs-on: ${{ matrix.os }} + strategy: + matrix: + <<: *runner_matrix + needs: build-library + + steps: + - *checkout + - *setup_jdk + - *setup_gradle + - *cache_konan + - *download_maven + + - name: Sample - kotlin-2-dynamic-sample + run: cd samples/kotlin-2-dynamic-sample && ./local-check.sh + shell: bash + + - *publish_test_report + - *upload_reports + + check-kotlin-2-tests: + runs-on: ${{ matrix.os }} + strategy: + matrix: + <<: *runner_matrix + needs: build-library + + steps: + - *checkout + - *setup_jdk + - *setup_gradle + - uses: browser-actions/setup-chrome@v1 + - *cache_konan + - *download_maven + + - name: Sample - kotlin-2-tests + run: cd samples/kotlin-2-tests && ./local-check.sh + shell: bash + + - *publish_test_report + - *upload_reports + check-cm-resources-sample: runs-on: ${{ matrix.os }} strategy: diff --git a/.github/workflows/compilation-check.yml b/.github/workflows/compilation-check.yml index bfa220a78..0332c9275 100644 --- a/.github/workflows/compilation-check.yml +++ b/.github/workflows/compilation-check.yml @@ -498,6 +498,99 @@ jobs: with: name: code-coverage-report-${{ github.job }}-${{ matrix.os }} path: "**/build/reports/**/*" + check-kotlin-2-dynamic-sample: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - macOS-latest + - windows-latest + - ubuntu-latest + needs: build-library + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: zulu + - name: Setup Gradle + uses: gradle/gradle-build-action@v3 + with: + cache-read-only: ${{ github.ref != 'refs/heads/master' && github.ref != + 'refs/heads/develop' }} + - name: Cache .konan + uses: actions/cache@v4 + with: + path: ~/.konan + key: ${{ matrix.os }}-konan-${{ hashFiles('**/*.gradle*', 'gradle/**/*') }} + - name: Download maven artifacts + uses: actions/download-artifact@v4 + with: + name: maven + path: ~/.m2/repository/dev/icerock + - name: Sample - kotlin-2-dynamic-sample + run: cd samples/kotlin-2-dynamic-sample && ./local-check.sh + shell: bash + - name: Publish Test Report + uses: mikepenz/action-junit-report@v4 + if: ${{ always() }} + with: + report_paths: "**/build/test-results/**/TEST-*.xml" + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Archive reports + uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: code-coverage-report-${{ github.job }}-${{ matrix.os }} + path: "**/build/reports/**/*" + check-kotlin-2-tests: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - macOS-latest + - windows-latest + - ubuntu-latest + needs: build-library + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: zulu + - name: Setup Gradle + uses: gradle/gradle-build-action@v3 + with: + cache-read-only: ${{ github.ref != 'refs/heads/master' && github.ref != + 'refs/heads/develop' }} + - uses: browser-actions/setup-chrome@v1 + - name: Cache .konan + uses: actions/cache@v4 + with: + path: ~/.konan + key: ${{ matrix.os }}-konan-${{ hashFiles('**/*.gradle*', 'gradle/**/*') }} + - name: Download maven artifacts + uses: actions/download-artifact@v4 + with: + name: maven + path: ~/.m2/repository/dev/icerock + - name: Sample - kotlin-2-tests + run: cd samples/kotlin-2-tests && ./local-check.sh + shell: bash + - name: Publish Test Report + uses: mikepenz/action-junit-report@v4 + if: ${{ always() }} + with: + report_paths: "**/build/test-results/**/TEST-*.xml" + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Archive reports + uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: code-coverage-report-${{ github.job }}-${{ matrix.os }} + path: "**/build/reports/**/*" check-cm-resources-sample: runs-on: ${{ matrix.os }} strategy: @@ -543,4 +636,4 @@ jobs: if: ${{ always() }} with: name: code-coverage-report-${{ github.job }}-${{ matrix.os }} - path: "**/build/reports/**/*" \ No newline at end of file + path: "**/build/reports/**/*" diff --git a/gradle/moko.versions.toml b/gradle/moko.versions.toml index 5214152c5..9fb4c8c78 100644 --- a/gradle/moko.versions.toml +++ b/gradle/moko.versions.toml @@ -1,5 +1,5 @@ [versions] -resourcesVersion = "0.24.4" +resourcesVersion = "0.24.5" [libraries] resources = { module = "dev.icerock.moko:resources", version.ref = "resourcesVersion" } diff --git a/local-samples-check.sh b/local-samples-check.sh index 4eee84325..332161df9 100755 --- a/local-samples-check.sh +++ b/local-samples-check.sh @@ -14,4 +14,6 @@ set -e (cd samples/compose-resources-gallery && ./local-check.sh) (cd samples/default-hierarchy-gallery-mobile && ./local-check.sh) (cd samples/kotlin-2-sample && ./local-check.sh) +(cd samples/kotlin-2-dynamic-sample && ./local-check.sh) +(cd samples/kotlin-2-tests && ./local-check.sh) (cd samples/cm-resources-sample && ./local-check.sh) diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/actions/apple/CopyResourcesFromKLibsAction.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/actions/apple/CopyResourcesFromKLibsAction.kt index 92cf763af..07d8ebd5d 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/actions/apple/CopyResourcesFromKLibsAction.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/actions/apple/CopyResourcesFromKLibsAction.kt @@ -11,6 +11,7 @@ import org.gradle.api.logging.Logger import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink import org.jetbrains.kotlin.library.KotlinLibraryLayout import org.jetbrains.kotlin.library.impl.KotlinLibraryLayoutImpl +import org.jetbrains.kotlin.library.impl.javaFile import java.io.File internal abstract class CopyResourcesFromKLibsAction : Action { @@ -19,50 +20,69 @@ internal abstract class CopyResourcesFromKLibsAction : Action { linkTask: KotlinNativeLink, outputDir: File ) { - val packedKlibs: List = linkTask.klibs - .filter { it.exists() } - .filter { it.extension == "klib" } - .map { it } - val unpackedKlibs: List = linkTask.klibs - .filter { it.exists() } - // we need only unpacked klibs - .filter { it.name == "manifest" && it.parentFile.name == "default" } - // manifest stored in klib inside directory default - .map { it.parentFile.parentFile } + val logger: Logger = linkTask.logger - (packedKlibs + unpackedKlibs) - .forEach { inputFile -> - linkTask.logger.info("found dependency $inputFile, try to copy resources") - - val layout: KotlinLibraryLayout = getKotlinLibraryLayout(inputFile) - - copyResourcesFromKlib( - logger = linkTask.logger, - layout = layout, - outputDir = outputDir, - ) + linkTask.klibs + .onEach { logger.debug("found klib dependency {}", it) } + .flatMap { getBundlesFromSources(sourceFile = it, logger = logger) } + .forEach { bundle -> + logger.info("copy $bundle to $outputDir") + bundle.copyRecursively(File(outputDir, bundle.name), overwrite = true) } } - private fun copyResourcesFromKlib(logger: Logger, layout: KotlinLibraryLayout, outputDir: File) { - logger.info("copy resources from $layout into $outputDir") + /** + * Search bundles in klib different types. + * + * We know about 3 types of klib in filesystem: + * 1. packed klib - single file with .klib extension + * 2. unpacked klib directory - root directory with klib content (used for local project + * dependencies) + * 3. unpacked klib content - all files inside klib directory (used for current project + * compilation results) + * + * @param sourceFile file from linking task dependencies and sources list + * @param logger gradle logger + * + * @return list of .bundle directories founded in klibs + */ + private fun getBundlesFromSources(sourceFile: File, logger: Logger): List { + val isPackedKlib = sourceFile.isFile && sourceFile.extension == "klib" + val isUnpackedKlib = sourceFile.isDirectory - try { - File(layout.resourcesDir.path).copyRecursively( - target = outputDir, - overwrite = true - ) - } catch (@Suppress("SwallowedException") exc: NoSuchFileException) { - logger.info("resources in $layout not found") - } catch (@Suppress("SwallowedException") exc: java.nio.file.NoSuchFileException) { - logger.info("resources in $layout not found (empty lib)") + return if (isPackedKlib || isUnpackedKlib) { + logger.info("found klib {}", sourceFile) + getBundlesFromKotlinLibrary(sourceFile) + } else if (sourceFile.name == "manifest" && sourceFile.parentFile.name == "default") { + // for unpacked klibs we can see content files instead of klib directory. + // try to check this case + logger.info("found manifest of klib {}", sourceFile) + val unpackedKlibRoot: File = sourceFile.parentFile.parentFile + getBundlesFromKotlinLibrary(unpackedKlibRoot) + } else { + logger.debug("found some file {}", sourceFile) + emptyList() } } + private fun getBundlesFromKotlinLibrary( + klibFile: File + ): List { + val layout: KotlinLibraryLayout = getKotlinLibraryLayout(klibFile) + return layout.resourcesDir.listFilesOrEmpty + .filter { it.isDirectory && it.extension == "bundle" } + .map { it.javaFile() } + } + private fun getKotlinLibraryLayout(file: File): KotlinLibraryLayout { val klibKonan = org.jetbrains.kotlin.konan.file.File(file.path) val klib = KotlinLibraryLayoutImpl(klib = klibKonan, component = "default") + // while klib zipped we can't check resources directory, so we should unpack all klibs :( + // maybe will be better if we will write some state in cache as build result file with + // klib path, hash, resources count. to not extract klibs that we already know that not + // contains any resources. BUT maybe extraction will be faster then hashing for this logic. + // so this improvement should be checked in future return if (klib.isZipped) klib.extractingToTemp else klib } } diff --git a/samples/kotlin-2-dynamic-sample/.gitignore b/samples/kotlin-2-dynamic-sample/.gitignore new file mode 100644 index 000000000..fdbf6bc0d --- /dev/null +++ b/samples/kotlin-2-dynamic-sample/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +.idea +.DS_Store +.kotlin +build +captures +.externalNativeBuild +.cxx +local.properties +xcuserdata \ No newline at end of file diff --git a/samples/kotlin-2-dynamic-sample/README.md b/samples/kotlin-2-dynamic-sample/README.md new file mode 100644 index 000000000..3a32a5129 --- /dev/null +++ b/samples/kotlin-2-dynamic-sample/README.md @@ -0,0 +1,6 @@ +# Sample of mobile app with Kotlin 2.1, multimodule and dynamic framework + +## Build + +1. publish moko-resources to local maven - `./gradlew publishToMavenLocal` in `moko-resources` root +2. build sample (in IDE or by `./gradlew build`) diff --git a/samples/kotlin-2-dynamic-sample/androidApp/build.gradle.kts b/samples/kotlin-2-dynamic-sample/androidApp/build.gradle.kts new file mode 100644 index 000000000..021a2d330 --- /dev/null +++ b/samples/kotlin-2-dynamic-sample/androidApp/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.compose.compiler) +} + +android { + namespace = "app.kotlin2sample.android" + compileSdk = 34 + defaultConfig { + applicationId = "app.kotlin2sample.android" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + } + buildFeatures { + compose = true + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(projects.shared) + implementation(libs.compose.ui) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.compose.material3) + implementation(libs.androidx.activity.compose) + implementation(moko.resourcesCompose) + debugImplementation(libs.compose.ui.tooling) +} \ No newline at end of file diff --git a/samples/kotlin-2-dynamic-sample/androidApp/src/main/AndroidManifest.xml b/samples/kotlin-2-dynamic-sample/androidApp/src/main/AndroidManifest.xml new file mode 100644 index 000000000..afe828631 --- /dev/null +++ b/samples/kotlin-2-dynamic-sample/androidApp/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/kotlin-2-dynamic-sample/androidApp/src/main/java/app/thirtyninth/compose/navigation/kotlin2sample/android/MainActivity.kt b/samples/kotlin-2-dynamic-sample/androidApp/src/main/java/app/thirtyninth/compose/navigation/kotlin2sample/android/MainActivity.kt new file mode 100644 index 000000000..1cfbf627d --- /dev/null +++ b/samples/kotlin-2-dynamic-sample/androidApp/src/main/java/app/thirtyninth/compose/navigation/kotlin2sample/android/MainActivity.kt @@ -0,0 +1,49 @@ +package app.thirtyninth.compose.navigation.kotlin2sample.android + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import app.kotlin2sample.Greeting +import dev.icerock.moko.resources.compose.localized + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MyApplicationTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Column { + GreetingView(Greeting().greet()) + Text(text = Greeting().getMR().localized()) + Text(text = stringResource(id = app.kotlin2sample.R.string.hello_world)) + } + } + } + } + } +} + +@Composable +fun GreetingView(text: String) { + Text(text = text) +} + +@Preview +@Composable +fun DefaultPreview() { + MyApplicationTheme { + GreetingView("Hello, Android!") + } +} diff --git a/samples/kotlin-2-dynamic-sample/androidApp/src/main/java/app/thirtyninth/compose/navigation/kotlin2sample/android/MyApplicationTheme.kt b/samples/kotlin-2-dynamic-sample/androidApp/src/main/java/app/thirtyninth/compose/navigation/kotlin2sample/android/MyApplicationTheme.kt new file mode 100644 index 000000000..b453dcf0b --- /dev/null +++ b/samples/kotlin-2-dynamic-sample/androidApp/src/main/java/app/thirtyninth/compose/navigation/kotlin2sample/android/MyApplicationTheme.kt @@ -0,0 +1,55 @@ +package app.thirtyninth.compose.navigation.kotlin2sample.android + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun MyApplicationTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (darkTheme) { + darkColorScheme( + primary = Color(0xFFBB86FC), + secondary = Color(0xFF03DAC5), + tertiary = Color(0xFF3700B3) + ) + } else { + lightColorScheme( + primary = Color(0xFF6200EE), + secondary = Color(0xFF03DAC5), + tertiary = Color(0xFF3700B3) + ) + } + val typography = Typography( + bodyMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ) + ) + val shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) + ) + + MaterialTheme( + colorScheme = colors, + typography = typography, + shapes = shapes, + content = content + ) +} diff --git a/samples/kotlin-2-dynamic-sample/androidApp/src/main/res/values/styles.xml b/samples/kotlin-2-dynamic-sample/androidApp/src/main/res/values/styles.xml new file mode 100644 index 000000000..6b4fa3d08 --- /dev/null +++ b/samples/kotlin-2-dynamic-sample/androidApp/src/main/res/values/styles.xml @@ -0,0 +1,3 @@ + +