mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
We were shipping a QR decoder we could not verify. io.github.zxing-cpp:android ships four .so files built by a third party on toolchains we cannot see, and nothing in this tree could check them -- while tools/arti-build holds libarti_android.so to a pinned-NDK, canonical-path, byte-for-byte reproducible standard. A QR scanner is a thing you point at a stranger's phone; there was no principled reason for the binary that parses the result to be the exempt one. tools/zxing-cpp-build mirrors tools/arti-build: the NDK revision is pinned (and is deliberately the same revision arti pins, so one install serves both), the upstream tag is pinned and cloned at that tag only, absolute paths are remapped, SOURCE_DATE_EPOCH comes from the tag's commit rather than from build time, and everyone builds at the same canonical path. Verified, not asserted: two clean builds of arm64-v8a produced identical bytes (dec4397c3e2f1e482b1905119dd4ea9e285f3420ffe4e66f38b2d22f54639dbf) and verify-reproducible.sh confirms they match what is committed. NDK discovery reads each candidate's source.properties and refuses anything but the pinned revision -- no wildcards, borrowing arti's r25b-vs-r27 lesson rather than re-learning it. After each build the script decodes the library's own .note.android.ident and fails unless the min SDK and NDK build number are what was asked for: the gate checks the input toolchain, the stamp checks the output, and only the second catches a stale CMake cache slipping a different compiler past the first. All four ABIs, unlike arti's two. Tor is optional and can be absent; a scanner that fails to load is a broken core feature, and what this replaced was pure Java that worked everywhere. Dropping the AAR means carrying the two things it supplied besides the binary: - Its Kotlin half, vendored verbatim at src/main/java/zxingcpp. The package and class name are load-bearing -- the library exports Java_zxingcpp_BarcodeReader_readYBuffer -- so it keeps both, its upstream Apache-2.0 header, and an exclusion from spotless so our MIT header is never stamped onto someone else's file. - Its consumer ProGuard rule. Without -keep class zxingcpp.**, R8 renames the class and the scanner fails to start in release builds only, with no build error and no warning. Not a size change: the published AAR's libraries are already stripped, and ours come out only marginally smaller. This buys verifiability. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134jvyriixNTHST4WRbbqbX
154 lines
5.8 KiB
Kotlin
154 lines
5.8 KiB
Kotlin
import com.android.build.gradle.tasks.GenerateResValues
|
|
import com.diffplug.gradle.spotless.SpotlessExtensionPredeclare
|
|
import java.util.Properties
|
|
|
|
// Local SonarQube analysis is opt-in: it activates only when `sonar.host.url`
|
|
// is present in local.properties (gitignored) AND a sonar task was requested,
|
|
// so neither developers who haven't opted in nor ordinary builds/IDE syncs of
|
|
// opted-in developers resolve or apply the scanner plugin. The Kotlin DSL
|
|
// compiles this buildscript {} section in an earlier stage that can't see the
|
|
// file's imports (hence the qualified Properties) or share code with the body,
|
|
// but it can publish values — the gate is computed once here and read below
|
|
// via the project's extra properties.
|
|
buildscript {
|
|
val localProperties = File(rootDir, "local.properties")
|
|
val sonarProperties =
|
|
java.util.Properties().apply {
|
|
if (localProperties.exists()) localProperties.inputStream().use { load(it) }
|
|
}
|
|
extra.set("sonarProperties", sonarProperties)
|
|
val sonarEnabled =
|
|
sonarProperties.getProperty("sonar.host.url") != null &&
|
|
gradle.startParameter.taskNames.any { it.substringAfterLast(":") in setOf("sonar", "sonarqube") }
|
|
extra.set("sonarEnabled", sonarEnabled)
|
|
if (sonarEnabled) {
|
|
repositories {
|
|
gradlePluginPortal()
|
|
}
|
|
dependencies {
|
|
// LGPL-3.0, build-time only — never linked into shipped artifacts.
|
|
classpath(libs.sonarqube.gradle.plugin)
|
|
}
|
|
}
|
|
}
|
|
|
|
plugins {
|
|
alias(libs.plugins.androidApplication) apply false
|
|
alias(libs.plugins.androidLibrary) apply false
|
|
alias(libs.plugins.jetbrainsKotlinJvm) apply false
|
|
alias(libs.plugins.androidBenchmark) apply false
|
|
alias(libs.plugins.diffplugSpotless)
|
|
alias(libs.plugins.googleServices) apply false
|
|
alias(libs.plugins.jetbrainsComposeCompiler) apply false
|
|
alias(libs.plugins.composeMultiplatform) apply false
|
|
alias(libs.plugins.kotlinMultiplatform) apply false
|
|
alias(libs.plugins.androidKotlinMultiplatformLibrary) apply false
|
|
alias(libs.plugins.serialization)
|
|
alias(libs.plugins.googleKsp) apply false
|
|
}
|
|
|
|
// Shared app version for all subprojects — read from gradle/libs.versions.toml.
|
|
// Android versionCode is the `appCode` entry in the same catalog (must be monotonic int).
|
|
// Desktop packageVersion inherits via project.version in desktopApp/build.gradle.kts.
|
|
val appVersion = libs.versions.app.get()
|
|
|
|
allprojects {
|
|
version = appVersion
|
|
|
|
configurations.configureEach {
|
|
resolutionStrategy.cacheChangingModulesFor(0, "seconds")
|
|
}
|
|
|
|
apply(plugin = "com.diffplug.spotless")
|
|
|
|
if (project === rootProject) {
|
|
spotless {
|
|
predeclareDeps()
|
|
}
|
|
configure<SpotlessExtensionPredeclare> {
|
|
kotlin {
|
|
ktlint("1.7.1")
|
|
}
|
|
}
|
|
} else {
|
|
spotless {
|
|
kotlin {
|
|
target("src/**/*.kt")
|
|
// Third-party sources vendored verbatim keep their own license header and
|
|
// formatting. Stamping our MIT header onto someone else's Apache-2.0 file
|
|
// would misstate its provenance, and reformatting it would make the next
|
|
// re-vendor a merge conflict instead of a copy.
|
|
targetExclude("src/main/java/zxingcpp/**/*.kt")
|
|
|
|
ktlint("1.7.1")
|
|
licenseHeaderFile(
|
|
rootProject.file(".spotless/copyright.kt"),
|
|
"@file:|package|import|class|object|sealed|open|interface|abstract ",
|
|
)
|
|
}
|
|
|
|
kotlinGradle {
|
|
target("*.gradle.kts")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
subprojects {
|
|
afterEvaluate {
|
|
try {
|
|
tasks.named("preBuild") {
|
|
dependsOn("spotlessApply")
|
|
}
|
|
} catch (ignored: UnknownTaskException) {
|
|
tasks.matching {
|
|
it.name.startsWith("pre") && it.name.endsWith("Build")
|
|
}.configureEach {
|
|
dependsOn("spotlessApply")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Second half of the opt-in local SonarQube support gated above in buildscript {}.
|
|
// All sonar.* entries in local.properties are forwarded as system properties, so
|
|
// `./gradlew sonar` behaves exactly like passing them via -Dsonar.xxx=... on the
|
|
// command line. sonar.projectKey/projectName default to the root project name
|
|
// ("Amethyst") and only need overriding in local.properties if desired.
|
|
val sonarEnabled = extra["sonarEnabled"] as Boolean
|
|
if (sonarEnabled) {
|
|
val sonarProperties = extra["sonarProperties"] as Properties
|
|
apply(plugin = "org.sonarqube")
|
|
|
|
sonarProperties
|
|
.stringPropertyNames()
|
|
.filter { it.startsWith("sonar.") }
|
|
.forEach { System.setProperty(it, sonarProperties.getProperty(it)) }
|
|
|
|
// The scanner's sonarResolver task reads AGP's generated-res-values provider
|
|
// but doesn't depend on the task that produces it — wire it up in every
|
|
// module that has both (today only :amethyst enables resValues, but the
|
|
// scanner defect is module-agnostic).
|
|
subprojects {
|
|
tasks.named { it == "sonarResolver" }.configureEach {
|
|
dependsOn(tasks.withType<GenerateResValues>())
|
|
}
|
|
}
|
|
}
|
|
|
|
val installGitHook = tasks.register<Copy>("installGitHook") {
|
|
val dotGit = File(rootProject.rootDir, ".git")
|
|
val hooksDir: File = if (dotGit.isFile) {
|
|
// Git worktree: .git is a file with "gitdir: <path>"
|
|
val gitDir = File(dotGit.readText().trim().replace("gitdir: ", ""))
|
|
File(gitDir, "hooks")
|
|
} else {
|
|
File(dotGit, "hooks")
|
|
}
|
|
from(File(rootProject.rootDir, ".git-hooks/pre-commit"))
|
|
from(File(rootProject.rootDir, ".git-hooks/pre-push"))
|
|
into(hooksDir)
|
|
filePermissions { unix("0777") }
|
|
}
|
|
tasks.getByPath(":amethyst:preBuild").dependsOn(installGitHook)
|