你是否曾經試著為 iOS 項目搭建一台支持持續集成的服務器,從我的個人經驗而言,這可不是一個輕松的活。首先需要准備一台 Mac 電腦,並安裝好全部所需的軟件和插件。你要負責管理所有的用戶賬戶,並提供安全保護。你需要授予訪問倉庫的權限,並配置所有的編譯步驟和證書。在項目運行時期,你需要保持服務器的穩健和最新。
最後,原本你想節省的時間,會發現你花費了大量的時間去維護這台服務器。不過如果你的項目托管在 GitHub) 上,現在有了新的希望:Travis CI。該服務可以為你的項目提供持續集成的支持,也就意味著它會負責好托管一個項目的所有細節。在Ruby 的世界中,Travis CI 已久負盛名。從 2013 年 4 月起,Travis 也開始支持 iOS 和 Mac 平台。
在這篇文章中,我將向你展示如何一步步的在項目中集成 Travis。不僅包括項目的編譯和單元測試的運行,還包括將應用部署到你所有的測試設備上。為了演示,我在 GitHub 上放了一個示例項目。在這篇文章的最後,我會教你一些提示:如何用 Travis 去定位程序中的錯誤。
我最喜歡 Travis 的一點就是它與 GitHub 的 Web UI 集成的非常好。例如 pull 請求。Travis 會為每次請求都執行編譯操作。如果一切正常,pull 請求在 GitHub 上看起來就像這樣:
萬一編譯不成功,GitHub 頁面會修改相應的顏色,給予提醒:
每日更新關注:http://weibo.com/hanjunqiang 新浪微博!
讓我們看一下如何將 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 和jspahrsummers/objc-build-scripts" target="_blank">一些默認的編譯腳本進行過預配置。上述的配置項已經足夠編譯你的項目了。
預裝的編譯腳本會分析你的 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 是來自 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 開發人員知道的那樣,這可能是最困難的一步。後面,我將寫一些腳本在服務器上給應用程序簽名。
每日更新關注:http://weibo.com/hanjunqiang 新浪微博!
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.cer.p12 -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.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
的文件:
#!/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 控制台驗證是否一切正常。如果一切正常的話,下面來看看如何將簽好名的應用程序部署給測試人員。
每日更新關注:http://weibo.com/hanjunqiang 新浪微博!
這裡有兩個知名的服務可以幫助你發布應用程序: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 的 API,首先需要獲得apitoken 和teamtoken。再強調一下,我們需要確保它們是加密的。在命令行中執行如下命令:
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 賬號,並創建一個新的應用程序。然後在概述頁面獲取一個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 一個月以來,並不總是那麼順暢。知道如何不通過直接訪問編譯環境就能找出問題是非常重要的。
在寫本文的時候,還沒有可以下載的虛擬機映像 (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,取得支持,以我的經驗來說,他們的響應非常迅速。
每日更新關注:http://weibo.com/hanjunqiang 新浪微博!
Travis CI 跟市面上同類產品相比還是有一些限制。因為 Travis 運行在一個預先配置好的虛擬機上,因此必須為每次編譯都安裝一遍所有的依賴。這會花費一些額外的時間。不過 Travis 團隊已經在著手提供一種緩存機制解決這個問題了。
在一定程度上,你會依賴於 Travis 所提供的配置。比如你只能使用 Travis 內置的 Xcode 版本進行編譯。如果你本地使用的 Xcode 版本較新,你的項目在服務器上可能無法編譯通過。如果 Travis 能夠為不同的 Xcode 版本都分別設置一個對應虛擬機會就好了。
對於復雜的項目來說,你可能希望把整個編譯任務分為編譯應用,運行集成測試等等。這樣你可以快速獲得編譯信息而不用等所有的測試都完成。目前 Travis 還沒有直接支持有依賴的編譯。
當項目被 push 到 GitHub 上時,Travis 會自動觸發。不過編譯動作不會立即觸發,你的項目會被放到一個根據項目所用語言不同而不同的一個全局編譯隊列,不過專業版允許並發編譯。
Travis CI 提供了一個功能完整的持續集成環境,以進行應用程序的編譯、測試和部署。對於開源項目來說,這項服務是完全免費的。很多社區項目都得益於 GitHub 強大的持續集成能力。你可能已經看過如下這樣的按鈕:
對於商業項目,Travis 專業版也能為私有倉庫提供快捷、簡便的持續集成支持。
如果你還沒有用過 Travis,趕緊去試試吧,它棒極了!