茶器艺科智造HarmonyOS应用实战-17-Windows装APP而Shell只装HAP,为什么同一仓库出现两套结果:统一构建安装语义

先设想一个由当前脚本分支推导出的风险场景:同事 A 在 Windows 执行 debug.ps1 run,同事 B 在 macOS 执行 chaqi-auto-debug.sh。两边都看到“完成”,进入页面后的结果却可能不同:一边带着最新 HSP 页面,另一边可能沿用设备上旧的共享包;或者 Windows 输出目录残留旧 APP,脚本按文件存在性选择了与本轮构建不同的产物。最难查的地方在于,两个命令都叫“一键调试”,实际执行的构建任务、产物集合、签名要求和安装覆盖策略并不相同。这是静态风险推演,不是本次已经执行构建和安装的实测记录。

我核对的源码修订为 f671fcd8de277973bd7518a3c462f68384575f1e。Windows 脚本调用项目级 assembleApp,随后按文件存在情况选择 signed APP、signed HAP、unsigned HAP,并在 signed HAP 与 signed HSP 同时存在时把安装集合改为 HAP+HSP;Shell 侧 chaqi-build.sh 调用 assembleHapchaqi-auto-debug.sh 无参调用安装脚本时默认选择 signed entry HAP,而 chaqi-install-hap.sh 自身允许第一个参数覆盖 HAP 路径。本文只审计脚本并提出统一契约,没有执行构建、清理产物、连接设备、卸载或安装。

跨平台构建安装语义封面

本文解决:

  1. 精确还原 Windows 与 Shell 的每一个分支。
  2. 解释 APP、HAP、HSP 是产物集合问题,不能只比较命令名。
  3. 找出“按文件存在选择”引入旧产物的方式。
  4. 为 full-app 与 module-set 两种意图建立显式模式。
  5. 统一签名、设备、卸载和安装回执。
  6. 给出仅依据脚本的验证矩阵,不推断任何真实设备结果。

一、故障现场先按产物身份记录

一个有价值的现场记录不应只写“Windows 正常、Mac 异常”,而应至少包含:

平台
→ 实际 hvigor 任务
→ 本轮开始与结束时间
→ 被选中的产物绝对路径
→ 产物修改时间、大小、摘要
→ 安装命令完整参数
→ 目标设备序列号
→ 是否卸载
→ 安装返回码
→ 启动后模块版本回读

若 B 端只装 entry HAP,设备上又恰好保留此前安装的 libraryhsp,应用可能看起来能打开,但页面代码不一定来自同一轮构建。反过来,若系统拒绝缺少配套 HSP 的 HAP,失败会更明显。两种结果都需要真实命令输出才能确认,本文不会从脚本静态内容预测设备必然采用哪一种。

二、Windows 构建 APP,安装分支却并不固定

debug.ps1 的构建部分明确使用项目模式:

& $HvigorwBat --no-daemon --mode project -p product=default -p buildMode=debug assembleApp
if ($LASTEXITCODE -ne 0) {
  throw "hvigor exit $LASTEXITCODE"
}

本轮构建意图是应用级 APP。脚本随后定义 signed/unsigned APP、signed/unsigned HAP 与 signed HSP 五条路径,选择逻辑如下:

$packageToInstall = $AppUnsigned
if ([System.IO.File]::Exists($AppSigned)) {
  $packageToInstall = $AppSigned
} elseif ([System.IO.File]::Exists($HapSigned)) {
  $packageToInstall = $HapSigned
} elseif ([System.IO.File]::Exists($HapUnsigned)) {
  $packageToInstall = $HapUnsigned
}

$packagesToInstall = @($packageToInstall)
if ([System.IO.File]::Exists($HapSigned) -and
    [System.IO.File]::Exists($HspSigned)) {
  $packagesToInstall = @($HapSigned, $HspSigned)
}

所以“Windows 装 APP”只是一个条件分支。只要两个 signed 模块产物同时存在,最终集合就被替换为 HAP+HSP。脚本没有验证这些文件是否由本轮 assembleApp 产生,也没有把选择绑定到构建任务的输出清单。

三、Shell 明确构建 HAP,自动调试默认安装一个 signed HAP

Shell 自动调试依次调用 build、install、launch:

"$ROOT/scripts/chaqi-build.sh"
"$ROOT/scripts/chaqi-install-hap.sh"
"$ROOT/scripts/chaqi-launch-app.sh"

构建脚本的核心命令是:

exec "$HVIGORW" assembleHap -p product=default -p buildMode=debug "$@"

安装脚本的默认语义和路径覆盖能力可摘成:

HAP="${1:-$ROOT/entry/build/default/outputs/default/entry-default-signed.hap}"
test -f "$HAP" || exit 1
if test -n "$HDC_TARGET"; then
  exec hdc -t "$HDC_TARGET" install -r "$HAP"
fi
exec hdc install -r "$HAP"

当前根工程包含 entry、libraryhsp、libraryhar,entry 的 oh-package 同时依赖 HSP 与 HAR。chaqi-auto-debug.sh 没有给安装脚本传自定义路径,因此自动调试默认安装 entry signed HAP;安装脚本自身虽允许调用者覆盖 HAP 路径,但仍只向 hdc install 传一个 HAP。Shell 侧没有解析 libraryhsp signed HSP,也没有改用 APP。由静态脚本能确认的是“自动调试默认只传一个 entry signed HAP”,不能进一步宣称 HSP 一定缺失、一定从 HAP 内得到,或一定由设备旧版本满足;这些需要产物解包和设备安装回执。

从意图到唯一产物清单的统一流程

四、文档说明与脚本也出现了偏差

DEPLOY.md 写道 debug.ps1 run 内部调用 assembleHap + hdc install,但当前脚本第 92 行实际调用 assembleApp。README 同时把 assembleHap --no-daemon 列为主要构建命令,把 debug.ps1 run 列为 Windows 一键调试。

这会直接误导排错范围。新人可能以为两端都在构建 HAP,只比较 entry 文件;实际 Windows 已产生 APP,并可能安装 APP 或 HAP+HSP。应把差异写成清单:

维度Windows debug.ps1Shell 当前脚本
构建任务assembleAppassembleHap
主要目标项目级 APPentry signed HAP
HSP 处理两个 signed 模块都存在时成对传入未显式传入
unsigned 回退存在
安装前卸载对每台目标执行不卸载,使用 -r
默认目标未设时遍历所有连接交给 hdc 默认目标
产物新鲜度只看存在性只看存在性
完成回执DEBUG_RUN_OK完成文字

五、先定义两个显式模式

建议不要再用“run”隐藏产物策略,而是公开模式:

模式构建目标安装集合用途
full-appassembleApp本轮 signed APP验证整个应用图,默认推荐
module-set对应模块任务本轮 signed HAP + 配套 signed HSP模块联调或工具不支持 APP
entry-onlyassembleHapsigned entry HAP仅当工程确认不需要独立 HSP

当前 entry 依赖 libraryhsp,所以 entry-only 不应作为默认。full-app 更容易表达“一轮构建形成一个应用候选包”;module-set 则必须把 HSP 清单显式带入。

统一配置可以很小,以下尚未加入仓库:

{
  "mode": "full-app",
  "product": "default",
  "buildMode": "debug",
  "requireSigned": true,
  "installPolicy": "replace",
  "devicePolicy": "exactly-one"
}

Windows 与 Shell 读取同一语义,不必共享同一种脚本语言。关键是同样的 mode 必须产生相同任务和产物角色。

六、禁止按“目录里有什么”改变本轮模式

旧文件是跨平台差异的重要来源。假设昨天构建过 signed APP,今天修改 HSP 后只执行 assembleHap;文件系统里两类产物仍都存在。脚本若只按 Test-Path 优先级选择,安装对象就可能与本轮任务脱节。

建议每轮生成 ArtifactManifest:

interface ArtifactRecord {
  role: 'app' | 'entry-hap' | 'library-hsp';
  path: string;
  signed: boolean;
  size: number;
  sha256: string;
  modifiedAtMs: number;
}

interface DebugArtifactManifest {
  runId: string;
  startedAtMs: number;
  task: 'assembleApp' | 'assembleHap';
  product: string;
  artifacts: ArtifactRecord[];
}

脚本在构建前记录 startedAt,构建后只接受修改时间不早于本轮、路径符合当前 mode、签名角色满足契约的文件。更稳的做法是从 hvigor 实际输出或已知输出目录生成清单并计算 SHA-256,而不是靠固定优先级猜一个文件。

可选的专用临时输出目录能减少旧产物干扰;是否清理现有 build 目录应由显式参数控制,不能在默认调试时悄悄删除用户要保留的证据。

七、构建计划与安装计划分开验证

统一执行器先产生计划,再执行。计划阶段不连接设备:

MODE=full-app
BUILD_TASK=assembleApp
EXPECTED_ROLES=app
SIGNED_REQUIRED=true
DEVICE_POLICY=exactly-one
UNINSTALL=false

执行前检查不变量:

if ($Plan.Mode -eq 'full-app' -and $Artifacts.Count -ne 1) {
  throw 'full-app requires exactly one APP artifact'
}
if ($Plan.RequireSigned -and -not $Artifacts[0].Signed) {
  throw 'signed artifact required'
}
if ($Artifacts[0].ModifiedAtMs -lt $Plan.StartedAtMs) {
  throw 'stale artifact'
}

Shell 实现同样门禁。这样 assembleApp 成功后不会因为目录里正好有 HAP+HSP 而改变安装语义。

华为 hdc 指南说明 hdc 可安装 HAP 和应用间 HSP,并从 API 22 起支持 APP;多包参数和目标系统能力仍以当前 hdc 帮助与官方文档为准。项目 target SDK 为 22,不等于连接设备和本机 hdc 必然支持所有分支,执行前仍需版本预检。

八、安装覆盖策略也必须统一

Windows 当前对每个目标先 uninstall,再 install;Shell 使用 install -r。两者会直接影响草稿、缓存和历史状态。

建议拆成三种策略:

replace  覆盖安装,保留允许保留的数据,用于常规迭代
clean    明确卸载后重装,用于冷启动或迁移测试
install  仅首次安装,已存在则失败

默认 run 更适合 replace;clean 应显式出现,并在日志里突出会清除应用数据。普通 UI 调试不应被默认分支清空草稿。

安装回执至少包含:

{
  "runId": "20260911T103000Z-a1b2",
  "mode": "full-app",
  "artifactRole": "app",
  "sha256Prefix": "7ac31e4f",
  "target": "device-serial",
  "policy": "replace",
  "installExitCode": 0,
  "launchExitCode": 0
}

退出码 0 只说明命令成功返回。要确认设备运行的是本轮包,还要在应用或 bm 信息中回读版本、构建标识或模块摘要。

统一计划、产物清单、设备门禁与回执结构

九、验证矩阵先 dry-run,再触碰设备

方案落地后先运行 dry-run,确认两端给出同样计划。下表不是当前结果:

场景Windows 预期Shell 预期阻断条件
full-app dry-runassembleApp + 1 signed APP同上发现 HAP/HSP 被选中
module-set dry-runsigned HAP/HSP同上缺任一角色
entry-only明确拒绝或需 override同上entry 仍依赖 HSP
只有旧 APPstale 阻断stale 阻断时间早于 startedAt
只有 unsigned签名门禁失败同上requireSigned=true
多台设备exactly-one 阻断同上未指定 target
replace不卸载不卸载脚本出现 uninstall
clean明确选择后卸载同上非显式 clean
安装成功生成回执生成回执缺 target/hash/exit
启动后回读构建标识同上设备版本不匹配

建议接口:

debug full-app --dry-run
debug full-app --target <serial>
debug module-set --target <serial>
debug clean full-app --target <serial>
debug artifacts --run-id <id>

真实命令名可按团队习惯实现。模式与危险动作显式出现,Windows 和 Shell 都输出相同字段,才能比较结果。

十、排错表与结论边界

现象首查脚本位置可能根因修复方向
两个平台页面版本不同task、artifact hash一边 APP,一边单 HAP统一 mode 与清单
assembleApp 后安装 HAP/HSPWindows 覆盖分支文件存在触发重新赋值禁止按存在性改 mode
Shell 能启动但像旧页面HSP 角色与设备现状单 HAP 未绑定本轮 HSPfull-app 或 module-set
Windows 草稿消失uninstall 与 -r覆盖策略不同默认 replace,clean 显式
签名问题只在一端unsigned fallbackWindows 静默降级统一 requireSigned
改了代码但 hash 没变modifiedAt/runId选到旧产物新鲜度门禁
多设备结果混在一起target policy默认目标不同exactly-one 并回显序列号
文档说 assembleHapDEPLOY.md文档落后于脚本从模式定义生成说明
安装返回 0 仍不放心设备回读只有主机侧退出码回读版本与构建标识
APP 分支不受当前工具支持hdc 版本工具与系统能力不匹配显式切 module-set

当前脚本事实足以说明两套语义:Windows 构建 APP,却可能按文件存在改装 APP 或 HAP+HSP,并允许 unsigned 回退;Shell 自动调试构建 HAP,并在无参安装调用中默认选择 signed entry HAP,安装脚本则允许第一个参数覆盖 HAP 路径。它们还采用不同卸载策略和默认设备策略。本文没有执行任何构建或安装,所以不能说某台 Windows 设备实际装了 APP,也不能说 Shell 的 HAP 实际成功运行。只有统一模式、绑定本轮产物清单、指定目标并回读设备构建身份后,跨平台“同一结果”才有可复核含义。

Logo

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

更多推荐