diff --git a/globallydynamic-android-lib/deps.gradle b/globallydynamic-android-lib/deps.gradle index e5dfdc5e..5592da14 100644 --- a/globallydynamic-android-lib/deps.gradle +++ b/globallydynamic-android-lib/deps.gradle @@ -24,7 +24,7 @@ def versions = [ play : '1.10.3', dynamicability : '1.0.17.300', autoservice : '1.0-rc6', - globallydynamic_gradle: '1.8.0', + globallydynamic_gradle: '1.9.0', uiautomator : '2.2.0', javadoc : '0.3.0' ] diff --git a/globallydynamic-android-lib/minimal-sample/build.gradle b/globallydynamic-android-lib/minimal-sample/build.gradle index 8151b0e2..0bcd011b 100644 --- a/globallydynamic-android-lib/minimal-sample/build.gradle +++ b/globallydynamic-android-lib/minimal-sample/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:8.6.0" - classpath "com.jeppeman.globallydynamic.gradle:plugin:1.8.0" + classpath "com.jeppeman.globallydynamic.gradle:plugin:1.9.0" } } diff --git a/globallydynamic-gradle-plugin/gradle.properties b/globallydynamic-gradle-plugin/gradle.properties index 01d57feb..117d5c4b 100644 --- a/globallydynamic-gradle-plugin/gradle.properties +++ b/globallydynamic-gradle-plugin/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.jeppeman.globallydynamic.gradle -VERSION_NAME=1.9.0-SNAPSHOT +VERSION_NAME=1.10.0-SNAPSHOT POM_DESCRIPTION=GloballyDynamic - Gradle plugin to facilitate for local dynamic delivery for Android. diff --git a/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasks.kt b/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasks.kt index a983f67d..3a11ad01 100644 --- a/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasks.kt +++ b/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasks.kt @@ -19,8 +19,8 @@ import com.google.common.util.concurrent.MoreExecutors import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonObject +import com.jeppeman.globallydynamic.gradle.extensions.* import com.jeppeman.globallydynamic.gradle.extensions.deleteCompletely -import com.jeppeman.globallydynamic.gradle.extensions.getTaskName import com.jeppeman.globallydynamic.gradle.extensions.unzip import org.gradle.api.DefaultTask import org.gradle.api.Project @@ -41,7 +41,8 @@ abstract class ApkProducerTask : DefaultTask() { private fun JsonObject.getPropertyCompat(propName: String) = get(propName) ?: get("m${propName.capitalize()}") - protected open val buildMode: BuildApksCommand.ApkBuildMode = BuildApksCommand.ApkBuildMode.DEFAULT + @get:Input + protected abstract val buildMode: BuildApksCommand.ApkBuildMode protected abstract fun processApkSet(apkSet: Path) @get:InputFiles @@ -54,7 +55,6 @@ abstract class ApkProducerTask : DefaultTask() { private set @get:OutputDirectory - @get:PathSensitive(PathSensitivity.ABSOLUTE) lateinit var outputDir: File protected set @@ -65,6 +65,7 @@ abstract class ApkProducerTask : DefaultTask() { @get:Nested abstract val aapt2: Aapt2Input + @get:Input lateinit var variantName: String private set @@ -141,19 +142,8 @@ abstract class ApkProducerTask : DefaultTask() { override fun execute(task: T) { task.variantName = applicationVariant.name task.signed = signed - task.bundleDir = task.project.buildDir - .toPath() - .resolve("intermediates") - .resolve("intermediary_bundle") - .resolve(applicationVariant.name) - .toFile() - task.signingConfig = task.project.buildDir - .toPath() - .resolve("intermediates") - .resolve("signing_config_data") - .resolve(applicationVariant.name) - .resolve("signing-config-data.json") - .toFile() + task.bundleDir = task.project.intermediaryBundleDir(applicationVariant) + task.signingConfig = task.project.intermediarySigningConfig(applicationVariant) createProjectServices(task.project).initializeAapt2Input(task.aapt2) } } @@ -196,6 +186,8 @@ abstract class BuildUniversalApkTask : ApkProducerTask() { } abstract class BuildBaseApkTask : ApkProducerTask() { + override val buildMode: BuildApksCommand.ApkBuildMode = BuildApksCommand.ApkBuildMode.DEFAULT + override fun processApkSet(apkSet: Path) { val tempDir = Paths.get(outputDir.absolutePath, "temp") tempDir.deleteCompletely() diff --git a/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/UploadBundleTask.kt b/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/UploadBundleTask.kt index 327b6896..e8764368 100644 --- a/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/UploadBundleTask.kt +++ b/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/UploadBundleTask.kt @@ -4,7 +4,7 @@ import com.android.build.gradle.api.ApplicationVariant import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonObject -import com.jeppeman.globallydynamic.gradle.extensions.getTaskName +import com.jeppeman.globallydynamic.gradle.extensions.* import com.jeppeman.globallydynamic.gradle.extensions.stackTraceToString import com.jeppeman.globallydynamic.gradle.extensions.toBase64 import org.apache.http.HttpException @@ -145,21 +145,8 @@ open class UploadBundleTask : DefaultTask() { task.applicationId = applicationVariant.applicationId task.version = applicationVariant.versionCode task.serverInfo = task.project.resolveServerInfo(extension) - task.bundleDir = task.project.buildDir - .toPath() - .resolve("intermediates") - .resolve("intermediary_bundle") - .resolve(applicationVariant.name) - .resolve(applicationVariant.getTaskName("package", "Bundle")) - .toFile() - task.signingConfig = task.project.buildDir - .toPath() - .resolve("intermediates") - .resolve("signing_config_data") - .resolve(applicationVariant.name) - .resolve(applicationVariant.getTaskName("signingConfigWriter")) - .resolve("signing-config-data.json") - .toFile() + task.bundleDir = task.project.intermediaryBundleDir(applicationVariant) + task.signingConfig = task.project.intermediarySigningConfig(applicationVariant) } } } diff --git a/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/extensions/Project.kt b/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/extensions/Project.kt new file mode 100644 index 00000000..aa823d78 --- /dev/null +++ b/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/extensions/Project.kt @@ -0,0 +1,22 @@ +package com.jeppeman.globallydynamic.gradle.extensions + +import com.android.build.gradle.api.ApplicationVariant +import org.gradle.api.Project +import java.io.File + +fun Project.intermediaryBundleDir(variant: ApplicationVariant): File = buildDir + .toPath() + .resolve("intermediates") + .resolve("intermediary_bundle") + .resolve(variant.name) + .resolve(variant.getTaskName("package", "Bundle")) + .toFile() + +fun Project.intermediarySigningConfig(variant: ApplicationVariant): File = buildDir + .toPath() + .resolve("intermediates") + .resolve("signing_config_data") + .resolve(variant.name) + .resolve(variant.getTaskName("signingConfigWriter")) + .resolve("signing-config-data.json") + .toFile() \ No newline at end of file diff --git a/globallydynamic-gradle-plugin/plugin/src/test/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasksTest.kt b/globallydynamic-gradle-plugin/plugin/src/test/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasksTest.kt new file mode 100644 index 00000000..8f42e548 --- /dev/null +++ b/globallydynamic-gradle-plugin/plugin/src/test/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasksTest.kt @@ -0,0 +1,222 @@ +package com.jeppeman.globallydynamic.gradle + +import com.google.common.truth.Truth.assertThat +import com.squareup.javapoet.ClassName +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.TypeSpec +import org.gradle.testkit.runner.TaskOutcome +import org.junit.jupiter.api.Test +import org.junit.platform.runner.JUnitPlatform +import org.junit.runner.RunWith +import java.nio.file.Paths +import java.util.zip.ZipFile +import kotlin.io.path.exists +import kotlin.io.path.fileSize + +abstract class ApkProducerTasksTest : BaseTaskTest() { + override val installTimeFeatureName: String = "installtimefeature" + override val onDemandFeatureName: String = "ondemandfeature" + + protected val appModuleOutputsDir get() = appModuleProjectDirPath.resolve("build").resolve("outputs") + + override fun beforeEach() { + val testResPath = Paths.get("src", "test", "resources") + val keyStoreFile = Paths.get(testResPath.toString(), "test.keystore") + val keyStorePath = rootProjectDirPath.resolve("test.keystore").apply { + toFile().writeBytes(keyStoreFile.toFile().inputStream().readBytes()) + } + val typeSpec = TypeSpec.classBuilder("GloballyDynamicActivity") + .superclass(ClassName.get("androidx.appcompat.app", "AppCompatActivity")) + .build() + + val stringsFile = appModuleSourceDir.resolve("res") + .resolve("values") + .apply { toFile().mkdirs() } + .resolve("strings.xml") + + stringsFile.toFile().writeText( + """ + + Install Time Feature + On Demand Feature + + """.trimIndent() + ) + + JavaFile.builder(BASE_PACKAGE_NAME, typeSpec) + .build() + .writeTo(appModuleSourceDir) + + appModuleAndroidManifestFilePath.toFile().writeText( + """ + + + + """.trimIndent() + ) + + appModuleBuildFilePath.toFile().writeText( + """ + plugins { + id 'com.android.application' + id 'com.jeppeman.globallydynamic' + } + + android { + namespace '$BASE_PACKAGE_NAME' + + compileSdk 32 + + defaultConfig { + minSdk 16 + targetSdk 32 + versionCode $VERSION_CODE + } + + signingConfigs { + debug { + storeFile file('$keyStorePath') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + + dynamicFeatures = [':$onDemandFeatureName', ':$installTimeFeatureName'] + } + + dependencies { + implementation 'com.jeppeman.globallydynamic.android:gplay:${ANDROID_LIB_VERSION}' + implementation 'androidx.appcompat:appcompat:1.4.2' + } + """.trimIndent() + ) + + onDemandFeatureAndroidManifestFilePath.toFile().writeText( + """ + + + + + + + + + + + + """.trimIndent() + ) + + installTimeFeatureAndroidManifestFilePath.toFile().writeText( + """ + + + + + + + + + + + + + + + + + + + + + + """.trimIndent() + ) + } +} + +private const val VERSION_CODE = 1 +private const val VARIANT = "debug" + +@RunWith(JUnitPlatform::class) +class BuildUniversalApkTaskTest : ApkProducerTasksTest() { + override val taskName: String = ":app:buildUniversalApkFor${VARIANT.capitalize()}" + + private val producedApk get() = appModuleOutputsDir.resolve("universal_apk") + .resolve(VARIANT) + .resolve("universal.apk") + + @Test + fun `task should produce a universal apk`() { + val result = runTask() + + assertThat(result.task(taskName)?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(producedApk.exists()).isTrue() + assertThat(producedApk.fileSize()).isGreaterThan(0L) + assertThat(ZipFile(producedApk.toFile()).entries().asSequence().any { it.name.matches("META-INF/.+\\.RSA".toRegex()) }).isTrue() + } +} + +@RunWith(JUnitPlatform::class) +class BuildUnsignedUniversalApkTaskTest : ApkProducerTasksTest() { + override val taskName: String = ":app:buildUnsignedUniversalApkFor${VARIANT.capitalize()}" + + private val producedApk get() = appModuleOutputsDir.resolve("universal_apk") + .resolve(VARIANT) + .resolve("universal-unsigned.apk") + + @Test + fun `task should produce an unsigned universal apk`() { + val result = runTask() + + assertThat(result.task(taskName)?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(producedApk.exists()).isTrue() + assertThat(producedApk.fileSize()).isGreaterThan(0L) + assertThat(ZipFile(producedApk.toFile()).entries().asSequence().any { it.name.matches("META-INF/.+\\.RSA".toRegex()) }).isFalse() + } +} + +@RunWith(JUnitPlatform::class) +class BuildBaseApkTaskTest : ApkProducerTasksTest() { + override val taskName: String = ":app:buildBaseApkFor${VARIANT.capitalize()}" + + private val producedApk get() = appModuleOutputsDir.resolve("base_apk") + .resolve(VARIANT) + .resolve("base-master.apk") + + @Test + fun `task should produce a base apk`() { + val result = runTask() + + assertThat(result.task(taskName)?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(producedApk.exists()).isTrue() + assertThat(producedApk.fileSize()).isGreaterThan(0L) + assertThat(ZipFile(producedApk.toFile()).entries().asSequence().any { it.name.matches("META-INF/.+\\.RSA".toRegex()) }).isTrue() + } +} + +@RunWith(JUnitPlatform::class) +class BuildUnsignedBaseApkTaskTest : ApkProducerTasksTest() { + override val taskName: String = ":app:buildUnsignedBaseApkFor${VARIANT.capitalize()}" + + private val producedApk get() = appModuleOutputsDir.resolve("base_apk") + .resolve(VARIANT) + .resolve("base-master-unsigned.apk") + + @Test + fun `task should produce an unsigned base apk`() { + val result = runTask() + + assertThat(result.task(taskName)?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(producedApk.exists()).isTrue() + assertThat(producedApk.fileSize()).isGreaterThan(0L) + assertThat(ZipFile(producedApk.toFile()).entries().asSequence().any { it.name.matches("META-INF/.+\\.RSA".toRegex()) }).isFalse() + } +}