简介:Gradle 8.6 全量发行包面向 Java、Android 及其他 JVM 语言开发者,也适合需要研究构建工具内部实现、缓存机制与插件体系的进阶用户。压缩包约 209.99MB,共 2000 个文件,其中以 1962 个 Java 源文件为主体,辅以 34 个 properties 配置、3 个 txt 与 1 个 pdf 说明文档,便于从源码、配置和文档多个层面理解构建流程与模块组织。内容围绕自定义加密密钥配置缓存、构建初始化脚手架、构建创作 API 更新及性能优化等改进展开,读者可据此考察缓存安全策略、项目初始化模板、插件生命周期、依赖解析与任务执行逻辑,并对照核心实现梳理构建自动化中的关键设计。已有 1673 人学习,适合希望深入掌握 Gradle 8.6 构建能力、排查复杂构建问题或开发高质量插件的开发者参考,也为离线查阅、源码研读与构建工具二次研究提供完整样本。
1. gradle-8.6-all.zip 不是绿色安装包:先想清楚它该落在哪个目录
又见到有人把 gradle-8.6-all.zip 解压到 D 盘根目录,再把环境变量指向它,然后构建是跑起来了,可换一台机器就得从头再摆一遍。这个压缩包属于 Gradle 官方发行版的可解压分发包,并不是那种开箱即用的绿色软件;-all相比-bin多带了 Gradle 自身的源码和 DSL 文档。它真正有价值的地方,是能被放进 wrapper 的缓存目录,让一台连不上外网的机器用 gradle-8.6 跑完整构建。适合三类人:要写自定义 Gradle 插件、要在离线环境里复现构建、以及被 IDE 反复下载发行版折腾过的从业者。
2. -all、-bin 与 wrapper 缓存:三个必须先分清的概念
下载之前如果不把这三件事分清,后面大概率会出现「包明明在本地,构建时还是去网上下载」的情况。那并不是玄学,而是 Gradle 的发行版缓存按 URL 做了隔离,你以为它按版本号存放,其实它的实际机制和你预想的并不一样。
2.1 -all 多出来的 src 和 docs 到底给谁用
解开压缩包看目录结构最直观:bin是启动脚本,lib是全部 jar(gradle-core、gradle-api、kotlin-dsl 都在里面),init.d放初始化脚本,LICENSE和NOTICE是协议文件。-all额外多出两样——docs是 DSL 参考和 javadoc,src是 Gradle 自己的源码。
这两样东西平时用不上,但在两个场景里价值很高。第一个是写自定义插件:在buildSrc或者独立插件工程里实现Plugin<Project>时,你在 IDE 里按住 Ctrl 点进project.getTasks(),-bin给你的是反编译出来的占位代码,-all给的是带注释的真实实现。第二个是排查 Gradle 内部行为,比如 configuration cache 序列化失败时报了一串莫名其妙的类型,你能直接翻源码确认它是哪一步做的检查,而不是靠猜。
| 维度 | gradle-8.6-bin.zip | gradle-8.6-all.zip |
|---|---|---|
| 压缩包内容 | bin、lib、init.d、协议文件 | 在 bin 全部内容之外多出 docs 与 src |
| IDE 里点进 Gradle API | 反编译占位代码 | 真实源码与注释 |
| 离线查 DSL 参考 | 只能靠记忆 | docs 目录里是完整文档 |
| 写自定义插件的调试体验 | 断点能打,读源码难受 | 断点与源码对照着看 |
| 缓存目录 | 由 distributionUrl 决定 | 与 bin 完全隔离,互不复用 |
选型就这么简单:纯业务项目、只跑build、test、publish,用-bin就够;工程里有buildSrc、有自研插件、或者你经常需要在离线环境里翻文档,直接上-all,多出来的体积换来的是长期可读性。我一般给团队的标准模板就写-all,省得后面有人要写插件时再回头换。
2.2 wrapper 缓存目录名是一串 Base36,不是版本号
这是最容易出错的一点。wrapper 把发行版放在${GRADLE_USER_HOME}/wrapper/dists/gradle-8.6-all/<hash>/,最后那级<hash>不是时间戳也不是随机数,而是distributionUrl 字符串的 MD5 摘要转成 Base36。Gradle 这么设计有它的道理:同一个版本号,来自官方地址、来自内网地址、来自本地文件,三者被视为三个不同的发行版,各自独立缓存,避免不同来源的包混在一起。
理解了这个机制,很多现象就能解释清楚了。你手动把 zip 放进 dists 目录,wrapper 又新建一个目录重新下载,说明你放的目录名和它算出来的目录名对不上。你在gradle-wrapper.properties里把地址从-bin改成-all,发现要重新下一遍,因为 URL 变了,hash 跟着变。你在内网改写了域名,hash 同样会变。
目录之下还有两个文件值得记住:gradle-8.6-all.zip是原始压缩包,gradle-8.6-all.zip.ok是安装完成标记,解压出来的gradle-8.6/是真正被使用的发行版。另外还有gradle-8.6-all.zip.lck,那是进程锁,多个 daemon 同时启动时用它做互斥。缺了.ok,wrapper 会认为上次安装没做完,重新走一遍解压甚至重新下载。
提示:
zipStoreBase和zipStorePath控制 zip 存放位置,默认与解压目录一致。把这两个值改到别的盘,只影响原始 zip 的落点,不影响wrapper/dists的结构。
2.3 先校验 SHA-256,再谈离线
离线环境里最怕的其实不是版本不对,而是包在流转过程中被改过、被工具重打包过、或者下到一半断了。校验成本极低,顺手做掉。
# Linux / macOS,先算一遍,把结果贴进变更单或 README sha256sum gradle-8.6-all.zip shasum -a 256 gradle-8.6-all.zip # macOS 自带的是 shasum # Windows PowerShell,等价写法 Get-FileHash .\gradle-8.6-all.zip -Algorithm SHA256 # 官方同时发布了 .sha256 文件,直接比对最省事 curl -fsSLO https://services.gradle.org/distributions/gradle-8.6-all.zip.sha256 cat gradle-8.6-all.zip.sha256逻辑说明:sha256sum输出的是 64 位十六进制摘要,与官方.sha256文件里的值逐字符比对即可。参数上没什么可调的,重点是不要凭记忆把一串 hash 写进配置,每次都以当次下载到的.sha256文件为准。这个值后面会写进distributionSha256Sum,一旦写进去,zip 内容有任何字节变化,wrapper 会直接拒绝启动,这是内网流转里最便宜的一道防篡改措施。
3. 三种把 8.6 接进项目的方式:从自动下载到手动落盘
同一种诉求有三种实现路径,选择依据是「目标机器能不能上网」和「这份 zip 要给多少个项目复用」。能上网的走自动下载最省心,完全隔离的走手动落盘,介于两者之间的把 zip 挂到共享盘、用file:协议指向它。
3.1 最稳的方式:让 wrapper 自己下
只要本机有一个能用的 gradle 可执行文件(任意 8.x 都行),在项目根目录执行一条命令就能把 wrapper 生成好。
# --distribution-type all 是关键,不加默认生成 -bin gradle wrapper --gradle-version 8.6 --distribution-type all # 生成后确认这几个文件都在 ls -l gradlew gradlew.bat gradle/wrapper/gradle-wrapper.jar gradle/wrapper/gradle-wrapper.properties命令说明:--gradle-version决定distributionUrl里写哪个版本号,--distribution-type决定后缀是-bin还是-all。生成的gradle-wrapper.jar负责引导下载,gradlew是 Unix 侧启动脚本,gradlew.bat是 Windows 侧。
生成的 properties 文件长这样,每一行都值得看一眼:
# gradle/wrapper/gradle-wrapper.properties distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/distsdistributionUrl里那个反斜杠是.properties格式对冒号的转义,不能删,删了会被当成 key 的分隔符。validateDistributionUrl=true表示启动时先探一下 URL 是否可达,内网环境里如果探测被墙掉会报错,可以临时改成false,但更推荐直接换成file:协议。networkTimeout单位是毫秒,内网走代理下载慢的话调到 30000 更合适。
执行./gradlew --version会触发首次下载,包会落到 2.2 说的那个 hash 目录里。这一步在有网机器上跑通,再把整个wrapper/dists目录带走,是最省事的复制方式。
3.2 手动落盘:算准目录名、补上 .ok 标记
完全隔离的机器上,只能手动放。关键是先把目录名算对。下面这段 Python 就是把 Gradle 的命名规则复现出来。
import hashlib DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz" def base36(value: int) -> str: """把整数转成 Base36 字符串,Gradle 的 PathAssembler 就是这么做的""" if value == 0: return "0" out = [] while value: value, rem = divmod(value, 36) out.append(DIGITS[rem]) return "".join(reversed(out)) def dist_hash(distribution_url: str) -> str: """distributionUrl 的 MD5 摘要 -> Base36,得到缓存目录名""" digest = hashlib.md5(distribution_url.encode("utf-8")).digest() return base36(int.from_bytes(digest, "big")) if __name__ == "__main__": # 注意:这里必须是 properties 里去掉反斜杠转义之后的完整 URL url = "https://services.gradle.org/distributions/gradle-8.6-all.zip" print(dist_hash(url))逻辑与参数说明:hashlib.md5在这里只用来生成目录名,与安全无关,不要拿它做完整性校验。int.from_bytes(digest, "big")把 16 字节摘要按大端解释成一个大整数,再转 Base36,这一步和 Gradle 内部实现一致。传进去的 URL 必须与 properties 文件中解转义后的字符串逐字符相同:协议是https还是http、后缀是-all还是-bin、末尾有没有空格,任何一点差异都会算出一个完全不同的目录名,然后你会发现 wrapper 又去下载了。
目录名算好之后,落盘动作就四步:
set -e VER=8.6 HASH=$(python3 dist_hash.py) # 上一步脚本的输出 DIST="$HOME/.gradle/wrapper/dists/gradle-${VER}-all/${HASH}" mkdir -p "$DIST" cp "gradle-${VER}-all.zip" "$DIST"/ # 原始 zip 留在目录里,不要删 cd "$DIST" unzip -q "gradle-${VER}-all.zip" # 解出 gradle-8.6/ 这一层 touch "gradle-${VER}-all.zip.ok" # 安装完成标记,缺了它前功尽弃 # 自检:这三条都应该有输出 ls -d "$DIST/gradle-${VER}" ls "$DIST/gradle-${VER}/bin/gradle" ls "$DIST/gradle-${VER}-all.zip.ok"参数说明:unzip -q的-q只是静默输出,不影响结果;touch创建的空文件就是.ok标记,名字必须是「zip 文件名 +.ok」,写成gradle-8.6.ok是不认的。自检那三条命令建议固定成习惯,尤其是第二条,它确认了bin/gradle在正确层级——多套一层目录是最常见的翻车点。
3.3 distributionUrl 指向本地文件
如果这份 zip 要服务多个项目,逐个塞缓存目录太笨。把它放到共享盘或者构建机的固定路径,然后让 properties 直接指过去。
# Linux / macOS 构建机 distributionUrl=file\:/opt/dist/gradle-8.6-all.zip # Windows 构建机(注意盘符前的三条斜杠) # distributionUrl=file\:///D:/toolchain/gradle-8.6-all.zipfile:后面的冒号同样需要反斜杠转义。这种写法的好处是 zip 只有一份,改一次全项目生效;坏处是路径成了硬编码,不同机器上路径不一致时会直接报找不到。折中做法是在gradle.properties里留一个变量,用-P或环境变量注入,但这需要改构建脚本,简单项目没必要。
这种写法下distributionSha256Sum依然生效,wrapper 读文件后会先校验再解压,比走网络时的校验更有意义——因为本地文件被误改的概率其实不低。
3.4 三条命令验证到底用的是哪个 Gradle
装完不要凭感觉,用命令确认。
./gradlew --version # 看 Gradle、Kotlin、Groovy、JVM、OS 五行 ./gradlew -q javaToolchains # 看 toolchain 探测到哪些 JDK find "$HOME/.gradle/wrapper/dists" -maxdepth 3 -name "*.ok" # 确认安装标记第一条输出的第一行是 Gradle 版本,第二行 Kotlin,第三行 Groovy,第四行 JVM,第五行 OS。重点看第一行是不是 8.6,以及 JVM 那行是不是你预期的 JDK——Gradle 8.6 跑在 JDK 8 到 21 上都行,但跑在哪个 JDK 上会直接影响构建脚本里能用的语法。第二条列出 toolchain 探测结果,离线环境里这一条尤其要看,探测不到 JDK 会在编译阶段才报错。第三条确认.ok标记存在,没有标记就说明缓存不完整。
4. 离线构建:--offline 只解决一半问题
很多人以为把发行版塞进缓存、加上--offline就万事大吉,结果第一次跑就卡在依赖解析上。--offline的作用范围比想象中窄得多,它只保证「不发起网络请求」,不保证「本地有货」。
4.1 --offline 到底关掉了什么
加上这个参数后,Gradle 会禁用所有仓库的网络访问,包括插件仓库、依赖仓库、以及 artifact 的远程查询。一旦某个模块在本地缓存里找不到匹配的版本,构建直接失败,报错通常是No cached version of com.xxx:yyy:1.2.3 available for offline mode。
也就是说,--offline是一道限制,只能约束行为:它阻止 Gradle 去联网,但并不会补充依赖。真正决定离线能否构建成功的,是~/.gradle/caches/modules-2里有没有那批 jar、pom 和元数据。发行版和依赖缓存是两套完全独立的东西:前者解决「用哪个 Gradle 跑」,后者解决「编译时链接什么」,很多人只做了前者,然后在编译阶段才失败。
还有一个细节:--offline对动态版本(1.2.+、latest.release)很不友好,因为动态版本必须去远程查最新,离线状态下会直接用缓存里已有的解析结果,可能与在线时不一致。生产构建应尽量把版本固定下来,这和后面第 6 章说的版本目录是同一件事。
4.2 把依赖预热进本地缓存
标准做法是在有网机器上用同一份 wrapper、同一套 JVM 参数先跑一遍,再把缓存搬过去。
# 1) 强制刷新并解析一遍运行时依赖,把 pom / jar / module 元数据拉全 ./gradlew --refresh-dependencies dependencies --configuration runtimeClasspath # 2) 完整构建一次,插件、注解处理器、测试依赖才会被拉下来 ./gradlew build -x test # 3) 打包缓存,只搬 modules-2 最稳 tar -czf gradle-cache-8.6.tgz -C "$HOME" .gradle/caches/modules-2逻辑说明:第 1 步的--refresh-dependencies会忽略本地已有的过期标记,重新走一遍解析,确保元数据是最新的;dependencies任务只打印依赖树,但解析过程本身就会把文件下载到缓存。第 2 步必须做,因为插件(buildscript classpath 上的那些)和注解处理器不在普通依赖树里,只跑dependencies是拉不到的。第 3 步只搬modules-2,因为caches/transforms-*里是产物转换结果,跨机器、跨路径搬运容易失效,让它在新机器上重新生成更省心。
搬到目标机器后解压到$GRADLE_USER_HOME/caches/,再跑./gradlew build --offline验证。如果报某个模块找不到,就回到有网机器上单独解析那个配置,别指望一次性全对——依赖是逐步收敛的,第一次搬完总会有遗漏。
4.3 init.gradle 统一内网仓库
公司内网一般有自建的 Maven 仓库,把所有项目的仓库地址统一起来,比在每个build.gradle里改要省事得多。
// 放在 $GRADLE_USER_HOME/init.d/offline-repos.gradle def internal = "http://repo.internal/maven2" settingsEvaluated { settings -> settings.pluginManagement { repositories { maven { url = uri(internal) allowInsecureProtocol = true // 内网 http 必须显式允许 } gradlePluginPortal() } } } allprojects { buildscript { repositories { maven { url = uri(internal) allowInsecureProtocol = true } } } repositories { maven { url = uri(internal) allowInsecureProtocol = true } mavenLocal() } }说明:init.d目录下的脚本会在每个构建开始前执行,settingsEvaluated保证在 settings 求值完成后才注入插件仓库,时序不对的话pluginManagement会报只读。allowInsecureProtocol = true是 Gradle 6.0 之后对 http 仓库的强制要求,内网环境里不加这一行会直接拒绝解析。顺序上把内网仓库放在最前面,公共仓库放后面做兜底,离线时前面的仓库命中率越高越好。
新项目更推荐在settings.gradle.kts里用dependencyResolutionManagement配RepositoriesMode.FAIL_ON_PROJECT_REPOS,把仓库锁在 settings 层,子项目里写repositories直接编译失败。这样内网规范是强制的,而不是靠自觉。
4.4 把 GRADLE_USER_HOME 迁出系统盘
缓存的目录默认在用户目录下,一台机器上几个项目加上几个版本,几十 GB 很常见,系统盘吃紧时构建会跟着变慢。
# 临时生效,适合 CI 单次任务 GRADLE_USER_HOME=/data/gradle ./gradlew build --offline # 长期生效:写进 shell 配置,同时记得在 IDE 里单独指定 export GRADLE_USER_HOME=/data/gradle迁移时要把wrapper/dists、caches、daemon、notifications一起搬,只搬一部分等于没搬。特别注意daemon/8.6/目录,daemon 的注册信息在那里,搬漏了会看到 daemon 反复重启。还有一点很容易忽略:IDE 里的 Gradle 用户主目录是独立设置项,命令行设了环境变量,IDE 未必跟着走,两边用两套缓存的话,你在命令行预热好的依赖,IDE 里照样要重新下。这个问题不排查会浪费很多时间。
5. 避坑:五份「装完还是跑不起来」的排查记录
下面这几条都是实际踩过的,按「现象 → 原因 → 解决」记,遇到时对照着看比自己猜快得多。
5.1 装了包,构建开头还是卡在 Downloading
现象:日志里反复出现Downloading https://services.gradle.org/distributions/gradle-8.6-all.zip,内网机器上卡到超时失败,但目录里明明有 zip。
原因:缺少gradle-8.6-all.zip.ok标记文件。wrapper 判断「是否已安装」看的是这个标记,不是看目录存不存在、也不是看 zip 在不在。手动放包时漏了touch,它每次都认为上次安装没完成。
解决:进到对应 hash 目录,touch gradle-8.6-all.zip.ok,然后确认gradle-8.6/bin/gradle存在。用 3.2 的自检三条命令走一遍,三条都有输出才算干净。
5.2 包放对了位置,wrapper 又新建一个目录重新下
现象:wrapper/dists/gradle-8.6-all/下出现了两个 hash 目录,一个是你的,一个是空的或者正在下载的。
原因:目录名与当前distributionUrl算出来的 Base36 不一致。最常见的差异有四种:协议从 https 改成 http、后缀从-bin改成-all、URL 末尾多了空格、内网把域名改写成了另一个地址。
解决:用 3.2 的脚本,直接拿 properties 文件里那行 URL(去掉反斜杠转义)重新算一次,把 zip 挪到新目录并补.ok。更省事的办法是先让 wrapper 失败一次,它会把创建的目录建出来,你把这个目录名抄下来直接用。
5.3 启动脚本报找不到主类
现象:Could not find or load main class org.gradle.launcher.GradleMain,或者提示gradle-8.6/lib/xxx.jar不存在。
原因:解压层级错了。常见两种情况:直接把 zip 解压到了 dists 目录,少了一层版本目录;或者在 dists 目录里又建了一层同名目录,变成.../<hash>/gradle-8.6/gradle-8.6/lib。
解决:ls $DIST的输出应该直接看到gradle-8.6/这一个目录加 zip 和.ok文件;进到gradle-8.6/里应该直接看到bin、lib、docs、src。多一层就mv上来,少一层就重新解压到正确位置。
5.4 toolchain 报找不到匹配的 JDK
现象:No matching toolchains found for requested specification: {languageVersion=17},但机器上确实装过 JDK 17。
原因:toolchain 自动探测只认标准安装路径和显式配置的路径。离线环境下 JDK 自动下载(foojay resolver)根本拉不到插件,探测失败就直接报错。
解决:在gradle.properties里显式写路径,并关掉自动下载:
# 路径按实际安装位置改,多个用逗号分隔 org.gradle.java.installations.paths=/opt/jdk-17,/opt/jdk-21 org.gradle.java.installations.auto-detect=true org.gradle.java.installations.auto-download=false改完执行./gradlew -q javaToolchains确认能列出来,再跑构建。
5.5 缓存被锁或权限不足
现象:Could not open cache .../journal-1/file-access.bin、Permission denied,或者构建莫名卡在Waiting to acquire exclusive lock。
原因:用sudo跑过一次,缓存文件属主变成 root,之后普通用户读写不了;或者两个 daemon、两个终端在抢同一份缓存锁。
解决:先./gradlew --stop停掉所有 daemon,再sudo chown -R $(id -u):$(id -g) ~/.gradle把属主改回来。CI 环境里更稳的做法是每个 job 用独立的GRADLE_USER_HOME,从根上避免共享缓存互相干扰。
6. 把 8.6 钉死:distributionSha256Sum 与依赖校验
前面几步解决的是「能不能用」,这一步解决的是「半年后还能不能跑出一样的结果」。发行版和依赖都会漂移,钉死它们要靠两个东西:wrapper 的校验和、以及依赖校验元数据。
第一件事,把 SHA-256 写进 wrapper 配置。这个值来自官方发布的.sha256文件,或者你自己在可信环境里算出来并登记过的那串。
# gradle/wrapper/gradle-wrapper.properties distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip distributionSha256Sum=这里填官方 .sha256 文件里的那串值开启之后,wrapper 在解压前会先校验,内容有任何字节改动都会直接拒绝启动,报错信息里会给出实际值与期望值。代价是每次更换发行版都要同步更新这一行,换来的是「包里被塞了东西」这类问题在启动阶段就暴露,而不是等到构建产出可疑产物才发现。
第二件事,给依赖也加上校验。执行一次下面的命令,Gradle 会把当前解析到的所有 artifact 的摘要写进gradle/verification-metadata.xml:
# 生成 sha256 校验元数据,随项目一起提交 ./gradlew --write-verification-metadata sha256 help # 之后每次构建默认按严格模式校验 ./gradlew build这个文件提交进版本库之后,任何人改动依赖版本、或者仓库里同版本号的内容被替换,构建都会失败并提示校验不通过。维护方式是「有变更就重新生成对应片段」,不要手改,手改容易把整个<components>段落写坏。
再往前一步是版本目录。把 Gradle 版本、插件版本、依赖版本集中到gradle/libs.versions.toml里,构建脚本只引用别名。这样做最实在的好处是,离线环境里要核对「这台机器上到底该有哪些 jar」时,只看一个文件就够,不用在十几个build.gradle里翻。我一般会把 wrapper 的版本号和 toml 里的 plugin 版本放在同一个变更里改,避免出现「gradle 升到 8.6 但插件还停留在老版本」的错配——那种问题在线时表现为警告,离线时往往直接构建失败,而且报错位置和真正的原因隔了好几层。
注意:
distributionSha256Sum一旦写死,换用file:协议指向本地 zip 时同样会校验,所以本地那份 zip 必须是没被工具重新打包过的原始文件。有人用解压软件重新压缩一遍再放上去,校验必然不过。
从那以后我每次往离线环境搬 Gradle,都会强制走一遍:算 hash 目录、放 zip、解压、补.ok、跑--version和javaToolchains两条命令、最后--offline构建一次。这六步花不到五分钟,但省下来的排查时间往往是几个小时。希望帮到你。
本文还有配套的精品资源,点击获取