Android 在 Gitlab Pipelines 那一兩件事

Jast Lai
Jastzeonic
Published in
17 min readDec 24, 2019

估計發文時間是聖誕夜前後,總之先來首歌吧。

真是讓人煩躁的一天。

前言

為什麼會想研究這個玩意呢?故事是這樣的:某天下班前,PM 跟我要了 APK ,我按照平常的習慣,包了包 APK 給他,拎著包包就下班去了,因為我待會跟人有約,急著要離開。在前往赴約的路上,PM 送了訊息給我,說是沒辦法安裝,我悶了一下,我在離開前忘記檢查 APK 容量,估計是一個不小心包壞了,但我手邊沒電腦,只能在百般無奈下,雙手一攤,說至少等到我手邊有電腦的時候…

其實我當下有個想法是,告訴 PM ,去把我電腦打開,然後開啟 Android Studio (以下略。

後來,我來到了公司,PM 直接把測試機拿來要我裝,我打開 Android Studio ,發現 adb 的 network connect 又斷了,因為電腦關機或進修眠模式一段時間後會自己斷掉,我打開了 Terminal ,下了 adb connect $(ip) ,adb 跟我說被拒,看來是手機自己重開機過了,把 port:5555 關掉了…結果最後又要接線,更重要的是,主要是 PM 沒有手段可以把 apk 弄到那台測試機上,所以才來找我的,想想,教他怎麼接到電腦上好像會比較省事…。

我想了一下,其實我包 raw apk 其實也只是下個 gradlew assembleDebug,然後再把生出來的 apk 用 Slack 丟給 PM 的,那其實測試機只要有 Slack 就可以安裝 apk 了,這樣我也就不用接線去裝測試機了。這一整段過程其實都是可以自動完成的,只要我寫個 Script 就行了,這樣我只要有人來哎的時候,下個指令,就行了。

完美的計劃。

但是,碰到上星期那個情況…我人不在我電腦前,難道我要叫 PM 去開電腦,然後下指令嗎?

完美的計劃。

當然這提案很快就被否決了,理由是 PM 把專案抓下來在自己的電腦上下指令更快,那當然這個提案也只得到一個句點,然後就沒然後了,可…可惡阿。

其實到這邊需求也很明確了:我要的是當我和 PM 或任何人手邊沒電腦時,可以一個簡單的動作把 APK build 出來,然後能夠迅速裝到手機上。

我起初想到的是架一台 Jenkins ,但是公司裡機子搶得很兇,非常兇,身為一個小小的 Android 工程師,要用「因為接線太麻煩,所以幫我開一台機子」當作理由去要機器,大概等到真把抽屜裡的離職單真交出去了都還拿不到,以我這邊的情況來說,架在自己 Local 端跟我寫 script 自己 run 的差別在於,我會開一個 port 給人連,然後可以按個按鈕完成,但是這樣我的電腦就得一直開著了,其實好像也不是不行,但是總覺得會有很多的地麻煩。

恰巧,公司用的是 Gitlab ,那其實在看 pr 的時候,就常會看到有個 CI/CD 的欄位在那邊空著,我想想,不然就用一下試試看好了。

那麼就開始吧

在這裡,我有個 Android 專案,我把他切到 pipelines 的選項去發現:

空空如也,按了一下正中間的 Get started with Pipelines ,也只是顯示一堆英文而已,根本也沒開始,到這裡我放棄了,因為我寫到這邊我的目的也達到了,我只是要在聖誕夜散播怨念而已~

#゚Å゚)⊂彡☆))゚Д゚)・∵

呃,實際上 GitLab 的 CI/CD 要用一個 yml 檔去建立,只要你在該專案的根目錄下新增一個 .gitlab-ci.yml ,然後推上 repo 就行了。

這裡我新增了一個 yml 檔案,然後 commit、push,再回來看看 GitLab 的 Pipelines:

出現惹,而且還自己跑了起來。

Pipeline 會觸發的時機點很簡單,就是 Push 一個 Commit 或者是 PR (在 GitLab 叫 Merge Request) 被 Accept 的時候。

就這樣啦,收工。

⊂彡☆))Д′)

我做了甚麼

原則上照我現在的設定是,只要一 push,就會自己開始跑,這樣肯定有問題的,那這個待會再說明,我們先從 yml 開始說起:

image: openjdk:8-jdk

variables:
ANDROID_COMPILE_SDK: "29"
ANDROID_BUILD_TOOLS: "29.0.2"
ANDROID_SDK_TOOLS: "4333796"

before_script:
- apt-get --quiet update --yes
- apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
- wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip
- unzip -d android-sdk-linux android-sdk.zip
- echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null
- echo y | android-sdk-linux/tools/bin/sdkmanager "platform-tools" >/dev/null
- echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null
- export ANDROID_HOME=$PWD/android-sdk-linux
- export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/
- chmod +x ./gradlew
# temporarily disable checking for EPIPE error and use yes to accept all licenses
- set +o pipefail
- yes | android-sdk-linux/tools/bin/sdkmanager --licenses
- set -o pipefail

stages:
- build

Debug:
stage: build
script:
- ./gradlew assembleDebug
artifacts:
paths:
- app/build/outputs/apk/debug/*.apk

這是我方才推上去的 yml ,雖然中間很多疑似 pointer 的符號,但是瞭解了真的不難,我這邊簡單做點解釋:

image: openjdk:8-jdk

原則上這句是在告訴 GitLab Runner ,要使用甚麼 Docker image ,GitLab Runner 甚麼玩意,就是跑 Pipelines 的東西,那其實這個 image 裡面除了 java 以外甚麼都沒有,單純這樣做會造成一些問題,這就是後續的課題了,如果只是試試身手的話,這樣寫無傷大雅,稍後再多做說明。

variables:
ANDROID_COMPILE_SDK: "29"
ANDROID_BUILD_TOOLS: "29.0.2"
ANDROID_SDK_TOOLS: "4333796"

那因為要建置的是 Android ,所以要那個一大坨的 Android SDK ,因為環境設定指令很多,版本混雜在裡頭怕混亂,所以把它弄成變數。

before_script:
- apt-get --quiet update --yes
- apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
- wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip
- unzip -d android-sdk-linux android-sdk.zip
- echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null
- echo y | android-sdk-linux/tools/bin/sdkmanager "platform-tools" >/dev/null
- echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null
- export ANDROID_HOME=$PWD/android-sdk-linux
- export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/
- chmod +x ./gradlew
# temporarily disable checking for EPIPE error and use yes to accept all licenses
- set +o pipefail
- yes | android-sdk-linux/tools/bin/sdkmanager --licenses
- set -o pipefail

那這邊就是拉 SDK 阿、安裝 SDK 阿、設定 gradle 指令權限的動作了。

stages:
- build

Debug:
stage: build
script:
- ./gradlew assembleDebug
artifacts:
paths:
- app/build/outputs/apk/debug/*.apk

這段就是 GitLab Pipelines 的精華了,這裡也是為什麼 GitLab 的 CI/CD 叫 Pipeline 的原因 。

這邊我先解釋幾個名詞,GitLab 的一個專案裏頭一整套 CI/CD 是一個 Pipelines,而 Pipelines 底下則會有多個 Stages ,每個 Stages 底下則會有各式不同的 Job。

Pipelines 在我們這邊管它叫做管線,那管線有個特色,就是某處堵住,流體就沒有辦法向下流了,但同時也能有複數的管線同步進行。

那 Stage 指得就是階段,以上面的例子來說,我如果加上 unit test 然後再做通知,可能會寫這成這樣:

stages:
- build
- test
- notify

那這樣就有三個 stage,那 stage 具體在做什麼呢?

Debug:
stage: build
script:
- ./gradlew assembleDebug
artifacts:
paths:
- app/build/outputs/apk/debug/*.apk

Debug 指得是該 Job 的名稱,stage 指得是他運行的階段,對應的就是 stages 的名字,那像這個 job 對應的就是 stages 的 build。s 的意思是複數,也就是說 build 可以對應多個,比如說,我有三個版本 Debug 、 Stage、 Release這邊我可能會寫成這樣:

Debug:
stage: build
script:
- ./gradlew assembleDebug
artifacts:
paths:
- app/build/outputs/apk/debug/*.apk
Stage:
stage: build
script:
- ./gradlew assembleStage
artifacts:
paths:
- app/build/outputs/apk/stage/*.apk
Release:
stage: build
script:
- ./gradlew assembleRelease
artifacts:
paths:
- app/build/outputs/apk/release/*.apk

那個三個 Job 會在跑 build 的時候同時進行。

在 stage 下,可以看到一個叫做 script 的玩意,他的底下是寫 Android 的人多半不會太陌生的 ./gradlew assembleDebug。從結果來說,這個其實就是指令集,原則上,會在 Terminal 手動下的指令,寫在這裡就是了。

Debug:
stage: build
script:
- ./gradlew assembleDebug
- ./run_some_test
- ./become_as_God
artifacts:
paths:
- app/build/outputs/apk/debug/*.apk

照上面寫的,這裡會逐一執行,先是 assemble 再來 run

artifacts 則是成品,有設定這行的話,就可以在 pipelines 找到這個:

代表 GitLab 會幫你把你指定檔案連同指定目錄打包成 Zip 檔,在這裡提供下載。

還有幾個問題

剛才有提到,只要每次 Push 每次 Accept PR,就會自動開始 Run 起來,是說反正 GitLab 免費,就讓他去跑也無所謂,但是如果像我一樣有加通知的話,每次推每次都有消息,會有這麼點尷尬。這時我會希望他只在我動到某個 branch 的時候才去 run 該怎麼辦?比如說,我常常更動其他的 branch ,但我只希望他在 master 被更新時候才去 Run,這個情況可以用 only:

Debug:
stage: build
only:
- master

script:
- ./gradlew assembleDebug
artifacts:
paths:
- app/build/outputs/apk/debug/*.apk

這樣在 master 被更新時, GitLab Runner 才會做動。

然後最要命的問題是,如果像我一樣,yml 裏頭只寫了:

image: openjdk:8-jdk

會發現一個駭人的事實:每次 Run 每次都要抓一次新的 Android SDK。

這是理所當然的,因為 openjdk:8-jdk 裡只有 jdk,沒有 Android SDK,故然,每次 Run 一次新的,都要重新裝一次 SDK;都要重新 Accept 一次條款,如此就算了,最重要的是: Hen 慢。

每次被索討 APK ,都要坐在電腦前面當塑膠,有點這麼不是滋味。

環境設定一項是起頭最擾人的地方,反正不論如何,這點似乎都避不掉。

在 GitLab 左邊這排可以注意到下邊有個叫做 Packages 裏頭有個 Container Registry 的,點進裏頭,長這樣:

呃…好那就照著指令去下吧。首先要安裝 Docker ,這個我就不多說了。

docker login registry.gitlab.com

這步沒有問題,原則上就是登入的作業,輸入帳號密碼,或者是有 Gitlab 的 Access token 就能登入了

docker build -t registry.example.com/group/project/image .

這一步像是我這樣對 Docker 不熟的通常會碰到問題,這邊會噴一個錯誤,說是找不到 Dockerfile,那顯然這邊要寫一個 Dockerfile,用以生成可以用來 build Android 環境的 container:

FROM openjdk:8-jdk# Set Enviorment
ENV SDK_URL="https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip" \
ANDROID_HOME="/usr/local/android-sdk" \
ANDROID_VERSION=29 \
ANDROID_BUILD_TOOLS_VERSION=29.0.2
# Download Android SDK
RUN mkdir "$ANDROID_HOME" .android \
&& cd "$ANDROID_HOME" \
&& curl -o sdk.zip $SDK_URL \
&& unzip sdk.zip \
&& rm sdk.zip \
&& mkdir "$ANDROID_HOME/licenses" || true \
&& echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license"
# && yes | $ANDROID_HOME/tools/bin/sdkmanager --licenses
# Install Android Build Tool and Libraries
RUN $ANDROID_HOME/tools/bin/sdkmanager --update
RUN $ANDROID_HOME/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \
"platforms;android-${ANDROID_VERSION}" \
"platform-tools"

那 Dockerfile 會是一個空擋,所以做完最好先 touch 一下,弄完之後,再跑一次方才的指令完以後。

要抓不少東西,得等一下

等它完成之後,再下這個指令:

docker push registry.example.com/group/project/image

接著就讓他跑一下吧。

跑完後就能在 Container Registry 看到剛才推上來的 image 惹

那再來呢?要去改 yml 檔,因為已經有 Android Sdk 了,所以不需要再抓那些 Sdk ,而且,也需要把 image 換成這個 Container:

image: registry.example.com/group/project/image:latest

stages:
- build

Debug:
stage: build
script:
- ./gradlew assembleDebug

artifacts:
paths:
- app/build/outputs/apk/debug/*.apk

這樣,少了抓取 SDK 的動作,應該就可以讓 build 快上不少了。

比起之前還要再抓 SDK ,這次 Build 大約只需要兩分鐘,因為還有下載 Gradle file,所以估計是可以再更快。

結語

GitLab 的 Pipelines 用起來比想像中簡單,如果沒有碰到 Docker 這塊的話其實可以很快就搞定,只是後來我發現了每次 Run 都有這麼一點久,細看才發現每次都要下載一次 SDK,真的是很歡樂,後來才看了一下 Contianer 要怎麼弄,其實以優先順序,我會更願意先用 Jenkins ,因為相對熟悉些,但基於周邊環境的考量,後來還是用 GitLab 來搞,算是淺淺的研究了一下。此外還有碰到 APK 簽署的問題,因為目前只有build debug 版的需求,所以先丟一個測試用的簽署金鑰暫時沒問題,待要正式上版時,還有金鑰保存的問題要解決。

寫著寫著也快到聖誕節了,不知道各位今年聖誕夜是怎麼過的,至少我走悲愴路線,窩在家裡獨自一人寫 Medium (深深的怨念)。

如果有任何問題,或是看到寫錯字,請不要吝嗇對我發問或對我糾正,您的回覆和迴響會是我邊緣人我寫文章最大的動力。

--

--

Jast Lai
Jastzeonic

A senior who happened to be an Android engineer.