diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15479af40..33c1182ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,6 +41,8 @@ jobs: - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Check code format run: ./gradlew spotlessCheck diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4550b2013..c9fcccd1c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,11 +42,18 @@ jobs: - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + + - name: Decode signing keystore + run: echo "$SIGNING_KEY" | base64 -d > "${{ runner.temp }}/antsy.keystore" + env: + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} - name: Build run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater env: - storeFileBase64: ${{ secrets.SIGNING_KEY }} + storeFile: ${{ runner.temp }}/antsy.keystore storePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyAlias: ${{ secrets.ALIAS }} keyPassword: ${{ secrets.KEY_PASSWORD }} @@ -93,10 +100,15 @@ jobs: with: cache-disabled: true + - name: Decode signing keystore + run: echo "$SIGNING_KEY" | base64 -d > "${{ runner.temp }}/antsy.keystore" + env: + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + - name: Build run: ./gradlew assembleFoss -Penable-updater env: - storeFileBase64: ${{ secrets.SIGNING_KEY }} + storeFile: ${{ runner.temp }}/antsy.keystore storePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyAlias: ${{ secrets.ALIAS }} keyPassword: ${{ secrets.KEY_PASSWORD }} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 380c4a708..25ac3afd9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,11 +1,13 @@ +import com.android.build.api.variant.ApplicationVariant +import com.android.build.api.variant.BuildConfigField import mihon.gradle.Config -import mihon.gradle.getBuildTime +import mihon.gradle.getCurrentTime import mihon.gradle.getLatestCommitCount import mihon.gradle.getLatestCommitSha +import mihon.gradle.getLatestCommitTime import mihon.gradle.tasks.ReplaceShortcutsPlaceholderTask import java.io.FileInputStream import java.util.Properties -import kotlin.io.encoding.Base64 plugins { alias(mihonx.plugins.android.application) @@ -36,9 +38,6 @@ android { versionCode = 30 versionName = "0.20.4" - buildConfigField("String", "COMMIT_COUNT", "\"${getLatestCommitCount()}\"") - buildConfigField("String", "COMMIT_SHA", "\"${getLatestCommitSha()}\"") - buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLatestCommitTime = false)}\"") buildConfigField("boolean", "TELEMETRY_INCLUDED", "${Config.includeTelemetry}") buildConfigField("boolean", "UPDATER_ENABLED", "${Config.enableUpdater}") @@ -46,14 +45,9 @@ android { } if (System.getenv("MIHON_GITHUB_RELEASE").toBoolean()) { - val tempStoreFile = file(System.getenv("RUNNER_TEMP")).resolve("antsy.keystore") - - val storeFileBytes = System.getenv("storeFileBase64").let(Base64::decode) - tempStoreFile.outputStream().use { it.write(storeFileBytes) } - signingConfigs { named("debug") { - storeFile = tempStoreFile + storeFile = file(System.getenv("storeFile")) storePassword = System.getenv("storePassword") keyAlias = System.getenv("keyAlias") keyPassword = System.getenv("keyPassword") @@ -75,7 +69,6 @@ android { buildTypes { val debug = getByName("debug") { applicationIdSuffix = ".dev" - versionNameSuffix = "-${getLatestCommitCount()}" isPseudoLocalesEnabled = true } val release = getByName("release") { @@ -87,8 +80,6 @@ android { isProfileable = true proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro") - - buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLatestCommitTime = true)}\"") } val commonMatchingFallbacks = listOf(release.name) @@ -105,11 +96,7 @@ android { applicationIdSuffix = ".debug" - versionNameSuffix = debug.versionNameSuffix - matchingFallbacks.addAll(commonMatchingFallbacks) - - buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLatestCommitTime = false)}\"") } create("benchmark") { initWith(release) @@ -337,7 +324,32 @@ dependencies { testImplementation(libs.kotlinx.coroutines.test) } +val latestCommitCount = getLatestCommitCount() +val latestCommitSha = getLatestCommitSha() +val latestCommitTime = getLatestCommitTime() +val currentTime = getCurrentTime() + +fun ApplicationVariant.buildConfigField(type: String, name: String, value: Provider) { + buildConfigFields?.put(name, value.map { BuildConfigField(type, it, null) }) +} + androidComponents { + onVariants { variant -> + val isUnstableBuild = variant.buildType == "debug" || variant.buildType == "nightly" + val buildTime = if (isUnstableBuild) currentTime else latestCommitTime + + variant.buildConfigField("String", "COMMIT_COUNT", latestCommitCount.map { "\"$it\"" }) + variant.buildConfigField("String", "COMMIT_SHA", latestCommitSha.map { "\"$it\"" }) + variant.buildConfigField("String", "BUILD_TIME", buildTime.map { "\"$it\"" }) + + if (isUnstableBuild) { + variant.outputs.forEach { output -> + val versionName = output.versionName.get() + output.versionName.set(latestCommitCount.map { "$versionName-$it" }) + } + } + } + onVariants { variant -> val resSource = variant.sources.res ?: return@onVariants diff --git a/gradle.properties b/gradle.properties index d933de472..f06c14e3a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,6 +4,7 @@ kotlin.code.style=official # Configuration should be synced with [/gradle/build-logic/gradle.properties] org.gradle.caching=true +org.gradle.configuration-cache=true org.gradle.configureondemand=true org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 org.gradle.parallel=true diff --git a/gradle/build-logic/gradle.properties b/gradle/build-logic/gradle.properties index b01389627..9457ef797 100644 --- a/gradle/build-logic/gradle.properties +++ b/gradle/build-logic/gradle.properties @@ -3,6 +3,7 @@ # Gradle properties are not passed to included builds # https://github.com/gradle/gradle/issues/2534 org.gradle.caching=true +org.gradle.configuration-cache=true org.gradle.configureondemand=true org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 org.gradle.parallel=true diff --git a/gradle/build-logic/src/main/kotlin/mihon/gradle/BuildInfo.kt b/gradle/build-logic/src/main/kotlin/mihon/gradle/BuildInfo.kt new file mode 100644 index 000000000..c33ab3de0 --- /dev/null +++ b/gradle/build-logic/src/main/kotlin/mihon/gradle/BuildInfo.kt @@ -0,0 +1,89 @@ +package mihon.gradle + +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.logging.Logging +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters +import org.gradle.process.ExecOperations +import java.io.ByteArrayOutputStream +import javax.inject.Inject +import kotlin.time.Clock +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.Instant + +private val logger = Logging.getLogger("mihon.gradle.BuildInfo") + +fun Project.getLatestCommitCount(): Provider { + return git("rev-list", "--count", "HEAD", default = provider { "0" }).map { it.first } +} + +fun Project.getLatestCommitSha(): Provider { + return git("rev-parse", "--short", "HEAD", default = provider { "unknown" }).map { it.first } +} + +fun Project.getLatestCommitTime(): Provider { + return git("log", "-1", "--format=%ct", default = getCurrentTime()).map { (value, isDefault) -> + if (isDefault) value else Instant.fromEpochSeconds(value.toLong()).toString() + } +} + +fun Project.getCurrentTime(): Provider { + return providers.of(CurrentTimeValueSource::class.java) {} +} + +private fun Project.git(vararg command: String, default: Provider): Provider> { + val workTree = rootProject.layout.projectDirectory + return providers.of(GitValueSource::class.java) { + parameters.command.set(listOf("git", *command)) + parameters.workTree.set(workTree) + parameters.default.set(default) + } +} + +private abstract class GitValueSource : ValueSource, GitValueSource.Parameters> { + + interface Parameters : ValueSourceParameters { + val command: ListProperty + val workTree: DirectoryProperty + val default: Property + } + + @get:Inject + abstract val execOperations: ExecOperations + + override fun obtain(): Pair { + val command = parameters.command.get() + val output = ByteArrayOutputStream() + val error = ByteArrayOutputStream() + + val reason = try { + val result = execOperations.exec { + commandLine = command + workingDir = parameters.workTree.get().asFile + standardOutput = output + errorOutput = error + isIgnoreExitValue = true + } + if (result.exitValue == 0) return output.toString().trim() to false + error.toString().trim().ifEmpty { "exited with code ${result.exitValue}" } + } catch (e: Exception) { + e.message?.trim().orEmpty().ifEmpty { e.toString() } + } + + val default = parameters.default.get() + logger.warn("`${command.joinToString(" ")}` failed, defaulting to \"$default\": $reason") + return default to true + } +} + +private abstract class CurrentTimeValueSource : ValueSource { + + override fun obtain(): String { + val now = Clock.System.now() + return (now - now.nanosecondsOfSecond.nanoseconds).toString() + } +} diff --git a/gradle/build-logic/src/main/kotlin/mihon/gradle/Commands.kt b/gradle/build-logic/src/main/kotlin/mihon/gradle/Commands.kt deleted file mode 100644 index 76dcb044b..000000000 --- a/gradle/build-logic/src/main/kotlin/mihon/gradle/Commands.kt +++ /dev/null @@ -1,43 +0,0 @@ -package mihon.gradle - -import org.gradle.api.Project -import kotlin.time.Clock -import kotlin.time.Duration.Companion.nanoseconds -import kotlin.time.Instant - -// Git is needed in your system PATH for these commands to work. -// If it's not installed, you can return a random value as a workaround -fun Project.getLatestCommitCount(): String { - return exec("git rev-list --count HEAD") - // return "1" -} - -fun Project.getLatestCommitSha(): String { - return exec("git rev-parse --short HEAD") - // return "1" -} - -/** - * @param useLatestCommitTime If `true`, the build time is based on the timestamp of the last Git commit; - * otherwise, the current time is used. Both are in UTC. - * @return An ISO 8601 formatted string representing the build time. - */ -fun Project.getBuildTime(useLatestCommitTime: Boolean): String { - return if (useLatestCommitTime) { - val epoch = exec("git log -1 --format=%ct").toLong() - Instant.fromEpochSeconds(epoch).toString() - } else { - val now = Clock.System.now() - (now - now.nanosecondsOfSecond.nanoseconds).toString() - } -} - -fun Project.exec(command: String): String { - return providers.exec { - commandLine = command.split(" ") - } - .standardOutput - .asText - .get() - .trim() -}