应用的发布配置和权限关系都核对过,打包前却仍可能发生一种不容易察觉的变化:研发同事没有修改业务代码,依赖更新却使构建输入发生了漂移。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%')
  }
}

DevEco Studio中的候选包对账工作台

界面上“已载入”只意味着数据可展示,是否允许进入发布环节应由独立规则判断。比如锁文件的依赖树与本次安装清单不符,应标为阻断;单纯的包体摘要变化则需要解释其来源,不宜自动归为故障。当前模块没有依赖某个库时,也应避免将其他产品的依赖算入差异。多产品工程必须分别保存基线,不能将debug和release共用一份哈希清单。

排错要从内容、路径和产品三处入手

可以准备三类有代表性的输入。第一类是在声明中把某个版本从精确值改为范围值,再重新安装,观察基线中是否出现解析版本变化。第二类是本地HAR文件内容调整而引用路径不变,此时路径字符串相同但文件摘要应当变化。第三类是候选文件被重新打包后,重新计算包体SHA-256,并确认页面对应的时间线、文件名和内容摘要都切换到了新的候选标识。

若依赖树读取失败,先检查实际工程目录和模块级配置,再检查锁文件与ohpm版本;若仅某个构建产品异常,检查产品对模块的引用关系以及签名和构建模式。不要为通过审查而清空锁文件:没有解析记录的构建无法形成可靠的依赖基线。另一方面,也不要把所有依赖升级都列为阻断,能否放行取决于变化类型和后续回归覆盖情况。

交付包体与依赖摘要核对结果

官方资料可参考华为开发者的依赖配置与编译行为说明、oh-package.json5配置规则、ohpm list命令、ohpm clean锁文件处理和Hvigor命令行构建。这些资料分别解释依赖如何配置、如何检查安装树以及如何产生实际候选产物。

到这一步,交付审查工作台保存的不再是孤立的“依赖已安装”标记,而是产品快照、锁定结果和具体候选包的可追溯关系。下一篇会把这些结果与权限变更、模块调整和跨设备回归覆盖合并,建立发布差异门禁,决定哪些变化必须补足回归后才能提交。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐