mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-17 04:35:46 +00:00
Some checks failed
Apple / iOS simulator (push) Has been cancelled
Apple / tvOS simulator (push) Has been cancelled
Platform Verification / Android shared modules (push) Has been cancelled
Platform Verification / Android release smoke (push) Has been cancelled
Platform Verification / Android instrumentation (push) Has been cancelled
The TV flavor's minSdk 24 (from the desugaring commit) broke the TV manifest merge outright: libmpv declares minSdk 26 in its own manifest, and Gradle refuses to merge a lower app minSdk against it. Android 7.0/7.1 (API 24-25) TV devices are unsupported as a result — MPV is a hard dependency of the player backend, not something worth gating behind a runtime API check for two OS versions.
402 lines
15 KiB
Text
402 lines
15 KiB
Text
import java.util.Properties
|
|
import org.gradle.api.GradleException
|
|
import org.gradle.api.tasks.Exec
|
|
import org.gradle.api.tasks.testing.Test
|
|
|
|
plugins {
|
|
alias(libs.plugins.fluxa.android.application)
|
|
alias(libs.plugins.fluxa.android.compose)
|
|
alias(libs.plugins.fluxa.android.hilt)
|
|
alias(libs.plugins.ksp)
|
|
}
|
|
|
|
|
|
val localProperties = Properties().apply {
|
|
val localFile = rootProject.file("local.properties")
|
|
if (localFile.exists()) {
|
|
localFile.inputStream().use { load(it) }
|
|
}
|
|
}
|
|
|
|
fun secret(name: String, default: String = ""): String {
|
|
return providers.gradleProperty(name).orNull
|
|
?: System.getenv(name)
|
|
?: localProperties.getProperty(name, default)
|
|
}
|
|
|
|
android {
|
|
namespace = "com.fluxa.app"
|
|
compileSdk = 36
|
|
|
|
defaultConfig {
|
|
applicationId = "com.fluxa.app"
|
|
minSdk = 24
|
|
targetSdk = 35
|
|
versionCode = 700
|
|
versionName = "2.1.7"
|
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
|
|
buildConfigField("String", "TRAKT_CLIENT_ID", "\"${secret("TRAKT_CLIENT_ID")}\"")
|
|
buildConfigField("String", "TRAKT_CLIENT_SECRET", "\"${secret("TRAKT_CLIENT_SECRET")}\"")
|
|
buildConfigField("String", "SIMKL_CLIENT_ID", "\"${secret("SIMKL_CLIENT_ID")}\"")
|
|
buildConfigField("String", "ANILIST_CLIENT_ID", "\"${secret("ANILIST_CLIENT_ID")}\"")
|
|
buildConfigField("String", "NUVIO_SUPABASE_URL", "\"${secret("FLUXA_NUVIO_SUPABASE_URL")}\"")
|
|
buildConfigField("String", "NUVIO_SUPABASE_KEY", "\"${secret("FLUXA_NUVIO_SUPABASE_KEY")}\"")
|
|
|
|
}
|
|
|
|
testOptions {
|
|
unitTests.isReturnDefaultValues = true
|
|
}
|
|
|
|
splits {
|
|
abi {
|
|
isEnable = gradle.startParameter.taskNames.any { it.contains("release", ignoreCase = true) }
|
|
reset()
|
|
include("arm64-v8a", "armeabi-v7a", "x86")
|
|
isUniversalApk = false
|
|
}
|
|
}
|
|
|
|
bundle {
|
|
abi {
|
|
enableSplit = true
|
|
}
|
|
}
|
|
|
|
flavorDimensions += "device"
|
|
productFlavors {
|
|
create("mobile") {
|
|
dimension = "device"
|
|
minSdk = 30
|
|
applicationId = "com.fluxa.app.mobile"
|
|
buildConfigField("String", "DEVICE_FLAVOR", "\"mobile\"")
|
|
buildConfigField("Boolean", "IS_TV", "false")
|
|
}
|
|
create("tv") {
|
|
dimension = "device"
|
|
// libmpv requires API 26; Android TV 7.0/7.1 (API 24-25) devices are unsupported.
|
|
minSdk = 26
|
|
applicationId = "com.fluxa.app.tv"
|
|
buildConfigField("String", "DEVICE_FLAVOR", "\"tv\"")
|
|
buildConfigField("Boolean", "IS_TV", "true")
|
|
}
|
|
}
|
|
|
|
val releaseStoreFile = secret("FLUXA_RELEASE_STORE_FILE")
|
|
val releaseStorePassword = secret("FLUXA_RELEASE_STORE_PASSWORD")
|
|
val releaseKeyAlias = secret("FLUXA_RELEASE_KEY_ALIAS")
|
|
val releaseKeyPassword = secret("FLUXA_RELEASE_KEY_PASSWORD")
|
|
val hasReleaseSigningCredentials = listOf(
|
|
releaseStoreFile,
|
|
releaseStorePassword,
|
|
releaseKeyAlias,
|
|
releaseKeyPassword
|
|
).all { it.isNotBlank() }
|
|
|
|
signingConfigs {
|
|
if (hasReleaseSigningCredentials) {
|
|
create("release") {
|
|
storeFile = rootProject.file(releaseStoreFile)
|
|
storePassword = releaseStorePassword
|
|
keyAlias = releaseKeyAlias
|
|
keyPassword = releaseKeyPassword
|
|
}
|
|
}
|
|
}
|
|
|
|
buildTypes {
|
|
release {
|
|
if (hasReleaseSigningCredentials) {
|
|
signingConfig = signingConfigs.getByName("release")
|
|
} else {
|
|
signingConfig = signingConfigs.getByName("debug")
|
|
}
|
|
isMinifyEnabled = true
|
|
isShrinkResources = true
|
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
}
|
|
create("benchmark") {
|
|
initWith(getByName("release"))
|
|
signingConfig = signingConfigs.getByName("debug")
|
|
matchingFallbacks += listOf("release")
|
|
}
|
|
}
|
|
buildFeatures {
|
|
buildConfig = true
|
|
}
|
|
packaging {
|
|
jniLibs {
|
|
useLegacyPackaging = false
|
|
pickFirsts.add("**/*.so")
|
|
}
|
|
}
|
|
|
|
sourceSets {
|
|
getByName("main") {
|
|
jniLibs.srcDir(layout.buildDirectory.dir("generated/rustJniLibs"))
|
|
}
|
|
}
|
|
}
|
|
|
|
val rustCrateDir = rootProject.layout.projectDirectory.asFile.resolve("../fluxa-core").canonicalFile
|
|
val rustStreamingCrateDir = rootProject.layout.projectDirectory.asFile.resolve("../fluxa-core/fluxa-streaming-engine").canonicalFile
|
|
val rustJniOutputDir = layout.buildDirectory.dir("generated/rustJniLibs")
|
|
val androidNdkDir = providers.environmentVariable("ANDROID_NDK_HOME").orElse(
|
|
providers.environmentVariable("ANDROID_NDK_ROOT").orElse(
|
|
providers.provider {
|
|
val localSdk = rootProject.file("local.properties")
|
|
.takeIf { it.exists() }
|
|
?.readLines()
|
|
?.firstOrNull { it.startsWith("sdk.dir=") }
|
|
?.substringAfter("sdk.dir=")
|
|
listOfNotNull(
|
|
localSdk?.let { "$it/ndk/28.2.13676358" },
|
|
"${System.getProperty("user.home")}/Android/Sdk/ndk/28.2.13676358",
|
|
"${System.getProperty("user.home")}/Android/Sdk/ndk/27.1.12297006"
|
|
).firstOrNull { file(it).exists() }.orEmpty()
|
|
}
|
|
)
|
|
)
|
|
|
|
val rustAndroidTargets = listOf(
|
|
Triple("arm64-v8a", "aarch64-linux-android", "AARCH64_LINUX_ANDROID"),
|
|
Triple("armeabi-v7a", "armv7-linux-androideabi", "ARMV7_LINUX_ANDROIDEABI"),
|
|
Triple("x86", "i686-linux-android", "I686_LINUX_ANDROID"),
|
|
)
|
|
|
|
val rustProfile = if (gradle.startParameter.taskNames.any {
|
|
it.contains("release", ignoreCase = true) || it.contains("benchmark", ignoreCase = true)
|
|
}) "release" else "debug"
|
|
val rustCargoProfileArgs = if (rustProfile == "release") listOf("--release") else emptyList()
|
|
val rustHostTag = when {
|
|
org.gradle.internal.os.OperatingSystem.current().isMacOsX -> "darwin-x86_64"
|
|
org.gradle.internal.os.OperatingSystem.current().isWindows -> "windows-x86_64"
|
|
else -> "linux-x86_64"
|
|
}
|
|
val rustHostLibraryName = when {
|
|
org.gradle.internal.os.OperatingSystem.current().isMacOsX -> "libfluxa_core.dylib"
|
|
org.gradle.internal.os.OperatingSystem.current().isWindows -> "fluxa_core.dll"
|
|
else -> "libfluxa_core.so"
|
|
}
|
|
|
|
val rustTargetTasks = rustAndroidTargets.map { (abi, target, envName) ->
|
|
val taskName = "buildFluxaCore${abi.split("-", "_").joinToString("") { it.replaceFirstChar(Char::uppercaseChar) }}"
|
|
tasks.register<Exec>(taskName) {
|
|
group = "build"
|
|
description = "Builds the Fluxa Rust core for $abi."
|
|
workingDir = rustCrateDir
|
|
val linkerName = when (target) {
|
|
"armv7-linux-androideabi" -> "armv7a-linux-androideabi26-clang"
|
|
else -> "${target}26-clang"
|
|
}
|
|
commandLine("cargo", "build", "--target", target, *rustCargoProfileArgs.toTypedArray())
|
|
inputs.files(fileTree(rustCrateDir) {
|
|
exclude("target/**", ".git/**", ".agents/**", ".codex/**", "fluxa-streaming-engine/target/**")
|
|
})
|
|
outputs.file(rustJniOutputDir.map { it.file("$abi/libfluxa_core.so") })
|
|
doFirst {
|
|
val ndkDir = androidNdkDir.get()
|
|
if (ndkDir.isBlank() || !file(ndkDir).exists()) {
|
|
throw GradleException("Android NDK is required to build ../fluxa-core.")
|
|
}
|
|
val toolchainRoot = file("$ndkDir/toolchains/llvm/prebuilt/$rustHostTag")
|
|
val toolchainBin = toolchainRoot.resolve("bin")
|
|
val clang = file(toolchainBin.resolve(linkerName)).absolutePath
|
|
environment("CARGO_TARGET_${envName}_LINKER", clang)
|
|
environment("CC_$target", clang)
|
|
environment("CC_${target.replace("-", "_")}", clang)
|
|
val ar = file(toolchainBin.resolve("llvm-ar")).absolutePath
|
|
environment("AR_$target", ar)
|
|
environment("AR_${target.replace("-", "_")}", ar)
|
|
|
|
val clangTargetTriple = linkerName.removeSuffix("-clang")
|
|
val sysroot = toolchainRoot.resolve("sysroot")
|
|
val clangVersionDir = toolchainRoot.resolve("lib/clang").listFiles()
|
|
?.filter { it.isDirectory }
|
|
?.maxByOrNull { it.name.toIntOrNull() ?: -1 }
|
|
environment("LIBCLANG_PATH", toolchainRoot.resolve("lib").absolutePath)
|
|
environment(
|
|
"BINDGEN_EXTRA_CLANG_ARGS",
|
|
buildString {
|
|
append("--target=$clangTargetTriple")
|
|
append(" --sysroot=${sysroot.absolutePath}")
|
|
if (clangVersionDir != null) {
|
|
append(" -resource-dir=${clangVersionDir.absolutePath}")
|
|
}
|
|
}
|
|
)
|
|
}
|
|
doLast {
|
|
val builtLibrary = file("$rustCrateDir/target/$target/$rustProfile/libfluxa_core.so")
|
|
if (!builtLibrary.exists()) {
|
|
throw GradleException("Rust build did not produce ${builtLibrary.absolutePath}")
|
|
}
|
|
copy {
|
|
from(builtLibrary)
|
|
into(rustJniOutputDir.get().dir(abi))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tasks.register("buildFluxaCore") {
|
|
group = "build"
|
|
description = "Builds the Fluxa Rust core for Android JNI ABIs."
|
|
dependsOn(rustTargetTasks)
|
|
}
|
|
|
|
val rustStreamingTargetTasks = rustAndroidTargets.map { (abi, target, envName) ->
|
|
val taskName = "buildFluxaStreamingEngine${abi.split("-", "_").joinToString("") { it.replaceFirstChar(Char::uppercaseChar) }}"
|
|
tasks.register<Exec>(taskName) {
|
|
group = "build"
|
|
description = "Builds the Fluxa streaming engine for $abi."
|
|
workingDir = rustStreamingCrateDir
|
|
val linkerName = when (target) {
|
|
"armv7-linux-androideabi" -> "armv7a-linux-androideabi26-clang"
|
|
else -> "${target}26-clang"
|
|
}
|
|
commandLine("cargo", "build", "--target", target, *rustCargoProfileArgs.toTypedArray())
|
|
inputs.files(fileTree(rustStreamingCrateDir) {
|
|
exclude("target/**", ".git/**", ".agents/**", ".codex/**")
|
|
})
|
|
outputs.file(rustJniOutputDir.map { it.file("$abi/libfluxa_streaming_engine.so") })
|
|
doFirst {
|
|
val ndkDir = androidNdkDir.get()
|
|
if (ndkDir.isBlank() || !file(ndkDir).exists()) {
|
|
throw GradleException("Android NDK is required to build fluxa-streaming-engine.")
|
|
}
|
|
val toolchainBin = file("$ndkDir/toolchains/llvm/prebuilt/$rustHostTag/bin")
|
|
val clang = file(toolchainBin.resolve(linkerName)).absolutePath
|
|
environment("CARGO_TARGET_${envName}_LINKER", clang)
|
|
environment("CC_$target", clang)
|
|
environment("CC_${target.replace("-", "_")}", clang)
|
|
val ar = file(toolchainBin.resolve("llvm-ar")).absolutePath
|
|
environment("AR_$target", ar)
|
|
environment("AR_${target.replace("-", "_")}", ar)
|
|
}
|
|
doLast {
|
|
val builtLibrary = file("$rustCrateDir/target/$target/$rustProfile/libfluxa_streaming_engine.so")
|
|
if (!builtLibrary.exists()) {
|
|
throw GradleException("Rust build did not produce ${builtLibrary.absolutePath}")
|
|
}
|
|
copy {
|
|
from(builtLibrary)
|
|
into(rustJniOutputDir.get().dir(abi))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tasks.register("buildFluxaStreamingEngine") {
|
|
group = "build"
|
|
description = "Builds the Fluxa streaming engine for Android JNI ABIs."
|
|
dependsOn(rustStreamingTargetTasks)
|
|
}
|
|
|
|
|
|
tasks.matching { it.name == "preBuild" }.configureEach {
|
|
dependsOn("buildFluxaCore")
|
|
dependsOn("buildFluxaStreamingEngine")
|
|
}
|
|
|
|
tasks.withType<Test>().configureEach {
|
|
dependsOn(rootProject.tasks.named("buildFluxaCoreHost"))
|
|
dependsOn(rootProject.tasks.named("buildFluxaStreamingEngineHost"))
|
|
jvmArgs("-Djava.library.path=${rustCrateDir.resolve("target/debug").absolutePath}")
|
|
systemProperty(
|
|
"jna.library.path",
|
|
rustCrateDir.resolve("target/debug").absolutePath
|
|
)
|
|
systemProperty(
|
|
"fluxa.core.library.path",
|
|
rustCrateDir.resolve("target/debug/$rustHostLibraryName").absolutePath
|
|
)
|
|
}
|
|
|
|
val requestedTasks = gradle.startParameter.taskNames.map { it.lowercase() }
|
|
val requiresSignedRelease = requestedTasks.any { task ->
|
|
"release" in task && listOf("bundle", "package", "install", "publish").any { keyword -> keyword in task }
|
|
}
|
|
|
|
if (requiresSignedRelease && !listOf(
|
|
secret("FLUXA_RELEASE_STORE_FILE"),
|
|
secret("FLUXA_RELEASE_STORE_PASSWORD"),
|
|
secret("FLUXA_RELEASE_KEY_ALIAS"),
|
|
secret("FLUXA_RELEASE_KEY_PASSWORD")
|
|
).all { it.isNotBlank() }
|
|
) {
|
|
throw GradleException("Release signing credentials are required for release build tasks.")
|
|
}
|
|
|
|
dependencies {
|
|
// Submodules
|
|
implementation(project(":core"))
|
|
implementation(project(":shared"))
|
|
implementation(project(":data"))
|
|
implementation(project(":player"))
|
|
|
|
implementation(libs.androidx.core.ktx)
|
|
implementation(libs.androidx.documentfile)
|
|
testImplementation(libs.jna)
|
|
testImplementation(libs.okhttp.mockwebserver)
|
|
implementation(libs.androidx.activity.compose)
|
|
implementation(libs.bundles.coroutines)
|
|
implementation(libs.bundles.lifecycle)
|
|
implementation(libs.androidx.work.runtime)
|
|
implementation(libs.androidx.hilt.navigation.compose)
|
|
implementation(libs.androidx.hilt.work)
|
|
ksp(libs.androidx.hilt.compiler)
|
|
|
|
// Compose
|
|
implementation(platform(libs.androidx.compose.bom))
|
|
implementation(libs.androidx.compose.ui)
|
|
implementation(libs.androidx.compose.ui.graphics)
|
|
implementation(libs.androidx.compose.material3)
|
|
implementation(libs.androidx.compose.material.icons)
|
|
implementation(libs.androidx.compose.foundation)
|
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
|
implementation(libs.androidx.compose.runtime.tracing)
|
|
|
|
// TV
|
|
implementation(libs.androidx.tv.foundation)
|
|
implementation(libs.androidx.tv.material)
|
|
|
|
// Image loading
|
|
implementation(libs.bundles.coil3)
|
|
|
|
// Room
|
|
implementation(libs.androidx.room.runtime)
|
|
|
|
// Media3 (for ExoPlayer access and @UnstableApi)
|
|
implementation(libs.bundles.media3)
|
|
implementation(libs.media3.session)
|
|
|
|
// Serialization / networking
|
|
implementation(libs.retrofit.gson)
|
|
implementation(libs.okhttp.logging)
|
|
|
|
// CloudStream plugin host
|
|
implementation(libs.cloudstream) {
|
|
exclude(group = "org.mozilla", module = "rhino")
|
|
exclude(group = "com.github.AmarullisVFX", module = "newpipeextractor")
|
|
exclude(group = "com.github.AmaryllisVFX", module = "newpipeextractor")
|
|
exclude(group = "com.github.AmaryllisVFX.newpipeextractor")
|
|
exclude(group = "info.debatty", module = "java-string-similarity")
|
|
}
|
|
implementation(libs.jackson.databind)
|
|
implementation(libs.jackson.module.kotlin)
|
|
|
|
// Misc
|
|
implementation(libs.zxing)
|
|
implementation(libs.androidx.palette)
|
|
implementation(libs.androidx.profileinstaller)
|
|
implementation(libs.androidx.biometric)
|
|
implementation(libs.androidx.browser)
|
|
|
|
testImplementation(libs.junit)
|
|
androidTestImplementation(libs.junit)
|
|
androidTestImplementation(libs.androidx.test.runner)
|
|
androidTestImplementation(libs.androidx.test.junit)
|
|
}
|