diff --git a/.github/workflows/gradle.yaml b/.github/workflows/gradle.yaml index 96475b37e..7b78a25aa 100644 --- a/.github/workflows/gradle.yaml +++ b/.github/workflows/gradle.yaml @@ -118,21 +118,15 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} run: cd gui && pnpm run build - - name: Decode keystore secret to file - env: - ANDROID_STORE_FILE: ${{ secrets.ANDROID_STORE_FILE }} - run: | - mkdir -p server/android/secrets/ - echo $ANDROID_STORE_FILE | base64 --decode > server/android/secrets/keystore.jks - - name: Build with Gradle run: ./gradlew :server:android:build env: + ANDROID_STORE_FILE: ${{ secrets.ANDROID_STORE_FILE }} ANDROID_STORE_PASSWD: ${{ secrets.ANDROID_STORE_PASSWD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWD: ${{ secrets.ANDROID_KEY_PASSWD }} - - name: Upload the Android Build Artifact + - name: Upload the Android build artifact uses: actions/upload-artifact@v5 with: # Artifact name @@ -154,6 +148,24 @@ jobs: files: | ./SlimeVR-android.apk + - name: Build Google Play release bundle + if: startsWith(github.ref, 'refs/tags/') + run: ./gradlew :server:android:bundleRelease + env: + ANDROID_STORE_FILE: ${{ secrets.ANDROID_GPLAY_STORE_FILE }} + ANDROID_STORE_PASSWD: ${{ secrets.ANDROID_GPLAY_STORE_PASSWD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_GPLAY_KEY_ALIAS }} + ANDROID_KEY_PASSWD: ${{ secrets.ANDROID_GPLAY_KEY_PASSWD }} + + - name: Upload the Google Play artifact + uses: actions/upload-artifact@v5 + if: startsWith(github.ref, 'refs/tags/') + with: + # Artifact name + name: 'SlimeVR-Android-GPDev' # optional, default is artifact + # A file, directory or wildcard pattern that describes what to upload + path: server/android/build/outputs/bundle/release/* + bundle-linux: strategy: matrix: diff --git a/server/android/build.gradle.kts b/server/android/build.gradle.kts index 4fbb07278..1c2628c14 100644 --- a/server/android/build.gradle.kts +++ b/server/android/build.gradle.kts @@ -5,8 +5,11 @@ * For more details take a look at the Java Libraries chapter in the Gradle * User Manual available at https://docs.gradle.org/6.3/userguide/java_library_plugin.html */ +import com.android.build.gradle.internal.tasks.BaseTask import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.util.Base64 plugins { kotlin("android") @@ -28,7 +31,7 @@ java { } } -tasks.register("copyGuiAssets") { +val copyGuiAssets = tasks.register("copyGuiAssets") { val target = layout.projectDirectory.dir("src/main/assets/web-gui") delete(target) from(rootProject.layout.projectDirectory.dir("gui/dist")) @@ -37,17 +40,45 @@ tasks.register("copyGuiAssets") { throw GradleException("You need to run \"pnpm run build\" on the gui folder first!") } } -tasks.register("validateKeyStore") { - val storeFile = android.buildTypes.getByName("release").signingConfig?.storeFile - // Only warn for now since this is run even when irrelevant - if (storeFile?.isFile != true) { - logger.error("Android KeyStore file does not exist or is not a file: ${storeFile?.path}") - } else if (storeFile.length() <= 0) { - logger.error("Android KeyStore file is empty: ${storeFile.path}") +tasks.preBuild { + dependsOn(copyGuiAssets) +} + +// Set up signing pre/post tasks +val preSign = tasks.register("preSign") { + dependsOn(writeTempKeyStore) +} +val postSign = tasks.register("postSign") { + finalizedBy(deleteTempKeyStore) +} +tasks.withType { + dependsOn(preSign) + finalizedBy(postSign) +} + +// Handle GitHub secret Android KeyStore files +val envKeyStore: String? = System.getenv("ANDROID_STORE_FILE")?.takeIf { it.isNotBlank() } +val tempKeyStore = project.layout.buildDirectory.file("tmp/keystore.tmp.jks").get().asFile +val writeTempKeyStore = tasks.register("writeTempKeyStore") { + if (envKeyStore != null) { + doLast { + tempKeyStore.apply { + ensureParentDirsCreated() + tempKeyStore.writeBytes(Base64.getDecoder().decode(envKeyStore)) + tempKeyStore.deleteOnExit() + } + } + finalizedBy(deleteTempKeyStore) + } else { + enabled = false } } -tasks.preBuild { - dependsOn(":server:android:copyGuiAssets", ":server:android:validateKeyStore") +val deleteTempKeyStore = tasks.register("deleteTempKeyStore") { + if (envKeyStore != null) { + delete(tempKeyStore) + } else { + enabled = false + } } tasks.withType { @@ -136,17 +167,30 @@ android { // Defines a user-friendly version name for your app. versionName = extra["gitVersionName"] as? String ?: "v0.0.0" - logger.lifecycle("Configured for SlimeVR Android version $versionName ($versionCode)") + logger.lifecycle("i: Configured for SlimeVR Android version \"$versionName\" ($versionCode).") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { - create("release") { - storeFile = file("./secrets/keystore.jks") - storePassword = System.getenv("ANDROID_STORE_PASSWD") - keyAlias = System.getenv("ANDROID_KEY_ALIAS") - keyPassword = System.getenv("ANDROID_KEY_PASSWD") + val inputKeyStore: File? = if (envKeyStore != null) { + logger.lifecycle("i: \"ANDROID_STORE_FILE\" environment variable found, using for signing config.") + tempKeyStore + } else { + file("secrets/keystore.jks").takeIf { it.canRead() && it.length() > 0 } + } + + if (inputKeyStore != null) { + logger.info("i: Configuring signing for Android KeyStore file: \"${inputKeyStore.path}\".") + + create("release") { + storeFile = inputKeyStore + storePassword = System.getenv("ANDROID_STORE_PASSWD") + keyAlias = System.getenv("ANDROID_KEY_ALIAS") ?: "key0" + keyPassword = System.getenv("ANDROID_KEY_PASSWD") + } + } else { + logger.warn("w: Android KeyStore file is not valid or not found, skipping signing.") } } @@ -168,7 +212,7 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) - signingConfig = signingConfigs.getByName("release") + signingConfig = signingConfigs.findByName("release") } }