Gradle 7.x仓库收口:repositoriesMode三模式与依赖治理
经常看到有团队把 Gradle 从 6.x 拉到 7.x,改完 wrapper、跑一次 sync,屏幕上直接糊出来一行红字:Build was configured to prefer settings repositories over project repositories but repository 'maven' was added by build file 'app/build.gradle'。第一反应都是去搜,然后就撞上了repositoriesMode这个以前从来没见过的属性。照着文档抄一行repositoriesMode.set(RepositoriesMode.PREFER_PROJECT),红字没了,人也就走了——过两周依赖解析开始抽风,某个内部构件死活拉不到,又回头怀疑是不是私服挂了。
这篇就把repositoriesMode掰开讲清楚:它在 Gradle 7.0 之后到底扮演什么角色、三个取值各自会把构建带向哪个方向、在多模块和插件满天飞的工程里该选哪个,以及升级过程中那些看着像它的错其实根本不是它的问题。刚上手 Gradle 的同学可以照着把仓库收口做一遍,已经在维护大型多模块工程的老手,建议重点看第三、六节,那里是真正会踩坑的地方。
1. 升级到 7.x 之后,为什么大家突然开始查 repositoriesMode
1.1 一条报错把问题顶到脸上
先看报错本身。Gradle 给出的提示信息其实信息量很足,不同小版本措辞略有差别,但骨架一致,基本都是三段式:
Build was configured to prefer settings repositories over project repositories but repository 'maven' was added by build file 'app/build.gradle'翻译成人话就是:当前构建被配置成"settings 里的仓库优先",但你在app/build.gradle里又加了一个叫maven的仓库,两件事冲突了。三段分别对应:当前生效的模式、惹事的仓库名、谁加的。
第三段最好用,也最容易被忽略。它可能出现三种值:by build file 'xxx.gradle'——你自己写的;by plugin 'com.xxx.yyy'——某个插件在代码里加的;by settings file 'settings.gradle'——settings 自己加的(这种情况一般是配置写重了)。看到by plugin的时候,你去项目里全文搜索repositories是搜不到的,因为那行代码在插件的 jar 包里。这是排查这类问题的头号障碍,后面第六节会专门说怎么定位。
还有一种是Build was configured to fail if project repositories are declared but ...,这是FAIL_ON_PROJECT_REPOS模式下的措辞,语气更硬,意思是不管你加什么,只要项目级出现仓库声明就直接让构建失败。
1.2 Gradle 7.0 前后到底动了什么
得先把时间线捋清楚,不然容易把锅扣错。集中式仓库声明这块能力不是 7.0 才有的,它是 6.8 引入的孵化特性,dependencyResolutionManagement块连同repositoriesMode一起进来,7.0 正式转正、去掉孵化标记。所以"升级到 7.0 之后才出现这个属性"这个说法不太准确,准确的说法是:7.0 之后这套东西成了官方推荐的写法,大量脚手架和工程模板开始默认往里写,于是普通人第一次撞上它。
与此同时 7.0 还顺手清理了一批陈年旧账,比如compile、runtime这些老配置被彻底移除,仓库url的赋值一律要走uri()包装,url 'https://xxx'这种直接扔字符串的写法在升级时经常报类型转换错误。这些改动和repositoriesMode一点关系都没有,但它们会在同一次升级里集中爆发,混在同一屏报错里,很容易让人误以为是仓库模式在作妖。我见过的典型误判就是:明明报的是url类型错误,人却跑去改repositoriesMode,越改越乱。
还有一个关键事实值得记牢:repositoriesMode的默认值是PREFER_PROJECT,7.0 没有改这个默认值。也就是说,一个干净的老工程原样升到 7.x,不会因为仓库模式而失败。真正失败的那些,多半是工程模板、公司脚手架或者新建工程向导里已经提前写好了PREFER_SETTINGS或FAIL_ON_PROJECT_REPOS,老代码一进来就冲突。搞清楚这一点,排查方向立刻从"Gradle 坑我"变成"谁在我的 settings 里动了手脚"。
1.3 一句话说清它到底管什么
repositoriesMode解决的是一个非常具体的问题:当 settings 文件和项目构建脚本两处都声明了依赖仓库时,听谁的。
它不是一个"合并开关",而是"优先级与准入开关"。这里有个特别容易误解的点:很多人以为两处声明会叠加成一个大列表,实际不是。它是覆盖关系,要么全听 settings 的,要么全听项目的。用公司采购打个比方:PREFER_PROJECT是各部门自己下单,总部那份采购清单作废;PREFER_SETTINGS是只认总部清单,部门自己买的当没看见;FAIL_ON_PROJECT_REPOS是部门敢自己下单,流程直接驳回。
理解了这三句话,后面所有细节都是它们的推论。而之所以要把这个开关当回事,是因为仓库列表一旦失控,带来的不只是"能不能拉到包"的问题,还有构建速度、依赖来源可信度、以及团队之间配置漂移这三笔隐形成本。
2. 三种取值的行为差异、后果与选型
2.1 PREFER_PROJECT:默认值,兼容优先
行为很干脆:只要某个项目里声明了任意一个仓库,settings 里dependencyResolutionManagement.repositories声明的仓库就整份失效,不是逐个比较、也不是取并集。反过来说,如果这个项目一个仓库都没声明,那就老老实实用 settings 的。这个"任意一个就整份失效"的语义是最反直觉的地方,我第一次读文档时也反复确认了两遍。
// settings.gradle dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.PREFER_PROJECT) repositories { mavenCentral() } }这套模式下,mavenCentral()只在"所有项目都没写仓库"时才生效。只要有一个模块写了repositories { maven { ... } },那个模块就走自己的,其他没写的模块继续用 settings 的——注意粒度是按项目计算的,不是整个工程一刀切。
选它的理由通常只有一个:过渡期。工程里插件太多、模块太杂,一时半会儿收不干净,先用默认值让构建跑起来。代价是仓库治理彻底失效,插件想加什么仓库就加什么仓库,镜像配置、私服策略、离线构建全部形同虚设。
2.2 PREFER_SETTINGS:静默覆盖,最容易被误用
行为是 settings 的仓库优先生效,项目里声明的(包括插件偷偷加的)被忽略掉。注意是"忽略",不是"报错"——构建照常跑,只在部分版本的告警日志里能看到一句不疼不痒的提示。
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) repositories { mavenCentral() } }这个模式最大的问题是静默失败。假设某个内部插件依赖一个只在公司私服上才有的构建期依赖,它自己在代码里加了私服地址。你把模式切成PREFER_SETTINGS,这个地址被忽略,构建跑到一半报Could not find com.example:xxx:1.0.0。你打开build.gradle一看,私服地址明明写在里面啊——但那是插件写的,而且已经被忽略了,你在项目里怎么找都找不到线索。
我的建议是把它当止血带用,不当长期方案:先用它把构建从红色拉回绿色,趁着能跑赶紧把缺的仓库补进 settings,然后在 CI 上切到FAIL_ON_PROJECT_REPOS做最终校验。直接拿PREFER_SETTINGS当终态,等于给自己埋一个只有换机器、换网络环境才复现的定时炸弹。
2.3 FAIL_ON_PROJECT_REPOS:强制收口,团队协作首选
行为最硬:只要检测到任何项目级仓库声明——不管是你写的还是插件加的——配置阶段立刻失败,构建根本进不到编译。
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { url = uri("https://repo.example.com/repository/maven-public/") } mavenCentral() } }它的价值在于把"团队约定"变成了"构建期强制检查"。口头约定"仓库统一写在 settings 里"这种事,撑不过三个月,尤其是有新人加入、有新模块从别的工程拷进来的时候。而FAIL_ON_PROJECT_REPOS会在第一次 CI 跑起来的时候就告诉你:有人违规了,哪一行,哪个文件。
代价也很实在:改造初期会非常痛。历史工程里少说有十几个地方散落着仓库声明,插件注入的那些更是防不胜防。我的做法是分两步走——先在本地或一个独立分支上开这个模式,把报错一条条消灭;等本地全绿了再合入主干,避免影响其他人的日常开发节奏。
2.4 三种模式对照与选型建议
| 模式 | 项目声明了仓库 | settings 声明了仓库 | 构建是否中断 | 典型使用场景 |
|---|---|---|---|---|
PREFER_PROJECT | 项目仓库生效,settings 整份失效 | 仅当项目未声明时生效 | 否 | 老工程过渡期、插件生态复杂且短期无法收敛 |
PREFER_SETTINGS | 被静默忽略 | 生效 | 否 | 紧急止血、临时统一镜像、排查依赖来源 |
FAIL_ON_PROJECT_REPOS | 直接失败 | 生效 | 是 | 新工程、多模块主干、CI 强制校验 |
选型的判断标准其实就一条:这个工程里还有没有人有正当理由在模块内声明仓库。如果答案是"没有",直接上FAIL_ON_PROJECT_REPOS,别犹豫;如果答案是"有,但我不知道是哪个插件",那先用PREFER_SETTINGS撑住,同时把定位插件这件事排进迭代。PREFER_PROJECT只应该出现在"我刚升完级,先让它跑起来,明天再说"这个时间窗口里,超过一周就是技术债。
3. 别把三块仓库搞混:作用域全景
3.1 dependencyResolutionManagement 只管"项目依赖"
这一块的职责边界必须刻在脑子里:它管的是项目里configurations(比如implementation、api、testImplementation、releaseRuntimeClasspath)解析依赖时用哪份仓库列表。也就是build.gradle里那个裸的repositories { }块,被它接管了。
// build.gradle(项目级) dependencies { implementation 'com.example.core:core-utils:2.3.1' }上面这个依赖会走dependencyResolutionManagement.repositories里的仓库去解析,repositoriesMode影响的就是它。范围就这么大,不多不少。
3.2 pluginManagement 管"插件",且必须写在最前面
插件解析走的是另一套:pluginManagement.repositories。它和repositoriesMode完全无关,你就算把模式设成FAIL_ON_PROJECT_REPOS,插件仓库该从哪来还是从哪来。而且pluginManagement块必须是 settings 文件的第一个块,前面只能有注释,写在别的地方 Gradle 会直接报错。
// settings.gradle.kts pluginManagement { repositories { maven { url = uri("https://repo.example.com/repository/gradle-plugins/") } gradlePluginPortal() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { url = uri("https://repo.example.com/repository/maven-public/") } mavenCentral() } } rootProject.name = "demo" include(":app", ":core")这段是完整可用的骨架,Groovy DSL 版本把include(":app")换成include ':app'、url = uri(...)保持不变即可。注意两个块的仓库列表通常是不同的:插件仓库要放插件门户,依赖仓库不需要。
3.3 buildscript 里的 repositories 完全不受约束
这是踩坑率最高的一条。buildscript { repositories { ... } }解析的是构建脚本自身的 classpath(比如各种 Gradle 插件 jar),它走的是buildscript.repositories,跟dependencyResolutionManagement是两套独立的东西。
// build.gradle buildscript { repositories { maven { url = uri("https://repo.example.com/repository/gradle-plugins/") } } dependencies { classpath 'com.example.tools:build-tools:1.4.0' } }很多人开了FAIL_ON_PROJECT_REPOS,发现build.gradle里这段居然还能正常跑,于是怀疑模式没生效。其实它本来就管不到这里。真要收口 buildscript 的仓库,只能靠代码评审和自定义检查脚本,repositoriesMode帮不上忙。
3.4 included build 与 buildSrc 各自为政
includeBuild引入的复合构建、以及buildSrc目录,本质上都是独立的构建,各自有自己的 settings。父工程设了FAIL_ON_PROJECT_REPOS,不会自动传递下去,子构建该怎样还怎样。所以在大型工程里,你可能会遇到"主工程明明收口了,buildSrc里还躺着三个仓库"的情况。
处理办法很简单:把buildSrc/settings.gradle(或.kts)也按同样的方式配一遍。别嫌麻烦,buildSrc的依赖解析出问题的时候,报错信息比主工程更难读,因为它连模块名都不会给你。
3.5 作用域对照速查
| 配置位置 | 影响什么 | 受 repositoriesMode 约束 | 必须写在 |
|---|---|---|---|
pluginManagement.repositories | 插件解析 | 否 | settings 文件第一个块 |
dependencyResolutionManagement.repositories | 项目依赖解析 | 是(它就是开关本体) | settings 文件中任意位置 |
项目repositories { } | 项目依赖解析 | 是 | build.gradle |
buildscript.repositories | 构建脚本 classpath | 否 | build.gradle |
buildSrc/settings.gradle | buildSrc 自身 | 否(独立构建) | buildSrc 目录下 |
这张表建议直接存进团队文档。仓库解析类的问题,九成都能靠它快速定位到"到底是哪一层出了问题"。
4. 实操:把散落各处的仓库收敛到 settings
4.1 第一步:摸底,把所有声明扫出来
别急着改配置,先搞清楚现状。Linux 或 macOS 下用:
grep -rn --include="*.gradle" --include="*.gradle.kts" -E "repositories\s*\{" .Windows 的 cmd 环境:
findstr /s /n /c:"repositories" *.gradle *.gradle.kts第一遍只找repositories {,把文件路径和行号列出来。然后针对每个命中位置再单独确认里面有没有mavenLocal()、flatDir、ivy {、maven { url ... }这些特殊写法。flatDir尤其要注意,它绕开了正常的坐标体系,收敛到 settings 里会出问题,得单独处理或直接干掉。
另外顺手跑一下:
./gradlew buildEnvironment它会把构建脚本 classpath 的依赖树和解析来源打出来,能帮你发现一些静态搜索漏掉的、由插件动态注入的构建期依赖。
4.2 第二步:改写 settings(Groovy 与 KTS 双版本)
Groovy DSL 版本:
// settings.gradle pluginManagement { repositories { maven { url = uri("https://repo.example.com/repository/gradle-plugins/") } gradlePluginPortal() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { name = "internal" url = uri("https://repo.example.com/repository/maven-public/") } mavenCentral() } } rootProject.name = "demo" include ':app', ':core'Kotlin DSL 版本:
// settings.gradle.kts pluginManagement { repositories { maven { url = uri("https://repo.example.com/repository/gradle-plugins/") } gradlePluginPortal() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { name = "internal" url = uri("https://repo.example.com/repository/maven-public/") } mavenCentral() } } rootProject.name = "demo" include(":app", ":core")两个细节值得说。一是repositoriesMode.set(...)这种写法在任何版本都稳,Groovy 下直接repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS也能跑,但如果你在多个 Gradle 版本之间来回切,用.set()更省心。二是给私服仓库加个name,构建日志和--info输出里会显示这个名字,比默认的maven、maven2好认一百倍,排查时能省不少时间。
4.3 第三步:清理 build.gradle 里的残留
根工程的build.gradle里通常长这样:
// 改造前 allprojects { repositories { maven { url = uri("https://repo.example.com/repository/maven-public/") } mavenCentral() } }// 改造后:整段删掉,别留空壳 // allprojects 里只留真正需要的配置,比如统一的编译参数 allprojects { tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' } }注意别只是把repositories { }里面清空、留下一个空块。空块在FAIL_ON_PROJECT_REPOS下不报错(因为没声明具体仓库),但会给后来人错误暗示:"这里本来是有仓库的,可能以后还要加回来"。直接删干净,配合 git blame 一眼能看到历史。
各子模块的build.gradle同理。这一步用第一步扫出来的清单逐个处理,改完再跑一次 grep 确认没有漏网的。
4.4 第四步:对付"插件偷偷加仓库"
这一步最费时间,但也有章法。流程是这样:
第一,跑一次构建,从报错里把插件 id 抄下来,形如by plugin 'com.example.foo'。
第二,用--stacktrace拿完整堆栈:
./gradlew help --stacktrace堆栈里能找到插件内部调用repositories.maven { }的具体位置,虽然看不到源码,但类名和方法名足够你确认是哪个插件了。
第三,按优先级做取舍。优先升级插件,新版本大多遵守了集中式仓库的约定,尤其是主流的构建插件,这两年基本都改过了。次选是把它需要的仓库补进 settings,注意补的时候带上content过滤(见第五节),别让一个插件的私服地址污染所有依赖的解析路径。最后才是临时降级到PREFER_SETTINGS,而且要在代码里加注释写清楚原因和移除条件。
有一种做法是在 settings 里用gradle.beforeProject { }钩子把项目仓库清空,我不太推荐。它确实能让构建跑起来,但等于把所有仓库来源都变成了"黑盒",以后谁也不知道某个依赖是从哪拉下来的,出问题时排查成本翻倍。
4.5 第五步:验证收口结果
改完之后别只看 sync 成不成功,跑这几条:
# 强制刷新依赖,确认所有构件都能从 settings 声明的仓库拿到 ./gradlew --refresh-dependencies :app:dependencies --configuration releaseRuntimeClasspath # 检查构建脚本 classpath 有没有意外来源 ./gradlew buildEnvironment # 打开全部告警,看有没有被忽略的弃用提示 ./gradlew help --warning-mode all--refresh-dependencies这一步很关键。Gradle 有本地依赖缓存(默认在用户目录的.gradle/caches/modules-2下),如果只跑普通构建,很可能全部命中缓存,你以为收口成功了,换台机器直接崩。强制刷新能把真实解析路径完整走一遍。
5. 顺手把仓库治理做扎实:内容过滤与版本目录
5.1 content 过滤:让每个仓库只回答自己该回答的问题
仓库收口之后,下一个问题是:Gradle 会按顺序挨个仓库尝试,没命中就返回 404 再试下一个。仓库越多、顺序越靠后,无谓的网络往返就越多。content块能告诉 Gradle"这个仓库只负责这几组坐标",其他的别来问。
dependencyResolutionManagement { repositories { maven { name = "internal" url = uri("https://repo.example.com/repository/internal/") content { includeGroup("com.example.internal") includeGroupByRegex("com\\.example\\.internal\\..*") } } maven { name = "public" url = uri("https://repo.example.com/repository/maven-public/") content { excludeGroup("com.example.internal") } } mavenCentral() } }includeGroupByRegex里的反斜杠要写双份,这是 Kotlin 字符串转义的锅,第一次写基本都会错一遍。excludeGroup的价值在于防止同名构件从公共仓库误拉——内部构件如果恰好和公共坐标撞名,那就不只是慢的问题了,可能直接拉到错误的版本。
5.2 exclusiveContent:锁死某个坐标只能从指定仓库拿
content是"建议",exclusiveContent是"规定"。被它圈定的 group,Gradle 只会去指定的那个仓库找,别处一律不看。
dependencyResolutionManagement { repositories { exclusiveContent { forRepository { maven { name = "internal" url = uri("https://repo.example.com/repository/internal/") } } filter { includeGroup("com.example.internal") } } mavenCentral() } }这段配置的意义在于供应链层面的确定性。内部构件必须在内部仓库解析,这条规则如果只写在文档里,早晚会有人为了图快加个mavenLocal()绕过去。写成exclusiveContent,绕不过去。
5.3 mavenContent 与 releasesOnly:别让快照版本污染发布流程
还有一个很实用的小配置,用来限制仓库只提供正式版或只提供快照版:
maven { name = "internal-releases" url = uri("https://repo.example.com/repository/releases/") mavenContent { releasesOnly() } } maven { name = "internal-snapshots" url = uri("https://repo.example.com/repository/snapshots/") mavenContent { snapshotsOnly() } }不这么分的时候,Gradle 会在 release 仓库里也去查-SNAPSHOT版本,白白多一次 404。多模块工程里快照依赖又多,累积起来相当可观。这属于典型的"配置五分钟,长期受益"。
5.4 把版本也一起收口:versionCatalogs
仓库收口解决的是"从哪拿",版本目录解决的是"拿哪个版本"。Gradle 7.0 同期引入的版本目录,正好可以和仓库治理配套使用。
# gradle/libs.versions.toml [versions] core = "2.3.1" [libraries] core-utils = { module = "com.example.core:core-utils", version.ref = "core" }// settings.gradle.kts dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { url = uri("https://repo.example.com/repository/maven-public/") } mavenCentral() } versionCatalogs { create("libs") { from(files("gradle/libs.versions.toml")) } } }放在dependencyResolutionManagement里的好处是:仓库策略、版本策略、仓库准入模式三件事集中在同一个小文件里,评审时一眼能看完。分散在十几个build.gradle里的时代该过去了。
5.5 收口带来的收益,给个量级估算
不用太精确,算个大概就有体感。假设一个 20 模块的工程,去重后有 120 个外部依赖,改造前每个模块各自声明 3 个仓库,改造后 settings 里配 2 个仓库。
| 阶段 | 仓库声明位置 | 首次解析请求量级 | 说明 |
|---|---|---|---|
| 改造前 | 20 个模块 × 3 仓库 | 约 360 次元数据请求 | 按依赖数 × 仓库数粗略估算,未命中也要走一次往返 |
| 改造后 | settings 统一 2 仓库 | 约 120 至 240 次 | 命中率提升后多数依赖一次命中 |
| 加 content 过滤 | 同上 | 约 120 至 150 次 | 私服不再被问公共依赖,少掉大量无效探测 |
这组数字是量级估算,实际值受缓存命中、网络 RTT、仓库响应速度影响很大,别拿去当性能报告。但方向是明确的:仓库数量减半,请求量就接近减半,CI 上每次构建省下的几十秒,一年下来很客观。
6. 常见问题与排查实录
6.1 报错指向 build.gradle,但那行根本不是 repositories
这是最典型的一种。报错说was added by build file 'app/build.gradle',你打开对应行号一看,是一行apply plugin: 'com.example.foo',压根没有仓库声明。
原因很直接:插件在apply的时候执行了自己的配置代码,里面调用了project.repositories.maven { }。Gradle 把责任归到了"触发点"所在的构建文件上,而不是实际执行加仓库动作的插件代码。
定位方式就是前面说的--stacktrace,堆栈会指向插件内部的类。另外--info级别日志也会打出仓库被添加的时机,配合时间线能判断是哪个插件在配置阶段动的手。找到之后按 4.4 节的取舍流程处理。
6.2 切到 FAIL_ON_PROJECT_REPOS 之后开始大量 Could not find
这种"改完模式就跑不动"的情况,八成是 settings 的仓库列表不全。改造前的仓库分散在十几个文件里,你未必全抄过来了。
先用dependencyInsight精确看某个依赖的解析路径:
./gradlew :app:dependencyInsight \ --dependency core-utils \ --configuration releaseRuntimeClasspath \ --info输出里会列出 Gradle 尝试过的仓库以及每个仓库的响应。如果某个仓库压根没出现在尝试列表里,说明它没进 settings;如果出现了但报 404,说明坐标或路径不对,可能是私服的 group 路径拼接规则和公共仓库不一样。
还有一种隐蔽情况:仓库确实在 settings 里,但被content过滤挡住了。这时候日志里会显示"仓库被跳过"之类的信息,--info下能看到。加了过滤之后一定要回头验证一遍所有依赖,别只验证主模块。
6.3 mavenLocal 带来的"本地能跑、CI 挂"
mavenLocal()会把本机 Maven 本地仓库加进解析范围。它的特点是:解析出来的构件完全不做校验,而且优先级取决于声明位置。开发机上因为之前手动install过某个内部包,构建顺畅;CI 上没有这个本地仓库,直接找不到。
我的处理原则是:mavenLocal()不写进 settings 的公共仓库列表,只在确实需要本地联调时,通过开发者自己的初始化脚本临时加,用完就撤。写进 settings 里当常驻配置,早晚会变成团队级的偶发故障源。这条经验是用两次深夜排查换来的,不太好受。
6.4 混合工程里"改了又被覆盖"
有些跨端或移动端工程,构建脚本是由工具链自动生成的。你手动删掉repositories { },下次同步一跑,它又回来了,FAIL_ON_PROJECT_REPOS继续报错,形成死循环。
应对方式分两层:短期用PREFER_SETTINGS兜住,让构建先能跑;同时去找生成源头,把工具链版本升上去。这类工具链在新版本里基本都改成了往 settings 写仓库,升级路径通常是通的。实在升不上去,就在 CI 上单独跑一个检查脚本,比对生成文件是否被改动,把问题显式暴露出来而不是让它静默存在。
6.5 改了仓库配置但不生效
检查缓存。Gradle 的元数据缓存放在用户目录的.gradle/caches/modules-2下面,仓库地址变了但缓存没失效的话,可能继续用旧结果。先用--refresh-dependencies跑一次,多数情况能解决。还是不行的话,再考虑清掉对应的缓存目录,但别养成"一有问题就删缓存"的习惯——那会掩盖真正的问题。
6.6 常见问题速查表
| 现象 | 大概率原因 | 处理方向 |
|---|---|---|
| 报错指向 build.gradle 但该行无仓库声明 | 插件在 apply 时注入仓库 | --stacktrace定位插件,升级或补仓库 |
| 切模式后出现 Could not find | settings 仓库列表不全或被 content 挡了 | dependencyInsight --info看尝试路径 |
| buildscript 里的仓库依然生效 | repositoriesMode不管 buildscript | 靠代码评审和自定义检查收口 |
| buildSrc 里还有仓库声明 | 独立构建,配置不继承 | 单独给 buildSrc 配 settings |
| 本地正常 CI 失败 | 依赖了mavenLocal()的产物 | 移除常驻的 mavenLocal 声明 |
| 改了配置无变化 | 元数据缓存未失效 | --refresh-dependencies,再考虑清缓存 |
| 手改的配置反复被还原 | 工具链自动生成构建脚本 | 升级工具链或在 CI 加检查 |
7. 几点实践体会
新起一个工程,我从第一天就把repositoriesMode设成FAIL_ON_PROJECT_REPOS。这个阶段成本最低,因为还没人往里扔东西,边立规矩边开发,比半年后再来治理轻松十倍不止。等工程铺开了、十几个模块、一堆插件,再想收口就是一场硬仗。
老工程改造,我一般分两个迭代。第一个迭代切PREFER_SETTINGS,同时把 CI 上的--warning-mode all打开,让所有被忽略的仓库声明都暴露在日志里,一条条抄进 settings。第二个迭代再切FAIL_ON_PROJECT_REPOS,这时候基本只剩插件注入的那几个钉子户要处理。中间这两三周别省,省了就是给自己挖坑。
最后一个小技巧:把"仓库声明只允许出现在 settings"这条规则写成 CI 的一个 grep 检查,跑在构建之前。几十行脚本的事,比任何文档和口头约定都管用。人都会忘,脚本不会。