ohpm + oh-package-lock.json5:三方依赖重复版本、来源漂移与发布快照审计【鸿蒙心迹】
依赖安装成功,不等于依赖状态适合发布。一个工程在开发机上能编译,换到流水线却解析出不同版本;两个模块都依赖同名库,最终树里却留下两份;锁文件改了几百行,评审者只看到“Sync 后自动变化”。这些问题往往没有立即报错,却会把差异推迟到包体、运行行为或下一次升级。
本篇做了一个发布前诊断 Demo LockLens。它不替代 ohpm,也不自行实现版本解析,而是读取 ohpm 可观察的依赖树与当前锁定快照,生成一份更适合代码评审的摘要。演示审计编号 lock_20261001_05,时间 11:27,直接依赖 18 项,解析节点 31 项,重复版本组 2 个,来源漂移 1 项,总状态 BLOCKED。示例包名以 @demo/ 开头,明确是教学数据,不代表真实仓库版本。

一、真正要审的是“本次会发布什么”
oh-package.json5 描述的是开发者希望使用的依赖范围。^1.4.0、~1.4.0 或固定版本表达不同更新边界,但它们仍然不是最终解析结果。锁文件和已安装依赖树才更接近“这次构建实际看到了什么”。
因此 LockLens 不用“配置文件里只有一个名字”证明依赖唯一。它同时看四类事实:
- 直接声明:工程与各模块的
dependencies、devDependencies和动态依赖; - 解析结果:通过
ohpm list获得的父子关系与安装版本; - 锁定来源:版本对应的解析来源与完整性信息是否发生非预期变化;
- 发布基线:上一次确认过的快照与本次候选构建之间有哪些差异。
这四类信息回答的问题不同。直接声明能找到责任模块,依赖树能找到重复版本的父链,来源信息能识别同版本却换了仓库或本地路径,基线则让评审者知道变化是不是本次需求的一部分。
官方资料说明 ohpm 可以列出依赖关系,支持依赖去重、冲突处理和 overrides。工具的能力不等于工程可以不做判断:自动选择较大版本可能让安装继续,但上层库是否兼容仍需要验证;overrides 能统一版本,也可能把一个真实不兼容问题压成表面整齐。
二、声明范围先保守,更新动作再单独发生
演示工程有 entry、feature-gallery 与 common-net 三个模块。问题包 @demo/image-cache 被两条父链带入,版本分别为 1.4.2 与 1.5.0;@demo/net-core@2.3.1 的版本没变,但来源从团队镜像变成了临时文件地址。
这段配置解决什么问题:把直接依赖的意图留在版本库里,不用模糊的“latest”代替发布判断。
{
"name": "entry",
"version": "1.0.0",
"dependencies": {
"@demo/gallery-shell": "1.2.0",
"@demo/net-core": "2.3.1"
},
"devDependencies": {
"@demo/mock-server": "0.8.0"
}
}
示例使用固定版本,是为了让审计数据容易复现,不是在要求所有项目永远固定依赖。官方 ohpm update 按 semver 规则处理 ^、~ 和 tag;团队完全可以使用范围声明,但更新应成为一个明确动作:单独提交锁文件变化、生成差异报告、完成回归,再进入发布分支。
最难评审的是把业务代码与大规模锁文件更新混在一个提交里。即使每个变化都合法,评审者也很难判断是哪条直接依赖触发了 20 个传递节点变化。更实际的做法是把升级拆成独立提交,并让工具输出“根依赖 → 传递依赖 → 解析版本”的路径。
dependencies 与 devDependencies 也不能混看。开发工具出现在发布依赖里可能增加包体或引入不必要代码;反过来,运行时需要的库误放到开发依赖,可能在不同构建环境中暴露问题。动态依赖还有自己的装载语义,审计时应保留分类,不能把三组键合并后丢掉来源。
三、先让 ohpm 输出树,再由适配层归一化
直接解析 oh_modules 目录结构很脆弱。包管理器的安装布局、去重方式和目标路径可能变化,脚本不应该把磁盘目录当成稳定 API。LockLens 优先执行 ohpm list --json 获取机器可读结果,并把命令、客户端版本与工作目录写入报告头。
不同 ohpm 客户端的 JSON 字段应以当前官方文档与实际输出为准,所以工具把原始结果留档,再通过一个小适配器转换成自己的 ResolvedNode。后续重复版本分析只依赖内部结构,客户端升级时只改适配层。
这段 TypeScript 解决什么问题:遍历归一化后的依赖树,记录每个“包名 + 版本”的父链,避免只看到重复数量却找不到是谁带进来的。
interface ResolvedNode {
name: string;
version: string;
source?: string;
dependencies: ResolvedNode[];
}
interface SeenVersion {
version: string;
paths: string[];
}
function collect(
node: ResolvedNode,
path: string[],
index: Map<string, Map<string, string[]>>
): void {
const current = [...path, `${node.name}@${node.version}`];
const versions = index.get(node.name) ?? new Map<string, string[]>();
const paths = versions.get(node.version) ?? [];
paths.push(current.join(' > '));
versions.set(node.version, paths);
index.set(node.name, versions);
node.dependencies.forEach((child) => collect(child, current, index));
}
function duplicateGroups(index: Map<string, Map<string, string[]>>): string[] {
return [...index.entries()]
.filter(([, versions]) => versions.size > 1)
.map(([name]) => name)
.sort();
}
Map<包名, Map<版本, 父链[]>> 看起来比一个计数器复杂,但报告需要回答“怎么来的”。@demo/image-cache 有两个版本并不可直接判定错误;若两个上层库分别限定了不兼容范围,强制去重反而可能破坏其中一个。工具先阻断候选发布,再让开发者检查两个父链的约束与变更记录。
递归还要防循环与异常深度。正常依赖图会被包管理器处理,但适配器面对损坏或非预期 JSON 时仍应设置访问标记和节点上限。报告工具不能因为一份坏输入占满内存,最终让流水线只留下“脚本退出”而没有诊断信息。

DevEco Studio 演示图中,左侧是 LockLens 工程与脚本,中间标出父链收集逻辑,右侧模拟器显示 BLOCKED,底部日志固定为 audit=lock_20261001_05 direct=18 resolved=31 duplicates=2 drift=1。这张图解释本地诊断界面,不冒充 DevEco 编译或正式发布记录。
四、重复版本与来源漂移要分开判
两个版本同时存在,属于解析结果差异;同一个版本的来源发生变化,属于供应链与复现性差异。把它们都叫“版本冲突”,会让修复动作变得混乱。
来源漂移的典型情况包括:仓库地址变化、本地 file: 依赖进入候选发布、HAR 路径从稳定目录切到个人目录,或者同版本元数据对应的完整性信息变化。工具不需要推断恶意,只要把变化标出来并要求解释。
这段代码解决什么问题:把本次归一化快照与已确认基线比较,分别输出版本变化和来源变化。
interface LockRecord {
name: string;
version: string;
source: string;
integrity: string;
}
function diffLock(
before: Map<string, LockRecord>,
after: Map<string, LockRecord>
): string[] {
const issues: string[] = [];
for (const [key, next] of after) {
const prev = before.get(key);
if (!prev) continue;
if (prev.version !== next.version) {
issues.push(`${key}: VERSION ${prev.version} -> ${next.version}`);
} else if (prev.source !== next.source ||
prev.integrity !== next.integrity) {
issues.push(`${key}: SOURCE_DRIFT`);
}
}
return issues.sort();
}
这里比较的是适配器生成的 LockRecord,不是声称 oh-package-lock.json5 必然使用这些字段名。真实工具应把当前锁文件结构转换后再比较,并把原始片段位置附在报告里。这样既能隔离格式变化,也不会隐藏证据。
演示中的漂移项是 @demo/net-core@2.3.1:版本相同,来源却从团队镜像变为 file:../temp/net-core.har。如果只是按版本号比较,它会被当成“没有变化”。对于候选发布,这种变化至少要确认临时文件是否可在干净环境解析、是否纳入版本控制、是否与镜像包内容一致。
五、修复重复依赖前,先看两条父链是否可兼容
@demo/image-cache@1.4.2 来自 gallery-shell,1.5.0 来自 preview-kit。处理顺序不是立即写 overrides,而是先回答三个问题:两个上层库各自声明了什么范围;1.5.0 是否兼容 1.4.2 的调用;重复版本是否真的进入同一运行边界。
若上层约束存在交集,可以升级较旧的直接依赖并重新解析,让包管理器自然去重。若没有交集,需要升级上层库、提交兼容补丁或暂时接受双版本。接受双版本也要有记录:包体影响、类型身份风险、序列化边界和清理日期。
官方资料提供 overrides 机制,可以把依赖树中的版本替换为具体版本、模糊版本、本地 HAR 或源码目录。它适合经过兼容验证后的集中约束,不适合作为“让报告变绿”的第一步。强制替换后至少要跑受影响模块的构建、启动与关键用例,并保留替换原因。
严格冲突检查同样如此。当前官方 ohpm update 文档列出了严格冲突处理选项及适用客户端版本。流水线启用前应先固定 ohpm 客户端范围,并在报告中打印 ohpm --version。否则开发机不认识参数、CI 认识参数,工具本身就制造了环境差异。

11:27 的运行页使用同一份快照:18 个直接依赖、31 个解析节点、2 个重复版本组、1 个来源漂移,状态为 BLOCKED。红色箭头分别指向重复组与漂移项,让“为什么阻断”在手机页上也能读懂。
六、发布快照不应该由 UI 自己拼
诊断页只是报告的一个消费者。真正的事实文件应由命令行脚本生成,包含审计编号、提交哈希、ohpm 版本、模块范围、原始命令、重复父链、来源差异和最终状态。ArkUI 页面读取精简后的 JSON,用于开发版演示;CI 则直接根据进程退出码判断。
这段 ArkTS 代码解决什么问题:把机器报告映射成稳定的诊断视图,不在页面里重新计算依赖结论。
interface LockAuditSummary {
auditId: string;
status: 'PASSED' | 'BLOCKED';
direct: number;
resolved: number;
duplicateGroups: number;
sourceDrift: number;
}
@Entry
@Component
struct DependencyReportPage {
@State summary: LockAuditSummary = {
auditId: 'lock_20261001_05',
status: 'BLOCKED',
direct: 18,
resolved: 31,
duplicateGroups: 2,
sourceDrift: 1
};
build() {
Column({ space: 12 }) {
Text(`依赖审计 ${this.summary.auditId}`)
Text(this.summary.status).fontColor('#C62828')
Text(`直接依赖 ${this.summary.direct}`)
Text(`解析节点 ${this.summary.resolved}`)
Text(`重复版本组 ${this.summary.duplicateGroups}`)
Text(`来源漂移 ${this.summary.sourceDrift}`)
}.padding(24).alignItems(HorizontalAlign.Start)
}
}
这个页面只适合开发构建或独立诊断应用。正式安装包不应携带内部依赖路径、仓库地址和完整性信息。报告可以作为流水线附件保存,页面资源则按 variant 排除。
状态的计算也应集中在脚本里:存在未批准的重复版本、来源漂移或本地路径依赖时返回非零退出码;已登记并未过期的例外可以带理由放行。UI 只能展示结果,不能因某个卡片被关闭就把 BLOCKED 改成 PASSED。

详情页列出两条父链:gallery-shell → image-cache@1.4.2 与 preview-kit → image-cache@1.5.0,再单独展示 net-core@2.3.1 的来源漂移。建议动作是“核对兼容范围后再决定升级或 overrides”,而不是直接给出一个看似万能的版本号。
七、锁文件评审要控制噪声
锁文件变化大,不代表只能跳过评审。工具可以先做稳定排序,再把差异按“新增、删除、版本变化、来源变化、完整性变化”分组。每组只展示直接责任链与关键字段,完整原始 diff 作为附件保留。
生成快照时要确保确定性。同一提交、同一 ohpm 客户端、同一参数连续执行,节点顺序和路径表示应该一致。绝对路径统一转成仓库相对路径,避免开发机用户名进入报告;时间戳放在元数据,不参与内容摘要。
不要在审计失败后自动删除锁文件重装。官方 ohpm clean 默认会清理安装产物和锁文件,并提供保留锁文件选项;删除锁文件意味着重新解析,可能让原问题消失,也可能产生一批全新变化。诊断阶段应先保存现场,再决定是否重新解析。
若确实要验证可复现性,可以在干净工作区使用已提交的配置和锁文件安装,然后比较归一化快照。这个验证与“本机清缓存后能装上”不是一回事:前者确认版本库足以重建依赖,后者可能只是修复了本地状态。
八、工具能阻断什么,不能替你决定什么
LockLens 可以阻断未解释的双版本、来源漂移和快照变化,却不能判断某个库是否安全,也不能证明两个版本一定不兼容。漏洞信息、许可证、维护状态和运行行为需要其他证据。
它也不替代 ohpm 的解析。版本范围、冲突处理、去重和 overrides 都由包管理器负责;本工具读取结果、保留父链、把变化变成发布决策。脚本如果开始自行选择“最大版本”,就越过了观察边界,容易与真实安装结果产生第二套规则。
本文没有执行真实 ohpm 安装、DevEco 编译或应用市场发布。示例包、版本、31 个节点和阻断结果都是一致性演示。接入项目时,应先用当前客户端的 ohpm list --json 输出校准适配器,再用一个已知依赖树做回归。
依赖治理最怕两种极端:锁文件完全不看,或任何变化都禁止。更好的做法是把变化缩成可解释的几类,让升级有入口、漂移有证据、例外有期限。这样三方库不是一团“Sync Now 生成的文件”,而是一份能进入版本评审的发布快照。
九、给每种差异一个不同的处理出口
如果所有异常都只显示 BLOCKED,工具用久了就会失去指导性。报告应把阻断状态与建议动作分开:状态决定能否继续发布,建议动作告诉开发者下一步收集什么证据。
新增直接依赖要检查责任模块、用途、许可证和引入范围;新增传递依赖要显示父链,确认它是否随预期升级进入;版本变化要同时给出旧版本、新版本和声明范围;来源漂移要比较仓库、本地路径与完整性信息;依赖删除则要确认业务代码、资源和混淆配置没有残留。
重复版本组需要更细的出口。若两个版本的约束有交集,建议升级上层库并重新解析;若没有交集,报告要求登记兼容风险,而不是自动 overrides;若两个版本分别只存在于开发与生产边界,先确认最终产物是否都包含,再决定阻断级别。这样的建议不替开发者做决定,却能防止所有问题都被一句“统一版本”处理。
本地路径依赖进入候选发布通常应高优先级阻断,因为干净环境未必存在相同文件。例外是仓库内受版本控制的本地模块或 HAR,此时路径必须转为仓库相对位置,并验证实际包名与依赖键一致。官方 FAQ 也提醒依赖名称与包内 name 不一致会导致构建问题,审计器可以把这项作为单独规则。
tag 依赖也值得单列。tag 指向可以变化,同一份 oh-package.json5 在不同时间可能解析出不同版本。若团队允许 tag,只能依赖已提交锁文件保持结果,并在主动更新时把 tag 对应变化写进报告。候选发布阶段不应悄悄重新解析 tag 后覆盖旧快照。
豁免机制必须有到期时间。某个双版本因上游暂未修复而接受,可以记录负责人、影响模块、验证用例和复查日期;到期后恢复阻断。没有期限的忽略项会把报告变成“已知但永远不处理”的清单,而且新父链混入同一包名时也容易被旧豁免遮住。
十、把依赖快照接入团队流程
本地阶段,开发者修改 oh-package.json5 后运行同步,再执行 ohpm list --json 和 LockLens。报告出现变化时先看父链,不急着提交锁文件。确认属于本次需求后,把配置、锁文件和精简报告放进同一提交。
合并请求阶段,流水线在干净环境安装依赖,生成第二份快照,与提交中的预期报告比较。如果本地与 CI 的归一化结果不同,优先检查 ohpm 客户端、参数文件、仓库配置和工作目录,而不是继续构建。客户端版本本身必须进入报告,否则“同一命令”可能不是同一行为。
候选发布阶段,只允许已确认快照进入构建。这里不再自动执行无范围的 ohpm update,因为发布构建的目标是复现,不是顺手追最新。确需升级时退回依赖更新流程,重新生成报告并完成回归。
归档阶段,保存精简 JSON、原始 ohpm list 输出、锁文件摘要和提交哈希。无需永久保留整个 oh_modules,那既大又受平台影响;能重建判断过程的文本证据更有价值。若以后出现版本差异,可以知道当时解析了什么,而不是依赖开发机缓存。
多模块工程还要明确扫描根。只在 entry 目录执行,可能看不到其他 feature 的直接依赖;盲目递归整个仓库,又可能把样例、测试夹具和工具工程算进发布。可以从 build-profile.json5 的模块配置建立候选集合,再按构建 product 选择实际模块。报告顶部列出扫描范围,让“18 个直接依赖”有清晰口径。
最后给报告本身做回归。准备三份固定夹具:无差异、双版本、同版本来源漂移。每次升级 ohpm 客户端或修改适配器,都对三份输入生成快照并比较预期。否则诊断工具可能在包管理器变化后静默漏报,而团队仍把绿色状态当作可靠信号。
十一、一次可复查的发布判断长什么样
对 lock_20261001_05,结论不是“ohpm 有冲突”,而是三条可以执行的判断:image-cache 出现两个解析版本,需要核对两条父链的范围交集;net-core@2.3.1 版本未变但来源漂移,需要恢复团队镜像或证明本地包等价;在解释完成前,候选发布保持阻断。
修复后,新的报告应明确显示重复版本组从 2 降到预期值、来源漂移从 1 归零,并附上对应提交。若团队决定保留某个双版本,数量可以不归零,但必须由未过期豁免覆盖。PASSED 因此不是“树里没有复杂情况”,而是“所有差异都有当前版本认可的证据”。
这种口径比追求绝对整齐更适合大型工程。依赖树天然会复杂,工具的工作不是把复杂性藏起来,而是让每次发布都能回答:哪些变化是有意的,哪些变化尚未解释,谁将在什么时间处理。能回答这三个问题,锁文件才真正进入工程管理。
十二、参考资料
- 华为开发者文档:ohpm list
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-ohpm-list - 华为开发者文档:ohpm update
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-ohpm-update - 华为开发者文档:ohpm clean
https://developer.huawei.com/consumer/cn/doc/doccenter-deveco-studio/ide-ohpm-clean - 华为开发者服务:OHPM 三方库与 overrides、去重能力
https://developer.huawei.com/consumer/cn/deveco-service
更多推荐




所有评论(0)