HarmonyOS交付审查连载03:依赖锁定与包体摘要对账【共创稿事节】
应用的发布配置和权限关系都核对过,打包前却仍可能发生一种不容易察觉的变化:研发同事没有修改业务代码,依赖更新却使构建输入发生了漂移。oh-package.json5中写的是允许范围,安装目录反映的是当前机器取得的结果,实际交付包又属于另一个层次。若把这三处混成“依赖已经检查”,版本回溯时往往找不到差异从何而来。
这次继续完善交付审查工作台。前两篇留下的产品快照和功能权限矩阵保持不变,新增依赖清单、安装锁定信息与候选包指纹三组证据。目标是在同一屏幕说明“声明了什么、解析到了什么、最终提交的是哪一个文件”,并且在发现不一致时指向能够修改的工程位置。

声明版本不等于落地版本
oh-package.json5允许在工程或模块的dependencies中使用精确版本、范围版本和本地模块引用。例如^2.0.0表达了一个版本范围,不是安装结果的精确标识。模块引用和本地HAR也不能照搬远程包的哈希核对方式:前者更依赖被引用目录的内容,后者需要检查输入文件本身。给审查工具设计数据模型时,应当让依赖来源和来源类型成为显式字段。
export type DependencyKind = 'registry' | 'module' | 'file'
export interface DeclaredDependency {
moduleName: string
packageName: string
requested: string
kind: DependencyKind
}
export interface ResolvedDependency {
packageName: string
resolvedVersion: string
source: string
}
export interface DependencyFinding {
path: string
severity: 'block' | 'review'
message: string
}
export function inferKind(specifier: string): DependencyKind {
if (specifier.startsWith('@module:')) return 'module'
if (specifier.startsWith('file:')) return 'file'
return 'registry'
}
模型里的moduleName特指声明位于哪个工程模块,避免两个模块依赖同一个库却被合并成一条记录。source记录锁定结果的来源,不承担下载行为。发布包检查器接收这些归一化对象;读取JSON5、查看oh-package-lock.json5和执行命令属于开发机侧的采集层。这样分离以后,ArkTS页面没有访问本地工程磁盘的隐式前提。
依赖收集要有稳定的比较顺序
收集入口按当前构建产品引用到的模块逐个处理。对每个模块先读取自身的oh-package.json5,取出dependencies;随后读取工程本次使用的锁文件并建立解析结果索引。devDependencies是否参与目标构建,应以实际的构建模式和模块类型为准,不能简单认为所有字段都会进入HAP。华为开发者在依赖项指南中还特别说明,不同字段对HAR、HSP构建的收集行为可能不同,因此应优先把当前模块实际依赖配置放在模块级。
比较逻辑不必和包管理器重新实现一套版本求解。审查器关注的是“记录是否齐全”和“本次交付相比基线是否发生变化”。对远程依赖,它核对名称与最终解析版本;对本地依赖,它保留引用路径和文件摘要;对依赖名称变更,它同时给出新增与移除,不把改名误写成升级。
export function compareDependencies(
declared: DeclaredDependency[],
resolved: ResolvedDependency[]
): DependencyFinding[] {
const findings: DependencyFinding[] = []
const index: Map<string, ResolvedDependency> = new Map()
for (const item of resolved) index.set(item.packageName, item)
for (const item of declared) {
if (item.kind !== 'registry') continue
const locked = index.get(item.packageName)
if (!locked) {
findings.push({
path: `${item.moduleName}/oh-package.json5`,
severity: 'block',
message: `${item.packageName}缺少解析结果`
})
} else if (!locked.resolvedVersion.trim()) {
findings.push({
path: `${item.moduleName}/oh-package.json5`,
severity: 'block',
message: `${item.packageName}未记录具体版本`
})
}
}
return findings
}
这里的compareDependencies只检查输入完整性,不去自行推导^和~匹配规则。范围规则由ohpm负责,审查器保存安装后确切的依赖树,并将它与上一候选基线做差异比较。若锁文件不存在、尚未安装或者解析层无法读取,页面必须显示“缺少基线”,而不是把空结果解释为零依赖。
从安装锁定延伸到交付指纹
一个可操作的工程核对流程是:先确定构建产品;检查模块依赖声明;保留对应的oh-package-lock.json5;使用ohpm list -d 10查看安装树;随后用与发布配置匹配的构建任务产生候选包。ohpm clean默认会清理锁文件,在排查缓存污染时若需要保留锁定信息,可采用文档列出的--keep-lockfile参数。正式交付的证据应来自确定的候选包文件,而非控制台里最后一条绿色消息。
ohpm install
ohpm list -d 10
# Windows可使用hvigorw.bat,其他平台采用项目对应的启动脚本
./hvigorw assembleApp -p buildMode=release
命令的结果还需要与项目的签名配置、产品目标和产物路径对照。这里不预设某个文件名必然存在:不同模块与产品可能生成不同产物,采集层应从构建配置确定候选文件的实际路径,要求文件存在、大小非零,再计算字节摘要。APP是应用交付包,不能用一个未签名的中间HAP替代。摘要的主要用途是绑定“本次提交的具体字节”,并不表示两个独立构建天然会有相同哈希。
import { createHash } from 'node:crypto'
import { createReadStream, statSync } from 'node:fs'
export async function fingerprint(filePath) {
const info = statSync(filePath)
if (!info.isFile() || info.size === 0) throw new Error('候选产物为空')
const digest = createHash('sha256')
for await (const chunk of createReadStream(filePath)) digest.update(chunk)
return { name: filePath.split(/[\\/]/).pop(),
bytes: info.size, sha256: digest.digest('hex') }
}
这段脚本放在开发机的交付流水线上运行,结果作为普通数据传入审查页面。摘要字段不应截断后用于机器比较;界面为了阅读可以展示前十二位,但导出清单必须保留完整SHA-256。任何一次重新签名、重新打包或替换文件之后,都应针对新的候选文件重新计算摘要,不沿用前一次记录。
用同一个候选标识驱动界面
工作台在展示某个产物时,需要同时展示构建产品、应用有效包名、版本码、依赖基线摘要和包体摘要。最容易出现的交互问题是用户切换构建产品时,旧的异步检查任务返回,将新产品的结果覆盖。可以给每次导入建立递增标识,只有最后一次请求有资格更新列表。
interface ArtifactSummary {
product: string
versionCode: number
packageSha256: string
dependencyCount: number
}
@Entry
@Component
struct FingerprintPage {
@State summary: ArtifactSummary | null = null
@State stateText: string = '等待选择候选文件'
private requestVersion: number = 0
async inspect(load: () => Promise<ArtifactSummary>): Promise<void> {
const current = ++this.requestVersion
this.stateText = '正在整理交付字段'
try {
const result = await load()
if (current !== this.requestVersion) return
this.summary = result
this.stateText = '候选信息已载入'
} catch (_) {
if (current === this.requestVersion) this.stateText = '读取失败,请检查来源路径'
}
}
build() {
Column({ space: 16 }) {
Text('交付包体对账').fontSize(24).fontWeight(FontWeight.Bold)
Text(this.stateText)
if (this.summary) {
Text(`产品:${this.summary.product}`)
Text(`版本码:${this.summary.versionCode}`)
Text(`依赖条目:${this.summary.dependencyCount}`)
Text(`SHA-256:${this.summary.packageSha256.slice(0, 12)}…`)
}
}.padding(20).width('100%')
}
}

界面上“已载入”只意味着数据可展示,是否允许进入发布环节应由独立规则判断。比如锁文件的依赖树与本次安装清单不符,应标为阻断;单纯的包体摘要变化则需要解释其来源,不宜自动归为故障。当前模块没有依赖某个库时,也应避免将其他产品的依赖算入差异。多产品工程必须分别保存基线,不能将debug和release共用一份哈希清单。
排错要从内容、路径和产品三处入手
可以准备三类有代表性的输入。第一类是在声明中把某个版本从精确值改为范围值,再重新安装,观察基线中是否出现解析版本变化。第二类是本地HAR文件内容调整而引用路径不变,此时路径字符串相同但文件摘要应当变化。第三类是候选文件被重新打包后,重新计算包体SHA-256,并确认页面对应的时间线、文件名和内容摘要都切换到了新的候选标识。
若依赖树读取失败,先检查实际工程目录和模块级配置,再检查锁文件与ohpm版本;若仅某个构建产品异常,检查产品对模块的引用关系以及签名和构建模式。不要为通过审查而清空锁文件:没有解析记录的构建无法形成可靠的依赖基线。另一方面,也不要把所有依赖升级都列为阻断,能否放行取决于变化类型和后续回归覆盖情况。

官方资料可参考华为开发者的依赖配置与编译行为说明、oh-package.json5配置规则、ohpm list命令、ohpm clean锁文件处理和Hvigor命令行构建。这些资料分别解释依赖如何配置、如何检查安装树以及如何产生实际候选产物。
到这一步,交付审查工作台保存的不再是孤立的“依赖已安装”标记,而是产品快照、锁定结果和具体候选包的可追溯关系。下一篇会把这些结果与权限变更、模块调整和跨设备回归覆盖合并,建立发布差异门禁,决定哪些变化必须补足回归后才能提交。
更多推荐


所有评论(0)