mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
Upstream mpv-android moved its build to NDK r30 (30.0.16248370), and each mpv-android revision pins its own NDK, so the two move together. The new revision also updates harfbuzz to 14.5.0, unibreak to 8.0, fribidi to 1.0.17, libxml2 to 2.15.4, fontconfig to 2.18.3 and curl to 8.22.0, and builds libass with assembly on x86_64 (nasm, already installed in CI). r30 brings an LLVM update with codegen and LTO fixes and sysroots up to API 37. It is also the NDK current SDK managers install, so a fresh setup no longer needs r29 installed beside it. The script change invalidates the cached libmpv tree, so CI rebuilds it once. The staged libraries are the same set as before, all 16 KiB aligned.
312 lines
12 KiB
Kotlin
312 lines
12 KiB
Kotlin
import org.gradle.api.tasks.Exec
|
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
|
|
plugins {
|
|
alias(libs.plugins.android.application)
|
|
alias(libs.plugins.kotlin.android)
|
|
alias(libs.plugins.kotlin.compose)
|
|
alias(libs.plugins.ktlint)
|
|
alias(libs.plugins.roborazzi)
|
|
}
|
|
|
|
val repositoryRoot = rootProject.projectDir.parentFile
|
|
val workspaceVersion = Regex("""(?m)^version\s*=\s*\"([^\"]+)\"""")
|
|
.find(repositoryRoot.resolve("Cargo.toml").readText())
|
|
?.groupValues?.get(1)
|
|
?: error("workspace version is missing from Cargo.toml")
|
|
|
|
fun semverVersionCode(version: String): Int {
|
|
val match = Regex("""^(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?$""").matchEntire(version)
|
|
?: error("Android version must start with major.minor.patch: $version")
|
|
val (major, minor, patch) = match.destructured.toList().take(3).map(String::toLong)
|
|
require(minor in 0..999 && patch in 0..999) { "minor and patch must fit three digits" }
|
|
val code = major * 1_000_000 + minor * 1_000 + patch
|
|
require(code in 1..2_100_000_000) { "semantic version is outside Android's version-code range" }
|
|
return code.toInt()
|
|
}
|
|
|
|
// The visible Android version is the workspace/release-tag version. Only the
|
|
// monotonically increasing Play version code may be overridden by release CI.
|
|
val releaseVersionName = workspaceVersion
|
|
val versionCodeOverride = providers.environmentVariable("ANDROID_VERSION_CODE").orNull
|
|
val releaseVersionCode = versionCodeOverride?.toIntOrNull()
|
|
?: if (versionCodeOverride == null) semverVersionCode(releaseVersionName)
|
|
else error("ANDROID_VERSION_CODE must be an integer")
|
|
require(releaseVersionCode in 1..2_100_000_000) { "ANDROID_VERSION_CODE must be a positive Play-compatible integer" }
|
|
|
|
tasks.register("printAndroidVersion") {
|
|
group = "help"
|
|
description = "Prints the resolved Android version name and code for release validation."
|
|
doLast {
|
|
println("versionName=$releaseVersionName")
|
|
println("versionCode=$releaseVersionCode")
|
|
}
|
|
}
|
|
|
|
android {
|
|
namespace = "io.narl.protonstream"
|
|
compileSdk = 36
|
|
buildToolsVersion = "37.0.0"
|
|
ndkVersion = "30.0.16248370"
|
|
|
|
defaultConfig {
|
|
applicationId = "io.narl.protonstream"
|
|
minSdk = 31
|
|
targetSdk = 36
|
|
versionCode = releaseVersionCode
|
|
versionName = releaseVersionName
|
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
|
|
ndk {
|
|
abiFilters += listOf("arm64-v8a", "x86_64")
|
|
}
|
|
externalNativeBuild {
|
|
cmake {
|
|
arguments += "-DPSTR_MPV_ROOT=${layout.buildDirectory.dir("generated/mpv").get().asFile.absolutePath}"
|
|
arguments += "-DPSTR_RUST_JNI_ROOT=${layout.buildDirectory.dir("generated/jniLibs").get().asFile.absolutePath}"
|
|
cppFlags += listOf("-std=c++20")
|
|
}
|
|
}
|
|
}
|
|
|
|
buildFeatures {
|
|
compose = true
|
|
buildConfig = true
|
|
}
|
|
|
|
sourceSets.named("main") {
|
|
java.srcDir(layout.buildDirectory.dir("generated/source/uniffi"))
|
|
jniLibs.srcDir(layout.buildDirectory.dir("generated/jniLibs"))
|
|
jniLibs.srcDir(layout.buildDirectory.dir("generated/mpv/jniLibs"))
|
|
}
|
|
|
|
externalNativeBuild {
|
|
cmake {
|
|
path = file("src/main/cpp/CMakeLists.txt")
|
|
version = "3.22.1"
|
|
}
|
|
}
|
|
|
|
val releaseStore = System.getenv("ANDROID_RELEASE_STORE_FILE")
|
|
if (releaseStore != null) {
|
|
signingConfigs.create("release") {
|
|
storeFile = file(releaseStore)
|
|
storePassword = System.getenv("ANDROID_RELEASE_STORE_PASSWORD")
|
|
keyAlias = System.getenv("ANDROID_RELEASE_KEY_ALIAS")
|
|
keyPassword = System.getenv("ANDROID_RELEASE_KEY_PASSWORD")
|
|
}
|
|
}
|
|
|
|
buildTypes {
|
|
debug {
|
|
applicationIdSuffix = ".debug"
|
|
versionNameSuffix = "-debug"
|
|
}
|
|
release {
|
|
isMinifyEnabled = true
|
|
isShrinkResources = true
|
|
if (releaseStore != null) signingConfig = signingConfigs.getByName("release")
|
|
proguardFiles(
|
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
"proguard-rules.pro",
|
|
// Applied to the shipping APK on purpose; the file explains the
|
|
// trade-off and what each keep is repairing.
|
|
"proguard-instrumentation.pro",
|
|
)
|
|
testProguardFiles("proguard-test-rules.pro")
|
|
}
|
|
}
|
|
|
|
// The minified path is what ships, and R8 breaks reflective UniFFI/JNA
|
|
// surfaces in ways no debug run can see. CI sets this to `release` and runs
|
|
// connectedAndroidTest against it; locally the default keeps builds fast.
|
|
testBuildType = providers.environmentVariable("ANDROID_TEST_BUILD_TYPE").orNull ?: "debug"
|
|
|
|
lint {
|
|
warningsAsErrors = true
|
|
abortOnError = true
|
|
checkDependencies = true
|
|
// The baseline records what was already here when the gate went in, so
|
|
// anything *new* fails. It is not a permanent exemption: every entry in
|
|
// it maps to an open bug in docs/BUGS.md (the SharedPreferences
|
|
// `commit()` calls are B20, the PiP one is B40), and the file should get
|
|
// shorter as those land. Regenerate with `gradlew updateLintBaseline`
|
|
// after a fix, and never to make an unrelated failure go away.
|
|
baseline = file("lint-baseline.xml")
|
|
// Generated UniFFI bindings are not ours to fix.
|
|
disable += setOf("UnusedIds")
|
|
// Dependency freshness is not a defect, and as errors these break the
|
|
// gate whenever someone else cuts a release. Upgrades are a deliberate
|
|
// act with their own verification; `gradlew lintDebug -Dlint...` or a
|
|
// dependency bot is the right place to surface them, not CI.
|
|
informational += setOf(
|
|
"GradleDependency",
|
|
"NewerVersionAvailable",
|
|
"AndroidGradlePluginVersion",
|
|
// The same for the platform: raising targetSdk opts into new
|
|
// behaviour and needs its own pass on a device.
|
|
"OldTargetApi",
|
|
)
|
|
sarifReport = true
|
|
}
|
|
|
|
testOptions {
|
|
unitTests {
|
|
isIncludeAndroidResources = true
|
|
isReturnDefaultValues = true
|
|
}
|
|
}
|
|
|
|
packaging {
|
|
jniLibs.useLegacyPackaging = false
|
|
resources.excludes += setOf("/META-INF/{AL2.0,LGPL2.1}")
|
|
}
|
|
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
targetCompatibility = JavaVersion.VERSION_17
|
|
}
|
|
|
|
}
|
|
|
|
kotlin {
|
|
compilerOptions {
|
|
jvmTarget.set(JvmTarget.JVM_17)
|
|
}
|
|
}
|
|
|
|
val rustJniDir = layout.buildDirectory.dir("generated/jniLibs")
|
|
val uniffiSourceDir = layout.buildDirectory.dir("generated/source/uniffi")
|
|
val hostRustLibrary = repositoryRoot.resolve(
|
|
"target/debug/${System.mapLibraryName("pstr_android")}",
|
|
)
|
|
|
|
val buildLibmpvAndroid by tasks.registering(Exec::class) {
|
|
group = "native"
|
|
description = "Builds the pinned GPL libmpv and stages both Android ABIs."
|
|
workingDir(repositoryRoot)
|
|
commandLine("bash", repositoryRoot.resolve("scripts/build-libmpv-android.sh").absolutePath)
|
|
// No declared outputs: the script's STAMP is the up-to-date check. Gradle
|
|
// deletes the declared outputs of a task it has no history for, which on a
|
|
// fresh CI checkout threw away the tree just restored from the cache and
|
|
// rebuilt libmpv from source on every run.
|
|
}
|
|
|
|
val buildRustHost by tasks.registering(Exec::class) {
|
|
group = "rust"
|
|
description = "Builds the host cdylib used by UniFFI binding generation."
|
|
workingDir(repositoryRoot)
|
|
commandLine("cargo", "build", "-p", "pstr-android", "--lib")
|
|
inputs.files(fileTree(repositoryRoot.resolve("crates")) { include("**/*.rs", "**/Cargo.toml") })
|
|
outputs.file(hostRustLibrary)
|
|
}
|
|
|
|
val generateUniFfiBindings by tasks.registering(Exec::class) {
|
|
group = "rust"
|
|
description = "Generates Kotlin bindings from the pstr-android cdylib."
|
|
dependsOn(buildRustHost)
|
|
workingDir(repositoryRoot)
|
|
doFirst { uniffiSourceDir.get().asFile.mkdirs() }
|
|
commandLine(
|
|
"cargo", "run", "-p", "pstr-android", "--features", "bindgen", "--bin", "uniffi-bindgen", "--", "generate",
|
|
"--library", hostRustLibrary.absolutePath,
|
|
"--language", "kotlin",
|
|
"--no-format",
|
|
"--out-dir", uniffiSourceDir.get().asFile.absolutePath,
|
|
)
|
|
inputs.file(hostRustLibrary)
|
|
outputs.dir(uniffiSourceDir)
|
|
}
|
|
|
|
val buildRustAndroid by tasks.registering(Exec::class) {
|
|
group = "rust"
|
|
description = "Builds pstr-android for every supported ABI with cargo-ndk."
|
|
workingDir(repositoryRoot)
|
|
doFirst { rustJniDir.get().asFile.mkdirs() }
|
|
commandLine(
|
|
"cargo", "ndk",
|
|
"--target", "arm64-v8a",
|
|
"--target", "x86_64",
|
|
"--platform", "31",
|
|
"--output-dir", rustJniDir.get().asFile.absolutePath,
|
|
"build", "--release", "-p", "pstr-android", "--lib",
|
|
)
|
|
environment("CARGO_TARGET_DIR", repositoryRoot.resolve("target/android").absolutePath)
|
|
inputs.files(fileTree(repositoryRoot.resolve("crates")) { include("**/*.rs", "**/Cargo.toml") })
|
|
outputs.dir(rustJniDir)
|
|
}
|
|
|
|
tasks.named("preBuild") {
|
|
dependsOn(generateUniFfiBindings, buildRustAndroid, buildLibmpvAndroid)
|
|
}
|
|
|
|
tasks.configureEach {
|
|
if (name.contains("CMake", ignoreCase = true)) {
|
|
dependsOn(buildRustAndroid, buildLibmpvAndroid)
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
implementation(libs.androidx.core)
|
|
implementation(libs.androidx.activity.compose)
|
|
implementation(libs.androidx.lifecycle.runtime)
|
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
|
implementation(libs.androidx.navigation.compose)
|
|
implementation(libs.androidx.work.runtime)
|
|
implementation(libs.jna) { artifact { type = "aar" } }
|
|
|
|
implementation(platform(libs.androidx.compose.bom))
|
|
implementation(libs.androidx.compose.ui)
|
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
|
implementation(libs.androidx.compose.runtime.livedata)
|
|
implementation(libs.androidx.compose.material3)
|
|
implementation(libs.androidx.compose.material.icons.extended)
|
|
implementation(libs.androidx.compose.material3.adaptive)
|
|
implementation(libs.androidx.compose.material3.navigation.suite)
|
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
|
|
|
testImplementation(libs.junit)
|
|
testImplementation(libs.robolectric)
|
|
testImplementation(libs.androidx.test.core)
|
|
testImplementation(libs.androidx.work.testing)
|
|
testImplementation(platform(libs.androidx.compose.bom))
|
|
testImplementation(libs.androidx.compose.ui.test.junit4)
|
|
testImplementation(libs.roborazzi)
|
|
testImplementation(libs.roborazzi.compose)
|
|
testImplementation(libs.roborazzi.junit.rule)
|
|
|
|
androidTestImplementation(libs.androidx.test.junit)
|
|
androidTestImplementation(libs.androidx.test.core)
|
|
androidTestImplementation(libs.androidx.test.runner)
|
|
androidTestImplementation(libs.androidx.work.testing)
|
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
|
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
|
}
|
|
|
|
ktlint {
|
|
version.set("1.5.0")
|
|
android.set(true)
|
|
ignoreFailures.set(false)
|
|
filter {
|
|
// UniFFI writes these; they are regenerated on every build.
|
|
exclude { it.file.path.contains("${File.separator}generated${File.separator}") }
|
|
// Vendored from rustls-platform-verifier. Restyling it would make the
|
|
// next sync with upstream a merge conflict for no benefit.
|
|
exclude { it.file.path.contains("${File.separator}org${File.separator}rustls${File.separator}") }
|
|
}
|
|
}
|
|
|
|
// Screenshot baselines live beside the tests that draw them, so a change to how
|
|
// a screen looks shows up in review as a changed image. `recordRoborazziDebug`
|
|
// rewrites them; `verifyRoborazziDebug` fails on any difference.
|
|
roborazzi {
|
|
outputDir.set(file("src/test/screenshots"))
|
|
}
|
|
|
|
// The generated UniFFI directory is a main source dir, so ktlint reads it even
|
|
// though the filter above drops every file in it. Without this, Gradle refuses
|
|
// the build for an undeclared dependency between the two tasks.
|
|
tasks.matching { it.name.startsWith("runKtlintCheckOver") || it.name.startsWith("runKtlintFormatOver") }
|
|
.configureEach { dependsOn(generateUniFfiBindings) }
|