import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) } // Release signing is loaded from keystore.properties, which exists only on a machine // that owns the signing key (never committed — see .gitignore). F-Droid and CI have no // such file and build unsigned; that unsigned output is exactly what F-Droid's // reproducible-build check compares against before re-applying our signature. val keystorePropertiesFile = rootProject.file("keystore.properties") val keystoreProperties = Properties().apply { if (keystorePropertiesFile.exists()) keystorePropertiesFile.inputStream().use { load(it) } } android { namespace = "ca.ksamad.encore" compileSdk = 35 // Pin the build-tools to exactly what Nix supplies. Without this, AGP picks // its own default version and tries to auto-install it into the read-only // Nix SDK, which fails. Keep this in sync with flake.nix's buildToolsVersion. buildToolsVersion = "35.0.0" defaultConfig { applicationId = "ca.ksamad.encore" minSdk = 24 targetSdk = 35 versionCode = 1 versionName = "0.1.0" } signingConfigs { // Present only when the developer keystore is configured; absent on F-Droid/CI. if (keystorePropertiesFile.exists()) { create("release") { storeFile = file(keystoreProperties.getProperty("storeFile")) storePassword = keystoreProperties.getProperty("storePassword") keyAlias = keystoreProperties.getProperty("keyAlias") keyPassword = keystoreProperties.getProperty("keyPassword") } } } buildTypes { release { // Sign with the release key when configured; otherwise stay unsigned so // F-Droid can build/verify reproducibly and re-apply our signature. signingConfig = signingConfigs.findByName("release") // R8 code shrinking + resource shrinking. Critical with // material-icons-extended: it bundles thousands of vector icons, and // R8 strips everything we don't reference (debug builds can't shrink, // so the debug APK stays large — that's expected). isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" } buildFeatures { compose = true } testOptions { unitTests.all { // Surface `println`/stdout from unit tests in the Gradle console. // Handy for the gated MPD integration test (see MpdServerIntegrationTest). it.testLogging { showStandardStreams = true } } } } // --- Nix reproducible-build support ----------------------------------------- // The Nix dependency-capture step (`nixDownloadDeps`) resolves *every* resolvable // configuration at once to record what Gradle downloads. Several AGP/Kotlin // configurations aren't meant to be resolved that way and break the sweep: // * `*DependenciesMetadata` (Kotlin) resolve with no version under a BOM. // * the unit-/android-test classpaths carry a `:app -> :app` self-dependency // that is ambiguous without AGP's own artifact-view based resolution. // These tweaks apply ONLY during that capture step, detected via the // IN_GRADLE_UPDATE_DEPS env var the update script sets. Ordinary builds — in the // dev shell or the real `nix build` (assembleDebug) — see none of this and use // 100% standard Android configuration, including their own test resolution. if (System.getenv("IN_GRADLE_UPDATE_DEPS") == "1") { // Drop the test components entirely for the sweep, so their compile/runtime // classpaths (which carry the ambiguous `:app -> :app` self-dependency) are // never created. Removing the component is clean; merely marking those // classpaths non-resolvable instead makes the Kotlin plugin fail. androidComponents { beforeVariants(selector().all()) { variantBuilder -> variantBuilder.enableAndroidTest = false variantBuilder.enableUnitTest = false } } // Kotlin's `*DependenciesMetadata` configs resolve with no version under a // BOM; they pull nothing the real classpaths don't, so skip them. configurations.configureEach { if (name.endsWith("DependenciesMetadata")) { isCanBeResolved = false } } // With the unit-test component gone, still record the test-only artifacts // (JUnit) via a clean configuration that has no project self-dependency, so // the captured lockfile is complete enough for `testDebugUnitTest` to run // offline during `nix build`. val nixCaptureTestDeps = configurations.create("nixCaptureTestDeps") dependencies.addProvider(nixCaptureTestDeps.name, libs.junit) } dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.activity.compose) implementation(libs.kotlinx.coroutines.android) implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.media) implementation(libs.coil.compose) implementation(libs.androidx.palette) // The Compose BOM aligns every Compose artifact to one tested version set, // so the individual Compose deps below are declared without versions. implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.material.icons.extended) debugImplementation(libs.androidx.ui.tooling) testImplementation(libs.junit) }