Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions src/main/groovy/com/deploygate/gradle/plugins/Config.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,4 @@ class Config {
VERSION_NAME = "unavailable"
}
}

static boolean shouldOpenAppDetailAfterUpload() {
return System.getenv(DeployGatePlugin.ENV_NAME_OPEN_APP_DETAIL_AFTER_UPLOAD)
}
}
101 changes: 89 additions & 12 deletions src/main/groovy/com/deploygate/gradle/plugins/DeployGatePlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import static com.deploygate.gradle.plugins.internal.agp.AndroidGradlePlugin.and
import static com.deploygate.gradle.plugins.internal.agp.AndroidGradlePlugin.androidBundleTaskName
import static com.deploygate.gradle.plugins.internal.gradle.ProviderFactoryUtils.environmentVariable

import com.deploygate.gradle.plugins.artifacts.AabInfo
import com.deploygate.gradle.plugins.artifacts.ApkInfo
import com.deploygate.gradle.plugins.artifacts.DefaultPresetAabInfo
import com.deploygate.gradle.plugins.artifacts.DefaultPresetApkInfo
import com.deploygate.gradle.plugins.dsl.DeployGateExtension
Expand Down Expand Up @@ -55,26 +57,47 @@ class DeployGatePlugin implements Plugin<Project> {

setupExtension(project)

GradleCompat.init(project)

// the presence of the value is same to the existence of the directory.
// Defer file operations to execution time for Configuration Cache compatibility
Provider<String> credentialDirPathProvider = project.providers.systemProperty("user.home").map { home ->
File f = new File(home, '.dg')
(f.directory || f.mkdirs()) ? f.absolutePath : null
new File(home, '.dg').absolutePath
}

// Detect AGP version for HttpClient
def agpVersionProvider = project.providers.provider {
try {
def agpPlugin = project.plugins.findPlugin("com.android.application")
if (agpPlugin) {
return AndroidGradlePlugin.getVersionString(agpPlugin.class.classLoader)
}
} catch (Throwable ignored) {
}
return "unknown"
}

def httpClientProvider = project.gradle.sharedServices.registerIfAbsent("httpclient", HttpClient) { spec ->
spec.parameters.endpoint.set(environmentVariable(project.providers, "TEST_SERVER_URL").orElse(Config.getDEPLOYGATE_ROOT()))
spec.parameters.agpVersion.set(agpVersionProvider)
spec.parameters.pluginVersion.set(project.providers.provider { Config.VERSION })
spec.parameters.pluginVersionCode.set(project.providers.provider { Config.VERSION_CODE.toString() })
spec.parameters.pluginVersionName.set(project.providers.provider { Config.VERSION_NAME })
}

def localServerProvider = project.gradle.sharedServices.registerIfAbsent("httpserver", LocalServer) { spec ->
spec.parameters.httpClient.set(httpClientProvider)
spec.parameters.credentialsDirPath.set(credentialDirPathProvider)
}

// Use Provider API for configuration cache compatibility
def extension = project.extensions.getByName(EXTENSION_NAME) as DeployGateExtension
def appOwnerNameProvider = project.providers.provider { extension.appOwnerName }
def apiTokenProvider = project.providers.provider { extension.apiToken }
def endpointProvider = project.providers.provider { extension.endpoint }
def openBrowserProvider = environmentVariable(project.providers, ENV_NAME_OPEN_APP_DETAIL_AFTER_UPLOAD).map { it?.toBoolean() ?: false }

def loginTaskProvider = project.tasks.register(Constants.LOGIN_TASK_NAME, LoginTask) { task ->
task.explicitAppOwnerName.set(project.deploygate.appOwnerName)
task.explicitApiToken.set(project.deploygate.apiToken)
task.explicitAppOwnerName.set(appOwnerNameProvider)
task.explicitApiToken.set(apiTokenProvider)
task.credentialsDirPath.set(credentialDirPathProvider)
task.httpClient.set(httpClientProvider)
task.localServer.set(localServerProvider)
Expand All @@ -98,37 +121,47 @@ class DeployGatePlugin implements Plugin<Project> {
task.group = Constants.TASK_GROUP_NAME
}

project.deploygate.deployments.configureEach { NamedDeployment deployment ->
extension.deployments.configureEach { NamedDeployment deployment ->
project.tasks.named(Constants.SUFFIX_APK_TASK_NAME).configure { task ->
task.dependsOn(Constants.uploadApkTaskName(deployment.name))
}

project.tasks.named(Constants.SUFFIX_AAB_TASK_NAME).configure { task ->
task.dependsOn(Constants.uploadAabTaskName(deployment.name))
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[spotless] reported by reviewdog 🐶

Suggested change

project.tasks.register(Constants.uploadApkTaskName(deployment.name), UploadApkTask) { task ->
task.description = "Deploy assembled ${deployment.name} APK to DeployGate"
task.group = Constants.TASK_GROUP_NAME

if (!deployment.skipAssemble) {
task.logger.debug("${deployment.name} required assmble but ignored")
task.logger.debug("${deployment.name} required assemble but ignored")
}

task.credentials.set(loginTaskProvider.map { it.credentials })
task.deployment.copyFrom(deployment)
task.apkInfo.set(new DefaultPresetApkInfo(deployment.name))
task.httpClient.set(httpClientProvider)
task.endpoint.set(endpointProvider)
task.openBrowserAfterUpload.set(openBrowserProvider)
task.usesService(httpClientProvider)
task.dependsOn(loginTaskProvider)
}

project.tasks.register(Constants.uploadAabTaskName(deployment.name), UploadAabTask) { task ->
task.description = "Deploy bundled ${deployment.name} AAB to DeployGate"
task.group = Constants.TASK_GROUP_NAME

if (!deployment.skipAssemble) {
task.logger.debug("${deployment.name} required assmble but ignored")
task.logger.debug("${deployment.name} required assemble but ignored")
}

task.credentials.set(loginTaskProvider.map { it.credentials })
task.deployment.copyFrom(deployment)
task.aabInfo.set(new DefaultPresetAabInfo(deployment.name))
task.httpClient.set(httpClientProvider)
task.endpoint.set(endpointProvider)
task.openBrowserAfterUpload.set(openBrowserProvider)
task.usesService(httpClientProvider)
task.dependsOn(loginTaskProvider)
}
Expand All @@ -139,10 +172,13 @@ class DeployGatePlugin implements Plugin<Project> {
def variantProxy = new IApplicationVariantImpl(variant)

namedOrRegister(project, Constants.uploadApkTaskName(variantProxy.name), UploadApkTask).configure { task ->
task.description = "Deploy assembled ${variantProxy.name} APK to DeployGate"
task.credentials.set(loginTaskProvider.map { it.credentials })

task.apkInfo.set(variantProxy.packageApplicationTaskProvider().map {getApkInfo(it, variantProxy.name) })
task.apkInfo.set(createApkInfoProvider(variantProxy, agpVersionProvider))
task.httpClient.set(httpClientProvider)
task.endpoint.set(endpointProvider)
task.openBrowserAfterUpload.set(openBrowserProvider)
task.usesService(httpClientProvider)

if (deployment.skipAssemble.get()) {
Expand All @@ -153,10 +189,13 @@ class DeployGatePlugin implements Plugin<Project> {
}

namedOrRegister(project, Constants.uploadAabTaskName(variantProxy.name), UploadAabTask).configure { task ->
task.description = "Deploy bundled ${variantProxy.name} AAB to DeployGate"
task.credentials.set(loginTaskProvider.map { it.credentials })

task.aabInfo.set(variantProxy.packageApplicationTaskProvider().map {getAabInfo(it, variantProxy.name, project.buildDir) })
task.aabInfo.set(createAabInfoProvider(variantProxy, agpVersionProvider, project))
task.httpClient.set(httpClientProvider)
task.endpoint.set(endpointProvider)
task.openBrowserAfterUpload.set(openBrowserProvider)
task.usesService(httpClientProvider)

if (deployment.skipAssemble.get()) {
Expand All @@ -182,4 +221,42 @@ class DeployGatePlugin implements Plugin<Project> {
// TODO we should use ExtensionSyntax as the 1st argument but we need to investigate the expected side effects first.
project.extensions.create(DeployGateExtension, EXTENSION_NAME, DeployGateExtension, deployments)
}

/**
* Creates a provider for APK info that properly chains the package task provider
* with the AGP version provider for configuration cache compatibility.
*
* @param variantProxy The application variant proxy
* @param agpVersionProvider Provider for the AGP version string
* @return Provider for ApkInfo
*/
private static Provider<ApkInfo> createApkInfoProvider(
@NotNull IApplicationVariantImpl variantProxy,
@NotNull Provider<String> agpVersionProvider) {
return variantProxy.packageApplicationTaskProvider().flatMap { packageTask ->
agpVersionProvider.map { agpVersion ->
getApkInfo(packageTask, variantProxy.name, agpVersion)
}
}
}

/**
* Creates a provider for AAB info that properly chains the package task provider
* with the AGP version provider for configuration cache compatibility.
*
* @param variantProxy The application variant proxy
* @param agpVersionProvider Provider for the AGP version string
* @param project The project instance for accessing build directory
* @return Provider for AabInfo
*/
private static Provider<AabInfo> createAabInfoProvider(
@NotNull IApplicationVariantImpl variantProxy,
@NotNull Provider<String> agpVersionProvider,
@NotNull Project project) {
return variantProxy.packageApplicationTaskProvider().flatMap { packageTask ->
agpVersionProvider.map { agpVersion ->
getAabInfo(packageTask, variantProxy.name, project.layout.buildDirectory.get().asFile, agpVersion)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
package com.deploygate.gradle.plugins.artifacts

import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.jetbrains.annotations.NotNull
import org.jetbrains.annotations.Nullable

interface AabInfo {
@Input
@NotNull
String getVariantName()

@InputFile
@Optional
@PathSensitive(PathSensitivity.ABSOLUTE)
@Nullable
File getAabFile()
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
package com.deploygate.gradle.plugins.artifacts

import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.jetbrains.annotations.NotNull
import org.jetbrains.annotations.Nullable

interface ApkInfo {
@Input
@NotNull
String getVariantName()

@InputFile
@Optional
@PathSensitive(PathSensitivity.ABSOLUTE)
@Nullable
File getApkFile()

@Input
boolean isSigningReady()

@Input
boolean isUniversalApk()
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ class PackageAppTaskCompat {
}

@NotNull
static ApkInfo getApkInfo(@NotNull /* PackageApplication */ packageAppTask, @NotNull String variantName) {
static ApkInfo getApkInfo(@NotNull /* PackageApplication */ packageAppTask, @NotNull String variantName, @NotNull String agpVersion) {
// outputScope is retrieved by the reflection
Collection<String> apkNames = getApkNames(packageAppTask)
Collection<String> apkNames = getApkNames(packageAppTask, agpVersion)
File outputDir = getOutputDirectory(packageAppTask)
boolean isUniversal = apkNames.size() == 1
boolean isSigningReady = hasSigningConfig(packageAppTask)
boolean isSigningReady = hasSigningConfig(packageAppTask, agpVersion)

return new DirectApkInfo(
variantName,
Expand All @@ -25,12 +25,12 @@ class PackageAppTaskCompat {
}

@NotNull
static AabInfo getAabInfo(@NotNull /* PackageApplication */ packageAppTask, @NotNull String variantName, @NotNull File buildDir) {
static AabInfo getAabInfo(@NotNull /* PackageApplication */ packageAppTask, @NotNull String variantName, @NotNull File buildDir, @NotNull String agpVersion) {
final String aabName

// TODO Use Artifact API
// outputScope is retrieved by the reflection
Collection<String> apkNames = getApkNames(packageAppTask)
Collection<String> apkNames = getApkNames(packageAppTask, agpVersion)
aabName = ((String) apkNames[0]).replaceFirst("\\.apk\$", ".aab")

def outputDir = new File(buildDir, "outputs/bundle/${variantName}")
Expand All @@ -42,8 +42,8 @@ class PackageAppTaskCompat {
}

@PackageScope
static boolean hasSigningConfig(packageAppTask) {
if (AndroidGradlePlugin.isInternalSigningConfigData()) {
static boolean hasSigningConfig(packageAppTask, String agpVersion) {
if (AndroidGradlePlugin.isInternalSigningConfigData(agpVersion)) {
return packageAppTask.signingConfigVersions.any { it.exists() }
} else {
return packageAppTask.signingConfigData.resolve() != null
Expand All @@ -54,8 +54,8 @@ class PackageAppTaskCompat {
return packageAppTask.outputDirectory.getAsFile().get()
}

static Collection<String> getApkNames(packageAppTask) {
if (AndroidGradlePlugin.hasOutputsHandlerApiOnPackageApplication()) {
static Collection<String> getApkNames(packageAppTask, String agpVersion) {
if (AndroidGradlePlugin.hasOutputsHandlerApiOnPackageApplication(agpVersion)) {
return packageAppTask.outputsHandler.get().getOutputs { true }.collect { it.outputFileName }
} else {
return packageAppTask.variantOutputs.get().collect { it.outputFileName.get() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class DeployGateExtension implements ExtensionSyntax {

String appOwnerName

String endpoint = 'https://deploygate.com'

@NotNull
private final NamedDomainObjectContainer<NamedDeployment> deployments

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,17 @@ import org.jetbrains.annotations.NotNull
import org.slf4j.Logger

class AndroidGradlePlugin {
private static VersionString AGP_VERSION

static void ifPresent(@NotNull Project project, @NotNull Action<?> onFound) {
try {
def agpPlugin = project.plugins.findPlugin("com.android.application")

if (agpPlugin) {
AGP_VERSION = VersionString.tryParse(getVersionString(agpPlugin.class.classLoader))
checkModelLevel(agpPlugin.class.classLoader, project.logger)
onFound.execute("dummy")
} else {
project.plugins.matching { it.class.name == "com.android.build.gradle.AppPlugin" }.whenPluginAdded { Plugin plugin ->
project.logger.warn("com.android.application should be applied before DeployGate plugin")
AGP_VERSION = VersionString.tryParse(getVersionString(plugin.class.classLoader))
checkModelLevel(agpPlugin.class.classLoader, project.logger)
checkModelLevel(plugin.class.classLoader, project.logger)
onFound.execute("dummy")
}
}
Expand All @@ -31,27 +27,41 @@ class AndroidGradlePlugin {
}
}

static VersionString getVersion() {
if (!AGP_VERSION) {
AGP_VERSION = VersionString.tryParse(getVersionString())
}

return AGP_VERSION
/**
* Parses the AGP version string into a VersionString object.
* This method no longer caches the version in a static field to support configuration cache.
*
* @param versionString The AGP version string to parse (e.g., "8.1.0")
* @return Parsed VersionString object, or null if parsing fails
* @since 3.0.0
*/
static VersionString getVersion(@NotNull String versionString) {
return VersionString.tryParse(versionString)
}

/**
* Checks if AGP uses internal signing config data structure.
* This change was introduced in AGP 8.3.0.
*
* @param versionString The AGP version string
* @return true if AGP version is 8.3.0 or higher
* @since AGP 8.3.0 https://cs.android.com/android-studio/platform/tools/base/+/ff361912406f0eafc42b6ff2a293ee8a17ff77ee:build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/PackageAndroidArtifact.kt;dlc=c2e97e2ca61a5575ccfb48f9528a11c38d651841
*/
static boolean isInternalSigningConfigData() {
def version = getVersion()
static boolean isInternalSigningConfigData(@NotNull String versionString) {
def version = getVersion(versionString)
return version.major >= 8 && version.minor >= 3
}

/**
* Checks if AGP has the OutputsHandler API on PackageApplication task.
* This API was introduced in AGP 8.1.0.
*
* @param versionString The AGP version string
* @return true if AGP version is 8.1.0 or higher
* @since AGP 8.1.0 https://android.googlesource.com/platform/tools/base/+/da5cbdf59f91f7480a5d9615a20f766d19c6034a%5E%21/#F32
*/
static boolean hasOutputsHandlerApiOnPackageApplication() {
def version = getVersion()
static boolean hasOutputsHandlerApiOnPackageApplication(@NotNull String versionString) {
def version = getVersion(versionString)
return version.major >= 8 && version.minor >= 1
}

Expand All @@ -71,7 +81,7 @@ class AndroidGradlePlugin {
* @param classLoader might be based on AGP's class loader
* @return
*/
private static String getVersionString(ClassLoader classLoader) {
static String getVersionString(ClassLoader classLoader) {
try {
return classLoader.loadClass("com.android.Version").getField("ANDROID_GRADLE_PLUGIN_VERSION").get(null)
} catch (Throwable ignored) {
Expand Down
Loading
Loading