ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Flutter发布流水线鸿蒙化改造:版本资产与签名适配全攻略

Flutter发布流水线鸿蒙化改造:版本资产与签名适配全攻略 这几年的Flutter项目但凡做到需要对外发版的阶段基本都会在 release_tools 这类工具上花不少时间。我这里说的 release_tools不单指某个固定仓库名的开源库更准确讲是 Flutter 工程里那套管版本号、管变更记录、管构建产物归档上传的发布工具链。谁维护过多个应用的发版流程谁就知道真正决定“能不能顺利发出去”的往往不是编译那一下而是版本号从哪儿来到哪儿去、HAP 包放哪儿、签名文件够不够新、变更说明有没有漏写——这些细碎动作全串起来才叫发布流水线。现在的问题是Flutter 要跑在鸿蒙上已经不是新话题了。Flutter 引擎在 OpenHarmony 上的移植、HAP 构建、鸿蒙侧的签名体系这些能力陆续补齐之后最尴尬的反而是过去那套为 Android/iOS 设计的 release_tools 完全派不上用场。源码能编译但发布流水线断在半路hvigor 不认识、产物目录对不上、签名上下文完全不同、版本号也被 oh-package.json5 这套鸿蒙配置搅乱了。这篇东西就是讲我怎么把这套工具链改造成鸿蒙版本并真正跑通上线全流程的。适合正在做 Flutter 鸿蒙化、或者刚接手跨端发版工具的小伙伴参考。1. 先拆清楚release_tools 在传统 Flutter 发布里到底控制了什么1.1 别光看“版本号1”发布流水线的内核是状态机很多刚接触发布工具的人以为 release_tools 就是替你执行一句 version: 1.2.03然后打 tag。真做过的人都知道发版就是一台状态机从开发分支拉出来校验当前状态是否干净确认要不要提前打 hotfix执行版本递增触发构建收集产物生成变更日志归档到制品库再发通知。每一步之间都有前置条件任何一个产物没生成完后面都只能停下来等。所以我在设计鸿蒙化适配的时候没有急着去改“构建命令”而是先把这套状态机画清楚。传统 Flutter 发布里release_tools 主要管四类事版本状态读 pubspec.yaml 的 version 字段按 semantic version 规则做 bump改完回写并生成 git tag。变更状态从 commit 信息里筛出 feature/fix/breaking change自动汇总成 CHANGELOG也可以把 release notes 同步给应用商店。产物状态把 Android 的 APK/AAB、iOS 的 IPA、dSYM 符号表、mapping 文件全部收集到同一个归档目录计算哈希做完整性校验。发布状态对接内部制品库、群机器人、邮件通知把归档包推到对应环境。这套东西用熟了以后发版就是一条流水线你只需要在入口填一个版本类型它自己把后面全部走完。1.2 鸿蒙化以后原来的流水线断在哪儿鸿蒙化以后我再跑这套 release_tools第一反应是构建命令换了但跑完发现远不止换命令那么简单。逐项对下来原来四类状态几乎都断了。第一版本状态。Flutter 侧用的是 pubspec.yaml 的 version但鸿蒙应用侧的包版本在 app.json5 和 module.json5 里且鸿蒙的 versionCode 是一个整数和 Flutter 的 build number 语义接近但不完全一样。release_tools 如果只改 pubspec.yaml打出来的 HAP 包里版本号还是旧的。这直接会导致应用市场上传时报版本错误。第二变更状态。变更日志生成本身不依赖平台这部分还能用。但鸿蒙应用市场的 release notes 有它自己的字段要求和 GP 的“主要变更”描述格式不完全一致需要做一层模板转换。第三产物状态。Android 产物是 build/app/outputs/apk/release/app-release.apkAndroid 的混淆 mapping 在 build/app/outputs/mapping/release/。鸿蒙完全不是这套目录HAP 包在模块目录下的 build/default/outputs/default/ 里.hsp 和 .har 又各有各的输出位置AAB/APK 那套收集逻辑直接作废。第四发布状态。签名上下文彻底换了。Android 用 keystore signingConfig鸿蒙用 .p12 证书、.cer、.p7b Profile 和三方 hap-sign-tool 做签名release_tools 里跟 Gradle 绑定的签名逻辑全部要重写。所以“鸿蒙化适配”的本质其实不是把命令从 gradlew 换成 hvigorw而是把 release_tools 这台状态机里面向平台的那一层全部抽出来重写。你只有先承认这一点后面才不会越改越乱。2. 开始适配之前先给“版本资产”定标准2.1 什么叫版本资产为什么它比源码更需要管这里我要强调一个词版本资产。它指的不是代码而是“一个版本发布出去时所有需要被存档、被校验、被回溯的东西”。我手里一个完整的鸿蒙发布版本通常包含这些东西资产类型说明缺失后果HAP 主包用户下载安装的主包无法上线HSP/HAR 依赖包动态共享包、静态共享包动态特性失效SHA256SUMS所有产物的哈希清单无法校验完整性签名材料副本证书、Profile、签名后的包无法做补签/追溯版本配置快照app.json5、oh-package.json5无法查明版本来源CHANGELOG/Release Notes给市场和用户看的变更说明上架被拒构建日志hvigor 日志、错误日志出问题没法查在 Android/iOS 时代这些资产散落各处release_tools 里方便起见也没太管到了鸿蒙化之后必须管起来。因为鸿蒙侧构建链路相对新工具链版本迭代快今天能编过的包三个月后可能就编不过了如果没有把当时的构建环境、依赖版本、签名材料都存档后面想补一个 hotfix 包会非常痛苦。所以我在改造 release_tools 时第一步不是写代码而是把“发布目录结构”先定下来。用的目录大概是这样的release/ harmonyos/ app-1.2.0/ app-1.2.0.hap app-1.2.0.hsp SHA256SUMS changelog.md release-notes.md mapping/ signing/ cert.cer profile.p7b logs/ build-1234.log每个版本一个目录所有该版本相关的资产从第一天就归拢到一起。这个目录结构就是整个 release_tools 鸿蒙化改造的“数据模型”后面所有脚本都是围绕这个模型转的。我强烈建议你也先干这件事别一上来就改构建代码。数据结构不定后面写多少逻辑都是在打补丁。2.2 版本号映射规则谁来当“唯一真源”资产目录定了之后最核心的问题就是版本号到底听谁的。原来在 Flutter 项目里pubspec.yaml 的 version 就是唯一真源Android/iOS 构建时会自动读这个值。鸿蒙这边不行Flutter 引擎跑的是鸿蒙适配分支构建 HAP 时 app.json5 里的 versionCode 必须自己维护。我不建议搞双头维护两边各改各的一定会漂移。我的做法是让 pubspec.yaml 的 version 继续当唯一真源鸿蒙侧的 app.json5、oh-package.json5 全部由 release_tools 在预构建阶段同步生成。规则如下version 的1.2.0段映射到鸿蒙的 versionName格式保持1.2.0。version 的3段映射到 versionCode。鸿蒙 versionCode 是整数直接用3但为了避免跨大版本混淆我习惯编码成1002003规则是大版本号两位、中版本两位、小版本两位、build 号三位即01 02 00 003。这个编码规则不是鸿蒙强制的是我为了发布流水线里排序、比对、识别版本高低时方便定的。鸿蒙只要求 versionCode 是随版本增大而增大的整数只要满足这一点编码规则你可以根据团队习惯来关键是 release_tools 内部必须一致。这里还要注意一个坑鸿蒙有些模块配置里的版本号叫version有些叫versionName不同 SDK 版本对字段名的要求不一样。我在适配时用脚本先扫描 build-profile.json5 和 module.json5确认当前工程用的字段名再决定同步写入哪个 key。适配不能写死这就是“鸿蒙级精密”的第一步。3. release_tools 鸿蒙化适配实战3.1 把构建执行器抽象出来别直接写死 hvigor最直接的做法是在 release_tools 里加一个 platform 参数platform 为 android 时走原有 gradlew 逻辑platform 为 harmonyos 时走新增的 hvigorw 逻辑。核心代码如下思路就是一个“策略模式”class HvigorBuilder: def __init__(self, project_dir, module_nameentry): self.project_dir project_dir self.module_name module_name def assemble_release(self, extra_paramsNone): cmd [ hvigorw, assembleHap, --mode, module, -p, fmodule{self.module_name}default, --no-daemon, ] if extra_params: cmd extra_params return subprocess.run(cmd, cwdself.project_dir, checkTrue)你可能觉得这不就是改个命令嘛。真正跑过就知道坑在后面。hvigorw 的退出码、日志输出格式、增量构建状态、以及 Windows 下需要把 hvigorw.bat 和 shell 脚本分开处理这些都要封装进执行器里。我在第一版踩了“以为 hvigorw 跑完退出码是 0 就万事大吉”的坑结果是 HAP 包虽然生成但某些模块因为签名失败被跳过退出码仍然为 0。所以执行器里除了看退出码还要强制校验 HAP 文件确实是本次构建新生成的修改时间戳要在构建开始之后。这一条我直接写成了 release_tools 的硬性检查项。3.2 签名这块是整个适配里最绕的部分鸿蒙的签名链路和 Android 相比有一个非常大的差异Android 构建时 gradle 会直接读取 keystore 并完成签名你只要把密码放进 gradle.properties 就行。鸿蒙这边hvigor 虽然在 build-profile.json5 里也能配置 signingConfigs但一旦涉及上传到应用市场的正式包大多数团队还是希望走独立签名工具 hap-sign-tool这样发布和本地构建能解耦。release_tools 里我建议做成两种方式都支持但默认走独立签名中间流程是这样从密钥管理服务里拉取正式证书 .cer 和 Profile .p7b不落盘到 CI 工作区只在签名时写入临时目录。调用 hap-sign-tool 对构建出来的未签名 HAP 做签名。签名后立刻用hap-sign-tool verify-app校验签名结果确保 Profile 没过期、证书链正常。校验通过才把签名后的 HAP 移动到 release 目录。这里有个非常实际的坑鸿蒙的 Profile 文件是有有效期限制的不同于 Android keystore 十年起步Profile 可能是几个月或一年。如果 release_tools 没有在签名前检查 Profile 有效期你会在发布当天才收到“证书已过期”的错误。我的方案是在 release_tools 里加一个check_signing_profile_expiry动作提前 30 天就在流水线里报警。这个功能看起来小真正救过我一次值得加。3.3 产物收集别再用 Android 的路径思维找 HAP产物收集是很多人在鸿蒙化时最憋屈的地方。你在 Android 里习惯build/app/outputs/到鸿蒙后会发现每个 HAP 包保存在各自模块的build/default/outputs/default/目录下而且在不同版本 DevEco Studio / hvigor 里这个路径还可能有出入。release_tools 里我写了一段比较稳的产物探测逻辑find $MODULE_DIR/build -type f \( -name *.hap -o -name *.hsp -o -name *.har \) -newer $BUILD_START_MARKER | sort对就是加了一个-newer判断只看本次构建开始之后生成的文件。这样就算缓存里有旧的 HAP 包也不会被误收集。另外主模块产物一般叫entry-default-unsigned.hap或entry-default-signed.hap命名随 SDK 版本变化所以我不会用固定的文件名匹配而是用-name *.hap配合排除 unsigned 关键字再根据 release_tools 配置的 moduleName 选主包。收集完以后立刻做三件事算哈希、写 SHA256SUMS、锁目录只读。哈希文件必须和 HAP 包放同一个 release 目录里别单独存。这样后续任何人拿到目录就能自校验而不是依赖 release_tools 本身。3.4 变更日志和发布说明要做成两套模板很多团队把 CHANGELOG 和大版本发布说明混在一起鸿蒙化之后会发现不好用。应用市场上传时通常只接受一版简洁的“用户可见变更”而仓库里的 CHANGELOG 更多是给开发看的完整 commit 汇总。两者价值完全不同。release_tools 里生成逻辑可以这样分完整版 CHANGELOG从上一个 release tag 开始把 conventional commits 全部按类型聚合写入CHANGELOG.md。用户版 Release Notes只取 feature 和 fix 类型并用一句话对每条 commit 做摘要甚至可以过滤掉内部重构类变更。具体模板可以在 release_tools 配置里维护release_notes: include_types: [feat, fix, perf] exclude_scope: [internal, refactor] max_items: 20这一层做得细一点你上架时会特别舒服。我第一次在鸿蒙市场上传时直接把完整 CHANGELOG 粘上去结果审核被要求重新精简后来才在 release_tools 里加了“两套文案”的机制。这类问题不会出现在编译报错里但它是发布流水线实际体验的一部分。4. 版本资产管理的实战细节4.1 构建前的“版本一致性预检”千万不能省鸿蒙应用市场对版本号的校验比 GP 还要敏感一些。你上传 HAP 时如果包内 versionName 和你在后台填写的版本一致但 versionCode 与上一次上传的包相比没有增大上传会被直接拒绝。而且这个拒绝消息不一定很明确有时只告诉你“版本信息不正确”。release_tools 预检逻辑我建议这样做读远端应用市场上一次成功发布的 versionCode存到本地状态文件里。构建前解析当前工程 app.json5 里的 versionCode。如果两者相等或更小立即中止发布并提示“需要先 bump 版本”。如果当前版本已经存在同名 git tag也要警告防止重复发布覆盖历史版本。我甚至会在 release_tools 里把“上一次上架版本号”这个状态和 HAP 包一起存档一份放在release/current/目录中。这样每次发版时release_tools 直接读本地状态不用老是依赖市场 API。市场 API 能拿到的话也读但那可能不是所有流水线场景都方便本地状态更可控。4.2 多产物并行发布时的资产同步问题一个大的鸿蒙应用往往不止一个 HAP还有 HSP 动态共享包、HAR 静态共享包。多模块并行构建时release_tools 要注意一个点主包和子包必须使用同一版本编码不能某个模块还停留在上一个版本。我第一次做多模块鸿蒙发布时就出现了主包 versionCode 是 1002003、某个 HSP 模块 versionCode 还是 1002001 的错位安装后系统直接提示模块不兼容。解决办法是在同步版本号阶段就递归扫描工程下所有模块的 module.json5 / build-profile.json5把所有模块的 versionCode 统一为同一个值。release_tools 里可以直接写一个sync_all_module_versions的函数这样做的好处是后续不管新增多少模块版本一致性都由流水线兜底而不是靠人肉检查。4.3 回滚和灰度场景对版本资产的要求回滚是发布流水线里最容易被人忽略的部分。很多人以为回滚就是把上一个包重新传一遍但实际在鸿蒙侧如果你上一个版本的包已经打了 A/B 灰度或者已经有一部分用户升级到新版本简单回滚包是不够的。release_tools 需要能根据版本资产目录快速生成“指定历史版本”的发布文件集合。我的做法是release_tools 记录每个版本的可回滚范围比如“该版本从哪个版本升级而来对应哪些用户分桶”。这套信息不需要太复杂关键是把当前生效版本的资产目录、签名材料、变更说明完整保留。只要上面那个 release 目录结构一直严格执行回滚就是一行命令的事把release/app-1.1.0/里的 HAP 和校验和提取出来走一遍签名校验和哈希校验然后重新归档成待上传目录。这套链路在 Android 时代我就跑过鸿蒙化后只是把产物路径和签名工具换了一下。5. 鸿蒙化过程中常见问题速查5.1 hvigorw 执行退出的信号会被“吞掉”我遇到过最迷惑的问题是 hvigor 构建过程中某个子任务失败但最终 hvigorw 进程仍然返回退出码 0。后来排查发现是 hvigorw 把部分 warning 和 error 都打到了 stdout而不是 stderr而 release_tools 里的 subprocess 调用只检查了 returncode。处理办法有两层。第一层release_tools 的构建日志要同时捕获 stdout 和 stderr并且对输出里的FAILURE、ERROR、BUILD FAILED做关键字扫描命中即判失败。第二层构建完成后强制检查目标输出目录里有没有本次新产物没有就视为失败。这两层都过了再进入产物收集基本能把假成功堵死。5.2 签名 Profile 过期导致“上午还好好的下午就挂了”这个是所有鸿蒙签名方案都绕不开的。Profile 是有生命周期的而且不同环境的生效时间有延迟。release_tools 里我加了一道“签名前检查”并把 Profile 有效期写进版本资产里存档。一旦检测到距过期不足 30 天流水线会跳过正常发布并给维护者发提醒。你可以把这道检查做得更精细结合签名材料的签发时间计算证书链上每一级的有效期任何一个环节快到期都预警。这个功能本身加不了几行代码但能帮你把“发版当天发现证书过期”的概率降到接近零。5.3 pubspec.yaml 改了版本HAP 里还是旧版本这个问题几乎每个做 Flutter 鸿蒙化的团队都会碰到一次。原因就是 Flutter 侧版本号与鸿蒙侧版本号的来源不一致。你 release_tools 里 bump 了 pubspec.yaml但 app.json5 的 versionName/versionCode 没有同步构建出来自然还是旧版。适配时必须把“同步版本号”放到构建动作之前并且这个同步要写入 release_tools 的主流程不能靠开发人员手动执行。我的经验是同步完以后立刻跑一次git diff确认改动范围同时把改动后的 app.json5 内容打印到构建日志里。虽然打印文件内容听起来有点啰嗦但每次发版能亲眼确认版本号对了很多事故都能提前拦住。5.4 应用市场对 release notes 的长度和格式有隐藏限制使用 release_tools 自动生成的 release_notes 往往比较长提交鸿蒙应用市场时可能被截断或直接驳回。我在适配时专门为“市场短文案”做了一个模板限定 500 字以内每条变更控制在 20 个字左右。这套逻辑不依赖任何第三方服务就是纯字符串处理却让整个发布流程顺了很多。顺便说一句鸿蒙应用市场后台经常改版上传接口的字段时而叫 releaseNote、时而叫 changeLog。release_tools 里最好在一个单独模块里做这些映射不要散落在脚本各处。集中管理会好维护得多。6. 一些给后来人的适配建议6.1 别把 release_tools 改造成“鸿蒙专用工具”你的 Flutter 项目一定还有 Android/iOS 发版需求。所以鸿蒙化适配的正确姿势是在 release_tools 里加一个平台抽象层而不是新写一套只支持鸿蒙的工具。我在适配时始终保留原有的 android/ios 构建链路鸿蒙作为一个新的 platform 选项并进去。这样同一个版本号、同一份 CHANGELOG、同一套发布流程可以同时输出三端产物。代码层面不建议为了“统一”而强行让所有平台用同一套实现。Android 有 Gradle 生态iOS 有 xcodebuild 和 TestFlight鸿蒙有 hvigor 和 hap-sign-tool底层本来就不一样。你只需要在“发布流水线”这个抽象层保持统一具体执行器的差异交给适配器去处理。6.2 自动化到什么程度才算“精密”我理解的鸿蒙级精密自动化不是把什么动作都自动化而是在每个关键节点都加“校验-确认-存档”。自动构建 HAP 是自动化但构建完自动检查哈希并记录到版本资产才是精密的自动化自动签名是自动化但签名前检查 Profile 有效期并提前 30 天预警才是精密的自动化自动发布 release notes 是自动化但能根据用户可见性自动过滤内部 commit才是精密的自动化。很多工具为了自动化而自动化跑完一大圈最后交出来的还是杂乱的产物。实际上 release_tools 这类发布工具核心价值是“让版本变成可追溯的资产”而不是“少敲几次命令”。6.3 后续可以这样往下扩展鸿蒙化适配做完以后我自己留了几个扩展方向你要是也在做这块可以参考。第一个方向是把发行说明生成与文档站点打通每发一版自动更新在线文档第二个方向是接入内部的质量门禁比如把 HAP 的包体体积变化、动态权限声明变更作为发布准入条件第三个方向是给版本资产目录加上访问控制签名材料和 Profile 只允许发布人员读取但日志和 CHANGELOG 可以开放给全员。最后再说一个很实际的小技巧release_tools 的鸿蒙适配脚本里一定要把hvigorw --version和你的 Flutter 鸿蒙引擎 commit hash 一起打印到构建日志并且写进版本资产快照。鸿蒙侧 Flutter 引擎迭代很快不同 commit 打出来的 HAP 行为可能有差异。如果哪天线上问题需要回溯“这个包是用哪版引擎打的”这条记录会救你一命。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进