@file:OptIn(gobley.gradle.InternalGobleyGradleApi::class) import gobley.gradle.cargo.dsl.appleMobile import gobley.gradle.cargo.dsl.jvm import gobley.gradle.cargo.tasks.CargoBuildTask import gobley.gradle.cargo.tasks.CargoCheckTask import gobley.gradle.GobleyHost import gobley.gradle.rust.targets.RustAndroidTarget import gobley.gradle.rust.targets.RustAppleMobileTarget import gobley.gradle.rust.targets.RustTarget import gobley.gradle.Variant import org.gradle.api.DefaultTask import org.gradle.api.provider.ListProperty import org.gradle.api.tasks.Input import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import org.gradle.jvm.tasks.Jar import org.jetbrains.kotlin.gradle.dsl.JvmTarget abstract class VerifyHostCargoTaskSelection : DefaultTask() { @get:Input abstract val mismatches: ListProperty @TaskAction fun verify() { check(mismatches.get().isEmpty()) { "Cargo tasks do not match the current host: ${mismatches.get().joinToString()}" } } } plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) alias(libs.plugins.gobleyCargo) alias(libs.plugins.gobleyUniffi) alias(libs.plugins.kotlinAtomicfu) } val appVersion = providers.gradleProperty("vnidrop.version").get() val desktopRustVariant = providers.gradleProperty("vnidrop.desktop.rustVariant") .map { value -> when (value.trim().lowercase()) { "debug" -> Variant.Debug "release" -> Variant.Release else -> error("vnidrop.desktop.rustVariant must be either debug or release") } } .orElse(Variant.Debug) // Compile-time switches (gradle.properties or -P…). // included=false: no Share-diagnostics toggle, no telemetry/crash auto-upload stack. // endpoint/key both empty: transport is NoOp (safe default until Cloudflare is deployed). val diagnosticsIncluded: Boolean = (findProperty("vnidrop.diagnostics.included") as String?)?.toBooleanStrictOrNull() ?: false val diagnosticsEndpoint: String = (findProperty("vnidrop.diagnostics.endpoint") as String?)?.trim().orEmpty() val diagnosticsIngestKey: String = (findProperty("vnidrop.diagnostics.ingestKey") as String?)?.trim().orEmpty() check(!diagnosticsIncluded || diagnosticsEndpoint.isEmpty() == diagnosticsIngestKey.isEmpty()) { "vnidrop.diagnostics.endpoint and vnidrop.diagnostics.ingestKey must be configured together" } val diagnosticsBuildConfigDir = layout.buildDirectory.dir("generated/diagnostics/commonMain/kotlin") val generateDiagnosticsBuildConfig by tasks.registering { group = "build" description = "Generates DiagnosticsBuildConfig from vnidrop.diagnostics.* properties" val outputDir = diagnosticsBuildConfigDir val included = diagnosticsIncluded val endpoint = if (included) diagnosticsEndpoint else "" val ingestKey = if (included) diagnosticsIngestKey else "" inputs.property("vnidrop.diagnostics.included", included) inputs.property("vnidrop.diagnostics.endpoint", endpoint) inputs.property("vnidrop.diagnostics.ingestKey", ingestKey) outputs.dir(outputDir) doLast { val packageDir = outputDir.get().asFile.resolve("com/vnidrop/app/diagnostics") packageDir.mkdirs() fun esc(value: String): String = buildString { for (ch in value) { when (ch) { '\\' -> append("\\\\") '"' -> append("\\\"") '\n' -> append("\\n") '\r' -> append("\\r") '\t' -> append("\\t") '$' -> append("\\$") else -> append(ch) } } } packageDir.resolve("DiagnosticsBuildConfig.kt").writeText( """ |package com.vnidrop.app.diagnostics | |/** | * Generated by shared/build.gradle.kts from vnidrop.diagnostics.* properties. | * | * - included=false → no opt-in UI / telemetry / crash hooks | * - endpoint/key both empty → [NoOpDiagnosticsTransport] (no network) | */ |object DiagnosticsBuildConfig { | const val INCLUDED: Boolean = $included | const val ENDPOINT: String = "${esc(endpoint)}" | const val INGEST_KEY: String = "${esc(ingestKey)}" |} | """.trimMargin(), ) } } kotlin { if (GobleyHost.current.platform == GobleyHost.Platform.MacOS) { listOf( iosArm64(), iosSimulatorArm64() ).forEach { iosTarget -> iosTarget.binaries.framework { baseName = "Shared" isStatic = true } } } androidTarget { compilerOptions { jvmTarget = JvmTarget.JVM_11 } } jvm() sourceSets { commonMain { kotlin.srcDir(files(diagnosticsBuildConfigDir).builtBy(generateDiagnosticsBuildConfig)) } androidMain.dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.core.ktx) implementation(libs.google.code.scanner) implementation(libs.compose.uiToolingPreview) } commonMain.dependencies { implementation(libs.compose.runtime) implementation(libs.compose.foundation) implementation(libs.compose.material3) implementation(libs.compose.ui) implementation(libs.compose.components.resources) implementation(libs.compose.uiToolingPreview) implementation(libs.compottie.lite) implementation(libs.androidx.lifecycle.viewmodelCompose) implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.androidx.datastore) implementation(libs.androidx.datastore.preferences) implementation(libs.kotlinx.coroutinesCore) implementation(libs.qrcode.kotlin) } commonTest.dependencies { implementation(libs.kotlin.test) implementation(libs.kotlinx.coroutinesTest) } jvmTest.dependencies { implementation(compose.desktop.currentOs) implementation(libs.compose.uiTest) } } } dependencies { debugImplementation(libs.compose.uiTooling) } android { namespace = "com.vnidrop.app.shared" compileSdk = libs.versions.android.compileSdk.get().toInt() defaultConfig { minSdk = libs.versions.android.minSdk.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } val hostCargoTargets = buildSet { add(GobleyHost.current.rustTarget) addAll(RustAndroidTarget.entries) if (GobleyHost.current.platform == GobleyHost.Platform.MacOS) { addAll(RustAppleMobileTarget.entries) } } cargo { packageDirectory = layout.projectDirectory.dir("../crates/vnidrop") publishJvmArtifacts = true jvmVariant.set(desktopRustVariant) androidTargetsToBuild.set(setOf(RustAndroidTarget.Arm64, RustAndroidTarget.X64)) builds.jvm { variants { // Desktop distributions are built per host. Do not publish disabled // cross-platform native jars into the app runtime classpath. embedRustLibrary.set(rustTarget == GobleyHost.current.rustTarget) } } builds.appleMobile { variants { buildTaskProvider.configure { if (rustTarget.cinteropName == "ios") { additionalEnvironment.put("IPHONEOS_DEPLOYMENT_TARGET", "16.0.0") } } } } builds.configureEach { val buildOnCurrentHost = rustTarget in hostCargoTargets installTargetBeforeBuild.set(buildOnCurrentHost) variants { buildTaskProvider.configure { enabled = buildOnCurrentHost } checkTaskProvider.configure { enabled = buildOnCurrentHost } } } } uniffi { generateFromLibrary { namespace = "vnidrop" build.set(GobleyHost.current.rustTarget) variant.set(desktopRustVariant) } } tasks.named("jvmJar") { manifest.attributes["Implementation-Version"] = appVersion } val verifyHostCargoTaskSelection = tasks.register( "verifyHostCargoTaskSelection" ) { group = "verification" description = "Verifies that Cargo tasks are enabled only for targets supported by this host." mismatches.convention(emptyList()) } afterEvaluate { verifyHostCargoTaskSelection.configure { mismatches.set(buildList { tasks.withType().forEach { if (it.enabled != (it.target.get() in hostCargoTargets)) add(it.path) } tasks.withType().forEach { if (it.enabled != (it.target.get() in hostCargoTargets)) add(it.path) } }) } } tasks.named("check") { dependsOn(verifyHostCargoTaskSelection) } tasks.configureEach { // Gobley does not currently treat every Rust source/API change as an input of // all platform cargo tasks. Without these inputs an incremental Android build // can package an older .so next to freshly generated UniFFI Kotlin bindings. if (name.startsWith("cargoBuild")) { inputs.files( fileTree(layout.projectDirectory.dir("../crates/vnidrop")) { include("Cargo.toml", "build.rs", "src/**/*.rs") }, layout.projectDirectory.file("../Cargo.toml"), layout.projectDirectory.file("../Cargo.lock"), ).withPathSensitivity(PathSensitivity.RELATIVE) } }