详解Shell脚本实现iOS自动化编译打包提交
详解shell脚本实现ios自动化编译打包提交。
现在涉及到编译打包的工作主要是以下两个:
提交测试版本给测试同事
提交app store审核
两个流程分别是:
修改证书和配置文件,然后「product -> archive」编译打包,之后在自动弹出的 「organizer」 中进行选择,根据需要导出 ad hoc enterprise 类型的 ipa 包。等待导出之后再提交到fir上,等fir提交完成就需要告知测试同事。整个流程下来一般都要半个多小时,而且需要人工监守操作。
第二个也是差不多,打包完之后需要操作几个步骤然后上传到app store,上传时间较长,而且中间可能会有错误需要处理。上传后等待苹果处理二进制包,苹果处理后上去选择构建包,点击提交审核。
所以研究下自动化编译打包,提高下效率,减少人工操作成本。
主要有两种实现途径,applescript和shell脚本,applescript没怎么研究,网上说是很强大的脚本语言。
下面主要讲shell脚本的实现,网上也有人实现了并托管在github上,可以参考下。
https://github.com/webfrogs/xcode_shell
shell脚本涉及的工具
主要是以下几个工具:
xcodebuild
xcrun
altool(提交到app store使用)
fir-cli(上传到fir时使用)
python的smtplib(之前已经写过python的发邮件了,所以就直接用没有用shell写。)
plistbuddy
buglysymbolios(bugly的符号表工具包)
xcodebuild和xcrun
xcodebuild和xcrun都是来自command line tools,xcode自带,如果没有可以通过以下命令安装:
xcode-select --install
或者在下面的链接下载安装:
https://developer.apple.com/downloads/
安装完可在以下路径看到这两个工具:
/applications/xcode.app/contents/developer/usr/bin/
xcodebuild
主要是用来编译,打包成archive和导出ipa包。
https://developer.apple.com/library/mac/documentation/darwin/reference/manpages/man1/xcodebuild.1.html
可以执行?xcodebuild -help?查看,主要展示了几种用法、一些可选项,最后是比较重要的exportoptionsplist文件的一些可选key,这个文件在后面导出ipa包会用到。
主要下面三个查看的命令比较重要:
-showsdksdisplay a compact list of the installed sdks
-showbuildsettingsdisplay a list of build settings and values
-list lists the targets and configurations in a project, or the schemes in a workspace
后面两个需要在xcode的project或者workspace目录下才能用。
xcrun
xcrun -h
主要是打包,看网上比较多是用这个工具打包各种渠道包。
altool
这个工具在网上搜索几乎没有什么结果,大概国内直接用命令行工具提交app store的比较少。后来在*上才找到相关的文档:
https://itunesconnect.apple.com/docs/usingapplicationloader.pdf
在上面的文档第38页讲述了如何使用altool上传二进制文件。
这个工具实际上是applicationloader,打开xcode-左上角xcode-open developer tool-application loader 可看到。有个“交付您的应用”操作,网上看到有人是直接用这个工具上传的。
altool的路径是:
/applications/xcode.app/contents/applications/application\ loader.app/contents/frameworks/itunessoftwareservice.framework/support/altool
使用时会提示下面的错误:
altool[] *** error: exception while launching itunestransporter:
transporter not found at path: /usr/local/itms/bin/itmstransporter.
you should reinstall the application.
建立个软链接可解决(类似于windows的快捷方式):
ln -s /applications/xcode.app/contents/applications/application\ loader.app/contents/itms /usr/local/itms
fir-cli
安装时会提示各种权限不允许,可以执行下面命令:
echo 'gem: --bindir /usr/local/bin' >> ~/.gemrc
sudo 'gem install fir-cli
fir有提供android studio、eclipse、gradle插件,可以看下。
https://fir.im/tools
这是?它的github地址,其中讲到有对?xcodebuild?原生指令进行了封装。
https://github.com/firhq/fir-cli/blob/master/readme.md
plistbuddy
plist在mac osx中起着举足轻重的作用,系统和程序使用plist文件来存储自己的安装/配置/属性等信息。而plistbuddy是mac里一个用于命令行下读写plist文件的工具,在/usr/libexec/下。可以通过它读取或修改plist文件的内容。
这里我仅通过它来获取内部版本号、外部版本号。在一些文章中见过用来修改plist文件的信息来导出出不同需要的包。
一些概念的区别
workspace、project、scheme、target的区别。
下面是官方文档:
https://developer.apple.com/library/ios/featuredarticles/xcodeconcepts/concept-targets.html#//apple_ref/doc/uid/tp40009328-ch4-sw1
下面从上往下大概说下,具体看文档比较好:
workspace
workspace是最大的集合,可以包含多个project,可以管理不同的project之间的关系。workspace是以xcworkspace的文件形式存在的。(这点和project一致)。workspace的存在是为了解决原来仅有project的时候不同的project之间的引用和调用困难的问题。同时,一个workspace的project共用一个编译路径。比如使用cocoapod、或者使用其他开发库/框架。
project
project是一个仓库,包含编译一个或多个product所需的文件、资源和信息,保持和聚合这些元素间的关系。(每个target能指定自己的build settings来覆盖project的)
source code, including header files and implementation files
libraries and frameworks, internal and external
resource files
image files
interface builder (nib) files
scheme
scheme包含了一些要构建的scheme,一些构建时用到的设置,一些要运行的测试。同时只能有一个scheme是有效的。
target
target是对应了具体一个想要构建的product,包含了一些构建这个product所需的配置和文件(build settings和build phases)。一个project可以包含多个target。
具体实现
看起来有两种实现方法:
网上可以查到的文章,大多数都是用xcodebuild和xcrun实现的,比如:
xcodebuild -workspace xxx -scheme xxx -configuration release
xcrun -sdk iphoneos packageapplication -v "/xxx/xxx.app" -o "/xxx/xxx"
这些文章都是相对比早期的,大多数用于打包不同渠道包。
另一种是xcodebuild的archive和-exportarchive,只有一两篇文章是用这个,而且也过时了,因为现在最新是需要用-exportoptionsplist这个选项。
我用的是第二种,并用上-exportoptionsplist选项,后面我会简单给下这两种的结果比较。脚本流程是:
准备两个plist文件,用于导出不同ipa包时使用。
获取命令行参数,区分上传到fir还是app store
清理构建目录
编译打包
导出包
上传到fir或者验证并上传到app store
发邮件通知
准备plist文件
根据xcodebuild -help提供的可选key可以知道,compilebitcode、embedondemandresourcesassetpacksinbundle、icloudcontainerenvironment、manifest、ondemandresourcesassetpacksbaseurl、thinning这几个key用于非app store导出的;uploadbitcode、uploadsymbols用于app store导出;method、teamid共用。
method的可选值为:
app-store, package, ad-hoc, enterprise, development, and developer-id
所以我建了两个文件:appstoreexportoptions.plist、adhocexportoptions.plist。
appstoreexportoptions.plist:method=app-store,uploadbitcode=yes,uploadsymbols=yes
adhocexportoptions.plist:method=ad-hoc,compilebitcode=no
获取命令行参数
用shell内置的getopts命令,这属于shell的范畴就不多讲了:
if [ $# -lt 1 ];then
echo "error! should enter the archive type (adhoc or appstore)."
echo ""
exit 2
fi
while getopts 't:' optname
do
case "$optname" in
t)
if [ ${optarg} != "adhoc" ] && [ ${optarg} != "appstore" ];then
echo "invalid parameter of $optarg"
echo ""
exit 1
fi
type=${optarg}
;;
*)
echo "error! unknown error while processing options"
echo ""
exit 2
;;
esac
done
清理构建目录
就如在xcode操作「product -> clean」。
log_path="/xxx/xxx"
configuration="release"
xcodebuild clean -configuration "$configuration" -alltargets >> $log_path
log_path是一个文档路径,只是用来记录命令的输出,因为都打在终端会很多,另外也方便后面分析。后面的命令也是如此。这里面带的选项可以根据需要参考xcodebuild -help的信息。
编译打包成archive
就如在xcode操作「product -> archive」
workspacename="xxx.xcworkspace"
scheme="xxx"
configurationbuilddir="xxx/build"
codesignidentity="iphone distribution: xxx, ltd. (xxxxxxxxxx)"
adhocprovisioningprofile="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
appstoreprovisioningprofile="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
configuration="release"
archivepath="/xxx/xxx.xcarchive"
xcodebuild archive -workspace "$workspacename" -scheme "$scheme" -configuration "$configuration" -archivepath "$archivepath" configuration_build_dir="$configurationbuilddir" code_sign_identity="$codesignidentity" provisioning_profile="$provisioningprofile" >> $log_path
这里的configuration_build_dir是中间文件生成的路径,可以不指定;code_sign_identity是证书名(在对应targets的build settings中选择完code sinning,再点击选择other...,就可以得到这串东西);provisioning_profile是配置文件(获取方法同code_sign_identity,格式一般是xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)。还可以添加其他参数,不设置的都是默认使用项目build settings里面的配置,包括code_sign_identity和provisioning_profile。
如果是workspace就用-workspace,就像编译带有cocoapods的项目,如果是普通项目则用-project。
执行完会生成一个.xcarchive文件和build文件夹如下:
.xcarchive
build文件夹
|------.a
|------.app
|------.app.dsym
|------.swiftmodule文件夹
|------arm.swiftdoc
|------arm.swiftmodule
|------arm64.swiftdoc
|------arm64.swiftmodule
将archive导出
xcodebuild -exportarchive -archivepath "$archivepath" -exportoptionsplist "$exportoptionsplist" -exportpath "/xxx/xxx" >> $log_path
其中$exportoptionsplist是对应使用的plist的完整路径(包括文件名)。
然后就会在指定的exportpath路径下生成.ipa文件。
上传到fir
firapitoken="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
ipapath="/xxx/xxx.ipa"
fir publish "$ipapath" -t "$firapitoken" >> $log_path
firapitoken在登录fir后,右上角-api token看到。
验证并上传到app store
altoolpath="/applications/xcode.app/contents/applications/application\ loader.app/contents/frameworks/itunessoftwareservice.framework/versions/a/support/altool"
${altoolpath} --validate-app -f ${ipapath} -u xxxxxx -p xxxxxx -t ios --output-format xml >>
${altoolpath} --upload-app -f ${ipapath} -u xxxxxx -p xxxxxx -t ios --output-format xml
在上面的pdf文档第38页讲明了用法和各个可选项,具体可以看下pdf。需要说明的是,生成的结果是xml打印在终端,可以保存到文档再解析出key来判断是否成功,目前这步还没做。
这是成功的结果:
os-version
10.11.2
success-message
no errors validating archive at /xxx/xxx.ipa
tool-version
1.1.902
xcode-versions
path
/applications/xcode.app
version.plist
buildversion
7
cfbundleshortversionstring
7.2
cfbundleversion
9548
productbuildversion
7c68
projectname
ideframeworks
sourceversion
9548000000000000
这是失败的结果(找不到itmstransporter的情况,用前面说的ln -s解决):
os-version
10.11.2
product-errors
code
-10001
message
transporter not found at path: /usr/local/itms/bin/itmstransporter. you should reinstall the application.
userinfo
mzunderlyingexception
transporter not found at path: /usr/local/itms/bin/itmstransporter. you should reinstall the application.
nslocalizeddescription
transporter not found at path: /usr/local/itms/bin/itmstransporter. you should reinstall the application.
nslocalizedfailurereason
transporter not found at path: /usr/local/itms/bin/itmstransporter. you should reinstall the application.
tool-version
1.1.902
xcode-versions
path
/applications/xcode.app
version.plist
buildversion
7
cfbundleshortversionstring
7.2
cfbundleversion
9548
productbuildversion
7c68
projectname
ideframeworks
sourceversion
9548000000000000
可见,成功会有个success-message的key,而失败会有product-errors的key。
邮件通知相关同事
发邮件时可能会想带上当前版本的一些信息,如版本号、内部版本号等,可以用plistbuddy实现读取甚至修改plist文件。
appinfoplistpath="`pwd`/xxx/xxx-info.plist"
bundleshortversion=$(/usr/libexec/plistbuddy -c "print cfbundleshortversionstring" ${appinfoplistpath})
bundleversion=$(/usr/libexec/plistbuddy -c "print cfbundleversion" ${appinfoplistpath})
之后便是发邮件:
python sendemail.py "测试版本 ios ${bundleshortversion}(${bundleversion})上传成功" "赶紧下载体验吧!https://fir.im/meijia"
或者
python sendemail.py "正式版本 ios ${bundleshortversion}(${bundleversion})提交成功" "ios ${bundleshortversion} 提交成功!"
python主要用smtplib,网上的文章大多都是旧的,特别是讲到ssl时特别复杂,其实具体看下smtplib的接口文档就可以实现了。另外有可能出现标题、内容乱码的现象。整合了下面的链接解决了:
下面是实现了ssl smtp登录的。
#!/usr/bin/env python3
#coding: utf-8
# sendemail title content
import sys
import smtplib
from email.mime.text import mimetext
from email.header import header
sender = 'xxxxxx@qq.com;'
receiver = 'xxx@qq.com;'
smtpserver = 'smtp.qq.com'
#smtpserver = 'smtp.exmail.qq.com'
username = sender
password = 'xxxxxx'
def send_mail(title, content):
try:
msg = mimetext(content,'plain','utf-8')
if not isinstance(title,unicode):
title = unicode(title, 'utf-8')
msg['subject'] = title
msg['from'] = sender
msg['to'] = receiver
msg["accept-language"]="zh-cn"
msg["accept-charset"]="iso-8859-1,utf-8"
smtp = smtplib.smtp_ssl(smtpserver,465)
smtp.login(username, password)
smtp.sendmail(sender, receiver, msg.as_string())
smtp.quit()
return true
except exception, e:
print str(e)
return false
if send_mail(sys.argv[1], sys.argv[2]):
print "done!"
else:
print "failed!"
可以赋值给msg['cc']实现抄送,经过测试,抄送的人过多会有一部分不成功,网上查了是这个库的bug。发送多个人用分号,另外末尾也要用分号。
上传符号表到bugly
用于分析解决崩溃bug挺好用的,而且他们的客服也很及时。
发现他们的2.4.1版本有问题,反馈后他们给了2.4.3版本,经测试没问题。
在bugly官网下载符号表工具
设置settings.txt
调用命令
java -jar buglysymbolios.jar -d -i $dsym -u -id "xxxxxxxxx" -key "xxxxxxxxxxx" -package "com.xxx.xxx" -version "$version" --o "xxx.zip"
注意版本号之类的要设置对。
简单例子
清理构建目录:
xcodebuild clean -configuration release -alltargets
归档(其他参数不指定的话,默认用的是.xcworkspace或.xcodeproj文件里的配置)
xcodebuild archive -workspace xxx.xcworkspace -scheme xxx -configuration release -archivepath ./xxx.xcarchive
导出ipa
xcodebuild -exportarchive -archivepath ./xxx.xcarchive -exportoptionsplist ./adhocexportoptions.plist -exportpath ./
上传fir
fir publish ./xxx.ipa -t xxxxxx
提交appstore
/applications/xcode.app/contents/applications/application loader.app/contents/frameworks/itunessoftwareservice.framework/versions/a/support/altool --validate-app -f ./xxx.ipa -u xxx -p xxx -t ios --output-format xml
/applications/xcode.app/contents/applications/application loader.app/contents/frameworks/itunessoftwareservice.framework/versions/a/support/altool --upload-app -f ./xxx.ipa -u xxx -p xxx -t ios --output-format xml
发邮件
python sendemail.py "邮件内容" "用户名" "密码"
上传符号表
java -jar buglysymbolios.jar -d -i $dsym -u -id "xxxxxxxxx" -key "xxxxxxxxxxx" -package "com.xxx.xxx" -version "$version" --o "xxx.zip"
对比实验
为了了解一些区别,我做了几个对比。我这里定义下三种方式,方便下面说明。
xcodebuild+xcrun(xcodebuild build和xcrun)
只用xcodebuild(archive和exportarchive),
xcode。
三种方式的对比
我使用xcodebuild+xcrun、仅xcodebuild、xcode三种分别对相同代码和配置进行操作,根据结果做比较:
xcodebuild+xcrun
ipa:40.7mb,.app:93.3mb,编译耗时:8m31s,打包耗时:15s。
仅xcodebuild
ipa:37.3mb,.app:74mb,.xcarchive:227.3mb,编译耗时:8m24s,打包耗时:26s。
xcode
ipa:37.3mb,.app:74mb,.xcarchive:227.3mb,编译耗时:8m40s,打包耗时:30s。
xcode生成的.xcarchive文件可以在以下路径看到:
/users/double/library/developer/xcode/archives
可以看出,仅使用xcodebuild的结果和使用xcode编译打包的结果是一致的,并且最终的ipa也可以正常安装使用。而第一种xcodebuild+xcrun的结果略大些,但是ipa也是可以正常使用的。这时需要了解下他们的区别。
xcodebuild+xcrun和仅xcodebuild的比较
使用xcrun打包方式二产生的.xcarchive中的.app
打包生成的.ipa文件大小同样为37.3mb,与方式二使用xcodebuild -exportarchive的结果一致!这样说明:使用xcrun的打包方法是正常的,和xcodebuild -exportarchive的结果一致,而且.ipa包仅和.app有关。那么说明,这两种方式的不同仅在于xcodebuild build和xcodebuild archive之间的不同。
删除.xcarchive中其他文件然后exportarchive
这时命令提示错误,但是上面我们已经得出结论.ipa的生成只和.app有关,所以可能的原因是,这个exportarchive命令会检查.archive的完整性和正确性,防止生成的.archive不完整或者是伪造的。下面做个实验看下。
命令到底做了什么
根据命令运行时输出的内容,看下中间做了什么
xcrun -sdk iphoneos packageapplication -v xxx.app -o xxx.ipa
packaging application: '/xxx/xxx.app'
arguments: output=/xxx/xxx.ipa verbose=1
environment variables:
sdkroot = /applications/xcode.app/contents/developer/platforms/iphoneos.platform/developer/sdks/iphoneos9.2.sdk
......
shell = /bin/bash
output directory: '/xxx/xxx.ipa'
temporary directory: '/var/folders/21/6s9bb23j0s1343pm7ltnlgpm0000gn/t/taoiik9ayk' (will not be deleted on exit when verbose set)
+ /bin/cp -rp /xxx/xxx.app /var/folders/21/6s9bb23j0s1343pm7ltnlgpm0000gn/t/taoiik9ayk/payload
program /bin/cp returned 0 : []
### checking original app
+ /usr/bin/codesign --verify -vvvv /xxx/xxx.app
program /usr/bin/codesign returned 0 : [/xxx/xxx.app: valid on disk
/xxx/xxx.xcarchive/products/applications/xxx.app: satisfies its designated requirement
]
done checking the original app
+ /usr/bin/zip --symlinks --verbose --recurse-paths /users/double/desktop/1.ipa .
program /usr/bin/zip returned 0 : [ adding: payload/(in=0) (out=0) (stored 0%)
adding: payload/xxx.app/ (in=0) (out=0) (stored 0%)
......
主要检查了环境变量,然后验证签名,然后压缩(看到了吗,居然是/usr/bin/zip),后面adding的基本都是.nib和.png等的压缩。看起来.archive只是一种压缩形式,包含了.app、.dsym、.plist和其他一些文件。
这里的codesign工具就是签名相关的,可以查看说明:
synopsis
codesign -s identity [-i identifier] [-r requirements] [-fv] [path ...]
codesign -v [-r requirement] [-v] [path|pid ...]
codesign -d [-v] [path|pid ...]
codesign -h [-v] [pid ...]
-s是签名,-v是验证。所以可以在.app生成后再签名。
xcodebuild clean
清理工作,根据参数删除指定的workplace、target、configuration(release或debug) 的中间文件,都是工程目录下的build文件夹。
xcodebuild archive
下面是里面主要的步骤:
create product structure 创建.app文件
compilec 编译文件(clang编译,指定了编译的sdk版本和指令集)
ld
createuniversalbinary (lipo)
compilestoryboard (ibtool )
compileassetcatalog (actool )
processinfoplistfile (builtin-infoplistutility )
generatedsymfile (dsymutil )
linkstoryboards(ibtool )
strip
processproductpackaging (builtin-productpackagingutility )
codesign (codesign --force --sign)
validate (builtin-validationutility )
总结
呼呼写了这么多,终于到总结部分了。这个过程学到了很多东西,脚本成果确实方便了很多,减少了编译打包过程中人工监守、人工操作的成本,并且测试和提交到appstore的包都验证过可用。