HarmonyOS开发:打包自动化——打包脚本
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.ts中import第三方模块时,可能报"module not found"。因为hvigor的运行环境和应用代码的运行环境不同,它用的是Node.js。
解法:hvigorfile.ts中只能导入:
@ohos/hvigor-ohos-plugin:官方构建插件- Node.js内置模块:
fs、path、crypto等 - 在
hvigor/hvigor-config.json5的dependencies中声明的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在构建自动化方面有以下改进:
-
hvigor 5.0插件API升级:插件API全面升级,支持更灵活的任务编排。新增
task.dependsOn()和task.mustRunAfter()方法,精确控制任务执行顺序。 -
构建缓存优化:hvigor 5.0的构建缓存更智能,支持跨模块缓存共享。多模块工程的构建速度提升30-50%。
-
并行构建:支持模块级并行构建,多核CPU利用率大幅提升。在
hvigor-config.json5中配置:
{
"modelVersion": "5.0.0",
"configuration": {
"parallel": true,
"maxParallelTasks": 4
}
}
-
构建配置验证:hvigor 5.0在构建前自动验证
build-profile.json5和module.json5的合法性,配置错误时给出明确的修复建议,而不是到构建中途才报错。 -
自定义构建报告:新增
build-report.json输出,包含构建耗时、各阶段时间分布、产物信息等。方便CI中分析构建性能瓶颈。
总结
打包自动化是从"手工作坊"到"流水线"的关键一步。写好构建脚本,一次配置,终身受益。
核心思路:
- 任务化:把打包拆成一个个小任务,每个任务只做一件事
- 参数化:渠道、版本、签名信息全用环境变量,不硬编码
- 自动化:构建、重命名、检查、上传,一条命令搞定
- 可追溯:每次构建生成报告,记录版本、哈希、时间
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐ hvigor API需要学习,但TypeScript写起来不费劲 |
| 使用频率 | ⭐⭐⭐⭐ CI/CD必用,日常开发也常用 |
| 重要程度 | ⭐⭐⭐⭐ 自动化是工程化的基础,没有自动化就没有效率 |
- 点赞
- 收藏
- 关注作者
评论(0)