多目标资产构建:单目标重建不能覆盖其他制品

自动关联目录:多目标资产构建:单目标重建不能覆盖其他制品

核心判断:多目标构建的提交单元不是“本次命令产出的目录”,而是“一个逻辑资产修订下的完整变体集合”;单目标重建只能替换自己的槽位,不能把其他已验证变体从索引中抹掉。

一、业务冲突:局部重建不能破坏全局可用性

一个源资产需要被多个运行目标消费。完整构建已经产出桌面、移动端和轻量目标的制品后,开发者为了快速预览,只重建其中一个目标。

最直接的实现是用“本次构建结果”重写顶层索引:

index.variants = artifactsBuiltThisRun

命令本身会成功,刚构建的目标也能加载,但其他目标会从索引中消失。它们的制品字节可能仍在磁盘上,系统却不再承认其存在。

这不是普通的增量构建问题,而是发布语义错误:一次局部操作覆盖了比自己更大的事实范围。

二、实际实现:目标槽位独立发布,顶层索引合并

现有实践已经形成了较完整的防线:

flowchart LR
    A[冻结源身份] --> B[目标私有 staging]
    B --> C[生成 Manifest 与包]
    C --> D[独立校验 staging]
    D --> E{目标槽位已存在?}
    E -->|同身份且校验通过| F[视为缓存命中]
    E -->|不存在| G[原子移动到目标槽位]
    E -->|身份冲突| H[失败,不覆盖]
    F --> I[读取其他已验证目标槽位]
    G --> I
    I --> J[合并并稳定排序变体]
    J --> K[临时索引原子替换正式索引]

每个平台制品都携带足够的身份和验证信息:逻辑资产、内容修订、源摘要、目标配置、工具链指纹、包摘要和大小等。构建先在作业私有目录完成,再由独立读取路径校验;只有验证通过,才进入规范目标槽位。

顶层索引不是只写本次输出。它还会扫描未参与本次构建的规范目标槽位,并且只保留满足以下条件的旧变体:

  • Manifest 和包字节可以独立验证;
  • 逻辑资产与内容修订一致;
  • 源摘要和构建配置一致;
  • 根对象及文件计数等关键身份一致。

因此,单目标重建实际执行的是“替换一个槽位并合并其余有效槽位”。

三、值得保留:正确性已经进入提交协议

这个实现有四点值得保留。

第一,staging 隔离。 构建失败不会在正式目录留下半个包,处理阶段也不能直接污染可用结果。

第二,Writer 与 Verifier 分离。 产物不是因为“刚写完”就可信,而是重新从 Manifest 和字节验证。它能发现写入逻辑自洽但 Reader 实际不可消费的问题。

第三,已存在目录不静默覆盖。 同身份且同内容才复用;身份冲突直接失败。这避免两个构建把同一规范位置解释成不同结果。

第四,索引按集合语义更新。 旧变体只有经过重新验证并证明属于同一源身份才会保留。增量构建因此不是“少做几步”,而是“复用可证明等价的结果”。

四、结构性问题:平台目录和顶层索引仍是两次提交

当前方案解决了“单目标重建抹掉其他目标”,但还没有形成真正的跨文件事务。

1. 目标槽位与索引存在崩溃窗口

新平台目录先原子移动成功,顶层索引随后才原子替换。若进程在两步之间终止,会出现两种状态:

  • 新制品有效但索引未引用,形成孤儿变体;
  • 重试时发现目标槽位已存在,需要额外判断它是可复用结果还是冲突残留。

单次 rename 只能保证一个路径切换,不能自动把两个独立路径变成事务。

2. 合并谓词是隐含兼容协议

“哪些字段相等才可保留旧变体”散落在校验逻辑中。未来新增影响字节语义的字段,如果忘记加入身份比较,旧制品可能被错误复用;如果一律失效,又会造成不必要的全量重建。

3. 并发 Writer 仍需显式仲裁

两个进程同时读取旧索引、分别构建不同目标,再各自写回时,后提交者可能覆盖先提交者刚加入的变体。作业内隔离不能解决跨作业的 lost update。

4. 失败只有“拒绝”,缺少修复闭环

发现规范槽位存在但身份冲突时,立即失败是正确的默认行为;但系统还需要明确的隔离、诊断和垃圾回收工具,否则人工删除目录会重新引入不可审计状态。

五、推荐方案:不可变 generation + 单指针提交

更完整的方案是把一个逻辑修订的全部平台变体写入新的不可变 generation,最后只切换一个当前指针:

PublishVariant(asset, revision, target, candidate):
    verified = independentlyVerify(candidate)
    base = loadCurrentGeneration(asset, revision)

    next = cloneVerifiedVariants(base)
    next.variants[target] = verified
    next.identity = canonicalBuildIdentity(revision, toolchain, profile)

    generationDir = writeImmutableGeneration(next)
    verifyGenerationIndex(generationDir)

    compareAndSwapCurrent(
        expectedGeneration = base.id,
        newGeneration = generationDir.id)

关键变化有两个:

  • 平台目录和变体索引位于同一个不可变 generation 中,构建完成前对 Reader 不可见;
  • current 指针通过比较并交换更新,两个并发 Writer 不会静默覆盖彼此,失败者必须基于新 generation 重新合并。

内容寻址的包可以跨 generation 复用,generation 只保存小型索引和引用。这样既保留增量构建,也把最终提交收敛为一次原子切换。

六、取舍与验证结论

目标槽位合并方案实现成本较低,也已经能正确支撑串行的单目标重建;不可变 generation 则增加了索引、引用计数、并发控制和垃圾回收成本。是否升级,取决于是否已经出现并发构建、频繁崩溃恢复或严格版本回退需求。

现有自动化验证已经覆盖了核心业务合同,但仍有明确缺口:

场景已证明的结果仍需补充
一次生成多个目标各变体共享逻辑身份,包可独立校验处理器升级后的精确失效范围
随后只重建一个目标其他有效变体仍保留在顶层索引新旧变体并发写入的冲突处理
staging 校验失败不发布候选制品每个文件系统上的崩溃恢复测试
已存在同身份制品可验证后复用冲突制品的隔离与修复工具
临时索引写入正式索引不暴露半写内容平台目录已提交、索引未提交的断电窗口

我的反思是:过去容易把“命令成功”“包已生成”“单平台能加载”当作构建完成。更准确的验收对象应该是提交后的资产版本集合:所有被索引的变体是否身份一致、可独立验证,局部重建是否只改变授权范围,任意中断点是否仍保留上一代完整状态。

决策规则很直接:

  • 单进程、低并发工具链,可以采用“平台槽位原子发布 + 校验后合并索引”;
  • 一旦需要并发构建、可靠回退或严格崩溃一致性,就应升级为“不可变 generation + 单指针提交”;
  • 无论采用哪种方案,都不能用本次命令的输出集合直接覆盖资产的完整变体集合。

资产身份的前置判断见:资产身份:路径能找到文件,但不能证明文件正确。运行时获取的所有权问题见:共享下载:工作可以合并,所有权不能合并。