HarmonyOS开发:HAP打包——应用打包流程

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产物

整个流程可以分成四大阶段:

  1. 编译阶段:ArkTS代码编译成ABC字节码,C/C++代码编译成.so库
  2. 资源处理阶段:资源文件编译、索引生成、rawfile处理
  3. 配置阶段:module.json和entry.json合并校验,权限声明检查
  4. 打包签名阶段:所有产物按目录结构组装成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",不告诉你是哪个。

解法 :打包前手动检查一遍引用路径。特别是pagesabilities里的srcEntry,确保文件真实存在。如果用了$profile:$media:引用,确保对应的资源文件在resources目录下。

坑2:SDK版本不匹配

compileSdkVersioncompatibleSdkVersion搞混的人太多了。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打包做了几项重要调整:

  1. ABC字节码版本升级 :HarmonyOS 6使用新版方舟编译器,ABC字节码格式有变化。如果你的HAP是用旧版SDK编译的,在HarmonyOS 6设备上可能无法运行。确保compileSdkVersion至少为12。

  2. module.json5新增字段 :HarmonyOS 6新增了virtualMachine字段,用于指定目标虚拟机类型。默认值是"ark",一般不需要改。

  3. 资源索引格式变化 :HarmonyOS 6的资源索引编译器升级,生成的resources.index格式与旧版不兼容。如果你的应用需要同时兼容HarmonyOS 5和6,compatibleSdkVersion设为10,但compileSdkVersion必须用12+编译。

  4. hvigor 5.0 :构建工具升级到hvigor 5.0,插件API有Breaking Change。如果你有自定义构建插件,需要适配新API。主要变化是HvigorTask接口的方法签名调整,run()方法现在返回Promise<void>

  5. HAP签名格式增强:HarmonyOS 6的签名算法升级,新增SHA-384支持。旧版签名的HAP在HarmonyOS 6上仍可安装,但新打包的HAP建议使用新签名算法。

总结

HAP打包这条链路,说复杂也不复杂------源码编译、资源处理、配置校验、打包签名,就这几步。说不复杂吧,每一步都有坑等着你。

关键记住三点:

  • 配置先行:module.json5和build-profile.json5是打包的"图纸",写错了后面全白搭
  • 理解产物:HAP就是个ZIP,遇到问题解压看看,比猜来猜去靠谱
  • 善用命令行:DevEco Studio能做的,hvigorw命令行都能做,CI/CD场景必须用命令行