Skip to content

Commit

Permalink
fix(core.gradle-plugin): 修复AGP 3.1.0及更高版本的兼容性
Browse files Browse the repository at this point in the history
主要以黑盒自动化测试驱动本次代码修复。详见:
projects/test/gradle-plugin-agp-compat-test/README.md

AGPCompatImpl更理想的方式是按AGP版本拆分成多个文件,
但考虑到AGP版本号获取的是一个字符串,而且业务有可能使用一些beta等版本的AGP,
版本号匹配风险比较大。所以目前实现方式是try-catch的方式。

移除pom中对com.android.tools.build依赖的声明。
这个声明会把AGP依赖带入构建项目中,可能会导致项目声明的AGP版本不生效。

fix Tencent#757
  • Loading branch information
shifujun committed Jan 10, 2022
1 parent 68d54d9 commit 3916e9a
Show file tree
Hide file tree
Showing 20 changed files with 658 additions and 62 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/pr-check-gradle-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: gradle-plugin-agp-compat-test

on:
pull_request:
branches: [ master ]
paths:
- 'projects/sdk/core/gradle-plugin'

jobs:
gradle-plugin-agp-compat-test:
runs-on: ubuntu-latest
env:
DISABLE_TENCENT_MAVEN_MIRROR: true
steps:
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v3.x
- name: checkout
uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: revert gradle wrapper mirror setting
run: echo 'distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip' > gradle/wrapper/gradle-wrapper.properties
- name: revert gradle wrapper mirror setting
working-directory: projects/test/gradle-plugin-agp-compat-test
run: echo 'distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip' > gradle/wrapper/gradle-wrapper.properties
- name: test AGP compatibility when core.gradle-plugin changed
working-directory: projects/test/gradle-plugin-agp-compat-test
run: ./test.sh

- name: stop gradle deamon for actions/cache
run: ./gradlew --stop
2 changes: 0 additions & 2 deletions buildScripts/gradle/maven.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ publishing {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', 'org.jetbrains.kotlin', 'kotlin-stdlib-jdk7', kotlin_version))
dependencies.append(getDependencyNode('compile', 'com.android.tools.build', 'gradle', build_gradle_version))
dependencies.append(getDependencyNode('compile', coreGroupId, 'transform-kit', publicationVersion))
dependencies.append(getDependencyNode('compile', coreGroupId, 'transform', publicationVersion))
dependencies.append(getDependencyNode('compile', coreGroupId, 'runtime', publicationVersion))
Expand Down Expand Up @@ -243,7 +242,6 @@ publishing {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', 'org.jetbrains.kotlin', 'kotlin-stdlib-jdk7', kotlin_version))
dependencies.append(getDependencyNode('compile', 'com.android.tools.build', 'gradle', build_gradle_version))
dependencies.append(getDependencyNode('compile', 'org.javassist', 'javassist', '3.28.0-GA'))

def scm = root.appendNode('scm')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Tencent is pleased to support the open source community by making Tencent Shadow available.
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package com.tencent.shadow.core.gradle

import com.android.build.gradle.BaseExtension
import com.android.build.gradle.api.BaseVariantOutput
import com.android.build.gradle.internal.dsl.ProductFlavor
import org.gradle.api.Project
import org.gradle.api.Task
import java.io.File

/**
* 不同版本AGP的兼容层
*/
internal interface AGPCompat {
fun getManifestFile(processManifestTask: Task): File
fun getPackageForR(project: Project, variantName: String): String
fun addFlavorDimension(baseExtension: BaseExtension, dimensionName: String)
fun setProductFlavorDefault(productFlavor: ProductFlavor, isDefault: Boolean)
fun getProcessManifestTask(output: BaseVariantOutput): Task
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.tencent.shadow.core.gradle

import com.android.SdkConstants
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.api.BaseVariantOutput
import com.android.build.gradle.internal.dsl.ProductFlavor
import com.android.build.gradle.tasks.ProcessApplicationManifest
import com.android.build.gradle.tasks.ProcessMultiApkApplicationManifest
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.provider.Property
import java.io.File

internal class AGPCompatImpl : AGPCompat {

override fun getProcessManifestTask(output: BaseVariantOutput): Task =
try {
output.processManifestProvider.get()
} catch (e: NoSuchMethodError) {
output.processManifest
}

override fun getManifestFile(processManifestTask: Task) =
when (processManifestTask.javaClass.superclass.simpleName) {
"ProcessMultiApkApplicationManifest" -> {
(processManifestTask as ProcessMultiApkApplicationManifest)
.mainMergedManifest.get().asFile
}
"ProcessApplicationManifest" -> {
try {
(processManifestTask as ProcessApplicationManifest)
.mergedManifest.get().asFile
} catch (e: NoSuchMethodError) {
//AGP小于4.1.0
val dir =
processManifestTask.outputs.files.files
.first { it.parentFile.name == "merged_manifests" }
File(dir, SdkConstants.ANDROID_MANIFEST_XML)
}
}
"MergeManifests" -> {
val dir = try {// AGP 3.2.0
processManifestTask.outputs.files.files
.first { it.parentFile.parentFile.parentFile.name == "merged_manifests" }
} catch (e: NoSuchElementException) {
// AGP 3.1.0
processManifestTask.outputs.files.files
.first { it.path.contains("intermediates${File.separator}manifests${File.separator}full${File.separator}") }
}
File(dir, SdkConstants.ANDROID_MANIFEST_XML)
}
else -> throw IllegalStateException("不支持的Task类型:${processManifestTask.javaClass}")
}

override fun getPackageForR(project: Project, variantName: String): String {
val linkApplicationAndroidResourcesTask =
project.tasks.getByName("process${variantName.capitalize()}Resources")
return getStringFromProperty(
when {
linkApplicationAndroidResourcesTask.hasProperty("namespace") -> {
linkApplicationAndroidResourcesTask.property("namespace")
}
linkApplicationAndroidResourcesTask.hasProperty("originalApplicationId") -> {
linkApplicationAndroidResourcesTask.property("originalApplicationId")
}
linkApplicationAndroidResourcesTask.hasProperty("packageName") -> {
linkApplicationAndroidResourcesTask.property("packageName")
}
else -> throw IllegalStateException("不支持的AGP版本")
}
)
}

override fun addFlavorDimension(baseExtension: BaseExtension, dimensionName: String) {
val flavorDimensionList = baseExtension.flavorDimensionList
as MutableList<String>? // AGP 3.6.0版本可能返回null
if (flavorDimensionList != null) {
flavorDimensionList.add(dimensionName)
} else {
baseExtension.flavorDimensions(dimensionName)
}
}

override fun setProductFlavorDefault(productFlavor: ProductFlavor, isDefault: Boolean) {
try {
productFlavor.isDefault = isDefault
} catch (ignored: NoSuchMethodError) {
// AGP 3.6.0版本没有这个方法,就不设置了。
// 设置Default主要是为了IDE中的Build Variants上下文自动选择时不要选成插件,
// 以便在IDE直接运行插件apk模块时运行Normal版本
}
}

companion object {
fun getStringFromProperty(x: Any?): String {
return when (x) {
is String -> x
is Property<*> -> x.get() as String
else -> throw Error("不支持的AGP版本")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,25 @@

package com.tencent.shadow.core.gradle

import com.android.SdkConstants.ANDROID_MANIFEST_XML
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.tasks.ManifestProcessorTask
import com.android.build.gradle.tasks.ProcessApplicationManifest
import com.android.build.gradle.tasks.ProcessMultiApkApplicationManifest
import com.tencent.shadow.core.gradle.extensions.PackagePluginExtension
import com.tencent.shadow.core.manifest_parser.generatePluginManifest
import com.tencent.shadow.core.transform.ShadowTransform
import com.tencent.shadow.core.transform_kit.AndroidClassPoolBuilder
import com.tencent.shadow.core.transform_kit.ClassPoolBuilder
import org.gradle.api.*
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.provider.Property
import java.io.File
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.jvm.isAccessible

class ShadowPlugin : Plugin<Project> {

private lateinit var androidClassPoolBuilder: ClassPoolBuilder
private lateinit var contextClassLoader: ClassLoader
private lateinit var agpCompat: AGPCompat

override fun apply(project: Project) {
val baseExtension = getBaseExtension(project)
agpCompat = buildAgpCompat(project)
val baseExtension = project.extensions.getByName("android") as BaseExtension

//在这里取到的contextClassLoader包含运行时库(classpath方式引入的)shadow-runtime
contextClassLoader = Thread.currentThread().contextClassLoader
Expand Down Expand Up @@ -114,9 +107,9 @@ class ShadowPlugin : Plugin<Project> {
}
}.forEach { pluginVariant ->
val output = pluginVariant.outputs.first()
val processManifestTask = output.processManifestProvider.get()
val manifestFile = getManifestFile(processManifestTask)
val variantName = manifestFile.parentFile.name
val processManifestTask = agpCompat.getProcessManifestTask(output)
val manifestFile = agpCompat.getManifestFile(processManifestTask)
val variantName = pluginVariant.name
val outputDir = File(project.buildDir, "generated/source/pluginManifest/$variantName")

// 添加生成PluginManifest.java任务
Expand All @@ -125,7 +118,7 @@ class ShadowPlugin : Plugin<Project> {
it.inputs.file(manifestFile)
it.outputs.dir(outputDir).withPropertyName("outputDir")

val packageForR = getPackageForR(project, variantName)
val packageForR = agpCompat.getPackageForR(project, variantName)

it.doLast {
generatePluginManifest(
Expand All @@ -144,15 +137,15 @@ class ShadowPlugin : Plugin<Project> {
}

private fun addFlavorForTransform(baseExtension: BaseExtension) {
baseExtension.flavorDimensionList.add(ShadowTransform.DimensionName)
agpCompat.addFlavorDimension(baseExtension, ShadowTransform.DimensionName)
try {
baseExtension.productFlavors.create(ShadowTransform.NoShadowTransformFlavorName) {
it.dimension = ShadowTransform.DimensionName
it.isDefault = true
agpCompat.setProductFlavorDefault(it, true)
}
baseExtension.productFlavors.create(ShadowTransform.ApplyShadowTransformFlavorName) {
it.dimension = ShadowTransform.DimensionName
it.isDefault = false
agpCompat.setProductFlavorDefault(it, false)
}
} catch (e: InvalidUserDataException) {
throw Error("请在android{} DSL之前apply plugin: 'com.tencent.shadow.plugin'", e)
Expand Down Expand Up @@ -183,50 +176,9 @@ class ShadowPlugin : Plugin<Project> {
var useHostContext: Array<String> = emptyArray()
}

fun getBaseExtension(project: Project): BaseExtension {
val plugin = project.plugins.getPlugin(AppPlugin::class.java)
if (com.android.builder.model.Version.ANDROID_GRADLE_PLUGIN_VERSION == "3.0.0") {
val method = BasePlugin::class.declaredFunctions.first { it.name == "getExtension" }
method.isAccessible = true
return method.call(plugin) as BaseExtension
} else {
return project.extensions.getByName("android") as BaseExtension
}
}

companion object {
private fun getManifestFile(processManifestTask: ManifestProcessorTask) =
when (processManifestTask) {
is ProcessMultiApkApplicationManifest -> {
processManifestTask.mainMergedManifest.get().asFile
}
is ProcessApplicationManifest -> {
try {
processManifestTask.mergedManifest.get().asFile
} catch (e: NoSuchMethodError) {
//AGP小于4.1.0
val dir =
processManifestTask.outputs.files.files
.first { it.path.contains("merged_manifests") }
File(dir, ANDROID_MANIFEST_XML)
}
}
else -> throw IllegalStateException("不支持的Task类型:${processManifestTask.javaClass}")
}

private fun getPackageForR(project: Project, variantName: String): String {
val linkApplicationAndroidResourcesTask =
project.tasks.getByName("process${variantName.capitalize()}Resources")
return (when {

linkApplicationAndroidResourcesTask.hasProperty("namespace") -> {
linkApplicationAndroidResourcesTask.property("namespace")
}
linkApplicationAndroidResourcesTask.hasProperty("originalApplicationId") -> {
linkApplicationAndroidResourcesTask.property("originalApplicationId")
}
else -> throw IllegalStateException("不支持的AGP版本")
} as Property<String>).get()
private fun buildAgpCompat(project: Project): AGPCompat {
return AGPCompatImpl()
}
}

Expand Down
10 changes: 10 additions & 0 deletions projects/test/gradle-plugin-agp-compat-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
*.iml
.gradle
/local.properties
.idea
.DS_Store
/build
/captures
.externalNativeBuild
.gradletasknamecache

11 changes: 11 additions & 0 deletions projects/test/gradle-plugin-agp-compat-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## core.gradle-plugin模块的AGP各版本黑盒测试

准备一个桩工程`stub-project`,通过命令行参数控制其AGP版本和Shadow版本。

自动化测试脚本: `test.sh`。其中先编译Shadow,发布到本地Maven,然后用这个Shadow版本进行测试。

注意脚本会echo出执行的命令,如果遇到测试失败,可复制命令手工重新执行。

### 确定实际使用的AGP版本:

查看`stub-project/build/intermediates/app_metadata/pluginDebug/app-metadata.properties`
1 change: 1 addition & 0 deletions projects/test/gradle-plugin-agp-compat-test/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
16 changes: 16 additions & 0 deletions projects/test/gradle-plugin-agp-compat-test/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx4096m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useAndroidX=true
org.gradle.caching=false

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
#distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionUrl=https\://mirrors.tencent.com/gradle/gradle-7.0.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading

0 comments on commit 3916e9a

Please sign in to comment.