一、背景
七鱼SDK在今年经过出海业务、视频客服业务的迭代后,发布流程已经变的繁琐,这些流程不仅费时费力,还特别容易出现人为的错误。于是,我从优化打包脚本出发,实现一键自动发布版本。
二、发版流程
七鱼现有的发版流程如下图:
发版流程.png
每次发版,都需要发上图中的国内版、国际版、cocpoads版。由于这三个版本所对应的资源和代码分支是有一定差别的,所以每次发完一种版本后,都需要手动切换对应的资源和配置,切换完之后,分别打蒲公英包和SDK的包。整个流程不仅繁琐耗时,而且非常容易出错。如果这个流程某个步骤出错了,很难人为的发现,更多是依赖客户或者技术支持的同事在真正使用场景时才发现包有问题。
三、打包脚本--Fastlane
基于上述打包流程的问题,如果能够通过脚本自动去执行,那么不仅可以简化发版流程,降低错误率,而且大量节省人力。
于是,我们打算使用Fastlane来优化流程。
1、Fastlane简介
Fastlane 是一整套的客户端 CI 工具集合,替代开发者处理构建和发布 App 中繁琐的任务,可以非常快速简单的搭建一个自动化发布服务,并且支持Android,iOS,MacOS。Fastlane本身没有一套特殊语法,使用的 Ruby 语言。Fastlane的安装以及环境的搭建,网上有很多的资料可以翻阅,本文将不赘述,本文将着重介绍如何实现上述复杂流程的一键发布。
2、Fastlane的使用
搭建好Fastlane的环境以后,在工程目录下执行 fastlane init,打开Fastlane文件,就可以开始编写脚本了。
platform :ios do
desc "Description of what the lane does"
lane :custom_lane do
# add actions here: https://docs.fastlane.tools/actions
end
end
这是每一个fastlane脚本的最简单形式。最外面的"ios"层是脚本入口,类似于main函数。在脚本中可以自定义action,其中fastlane是关键字,相当于函数的function,当然,也可以写成有参的形式:
lane :custom_lane do |options|
end
fastlane提供了一些内置的action能力,可以通过查阅官方文档来获取。
fastlane中的action既可以被内部调用,也可以被外部命令行调用。
例如内部调用:
platform :ios do
desc "Description of what the lane does"
lane :custom_lane do
# add actions here: https://docs.fastlane.tools/actions
end
lane :custom_lane_options do |options|
custom_lane
end
end
例如命令行调用:
bundle exec fastlane ios custom_lane
内部调用时,就和调用函数用法相同,命令行调用时,由于fastlane是依赖于bundle环境的,需要用bundle来驱动,同时需要申明执行的fastlane脚本名称“ios”。
四、一键发版
从上述发版流程我们可以知道,一次发版,总共需要产出以下几项:
1、国内版:蒲公英版本A、zip压缩包A
2、国际版:蒲公英版本B、zip压缩包B
3、cocoapods版
这三项的内容有一部分是重叠的,有一部分是不同的。
在写脚本之前,我们先对各版本内部组成有一个简单的了解:
蒲公英版本A、zip压缩包A:在线模块+视频模块+国内版NIM+国内版配置文件
蒲公英版本B、zip压缩包B:在线模块+国际版NIM+国际版配置文件
cocoapods版:在线模块+视频模块+国内版NIM(或国际版NIM)+国际版配置文件
其中,蒲公英版本是ipa包传到蒲公英平台;zip压缩包是各模块SDK组合而成。
1、各分支模块拆解
基于上述内容我们可以知道,“在线模块”是公共模块,剩余的模块是选配模块,并且各模块是需要单独打包的.其中,在线和视频模块需要使用xcframework形式的包,而NIM需要使用framework的包(保持与云信同步)。所以,我们可以先打在线模块、视频模块的包:
lane :sdk do |options|
#清理文件夹
clear_derived_data(derived_data_path: "./DerivedData")
clear_derived_data(derived_data_path: "./QYProduct")
#打SDK包
customSchemes = ["QYSDK", "QYVideoService"]
for customScheme in customSchemes
xcbuild(
scheme: customScheme,
configuration: "Release",
destination: "generic/platform=iOS Simulator",
sdk: "iphonesimulator",
xcargs: "-quiet ARCHS='x86_64' BITCODE_GENERATION_MODE=marker ",
derivedDataPath: "./DerivedData"
)
xcbuild(
scheme: customScheme,
destination: "generic/platform=iOS",
configuration: "Release",
sdk: "iphoneos",
xcargs: "-quiet ARCHS='arm64' BITCODE_GENERATION_MODE=bitcode ",
derivedDataPath: "./DerivedData"
)
create_xcframework(
frameworks_with_dsyms: {
"DerivedData/Build/Products/Release-iphonesimulator/#{customScheme}.framework" => { dsyms: Dir.pwd + "/../DerivedData/Build/Products/Release-iphonesimulator/#{customScheme}.framework.dSYM" },
"DerivedData/Build/Products/Release-iphoneos/#{customScheme}.framework" => { dsyms: Dir.pwd + "/../DerivedData/Build/Products/Release-iphoneos/#{customScheme}.framework.dSYM" } },
output: "QYProduct/QY_iOS_SDK/SDK/#{customScheme}.xcframework")
end
end
解析: fastlane中支持使用Xcode的工具来打包,官方已经提供xcbuild了,各入参可以根据项目需要自行配置,分别打真机和模拟器的包即可。打完包后,需要将两种包合并成xcframework,使用create_xcframework即可。
接下来我们需要打NIM的SDK包。由于国内版和国际版使用了不同的NIM分支,因此,我们在打包之前需要先切换分支,然后再打包。然而,我们的NIM模块是以submodule的方式存在的,而fastlane本身能使用submodule的功能很有限,于是我们想到,是否可以在fastlane内部去执行git指令,这样就可以间接的完成分支切换了,于是就有如下的代码:
cmd = "git submodule foreach git checkout . && git submodule foreach git checkout 9.2.8"
`#{cmd}`
fastlane支持在控制台间接执行git指令,如果有需要连续执行多条指令,指令直接可以使用“&&”来连接。上述指令则实现了清空子模块的变更,同时将分支切换到9.2.8。仔细观察,我们会发现,指令中有一个foreach,这是循环遍历执行子模块的指令。那么问题就出现了,我的工程是有多个子模块的,而我仅仅需要切换一个模块,恰好我需要执行的子模块处于第一个,当foreach执行出错后,会停止继续执行,这样也算勉强能够实现功能。除了foreach,是否有别的命令可以实现切换单个子模块分支呢?经过一番寻找和思考后,最终还是失败了,但也有想到两个替代方案。
方案一: git是支持命令扩展的,我们可以通过扩展的方式去自定义一个切换指定子模块分支的命令,然后再通过fastlane去执行这个命令。
方案二: 用shell脚本或note.js脚本来实现切换分支,然后通过fastlane去执行脚本。
这两种方案还待后续研究,此处先粗糙使用foreach过度一下。
我们但NIM包是需要根据不同分支打不同包的,因此我们就需要使用到带参数的fastlane action,参考如下:
lane :nim_package do |options|
case options[:type]
when 'abroad'
#国际版
else
#国内版
end
我们可以根据入参来选择需要切换的分支,这样,我们NIM打包action的完整内容如下:
lane :nim_package do |options|
code_path = File.expand_path("..", File.dirname(__FILE__)).to_s
case options[:type]
when 'abroad'
cmd = "git submodule foreach git checkout . && git submodule foreach git checkout 9.2.8"
clear_derived_data(derived_data_path: "./QYProduct/abroad")
out_path = "#{code_path}/QYProduct/abroad/QY_iOS_SDK/NIMSDK"
else
cmd = "git submodule foreach git checkout . && git submodule foreach git checkout feature_8.9.2_noopenssl_compress"
clear_derived_data(derived_data_path: "./QYProduct/enterprise")
out_path = "#{code_path}/QYProduct/enterprise/QY_iOS_SDK/NIMSDK"
end
`#{cmd}`
xcbuild(
scheme: "NIMSDK",
configuration: "Release",
destination: "generic/platform=iOS Simulator",
sdk: "iphonesimulator",
xcargs: "-quiet ARCHS='x86_64' BITCODE_GENERATION_MODE=marker ",
derivedDataPath: "./DerivedData"
)
xcbuild(
scheme: "NIMSDK",
destination: "generic/platform=iOS",
configuration: "Release",
sdk: "iphoneos",
xcargs: "-quiet ARCHS='arm64' BITCODE_GENERATION_MODE=bitcode ",
derivedDataPath: "./DerivedData"
)
iphonesimulator_path = "#{code_path}/DerivedData/Build/Products/Release-iphonesimulator/NIMSDK.framework"
iphoneos_path = "#{code_path}/DerivedData/Build/Products/Release-iphoneos/NIMSDK.framework"
command = "mkdir -p #{out_path} && cp -rf #{iphoneos_path} #{out_path}/NIMSDK.framework && lipo -create #{iphoneos_path}/NIMSDK #{iphonesimulator_path}/NIMSDK -output #{out_path}/NIMSDK.framework/NIMSDK"
`#{command}`
end
打完NIM的包后,我们得到了两个文件夹,分别存放了国内版和国际版的SDK,这样,我们后续发版的时候也不再需要手动调整文件夹的内容。
到目前为止,SDK的包都已经打好了,接下来需要打ipa包,并上传到蒲公英。
由于蒲公英的包也需要打国内和国外的包,同时需要分配不同的配置文件,因此,我们仍然需要用到带参数的action,并输出到指定的路径。
lane :qy_package do |options|
case options[:type]
when 'abroad'
copy_artifacts(
target_path: 'QYDemo/Resources',
artifacts: ['Abroad/QYConfigResource.bundle']
)
abroad
copy_artifacts(
target_path: 'QYProduct/abroad/QY_iOS_SDK/SDK',
artifacts: ['QYProduct/QY_iOS_SDK/SDK/QYSDK.xcframework']
)
else
copy_artifacts(
target_path: 'QYDemo/Resources',
artifacts: ['Default/QYConfigResource.bundle']
)
enterprise
copy_artifacts(
target_path: 'QYProduct/enterprise/QY_iOS_SDK/SDK',
artifacts: ['QYProduct/QY_iOS_SDK/SDK/QYSDK.xcframework','QYProduct/QY_iOS_SDK/SDK/QYVideoService.xcframework']
)
end
end
这里涉及到打包上传到蒲公英的环节,这里面涉及到证书的管理,可以参考另一篇文章fastlane通过match管理证书。我们先看一下打包并上传蒲公英的action:
platform :ios do
desc "企业包"
lane :enterprise do |options|
sync_code_signing(
type: "enterprise",
app_identifier: ["bundleId"],
readonly: true,
username: "用户名",
keychain_password: options[:keychain_password])
build_app(
scheme: "QYDemo",
include_bitcode: false,
destination: "generic/platform=iOS",
)
pgyer(api_key: "api_key",
password: "蒲公英包下载密码",
install_type: "2",
update_description: "update by fastlane")
end
涉及的action内容,在fastlane通过match管理证书有详细讲解,此文不重复赘述。
2、完整打包及优化风险项
至此,我们已经将所有需要打的包都打好了,接下来需要对一下额外的资源文件进一步分配处理,同时将调用上述所有的action:
lane :sdk_package do |options|
clear_derived_data(derived_data_path: "./DerivedData")
clear_derived_data(derived_data_path: "./QYProduct")
#打SDK包
sdk
#国内
nim_package(type:'enterprise')
qy_package(type:'enterprise')
#国外
nim_package(type:'abroad')
qy_package(type:'abroad')
version = get_version_number(
xcodeproj: "QYDemo.xcodeproj",
target: "QYDemo"
)
version_bump_podspec(
path: "QY_iOS_SDK.podspec",
version_number: "#{version}"
)
copy_artifacts(
target_path: 'QYProduct/QY_iOS_SDK',
artifacts: ['QY_iOS_SDK.podspec']
)
targets = ["abroad/QY_iOS_SDK","enterprise/QY_iOS_SDK","QY_iOS_SDK/SDK"]
for target in targets
copy_artifacts(
target_path: "QYProduct/#{target}",
artifacts: ['开发指南.md','Abroad/资源文件说明.md']
)
copy_artifacts(
target_path: "QYProduct/#{target}/Resources",
artifacts: ['QYDemo/Resources/QYResource.bundle', 'QYDemo/Resources/QYCustomResource.bundle', 'QYDemo/Resources/QYLanguage.bundle']
)
if target != "enterprise/QY_iOS_SDK"
copy_artifacts(
target_path: "QYProduct/#{target}/Resources",
artifacts: ['Abroad/QYConfigResource.bundle']
)
end
if target != "abroad/QY_iOS_SDK"
copy_artifacts(
target_path: "QYProduct/#{target}/Resources",
artifacts: ['QYDemo/Resources/QYVideoResource.bundle']
)
end
if target == "abroad/QY_iOS_SDK"
zip(
path: "QYProduct/#{target}",
output_path: "QYProduct/QY_iOS_SDK_v#{version}_Abroad.zip"
)
end
if target == "enterprise/QY_iOS_SDK"
zip(
path: "QYProduct/#{target}",
output_path: "QYProduct/QY_iOS_SDK_v#{version}.zip"
)
end
end
#打包完,恢复对应分支
cmd = "git submodule foreach git checkout . && git submodule foreach git checkout feature_8.9.2_noopenssl_compress"
`#{cmd}`
copy_artifacts(
target_path: 'QYDemo/Resources',
artifacts: ['Default/QYConfigResource.bundle']
)
sentry_upload_dif(
path: Dir.pwd + '/../QYProduct/'
)
end
这是一个主action,通过嵌套调用子action实现打包,在打完包后,将不同的资源和文件分配到每个包的路径中,同时,读取好版本号后写入podspec文件。
至此,我们完整的打包脚本已经完成,我们重新梳理一下这个脚本它做了哪些事情:
1、打在线模块、视频模块的xcframework
2、切换分支,并打不同分支的NIM的framework
3、根据不同的分支,打不同的ipa包,并使用match来自动管理证书,同时将包上传蒲公英
4、更新podspec、readme、资源包等内容,并分发到各版本对应文件夹路径
5、打zip压缩包,将分支恢复初始状态。
经过上述5个环节后,我们最终会得到如下的产物:
image.png image.png
接下来,我们仅需将对应的内容,发布到官网、cocoapods即可。
继续优化方向:
1、优化foreach遍历所有子模块的方式,实现精准切换子模块
风险考虑:
由于发版时需要将打包产物上传运维官网后台和cocoapods仓库,因此,可以让服务端提供自动上传的接口,通过脚本自动传包。
方向2实现后,整个发版过程仅需要一行命令即可完成,但由于每次打完包都是自动上传发布,没办法人为检验正确性,如果打包出了问题,或者打错包了,也会直接发出去,而cocoapods的版本管理是需要升版本号的,一旦出错,旧的版本号就废弃不能用了,存在一定的风险。
网友评论