最近这两天生完Xcode发现云可以用了,但是碍于把公司代码直接放到公网不太合适。但是也是一个机会偷偷自动化一下打包逻辑,毕竟人为操作的话,有几个时间节点需要等,年纪也大了,一打断就忘记接着干嘛了。还是能机器一气呵成的就机器来吧。
之前的打包逻辑主要有以下几个步骤。
- 修改版本号、build号、项目名称等信息;
- 使用xcode执行build archive;
- 在organizer中导出包,手动上传到 fir.im;
- 在organizer中导出包,手动上传到testflight;
按照这个思路逐步把逻辑脚本化,脚本的话当然还是用python比较好,人生苦短。
文件结构
主要有以下几个文件及文件结构:
- cicd.py # 主要执行脚本
- cicd # 放打包产物及配置等信息文件夹
|- changelog.txt # 版本修改信息,可以在fir.im中查看
|- ExportOptions.plist # 测试版本自动打包配置
|- ReleaseExportOptions.plist # 正式版本自动打包配置
|- build # 编译产物
|- output # ipa包
修改版本号、build号、项目名称等信息
使用到命令行工具 agvtool
详细原理及配置参考链接https://blog.csdn.net/xo19882011/article/details/121204208
配置好相关项目之后转到命令上来:
agvtool new-marketing-version version # 相应的发布版本
agvtool new-version -all buildnum # 相应的buildnum
这样打版本的准备就做好了。
打版本
使用到命令行工具 xcodebuild,这个命令的详细介绍还是看帮助文档比较好,然后再结合不太理解的参数搜搜,也参考了一下别人的博客。
xcodebuild archive \
-workspace xxx.xcworkspace \ # 项目里边一般都有cocospod,所以一般都是 xcworkspace
-scheme xxx \ # debug 跟 release 不同,使用的时候采用不同的scheme
-destination generic/platform=iOS \
-configuration xxx \ # build setting中配置的configuration,一般来说可能是 Debug / Release
-archivePath cicd/build/xxx.xcarchive \
-allowProvisioningUpdates \
-allowProvisioningDeviceRegistration
最后可能会报错,但是只要是产物在一般来说没啥问题。
导出ipa
使用到命令行工具 xcodebuild
xcodebuild -exportArchive \
-archivePath cicd/build/debug/xxx.xcarchive \
-exportOptionsPlist cicd/ExportOptions.plist \
-exportPath cicd/output/debug/
其中 ExportOptions.plist文件内容如下,也是对应着xcode导出包的时候每个步骤对应的配置,当前用到的是自动签名。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>compileBitcode</key>
<false/>
<key>method</key>
<string>development</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>xxxx</string>
<key>thinning</key>
<string><none></string>
</dict>
</plist>
上传ipa到fir.im
结合 fir.im 的文档 https://www.betaqr.com/docs,使用到发布应用那项。代码如下:
print(f'\033[36m--- 上传 ---\033[0m')
res = requests.post('http://api.bq04.com/apps', json={
'type': 'ios',
'bundle_id': 'com.xxxx', # bunldid
'api_token': 'xxxx' # 相应的 fir.im token
})
print('token is ', res.text)
res = json.loads(res.text)
binary_key = res['cert']['binary']['key']
binary_token = res['cert']['binary']['token']
binary_upload_url = res['cert']['binary']['upload_url']
icon_key = res['cert']['icon']['key']
icon_token = res['cert']['icon']['token']
icon_upload_url = res['cert']['icon']['upload_url']
res = requests.post(icon_upload_url, data={
'key': icon_key,
'token': icon_token,
}, files={'file': open('图标地址', 'rb')})
print('upload icon res ', res.content)
res = requests.post(binary_upload_url, data={
'key': binary_key,
'token': binary_token,
'x:name': 'xxxx',
'x:version': args.version,
'x:build': buildnum,
'x:changelog': open('cicd/changelog.txt', 'r').read(),
}, files={'file': open('cicd/output/debug/xxxx.ipa', 'rb')})
print('upload binary res ', res.content)
changelog记录一下版本更新内容,方便测试人员确认信息。
上传 Test Flight
使用到命令行工具 xcrun altool
xcrun altool \
--upload-app \
-f cicd/output/release/xxx.ipa \
-t ios \
--apiKey xxx \
--apiIssuer xxx \
--verbose
其中apiKey及apiIssuer在在 app store connect 的用户和访问 -> 密钥中新建及查看。
新建一个密钥,记录密钥 ID,并下载相应的私钥。把密钥放在以下目录其中之一中
'./private_keys'
'~/private_keys'
'~/.private_keys'
'~/.appstoreconnect/private_keys'
apiKey => 密钥ID
apiIssuer => Issuer ID
之后没啥问题就能直接上传了。
完整脚本
# coding = utf8
import os
import sys
import argparse
import requests
import json
import datetime
WORKSPACE = 'xxxx.xcworkspace'
BUNDLEID = 'com.xxxx'
FIRIMTOKEN = 'xxxx'
ICONPATH = 'Images.xcassets/AppIcon.appiconset/iphone1024.png'
DISPLAYNAME = 'xxxx'
TARGETNAME = 'xxxx'
API_KEY = 'xxxx'
API_ISSUER = 'xxxx'
DEBUG_SCHEME = 'xxxx'
DEBUG_CONFIGURE = 'Debug'
DEBUG_ARCHIVEPATH = 'cicd/build/debug/xxxx.xcarchive'
DEBUG_exportPath = 'cicd/output/debug/'
DEBUG_exportOptionsPlist = 'cicd/ExportOptions.plist'
DEBUG_archivePath = 'cicd/build/debug/xxxx.xcarchive'
RELEASE_SCHEME = 'xxxxDistribute'
RELEASE_CONFIGURE = 'xxxxDistribute'
RELEASE_ARCHIVEPATH = 'cicd/build/release/xxxxRelease.xcarchive'
RELEASE_exportPath = 'cicd/output/release/'
RELEASE_exportOptionsPlist = 'cicd/ReleaseExportOptions.plist'
RELEASE_archivePath = 'cicd/build/release/xxxxRelease.xcarchive'
def clean():
print(f'\033[36m--- 清理文档 ---\033[0m')
os.system('rm -rf cicd/build/*')
os.system('rm -rf cicd/output/*')
def configuration(version, buildnum):
print(f'\033[36m--- 设置版本号 ---\033[0m')
os.system('agvtool new-marketing-version {}'.format(version))
os.system('agvtool new-version -all {}'.format(buildnum))
def build(mode):
print(f'\033[36m--- 打版本 ---\033[0m')
scheme = ''
configure = ''
archivePath = ''
if mode == 'debug':
scheme = DEBUG_SCHEME
configure = DEBUG_CONFIGURE
archivePath = DEBUG_ARCHIVEPATH
else:
scheme = RELEASE_SCHEME
configure = RELEASE_CONFIGURE
archivePath = RELEASE_ARCHIVEPATH
os.system('''
xcodebuild archive \
-workspace {} \
-scheme {} \
-destination generic/platform=iOS \
-configuration {} \
-archivePath {} \
-allowProvisioningUpdates \
-allowProvisioningDeviceRegistration
'''.format(WORKSPACE, scheme, configure, archivePath))
def export(mode):
print(f'\033[36m--- 打ipa版本 ---\033[0m')
exportPath = ''
exportOptionsPlist = ''
archivePath = ''
if mode == 'debug':
exportPath = DEBUG_exportPath
exportOptionsPlist = DEBUG_exportOptionsPlist
archivePath = DEBUG_archivePath
else:
exportPath = RELEASE_exportPath
exportOptionsPlist = RELEASE_exportOptionsPlist
archivePath = RELEASE_archivePath
os.system('''
xcodebuild -exportArchive \
-archivePath {} \
-exportOptionsPlist {} \
-exportPath {}
'''.format(archivePath, exportOptionsPlist, exportPath))
def uploadFim(buildnum):
print(f'\033[36m--- 上传 ---\033[0m')
res = requests.post('http://api.bq04.com/apps', json={
'type': 'ios',
'bundle_id': BUNDLEID,
'api_token': FIRIMTOKEN
})
print('token is ', res.text)
res = json.loads(res.text)
binary_key = res['cert']['binary']['key']
binary_token = res['cert']['binary']['token']
binary_upload_url = res['cert']['binary']['upload_url']
icon_key = res['cert']['icon']['key']
icon_token = res['cert']['icon']['token']
icon_upload_url = res['cert']['icon']['upload_url']
res = requests.post(icon_upload_url, data={
'key': icon_key,
'token': icon_token,
}, files={'file': open(ICONPATH, 'rb')})
print('upload icon res ', res.content)
res = requests.post(binary_upload_url, data={
'key': binary_key,
'token': binary_token,
'x:name': DISPLAYNAME,
'x:version': args.version,
'x:build': buildnum,
'x:changelog': open('cicd/changelog.txt', 'r').read(),
}, files={'file': open('cicd/output/debug/{}.ipa'.format(TARGETNAME), 'rb')})
print('upload binary res ', res.content)
def uploadTestFlight():
print(f'\033[36m--- 上传 Test Flight ---\033[0m')
os.system('''
xcrun altool \
--upload-app \
-f cicd/output/release/{}.ipa \
-t ios \
--apiKey {} \
--apiIssuer {} \
--verbose
'''.format(TARGETNAME, API_KEY, API_ISSUER))
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='cicd parser')
parser.add_argument('-v', type=str, dest='version', metavar='版本号', default='', required=True)
parser.add_argument('-m', type=str, dest='mode', metavar='模式', default='debug')
args = parser.parse_args()
if args.version == '':
exit(0)
buildnum = datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d%H%M')
print('build num ', buildnum)
# 清理
clean()
# 配置
configuration(args.version, buildnum)
# build
build(args.mode)
# export
export(args.mode)
if args.mode == 'debug':
# 上传
uploadFim(buildnum)
else:
# 上传testflight
uploadTestFlight()