译文链接: http://blog.jobbole.com/52116/
你是否曾经试着为你的iOS项目搭建一台支持持续集成的服务器,从我的个人经验而言,这可不是一个轻松的活。你需要准备一部Mac,安装好全部所需的软件和插件。你要负责管理所有的用户账户并提供安全保护。
原本你想节省的时间,最终你会发现你花费了大量的时间去维护这台服务器。不过如果你的项目托管在GitHub上,现在有了新的希望:Travis CI。该服务可以为你的项目提供持续集成的支持,也就意味着它会负责好托管一个项目的所有细节。在Ruby的世界中,Travis CI已久负盛名。从2013年4月开始,Travis也开始支持iOS和Mac平台。
在这篇文章中,我会向你展示如何一步步为你的项目集成Travis。不仅包括编译项目和进行单元测试,还能够将你的应用投送到你所有的测试设备上。为了演示,我在gitHub上放了一个示例项目。在这篇文章的最后,我会教你如何用Travis去定位程序中的错误。
GitHub集成
我最喜欢Travis的一点就是他与GitHub的Web UI集成的非常好。譬如pull请求。Travis会为每次请求都执行编译操作。如果一切正常,pull请求在GitHub上看起来就像这样:
万一编译不成功,GitHub页面会相应的改变颜色给予提醒:
链接Travis和GitHub
让我们看一下如何链接你的GitHub项目到Travis。使用你的GitHub账号登陆Travis。对于私有工作目录,你需要注册一个Travis专业版账号。
登陆成功后,你就可以为你的项目打开Travis支持。找到属性页面,在此列出了你的所有GitHub项目。不过要注意,如果你此后创建了一个新的工作目录,要使用Sync now按钮进行同步。Travis只会偶尔更新你的项目列表。
现在只需要打开这个开关就可以为你的项目添加Travis服务。以后你会看到Travis会和你的GitHub项目设置相关联。下一步应该告诉Travis当它收到项目改动之后该做什么。
轻量级的项目配置
Travis CI需要你的项目的一些基本信息。在你项目的根目录创建一个名叫.travis.yml的文件,内容如下:
1
|
language: objective-c
|
Travis编译器运行在虚拟机环境下.已经使用Ruby,Homebrew,CocoaPods和一些编译脚本进行过配置。上述的配置项已经足够编译你的项目了。
预装的编译脚本会分析你的Xcode项目,编译项目下的所有Target。如果所有文件都没有编译错误测试也没有跳出项目就编译成功了。现在可以将你的改动Push到GitHub看看能成功编译。
虽然这看起来好像很简单,不过对你的项目不一定适用。几乎没有什么文档来指导用户如何配置默认的编译行为。举个栗子, 有一次我没有用模拟器的SDK导致检查应用签名时发生了错误。如果刚刚那个最轻量级的配置对你的项目不适用的话,让我们来看一下如何用定制的编译命令来适用Travis。
定义编译命令
Travis使用命令行来编译你的项目。因此,第一步就是使项目能够在本地编译。作为Xcode命令行工具的一部分,Apple提供了xcodebuild命令。
打开你的终端输入:
1
|
xcodebuild –help
|
这会列出xcodebuild可用的所有参数。如果命令执行不成功,确保你的命令行工具已经成功安装。通常一个常见的编译命令看起来像这样:
1
|
xcodebuild -project {project}.xcodeproj -target {target} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO
|
使用iphonesimulator SDK是为了避免应用签名错误。这是必须的一步直到我们稍后引入证书为止。通过设置ONLY_ACTIVE_ARCH=NO我们可以确保在模拟器的架构下编译。你也可以设置额外的属性。用man xcodebuild来阅读文档。
对于使用CocoaPods的项目,你需要指定workspace和scheme。
1
|
xcodebuild -workspace {workspace}.xcworkspace -scheme {scheme} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO
|
Schemes是由Xcode自动生成的,但这在服务器上不会发生。确保所有的scheme都被设为shared并加入到工作目录中。否则它只会在本地工作而不会被Travis CI识别。
我们的示例项目下的.travis.yml文件现在应该看起来像这样:
1
2
3
|
language: objective-c
script: xcodebuild -workspace TravisExample.xcworkspace -scheme TravisExample -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO
|
运行测试
通常对于测试来说你会使用如下的命令(注意test属性)
xcodebuild test -workspace {workspace}.xcworkspace -scheme {test_scheme} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO
不幸的是xcodebuild并不能支持多target以及iOS的应用测试。苹果已经在着手解决这个问题,不过我建议使用Xctool来代替。
Xctool
Xctool是来自Facebook的命令行工具,他可以帮助你更加轻松快捷的编译测试你的应用。他的彩色输出信息比xcodebuild更加简洁直观,结构清晰。同时还添加了对逻辑测试,应用测试的支持。
Travis中已经预装了xctool。要在本地测试的话,需要用Homebrew先安装xctool:
1
2
3
|
brew update
brew
install
xctool
|
用法非常简单,xctool 使用跟 xcodebuild相同的参数:
xctool test -workspace TravisExample.xcworkspace -scheme TravisExampleTests -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO
这要这些命令在本地能正常工作,我们就可以把他们添加到.travis.yml文件里:
language: objective-c
script:
– xctool -workspace TravisExample.xcworkspace -scheme TravisExample -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO
– xctool test -workspace TravisExample.xcworkspace -scheme TravisExampleTests -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO
目前我们所添加的配置已经足够编译一个框架类的应用。我们能够保证项目可以正常编译并通过测试。但对于真正的iOS应用来说,我们希望在真实的物理设备上进行测试。很显然,我们要借助Travis来帮我们自动部署。整个过程的第一步,我们需要给我们的应用签名。
应用签名
为了在Travis中给我们的应用签名,我们需要准备好所有必要的证书和配置文件。就像每个iOS开发人员知道的那样,这可能是最困难的一步。后面我们将写一些脚本来帮助我们在服务器上给应用签名。
证书和配置文件
从苹果的配置页面中下载或者从你的Keychain中导出,将它保存到你的项目目录下scripts/certs/apple.cer这个位置。
2. iPhone发布证书 + 私钥
如果你还没有一个iPhone发布证书你需要创建一个。登陆你的苹果开发者账号,你可以跟随下面的步骤创建一个生产环境的新证书(Certificates > Production > Add > App Store and Ad Hoc)。确保你已经下载并安装了这个证书。以后你可以在你的Keychain中找到它,还有一个捆绑的私钥。现在打开你Mac上的Keychain应用:
右击证书选择导出将其放在scripts/certs/dist.cer路径,在导出捆绑的私钥,保存到scripts/certs/dist.p12。可以根据你的需要设置一个密码。
Travis需要知道你的私钥密码,我们需要将其储存在某个地方。显然我们不想用简单的文本来存储这个密码。我们可以利用Travis的安全环境变量。打开终端进入包含.travis.yml文件的目录。首先用gem install travis命令安装Travis gem。安装完成后你就可以用以下命令添加密码:
travis encrypt “KEY_PASSWORD={password}” –add
这样就可以安装一个叫做KEY_PASSWORD的加密环境变量到你的.travis.yml配置文件。在任何可以被Travis CI执行的脚本中都可以使用这个变量。
3. iOS 移动设备备案文件(发布用)
如果你还没有一个发布用的移动设备备案文件。根据你的开发者账号类型,你可以选择Ad Hoc或者In House两种不同的备案文件(Provisioning Profiles > Distribution > Add > Ad Hoc or In House).下载将其保存到scripts/profile/目录下。
我们需要在Travis中访问此备案文件,所以我们需要将此文件的名字存储为一个全局变量。譬如我们可以将其命名为TravisExample_Ad_Hoc.mobileprovision,像这样添加:
1
2
3
4
5
|
env:
global:
- APP_NAME="TravisExample"
- 'DEVELOPER_NAME="iPhone Distribution: {your_name} ({code})"'
- PROFILE_NAME="TravisExample_Ad_Hoc"
|
这里还有两个声明的全局变量。APP_NAME通常指的就是你的项目主target的名字。DEVELOPER_NAME里是你在项目主target下Xcode Build Settings 中Code Signing Identity > Release里面看到的名字。最后搜索一下你应用的Ad Hoc或者In House配置文件,将其中的黑体文字全部去掉。根据你设置的不同,在一些属性的方括号里面可能不会有任何信息。
加密证书和备案文件
不过你的GitHub权限是公开的。你可能会想要给你的证书和备案文件加密,因为他们包含了你应用的重要信息。如果你使用的是一个私有目录,你可以跳过这一小节。
首先我们需要想出一个密码来加密我们所有的文件。在下面的例子中,我们使用foo这个单词,你完全可以将其替换为使用于你项目的更安全的密码。在命令行中需要使用openssl来加密这些敏感文件:
1
2
3
|
openssl aes-256-cbc -k "foo" -in scripts/profile/TravisExample_Ad_Hoc.mobileprovision -out scripts/profile/TravisExample_Ad_Hoc.mobileprovision.enc -a
openssl aes-256-cbc -k "foo" -in scripts/certs/dist.cer -out scripts/certs/dist.cer.enc -a
openssl aes-256-cbc -k "foo" -in scripts/certs/dist.p12 -out scripts/certs/dist.cer.p12 -a
|
这样可以创建.enc后缀的加密版本证书和配置文件。现在你可以删除或者忽略那些原始文件。但是有一点千万不要提交未加密的证书与配置文件,否则它们会出现在GitHub上。如果你不小心这样做了,立刻去寻求帮助。
现在我们的文件都已经加密,我们需要告诉Travis如何解密。所以我们需要提供解密的密码,使用之前创建KEY_PASSWORD变量一样的方式:
travis encrypt “ENCRYPTION_SECRET=foo” –add
Lastly, we have to tell Travis which files to decrypt. Add the following commands to the before-script phase in the .travis.yml:
最后我们需要告诉Travis那些文件需要解密。在.travis.yml文件的before-script阶段添加如下命令:
1
2
3
4
|
before_script:
- openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in scripts/profile/TravisExample_Ad_Hoc.mobileprovision.enc -d -a -out scripts/profile/TravisExample_Ad_Hoc.mobileprovision
- openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in scripts/certs/dist.p12.enc -d -a -out scripts/certs/dist.p12
- openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in scripts/certs/dist.p12.enc -d -a -out scripts/certs/dist.p12
|
现在你在GitHub上的都是安全的了,Travis可以正常的读取使用它们。但是还有一个安全问题你需要知道:在Travis的编译日志里可能会显示出解密的密码,不过对pull请求来说不会出现。
添加脚本
现在我们需要确保所有的证书都导入到Travis CI的钥匙串中。这一步我们需要在scripts文件夹下面添加一个新的文件add-key.sh:
1
2
3
4
5
6
7
|
#!/bin/sh
security create-keychain -p travis ios-build.keychain
security
import
.
/scripts/certs/apple
.cer -k ~
/Library/Keychains/ios-build
.keychain -T
/usr/bin/codesign
security
import
.
/scripts/certs/dist
.cer -k ~
/Library/Keychains/ios-build
.keychain -T
/usr/bin/codesign
security
import
.
/scripts/certs/dist
.p12 -k ~
/Library/Keychains/ios-build
.keychain -P $KEY_PASSWORD -T
/usr/bin/codesign
mkdir
-p ~
/Library/MobileDevice/Provisioning
\ Profiles
cp
.
/scripts/profile/
$PROFILE_NAME.mobileprovision ~
/Library/MobileDevice/Provisioning
\ Profiles/
|
这里我们创建了一个新的临时keychain叫做ios-build,其中包含了所有的证书。注意这里我们使用$KEY_PASSWORD来导入私钥。在最后一步,备案文件会拷贝到Library文件夹。
在创建这个文件后,确保给予它可执行的权限。在命令行里输入chmod a+x scripts/add-key.sh。下面所有的脚本都需要执行这一步。
现在所有的证书和配置文件都已就绪我们可以开始给我们的应用签名。注意一定要先编译应用再给其签名。由于我们需要知道编译好的二进制文件的位置,我推荐在编译命令中使用OBJROOT和SYMROOT这两个变量。另外,我们应该把SDK设置为iphoneos,configuration项改为Release。
xctool -workspace TravisExample.xcworkspace -scheme TravisExample -sdk iphoneos -configuration Release OBJROOT=$PWD/build SYMROOT=$PWD/build ONLY_ACTIVE_ARCH=NO
运行这个命令,你会发现应用编译后的二进制文件出现在build/Release-iphoneos文件夹下面。现在我们可以使用如下的脚本给应用签名和打包。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#!/bin/sh
if
[[
"$TRAVIS_PULL_REQUEST"
!=
"false"
]];
then
echo
"This is a pull request. No deployment will be done."
exit
0
fi
if
[[
"$TRAVIS_BRANCH"
!=
"master"
]];
then
echo
"Testing on a branch other than master. No deployment will be done."
exit
0
fi
PROVISIONING_PROFILE=
"$HOME/Library/MobileDevice/Provisioning Profiles/$PROFILE_NAME.mobileprovision"
OUTPUTDIR=
"$PWD/build/Release-iphoneos"
xcrun -log -sdk iphoneos PackageApplication
"$OUTPUTDIR/$APPNAME.app"
-o
"$OUTPUTDIR/$APPNAME.ipa"
-sign
"$DEVELOPER_NAME"
-embed
"$PROVISIONING_PROFILE"
|
第二行到第九行尤其重要。你不会想要给一个试验用的分支创建一次新的release。对pull请求也是一样。环境变量已经被禁用,所以pull请求也不会编译。
第十四行才是真正的签名操作。这会使build/Release-iphoneo文件下下面出现两个新文件TravisExample.ipa和TravisExample.app.dsym。第一个文件包含了将被安装到手机的应用。dsym文件包含了二进制文件的调试信息。这个文件对记录以应用在设备上的crash非常重要。后面我们将会使用它们来部署我们的应用。
最后要添加一些脚本语句移除临时的Keychain,将备案文件删除。你可以在本地测试一下这些语句,虽然不是很必要。
1
2
3
|
#!/bin/sh
security delete-keychain ios-build.keychain
rm
-f ~
/Library/MobileDevice/Provisioning
\ Profiles/$PROFILE_NAME.mobileprovision
|
最后一步我们需要告诉Travis什么时候执行这三个脚本。私钥需要在应用编译前添加,然后再进行签名和清除等操作。在.travis.yml文件里添加如下语句:
1
2
3
4
5
6
7
8
9
|
before_script:
- ./scripts/add-key.sh
- ./scripts/update-bundle.sh
script:
- xctool -workspace TravisExample.xcworkspace -scheme TravisExample -sdk iphoneos -configuration Release OBJROOT=$PWD/build SYMROOT=$PWD/build ONLY_ACTIVE_ARCH=NO
after_success:
- ./scripts/sign-and-upload.sh
after_script:
- ./scripts/remove-key.sh
|
完成这一步,我们可以将所有的文件放到GitHub上等待Travis给我们的应用签名。我们可以在项目页面下的Travis控制台验证签名是否成功。如果一切正常,我们来看一下如何将签名好的应用提交给测试人员。
发布应用
有两个知名的服务可以帮助你发布你的应用:TestFlight and HockeyApp。不管哪一个都足够满足你的需要。个人比较推荐HockeyApp,不过两种服务我都会教你怎么集成到项目里。
我们在sign-and-build.sh中添加脚本语句,首先添加一些发布日志:
1
2
|
RELEASE_DATE=`date '+%Y-%m-%d %H:%M:%S'`
RELEASE_NOTES="Build: $TRAVIS_BUILD_NUMBER\nUploaded: $RELEASE_DATE"
|
注意这里我们使用了一个Travis的全局环境变量(TRAVIS_BUILD_NUMBER)。
TestFlight
创建一个TestFlight账号并配置好你的应用。为了使用TestFlight的API,你需要先获得api_token和team_token,在强调一次,我们需要确保它们是加密的,在命令行执行以下命令:
1
2
|
travis encrypt
"TESTFLIGHT_API_TOKEN={api_token}"
--add
travis encrypt
"TESTFLIGHT_TEAM_TOKEN={team_token}"
--add
|
现在我们可以调用相应的API了。添加以下命令到sign-and-build.sh:
1
2
3
4
5
6
7
|
curl http:
//testflightapp
.com
/api/builds
.json \
-F
file
=
"@$OUTPUTDIR/$APPNAME.ipa"
\
-F dsym=
"@$OUTPUTDIR/$APPNAME.app.dSYM.zip"
\
-F api_token=
"$TESTFLIGHT_API_TOKEN"
\
-F team_token=
"$TESTFLIGHT_TEAM_TOKEN"
\
-F distribution_lists=
'Internal'
\
-F notes=
"$RELEASE_NOTES"
|
千万不要使用-v标记,这样会暴露你的加密token。
HockeyApp
注册一个HockeyApp账号,创建一个新应用。从网站上找到App ID信息。下面我们要生成API token。如果你希望自动部署最新的版本给测试人员,需要选择Full Access版本。
加密所有的token:
1
2
|
travis encrypt
"HOCKEY_APP_ID={app_id}"
--add
travis encrypt
"HOCKEY_APP_TOKEN={api_token}"
--add
|
在从sign-and-build.sh中调用API:
1
2
3
4
5
6
7
8
|
curl https:
//rink
.hockeyapp.net
/api/2/apps/
$HOCKEY_APP_ID
/app_versions
\
-F status=
"2"
\
-F notify=
"0"
\
-F notes=
"$RELEASE_NOTES"
\
-F notes_type=
"0"
\
-F ipa=
"@$OUTPUTDIR/$APPNAME.ipa"
\
-F dsym=
"@$OUTPUTDIR/$APPNAME.app.dSYM.zip"
\
-H
"X-HockeyAppToken: $HOCKEY_APP_TOKEN"
|
注意我们上传了dsym文件。如果你集成了TestFlight和HockeyApp SDK,你可以立刻收到易读的Crash报告。
Travis故障检查
超过一个月的Travis使用经历并不是完美的。知道如何不通过访问编译环境就能找出问题是非常重要。
写这篇文章时,还没有可用的Travis模拟器截图可以下载。如果Travis不能正常编译了,第一步先试着在本地重现问题。在本地执行跟Travis使用的相同的编译命令:
xctool …
如果要调试脚本语句,你要先定义环境变量。我的方法是创建一个全新的脚本来设置所有的环境变量。将这个脚本添加到.gitignore文件中,你不会想把它暴露给其他人。我的config.sh文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#!/bin/bash
# Standard app config
export
APP_NAME=TravisExample
export
DEVELOPER_NAME=iPhone Distribution: Mattes Groeger
export
PROFILE_NAME=TravisExample_Ad_Hoc
export
INFO_PLIST=TravisExample
/TravisExample-Info
.plist
export
BUNDLE_DISPLAY_NAME=Travis Example CI
# Edit this for local testing only, DON'T COMMIT it:
export
ENCRYPTION_SECRET=...
export
KEY_PASSWORD=...
export
TESTFLIGHT_API_TOKEN=...
export
TESTFLIGHT_TEAM_TOKEN=...
export
HOCKEY_APP_ID=...
export
HOCKEY_APP_TOKEN=...
# This just emulates Travis vars locally
export
TRAVIS_PULL_REQUEST=
false
export
TRAVIS_BRANCH=master
export
TRAVIS_BUILD_NUMBER=0
|
执行config.sh导出这些环境变量(确保config.sh有足够的权限),
1
|
. .
/config
.sh
|
接着输入echo $APP_NAME命令看看它能否正常工作,你不用做任何修改就可以在本地执行这些脚本语句。
如果你在本地得到的是不同的编译信息,可能好似因为你安装了不同版本的三方库。试着模拟和Travis模拟器相同的配置。Travis在他们的网站上里列出了所有安装的软件版本。你也可以在Travis配置文件中添加调试信息找出所有库文件的版本:
1
2
3
4
|
gem cocoapod --version
brew --version
xctool -version
xcodebuild -version -sdk
|
在本地安装好于服务器完全相同的软件再重新编译项目。
如果编译信息还是不一样,试着将重新Check out项目到一个新的目录。确保将所有的缓存信息都清空。Travis会为每次编译创建一个全新的虚拟机,所以不存在缓存的问题,但在你的本地机器上可能会出现。
一旦你在本地重现出和服务器上相同的错误,你就能定位到问题的根源,当然那取决于你的具体问题。Google一下通常是帮你找出问题的好方法。
如果一个问题影响到了Travis上其他的项目,那么可能是Travis环境配置的原因。我发现过几次这样的问题。如果发生这样的情况试着联系Travis的Support,以我的经历它们的响应非常迅速。
批评指正
Travis CI跟市面上同类产品相比还是有一些限制。因为Travis运行于一个预先配置好的模拟器,你必须为每次编译都安装一遍所有的依赖软件。这会花费一些额外的时间。不过Travis团队已经在着手提供一种缓存机制来解决这个问题了。
在一定程度上你依赖于Travis所提供的配置。比如你只能使用Travis内置的Xcode版本进行编译。如果你本地使用的Xcode版本较新,你的项目可能会在服务器上无法编译。如果Travis能够为不同的Xcode版本都设置一个对应虚拟机会更有帮助。
对于复杂的项目来说,你会希望把整个编译任务分为编译应用,运行集成测试等等。这样你可以快速获得编译信息而不用等所有的测试都完成。目前Travis还不能支持有依赖的编译。
当你将你的项目push到GitHub上时,Travis会自动触发。不过编译动作不会立即触发,你的项目会被放到一个根据项目所用语言不同而不同的一个全局编译队列,不过专业版可以允许并发编译。
总结
Travis CI提供了一个功能完整的持续集成环境帮助你编译测试和部署你的iOS应用。对于开源项目来说,这项服务是完全免费的。很多社区项目都得益于GitHub的持续集成能力。