為 iOS 建立 Travis CI
你是否曾經試著為 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
的檔案,檔案中的內容如下:
language: objective-c
Travis 編譯器執行在虛擬機器環境下。該編譯器已經利用 Ruby,Homebrew,CocoaPods 和一些預設的編譯指令碼進行過預配置。上述的配置項已經足夠編譯你的專案了。
預裝的編譯指令碼會分析你的 Xcode 專案,並對每個 target 進行編譯。如果所有檔案都沒有編譯錯誤,並且測試也沒有被打斷,那麼專案就編譯成功了。現在可以將相關改動 Push 到 GitHub 中看看能否成功編譯。
雖然上述配置過程真的很簡單,不過對你的專案不一定適用。這裡幾乎沒有什麼文件來指導使用者如何配置預設的編譯行為。例如,有一次我沒有用 iphonesimulator
SDK 導致程式碼簽名錯誤。如果剛剛那個最簡單的配置對你的專案不適用的話,讓我們來看一下如何對 Travis 使用自定義的編譯命令。
自定義編譯命令
Travis 使用命令列對專案進行編譯。因此,第一步就是使專案能夠在本地編譯。作為 Xcode 命令列工具的一部分,Apple 提供了 xcodebuild
命令。
開啟終端並輸入:
xcodebuild --help
上述命令會列出 xcodebuild
所有可用的引數。如果命令執行失敗了,確保命令列工具已經成功安裝。一個常見的編譯命令看起來是這樣的:
xcodebuild -project {project}.xcodeproj -target {target} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO
使用 iphonesimulator
SDK 是為了避免簽名錯誤。直到我們稍後引入證書之前,這一步是必須的。通過設定 ONLY_ACTIVE_ARCH=NO
我們可以確保利用模擬器架構編譯工程。你也可以設定額外的屬性,例如 configuration
,輸入 man xcodebuild
檢視相關文件。
對於使用 CocoaPods
的專案,需要用下面的命令來指定 workspace
和 scheme
:
xcodebuild -workspace {workspace}.xcworkspace -scheme {scheme} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO
schemes 是由 Xcode 自動生成的,但這在伺服器上不會發生。確保所有的 scheme 都被設為 shared
並加入到倉庫中。否則它只會在本地工作而不會被 Travis CI 識別。
我們示例專案下的 .travis.yml
檔案現在看起來應該像這樣:
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
對於 iOS 來說,並不能正確支援 target 和應用程式的測試。這裡有一些解決方案,不過我建議使用 Xctool。
Xctool
Xctool 是來自 Facebook 的命令列工具,它可以簡化程式的編譯和測試。它的彩色輸出資訊比 xcodebuild
更加簡潔直觀。同時還添加了對邏輯測試,應用測試的支援。
Travis 中已經預裝了 xctool。要在本地測試的話,需要用 Homebrew 安裝 xctool:
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
到此為止,介紹的內容對於使用 Travis 的 library 工程來說,已經足夠了。我們可以確保專案正常編譯並測試通過。但對於 iOS 應用來說,我們希望能在真實的物理裝置上進行測試。也就是說我們需要將應用部署到我們的所有測試裝置上。當然,我們希望 Travis 能自動完成這項任務。首先,我們需要給程式簽名。
應用程式的簽名
為了在 Travis 中能給程式簽名,我們需要準備好所有必須的證書和配置檔案。就像每個 iOS 開發人員知道的那樣,這可能是最困難的一步。後面,我將寫一些指令碼在伺服器上給應用程式簽名。
證書和配置檔案
1. 蘋果全球開發者關係認證
從蘋果官網下載證書,或者從鑰匙串中匯出。並將其儲存到專案的目錄 scripts/certs/apple.cer
中。
2. iPhone 釋出證書 + 私鑰
如果還沒有釋出證書的話,先建立一個。登入蘋果開發者賬號,按照步驟,建立一個新的生產環境證書 (Certificates
> Production
> Add
> App Store and Ad Hoc
)。然後下載並安裝證書。之後,可以在鑰匙串中找到它。開啟 Mac 中的 鑰匙串
應用程式:
右鍵單擊證書,選擇 Export...
將證書匯出至 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 需要訪問這個配置檔案,所以我們需要將這個檔案的名字儲存為一個全域性環境變數。並將其新增至 .travis.yml
檔案的全域性環境變數 section 中。例如,如果配置檔案的名字是 TravisExample_Ad_Hoc.mobileprovision
,那麼按照如下進行新增:
env:
global:
- APP_NAME="TravisExample"
- 'DEVELOPER_NAME="iPhone Distribution: {your_name} ({code})"'
- PROFILE_NAME="TravisExample_Ad_Hoc"
上面還聲明瞭兩個環境變數。第三行中的 APP_NAME
通常為專案預設 target 的名字。第四行的 DEVELOPER_NAME
是 Xcode 中,預設 target 裡面 Build Settings
的 Code Signing Identity
> Release
對應的名字。然後搜尋程式的 Ad Hoc
或 In House
配置檔案,將其中黑體文字取出。根據設定的不同,括弧中可能不會有任何資訊。
加密證書和配置檔案
如果你的 GitHub 倉庫是公開的,你可能希望對證書和配置檔案 (裡面包含了敏感資料) 進行加密。如果你用的是私有倉庫,可以跳至下一節。
首先,我們需要一個密碼來對所有的檔案進行加密。在我們的示例中,密碼為 “foo”,記住在你的工程中設定的密碼應該更加複雜。在命令列中,我們使用 openssl
加密所有的敏感檔案:
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.p12.enc -a
通過上面的命令,可以創建出以 .enc
結尾的加密檔案。之後可以把原始檔案忽略或者移除掉。至少不要把原始檔案提交到 GitHub 中,否則原始檔案會顯示在 GitHub 中。如果你不小心把原始檔案提交上去了,那麼請看這裡如何解決。
現在,我們的檔案已經被加密了,接下來需要告訴 Travis 對檔案進行解密。解密過程,需要用到密碼。具體使用方法跟之前建立的 KEY_PASSWORD
變數一樣:
travis encrypt "ENCRYPTION_SECRET=foo" --add
最後,我們需要告訴 Travis 哪些檔案需要進行解密。將下面的命令新增到 .travis.yml
檔案中的 before-script
部分:
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.cer.enc -d -a -out scripts/certs/dist.cer
- 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
的檔案:
#!/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/
通過上面的命令建立了一個名為 ios-build
的臨時鑰匙串,裡面包含了所有證書。注意,這裡我們使用了 $KEY_PASSWORD
來匯入私鑰。最後一步是將配置檔案拷貝至 Library
資料夾。
建立好檔案之後,確保給其授予了可執行的許可權:在命令列輸入:chmod a+x scripts/add-key.sh
即可。為了正常使用指令碼,必須要這樣處理一下。
至此,已經匯入了所有的證書和配置檔案,我們可以開始給應用程式簽名了。注意,在給程式簽名之前必須對程式進行編譯。由於我們需要知道編譯結果儲存在磁碟的具體位置,我建議在編譯命令中使用 OBJROOT
和 SYMROOT
來指定輸出目錄。另外,為了建立 release 版本,還需要把 SDK 設定為 iphoneos
,以及將 configuration 修改為 Release
:
xctool -workspace TravisExample.xcworkspace -scheme TravisExample -sdk iphoneos -configuration Release OBJROOT=$PWD/build SYMROOT=$PWD/build ONLY_ACTIVE_ARCH=NO 'CODE_SIGN_RESOURCE_RULES_PATH=$(SDKROOT)/ResourceRules.plist'
如果運行了上面的命令,那麼編譯完成之後,可以在 build/Release-iphoneos
目錄找到應用程式的二進位制檔案。接下來,就可以對其簽名,並建立 IPA
檔案了。為此,我們建立一個新的指令碼:
#!/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-iphoneos
目錄生成 2 個檔案:TravisExample.ipa
和 TravisExample.app.dsym
。第一個檔案包含了分發至手機上的應用程式。dsym
檔案包含了二進位制檔案的除錯資訊。這個檔案對於記錄裝置上的 crash 資訊非常重要。之後當我們部署應用程式的時候,會用到這兩個檔案。
最後一個指令碼是移除之前建立的臨時鑰匙串,並刪除配置檔案。雖然這不是必須的,不過這有助於進行本地測試。
#!/bin/sh
security delete-keychain ios-build.keychain
rm -f ~/Library/MobileDevice/Provisioning\ Profiles/$PROFILE_NAME.mobileprovision
最後一步,我們必須告訴 Travis 什麼時候執行這三個指令碼。在應用程式編譯、簽名和清除等之前,需要先新增私鑰。在 .travis.yml
檔案中新增如下內容:
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
完成上面的所有操作之後,我們就可以將所有內容 push 到 GitHub 上,等待 Travis 對應用程式進行簽名。我們可以在工程頁面下的 Travis 控制檯驗證是否一切正常。如果一切正常的話,下面來看看如何將簽好名的應用程式部署給測試人員。
部署應用程式
這裡有兩個知名的服務可以幫助你釋出應用程式:TestFlight 和 HockeyApp。不管選擇哪個都能夠滿足需求。就我個人來說,推薦使用 HockeyApp,不過這裡我會對這兩個服務都做介紹。
首先我們對 sign-and-build.sh
指令碼做一個擴充 -- 在裡面新增一些 release 記錄:
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。再強調一下,我們需要確保它們是加密的。在命令列中執行如下命令:
travis encrypt "TESTFLIGHT_API_TOKEN={api_token}" --add
travis encrypt "TESTFLIGHT_TEAM_TOKEN={team_token}" --add
現在我們可以呼叫相應的 API 了。並將下面的內容新增到 sign-and-build.sh
:
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"
千萬不要使用 verbose 標記 (-v
) -- 這會暴露加密 tokens。
HockeyApp
註冊一個 HockeyApp 賬號,並建立一個新的應用程式。然後在概述頁面獲取一個 App ID
。接下來,我們必須建立一個 API token。開啟這個頁面,並建立一個。如果你希望自動的將新版本部署給所有的測試人員,那麼請選擇 Full Access
版本。
對 App ID 和 token 進行加密:
travis encrypt "HOCKEY_APP_ID={app_id}" --add
travis encrypt "HOCKEY_APP_TOKEN={api_token}" --add
然後在 sign-and-build.sh
檔案中呼叫相關的 API:
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 一個月以來,並不總是那麼順暢。知道如何不通過直接訪問編譯環境就能找出問題是非常重要的。
在寫本文的時候,還沒有可以下載的虛擬機器映像 (VM images) 。如果 Travis 不能正常編譯,首先試著在本地重現問題。在本地執行跟 Travis 相同的編譯命令:
xctool ...
為了除錯 shell 指令碼,首先需要定義環境變數。我的做法是建立一個新的 shell 指令碼來設定所有的環境變數。記得將這個指令碼新增到 .gitignore
檔案中 -- 因為我們並不希望將該檔案公開暴露出去。針對示例工程來說, config.sh
指令碼檔案看起來是這樣的:
#!/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
然後試著執行 echo $APP_NAME
,以此檢查指令碼是否正確。如果正確的話,那麼現在我們不用做任何修改,就能在本地執行所有的 shell 指令碼了。
如果在本地得到的是不同的編譯資訊,那麼可能是使用了不同的庫和 gems。儘量試著將配置資訊設定為與 Travis VM 相同的資訊。Travis 在這裡列出了其所有安裝的軟體版本。你也可以在 Travis 的配置檔案中新增除錯資訊得到所有庫檔案的版本:
gem cocoapod --version
brew --version
xctool -version
xcodebuild -version -sdk
在本地安裝好與伺服器完全相同的軟體之後,再重新編譯專案。
如果獲取到的編譯資訊仍然不一樣,試著將專案 check out 到一個新的目錄。並確保所有的快取都已清空。每次編譯程式時,Travis 都會建立一個全新的虛擬機器,所以不存在快取的問題,但在你的本地機器上可能會出現。
一旦在本地重現出和伺服器上相同的錯誤,就可以開始調查具體問題了。當然導致問題的原因取決於具體問題。一般來說,通過 Google 都能找到引起問題的根源。
如果一個問題影響到了 Travis 上其它的專案,那麼可能是 Travis 環境配置的原因。我曾經遇到過幾次這樣的問題 (特別是剛開始時)。如果發生這樣的情況試著聯絡 Travis,取得支援,以我的經驗來說,他們的響應非常迅速。
點評
Travis CI 跟市面上同類產品相比還是有一些限制。因為 Travis 執行在一個預先配置好的虛擬機器上,因此必須為每次編譯都安裝一遍所有的依賴。這會花費一些額外的時間。不過 Travis 團隊已經在著手提供一種快取機制解決這個問題了。
在一定程度上,你會依賴於 Travis 所提供的配置。比如你只能使用 Travis 內建的 Xcode 版本進行編譯。如果你本地使用的 Xcode 版本較新,你的專案在伺服器上可能無法編譯通過。如果 Travis 能夠為不同的 Xcode 版本都分別設定一個對應虛擬機器會就好了。
對於複雜的專案來說,你可能希望把整個編譯任務分為編譯應用,執行整合測試等等。這樣你可以快速獲得編譯資訊而不用等所有的測試都完成。目前 Travis 還沒有直接支援有依賴的編譯。
當專案被 push 到 GitHub 上時,Travis 會自動觸發。不過編譯動作不會立即觸發,你的專案會被放到一個根據專案所用語言不同而不同的一個全域性編譯佇列,不過專業版允許併發編譯。
總結
Travis CI 提供了一個功能完整的持續整合環境,以進行應用程式的編譯、測試和部署。對於開源專案來說,這項服務是完全免費的。很多社群專案都得益於 GitHub 強大的持續整合能力。你可能已經看過如下這樣的按鈕:
對於商業專案,Travis 專業版也能為私有倉庫提供快捷、簡便的持續整合支援。
如果你還沒有用過 Travis,趕緊去試試吧,它棒極了!