Refactor: Convert build configuration to Gradle plugin pattern

- Migrate ProjectConfig from static object to proper Gradle plugin
- Add version type support (beta/rc) to version.properties
- Clean up legacy fastlane changelogs
- Update release script to support version types
- Remove automatic fastlane changelog generation from release script
- Add BuildConfig field injection for version info
This commit is contained in:
darken
2025-10-07 13:54:24 +02:00
committed by Matthias Urhahn
parent 12f0662146
commit 221f3ccbed
52 changed files with 179 additions and 241 deletions
+14 -9
View File
@@ -1,4 +1,5 @@
plugins {
id("projectConfig")
id("com.android.application")
id("kotlin-android")
id("com.google.devtools.ksp")
@@ -9,28 +10,32 @@ apply(plugin = "dagger.hilt.android.plugin")
apply(plugin = "androidx.navigation.safeargs.kotlin")
android {
compileSdk = ProjectConfig.compileSdk
namespace = ProjectConfig.packageName
compileSdk = projectConfig.compileSdk
defaultConfig {
applicationId = ProjectConfig.packageName
namespace = projectConfig.packageName
minSdk = ProjectConfig.minSdk
targetSdk = ProjectConfig.targetSdk
minSdk = projectConfig.minSdk
targetSdk = projectConfig.targetSdk
versionCode = ProjectConfig.Version.code + 0 // Base app
versionName = ProjectConfig.Version.name
versionCode = projectConfig.version.code.toInt()
versionName = projectConfig.version.name
testInstrumentationRunner = "eu.darken.capod.HiltTestRunner"
buildConfigField("String", "PACKAGENAME", "\"${projectConfig.packageName}\"")
buildConfigField("String", "VERSION_CODE", "\"${projectConfig.version.code}\"")
buildConfigField("String", "VERSION_NAME", "\"${projectConfig.version.name}\"")
}
// Enable automatic per-app language preferences generation
androidResources {
@Suppress("UnstableApiUsage")
generateLocaleConfig = true
}
signingConfigs {
val basePath = File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}")
val basePath = File(System.getProperty("user.home"), ".appconfig/${projectConfig.packageName}")
create("releaseFoss") {
setupCredentials(File(basePath, "signing-foss.properties"))
}
@@ -94,7 +99,7 @@ android {
val variantName: String = variantOutputImpl.name
if (listOf("release", "beta").any { variantName.lowercase().contains(it) }) {
val outputFileName = ProjectConfig.packageName +
val outputFileName = projectConfig.packageName +
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
"-${variantName.uppercase()}.apk"
@@ -1,20 +1,22 @@
package eu.darken.capod.common
import eu.darken.capod.BuildConfig
import android.util.Log
import androidx.annotation.Keep
import java.lang.reflect.Field
// Can't be const because that prevents them from being mocked in tests
@Suppress("MayBeConstant")
@Keep
object BuildConfigWrap {
val DEBUG: Boolean = BuildConfig.DEBUG
val BUILD_TYPE: BuildType = when (val typ = BuildConfig.BUILD_TYPE) {
val APPLICATION_ID = getBuildConfigValue("PACKAGENAME") as String
val DEBUG: Boolean = getBuildConfigValue("DEBUG") as Boolean
val BUILD_TYPE: BuildType = when (val typ = getBuildConfigValue("BUILD_TYPE") as String) {
"debug" -> BuildType.DEV
"beta" -> BuildType.BETA
"release" -> BuildType.RELEASE
else -> throw IllegalArgumentException("Unknown buildtype: $typ")
}
@Keep
enum class BuildType {
DEV,
BETA,
@@ -22,24 +24,36 @@ object BuildConfigWrap {
;
}
val FLAVOR: Flavor = when (val flav = BuildConfig.FLAVOR) {
val FLAVOR: Flavor = when (val flav = getBuildConfigValue("FLAVOR") as String?) {
"gplay" -> Flavor.GPLAY
"foss" -> Flavor.FOSS
null -> Flavor.NONE
else -> throw IllegalStateException("Unknown flavor: $flav")
}
enum class Flavor {
GPLAY,
FOSS,
NONE,
;
}
val APPLICATION_ID: String = BuildConfig.APPLICATION_ID
val VERSION_CODE: Long = (getBuildConfigValue("VERSION_CODE") as String).toLong()
val VERSION_NAME: String = getBuildConfigValue("VERSION_NAME") as String
val VERSION_CODE: Long = BuildConfig.VERSION_CODE.toLong()
val VERSION_NAME: String = BuildConfig.VERSION_NAME
val VERSION_DESCRIPTION_LONG: String = "v$VERSION_NAME ($VERSION_CODE) ${FLAVOR}_$BUILD_TYPE"
val VERSION_DESCRIPTION_SHORT: String = "v$VERSION_NAME $FLAVOR"
val VERSION_DESCRIPTION: String = "v$VERSION_NAME ($VERSION_CODE) ~ $FLAVOR/$BUILD_TYPE"
val VERSION_DESCRIPTION_SHORT: String = "v$VERSION_NAME ~ $FLAVOR"
val VERSION_DESCRIPTION_TINY: String = "v$VERSION_NAME"
private fun getBuildConfigValue(fieldName: String): Any? = try {
val c = Class.forName("eu.darken.capod.BuildConfig")
val f: Field = c.getField(fieldName).apply {
isAccessible = true
}
f.get(null)
} catch (e: Exception) {
e.printStackTrace()
Log.e("getBuildConfigValue", "fieldName: $fieldName")
null
}
}
@@ -59,7 +59,7 @@ class RecorderModule @Inject constructor(
triggerFile.createNewFile()
log(TAG, INFO) { "Build.Fingerprint: ${Build.FINGERPRINT}" }
log(TAG, INFO) { "BuildConfig.Versions: ${BuildConfigWrap.VERSION_DESCRIPTION_LONG}" }
log(TAG, INFO) { "BuildConfig.Versions: ${BuildConfigWrap.VERSION_DESCRIPTION}" }
copy(
recorder = newRecorder
@@ -74,7 +74,7 @@ class RecorderActivityVM @Inject constructor(
}
fun share() = launch {
val (path, size) = resultCacheCompressedObs.first()
val (path, _) = resultCacheCompressedObs.first()
val intent = Intent(Intent.ACTION_SEND).apply {
val uri = FileProvider.getUriForFile(
@@ -89,7 +89,7 @@ class RecorderActivityVM @Inject constructor(
type = "application/zip"
addCategory(Intent.CATEGORY_DEFAULT)
putExtra(Intent.EXTRA_SUBJECT, "CAPod DebugLog - ${BuildConfigWrap.VERSION_DESCRIPTION_LONG})")
putExtra(Intent.EXTRA_SUBJECT, "CAPod DebugLog - ${BuildConfigWrap.VERSION_DESCRIPTION})")
putExtra(Intent.EXTRA_TEXT, "Your text here.")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
@@ -40,7 +40,7 @@ class SettingsIndexFragment : PreferenceFragment2() {
}
override fun onPreferencesCreated() {
findPreference<Preference>("core.changelog")!!.summary = BuildConfigWrap.VERSION_DESCRIPTION_LONG
findPreference<Preference>("core.changelog")!!.summary = BuildConfigWrap.VERSION_DESCRIPTION
findPreference<Preference>("core.privacy")!!.setOnPreferenceClickListener {
webpageTool.open(PrivacyPolicy.URL)
true
+10
View File
@@ -3,10 +3,20 @@ plugins {
`java-library`
}
gradlePlugin {
plugins {
create("projectConfigPlugin") {
id = "projectConfig"
implementationClass = "ProjectConfigPlugin"
}
}
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation("com.android.tools.build:gradle:8.13.0")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.10")
+43 -51
View File
@@ -1,64 +1,56 @@
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
import java.io.FileInputStream
import java.util.Properties
object ProjectConfig {
const val packageName = "eu.darken.capod"
open class ProjectConfig {
val packageName = "eu.darken.capod"
val minSdk = 26
const val minSdk = 26
const val compileSdk = 36
const val targetSdk = 36
val compileSdk = 36
val targetSdk = 36
object Version {
lateinit var version: Version
override fun toString(): String {
return "ProjectConfig($packageName, min=$minSdk, compile=$compileSdk, target=$targetSdk, version=$version)"
}
fun init(project: Project) {
val versionProperties = Properties().apply {
load(FileInputStream(File("version.properties")))
val propsPath = File(project.rootDir, "version.properties")
println("Version: From $propsPath:")
load(FileInputStream(propsPath))
println("$this")
}
val major = versionProperties.getProperty("project.versioning.major").toInt()
val minor = versionProperties.getProperty("project.versioning.minor").toInt()
val patch = versionProperties.getProperty("project.versioning.patch").toInt()
val build = versionProperties.getProperty("project.versioning.build").toInt()
version = Version(
major = versionProperties.getProperty("project.versioning.major").toInt(),
minor = versionProperties.getProperty("project.versioning.minor").toInt(),
patch = versionProperties.getProperty("project.versioning.patch").toInt(),
build = versionProperties.getProperty("project.versioning.build").toInt(),
type = versionProperties.getProperty("project.versioning.type"),
)
}
val name = "${major}.${minor}.${patch}-rc${build}"
val code = major * 10000000 + minor * 100000 + patch * 1000 + build * 10
data class Version(
val major: Int,
val minor: Int,
val patch: Int,
val build: Int,
val type: String,
) {
val name: String
get() = "${major}.${minor}.${patch}-$type${build}"
val code: Long
get() = major * 10000000 + minor * 100000 + patch * 1000 + build * 10L
}
}
fun lastCommitHash(): String = Runtime.getRuntime().exec("git rev-parse --short HEAD").let { process ->
process.waitFor()
val output = process.inputStream.use { input ->
input.bufferedReader().use {
it.readText()
}
class ProjectConfigPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("projectConfig", ProjectConfig::class.java)
extension.init(project)
project.afterEvaluate { println("ProjectConfigPlugin loaded: $extension") }
}
process.destroy()
output.trim()
}
fun com.android.build.api.dsl.SigningConfig.setupCredentials(
signingPropsPath: File? = null
) {
val keyStoreFromEnv = System.getenv("STORE_PATH")?.let { File(it) }
if (keyStoreFromEnv?.exists() == true) {
println("Using signing data from environment variables.")
storeFile = keyStoreFromEnv
storePassword = System.getenv("STORE_PASSWORD")
keyAlias = System.getenv("KEY_ALIAS")
keyPassword = System.getenv("KEY_PASSWORD")
} else {
println("Using signing data from properties file.")
val props = Properties().apply {
signingPropsPath?.takeIf { it.canRead() }?.let { load(FileInputStream(it)) }
}
val keyStorePath = props.getProperty("release.storePath")?.let { File(it) }
if (keyStorePath?.exists() == true) {
storeFile = keyStorePath
storePassword = props.getProperty("release.storePassword")
keyAlias = props.getProperty("release.keyAlias")
keyPassword = props.getProperty("release.keyPassword")
}
}
}
}
@@ -0,0 +1,75 @@
import com.android.build.api.dsl.SigningConfig
import org.gradle.api.Project
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.TestDescriptor
import org.gradle.api.tasks.testing.TestListener
import org.gradle.api.tasks.testing.TestResult
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
import java.io.File
import java.io.FileInputStream
import java.util.Properties
val Project.projectConfig: ProjectConfig
get() = extensions.findByType(ProjectConfig::class.java)!!
fun SigningConfig.setupCredentials(
signingPropsPath: File? = null
) {
val keyStoreFromEnv = System.getenv("STORE_PATH")?.let { File(it) }
if (keyStoreFromEnv?.exists() == true) {
println("Using signing data from environment variables.")
storeFile = keyStoreFromEnv
storePassword = System.getenv("STORE_PASSWORD")
keyAlias = System.getenv("KEY_ALIAS")
keyPassword = System.getenv("KEY_PASSWORD")
} else {
println("Using signing data from properties file.")
val props = Properties().apply {
signingPropsPath?.takeIf { it.canRead() }?.let { load(FileInputStream(it)) }
}
val keyStorePath = props.getProperty("release.storePath")?.let { File(it) }
if (keyStorePath?.exists() == true) {
storeFile = keyStorePath
storePassword = props.getProperty("release.storePassword")
keyAlias = props.getProperty("release.keyAlias")
keyPassword = props.getProperty("release.keyPassword")
}
}
}
fun Test.setupTestLogging() {
testLogging {
events(
TestLogEvent.FAILED,
TestLogEvent.PASSED,
TestLogEvent.SKIPPED,
// TestLogEvent.STANDARD_OUT,
)
exceptionFormat = TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
addTestListener(object : TestListener {
override fun beforeSuite(suite: TestDescriptor) {}
override fun beforeTest(testDescriptor: TestDescriptor) {}
override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {}
override fun afterSuite(suite: TestDescriptor, result: TestResult) {
if (suite.parent != null) {
val messages = """
------------------------------------------------------------------------------------------------
| ${result.resultType} ${result.testCount} tests: ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)
------------------------------------------------------------------------------------------------
""".trimIndent()
println(messages)
}
}
})
}
}
@@ -1,2 +0,0 @@
v1.0.0:
- Initial release.
@@ -1,4 +0,0 @@
v1.3.13(1031300)
• Updated translations
• Updated internal dependencies
• Tweaked placement of UI elements
@@ -1,4 +0,0 @@
v1.3.13(1031301)
• Updated translations
• Updated internal dependencies
• Tweaked placement of UI elements
@@ -1,4 +0,0 @@
v1.3.13(1031302)
• Updated translations
• Updated internal dependencies
• Tweaked placement of UI elements
@@ -1,4 +0,0 @@
v1.3.13(1031303)
• Updated translations
• Updated internal dependencies
• Tweaked placement of UI elements
@@ -1,4 +0,0 @@
v1.3.13(1031304)
• Updated translations
• Updated internal dependencies
• Tweaked placement of UI elements
@@ -1,4 +0,0 @@
v1.3.13(1031305)
• Updated translations
• Updated internal dependencies
• Tweaked placement of UI elements
@@ -1,4 +0,0 @@
v1.3.13(1031306)
• Updated translations
• Updated internal dependencies
• Tweaked placement of UI elements
@@ -1,4 +0,0 @@
v1.3.13(1031307)
• Updated translations
• Updated internal dependencies
• Tweaked placement of UI elements
@@ -1,2 +0,0 @@
Bugfixes and performance improvements.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes and performance improvements.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes and performance improvements.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes and performance improvements.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes and performance improvements.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes and performance improvements.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes and performance improvements.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes and performance improvements.
¯\_(ツ)_/¯
@@ -1,5 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
A detailed changelog is available on GitHub:
https://github.com/d4rken-org/capod/releases/latest
@@ -1,5 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
A detailed changelog is available on GitHub:
https://github.com/d4rken-org/capod/releases/latest
@@ -1,5 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
A detailed changelog is available on GitHub:
https://github.com/d4rken-org/capod/releases/latest
@@ -1,5 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
A detailed changelog is available on GitHub:
https://github.com/d4rken-org/capod/releases/latest
@@ -1,2 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
@@ -1 +0,0 @@
Support for AirPods Pro 2 with USB-C
@@ -1,2 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
@@ -1,2 +0,0 @@
Bugfixes, performance improvements and maybe new features.
¯\_(ツ)_/¯
@@ -1,5 +0,0 @@
Updates contain bugfixes, performance improvements and maybe new features.
A detailed changelog is always available on GitHub.
FYI: Its just me here, sometimes replies might take a bit. Sorry for that!
¯\_(ツ)_/¯
@@ -1,5 +0,0 @@
Updates contain bugfixes, performance improvements and maybe new features.
A detailed changelog is always available on GitHub.
FYI: Its just me here, sometimes replies might take a bit. Sorry for that!
¯\_(ツ)_/¯
@@ -1,5 +0,0 @@
Updates contain bugfixes, performance improvements and maybe new features.
A detailed changelog is always available on GitHub.
FYI: Its just me here, sometimes replies might take a bit. Sorry for that!
¯\_(ツ)_/¯
@@ -1,5 +0,0 @@
🐛 Bug fixes, 🚀 performance boosts, maybe even ✨ new features.
Changelog: https://capod.darken.eu/changelog
FYI: Its just me here — thanks for understanding if replies take a bit. ¯\_(ツ)_/¯
@@ -1,5 +0,0 @@
🐛 Bug fixes, 🚀 performance boosts, maybe even ✨ new features.
Changelog: https://capod.darken.eu/changelog
FYI: Its just me here — thanks for understanding if replies take a bit. ¯\_(ツ)_/¯
@@ -1,5 +0,0 @@
🐛 Bug fixes, 🚀 performance boosts, maybe even ✨ new features.
Changelog: https://capod.darken.eu/changelog
FYI: Its just me here — thanks for understanding if replies take a bit. ¯\_(ツ)_/¯
@@ -1,5 +0,0 @@
🐛 Bug fixes, 🚀 performance boosts, maybe even ✨ new features.
Changelog: https://capod.darken.eu/changelog
FYI: Its just me here — thanks for understanding if replies take a bit. ¯\_(ツ)_/¯
@@ -1,5 +0,0 @@
🐛 Bug fixes, 🚀 performance boosts, maybe even ✨ new features.
Changelog: https://capod.darken.eu/changelog
FYI: Its just me here — thanks for understanding if replies take a bit. ¯\_(ツ)_/¯
@@ -1,5 +0,0 @@
🐛 Bug fixes, 🚀 performance boosts, maybe even ✨ new features.
Changelog: https://capod.darken.eu/changelog
FYI: Its just me here — thanks for understanding if replies take a bit. ¯\_(ツ)_/¯
@@ -1,5 +0,0 @@
🐛 Bug fixes, 🚀 performance boosts, maybe even ✨ new features.
Changelog: https://capod.darken.eu/changelog
FYI: Its just me here — thanks for understanding if replies take a bit. ¯\_(ツ)_/¯
@@ -1,5 +0,0 @@
🐛 Bug fixes, 🚀 performance boosts, maybe even ✨ new features.
Changelog: https://capod.darken.eu/changelog
FYI: Its just me here — thanks for understanding if replies take a bit. ¯\_(ツ)_/¯
+5 -20
View File
@@ -308,6 +308,7 @@ do-version-properties() {
V_MINOR_REGEX='^([a-zA-Z\.]+minor)=([0-9]+)$'
V_PATCH_REGEX='^([a-zA-Z\.]+patch)=([0-9]+)$'
V_BUILD_REGEX='^([a-zA-Z\.]+build)=([0-9]+)$'
V_TYPE_REGEX='^([a-zA-Z\.]+type)=(rc|beta)$'
PROPS_FILE_NEW=""
@@ -333,6 +334,10 @@ do-version-properties() {
updated="${BASH_REMATCH[1]}=${V_BUILD_COUNTER}"
echo "Found build, replacing: $line -> $updated"
PROPS_FILE_NEW+=$updated
elif [[ $line =~ $V_TYPE_REGEX ]]; then
updated="${BASH_REMATCH[1]}=${V_BUILD_TYPE}"
echo "Found type, replacing: $line -> $updated"
PROPS_FILE_NEW+=$updated
else
PROPS_FILE_NEW+="$line"
fi
@@ -360,25 +365,6 @@ do-versionfile() {
git add VERSION
}
# Update a version file that can be parsed by third-parties, e.g. F-Droid
do-fastlane-changelog() {
CHANGELOGS_DIR="fastlane/metadata/android/en-US/changelogs"
NEW_CHANGELOG="$CHANGELOGS_DIR/$V_CODE.txt"
[ -f "$NEW_CHANGELOG" ] && return
echo -e "\n${S_NOTICE}Creating new default fastlane changelog file for $V_CODE...\n"
cp "$CHANGELOGS_DIR/default.txt" "$NEW_CHANGELOG"
cat "$NEW_CHANGELOG"
echo -e "\n\n${I_OK} ${S_NOTICE}${ACTION_MSG} [${S_NORM}$NEW_CHANGELOG${S_NOTICE}] file"
# Stage file for commit
git add "$CHANGELOGS_DIR/$V_CODE.txt"
}
# Does the release branch already exist?
check-branch-exist() {
[ "$FLAG_NOBRANCH" = true ] && return
@@ -467,7 +453,6 @@ echo -e "\n${S_LIGHT}––––––"
# Update steps
do-version-properties
do-versionfile
do-fastlane-changelog
do-branch
do-commit
create-tag "${V_NAME}" "${REL_NOTE}"
+1
View File
@@ -3,4 +3,5 @@ project.versioning.major=3
project.versioning.minor=0
project.versioning.patch=0
project.versioning.build=1
project.versioning.type=beta
#############################