import java.util.Properties plugins { id("com.android.application") id("org.jetbrains.kotlin.plugin.compose") } /* * CONTRACT: release-signing material is machine-local. Environment variables * take precedence over the private properties file so CI and release hosts can * inject secrets without copying them into the source tree. */ val releaseSigningProperties = Properties() val releaseSigningPropertiesPath = providers.environmentVariable("TRAINLOG_RELEASE_CREDENTIALS_FILE").orNull ?: "${System.getProperty("user.home")}/.config/trainlog/release-signing.properties" val releaseSigningPropertiesFile = file(releaseSigningPropertiesPath) if (releaseSigningPropertiesFile.isFile) { releaseSigningPropertiesFile.inputStream().use(releaseSigningProperties::load) } fun releaseSigningValue(environmentName: String, propertyName: String): String? = providers.environmentVariable(environmentName).orNull ?.takeIf { it.isNotBlank() } ?: releaseSigningProperties.getProperty(propertyName)?.takeIf { it.isNotBlank() } val releaseStoreFile = releaseSigningValue("TRAINLOG_RELEASE_STORE_FILE", "storeFile") val releaseStorePassword = releaseSigningValue("TRAINLOG_RELEASE_STORE_PASSWORD", "storePassword") val releaseKeyAlias = releaseSigningValue("TRAINLOG_RELEASE_KEY_ALIAS", "keyAlias") val releaseKeyPassword = releaseSigningValue("TRAINLOG_RELEASE_KEY_PASSWORD", "keyPassword") val releaseSigningComplete = listOf( releaseStoreFile, releaseStorePassword, releaseKeyAlias, releaseKeyPassword, ).all { !it.isNullOrBlank() } android { namespace = "com.labfytools.trainlog" compileSdk = 37 defaultConfig { applicationId = "com.labfytools.trainlog" minSdk = 26 targetSdk = 36 versionCode = 3 versionName = "0.1.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { if (releaseSigningComplete) { create("release") { storeFile = file(requireNotNull(releaseStoreFile)) storePassword = requireNotNull(releaseStorePassword) keyAlias = requireNotNull(releaseKeyAlias) keyPassword = requireNotNull(releaseKeyPassword) } } } buildTypes { getByName("release") { if (releaseSigningComplete) { signingConfig = signingConfigs.getByName("release") } } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { compose = true } sourceSets { getByName("main") { /* CONTRACT: repository-level equipment and body-zone manifests * are the canonical shared sources. Android must not fork either * taxonomy into Kotlin constants. */ assets.directories.add("../../catalog") /* Shared production golden inputs are read by Kotlin directly and * generated into the C runner; neither platform authors copies. */ assets.directories.add("../../tests/fixtures") } } testOptions { unitTests.isIncludeAndroidResources = true unitTests.all { it.systemProperty( "user.home", layout.buildDirectory .get() .asFile .absolutePath, ) } } } /* WHY: Android Gradle otherwise emits app-release-unsigned.apk successfully, * which is build evidence but can be mistaken for a publishable artifact. * INVARIANT: every task that packages a release fails before packaging unless * all four external values exist and the configured keystore is a regular * file. Gradle/apksigner still perform the cryptographic validation. */ tasks.matching { it.name == "packageRelease" }.configureEach { doFirst { if (!releaseSigningComplete) { throw GradleException( "Trainlog release signing is not configured. Set the four " + "TRAINLOG_RELEASE_* variables or provide the private " + "release-signing.properties file.", ) } val configuredStore = file(requireNotNull(releaseStoreFile)) if (!configuredStore.isFile) { throw GradleException("Trainlog release keystore does not exist: $configuredStore") } } } dependencies { val composeBom = platform("androidx.compose:compose-bom:2026.08.00") implementation(composeBom) androidTestImplementation(composeBom) testImplementation(composeBom) implementation( "androidx.activity:activity-compose:1.13.0" ) implementation( "androidx.compose.foundation:foundation" ) implementation("androidx.compose.material3:material3") implementation( "androidx.compose.ui:ui" ) implementation( "androidx.compose.ui:ui-tooling-preview" ) debugImplementation( "androidx.compose.ui:ui-tooling" ) debugImplementation("androidx.compose.ui:ui-test-manifest") testImplementation("junit:junit:4.13.2") testImplementation("androidx.test:core:1.7.0") testImplementation("org.robolectric:robolectric:4.16.1") testImplementation("androidx.compose.ui:ui-test-junit4") androidTestImplementation("androidx.test.ext:junit:1.3.0") androidTestImplementation("androidx.test:runner:1.7.0") androidTestImplementation("androidx.compose.ui:ui-test-junit4") }