HarmonyOS开发:HAP打包------应用打包流程
核心要点:HAP是HarmonyOS应用的基本分发单元,理解从源码到HAP的完整打包链路,是掌控应用构建、发布、安装全流程的基石。
背景与动机
你写了一堆ArkTS代码,画了一堆UI界面,然后呢?代码躺在工程目录里,用户手机上可跑不起来。你得把代码、资源、配置文件打包成一个HAP------HarmonyOS Ability Package,才能装到设备上。
这事儿听起来简单,不就是"打包"嘛?但真上手你会发现一堆问题:打包命令怎么敲?配置文件里那些字段啥意思?为什么Debug包和Release包体积差这么多?为什么有时候打包报签名错误?为什么多模块工程打出来的包结构跟单模块不一样?
这些问题背后,是一条从源码到HAP的完整链路。搞不懂这条链路,你就只能对着报错信息干瞪眼。今天就把这条链路从头到尾捋清楚。
核心原理
HAP包到底是什么?
HAP本质上是一个ZIP压缩包,后缀名是.hap,里面装的是你的应用代码编译产物、资源和配置信息。把它解压开来,结构长这样:
MyApp.hap
├── entry.json # 模块配置信息
├── module.json # 模块元数据(Ability声明、权限等)
├── resources/ # 资源文件(图片、字符串、布局等)
│ ├── base/
│ └── rawfile/
├── ets/ # ArkTS编译产物
│ └── modules.abc # 方舟字节码
├── libs/ # 原生库(.so文件)
└── pack.info # 包信息摘要
看到没?HAP不是随便打包的,它有严格的目录结构规范。每个文件、每个目录都有明确的作用,设备端的包管理服务就靠这些结构来解析和安装你的应用。
打包流程全景图
从你按下"Build"到HAP文件生成,中间经历了什么?看这张图:
#mermaid-svg-ovBd1xfb18gctm6e{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ovBd1xfb18gctm6e .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ovBd1xfb18gctm6e .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ovBd1xfb18gctm6e .error-icon{fill:#552222;}#mermaid-svg-ovBd1xfb18gctm6e .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ovBd1xfb18gctm6e .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ovBd1xfb18gctm6e .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ovBd1xfb18gctm6e .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ovBd1xfb18gctm6e .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ovBd1xfb18gctm6e .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ovBd1xfb18gctm6e .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ovBd1xfb18gctm6e .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ovBd1xfb18gctm6e .marker.cross{stroke:#333333;}#mermaid-svg-ovBd1xfb18gctm6e svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ovBd1xfb18gctm6e p{margin:0;}#mermaid-svg-ovBd1xfb18gctm6e .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ovBd1xfb18gctm6e .cluster-label text{fill:#333;}#mermaid-svg-ovBd1xfb18gctm6e .cluster-label span{color:#333;}#mermaid-svg-ovBd1xfb18gctm6e .cluster-label span p{background-color:transparent;}#mermaid-svg-ovBd1xfb18gctm6e .label text,#mermaid-svg-ovBd1xfb18gctm6e span{fill:#333;color:#333;}#mermaid-svg-ovBd1xfb18gctm6e .node rect,#mermaid-svg-ovBd1xfb18gctm6e .node circle,#mermaid-svg-ovBd1xfb18gctm6e .node ellipse,#mermaid-svg-ovBd1xfb18gctm6e .node polygon,#mermaid-svg-ovBd1xfb18gctm6e .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ovBd1xfb18gctm6e .rough-node .label text,#mermaid-svg-ovBd1xfb18gctm6e .node .label text,#mermaid-svg-ovBd1xfb18gctm6e .image-shape .label,#mermaid-svg-ovBd1xfb18gctm6e .icon-shape .label{text-anchor:middle;}#mermaid-svg-ovBd1xfb18gctm6e .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ovBd1xfb18gctm6e .rough-node .label,#mermaid-svg-ovBd1xfb18gctm6e .node .label,#mermaid-svg-ovBd1xfb18gctm6e .image-shape .label,#mermaid-svg-ovBd1xfb18gctm6e .icon-shape .label{text-align:center;}#mermaid-svg-ovBd1xfb18gctm6e .node.clickable{cursor:pointer;}#mermaid-svg-ovBd1xfb18gctm6e .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ovBd1xfb18gctm6e .arrowheadPath{fill:#333333;}#mermaid-svg-ovBd1xfb18gctm6e .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ovBd1xfb18gctm6e .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ovBd1xfb18gctm6e .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ovBd1xfb18gctm6e .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ovBd1xfb18gctm6e .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ovBd1xfb18gctm6e .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ovBd1xfb18gctm6e .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ovBd1xfb18gctm6e .cluster text{fill:#333;}#mermaid-svg-ovBd1xfb18gctm6e .cluster span{color:#333;}#mermaid-svg-ovBd1xfb18gctm6e div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ovBd1xfb18gctm6e .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ovBd1xfb18gctm6e rect.text{fill:none;stroke-width:0;}#mermaid-svg-ovBd1xfb18gctm6e .icon-shape,#mermaid-svg-ovBd1xfb18gctm6e .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ovBd1xfb18gctm6e .icon-shape p,#mermaid-svg-ovBd1xfb18gctm6e .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ovBd1xfb18gctm6e .icon-shape .label rect,#mermaid-svg-ovBd1xfb18gctm6e .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ovBd1xfb18gctm6e .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ovBd1xfb18gctm6e .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ovBd1xfb18gctm6e :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-ovBd1xfb18gctm6e .source>*{fill:#4A90D9!important;stroke:#2C5F8A!important;color:#fff!important;}#mermaid-svg-ovBd1xfb18gctm6e .source span{fill:#4A90D9!important;stroke:#2C5F8A!important;color:#fff!important;}#mermaid-svg-ovBd1xfb18gctm6e .source tspan{fill:#fff!important;}#mermaid-svg-ovBd1xfb18gctm6e .compile>*{fill:#7B68EE!important;stroke:#5B48CE!important;color:#fff!important;}#mermaid-svg-ovBd1xfb18gctm6e .compile span{fill:#7B68EE!important;stroke:#5B48CE!important;color:#fff!important;}#mermaid-svg-ovBd1xfb18gctm6e .compile tspan{fill:#fff!important;}#mermaid-svg-ovBd1xfb18gctm6e .package>*{fill:#FF6B6B!important;stroke:#CC5555!important;color:#fff!important;}#mermaid-svg-ovBd1xfb18gctm6e .package span{fill:#FF6B6B!important;stroke:#CC5555!important;color:#fff!important;}#mermaid-svg-ovBd1xfb18gctm6e .package tspan{fill:#fff!important;}#mermaid-svg-ovBd1xfb18gctm6e .output>*{fill:#2ECC71!important;stroke:#25A55A!important;color:#fff!important;}#mermaid-svg-ovBd1xfb18gctm6e .output span{fill:#2ECC71!important;stroke:#25A55A!important;color:#fff!important;}#mermaid-svg-ovBd1xfb18gctm6e .output tspan{fill:#fff!important;} 源码工程
资源编译
ArkTS编译 → ABC字节码
原生库编译 → .so
配置文件合并与校验
资源索引生成
模块打包 → HAP
签名
最终HAP产物
整个流程可以分成四大阶段:
- 编译阶段:ArkTS代码编译成ABC字节码,C/C++代码编译成.so库
- 资源处理阶段:资源文件编译、索引生成、rawfile处理
- 配置阶段:module.json和entry.json合并校验,权限声明检查
- 打包签名阶段:所有产物按目录结构组装成ZIP,然后签名
hvigorw:打包的幕后推手
DevEco Studio点"Build"的时候,实际执行的是hvigorw命令。hvigor是HarmonyOS的构建工具,类似Android的Gradle,但基于TypeScript编写,扩展性更强。
核心命令就这几个:
bash
# Debug打包
hvigorw assembleHap --mode module -p module=entry@default -p product=default
# Release打包
hvigorw assembleHap --mode module -p module=entry@default -p product=default -p buildType=release
# 清理构建产物
hvigorw clean
你可能会问:--mode module是啥?-p module=entry@default又是啥?别急,后面代码实战部分会拆开讲。
代码实战
基础用法:单模块打包配置
一个最简单的Entry模块,打包配置长这样:
typescript
// entry/src/main/module.json5
{
"module": {
"name": "entry", // 模块名称,必须唯一
"type": "entry", // 模块类型:entry/feature/har/hsp
"deviceTypes": [ // 支持的设备类型
"phone",
"tablet",
"2in1"
],
"deliveryWithInstall": true, // 是否随应用一起安装
"installationFree": false, // 是否支持免安装
"pages": "$profile:main_pages", // 页面路由配置
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
]
}
}
这个配置文件决定了你的HAP里装什么、怎么装。type: "entry"表示这是主入口模块,每个应用有且只能有一个entry模块。deviceTypes决定了这个HAP能装到什么设备上------写错了,设备上就装不上。
再看构建配置:
typescript
// entry/hvigorfile.ts
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: hapTasks, // 使用HAP构建任务插件
plugins: [] // 可扩展自定义构建插件
}
typescript
// hvigor/hvigor-config.json5
{
"modelVersion": "5.0.0",
"dependencies": {
"@ohos/hvigor-ohos-plugin": "5.0.0" // hvigor插件版本
}
}
这是最基础的打包配置,基本上DevEco Studio创建工程时就帮你生成好了。但光知道这些不够,你得知道怎么改、改了会怎样。
进阶用法:构建变体与产物定制
实际项目中,你不可能只有一个构建配置。Debug要调试信息,Release要压缩混淆;国内版和国际版可能用不同的API地址;免费版和付费版功能不同。这就需要构建变体。
typescript
// entry/oh-package.json5
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {},
"devDependencies": {},
"dynamicDependencies": {} // 动态依赖配置
}
typescript
// build-profile.json5(工程级构建配置)
{
"app": {
"signingConfigs": [],
"compileSdkVersion": 12,
"compatibleSdkVersion": 10,
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "10.0.0",
"runtimeOS": "HarmonyOS",
"output": {
"artifactName": "MyApp", // 产物名称
"module": {
"entry": {
"compress": { // 压缩配置
"ark": true, // ArkTS字节码压缩
"resources": true // 资源压缩
}
}
}
}
},
{
"name": "beta", // Beta构建变体
"signingConfig": "beta",
"output": {
"artifactName": "MyApp-Beta"
}
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default",
"beta"
]
}
]
}
]
}
看到products数组没?每个对象就是一个构建变体。default是正式版,beta是测试版。打包时通过-p product=参数指定用哪个变体。
再来看一个更实用的场景------条件编译。你想在Debug模式下打印日志,Release模式下自动去掉:
typescript
// util/Logger.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0001;
const TAG = 'MyApp';
export class Logger {
// 通过构建配置注入的宏来控制日志
static debug(message: string, ...args: string[]): void {
if (__DEV__) { // __DEV__ 是构建时注入的全局变量
hilog.debug(DOMAIN, TAG, message, args);
}
}
static info(message: string, ...args: string[]): void {
hilog.info(DOMAIN, TAG, message, args);
}
static error(message: string, ...args: string[]): void {
hilog.error(DOMAIN, TAG, message, args);
}
}
完整示例:从零到HAP的打包脚本
下面是一个完整的打包流程示例,包含环境检查、构建、产物验证:
typescript
// scripts/build-hap.ets
import { hapTasks, OhosPluginId } from '@ohos/hvigor-ohos-plugin';
import { HvigorBuildTask, HvigorTask } from '@ohos/hvigor';
// 自定义构建任务:打包前检查
class PreBuildCheckTask implements HvigorTask {
name = 'preBuildCheck';
run(): void {
console.log('[PreBuild] 开始打包前检查...');
// 检查module.json5是否存在
const moduleJsonPath = './src/main/module.json5';
if (!fs.existsSync(moduleJsonPath)) {
throw new Error('module.json5 不存在,请检查模块配置');
}
// 检查签名配置
const buildProfile = JSON.parse(fs.readFileSync('../build-profile.json5', 'utf-8'));
const products = buildProfile.app?.products || [];
if (products.length === 0) {
console.warn('[PreBuild] 警告:未配置构建产物,将使用默认配置');
}
// 检查SDK版本
const compileSdk = buildProfile.app?.compileSdkVersion;
if (compileSdk && compileSdk < 10) {
throw new Error(`compileSdkVersion ${compileSdk} 过低,最低要求 10`);
}
console.log('[PreBuild] 检查通过 ✓');
}
}
// 自定义构建任务:打包后验证
class PostBuildVerifyTask implements HvigorTask {
name = 'postBuildVerify';
run(): void {
console.log('[PostBuild] 开始产物验证...');
// 查找HAP产物
const outputDir = './build/default/outputs/default/';
const hapFiles = fs.readdirSync(outputDir).filter(f => f.endsWith('.hap'));
if (hapFiles.length === 0) {
throw new Error('未找到HAP产物,构建可能失败');
}
// 验证HAP大小
for (const hap of hapFiles) {
const stat = fs.statSync(path.join(outputDir, hap));
const sizeMB = stat.size / (1024 * 1024);
console.log(`[PostBuild] ${hap}: ${sizeMB.toFixed(2)} MB`);
if (sizeMB > 50) {
console.warn(`[PostBuild] 警告:${hap} 体积超过50MB,建议优化`);
}
}
console.log('[PostBuild] 验证通过 ✓');
}
}
// 注册自定义任务
export default {
system: hapTasks,
plugins: [
{
pluginId: OhosPluginId.HAP,
apply() {
// 在assembleHap任务前插入检查
this.registerTask(new PreBuildCheckTask(), {
before: 'assembleHap'
});
// 在assembleHap任务后插入验证
this.registerTask(new PostBuildVerifyTask(), {
after: 'assembleHap'
});
}
}
]
}
命令行打包完整流程:
bash
# 1. 清理旧产物
hvigorw clean
# 2. 检查依赖
ohpm install
# 3. Debug打包
hvigorw assembleHap --mode module -p module=entry@default -p product=default
# 4. Release打包(带签名)
hvigorw assembleHap --mode module -p module=entry@default -p product=default -p buildType=release
# 5. 查看产物
ls -la entry/build/default/outputs/default/
踩坑与注意事项
坑1:module.json5配置错误导致打包失败
最常见的错误就是module.json5里写了不存在的Ability或Page。打包工具会校验这些引用,找不到文件直接报错。而且报错信息有时候很模糊,只告诉你"resource not found",不告诉你是哪个。
解法 :打包前手动检查一遍引用路径。特别是pages和abilities里的srcEntry,确保文件真实存在。如果用了$profile:或$media:引用,确保对应的资源文件在resources目录下。
坑2:SDK版本不匹配
compileSdkVersion和compatibleSdkVersion搞混的人太多了。compileSdkVersion是你编译时用的SDK版本,compatibleSdkVersion是你的应用最低支持的SDK版本。前者决定你能用哪些API,后者决定哪些设备能装你的应用。
如果你compileSdkVersion写了12,但用的API是10就有的,compatibleSdkVersion可以写10------这样SDK 10以上的设备都能装。但如果你compatibleSdkVersion写了12,那SDK 10的设备就装不了,哪怕你根本没用12的新API。
解法 :compatibleSdkVersion尽量写低,覆盖更多设备。compileSdkVersion尽量写高,用最新API。
坑3:HAP体积莫名偏大
有时候你代码没几行,HAP却好几MB。大概率是resources里塞了大图或者rawfile里有不必要的文件。
解法 :把HAP解压开看看,resources/rawfile/和resources/base/media/是重灾区。图片用WebP格式替代PNG/JPG,rawfile里不用的文件删掉。
坑4:hvigorw命令在CI环境执行失败
CI服务器上没有DevEco Studio,hvigorw执行可能报环境问题。常见的是Node.js版本不对或者ohpm没装。
解法:CI环境确保Node.js 16+,ohpm已安装并配置好镜像源。在脚本开头加环境检查:
bash
# 检查Node版本
node --version
# 检查ohpm
ohpm --version
# 如果ohpm未安装
# export OHPM_HOME=/path/to/ohpm
# export PATH=$OHPM_HOME/bin:$PATH
坑5:打包产物路径不固定
不同版本的DevEco Studio,HAP产物路径可能不一样。有的在build/default/outputs/default/,有的在build/default/outputs/entry/。
解法:不要硬编码产物路径,用通配符查找:
bash
# 查找所有HAP文件
find . -name "*.hap" -path "*/build/*"
HarmonyOS 6适配说明
HarmonyOS 6对HAP打包做了几项重要调整:
-
ABC字节码版本升级 :HarmonyOS 6使用新版方舟编译器,ABC字节码格式有变化。如果你的HAP是用旧版SDK编译的,在HarmonyOS 6设备上可能无法运行。确保
compileSdkVersion至少为12。 -
module.json5新增字段 :HarmonyOS 6新增了
virtualMachine字段,用于指定目标虚拟机类型。默认值是"ark",一般不需要改。 -
资源索引格式变化 :HarmonyOS 6的资源索引编译器升级,生成的
resources.index格式与旧版不兼容。如果你的应用需要同时兼容HarmonyOS 5和6,compatibleSdkVersion设为10,但compileSdkVersion必须用12+编译。 -
hvigor 5.0 :构建工具升级到hvigor 5.0,插件API有Breaking Change。如果你有自定义构建插件,需要适配新API。主要变化是
HvigorTask接口的方法签名调整,run()方法现在返回Promise<void>。 -
HAP签名格式增强:HarmonyOS 6的签名算法升级,新增SHA-384支持。旧版签名的HAP在HarmonyOS 6上仍可安装,但新打包的HAP建议使用新签名算法。
总结
HAP打包这条链路,说复杂也不复杂------源码编译、资源处理、配置校验、打包签名,就这几步。说不复杂吧,每一步都有坑等着你。
关键记住三点:
- 配置先行:module.json5和build-profile.json5是打包的"图纸",写错了后面全白搭
- 理解产物:HAP就是个ZIP,遇到问题解压看看,比猜来猜去靠谱
- 善用命令行:DevEco Studio能做的,hvigorw命令行都能做,CI/CD场景必须用命令行