Package net.fabricmc.loom.util.cache
Class ArtifactReuse
java.lang.Object
net.fabricmc.loom.util.cache.ArtifactReuse
「这件(批)产物已经由**这批输入**生成过」的判据:把生成时的输入指纹记在产物上,下次开工前比一遍.
解决的是什么问题
流水线里有一批任务的产物落在跨项目共享目录(<userCache>/<mcVersion>/)。对其中一个项目来说,
产物是「本项目从来没生产过的文件」,因此 Gradle 的 up-to-date 判定与构建缓存都帮不上忙
(对它们而言是 unknown output)——每个新项目首次构建都要把这些产物从头重算一遍,
哪怕它们就完好地躺在盘上。本类提供的判据是这些任务 action 的**前置判据**:输入没变且产物完好时直接返回。
为什么不能只看「文件存在」
判据失效的方向是不对称的:多算一次只是慢,而「误判可复用」会拿陈旧产物一路传给后续环节, 形成「标记是新的、内容是旧的」的静默损坏。所以判据必须能**证明输入没变**,而不只是证明产物在场。判据的三层
- 产物齐备且完好:交给
JarReusability.isReusable(Path)——与共享缓存其余就绪判据 同一把尺子(能作为 zip 打开且至少一个条目)。一批里有任何一个产物不完好即整体重跑。 - 产出规则未换代:
recipe是调用方给的产出规则身份(形如merge-jars/v1)。 产出**实现**变了而输入没变也绝不能复用,这一层靠纪律而非校验守住: 改了产出规则就必须递增自己那个 recipe 的版本号。 - 输入与产物都没变:见下面「两套身份,先廉价后精确」一层。
两套身份,先廉价后精确
一次构建的热路径不允许把输入 jar 整包读一遍(客户端 jar 20MB 级,实测 sha256 约 100ms), 而这条判据本身就发生在「本来要重算几秒到几十秒」之前。因此每一项都记两套身份:- 记录里存下
(size, lastModified, fileKey)与内容sha256; - 判定时先比 stat 三元组。**全等就认定是同一份内容,不再算摘要**——暖路径因此只有几次 stat 与一次小属性读,不读任何 jar;
- 三元组有任一不等就真的比
sha256:内容仍相同(例如下载走了 304 刷新了 mtime、 或同一份内容被另一次原子落位重新发布)照旧可复用,内容不同则判为不可复用。
AtomicFiles(或 Download 的 part 文件)原子落位——rename(2) 之后目标路径换成了
临时文件那个 inode(fileKey 必变)。也就是说,同一 inode 上的内容不会被就地改写换代。
反过来,若某处开始「就地重写这些 jar」,快速通道就会失效;那时唯一还能兜住的是慢通道的 sha256
——它虽然只在三元组不等时才跑,但就地重写必然改 mtime,所以仍然会真的比一次摘要。
两层合起来:「跳过摘要」只在不该变的文件确实没变时才发生。
标记存在哪里
用AttributeHelper(LoomHash / ETag / loom-target 的同一套载体):
支持的平台上写用户扩展属性,不支持时自动退化为同目录 <jar>.<属性名>.att 旁车文件。
两种形态都跟着产物走,因此跨进程、跨 daemon、跨项目都读得到。标记写在**锚点产物**上
(一批产物里的第一个,通常是消费侧最先读的那件)。
与原子发布的关系
本类只写「标记」这层元数据,从不删改产物内容,也从不就地重写:isReusable(java.nio.file.Path, java.lang.String, java.util.List<java.nio.file.Path>, java.util.List<java.nio.file.Path>)全程只读(不删、不改、不创建);record(java.nio.file.Path, java.lang.String, java.util.List<java.nio.file.Path>, java.util.List<java.nio.file.Path>)必须在全部产物内容已经完整落位之后调用(即AtomicFiles.publish(java.nio.file.Path, net.fabricmc.loom.util.cache.AtomicFiles.IOConsumer<java.nio.file.Path>)与AtomicFiles.move都返回之后)。内容先原子落位、标记后落位,中断只会留下 「有内容、没标记」的产物 → 下次判为不可复用并重算,方向是安全的。
-
Method Summary
Modifier and TypeMethodDescriptionstatic booleanReturns 这(批)产物是否可以原样复用(输入没变 + 产物齐备且完好).static void把「这(批)产物由这批输入生成」记到锚点产物上,供下次isReusable(java.nio.file.Path, java.lang.String, java.util.List<java.nio.file.Path>, java.util.List<java.nio.file.Path>)判定.static voidreportReused(Path anchor, String recipe) 播报「复用已有产物、跳过重算」.
-
Method Details
-
isReusable
Returns 这(批)产物是否可以原样复用(输入没变 + 产物齐备且完好)..所有「不可信」都归到
false,由调用方照常执行生产:任一产物缺失、损坏或被截断、锚点上没有标记、 标记格式不认识、recipe已换代、产物大小/内容与记录不符、输入数量或顺序变了、任一输入的摘要对不上。 判true只在**全部**证据都指向「就是这一批」时成立。本方法只读:不删、不改、不创建任何文件。除「本 JVM 之后再也打不开该路径」这一条 (
FileSystemUtil.UnrecoverableZipException,与JarReusability一致地向上抛, 否则上层会陷入「重建 → 仍然打不开」的循环)外,任何异常都归到false。- Parameters:
anchor- 标记所在的产物;必须属于outputsrecipe- 产出规则的身份(形如merge-jars/v1);规则变了要递增版本号outputs- 本次生产会写出的全部产物inputs- 生成本批产物所依赖的全部输入;顺序必须与record(java.nio.file.Path, java.lang.String, java.util.List<java.nio.file.Path>, java.util.List<java.nio.file.Path>)时一致- Returns:
- 这(批)产物是否可以原样复用(输入没变 + 产物齐备且完好)
-
record
public static void record(Path anchor, String recipe, List<Path> outputs, List<Path> inputs) throws IOException 把「这(批)产物由这批输入生成」记到锚点产物上,供下次isReusable(java.nio.file.Path, java.lang.String, java.util.List<java.nio.file.Path>, java.util.List<java.nio.file.Path>)判定.**必须在全部产物内容已经完整落位之后调用**(
AtomicFiles.publish(java.nio.file.Path, net.fabricmc.loom.util.cache.AtomicFiles.IOConsumer<java.nio.file.Path>)与AtomicFiles.move都返回之后)。顺序反过来的话,中断会让标记先就位、内容后缺席,下一次构建就会把一批残缺产物判为可复用。只写标记(用户扩展属性或旁车文件),不改产物内容、不改其 mtime。标记写不进去时应当让它抛出来: 那样下次仍会重算(方向安全),但用户需要知道共享目录不可写。
- Parameters:
anchor- 标记写到哪件产物上;必须属于outputsrecipe- 产出规则的身份;与isReusable(java.nio.file.Path, java.lang.String, java.util.List<java.nio.file.Path>, java.util.List<java.nio.file.Path>)传入的必须逐字相同outputs- 本次生产写出的全部产物inputs- 生成本批产物所依赖的全部输入;顺序参与指纹,两次调用必须一致- Throws:
IOException- 读不到产物或输入的元数据、或标记写不进去时
-
reportReused
播报「复用已有产物、跳过重算」.这行日志是本次优化的唯一观测点:没有它,「任务跑了但什么都没干」看起来与「任务根本没被调度」 一模一样。级别取 LIFECYCLE(Gradle 默认可见),与
JarReusability播报「不可复用」的级别对称。- Parameters:
anchor- 被复用的锚点产物recipe- 产出规则的身份
-