HarmonyOS开发:打包自动化——打包脚本

举报
Jack20 发表于 2026/06/25 20:39:08 2026/06/25
【摘要】 HarmonyOS开发:打包自动化——打包脚本📌 核心要点:hvigor构建脚本是基于TypeScript的自动化引擎,掌握自定义构建任务、多渠道打包和产物后处理,才能实现从"手动点按钮"到"一键出包"的跨越。 背景与动机你每次打包的流程是不是这样的:打开DevEco Studio → 点Build → 等几分钟 → 找到HAP文件 → 手动改个名字 → 复制到发布目录 → 再打个内测包...

HarmonyOS开发:打包自动化——打包脚本

📌 核心要点:hvigor构建脚本是基于TypeScript的自动化引擎,掌握自定义构建任务、多渠道打包和产物后处理,才能实现从"手动点按钮"到"一键出包"的跨越。

背景与动机

你每次打包的流程是不是这样的:打开DevEco Studio → 点Build → 等几分钟 → 找到HAP文件 → 手动改个名字 → 复制到发布目录 → 再打个内测包 → 再改个名字 → 再复制……

三个渠道打三遍,每次10分钟,一天打三次就是半小时。一个月就是15个小时,全花在重复操作上了。

更别提CI/CD了——没有自动化脚本,你的CI流水线就是个摆设。

打包自动化要解决的核心问题:让机器干重复的活,让人干有价值的活。构建脚本就是你的"打包机器人",你告诉它怎么打,它就怎么打,不喊累不出错。

核心原理

hvigor构建系统架构

hvigor是HarmonyOS的构建工具,基于TypeScript编写,类似Android的Gradle但更灵活:

flowchart TD
    A[hvigorw命令] --> B[hvigor引擎]
    B --> C[解析hvigorfile.ts]
    C --> D[构建任务图]
    D --> E[任务执行]

    E --> F[预处理任务]
    E --> G[编译任务]
    E --> H[打包任务]
    E --> I[后处理任务]

    F --> F1[依赖检查]
    F --> F2[代码生成]
    F --> F3[资源预处理]

    G --> G1[ArkTS编译]
    G --> G2[资源编译]
    G --> G3[原生编译]

    H --> H1[HAP打包]
    H --> H2[签名]
    H --> H3[APP打包]

    I --> I1[产物重命名]
    I --> I2[体积检查]
    I --> I3[上传分发]

    classDef cmd fill:#FF6B6B,stroke:#CC5555,color:#fff
    classDef engine fill:#4A90D9,stroke:#2C5F8A,color:#fff
    classDef task fill:#F39C12,stroke:#D68910,color:#fff
    classDef detail fill:#2ECC71,stroke:#25A55A,color:#fff

    class A cmd
    class B,C,D,E engine
    class F,G,H,I task
    class F1,F2,F3,G1,G2,G3,H1,H2,H3,I1,I2,I3 detail

hvigorfile.ts结构

hvigor的构建逻辑写在hvigorfile.ts中,每个模块一个:

// entry/hvigorfile.ts
import { hapTasks } from '@ohos/hvigor-ohos-plugin';

export default {
  system: hapTasks,       // 系统内置的HAP构建任务
  plugins: []             // 自定义构建插件
}

构建任务生命周期

flowchart LR
    A[preBuild] --> B[compileArkTS]
    B --> C[compileResources]
    C --> D[assembleHap]
    D --> E[signHap]
    E --> F[postBuild]

    classDef pre fill:#2ECC71,stroke:#25A55A,color:#fff
    classDef compile fill:#4A90D9,stroke:#2C5F8A,color:#fff
    classDef package fill:#F39C12,stroke:#D68910,color:#fff
    classDef post fill:#9B59B6,stroke:#8E44AD,color:#fff

    class A pre
    class B,C compile
    class D,E package
    class F post

你可以在任何任务前后插入自定义逻辑。

代码实战

基础用法:自定义构建任务

最简单的自定义任务——在构建前打印环境信息,构建后复制产物:

// entry/hvigorfile.ts
import { hapTasks, OhosPluginId, HvigorTask } from '@ohos/hvigor-ohos-plugin';

// 自定义任务:构建前环境检查
class EnvCheckTask implements HvigorTask {
  name = 'envCheck';

  async run(): Promise<void> {
    console.log('===== 构建环境检查 =====');
    console.log(`Node版本: ${process.version}`);
    console.log(`构建时间: ${new Date().toLocaleString()}`);
    console.log(`构建类型: ${process.env.BUILD_TYPE || 'debug'}`);
    console.log('========================');
  }
}

// 自定义任务:构建后产物整理
class ArtifactOrganizeTask implements HvigorTask {
  name = 'artifactOrganize';

  async run(): Promise<void> {
    console.log('===== 整理构建产物 =====');

    const outputDir = './build/default/outputs/default/';
    if (!fs.existsSync(outputDir)) {
      console.error('构建产物目录不存在');
      return;
    }

    // 查找HAP文件
    const hapFiles = fs.readdirSync(outputDir)
      .filter(f => f.endsWith('.hap'));

    for (const hap of hapFiles) {
      const stat = fs.statSync(path.join(outputDir, hap));
      const sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
      console.log(`  ${hap}: ${sizeMB} MB`);
    }

    console.log('========================');
  }
}

export default {
  system: hapTasks,
  plugins: [
    {
      pluginId: OhosPluginId.HAP,
      apply() {
        // 在构建前插入环境检查
        this.registerTask(new EnvCheckTask(), {
          before: 'assembleHap'
        });

        // 在构建后插入产物整理
        this.registerTask(new ArtifactOrganizeTask(), {
          after: 'signHap'
        });
      }
    }
  ]
}

进阶用法:多渠道打包

不同渠道的包可能需要不同的配置:渠道号、API地址、功能开关。手动改配置再打包?太原始了。

// scripts/channel-config.ets
// 多渠道配置

interface ChannelConfig {
  channel: string;              // 渠道标识
  appName: string;              // 应用名称
  apiUrl: string;               // API地址
  features: {                   // 功能开关
    payment: boolean;
    live: boolean;
    social: boolean;
  };
  extraMeta: Record<string, string>;  // 额外元数据
}

// 渠道配置表
const CHANNEL_CONFIGS: ChannelConfig[] = [
  {
    channel: 'huawei',
    appName: '我的应用',
    apiUrl: 'https://api.myapp.com',
    features: { payment: true, live: true, social: true },
    extraMeta: { market: 'huawei', region: 'cn' }
  },
  {
    channel: 'xiaomi',
    appName: '我的应用',
    apiUrl: 'https://api.myapp.com',
    features: { payment: true, live: false, social: true },
    extraMeta: { market: 'xiaomi', region: 'cn' }
  },
  {
    channel: 'overseas',
    appName: 'My App',
    apiUrl: 'https://api-global.myapp.com',
    features: { payment: true, live: true, social: false },
    extraMeta: { market: 'google_play', region: 'global' }
  }
];

export function getChannelConfig(channel: string): ChannelConfig | undefined {
  return CHANNEL_CONFIGS.find(c => c.channel === channel);
}

export function getAllChannels(): string[] {
  return CHANNEL_CONFIGS.map(c => c.channel);
}
// entry/hvigorfile.ts
// 多渠道打包脚本

import { hapTasks, OhosPluginId, HvigorTask } from '@ohos/hvigor-ohos-plugin';
import { getChannelConfig, getAllChannels } from '../scripts/channel-config';

// 多渠道打包任务
class MultiChannelBuildTask implements HvigorTask {
  name = 'multiChannelBuild';

  async run(): Promise<void> {
    const targetChannel = process.env.CHANNEL || 'huawei';
    const config = getChannelConfig(targetChannel);

    if (!config) {
      throw new Error(`未知渠道: ${targetChannel},支持: ${getAllChannels().join(', ')}`);
    }

    console.log(`\n===== 渠道打包: ${config.channel} =====`);
    console.log(`  应用名称: ${config.appName}`);
    console.log(`  API地址: ${config.apiUrl}`);
    console.log(`  功能开关: ${JSON.stringify(config.features)}`);

    // 1. 生成渠道配置文件(写入rawfile)
    this.generateChannelConfig(config);

    // 2. 更新字符串资源(应用名称)
    this.updateStringResource(config);

    // 3. 更新功能开关
    this.updateFeatureFlags(config);

    console.log('===== 渠道配置完成 =====\n');
  }

  // 生成渠道配置文件
  private generateChannelConfig(config: ChannelConfig): void {
    const configDir = './src/main/resources/rawfile/';
    if (!fs.existsSync(configDir)) {
      fs.mkdirSync(configDir, { recursive: true });
    }

    const configContent = JSON.stringify({
      channel: config.channel,
      apiUrl: config.apiUrl,
      features: config.features,
      extraMeta: config.extraMeta,
      buildTime: new Date().toISOString(),
      buildVersion: process.env.BUILD_VERSION || '1.0.0'
    }, null, 2);

    fs.writeFileSync(
      path.join(configDir, 'channel_config.json'),
      configContent
    );

    console.log('  ✅ 渠道配置文件已生成');
  }

  // 更新应用名称
  private updateStringResource(config: ChannelConfig): void {
    const stringResPath = './src/main/resources/base/element/string.json';
    if (!fs.existsSync(stringResPath)) return;

    const stringRes = JSON.parse(fs.readFileSync(stringResPath, 'utf-8'));
    const entryLabel = stringRes.string.find(
      (s: any) => s.name === 'EntryAbility_label'
    );
    if (entryLabel) {
      entryLabel.value = config.appName;
      fs.writeFileSync(stringResPath, JSON.stringify(stringRes, null, 2));
      console.log(`  ✅ 应用名称已更新: ${config.appName}`);
    }
  }

  // 更新功能开关
  private updateFeatureFlags(config: ChannelConfig): void {
    // 根据功能开关决定是否包含Feature模块
    const buildProfilePath = '../build-profile.json5';
    if (!fs.existsSync(buildProfilePath)) return;

    const buildProfile = JSON.parse(
      fs.readFileSync(buildProfilePath, 'utf-8')
    );

    // 过滤不需要的Feature模块
    const modules = buildProfile.modules || [];
    const filteredModules = modules.filter((m: any) => {
      if (m.name === 'entry' || m.name === 'shared_common') return true;
      if (m.name === 'feature_payment') return config.features.payment;
      if (m.name === 'feature_live') return config.features.live;
      if (m.name === 'feature_social') return config.features.social;
      return true;
    });

    buildProfile.modules = filteredModules;
    fs.writeFileSync(buildProfilePath, JSON.stringify(buildProfile, null, 2));

    console.log(`  ✅ 功能模块已更新: ${filteredModules.map((m: any) => m.name).join(', ')}`);
  }
}

export default {
  system: hapTasks,
  plugins: [
    {
      pluginId: OhosPluginId.HAP,
      apply() {
        this.registerTask(new MultiChannelBuildTask(), {
          before: 'assembleHap'
        });
      }
    }
  ]
}

多渠道打包命令:

# 华为渠道
CHANNEL=huawei hvigorw assembleHap --mode module -p module=entry@default -p product=release

# 小米渠道
CHANNEL=xiaomi hvigorw assembleHap --mode module -p module=entry@default -p product=release

# 海外渠道
CHANNEL=overseas hvigorw assembleHap --mode module -p module=entry@default -p product=release

# 批量打包所有渠道
for channel in huawei xiaomi overseas; do
  echo "===== 打包渠道: $channel ====="
  CHANNEL=$channel hvigorw assembleHap --mode module -p module=entry@default -p product=release
  # 重命名产物
  mv entry/build/default/outputs/default/entry-default-signed.hap \
     release/MyApp-${channel}-$(date +%Y%m%d).hap
done

完整示例:构建产物后处理

打包完成后,还需要做一系列后处理:产物重命名、体积检查、哈希计算、上传分发。把这些全自动化:

// scripts/post-build.ets
// 构建产物后处理脚本

import { createHash } from 'crypto';

interface BuildArtifact {
  name: string;
  path: string;
  size: number;
  md5: string;
  sha256: string;
  buildTime: string;
  channel: string;
  version: string;
}

// 后处理任务
class PostBuildProcessTask implements HvigorTask {
  name = 'postBuildProcess';

  async run(): Promise<void> {
    console.log('\n===== 开始构建后处理 =====');

    const outputDir = './build/default/outputs/default/';
    const releaseDir = '../release/';

    // 确保发布目录存在
    if (!fs.existsSync(releaseDir)) {
      fs.mkdirSync(releaseDir, { recursive: true });
    }

    // 查找所有HAP文件
    const hapFiles = fs.readdirSync(outputDir)
      .filter(f => f.endsWith('.hap'));

    const artifacts: BuildArtifact[] = [];

    for (const hap of hapFiles) {
      const hapPath = path.join(outputDir, hap);
      const stat = fs.statSync(hapPath);

      // 计算哈希
      const fileData = fs.readFileSync(hapPath);
      const md5 = createHash('md5').update(fileData).digest('hex');
      const sha256 = createHash('sha256').update(fileData).digest('hex');

      // 构建产物信息
      const artifact: BuildArtifact = {
        name: hap,
        path: hapPath,
        size: stat.size,
        md5: md5,
        sha256: sha256,
        buildTime: new Date().toISOString(),
        channel: process.env.CHANNEL || 'default',
        version: process.env.BUILD_VERSION || '1.0.0'
      };

      artifacts.push(artifact);

      // 1. 重命名产物
      const newName = this.generateArtifactName(artifact);
      const newPath = path.join(releaseDir, newName);
      fs.copyFileSync(hapPath, newPath);
      console.log(`  ✅ 产物已复制: ${newName}`);

      // 2. 体积检查
      this.checkArtifactSize(artifact);

      // 3. 生成产物清单
      this.generateManifest(artifact, newPath);
    }

    // 4. 生成构建报告
    this.generateBuildReport(artifacts);

    // 5. 上传到分发平台(可选)
    if (process.env.AUTO_UPLOAD === 'true') {
      await this.uploadArtifacts(artifacts);
    }

    console.log('===== 构建后处理完成 =====\n');
  }

  // 生成产物文件名
  private generateArtifactName(artifact: BuildArtifact): string {
    const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
    const sizeMB = (artifact.size / (1024 * 1024)).toFixed(1);
    return `MyApp-${artifact.channel}-v${artifact.version}-${date}-${sizeMB}MB.hap`;
  }

  // 体积检查
  private checkArtifactSize(artifact: BuildArtifact): void {
    const sizeMB = artifact.size / (1024 * 1024);
    const maxSize = 50;    // 最大允许50MB

    if (sizeMB > maxSize) {
      console.error(`${artifact.name} 体积 ${sizeMB.toFixed(2)}MB 超过限制 ${maxSize}MB`);
    } else if (sizeMB > maxSize * 0.8) {
      console.warn(`  ⚠️ ${artifact.name} 体积 ${sizeMB.toFixed(2)}MB 接近限制`);
    } else {
      console.log(`${artifact.name} 体积 ${sizeMB.toFixed(2)}MB 正常`);
    }
  }

  // 生成产物清单
  private generateManifest(artifact: BuildArtifact, releasePath: string): void {
    const manifestPath = releasePath.replace('.hap', '.manifest.json');
    const manifest = {
      name: artifact.name,
      channel: artifact.channel,
      version: artifact.version,
      buildTime: artifact.buildTime,
      size: artifact.size,
      md5: artifact.md5,
      sha256: artifact.sha256,
      releasePath: releasePath
    };

    fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
    console.log(`  ✅ 产物清单已生成: ${path.basename(manifestPath)}`);
  }

  // 生成构建报告
  private generateBuildReport(artifacts: BuildArtifact[]): void {
    const reportPath = '../release/build-report.json';
    const report = {
      buildTime: new Date().toISOString(),
      buildMachine: process.env.COMPUTERNAME || 'unknown',
      nodeVersion: process.version,
      channel: process.env.CHANNEL || 'default',
      version: process.env.BUILD_VERSION || '1.0.0',
      artifacts: artifacts.map(a => ({
        name: a.name,
        size: a.size,
        sizeFormatted: `${(a.size / (1024 * 1024)).toFixed(2)} MB`,
        md5: a.md5,
        sha256: a.sha256
      })),
      totalSize: artifacts.reduce((sum, a) => sum + a.size, 0)
    };

    fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
    console.log(`  ✅ 构建报告已生成: build-report.json`);
  }

  // 上传到分发平台
  private async uploadArtifacts(artifacts: BuildArtifact[]): Promise<void> {
    console.log('  📤 上传构建产物...');

    for (const artifact of artifacts) {
      try {
        // 调用AGC上传API或自建分发平台API
        // const response = await uploadToAGC(artifact);
        console.log(`${artifact.name} 上传成功`);
      } catch (error) {
        console.error(`${artifact.name} 上传失败: ${error}`);
      }
    }
  }
}

export default {
  system: hapTasks,
  plugins: [
    {
      pluginId: OhosPluginId.HAP,
      apply() {
        this.registerTask(new PostBuildProcessTask(), {
          after: 'signHap'
        });
      }
    }
  ]
}

完整的CI打包脚本

#!/bin/bash
# ci-build.sh - CI环境一键打包脚本

set -e    # 任何命令失败立即退出

# ===== 配置 =====
APP_NAME="MyApp"
VERSION="${BUILD_VERSION:-1.0.0}"
CHANNELS=("huawei" "xiaomi" "overseas")
RELEASE_DIR="./release"

# ===== 环境检查 =====
echo "===== 环境检查 ====="
echo "Node版本: $(node --version)"
echo "ohpm版本: $(ohpm --version)"
echo "hvigorw版本: $(hvigorw --version 2>/dev/null || echo 'unknown')"

# 检查签名环境变量
if [[ -z "$RELEASE_P12_PATH" || -z "$RELEASE_P12_PWD" ]]; then
  echo "❌ 缺少发布签名环境变量"
  exit 1
fi

# ===== 准备 =====
echo "===== 准备构建环境 ====="

# 清理旧产物
rm -rf "$RELEASE_DIR"
mkdir -p "$RELEASE_DIR"

# 安装依赖
ohpm install --all

# ===== 多渠道打包 =====
for CHANNEL in "${CHANNELS[@]}"; do
  echo ""
  echo "=========================================="
  echo "  打包渠道: $CHANNEL"
  echo "=========================================="

  # 清理构建缓存
  hvigorw clean

  # 设置环境变量
  export CHANNEL=$CHANNEL
  export BUILD_VERSION=$VERSION
  export BUILD_TYPE=release

  # 执行构建
  hvigorw assembleHap \
    --mode module \
    -p module=entry@default \
    -p product=release

  # 检查构建结果
  HAP_FILE=$(find ./entry/build -name "*.hap" -path "*/default/*" | head -1)
  if [[ -z "$HAP_FILE" ]]; then
    echo "❌ 渠道 $CHANNEL 构建失败:未找到HAP文件"
    continue
  fi

  # 重命名并复制
  DATE=$(date +%Y%m%d)
  NEW_NAME="${APP_NAME}-${CHANNEL}-v${VERSION}-${DATE}.hap"
  cp "$HAP_FILE" "$RELEASE_DIR/$NEW_NAME"

  # 计算哈希
  MD5=$(md5sum "$RELEASE_DIR/$NEW_NAME" | cut -d' ' -f1)
  SIZE=$(du -h "$RELEASE_DIR/$NEW_NAME" | cut -f1)

  echo "✅ 渠道 $CHANNEL 构建成功"
  echo "   文件: $NEW_NAME"
  echo "   大小: $SIZE"
  echo "   MD5: $MD5"
done

# ===== 生成总报告 =====
echo ""
echo "===== 构建总报告 ====="
echo "版本: $VERSION"
echo "构建时间: $(date)"
echo "渠道数量: ${#CHANNELS[@]}"
echo ""

ls -la "$RELEASE_DIR/"

echo ""
echo "✅ 所有渠道打包完成!"

踩坑与注意事项

坑1:hvigorfile.ts中导入模块失败

hvigorfile.tsimport第三方模块时,可能报"module not found"。因为hvigor的运行环境和应用代码的运行环境不同,它用的是Node.js。

解法:hvigorfile.ts中只能导入:

  • @ohos/hvigor-ohos-plugin:官方构建插件
  • Node.js内置模块:fspathcrypto
  • hvigor/hvigor-config.json5dependencies中声明的npm包

不要在hvigorfile.ts中导入应用代码或ArkTS模块。

坑2:自定义任务执行顺序错误

你注册了一个任务,期望它在assembleHap之前执行,但实际上它在之后执行了。原因是任务名写错了或者大小写不匹配。

解法:hvigor的任务名是大小写敏感的。确认任务名的方法:

# 列出所有可用任务
hvigorw tasks

# 查看任务依赖关系
hvigorw taskGraph

坑3:多渠道打包时构建缓存导致配置残留

打完华为渠道的包,再打小米渠道时,华为的配置文件还残留在rawfile中。因为hvigor的增量编译可能跳过了文件生成步骤。

解法:每次切换渠道前执行hvigorw clean,清理构建缓存。或者在自定义任务中强制重新生成配置文件,不依赖增量编译。

坑4:CI环境中hvigorw权限问题

Linux CI服务器上,hvigorw可能没有执行权限,直接报"permission denied"。

解法

# 赋予执行权限
chmod +x hvigorw

# 或者通过Node直接运行
node hvigor/hvigor-wrapper.js assembleHap --mode module -p module=entry@default

坑5:构建脚本修改后不生效

修改了hvigorfile.ts,但构建行为没变。因为hvigor可能缓存了旧的构建脚本。

解法:修改hvigorfile.ts后,执行一次hvigorw clean,确保新脚本被重新加载。

HarmonyOS 6适配说明

HarmonyOS 6在构建自动化方面有以下改进:

  1. hvigor 5.0插件API升级:插件API全面升级,支持更灵活的任务编排。新增task.dependsOn()task.mustRunAfter()方法,精确控制任务执行顺序。

  2. 构建缓存优化:hvigor 5.0的构建缓存更智能,支持跨模块缓存共享。多模块工程的构建速度提升30-50%。

  3. 并行构建:支持模块级并行构建,多核CPU利用率大幅提升。在hvigor-config.json5中配置:

{
  "modelVersion": "5.0.0",
  "configuration": {
    "parallel": true,
    "maxParallelTasks": 4
  }
}
  1. 构建配置验证:hvigor 5.0在构建前自动验证build-profile.json5module.json5的合法性,配置错误时给出明确的修复建议,而不是到构建中途才报错。

  2. 自定义构建报告:新增build-report.json输出,包含构建耗时、各阶段时间分布、产物信息等。方便CI中分析构建性能瓶颈。

总结

打包自动化是从"手工作坊"到"流水线"的关键一步。写好构建脚本,一次配置,终身受益。

核心思路:

  • 任务化:把打包拆成一个个小任务,每个任务只做一件事
  • 参数化:渠道、版本、签名信息全用环境变量,不硬编码
  • 自动化:构建、重命名、检查、上传,一条命令搞定
  • 可追溯:每次构建生成报告,记录版本、哈希、时间
维度 评价
学习难度 ⭐⭐⭐ hvigor API需要学习,但TypeScript写起来不费劲
使用频率 ⭐⭐⭐⭐ CI/CD必用,日常开发也常用
重要程度 ⭐⭐⭐⭐ 自动化是工程化的基础,没有自动化就没有效率
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。