欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

详解Shell脚本实现iOS自动化编译打包提交

程序员文章站 2022-08-04 15:30:56
详解shell脚本实现ios自动化编译打包提交。 现在涉及到编译打包的工作主要是以下两个: 提交测试版本给测试同事 提交app store审核 两个流程分别是: 修改证书和配置文件,然后「produ...

详解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的包都验证过可用。