一、实现背景
在多层次多组件项目中,多个组件之间存在复杂的依赖关系,组件作为SDK对外部提供时,需要一层一层的打出SDK包,然后修改源码依赖为SDK依赖再打出更上层的SDK包,为简化这一过程,特意写了脚本控制打包的流程。因电脑为windows,且cmd不支持某些高级shell语法,所以采用了PowerShell脚本来实现自动打包SDK。
二、环境参数
-
OS: WINDOWS 10
-
IDE: DevEco Studio 5.0.0 Release,Build Version: 5.0.3.910, built on November 1, 2024
-
SDK: HarmonyOS 5.0.0 Release SDK, based on OpenHarmony SDK Ohos_sdk_public 5.0.0.71 (API Version 12 Release)
-
PSVersion: 7.4.4
三、实现过程
1. 什么是PowerShell
PowerShell是Windows推出的一种跨平台的任务自动化解决方案,可在Windows、Linux、macOS上运行。PowerShell是在.NET公共语言运行时(CLR)上构建的,所有输入输出都是.NET对象。作为一种脚本语言,经常用在CI/CD环境中生成、测试和部署解决方案。
我的理解是PowerShell是CMD的升级版本,是对标Linux Shell的,完美适配Windows环境,同时兼容Linux和macOS。但是Windows系统默认不带PowerShell 7.x(与5.1低版本差异较大,建议使用高版本),所以需要从官网下载安装。我下载的zip安装包,zip安装包需要解锁,参考官方说明即可。
2. 使用 Visual Studio Code 进行 PowerShell 开发
Visual Studio Code (VS Code) 是 Microsoft 开发的跨平台脚本编辑器。
在VSC中添加PowerShell插件即可高亮PoserShell语法。
对PowerShell不熟练的同学可以添加AI工具插件,辅助编写脚本。例如TONGYI Lingma,GitHub Copilot,或者可以直接用集成了智能AI代码的工具的Cursor。
3.脚本环境配置
3.1 编译工具配置
定位到DevEco Studio安装位置,找到IDE内部自带的编译所需工具:ohpm、hvigorw、node环境,添加到系统Path环境变量
3.2 脚本全局变量配置
脚本需要知道项目根目录ROOT_DIR,临时存放组件编译产物地址LIB_DIR,编译模式BUILD_MODE,这些参数需要在脚本最开始的位置定义,代码如下:
$ROOT_DIR = $PSScriptRoot
$LIB_DIR = "$ROOT_DIR\libs"
$BUILD_MODE = "release"
4.脚本编写
脚本大的思路分两步走: 1.删除组件编译缓存; 2.重新编译组件
4.1 脚本删除缓存
每次打包编译前,脚本会删除项目中的缓存文件,确保每次编译生成的包为最新代码包。删除的文件包括:on_modules、build、oh-package-lock.json5、libs(LIB_DIR)
4.1.1 删除on_modules、build代码如下:
# 遍历目录并删除满足条件的目录及其子文件
Get-ChildItem -Path "$ROOT_DIR" -Depth 3 -Attributes Directory | ForEach-Object {
# 这里可以添加你的条件判断,例如基于目录名称或其他属性
# 如果条件满足,则删除目录及其子文件
$dir0 = $_.Name
$fullName = $_.FullName
if (( $dir0 -eq "oh_modules" ) -or ( $dir0 -eq "build" )) {
Write-Host -ForegroundColor Yellow "目录存在... $dir0 删除目录... $fullName"
Remove-Item "$fullName" -Recurse -Force
}
}
4.1.2 删除oh-package-lock.json5的代码如下:
Get-ChildItem -Path $ROOT_DIR -Recurse -Filter oh-package-lock.json5 | ForEach-Object {
$lockFileName = $_.FullName
Write-Output "文件存在,删除文件:$lockFileName"
Remove-Item "$lockFileName" -Force
}
4.1.3 libs(LIB_DIR)目录为存放组件编译产物(har包、tgz包...)的位置,每次编译前会清空,如果不存在则需要重新创建,代码如下:
# 检查目录是否存在
if (Test-Path "$LIB_DIR" ) {
Write-Host -ForegroundColor Yellow "目录存在,删除目录..."
Remove-Item "$LIB_DIR" -Recurse -Force
}
mkdir -p "$LIB_DIR"
4.2 脚本编译组件
每个组件需要单独编译,安装依赖和正式编译都有IDE自带脚本ohpm、hvigorw可直接调用完成,组件内源码依赖需要替换为SDK依赖的部分需要编写PowerShell脚本代码处理,下图即为一个组件编译主流程。
4.2.1 针对没有本地依赖需要替换的组件来说,可以去掉处理本地依赖环节,更新流程图如下:
每个模块都共用的代码可以抽取出来作为buildModule方法,主要涉及ohpm安装依赖,hvigorw编译组件,移动编译产物到LIB_DIR,代码如下:
function buildModule {
param(
$MODULE_PATH,
$MODULE_NAME,
$ASSEMBLE_TYPE
)
Write-Host -ForegroundColor Yellow "buildModule MODULE_PATH:$MODULE_PATH MODULE_NAME:$MODULE_NAME ASSEMBLE_TYPE:$ASSEMBLE_TYPE"
$ASSEMBLE = capitalize_first_letter $ASSEMBLE_TYPE
ohpm install --registry https://ohpm.openharmony.cn/ohpm/ --strict_ssl true
hvigorw --sync -p product=default -p buildMode=$BUILD_MODE --analyze=normal --parallel --incremental --daemon
hvigorw --mode module -p product=default -p module=$MODULE_NAME@default -p buildMode=$BUILD_MODE assemble$ASSEMBLE --analyze=normal --parallel --incremental --daemon
# 获取所有匹配的文件
$filesToCopy = Get-ChildItem -Path "$MODULE_PATH\$MODULE_NAME\build\default\outputs\default" -Include @("*.har", "*.tgz") -Recurse
# 复制这些文件到目标目录
foreach ($file in $filesToCopy) {
Copy-Item $file -Destination $LIB_DIR -Force
Write-Host -ForegroundColor Yellow "$file 已复制到 $LIB_DIR"
}
}
4.2.2 部分组件需要将源码依赖替换为SDK包依赖再编译,处理依赖的逻辑如下图:
其中具体替换每一条依赖键值对的逻辑如下:
单组件带替换依赖的buildModuleWithDependence方法代码如下:
function buildModuleWithDependence {
param(
$MODULE_PATH,
$MODULE_NAME,
$ASSEMBLE_TYPE,
$TGZ_LIST
)
$MODULE_LIB = "$MODULE_PATH\$MODULE_NAME\libs"
$package_path = "$MODULE_PATH\$MODULE_NAME\oh-package.json5"
$package_path_back = "$MODULE_PATH\$MODULE_NAME\oh-package_back.json5"
$package_path_temp = "$MODULE_PATH\$MODULE_NAME\oh-package_temp.json5"
Copy-Item $package_path $package_path_back
# 遍历JSON对象中的每个键值对
$oh_package = Get-Content $package_path | ConvertFrom-Json -AsHashTable
$hashTable = $oh_package["dependencies"]
$hashTable.GetEnumerator() | ForEach-Object {
$key = $_.Name
$value = $_.Value
$lowerKey = $key.ToLower() | Select-String '.har'
Write-Host -ForegroundColor Yellow "key:$key value: $value lowerKey: $lowerKey "
Write-Host -ForegroundColor Yellow "是文件依赖:$($value.Contains("file")) 不是har包依赖::$(!$lowerKey)"
if ( $value.Contains("file") -and !$lowerKey) {
$last_name = $($value -split "/" )[-1]
if ( ! (Test-Path $MODULE_LIB )) {
mkdir -p "$MODULE_LIB"
}
Remove-Item $MODULE_LIB/$last_name*.*
Write-Host -ForegroundColor Yellow "开始替换源码依赖为文件依赖 TGZ_LIST: $TGZ_LIST key: $key value: $value last_name: $last_name "
Write-Host -ForegroundColor Yellow "开始替换源码依赖为文件依赖 需要是tgz文件: $($TGZ_LIST -eq $key) 依赖的har已存在: $(Test-Path $LIB_DIR/$last_name".har")"
$har_denpendence = "file:./libs/$last_name.har"
$tgz_denpendence = "file:./libs/$last_name-default.tgz"
if ( $TGZ_LIST -eq $key ) {
if ( Test-Path $LIB_DIR/$last_name".har" ) {
Write-Host -ForegroundColor Red "$last_name 替换为本地tgz依赖1"
Copy-Item $LIB_DIR/$last_name"-default.tgz" $MODULE_LIB
$oh_package = Get-Content $package_path | ConvertFrom-Json -AsHashTable
$hashTable = $oh_package["dependencies"]
$hashTable[$key] = $tgz_denpendence
$oh_package["dependencies"] = $hashTable
$oh_package | ConvertTo-Json -Depth 10 | Out-File -FilePath $package_path
# Move-Item $package_path_temp $package_path -Force
}
else {
Write-Host -ForegroundColor Red "$last_name 不存在,跳过替换1"
}
}
else {
if ( Test-Path $LIB_DIR/$last_name".har" ) {
Write-Host -ForegroundColor Red "$last_name 替换为本地依赖2"
Copy-Item $LIB_DIR/$last_name".har" $MODULE_LIB
$oh_package = Get-Content $package_path | ConvertFrom-Json -AsHashTable
$hashTable = $oh_package["dependencies"]
$hashTable[$key] = $har_denpendence
$oh_package["dependencies"] = $hashTable
$oh_package | ConvertTo-Json -Depth 10 | Out-File -FilePath $package_path
# Move-Item $package_path_temp $package_path -Force
}
else {
Write-Host -ForegroundColor Red "$last_name 不存在,跳过替换2"
}
}
}
}
buildModule $MODULE_PATH $MODULE_NAME $ASSEMBLE_TYPE
Move-Item $package_path_back $package_path -Force
}
4.3 脚本编译计时
在编译脚本中添加计时,可以有效监控打包时长,用来做打包计划或者优化对应组件编译时长做参考,...为需要计时的代码块,前后代码示例如下:
# 创建一个 Stopwatch 对象
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
# 需要计时的代码块
...
...
...
# 停止计时
$stopwatch.Stop()
# 使用自定义格式输出 TimeSpan 对象
$formattedTime = $stopwatch.Elapsed.ToString("hh\:mm\:ss\.fff")
# 输出执行时间
Write-Host -ForegroundColor Red "Script block took $formattedTime to complete."
5. 多组件项目结构
5.1 多组件示例
多组件示例详情参考:Harmony NEXT:简易多层级组件架构
5.2 多组件打包顺序
打包SDK时,依赖关系需要脚本主动处理,从源码依赖修改为SDK依赖,脚本打包思路是:先打包最底层的组件,然后打包上一层组件,然后在打包上上层组件,类似叶子节点到根节点的思路。
上图为本次脚本打包项目依赖关系图,以该项目为例,脚本打包优先顺序为:
- utils
- commonA、commonB
- featureA、featureB、featureC
- featureTopA、featureTopB
同一优先级内的组件不分先后
以依赖关系较复杂的featureTopB为例,脚本打包顺序依次为:
- utils
- commonA、commonB
- featureA、featureC
- featureTopB
5.3 连续编译组件,输出最终产物包
此时又回到一开始的主体思路:删缓存==>编译组件,现在需要编译项目所需的所有组件
添加编译时间计时,编译所需所有组件,代码实现如下:
function startMakeHar {
# 创建一个 Stopwatch 对象
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
# 遍历目录并删除满足条件的目录及其子文件
Get-ChildItem -Path "$ROOT_DIR" -Depth 3 -Attributes Directory | ForEach-Object {
# 这里可以添加你的条件判断,例如基于目录名称或其他属性
# 如果条件满足,则删除目录及其子文件
$dir0 = $_.Name
$fullName = $_.FullName
if (( $dir0 -eq "oh_modules" ) -or ( $dir0 -eq "build" )) {
Write-Host -ForegroundColor Yellow "目录存在... $dir0 删除目录... $fullName"
Remove-Item "$fullName" -Recurse -Force
}
}
Get-ChildItem -Path $ROOT_DIR -Recurse -Filter oh-package-lock.json5 | ForEach-Object {
$lockFileName = $_.FullName
Write-Output "文件存在,删除文件:$lockFileName"
Remove-Item "$lockFileName" -Force
}
# 检查目录是否存在
if (Test-Path "$LIB_DIR" ) {
Write-Host -ForegroundColor Yellow "目录存在,删除目录..."
Remove-Item "$LIB_DIR" -Recurse -Force
}
mkdir -p "$LIB_DIR"
$commonsDir = "$ROOT_DIR\commons"
$featuresDir = "$ROOT_DIR\features"
# handleConfig
Write-Host -ForegroundColor Red "编译模块:utils"
buildModule "${commonsDir}" utils hsp
Write-Host -ForegroundColor Red "编译模块:commonA"
buildModuleWithDependence "${commonsDir}" commonA har
Write-Host -ForegroundColor Red "编译模块:commonB"
buildModuleWithDependence "${commonsDir}" commonB har
Write-Host -ForegroundColor Red "编译模块:featureA"
buildModuleWithDependence "${featuresDir}" featureA har
# Write-Host -ForegroundColor Red "编译模块:featureB"
# buildModuleWithDependence "${featuresDir}" featureB har
Write-Host -ForegroundColor Red "编译模块:featureC"
buildModuleWithDependence "${featuresDir}" featureC har
Write-Host -ForegroundColor Red "编译模块:featureTopB"
buildModuleWithDependence "${featuresDir}" featureTopB har "utils"
Write-Host -ForegroundColor Red "打包完成"
# 停止计时
$stopwatch.Stop()
# 使用自定义格式输出 TimeSpan 对象
$formattedTime = $stopwatch.Elapsed.ToString("hh\:mm\:ss\.fff")
# 输出执行时间
Write-Host -ForegroundColor Red "Script block took $formattedTime to complete."
}
6 脚本运行与产物调试
6.1 脚本运行
6.1.1 脚本内需要运行的方法只有:startMakeHar,因此将startMakeHar单独放到脚本末尾即可
6.1.2 脚本内默认项目位置ROOT_DIR是脚本当前运行所在位置,该脚本目前是放在项目根目录
6.1.3 在当前目录打开pwsh窗口,直接运行./build_featureTopB.ps1脚本文件,即可看到脚本开始初始化并删除各种缓存的日志:
6.1.4 脚本正常运行结束,会打印总体编译时间和产物存放位置:
6.2 产物调试
配合简易多层级组件架构,可以在SDK项目的demo组件中做发布前的调试:
四、 其他
- 当前脚本没有异常中断机制,编译中途出现异常,有可能最终包能打出来,但是打出来的包集成后不可用。因此当前需要主动验证SDK可用性,脚本打包时建议观察pwsh窗口报错信息
- 新增模块需要编译时,需要手动在
startMakeHar方法中增加打包模块,位置需要在它的依赖组件打包完成后,需要它的组件打包之前 - 后续更新mac脚本