plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) alias(libs.plugins.ktfmt) } // See the root build script for why this style and not ktfmt's default. ktfmt { kotlinLangStyle() } // The CA this app pins is baked in at build time from the certificates on // the machine doing the build -- `$XDG_CONFIG_HOME/dev-updater/certs/ca.pem`, // which the server generates on first start. DEV_UPDATER_CA overrides it. // // Reading it rather than keeping a pasted copy in the source means the // trust anchor follows the build machine, the CA's private key never has // to exist anywhere near this repo, and regenerating a CA needs a rebuild // instead of a paste -- so a stale constant can't quietly disagree with // the server the app is trying to reach. val pinnedCaPath: String = System.getenv("DEV_UPDATER_CA") ?: "${System.getenv("XDG_CONFIG_HOME") ?: "${System.getProperty("user.home")}/.config"}" + "/dev-updater/certs/ca.pem" abstract class GeneratePinnedCert : DefaultTask() { /** Where the certificate is looked for, reported in failures. */ @get:Input abstract val caPath: Property /** * The certificate itself, set only when it exists -- so a missing one produces this task's own * instructions rather than Gradle's "no such input file", which doesn't say what to run. */ @get:InputFile @get:Optional @get:PathSensitive(PathSensitivity.NONE) abstract val caCertificate: RegularFileProperty /** Wired by AGP through `addGeneratedSourceDirectory`. */ @get:OutputDirectory abstract val outputDir: DirectoryProperty @TaskAction fun generate() { val path = caPath.get() val ca = File(path) if (!ca.isFile) { throw GradleException( "No CA certificate at $path.\n" + "Start the server once on this machine first -- it generates the CA the " + "app pins, and the certificate has to exist before an APK can embed it.\n" + "Set DEV_UPDATER_CA=/path/to/ca.pem to build against a different one." ) } val pem = ca.readText().trim() if (!pem.startsWith("-----BEGIN CERTIFICATE-----")) { throw GradleException("$path is not a PEM certificate.") } // The PEM must start immediately after the opening quotes: a // leading newline costs Android's CertificateFactory its // "-----BEGIN" sniff, so it tries DER instead and fails at runtime // with an ASN.1 decode error, nowhere near this file. val file = outputDir.get().file("PinnedCaCertificate.kt").asFile file.parentFile.mkdirs() file.writeText( """ |// Generated from $path by the generatePinnedCert task. Do not edit. |package com.example.devupdater | |const val PINNED_CA_PEM = ""${'"'}$pem |""${'"'} | """ .trimMargin() ) } } val generatePinnedCert = tasks.register("generatePinnedCert") { val ca = file(pinnedCaPath) caPath.set(pinnedCaPath) if (ca.isFile) { caCertificate.set(ca) } } // AGP 9 wants generated sources registered through the variant API rather // than added to a source set, so the task dependency is carried properly. androidComponents { onVariants { variant -> variant.sources.java?.addGeneratedSourceDirectory( generatePinnedCert, GeneratePinnedCert::outputDir, ) } } android { namespace = "com.example.devupdater" compileSdk = 37 defaultConfig { applicationId = "com.example.devupdater" minSdk = 24 targetSdk = 37 versionCode = 1 versionName = "1.0" } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } buildTypes { getByName("release") { isMinifyEnabled = false } } compileOptions { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } } dependencies { implementation(libs.compose.runtime) implementation(libs.compose.foundation) implementation(libs.compose.material3) implementation(libs.compose.ui) implementation(libs.androidx.activity.compose) // NotificationCompat and ServiceCompat, which are how the // version-specific parts of a foreground service get written once // rather than behind a check per call. Declared rather than inherited // transitively, since this module calls it directly. implementation(libs.androidx.core.ktx) implementation(libs.zxing.embedded) // The half of this app that is the same as ai-app's: pinned TLS, the // enrollment store, and the scanner. implementation(project(":link")) }