diff --git a/KMP/README.md b/KMP/README.md index b6517f76..ab28411f 100644 --- a/KMP/README.md +++ b/KMP/README.md @@ -150,7 +150,30 @@ duplicate native symbols. ## Build from source Building the KMP project and its iOS targets requires macOS, Xcode command-line -tools, CMake, and JDK 11 or newer: +tools, CMake, and JDK 21. The checked-in Gradle daemon JVM criteria select JDK 21 +for both command-line builds and Android Studio imports; install it before syncing. + +If Android Studio cannot find `cmake` on its PATH, set `cmake.dir` to the CMake +installation directory containing `bin/cmake` in the untracked +`KMP/local.properties`. For example, with Homebrew on Apple Silicon: + +```properties +cmake.dir=/opt/homebrew +``` + +Both native configure and build tasks use this path. When `cmake.dir` is unset, +they use `cmake` from PATH. + +Open the `KMP` directory in Android Studio. The sample is enabled by +`MMKV_INCLUDE_SAMPLE=true` in `gradle.properties`. After syncing, select the +Android App run configuration for `:sample:composeApp` to run the Android sample. +Use `-PMMKV_INCLUDE_SAMPLE=false` for a library-only build. + +The sample enables Android and iOS by default. Its desktop configuration is +disabled because the MMKV wrapper does not yet provide a JVM target. The existing +desktop scaffold is retained behind `-PMMKV_ENABLE_DESKTOP=true` for development; +it still requires JVM support in the wrapper before it can build. Published-MMKV +mode always disables desktop, even when this flag is set. ```bash cd KMP @@ -161,4 +184,22 @@ cd KMP `com.tencent:mmkv:2.4.3` Android artifact has been published only to Maven Local. +To prepare the iOS sample, install CocoaPods and run the following from `KMP`: + +```bash +./gradlew :sample:composeApp:podInstall -PMMKV_INCLUDE_SAMPLE=true +open sample/iosApp/iosApp.xcworkspace +``` + +This generates the Pods support files, including the `.xcconfig` files referenced +by the Xcode project. Use the generated workspace for Xcode builds. The sample is +included automatically when Xcode invokes Gradle through the CocoaPods build script. + +When running the iOS sample from Android Studio, its linked Xcode project must +use `sample/iosApp/iosApp.xcworkspace` as well. If the IDE linked the `.xcodeproj` +before `podInstall` created the workspace, invoke **Unlink Xcode project** via +**Find Action**, then **Sync Project with Gradle Files** to detect the workspace. +Building the standalone project omits the Pods target and can fail with +`Framework 'Pods_iosApp' not found`. + Release maintainers should also follow [PUBLISHING.md](./PUBLISHING.md). diff --git a/KMP/gradle.properties b/KMP/gradle.properties index 93eb9372..02607b4e 100644 --- a/KMP/gradle.properties +++ b/KMP/gradle.properties @@ -1,6 +1,9 @@ # Project-wide Gradle settings. org.gradle.jvmargs=-Xmx2048m +# Include the sample application in Android Studio imports. +MMKV_INCLUDE_SAMPLE=true + # Android android.useAndroidX=true android.nonTransitiveRClass=false diff --git a/KMP/gradle/gradle-daemon-jvm.properties b/KMP/gradle/gradle-daemon-jvm.properties new file mode 100644 index 00000000..f65cfab4 --- /dev/null +++ b/KMP/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,2 @@ +# Keep the build JVM compatible with Gradle 8.14, including Android Studio imports. +toolchainVersion=21 diff --git a/KMP/mmkv/build.gradle.kts b/KMP/mmkv/build.gradle.kts index 78a99b5f..3dfd9ac2 100644 --- a/KMP/mmkv/build.gradle.kts +++ b/KMP/mmkv/build.gradle.kts @@ -18,6 +18,7 @@ * limitations under the License. */ +import java.util.Properties import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.tasks.Exec import org.gradle.api.tasks.bundling.Jar @@ -71,6 +72,19 @@ version = publishVersion val nativeInteropDir = project.file("nativeInterop") val nativeBuildRoot = nativeInteropDir.resolve("build") +// These custom Exec tasks do not use AGP's CMake discovery. Honor cmake.dir so +// IDE imports do not depend on the GUI process inheriting the shell's PATH. +val localProperties = Properties().apply { + val propertiesFile = rootProject.file("local.properties") + if (propertiesFile.isFile) { + propertiesFile.inputStream().use { load(it) } + } +} +val cmakeExecutable = localProperties.getProperty("cmake.dir") + ?.trim()?.takeIf { it.isNotEmpty() } + ?.let { rootProject.file(it).resolve("bin/cmake").absolutePath } + ?: "cmake" + fun taskSuffix(label: String): String = label.split('-', '_') .filter { it.isNotBlank() } @@ -90,6 +104,7 @@ fun registerCMakeBuildTask( inputs.file(nativeInteropDir.resolve("CMakeLists.txt")) inputs.file(nativeInteropDir.resolve("cinterop/mmkv.def")) + inputs.property("cmakeExecutable", cmakeExecutable) outputs.file(buildDir.resolve("CMakeCache.txt")) outputs.file(buildDir.resolve("include/MMKVBridge.h")) @@ -98,7 +113,7 @@ fun registerCMakeBuildTask( workingDir = nativeInteropDir commandLine( buildList { - add("cmake") + add(cmakeExecutable) add("-S") add(".") add("-B") @@ -126,6 +141,7 @@ fun registerCMakeBuildTask( inputs.file(nativeInteropDir.resolve("CMakeLists.txt")) inputs.file(nativeInteropDir.resolve("cinterop/mmkv.def")) + inputs.property("cmakeExecutable", cmakeExecutable) outputs.file(buildDir.resolve("libmmkv-kmp.a")) // Let CMake perform its own incremental source check. Gradle cannot // reliably model FetchContent/local Core source inputs here. @@ -134,7 +150,7 @@ fun registerCMakeBuildTask( workingDir = nativeInteropDir commandLine( buildList { - add("cmake") + add(cmakeExecutable) add("--build") add(buildDir.absolutePath) add("--config") diff --git a/KMP/sample/composeApp/build.gradle.kts b/KMP/sample/composeApp/build.gradle.kts index 7d1bdb34..6a79724f 100644 --- a/KMP/sample/composeApp/build.gradle.kts +++ b/KMP/sample/composeApp/build.gradle.kts @@ -7,6 +7,10 @@ plugins { } val usePublishedMMKV = providers.gradleProperty("MMKV_USE_PUBLISHED").orNull == "true" +// The MMKV wrapper currently has no JVM target. Keep the desktop scaffold +// opt-in so normal imports and aggregate builds only use supported targets. +val enableDesktop = !usePublishedMMKV && + providers.gradleProperty("MMKV_ENABLE_DESKTOP").orNull == "true" val mmkvVersion = providers.gradleProperty("VERSION_NAME").getOrElse("2.4.3") kotlin { @@ -16,8 +20,7 @@ kotlin { } } - if (!usePublishedMMKV) { - // Desktop is available only when the sample consumes the in-tree project. + if (enableDesktop) { jvm("desktop") { compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11) @@ -61,7 +64,7 @@ kotlin { implementation("androidx.activity:activity-compose:1.10.1") } } - if (!usePublishedMMKV) { + if (enableDesktop) { val desktopMain by getting { dependencies { implementation(compose.desktop.currentOs) @@ -71,7 +74,7 @@ kotlin { } } -if (!usePublishedMMKV) { +if (enableDesktop) { compose.desktop { application { mainClass = "com.tencent.mmkv.sample.MainKt" diff --git a/KMP/settings.gradle.kts b/KMP/settings.gradle.kts index f795e5cc..b96886c5 100644 --- a/KMP/settings.gradle.kts +++ b/KMP/settings.gradle.kts @@ -26,6 +26,9 @@ dependencyResolutionManagement { } include(":mmkv") -if (providers.gradleProperty("MMKV_INCLUDE_SAMPLE").orNull == "true") { +// Xcode's generated CocoaPods script passes platform/arch/configuration, but +// does not forward the MMKV_INCLUDE_SAMPLE flag used during pod installation. +val isCocoaPodsBuild = providers.gradleProperty("kotlin.native.cocoapods.platform").isPresent +if (providers.gradleProperty("MMKV_INCLUDE_SAMPLE").orNull == "true" || isCocoaPodsBuild) { include(":sample:composeApp") }