From cf43e6e4358556b7abfc8a52764bbfcbcf49bc8b Mon Sep 17 00:00:00 2001 From: davotoula Date: Wed, 8 Jul 2026 21:07:28 +0100 Subject: [PATCH] build: opt-in local SonarQube analysis via local.properties Adds a `sonar` Gradle target that activates only when `sonar.host.url` is present in local.properties (gitignored). Developers who don't opt in are unaffected: the scanner plugin is neither resolved nor applied, so no dependency downloads, no extra tasks, no config-time cost --- BUILDING.md | 62 +++++++++++++++++++++++++++++++++++++++ build.gradle.kts | 58 ++++++++++++++++++++++++++++++++++++ gradle/libs.versions.toml | 3 ++ 3 files changed, 123 insertions(+) diff --git a/BUILDING.md b/BUILDING.md index 7a85691bdf..30e464a2e4 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -213,6 +213,68 @@ toolchain drifted — file it before publishing. --- +## Local SonarQube analysis (opt-in) + +The build supports running a [SonarQube](https://www.sonarsource.com/products/sonarqube/) +analysis against a locally hosted server. It is **off by default**: unless you +opt in, the scanner plugin is neither downloaded nor applied and the build is +unaffected. + +### 1. Install and start a local SonarQube server + +Either run the official Docker image: + +```bash +docker run -d --name sonarqube -p 9000:9000 sonarqube:community +``` + +or download the [Community Build zip](https://www.sonarsource.com/products/sonarqube/downloads/), +unzip it, and start it (requires a JDK 17+ on `PATH`): + +```bash +cd sonarqube- +bin/macosx-universal-64/sonar.sh console # pick the folder matching your OS +``` + +Once it reports up, open (first login `admin`/`admin`, +you'll be asked to change it), create a **local project** named `Amethyst` with +project key `Amethyst`, and generate a **project analysis token** for it +(*Project Settings → Analysis Method → With Gradle*, or +*My Account → Security → Generate token*). The token looks like `sqp_…`. + +### 2. Point the build at your server + +Add the server and token to `local.properties` (gitignored — the token never +lands in the repo): + +```properties +sonar.host.url=http://localhost:9000 +sonar.token=sqp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +### 3. Run the analysis + +```bash +./gradlew sonar +``` + +When it finishes, browse the results at +. + +Every `sonar.*` entry in `local.properties` is forwarded to the scanner, so any +[analysis parameter](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/analysis-parameters/) +can be set there. `sonar.projectKey` / `sonar.projectName` default to the root +project name (`Amethyst`). + +Even when opted in, the scanner plugin only loads on invocations that actually +request the `sonar` task — ordinary builds and IDE syncs are unaffected (which +is also why `./gradlew tasks` doesn't list it). + +Note: the SonarQube Gradle scanner plugin is LGPL-3.0. It is a build-time-only +tool fetched after explicit opt-in; it is never linked into shipped artifacts. + +--- + ## Release runbook The release flow is driven by a tag push. Every cut ships Android + Desktop + diff --git a/build.gradle.kts b/build.gradle.kts index 3a9b1df6dd..6b87fbd049 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,36 @@ +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 `by extra`. +buildscript { + val localProperties = File(rootDir, "local.properties") + val sonarProperties by extra( + java.util.Properties().apply { + if (localProperties.exists()) localProperties.inputStream().use { load(it) } + }, + ) + val sonarEnabled by extra( + sonarProperties.getProperty("sonar.host.url") != null && + gradle.startParameter.taskNames.any { it.substringAfterLast(":") in setOf("sonar", "sonarqube") }, + ) + 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 @@ -73,6 +105,32 @@ subprojects { } } +// 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: Boolean by extra +if (sonarEnabled) { + val sonarProperties: Properties by extra + 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()) + } + } +} + val installGitHook = tasks.register("installGitHook") { val dotGit = File(rootProject.rootDir, ".git") val hooksDir: File = if (dotGit.isFile) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5fc22fe1fe..3d768731a6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,6 +62,7 @@ secp256k1KmpJniAndroid = "0.23.0" schnorr256k1Kmp = "1.0.5" securityCryptoKtx = "1.1.0" slf4j = "2.0.18" +sonarqubeGradlePlugin = "7.3.1.8318" spotless = "8.8.0" streamWebrtcAndroid = "1.3.10" translate = "17.0.3" @@ -210,6 +211,8 @@ secp256k1-kmp-common = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp", v secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" } secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" } schnorr256k1-kmp = { group = "com.vitorpamplona.schnorr256k1", name = "schnorr256k1-kmp", version.ref = "schnorr256k1Kmp" } +# Build-time only, gated behind the local.properties sonar opt-in in the root build script (LGPL-3.0). +sonarqube-gradle-plugin = { group = "org.sonarsource.scanner.gradle", name = "sonarqube-gradle-plugin", version.ref = "sonarqubeGradlePlugin" } stream-webrtc-android = { group = "io.getstream", name = "stream-webrtc-android", version.ref = "streamWebrtcAndroid" } unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" } play-services-cast-framework = { group = "com.google.android.gms", name = "play-services-cast-framework", version.ref = "playServicesCast" }