开头
Monorepo 简单的说,是指将公司的所有代码放到一个 Git / Mercurial / Subversion 的代码仓库中。对于很多没听说过这个概念的人而言,无异于天方夜谭。Git 仓库不应该是每个项目一个吗?对于很多用 monorepo 的公司,他们的 Git 仓库中不止有自己的代码,还包括了很多的依赖。
我们的工程结构呢是monorepo
, 但是因为之前需要治理基础库的依赖问题,所以有一部分公共的sdk就被从momorepo
中移出去了。
但是这种东西移除去治理完了是好事,但是如果碰到bug啥的就不是特别方便调试,同时因为基本都是framework这种基础库被移动了,所以如果万一有方法变更改动,其实主工程就比较难感知到,万一有什么影响也比较难调查。
所以当时就接到了这么个活,想个办法把这些仓库移动会主仓中去。
方案
然后我仔细思考了好久,顺便评估了方案的可行性。虽然这个方案最后没有使用吧,但是我觉得还是蛮有意思的,可以在这里和大家简单的说下。
代码仓库在这里,因为偷懒所以这个并没有远端maven,有兴趣的就自己看下源代码好了呢 原理比较简单 GradleTask
其中有一个痛点在于,因为之前代码被抽离了momorepo
,所以依赖相关的版本已经出现了差异,如果可以的话我们大佬希望在大仓内编译的情况下使用大仓内的配置。而我则想要的是在大仓外的那种便利性,所以需要同时兼顾这两种特性。
通过group moduleName告别substitute
之前在这篇文章介绍过substitute Android组件化问题思考
demo 工程 RouterAndroid
之前在另外一篇文章介绍过这一点哦,每个module都是有 group: moudleName:version
构成的。
gradle原生其实是能直接使用这个属性的,但是有个前提就是远端一定要有这个仓库地址。下面是我在路由组件上做的实验。当一个module
我们可以先执行下依赖树 ./gradlew app:dependencies
kaptDebug
--- com.github.leifzhang:compiler:0.5.1 -> project :compiler
--- com.google.auto.service:auto-service:1.0-rc7
| --- com.google.auto.service:auto-service-annotations:1.0-rc7
| --- com.google.auto:auto-common:0.10
| | --- com.google.guava:guava:23.5-jre -> 27.0.1-jre
| | --- com.google.guava:failureaccess:1.0.1
| | --- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
| | --- com.google.code.findbugs:jsr305:3.0.2
| | --- org.checkerframework:checker-qual:2.5.2
| | --- com.google.errorprone:error_prone_annotations:2.2.0
| | --- com.google.j2objc:j2objc-annotations:1.1
| | --- org.codehaus.mojo:animal-sniffer-annotations:1.17
| --- com.google.guava:guava:27.0.1-jre (*)
--- com.squareup:javapoet:1.13.0
--- org.apache.commons:commons-lang3:3.9
--- org.apache.commons:commons-collections4:4.1
--- project :RouterAnnotation
| --- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.30
| --- org.jetbrains.kotlin:kotlin-stdlib:1.4.30
| | --- org.jetbrains.kotlin:kotlin-stdlib-common:1.4.30
| | --- org.jetbrains:annotations:13.0
| --- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.30
| --- org.jetbrains.kotlin:kotlin-stdlib:1.4.30 (*)
--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.30 (*)
以kapt
的configuration
为例哦, 我们可以看出来,如果远端能拉取到这个aar的话,当项目内本身就存在一个group moduleName
完全能匹配上的模块的情况下这个aar就会被替换成源代码了。
但是因为大家习惯问题,大部分人都是不会写group的,同时也会乱定义一个module的名字,比如叫library
这种,所以最好能规避就规避下。
但是如果我们在纯本地开发的情况下,这个module
并没有发布到远端的情况下,因为在同步阶段就会报错,所以这个时候就还是要进行手动干预。在项目完成gradle配置的时候就将远端依赖替换成本地的project。这个之前也说过,只需要在根build.gradle
配置如下代码即可。
allprojects {
configurations.all {
resolutionStrategy.dependencySubstitution.all {
if (requested is ModuleComponentSelector) {
val moduleRequested = requested as ModuleComponentSelector
val p = rootProject.allprojects.find { p ->
(p.group == moduleRequested.group && p.name == moduleRequested.module)
}
if (p != null) {
useTarget(project(p.path), "selected local project")
}
}
}
}
}
小贴士 这个是可以直接作用于include building进来的module的
以下就是完全调整完之后的dependencies
代码语言:javascript复制 --- com.github.leifzhang:RouterLib:0.5.1 -> project :RouterLib
| --- project :RouterAnnotation (*)
| --- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.30 (*)
--- com.github.leifzhang:secondmoudle:0.5.1 -> project :secondmoudle
| --- androidx.databinding:viewbinding:4.1.1 (*)
| --- org.jetbrains.kotlin:kotlin-stdlib:1.4.30 -> 1.4.31 (*)
| --- org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.4.30
| | --- org.jetbrains.kotlin:kotlin-stdlib:1.4.30 -> 1.4.31 (*)
| --- androidx.appcompat:appcompat:1.3.0 (*)
| --- com.github.leifzhang:RouterAnnotation:0.5.1 -> project :RouterAnnotation (*)
| --- com.github.leifzhang:RouterLib:0.5.1 -> project :RouterLib (*)
| --- com.github.leifzhang:CoroutineSupport:0.5.1 -> project :CoroutineSupport
| --- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0 -> 1.4.3 (*)
| --- org.jetbrains.kotlin:kotlin-stdlib:1.4.30 -> 1.4.31 (*)
| --- androidx.core:core-ktx:1.3.2 -> 1.5.0 (*)
| --- androidx.appcompat:appcompat:1.2.0 -> 1.3.0 (*)
| --- project :RouterAnnotation (*)
| --- project :RouterLib (*)
Gradle-Repo 改造计划
先抛出问题,带着问题去看题解,include
和includeBuilding
之前的差别是什么?
include
将当前工程module
引入到project
内进行编译, 然后会使用当前project
的属性,比如ext还有公共配置,properties,文件夹路径等等。
includeBuilding
则是将该路径的project和当前工程进行混合编译,之后两个project都是相对独立的,同时也无法直接引用到对方的子module
。
所以从上述描述上来说哦,当两个project的相似度很高的情况下,比如有全局的ext,还有很多全局配置,configuration等等之类的情况下,我们可以直接采取include
的形式将module
引入。同时对于超大型工程来说,include
有一个得天独厚的优势,那么就是特别是在AGP
版本升级这种时候,因为使用的是主工程的project配置,所以基本就不用担心了。
但是如果当前的module
依赖了一些特殊的插件,在壳工程内没有定义,或者是一些基础配置在壳工程内不存在的情况下,那么这个被include
进来的module,就会出现各种奇奇怪怪的配置错误问题,所以失去了原有仓库内的独立性。
那么另外一种情况就是如果两个工程间的ext全局属性等等差异比较大的情况下,可以通过includeBuilding
,另外工程内有自定义的一些配置,通用性较低的情况下,个人建议也用includeBuilding
。特别是插件工程的情况下,尤为好用。
但是也正如前面介绍的那样,如果gradle版本或者agp版本不同步,则两个工程将无法完成includeBuilding
。
一般情况下,我们的repo源码切换插件,会在这两个技术栈中选择一种,去完成项目的源码切换工作。而这次的迁移方案,只有未成年人才会做选择题的吧,成年人就会说我全部都要。
repo-include.yaml
这次我们新定义了一个yaml
文件, 但是这个文件只负责将模块通过include
依赖进来。
// 全局控制开关
src: false
// 顾名思义
projects:
// 列表第一个数据结构
- branch: master
origin: https://github.com/Leifzhang/QRScaner.git
srcBuild: true
// 参与混编的moduleName
modules:
- name: QRScaner
- name: abb
是不是相比较于另外一些库,我们所需要定义的东西更少一点,至于为啥modules需要大家自己决定,其实有时候一个project里面会有sample工程,还有buildSrc等等,虽然可以直接剔除,但是还是要让同学们主动感知到哪些module需要参与混编的。
然后我们需要做的就是从项目根目录获取到这个文件,之后反序列化这个数据,然后将他们include到当前的settings结构下。
代码语言:javascript复制//根据模块路径获取到数据模型
fun inflateInclude(projectDir: File, repo: RepoInfo) {
val yaml = Yaml()
val dir = File(projectDir.parentFile, "subModules")
val f = File(projectDir, "repo-include.yaml")
if (f.exists()) {
val repoInfoYaml = yaml.load<LinkedHashMap<String, Any>>(FileInputStream(f))
if (repoInfoYaml.containsKey("projects")) {
val modulesList = repoInfoYaml["projects"]
if (modulesList is MutableList<*>) {
modulesList.forEach {
if (it is LinkedHashMap<*, *>) {
val module = parserInclude(it as LinkedHashMap<Any, Any>, dir)
module?.let {
repo.includeModuleInfo[module.name] = module
}
}
}
}
}
}
}
//生成数据模型
fun parserInclude(map: LinkedHashMap<Any, Any>, project: File): IncludeModuleInfo? {
val name = map["name"].toString()
val origin = map["origin"].toString()
val branch = map["branch"].toString()
val srcBuild = map["srcBuild"].toString().toBoolean()
val modules = map["modules"]
val moduleList = mutableListOf<String>()
if (modules is MutableList<*>) {
modules.forEach {
if (it is String) {
moduleList.add(it)
}
}
}
if (moduleList.isEmpty()) {
return null
}
return IncludeModuleInfo(name, origin, srcBuild, project, branch, moduleList)
}
上面是反序列化的代码,比较简单自行领悟就好了。
代码语言:javascript复制override fun apply(settings: Settings) {
// 添加project的监听,在模块Evaluate之前插入仓库不存在的源码切换代码
settings.gradle.addProjectEvaluationListener(object : ProjectEvaluationListener {
override fun beforeEvaluate(project: Project) {
project.configurations.all {
it.resolutionStrategy.dependencySubstitution.all { depend ->
if (depend.requested is ModuleComponentSelector) {
val moduleRequested = depend.requested as ModuleComponentSelector
val p = project.rootProject.allprojects.find { p ->
(p.group == moduleRequested.group && p.name == moduleRequested.module)
}
if (p != null) {
depend.useTarget(project.project(p.path), "selected local project")
}
}
}
}
}
override fun afterEvaluate(project: Project, p1: ProjectState) {
}
})
// 当模块settings完成初始化之后 反序列化数据结构
settings.gradle.addBuildListener(object : BuildAdapter() {
override fun settingsEvaluated(settings: Settings) {
//RepoLogger.setProject(settings.rootProject)
super.settingsEvaluated(settings)
var repoInfo = YamlUtils.inflate(settings.rootDir)
if (repoInfo.moduleInfoMap.isEmpty()) {
repoInfo = RepoInflater.inflate(settings.rootDir)
}
if (repoInfo.moduleInfoMap.isNotEmpty()) {
RepoLogger.info("RepoSettingPlugin start work")
} else {
return
}
// include building模式
repoInfo.moduleInfoMap.forEach { (s, moduleInfo) ->
if (moduleInfo.srcBuild) {
RepoLogger.info("${moduleInfo.name} 通过includeBuild 加入了工程构建中 ")
moduleInfo.settingProject()
settings.includeBuild(moduleInfo.moduleGitRootPath)
RepoLogger.info("module:${moduleInfo.name} 已加入到工程依赖 , 分支:" moduleInfo.curBranch())
}
}
// include 模式
repoInfo.includeModuleInfo.forEach { (s, moduleInfo) ->
moduleInfo.settingProject()
moduleInfo.projectNameList.forEach {
settings.include(":${it}")
RepoLogger.info("$it 路径为 ${moduleInfo.getModulePath(it)}");
settings.project(":${it}").projectDir =
moduleInfo.getModulePath(it)
RepoLogger.info("moudle:${it} 已加入到工程依赖 , 分支:" moduleInfo.curBranch())
}
}
}
})
}
这里有两个地方主义,一个就是当project被拉取到之后,给模块设置configuration的降级策略,之后完成上面说的group moduleName
的转化工作。
另外一点就是当setting完成构建之后,先解析数据结构,之后和以前说的一样发现特定路径下文件夹是否存在,如果不存在则就clone一个,如果存在的话则需要通过特定的命令行,执行工程的分支拉取操作。
完成上述步骤之后,则会将该模块或许通过includeBuilding
或者include
的形式参与到混编的流程中。
缺点是啥
因为这个方案其实和当前我们的momorepo有差别,我们完成了很多对于编译方面的优化,比如module基于commit的aar化等等。另外原始的结构更多的是基于软连接和相对路径相关的。
总结
虽然这个方案最后没有被采纳吧,但是我个人觉得其实也还是蛮有意思蛮好玩的。首先定义的这部分数据结构更简洁好方便,另外也充分的利用gradle原生提供的一部分能力,并在此基础上进行了补足和增强。