KKBOX iOS 開發過程中使用的 Jenkins 環境

先跟大家說明:目前架上版本的 KKBOX iOS 版本(6.2.50)有一些問題,在背景播放離線歌曲一兩首歌之後會暫停播放,如果你遇到這個問題,請先試試看關閉「歌曲淡入淡出」功能。另外,其實我們在發現問題的時候,也已經第一時間解決,但大家也知道…一套 iOS App 從修正完畢,到送到用戶手中,中間有一個很麻煩的關卡…。

KKBOX 在 2011 年左右在 iOS 版本的開發流程中導入 Jenkins,目前 iOS 開發部門負責大約七個主要產品,每個專案都透過 Jenkins 自動編譯每日發行版本,以及定時自動執行單元測試。隨著專案發展,一開始建置的環境已經沒有辦法因應現在的需求,所以我們最近大幅調整了 Jenkins 環境的架構。

建置 CI 系統的主機要能夠滿足兩個需求:速度快與容量大。

我們希望在每次改動程式碼之後,CI 主機都可以快速編譯出新版本,確認現在的程式碼沒有編譯問題,也可以讓測試人員隨時都可以拿到最新的版本,所以主機的編譯速度要快。

MacPro

另一方面,編譯出愈多的版本,也就等於是要 archive 更多的檔案,這樣當測試版本出現問題時,我們也可以直接去 Jenkins 找到當初版本的 debug symbol 確認問題—畢竟有些 crash 不見得可以被 HockeyApp 上收集到做過 symbolicate 過的 crash log,拿到沒有 symbolicate 過的 crash log,還是得自己透過 debug symbol 以及 atos 等工具解讀 crash log。在密集開發的時候,KKBOX iOS 開發部門一天大概會發出二十到三十個 build 出來,如果有重要的產品支線,每條支線也都有對應的 Jenkins job。

為了解決編譯速度的問題,我們買了 Mac Pro 當 build machine,買的時候直接要求上面插滿 64G 的 ram,但是 Mac Pro 所提供的 SSD 磁碟容量實在少得可憐。既然一台主機沒辦法同時滿足速度快與容量大兩個要求,那麼,我們就把不同的任務,分散在不同的主機上吧!我們使用 Jenkins 的 master/slave 架構,拿一台 Linux VM 當做 master,另外有四台 Mac 是 slave 的角色,負責編譯工作。

Master/Slave

選擇 Linux VM 當做 master 的原因是,KKBOX 前幾年為了開發的需要,購置了一台性能相當優異的 VM 主機,當 RD 同仁有開發需求時,都可以申請需要的 VM;主機虛擬化之後較易於備份,就算 iOS 開發部門自行管理的 Mac 有什麼問題,只要可以讓這個 VM 跑起來,還是可以輕鬆拿到之前已經編譯出來的版本。

話說其實我們曾經想過讓編譯用的 Mac 環境也變成 VM,放在這台 VM 主機上跑,只是一試下去,才發現當初購置的主機用的是 AMD CPU,不是 Intel CPU。

我們在 Jenkins 設定畫面中的「Manage Nodes」 中新增 Slave,這邊需要注意:我們該把 Mac 這邊設定成哪種 Slave? Jenkins 的 Slave 可以用 SSH 啟動,也可以用 Java Web Start 啟動,該用哪一種?

如果是 iOS 開發,我們建議 都使用 Java Web Start 啟動 Slave:就算我們使用 xcodebuild 或是 xctool 等 command line 工具編譯,編譯過程中也往往需要用到 Mac 的 window server。比方說,編譯 iOS App 的最後一步是 code sign,code sign 需要讀取 Keychain 中存放的 certificate,而就算你 unlock 了 Keychain,xcodebuild 有時候還是會跟你要求權限輸入系統密碼,密碼輸入框是 GUI 的,在沒有 window server 的環境下,這個密碼輸入框就跳不出來。

如果你要跑單元測試,iOS App 的單元測試又與 iOS Simulator 相依,必須要啟動 iOS Simulator 才能執行單元測試,而 iOS Simulator 必須要在有 window server 的環境下執行。

於是,雖然我們現在的編譯主力是 Mac Pro,還是留了幾台 Mac Mini 負責跑單元測試,因為就算是多強大、具備怎樣多工能力的主機,一台 Mac 同時間就只能夠跑一個 iOS Simulator,如果多個專案同時要跑 iOS 單元測試,我們得手動分派到指定的 Mac 上 錯開。此外,如果一套 library 是跨平台的,可以同時在 iOS 與 Mac OS X 上使用,我們會偏好寫成 Mac 的單元測試。

Provision Profiles

當 build machine 分散成多台,我們下一個挑戰便是,怎樣讓每一台主機都有編譯過程中需要的 provision profiles,而當 provision profiles 更新時,也可以快速更新到每一台 build machine 上頭。

cupertino 這個 opensource 專案可以解決我們的困擾。在每一台 Mac 上安裝這個工具後,使用 ios login 指令登入,接下來就可以用 command line 指令 ios profiles:download 更新 provision profiles。我們在 Jenkins 上建立了四個 Job,分別指定在四台不同的機器執行,而這個 Job 就會透過我們指定的 script 抓取指定的 provision profiles。

至於 Keychain 中的 certificate 呢,由於每個 certificate 都需要與當初產生 certificate 的 private key 連結,有了一台新機器,還是得手動搬移 certificate 與 private key。不過跟更新 provision profiles 比較起來,更新 certificate 的次數少很多,畢竟光是多了一台測試機,就得要更新一次 provision profiles。

Coverage Reports

我們在執行單元測試的時候,同時也會產生覆蓋率報告。關於怎樣在 Jenkins 上執行單元測試與產生覆蓋率報告,網路上有不少好文件可以參考,像是

用「iOS jenkins coverage」當關鍵字,就可以從搜尋引擎找到更多文章。這邊要說的是,我們把 Jenkins 分散成多台機器之後,變得不太容易閱讀「到底那一行程式碼被有被單元測試覆蓋到」。

看來看去,大家在 Jenkins 上都使用 Cobertura 這個 plug-in 產生覆蓋率報告,不過,如果你的 job 不是在 master 上執行,就會無法閱讀逐行程式碼的覆蓋率報告,而是顯示「找不到程式碼」錯誤。

原因是,Cobertura Plug-in 是在要閱讀逐行覆蓋率報告的時候,才去從 Job 目錄中尋找程式碼,而且只從 master 找;但假如你的 Job 是在 slave 上,程式碼也就放在 slave 上,就算你可以從 master 的介面看到 slave 的 workspace 內容,Cobertura 就是讀不到。

山不轉路轉。我們在執行了單元測試之後,就把中途產生的 gcda、gcno 檔案給 zip 起來,當做 artifact 給 archive 起來。當我們想要閱讀覆蓋率報告的時候,就下載這包 zip,使用 Desktop 版本的覆蓋率報告閱讀工具開啟,免費的有 CoverStory,付費的有 Xcoverage。不過還是要稍微花點時間,將你自己桌面環境中的程式碼 repo 路徑與 build machine 的路徑對應一下。

Apple Watch

KKBOX 在四月的時候推出支援 Apple Watch 的版本,要讓 Jenkins 編譯支援 Apple Watch 的 iOS App 還頂麻煩的。要支援 Apple Watch,原本的 iOS App 會增加 Watch App 與 Watch App Extension 兩個 bundle,iOS App、Watch App 與 Watch App Extension 都得要做 code sign 就罷了,麻煩的是,你還得要用三組不同的 provision profiles 做 code sign。

iOS App 與 Watch App Extension 相當程度要透過 shared data 溝通,而且也是蘋果在第一代 Apple Watch 系統上(WatchKit,第二代是接下來要推出的 watchOS2)建議的作法。如果要使用 shared data,那麼兩邊都需要被加入到 app group 中,要能夠被加入到 app group 中,就得使用綁定了 bundle ID 的 provision profile,而既然 iOS App 與 Watch App Extension 的 bundle ID 都不一樣,就得產生不同的 provision profile。

要寫一個支援 Apple Watch 的 build script,有人建議 先把 binary build 出來,然後刪掉原本的 code signature 重新 code sign,另外也看過有些人建議使用環境變數額外指定 Watch App 與 Watch App Extension 要使用哪組 provision profile。不管是哪種作法,其實都頂麻煩。

小結

Jenkins 設定有不少麻煩的地方。不過,即使 Jenkins 設定再麻煩,也無法掩蓋將工作自動化,減少人為操作錯誤的優點。我們可以自動化編出最新的版本,不但測試人員可以輕鬆拿到要測試的標的,更可以透過其他工具直接做自動化的黑箱測試,我們也可以透過單元測試,在有問題的時候立刻察覺。雖然我們的產品在釋出之後往往還是有不少問題,但代表的不是將工作自動化的方式沒用,而是我們還有地方做得不夠好。

About zonble

iOS Developer at KKBOX.
This entry was posted in iOS and tagged , , , . Bookmark the permalink.

7 Responses to KKBOX iOS 開發過程中使用的 Jenkins 環境

  1. Kros says:

    Hello,好奇問一下
    每次 Build 的時候,是只有 build Ad Hoc 版本,還是說也有 build App Store 版本呢?

  2. zonble says:

    我們是在要 submit 的時候才 build App Store 的版本。

    這跟我們怎麼使用 git 有關,我們的 git 流程是要 submit 的時候才去建立這次要 submit 的分支,如果要做 hot fix,也會從過去某個點拉出來,所以 submit 用的 Jenkins job 所在的 git branch 不固定。這個倒沒有怎麼做比較好的問題,而是怎樣比較能夠與工作流程搭配。

  3. thomas chiu says:

    hi!
    我是你們樓下的公司,公司取近也想開始推 CI,不知是否有機會請你來我們公司分享一下你們的經驗與作法
    謝謝

  4. Logan says:

    其實可以再加上自動上傳ipa到testflight的功能使CI更完整,使用pilot就可以輕鬆做到。

Leave a Reply

Your email address will not be published. Required fields are marked *