feat(config): shared app.properties for app-wide constants

Add a single source of truth (root app.properties) for public app-wide
constants, injected at build time on both platforms instead of hardcoding.

- Apple: generate-appconfig.sh -> Generated/AppConfig.swift (wired into
  `make apple-app-config`), consumed as AppConfig.privacyPolicyURL.
- KMP: generateAppConfig task -> AppConfig.kt (mirrors DiagnosticsBuildConfig),
  consumed as AppConfig.PRIVACY_POLICY_URL.

Replaces the stale hardcoded privacy-policy URL on both sides with
https://vnidrop.sudosy.fr/privacy/.

Also fix the Apple release core build: disable release LTO in build-core.sh
(Cargo forbids lto in a build-override) to avoid the proc-macro
"mis-aligned LINKEDIT string pool" corruption, so release archives are
compact instead of shipping the debug core.

Update the app icon.

Tests: shell test for the generator (escaping, missing/duplicate key),
plus XCTest and jvmTest asserting the generated value matches app.properties.
This commit is contained in:
2026-08-01 19:54:37 +02:00
parent d52ac52cea
commit 232fb125d3
11 changed files with 308 additions and 66 deletions

View File

@@ -110,6 +110,55 @@ val generateDiagnosticsBuildConfig by tasks.registering {
}
}
// App-wide public constants (privacy policy URL, …) from the shared app.properties,
// so Apple and KMP read one source of truth instead of hardcoding values.
val appProperties = java.util.Properties().apply {
rootProject.file("app.properties").inputStream().use(::load)
}
val privacyPolicyUrl: String = appProperties.getProperty("PRIVACY_POLICY_URL")?.trim().orEmpty()
check(privacyPolicyUrl.isNotEmpty()) { "PRIVACY_POLICY_URL must be set in app.properties" }
val appConfigDir = layout.buildDirectory.dir("generated/appconfig/commonMain/kotlin")
val generateAppConfig by tasks.registering {
group = "build"
description = "Generates AppConfig from the shared app.properties"
val outputDir = appConfigDir
val privacy = privacyPolicyUrl
inputs.property("PRIVACY_POLICY_URL", privacy)
outputs.dir(outputDir)
doLast {
val packageDir = outputDir.get().asFile.resolve("com/vnidrop/app")
packageDir.mkdirs()
fun esc(value: String): String = buildString {
for (ch in value) {
when (ch) {
'\\' -> append("\\\\")
'"' -> append("\\\"")
'\n' -> append("\\n")
'\r' -> append("\\r")
'\t' -> append("\\t")
'$' -> append("\\$")
else -> append(ch)
}
}
}
packageDir.resolve("AppConfig.kt").writeText(
"""
|package com.vnidrop.app
|
|/**
| * Generated by shared/build.gradle.kts from the shared app.properties.
| * Single source of truth for app-wide public constants (also used by Apple).
| */
|object AppConfig {
| const val PRIVACY_POLICY_URL: String = "${esc(privacy)}"
|}
|
""".trimMargin(),
)
}
}
kotlin {
androidTarget {
compilerOptions {
@@ -122,6 +171,7 @@ kotlin {
sourceSets {
commonMain {
kotlin.srcDir(files(diagnosticsBuildConfigDir).builtBy(generateDiagnosticsBuildConfig))
kotlin.srcDir(files(appConfigDir).builtBy(generateAppConfig))
}
androidMain.dependencies {
implementation(libs.androidx.activity.compose)

View File

@@ -52,7 +52,7 @@ import vnidrop.shared.generated.resources.os_version_title
import vnidrop.shared.generated.resources.value_unavailable
import vnidrop.shared.generated.resources.version_title
private const val PrivacyPolicyUrl = "https://github.com/vnidrop/vnidrop"
private val PrivacyPolicyUrl = com.vnidrop.app.AppConfig.PRIVACY_POLICY_URL
@Composable
internal fun AboutSettings(

View File

@@ -0,0 +1,40 @@
package com.vnidrop.app
import java.io.File
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* Verifies the build-time [AppConfig] (generated from the shared `app.properties`)
* exposes the expected, well-formed values to the shared UI.
*/
class AppConfigTest {
@Test
fun privacyPolicyUrlIsTheExpectedHttpsEndpoint() {
assertTrue(AppConfig.PRIVACY_POLICY_URL.startsWith("https://"), "must be https")
assertEquals("https://vnidrop.sudosy.fr/privacy/", AppConfig.PRIVACY_POLICY_URL)
}
@Test
fun privacyPolicyUrlMatchesTheSharedConfigFile() {
// Cross-check the generated constant against the single source of truth so a
// broken codegen (or drift) is caught, not just a hardcoded copy.
assertEquals(privacyUrlFromAppProperties(), AppConfig.PRIVACY_POLICY_URL)
}
private fun privacyUrlFromAppProperties(): String {
var dir: File? = File(System.getProperty("user.dir")).absoluteFile
repeat(8) {
val candidate = File(dir, "app.properties")
if (candidate.isFile) {
return candidate.readLines()
.firstOrNull { it.startsWith("PRIVACY_POLICY_URL=") }
?.substringAfter("PRIVACY_POLICY_URL=")
?: error("PRIVACY_POLICY_URL missing in ${candidate.path}")
}
dir = dir?.parentFile
}
error("app.properties not found from ${System.getProperty("user.dir")}")
}
}