| 1 |
import java.io.File |
| 2 |
import java.util.Properties |
| 3 |
|
| 4 |
plugins { |
| 5 |
alias(libs.plugins.android.application) |
| 6 |
alias(libs.plugins.kotlin.compose) |
| 7 |
alias(libs.plugins.kotlin.serialization) |
| 8 |
alias(libs.plugins.screenshot) |
| 9 |
} |
| 10 |
|
| 11 |
// The Rust core (rust-core/, over oxy-core) is built for the app's ABIs by |
| 12 |
// cargo-ndk and its Kotlin bindings generated by uniffi at build time |
| 13 |
// (oxygenhub-android#1): nothing built is kept in the repository. |
| 14 |
val rustCore: File = rootProject.file("rust-core") |
| 15 |
val rustJniLibs = layout.buildDirectory.dir("rustJniLibs") |
| 16 |
val rustBindings = layout.buildDirectory.dir("generated/uniffi/kotlin") |
| 17 |
|
| 18 |
fun androidNdk(): String { |
| 19 |
System.getenv("ANDROID_NDK_HOME")?.let { return it } |
| 20 |
val props = Properties() |
| 21 |
val local = rootProject.file("local.properties") |
| 22 |
if (local.exists()) local.inputStream().use { input -> props.load(input) } |
| 23 |
val sdk = props.getProperty("sdk.dir") ?: System.getenv("ANDROID_HOME") |
| 24 |
?: error("set sdk.dir in local.properties or ANDROID_HOME") |
| 25 |
val ndks = File(sdk, "ndk").listFiles()?.filter { f -> f.isDirectory }?.sortedBy { f -> f.name }.orEmpty() |
| 26 |
return ndks.lastOrNull()?.path ?: error("no NDK under $sdk/ndk: install one with the SDK manager") |
| 27 |
} |
| 28 |
|
| 29 |
val buildRustCore = tasks.register("buildRustCore", Exec::class) { |
| 30 |
description = "Builds rust-core for arm64-v8a and x86_64 with cargo-ndk." |
| 31 |
workingDir = rustCore |
| 32 |
inputs.dir(rustCore.resolve("src")) |
| 33 |
inputs.files(rustCore.resolve("Cargo.toml"), rustCore.resolve("Cargo.lock")) |
| 34 |
outputs.dir(rustJniLibs) |
| 35 |
environment("ANDROID_NDK_HOME", androidNdk()) |
| 36 |
commandLine("cargo", "ndk", "-t", "arm64-v8a", "-t", "x86_64", "--platform", "31", "-o", rustJniLibs.get().asFile.path, "build", "--release") |
| 37 |
} |
| 38 |
|
| 39 |
val generateRustBindings = tasks.register("generateRustBindings", Exec::class) { |
| 40 |
description = "Generates rust-core's Kotlin bindings with uniffi." |
| 41 |
dependsOn(buildRustCore) |
| 42 |
workingDir = rustCore |
| 43 |
inputs.dir(rustJniLibs) |
| 44 |
outputs.dir(rustBindings) |
| 45 |
commandLine( |
| 46 |
"cargo", "run", "--release", "--bin", "uniffi-bindgen", "generate", |
| 47 |
"--library", rustJniLibs.get().file("arm64-v8a/librust_core.so").asFile.path, |
| 48 |
"--language", "kotlin", "--out-dir", rustBindings.get().asFile.path, |
| 49 |
) |
| 50 |
} |
| 51 |
|
| 52 |
tasks.named("preBuild") { dependsOn(generateRustBindings) } |
| 53 |
|
| 54 |
// The version, from gradle.properties (#7). |
| 55 |
val appVersion: String = providers.gradleProperty("oxygenhub.version").get() |
| 56 |
val appVersionCode: Int = appVersion.split(".").map { it.toInt() }.let { (a, b, c) -> a * 10000 + b * 100 + c } |
| 57 |
|
| 58 |
// Release signing (#7): the release job decodes its keystore secret to a |
| 59 |
// file and names it and the password's file here. Without them a release |
| 60 |
// build is unsigned, so nothing else needs the key. |
| 61 |
val releaseKeystore: File? = providers.environmentVariable("OXYGENHUB_KEYSTORE").orNull?.let { File(it) }?.takeIf { it.isFile } |
| 62 |
val releasePassword: String? = |
| 63 |
providers.environmentVariable("OXYGENHUB_KEYSTORE_PASSWORD_FILE").orNull?.let { File(it).readText().trim() } |
| 64 |
|
| 65 |
android { |
| 66 |
namespace = "io.oxygenhub.android" |
| 67 |
compileSdk { |
| 68 |
version = release(37) |
| 69 |
} |
| 70 |
|
| 71 |
defaultConfig { |
| 72 |
applicationId = "io.oxygenhub.android" |
| 73 |
minSdk = 31 |
| 74 |
targetSdk = 37 |
| 75 |
versionCode = appVersionCode |
| 76 |
versionName = appVersion |
| 77 |
|
| 78 |
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" |
| 79 |
} |
| 80 |
|
| 81 |
signingConfigs { |
| 82 |
if (releaseKeystore != null && releasePassword != null) { |
| 83 |
create("release") { |
| 84 |
storeFile = releaseKeystore |
| 85 |
storeType = "PKCS12" |
| 86 |
storePassword = releasePassword |
| 87 |
keyAlias = "oxygenhub" |
| 88 |
keyPassword = releasePassword |
| 89 |
} |
| 90 |
} |
| 91 |
} |
| 92 |
|
| 93 |
buildTypes { |
| 94 |
// Unit test coverage as JaCoCo XML for the hub's Coverage tab |
| 95 |
// (#19): ./gradlew :app:createDebugUnitTestCoverageReport. |
| 96 |
debug { |
| 97 |
enableUnitTestCoverage = true |
| 98 |
} |
| 99 |
release { |
| 100 |
optimization { |
| 101 |
enable = false |
| 102 |
} |
| 103 |
signingConfigs.findByName("release")?.let { signingConfig = it } |
| 104 |
} |
| 105 |
} |
| 106 |
compileOptions { |
| 107 |
sourceCompatibility = JavaVersion.VERSION_11 |
| 108 |
targetCompatibility = JavaVersion.VERSION_11 |
| 109 |
} |
| 110 |
packaging { |
| 111 |
jniLibs { |
| 112 |
useLegacyPackaging = true |
| 113 |
} |
| 114 |
} |
| 115 |
buildFeatures { |
| 116 |
compose = true |
| 117 |
} |
| 118 |
// README screenshots, rendered from previews without an emulator: |
| 119 |
// ./gradlew :app:updateDebugScreenshotTest (src/screenshotTest). |
| 120 |
experimentalProperties["android.experimental.enableScreenshotTest"] = true |
| 121 |
sourceSets["main"].jniLibs.directories.add(rustJniLibs.get().asFile.path) |
| 122 |
sourceSets["main"].kotlin.directories.add(rustBindings.get().asFile.path) |
| 123 |
} |
| 124 |
|
| 125 |
dependencies { |
| 126 |
implementation(libs.jna) { artifact { type = "aar" } } |
| 127 |
implementation(platform(libs.androidx.compose.bom)) |
| 128 |
implementation(libs.androidx.activity.compose) |
| 129 |
implementation(libs.androidx.compose.material3) |
| 130 |
implementation(libs.androidx.compose.material3.navigation.suite) |
| 131 |
implementation(libs.androidx.compose.material3.adaptive) |
| 132 |
implementation(libs.androidx.compose.material3.adaptive.layout) |
| 133 |
implementation(libs.androidx.compose.material3.adaptive.navigation) |
| 134 |
implementation(libs.androidx.compose.material.icons.extended) |
| 135 |
implementation(libs.androidx.compose.ui) |
| 136 |
implementation(libs.androidx.compose.ui.graphics) |
| 137 |
implementation(libs.androidx.compose.ui.tooling.preview) |
| 138 |
implementation(libs.androidx.core.ktx) |
| 139 |
implementation(libs.androidx.lifecycle.runtime.ktx) |
| 140 |
implementation(libs.androidx.lifecycle.runtime.compose) |
| 141 |
implementation(libs.androidx.lifecycle.viewmodel.compose) |
| 142 |
implementation(libs.androidx.navigation.compose) |
| 143 |
implementation(libs.kotlinx.serialization.json) |
| 144 |
implementation(libs.kotlinx.coroutines.android) |
| 145 |
implementation(libs.okhttp) |
| 146 |
implementation(libs.markdown.renderer.m3) |
| 147 |
implementation(libs.highlights) |
| 148 |
testImplementation(libs.junit) |
| 149 |
testImplementation(libs.kotlinx.coroutines.test) |
| 150 |
testImplementation(libs.okhttp.mockwebserver) |
| 151 |
androidTestImplementation(platform(libs.androidx.compose.bom)) |
| 152 |
androidTestImplementation(libs.androidx.compose.ui.test.junit4) |
| 153 |
androidTestImplementation(libs.androidx.espresso.core) |
| 154 |
androidTestImplementation(libs.androidx.junit) |
| 155 |
screenshotTestImplementation(libs.screenshot.validation.api) |
| 156 |
screenshotTestImplementation(libs.androidx.compose.ui.tooling) |
| 157 |
screenshotTestImplementation(libs.kotlinx.coroutines.test) |
| 158 |
debugImplementation(libs.androidx.compose.ui.test.manifest) |
| 159 |
debugImplementation(libs.androidx.compose.ui.tooling) |
| 160 |
} |