背景
在前文中提及了模块 aar 化可提升编译效率。正好最近看到一篇文章(yechao-源码和AAR的依赖替换指南)描述了切换的实践过程,在文章最后提及了“灵活切换源码依赖和远端依赖”。让我想起在之前的公司中,也用到过源码与远端依赖的切换工具,其实现更加完善,具备 IDEA 插件来切换以及自动打包aar。
现在离开公司,没法用现成的了,那么是否可以自己撸一套简版的呢?说干就干。
需求
- 支持切换源码与远程依赖
- 支持自动打包aar并发布
需求分析
源码与远程依赖切换
既然是简版,切换的操作就先手动配置来实现吧。具体可参考文章中提到的方案来实现,即通过两个配置文件来实现 + substitute。
自动打包aar并发布
这里可拆分出两个问题:
-
何时触发打包发布
A: 触发时机,需要结合公司情况。这里想到的场景有两个:- 通过 github action 实现。在分支合并时,触发打包并发布
- 在 git 代码提交前(即 pre-commit),触发打包并发布
-
如何实现打包发布
A: 实现打包发布很简单。检测到修改的模块后,执行打包并发布至 maven 即可。
方案
此处触发打包发布时机采用 pre-commit 时.
整体思路如下:
开发
自动打包与发布
获取模块与gav坐标映射关系
在项目根目录中读取一个 module_aar.json 文件,该文件是有当前自定义 Gradle Plugin 生成,内部存储了模块与gav坐标的映射关系。文件内容格式如下:
module_aar.json 文件内容格式
[
{
"module": "accountApi",
"localPath": "account/api",
"gav": "com.BuildAAR.module:accountApi:0.0.3"
},
{
"module": "accountImpl",
"localPath": "account/impl",
"gav": "com.BuildAAR.module:accountImpl:0.0.3"
}
]
将读取的映射关系维护在 ConfigJsonHolder
中。
project.gradle.projectsEvaluated {
//解析 module_aar.json 获取模块源码与aar的映射
val moduleAARFile = File(rootProject.projectDir, "module_aar.json")
if (!moduleAARFile.exists()) return@projectsEvaluated
ConfigJsonHolder.instance.init(Gson().fromJson(moduleAARFile.readText(), object : TypeToken<List<ModuleAARConfig>>() {}.type))
//...
}
获取被修改的模块
可通过 git diff --cached --name-only
指令,获取暂存区中修改的文件名称。然后将获取的修改文件名称传入自定义 Gradle Task,与模块名称进行映射,获取到哪些模块修改了。
open class FindModifiedModuleTask: DefaultTask() {
@TaskAction
fun findModifiedModule() {
val moduleNames = project.subprojects.filter {
it.plugins.hasPlugin("com.android.library")
}
val modifiedModule = if (modifyFile.isNullOrEmpty()){
moduleNames.map { it.name }
}else {
modifyFile!!.split("\n").mapNotNull { path ->
return@mapNotNull moduleNames.find {
path.contains(it.rootProject.projectDir.toPath().relativize(it.projectDir.toPath()).toString())
}?.name
}.toSet()
}
}
//...
}
获取哪些模块需要升级
因为模块间存在依赖关系,所以当底层模块打包发布后,上层依赖也需要打包发布。例如: moduleA 依赖 moduleB,当 moduleB 发布改动并打包发布后,那对应 moduleA 中的依赖需要更新并重新打包发布。
针对这个问题,需要2步操作:1. 获取模块间依赖关系; 2. 根据依赖关系构建打包顺序。
获取模块间依赖关系
在自定义 Gradle Plugin 中,通过监听 projectsEvaluated()
可知项目所有模块都已评估完成,此时可获取所有模块的依赖关系。
project.gradle.projectsEvaluated {
//...
gradle.allprojects {
val subProject = this
if (subProject.name != rootProject.name) {
subProject.configurations.all {
//获取project方式依赖的关系
if (subProject.plugins.hasPlugin("com.android.library") && name.contains("implementation") || name.contains("compileOnly")) {
dependencies.filter { dep -> dep.group == rootProject.name }.forEach { dep ->
//模块被依赖的模块集合。如: moduleA 依赖 moduleB,则 dependenciesMap 存储为 moduleB = [moduleA]
dependenciesMap[dep.name] = dependenciesMap.getOrDefault(dep.name, emptySet()) + subProject.name
}
}
//...
}
}
}
}
构建打包顺序
获取到模块间依赖关系后,形成有向无环图,然后利用拓扑排序 + 上一步获取的被修改模块,构建出打包发布顺序。
open class FindModifiedModuleTask: DefaultTask() {
@Internal
var modifyFile: String?=null
@Internal
lateinit var dependencyMap: Map<String, Set<String>>
@TaskAction
fun findModifiedModule() {
//获取修改的模块
val moduleNames = project.subprojects.filter {
it.plugins.hasPlugin("com.android.library")
}
val modifiedModule = if (modifyFile.isNullOrEmpty()){
moduleNames.map { it.name }
}else {
modifyFile!!.split("\n").mapNotNull { path ->
return@mapNotNull moduleNames.find {
path.contains(it.rootProject.projectDir.toPath().relativize(it.projectDir.toPath()).toString())
}?.name
}.toSet()
}
//构建打包顺序
val needBuildModule = mutableSetOf<String>()
modifiedModule.forEach {
needBuildModule.add(it)
needBuildModule.addAll(dependencyMap[it] ?: emptyList())
}
val buildOder = LinkedList<String>()
val inDegree = mutableMapOf<String, Int>()
for (moduleName in needBuildModule) {
inDegree.putIfAbsent(moduleName, 0)
dependencyMap[moduleName]?.forEach {
inDegree[it] = inDegree.getOrDefault(it, 0) + 1
}
}
val queue = LinkedList<String>()
inDegree.forEach {
if (it.value == 0) {
queue.add(it.key)
}
}
while (queue.isNotEmpty()) {
val moduleName = queue.pop()
buildOder.add(moduleName)
dependencyMap[moduleName]?.forEach {
inDegree[it] = inDegree.getOrDefault(it, 0) - 1
if (inDegree[it] == 0) {
queue.add(it)
}
}
}
//直接输出,给 pre-commit 脚本使用
println("$buildOder")
}
}
打包 aar
在获取到打包顺序后,开始循环执行打包、发布。
打包操作没有什么特别的,直接执行 :$module:assembleRelease
指令即可。
每个模块打包结束后,接着执行发布操作。
发布 aar
发布操作有两个问题需要处理:
- 模块需要添加发布插件与配置
- 发布需要基于之前的版本进行+1
配置发布
既然已经做 Gradle Plugin 了,要是手动一个一个模块添加 maven-publish 插件岂不是很 low。我们可以直接在项目评估阶段注入即可。
project.gradle.afterProject {
val subProject = this
if (name != rootProject.name && plugins.hasPlugin("com.android.library")) {
plugins.apply("maven-publish")
//需要等到 components 就绪后才能注入 maven 发布配置
components.whenObjectAdded {
if (this.name != "release") return@whenObjectAdded
extensions.configure<PublishingExtension> {
publications {
create<MavenPublication>("releaseAar") {
groupId = applicationId
artifactId = this@afterProject.name
//这里只是占位
version = ConfigJsonHolder.instance.getConfig(subProject.name)?.gav?.split(":")?.lastOrNull()?: "0.0.1"
from(this@whenObjectAdded)
}
}
repositories {
mavenLocal()
}
}
}
}
}
执行发布
此处不能简单的执行 :$module:publish
操作。在执行发布前需要将 version 基于之前的版本 +1,并且在发布完成后需要记录模块与 gav 坐标的映射关系,为后面本地源码与远端依赖切换做准备。
tasks.register("configMaven") {
if (!subProject.hasProperty("module")) return@register
val moduleName = subProject.property("module")?.toString() ?: return@register
val moduleProject = subProject.allprojects.find { it.name == moduleName } ?: return@register
val moduleConfig = ConfigJsonHolder.instance.getConfig(moduleName)
//版本+1
moduleProject.extensions.getByType(PublishingExtension::class.java).apply {
(publications.getByName("releaseAar") as MavenPublication).let {
it.version = incrementVersion(it.version)
}
}
//发布aar
finalizedBy(moduleProject.tasks.findByName("publishToMavenLocal"))
//更新 ConfigHolder
val mavenVersion = moduleProject.extensions.getByType(PublishingExtension::class.java).let {
(it.publications.getByName("releaseAar") as MavenPublication).version
}
moduleProject.tasks.findByName("publishToMavenLocal")?.doLast {
if (moduleConfig == null) {
//新组件
ConfigJsonHolder.instance.addModuleConfig(ModuleAARConfig(moduleName, moduleProject.rootProject.projectDir.toPath().relativize(moduleProject.projectDir.toPath()).toString(), "${applicationId}:${moduleName}:${mavenVersion}"))
} else {
//旧组件,更新版本号
ConfigJsonHolder.instance.updateModuleConfig(moduleConfig.copy( gav = moduleConfig.gav.split(":").mapIndexed { index, s -> if (index == 2) mavenVersion else s }.joinToString(":")))
}
}
}
更新配置文件
按照构建顺序对所有模块打包、发布完成后,最新的模块与gav坐标映射关系已经维护在 ConfigJsonHolder
中。最后一步就是将最新的映射关系刷新到配置文件中。
tasks.register("updateConfig") {
doLast {
//更新 module_aar.json
if (ConfigJsonHolder.instance.getConfigList().isEmpty()) return@doLast
val moduleAARFile = File(project.projectDir, "module_aar.json")
if (!moduleAARFile.exists()) {
moduleAARFile.createNewFile()
}
//将 ConfigHolder 转成 json 覆盖写入文件
moduleAARFile.writeText(Gson().toJson(ConfigJsonHolder.instance.getConfigList()))
}
}
源码与远程依赖切换
读取配置文件
在上面'自动打包与发布'一节中已经获取了模块与gav坐标的映射关系。在切换时,就需要另一个配置文件,即哪些模块需要切换成源码。这里我们将配置定义在 local.properties
文件中,配置在此的好处是仅对自己有效,不会影响其他团队成员开发。
配置的关键字有两个 allLocalModule
和 localModule
。当 allLocalModule=true
时,所有模块全部切换成源码模式;当 allLocalModule=false & localModule=["$moduleName"]
,则将配置的模块切成源码模式,其他仍为远程依赖(即 gav 坐标)
project.gradle.projectsEvaluated {
//用一个工具类来读取 local.properties 文件
LocalPropertyUtil.load(rootProject.projectDir)
if (LocalPropertyUtil.checkNotExists() || !LocalPropertyUtil.checkKey("allLocalModule", "localModule")) {
//local.properties 不存在 or 没有配置allLocalModule 和 localModule => 全部切换成 aar
ModuleManager.addNeedToGavModule(rootProject.allprojects.map { it.name }.filter { it != rootProject.name })
} else if (LocalPropertyUtil.getProperty("allLocalModule") == "true") {
//全部切换成 源码
ModuleManager.addNeedToProjectModule(rootProject.allprojects.map { it.name }.filter { it != rootProject.name })
} else {
//获取配置的源码模块
val localModules = if (!LocalPropertyUtil.checkKey("localModule")) emptyList() else
Gson().fromJson<List<String>>(LocalPropertyUtil.getProperty("localModule"), object : TypeToken<List<String>>() {}.type)
//遍历所有模块,将不在配置或白名单的模块进行分组
rootProject.allprojects.map { it.name }.filter { it != rootProject.name }.partition { it !in excludeModules && it !in localModules}.let {
ModuleManager.addNeedToGavModule(it.first)
ModuleManager.addNeedToProjectModule(it.second)
}
}
}
执行切换
在上一步读取配置文件后,即可知道哪些模块需要切换成源码,哪些需要切换成 gav坐标依赖,这些模块都由 ModuleManager
来维护。那么下一步就是通过 substitute
进行替换即可。
project.gradle.projectsEvaluated {
gradle.allprojects {
val subProject = this
if (subProject.name != rootProject.name) {
subProject.configurations.all {
//依赖替换
resolutionStrategy.dependencySubstitution {
//源码 切换成 gav坐标依赖
ModuleManager.getAllGavModule().forEach { ConfigJsonHolder.instance.getConfig(it)?.gav?.let { gav ->
substitute(project(":$it")).using(module(gav))
}
}
//gav坐标依赖 切换成 源码
ModuleManager.getAllProjectModule().forEach {ConfigJsonHolder.instance.getConfig(it)?.gav?.let { gav ->
substitute(module(gav)).using(project(":$it"))
}
}
}
}
}
}
}
完整代码
踩坑记录
kotlin自定义Task编译报错
kotlin 中的类默认 final 修饰,在自定义 Gradle Task 时需要用 open 修饰,否则编译报错。
Could not create task of type 'MyTask'.
> Class Settings_gradle.MyTask is final.
maven-publish
配置注入时机
maven-publish
插件的配置时机需要在模块评估完成前注入(即 projectEvaluated() 触发前),否则编译报错。
Failed to apply plugin class 'org.gradle.api.publish.plugins.PublishingPlugin'.
> Cannot run Project.afterEvaluate(Action) when the project is already evaluated.
java-gradle-plugin
插件与maven发布配置问题
在Gradle 6.4及以后,可直接引入 java-gradle-plugin
插件来替代之前的 java
等插件,此插件自动实现 maven标注、java插件、gradleApi依赖。
在引入 java-gradle-plugin
和 maven-publish
插件实现发布操作时,正确的配置如下:
//插件配置
gradlePlugin{
plugins {
create("myPlugin") {
id = "com.stefan.plugin" //plugin id
implementationClass = "com.stefan.plugin.ModuleAarPlugin" // java-gradle-plugin 自动实现 META-INFO 配置
}
}
}
//发布配置
group = "com.stefan"
version = "1.0.0"
publishing {
//此处不能配置 publications{},否则 `java-gradle-plugin` 插件生成的 maven标注将失效。
repositories {
maven {
url = uri(layout.buildDirectory.dir("maven-repo"))
}
mavenLocal()
}
}
如果想自定义 artifactId,则需要配置两种。
但如果通过 plugin id (如: plugins { id("com.stefan.plugin") version "1.0.0" }
)方式引入,查找时仍用的 java-gradle-plugin
插件生成的 maven标注。
group = "com.stefan"
version = "1.0.0"
publishing {
publications {
create<MavenPublication>("maven") {
groupId = group as String
artifactId = "buildAAR"
version = version as String
from(components["java"])
}
// 生成插件标记的 publication
withType<MavenPublication>().configureEach {
if (name == "pluginMarkerMaven") {
artifactId = "com.stefan.plugin.gradle.plugin"
}
}
}
repositories {
maven {
url = uri(layout.buildDirectory.dir("maven-repo"))
}
mavenLocal()
}
}