OXYGENHUB
◆ v0.1.3 oxygenhub-android / app / build.gradle.kts
8.6 KiB · last changed A home screen widget: latest runs, unread, reviews waiting (closes #28) 7 days ago source blame history raw
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
    // The runner's Rust (OxygenFlow and its oxy-core) is built in too.
35
    inputs.files(fileTree(rootProject.file("oxygenflow")) { include("**/*.rs", "**/Cargo.toml"); exclude("**/target/**") })
36
    outputs.dir(rustJniLibs)
37
    environment("ANDROID_NDK_HOME", androidNdk())
38
    commandLine("cargo", "ndk", "-t", "arm64-v8a", "-t", "x86_64", "--platform", "31", "-o", rustJniLibs.get().asFile.path, "build", "--release")
39
}
40
41
val generateRustBindings = tasks.register("generateRustBindings", Exec::class) {
42
    description = "Generates rust-core's Kotlin bindings with uniffi."
43
    dependsOn(buildRustCore)
44
    workingDir = rustCore
45
    inputs.dir(rustJniLibs)
46
    inputs.file(rustCore.resolve("uniffi.toml"))
47
    outputs.dir(rustBindings)
48
    commandLine(
49
        "cargo", "run", "--release", "--bin", "uniffi-bindgen", "generate",
50
        "--library", rustJniLibs.get().file("arm64-v8a/librust_core.so").asFile.path,
51
        "--language", "kotlin", "--out-dir", rustBindings.get().asFile.path,
52
    )
53
}
54
55
tasks.named("preBuild") { dependsOn(generateRustBindings) }
56
57
// The Firebase client settings alerts need (#16), read from
58
// app/google-services.json without Google's Gradle plugin: none, no alerts.
59
val firebase: Map<String, String> = run {
60
    val file = File(projectDir, "google-services.json")
61
    if (!file.isFile) return@run emptyMap()
62
    @Suppress("UNCHECKED_CAST")
63
    val json = groovy.json.JsonSlurper().parse(file) as Map<String, Any?>
64
    val project = json["project_info"] as Map<String, Any?>
65
    val client = (json["client"] as List<Map<String, Any?>>).first {
66
        ((it["client_info"] as Map<String, Any?>)["android_client_info"] as Map<String, Any?>)["package_name"] == "io.oxygenhub.android"
67
    }
68
    mapOf(
69
        "app_id" to (client["client_info"] as Map<String, Any?>)["mobilesdk_app_id"].toString(),
70
        "project_id" to project["project_id"].toString(),
71
        "sender_id" to project["project_number"].toString(),
72
        "api_key" to ((client["api_key"] as List<Map<String, Any?>>).first()["current_key"]).toString(),
73
    )
74
}
75
76
// The version, from gradle.properties (#7).
77
val appVersion: String = providers.gradleProperty("oxygenhub.version").get()
78
val appVersionCode: Int = appVersion.split(".").map { it.toInt() }.let { (a, b, c) -> a * 10000 + b * 100 + c }
79
80
// Release signing (#7): the release job decodes its keystore secret to a
81
// file and names it and the password's file here. Without them a release
82
// build is unsigned, so nothing else needs the key.
83
val releaseKeystore: File? = providers.environmentVariable("OXYGENHUB_KEYSTORE").orNull?.let { File(it) }?.takeIf { it.isFile }
84
val releasePassword: String? =
85
    providers.environmentVariable("OXYGENHUB_KEYSTORE_PASSWORD_FILE").orNull?.let { File(it).readText().trim() }
86
87
android {
88
    namespace = "io.oxygenhub.android"
89
    compileSdk {
90
        version = release(37)
91
    }
92
93
    defaultConfig {
94
        applicationId = "io.oxygenhub.android"
95
        minSdk = 31
96
        targetSdk = 37
97
        versionCode = appVersionCode
98
        versionName = appVersion
99
100
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
101
        // Firebase for alerts (#16), from google-services.json when the
102
        // build has one (the release job writes it from a secret); empty,
103
        // the app takes no alerts.
104
        buildConfigField("String", "FIREBASE_APP_ID", "\"${firebase["app_id"].orEmpty()}\"")
105
        buildConfigField("String", "FIREBASE_PROJECT_ID", "\"${firebase["project_id"].orEmpty()}\"")
106
        buildConfigField("String", "FIREBASE_SENDER_ID", "\"${firebase["sender_id"].orEmpty()}\"")
107
        buildConfigField("String", "FIREBASE_API_KEY", "\"${firebase["api_key"].orEmpty()}\"")
108
    }
109
110
    signingConfigs {
111
        if (releaseKeystore != null && releasePassword != null) {
112
            create("release") {
113
                storeFile = releaseKeystore
114
                storeType = "PKCS12"
115
                storePassword = releasePassword
116
                keyAlias = "oxygenhub"
117
                keyPassword = releasePassword
118
            }
119
        }
120
    }
121
122
    buildTypes {
123
        // Unit test coverage as JaCoCo XML for the hub's Coverage tab
124
        // (#19): ./gradlew :app:createDebugUnitTestCoverageReport.
125
        debug {
126
            enableUnitTestCoverage = true
127
        }
128
        release {
129
            optimization {
130
                enable = false
131
            }
132
            signingConfigs.findByName("release")?.let { signingConfig = it }
133
        }
134
    }
135
    compileOptions {
136
        sourceCompatibility = JavaVersion.VERSION_11
137
        targetCompatibility = JavaVersion.VERSION_11
138
    }
139
    packaging {
140
        jniLibs {
141
            useLegacyPackaging = true
142
        }
143
    }
144
    buildFeatures {
145
        compose = true
146
        buildConfig = true
147
    }
148
    // README screenshots, rendered from previews without an emulator:
149
    // ./gradlew :app:updateDebugScreenshotTest (src/screenshotTest).
150
    experimentalProperties["android.experimental.enableScreenshotTest"] = true
151
    sourceSets["main"].jniLibs.directories.add(rustJniLibs.get().asFile.path)
152
    sourceSets["main"].kotlin.directories.add(rustBindings.get().asFile.path)
153
}
154
155
dependencies {
156
    implementation(libs.jna) { artifact { type = "aar" } }
157
    implementation(platform(libs.androidx.compose.bom))
158
    implementation(platform(libs.firebase.bom))
159
    implementation(libs.firebase.messaging)
160
    implementation(libs.androidx.activity.compose)
161
    implementation(libs.androidx.compose.material3)
162
    implementation(libs.androidx.compose.material3.navigation.suite)
163
    implementation(libs.androidx.compose.material3.adaptive)
164
    implementation(libs.androidx.compose.material3.adaptive.layout)
165
    implementation(libs.androidx.compose.material3.adaptive.navigation)
166
    implementation(libs.androidx.compose.material.icons.extended)
167
    implementation(libs.androidx.glance.appwidget)
168
    implementation(libs.androidx.glance.material3)
169
    implementation(libs.androidx.compose.ui)
170
    implementation(libs.androidx.compose.ui.graphics)
171
    implementation(libs.androidx.compose.ui.tooling.preview)
172
    implementation(libs.androidx.core.ktx)
173
    implementation(libs.androidx.lifecycle.runtime.ktx)
174
    implementation(libs.androidx.lifecycle.runtime.compose)
175
    implementation(libs.androidx.lifecycle.viewmodel.compose)
176
    implementation(libs.androidx.navigation.compose)
177
    implementation(libs.kotlinx.serialization.json)
178
    implementation(libs.kotlinx.coroutines.android)
179
    implementation(libs.okhttp)
180
    implementation(libs.markdown.renderer.m3)
181
    implementation(libs.highlights)
182
    testImplementation(libs.junit)
183
    testImplementation(libs.kotlinx.coroutines.test)
184
    testImplementation(libs.okhttp.mockwebserver)
185
    androidTestImplementation(platform(libs.androidx.compose.bom))
186
    androidTestImplementation(libs.androidx.compose.ui.test.junit4)
187
    androidTestImplementation(libs.androidx.espresso.core)
188
    androidTestImplementation(libs.androidx.junit)
189
    screenshotTestImplementation(libs.screenshot.validation.api)
190
    screenshotTestImplementation(libs.androidx.compose.ui.tooling)
191
    screenshotTestImplementation(libs.kotlinx.coroutines.test)
192
    debugImplementation(libs.androidx.compose.ui.test.manifest)
193
    debugImplementation(libs.androidx.compose.ui.tooling)
194
}