茶器艺科智造HarmonyOS应用实战-17-Windows装APP而Shell只装HAP,为什么同一仓库出现两套结果:统一构建安装语义
茶器艺科智造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 调用 assembleHap,chaqi-auto-debug.sh 无参调用安装脚本时默认选择 signed entry HAP,而 chaqi-install-hap.sh 自身允许第一个参数覆盖 HAP 路径。本文只审计脚本并提出统一契约,没有执行构建、清理产物、连接设备、卸载或安装。

本文解决:
- 精确还原 Windows 与 Shell 的每一个分支。
- 解释 APP、HAP、HSP 是产物集合问题,不能只比较命令名。
- 找出“按文件存在选择”引入旧产物的方式。
- 为 full-app 与 module-set 两种意图建立显式模式。
- 统一签名、设备、卸载和安装回执。
- 给出仅依据脚本的验证矩阵,不推断任何真实设备结果。
一、故障现场先按产物身份记录
一个有价值的现场记录不应只写“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.ps1 | Shell 当前脚本 |
|---|---|---|
| 构建任务 | assembleApp | assembleHap |
| 主要目标 | 项目级 APP | entry signed HAP |
| HSP 处理 | 两个 signed 模块都存在时成对传入 | 未显式传入 |
| unsigned 回退 | 存在 | 无 |
| 安装前卸载 | 对每台目标执行 | 不卸载,使用 -r |
| 默认目标 | 未设时遍历所有连接 | 交给 hdc 默认目标 |
| 产物新鲜度 | 只看存在性 | 只看存在性 |
| 完成回执 | DEBUG_RUN_OK | 完成文字 |
五、先定义两个显式模式
建议不要再用“run”隐藏产物策略,而是公开模式:
| 模式 | 构建目标 | 安装集合 | 用途 |
|---|---|---|---|
| full-app | assembleApp | 本轮 signed APP | 验证整个应用图,默认推荐 |
| module-set | 对应模块任务 | 本轮 signed HAP + 配套 signed HSP | 模块联调或工具不支持 APP |
| entry-only | assembleHap | signed 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-run | assembleApp + 1 signed APP | 同上 | 发现 HAP/HSP 被选中 |
| module-set dry-run | signed HAP/HSP | 同上 | 缺任一角色 |
| entry-only | 明确拒绝或需 override | 同上 | entry 仍依赖 HSP |
| 只有旧 APP | stale 阻断 | 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/HSP | Windows 覆盖分支 | 文件存在触发重新赋值 | 禁止按存在性改 mode |
| Shell 能启动但像旧页面 | HSP 角色与设备现状 | 单 HAP 未绑定本轮 HSP | full-app 或 module-set |
| Windows 草稿消失 | uninstall 与 -r | 覆盖策略不同 | 默认 replace,clean 显式 |
| 签名问题只在一端 | unsigned fallback | Windows 静默降级 | 统一 requireSigned |
| 改了代码但 hash 没变 | modifiedAt/runId | 选到旧产物 | 新鲜度门禁 |
| 多设备结果混在一起 | target policy | 默认目标不同 | exactly-one 并回显序列号 |
| 文档说 assembleHap | DEPLOY.md | 文档落后于脚本 | 从模式定义生成说明 |
| 安装返回 0 仍不放心 | 设备回读 | 只有主机侧退出码 | 回读版本与构建标识 |
| APP 分支不受当前工具支持 | hdc 版本 | 工具与系统能力不匹配 | 显式切 module-set |
当前脚本事实足以说明两套语义:Windows 构建 APP,却可能按文件存在改装 APP 或 HAP+HSP,并允许 unsigned 回退;Shell 自动调试构建 HAP,并在无参安装调用中默认选择 signed entry HAP,安装脚本则允许第一个参数覆盖 HAP 路径。它们还采用不同卸载策略和默认设备策略。本文没有执行任何构建或安装,所以不能说某台 Windows 设备实际装了 APP,也不能说 Shell 的 HAP 实际成功运行。只有统一模式、绑定本轮产物清单、指定目标并回读设备构建身份后,跨平台“同一结果”才有可复核含义。
更多推荐

所有评论(0)