工程:OverrideDriftGate|任务编号:OD-402|方向:三方库与构建审查
技术栈:ohpm、工程级 oh-package.json5、overrides、Node.js ECMAScript Modules、fs、assert

为什么写了 overrides 还要核对解析结果

维护鸿蒙工程的第三方依赖,我碰到过两类说法:“已经在工程根目录锁了版本,后面不会变”,以及“代码能打包就证明依赖是对的”。这两个判断都有缺口。工程级 overrides 表示开发者希望如何覆盖依赖树中的包,但最终参与某次构建的解析快照究竟包含哪些包、版本是否符合交付合同,仍应该留下可复核记录。更麻烦的是,仓库里可能有人修改了根配置,另一个人更新了依赖状态,提交时只看到一堆文件变化。

这一轮我把问题限定在覆盖策略与规范化解析清单的差集检查。不做漏洞扫描,不对许可证作法律判断,不读取远端仓库,也不声称脚本可以替代 ohpm 的依赖求解。工具取名 OverrideDriftGate,任务编号 OD-402,输入是根目录覆盖规则的一份构造样本,以及一份人为整理过的安装解析快照。两种输入需要由构建流水线提供,工具本身不会假装已经运行过 ohpm install。

样本包含四个示例包。@demo/renderer 期望与解析版本都是 1.4.0;@demo/telemetry 期望 2.1.0,解析快照却填了 2.2.0;@demo/maps 期望 3.0.1,实际快照缺失;@demo/theme 只存在于解析快照,没有对应覆盖规则。于是前三者分别是通过、版本漂移和缺包,最后一项只记录为跳过。预期汇总为 PASS 1 / HOLD 2 / SKIP 1。

根配置与模块配置不能混为一谈

华为开发者文档明确区分工程级和模块级 oh-package.json5。工程级文件位于工程根目录,overrides 可以替换依赖树里相应包的目标版本;如果把覆盖策略误写到模块级文件中,它不会按根覆盖规则生效。官方还支持版本范围、file: 本地依赖、@module: 以及参数化配置。它们都是真实的工程能力,不能因为示例脚本只认识精确版本就断言其他形式非法。

为了减少外部依赖,本次 fixtures/oh-package.json5 故意只使用严格 JSON 语法;它也是 JSON5 的合法子集,Node.js 原生 JSON.parse 可以读取。真实项目中的 JSON5 可能包含注释、未引号字段或尾逗号,直接 JSON.parse 会失败。生产环境应由受控 JSON5 解析器或已验证的工程工具导出规范化对象,不要自己用正则粗暴剥掉注释,尤其不能在字符串中破坏双斜杠地址。

第一段代码把覆盖声明读成对象,并明确校验其最小结构。看上去只是文件读写,实际上它决定了门禁的信任边界:路径来自仓库的受控目录,内容要求可解析且必须有 overrides 对象,任何读取异常都应阻止继续输出“通过”。

import { readFileSync } from 'node:fs'
import { join } from 'node:path'

const here = new URL('./demo/', import.meta.url)
const pkg = JSON.parse(readFileSync(new URL('fixtures/oh-package.json5', here), 'utf8'))
const normalized = JSON.parse(readFileSync(new URL('fixtures/resolved-normalized.json', here), 'utf8'))
if (!pkg.overrides || typeof pkg.overrides !== 'object') {
  throw new Error('missing root overrides object')
}
if (normalized.schema !== 'demo-normalized-resolution-v1') {
  throw new Error('unexpected snapshot schema')
}

注意示例的“解析快照”并不是对 oh-package-lock.json5 的通用解析。锁文件结构、平台字段和层级都可能随实际工具版本变化,随便写一个 JSON.parse 再按猜测字段取版本,比不检查还危险。我宁可要求上游先生成稳定的 name → version 字典,并同时保存上游命令、工具版本、源码提交标识,才能解释“这份快照由谁、在什么环境生成”。本文的字典刻意注明是人工构造,用于示例快照字段的核对方式。

三种风险状态和一种不评价状态

我的门禁只对明确配置了精确覆盖版本的包做严格比较。包名在覆盖规则中存在、解析快照也存在,版本字符完全相同,就标为 PASS;快照中找不到目标包就是 HOLD/MISSING;版本不等则是 HOLD/DRIFT。对快照中额外出现、但没有根覆盖要求的包,我标成 SKIP,它并不是“非法依赖”,只表示本规则不负责它。这样结果有明确边界,避免把未覆盖的正常传递依赖误伤。

第二段代码是核心。将两边包名取并集后排序,保证每次输出顺序稳定。对包含 ^、~、file: 或参数化路径的覆盖值,本示例进入 REVIEW,让人或更完整的解析规则处理,而不是直接给 HOLD。因为它们在 ohpm 中可能完全合法,只是我们还没有建立安全的等价比较方式。

const names = [...new Set([
  ...Object.keys(pkg.overrides),
  ...Object.keys(normalized.packages)
])].sort()
const rows = names.map((name) => {
  const expected = pkg.overrides[name] ?? null
  const actual = normalized.packages[name] ?? null
  if (expected === null) return { name, status: 'SKIP', actual }
  if (!/^\d+\.\d+\.\d+$/.test(expected)) {
    return { name, status: 'REVIEW', expected, actual }
  }
  if (actual === null) return { name, status: 'HOLD', reason: 'MISSING' }
  if (actual !== expected) return { name, status: 'HOLD', reason: 'DRIFT' }
  return { name, status: 'PASS', expected, actual }
})

这里没有把 2.1.0 到 2.2.0 的差异评价为“升级”或“降级风险级别”。语义化版本的大小只是一项线索,是否兼容还依赖库自身承诺、实际 API 行为和对应用目标设备的验证。本工具的判定更简单:仓库写下的精确交付约定是否被满足。

可重放样本与门禁退出码

随包 demo/fixtures 放了一份工程级 JSON5 子集样本和一份规范化快照,demo/override-audit.mjs 是完整可执行工具。使用 node demo/override-audit.mjs 可以打印四行结果并写出 demo/reports/override-report.json;带上 --ci 参数则遇到 HOLD 或 REVIEW 时返回进程退出码二。这个退出码是项目约定,意思是“预检拦截”,不是 Node.js 解释器报错。

第三段展示总结逻辑。先算每一类数量,再用断言保护构造样本,最后把报告写为 JSON。报告中记录任务号、工程名、逐项原因和免责声明,方便后续 CI 页面展示。不必把整份锁文件复制到日志;大型工程中那样做既影响阅读,还可能意外暴露仓库地址、身份标识或内部包名。

import assert from 'node:assert/strict'
const summary = {
  PASS: rows.filter((r) => r.status === 'PASS').length,
  HOLD: rows.filter((r) => r.status === 'HOLD').length,
  SKIP: rows.filter((r) => r.status === 'SKIP').length,
  REVIEW: rows.filter((r) => r.status === 'REVIEW').length
}
assert.deepEqual(summary, { PASS: 1, HOLD: 2, SKIP: 1, REVIEW: 0 })
console.log(`OD-402 PASS=${summary.PASS} HOLD=${summary.HOLD} SKIP=${summary.SKIP}`)
if (process.argv.includes('--ci') && (summary.HOLD || summary.REVIEW)) {
  process.exitCode = 2
}

本文实际用当前环境中的 Node.js 运行了脚本,获得 PASS=1 HOLD=2 SKIP=1。两条 HOLD 分别是 @demo/maps 的 MISSING 和 @demo/telemetry 的 DRIFT。另外单独以 --ci 参数运行时,预期程序退出码为二;不带参数时报告照常写出且退出码为零,便于本地查看。

发布前还需要哪几道人工关口

用在正式构建时,首先应固定生成规范化快照的步骤:工具版本、安装命令、仓库镜像来源和配置参数必须入档。不同机器如果使用不同用户级 .ohpmrc,实际解析上下文可能不同;单凭仓库内的根配置无法推断一切。其次,包含 file:、@param:、@module: 的覆盖规则要由对应的源路径或参数文件核对,不能只比对文本是否相同。

如果仅靠这个脚本就阻止上线,还容易出现一种反向误判:某个包没有被覆盖,正常随依赖树升级,我们把它标为 SKIP,并不意味着它一定安全。生产流程还需要许可审查、漏洞公告、兼容性测试、制品签名和实际构建结果等独立证据。本篇刻意不扩展那些方向,是为了让每一个状态都能清楚解释。

异常处理上,读取失败、JSON 解析失败、解析快照 schema 不符合预期都应让脚本以错误退出,绝不产生成功报告。输出到 CI 时最好先写临时文件再原子替换,避免构建被中断后留下半份 JSON。本例以单次脚本写文件,Node.js 退出后没有持续监听和额外资源需要释放;加入文件监控或并行扫描后,必须关闭监听器并限制并发读盘。

与前几轮许可证材料、资源大小、大小写冲突审计相比,这轮观察的是版本覆盖意图和解析版本事实。他们能够组成交付清单,但不应混为同一项“依赖安全检测”。我希望在 Review 中看到的是一眼可读的差异:哪些包符合覆盖合同,哪些无法对应,哪些暂不作结论,而不是一个只有红绿颜色的笼统评分。

官方资料与能力边界

  • 华为官方《oh-package.json5》:工程级配置与模块级配置、overrides 的生效范围及版本/本地路径形式,页面显示更新于 2026-10-16:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-hmos-oh-package-json5
  • 同一页面的 oh-package-lock.json5 与 overrideDependencyMap 章节可供进一步研究,
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐