[凡文]Docker+Jenkins+Gradle+GitLab在Linux服務端自動化構建Android包

凡人657090發表於2018-07-26

概述

在linux伺服器上安裝docker和jenkins,專案倉庫在搭建的gitlab私服上,然後在windows或者mac上編寫專案,最後push程式碼到gitlab指定分支時,觸發jenkins去gitlab上拉取專案,進行構建。這裡主要記錄一下配置和構建的過程,而不是怎麼在linux中安裝docker和jenkins。

jenkins配置

在安裝好的jenkins中,建立一個新任務,名稱一般和專案名稱一致。然後選中pipeline,點選“確定”,就建立了一個新任務。 建立新任務後,會進入配置頁面:

  1. General:GitLab Repository Name欄目,輸入專案組名+專案名,如clients/android_client
  2. Build Triggers:選擇Build when a change is pushed to GitLab,意思是當push程式碼到指定的分支時,就觸發構建。Allowed branches:允許觸發構建的分支,選擇的是Filter branches by name,然後填寫允許觸發構建的分支名稱,必須是專案裡存在的分支名稱。
  3. Advanced Project Options:不用管
  4. Pipeline:選擇pipeline script frm SCM(source code manage);SCM(原始碼管理)選擇Git,Repository URL填寫專案所在倉庫地址,是https而不是ssh;Credentials填寫倉庫的賬號密碼;Branches to build填寫需要構建的分支名稱; Script Path,是jenkins指令碼的路徑,指令碼放在專案下,如buildsystem/ci/jenkins/Jenkinsfile,是一個相對於專案的相對路徑。

最後點選儲存,這樣jenkins裡就配置好了。這裡配置了去哪個倉庫地址拉取原始碼,但是還得有人告訴你什麼時間該拉原始碼進行構建,這就需要在gitlab裡配置。

gitlab配置

這裡主要配置一個webhook,可以簡單理解成一個通知器,當專案發生某些事件時,就通知jenkins,然後jenkins再根據配置的事件型別和傳來的事件型別是否一致來決定是否需要拉取原始碼進行構建。比如,我們在jenkins裡配置了push到master就會拉取原始碼進行構建,那麼如果通知過來的是push到develop分支的事件,jenkins就不會拉取原始碼進行構建,如果通知過來的是push到master分支的事件,jenkins就會拉取原始碼進行構建。

你的賬號必須要是這個專案的管理者,不然是沒有許可權進行配置webhook的。點專案,在左側欄裡找到Settings,選擇integrations進入到webhook的配置頁面:

  1. URL:這個是在jenkins裡建立的任務的地址,緊跟在Build Triggers的Build when a change is pushed to GitLab後面,當事件發生時,會向這個地址傳送事件(是一個post請求)。
  2. Secret Token:這個是token認證,驗證是不是合法請求;在jenkins配置中的Build Triggers->Build when a change is pushed to GitLab->高階->Secret token,點選Generate會自動生成一個token。
  3. Trigger:配置哪些事件會通知Jenkins,根據需要,這裡選擇了Push events。
  4. 點Add webhook儲存配置

至此,gitlab裡也配置好了,現在push原始碼到指定的分支話,gitlab會通知jenkins,jenkins會執行構建。但是用什麼來構建呢?就是用在jenkins的Pipeline裡配置的script path指令碼來執行構建。它指向的是一份Jenkinsfile指令碼。

Jenkinsfile指令碼

兩個jenkins基礎概念node和stage。node是一個節點,可以有多個節點,一般執行耗時操作時才需要另開一個節點;stage是一個階段,每個階段幹一件具體的事情;可以一個node下有一個或多個stage,也可以一個stage下有一個或多個node。沒有特別需要一般都是一個node下多個stage。

當一個正確的事件觸發時,jenkins會執行這份Jenkinsfile指令碼。去配置的gitlab倉庫中拉取原始碼,拉取來時預設是master分支,這個都知道。接下來的實現就需要在Jenkinsfile指令碼中來做:

  1. 構建一個docker映象,就是一個Android環境,因為專案需要在Android環境下才能夠執行。可以在Docker Hub上找一個Android基映象檔案,然後自己的Dockerfile繼承這個基映象(FROM beevelop/android:latest)。Dockerfile示例如下:
FROM beevelop/android:latest
MAINTAINER KuickDeal "xiaoge@kuick.cn"
#安裝expect
RUN apt-get update && apt-get install -y --force-yes expect
# 設定環境變數
ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools
# 建立目錄
RUN mkdir -p /opt/workspace
# 拷貝所有檔案到目錄下
COPY . /opt/workspace
# 切換目錄
WORKDIR /opt/workspace
# 配置環境變數
ENV PATH ${PATH}:/opt/workspace/buildsystem/ci/tools/
# 更新sdk,要和專案的編譯版本一致
RUN echo y | android update sdk --all --no-ui --filter build-tools-27.0.3,extra-android-m2repository,android-27
# Cleaning
RUN apt-get clean

複製程式碼

BuildPushDocker.sh(構建、推送docker映象的shell指令碼):使用docker build -t ${image_name} -f ${Dockerfile_path} ${workspace}來構建映象。構建好後用docker push ${image_name}命令把映象push到公共的倉庫,以便在不同的終端可以使用這個映象。${image_name}是由在Docker Hub註冊的使用者名稱/tag組成的,例如註冊的使用者名稱為test.docker,tag為1.5,則${image_name}為test.docker/1.5。至此已經構建好一個Android映象,並且推到了公共倉庫。

  1. PullDocker.sh(拉取docker映象的shell指令碼):通過docker pull ${image_name}拉取映象,因為不需要在每次構建Android包的時候都構建一次docker映象,所以第1步一般情況下只執行一次,只需要去公共倉庫拉取映象就可以。在專案編譯版本改變時,需要重新構建一個安裝了對應AndroidSDK版本的映象。

  2. AssembleApk.sh(構建apk的shell指令碼):

docker run -v ${workspace}:/opt/workspace ${container_name} ./gradlew build --info
複製程式碼

在物理機的{workspace}目錄和docker的/opt/workspace目錄做一個對映,建立一個新的docker容器{container_name},在容器中執行命令./gradlew build 來構建apk。{workspace}是gradlew和gradlew.bat所在的目錄,也就是專案的根目錄,在物理機中,但是沒有Android環境。所以需要把這個目錄和有Android環境的docker映象的目錄/opt/workspace做一個對映。至此,debug包和release包就構建出來了。

現在都希望能更好的保護公司的原始碼,所以會對apk做一些加固。市面上加固平臺有很多,有阿里安全、騰訊加固寶、愛加密、梆梆加固、360加固等。兩年前用的是梆梆加固,但是發現加固後在小米note手機上會出現崩潰,所以果斷放棄了。後來一直用愛加密到現在(2018.6),手動上傳到平臺去加密。後面需要做自動化,所以諮詢了上面各家加固平臺是否有自動化指令碼加固。梆梆加固、騰訊加固寶沒有,放棄。阿里安全、愛加密有,但是收費,費用還不便宜,愛加密單買自動化加固就得2w一年,所以也暫時不考慮。最後只有360加固有自動化指令碼加固,還免費,所以採納了它。

  1. 360加固,需要他們的加固工具,所以需要先下載加固工具。這一步涉及到多個指令碼,JiaGu360Exist.sh、DownloadUnzip360.sh、JiaGu360.sh。第一個指令碼是檢測有沒有下載360的加固工具,如果沒有下載,則用第二個指令碼下載並解壓加固工具,最後用第三個指令碼對apk進行加固。根據有沒有目錄來判斷有沒有下載加固工具,比如把加固工具解壓到./jg目錄下,那隻需要判斷./jg這個目錄是否存在就可以了:
   for file in $(ls ${curPath})
   do
       zipFile=`echo ${file} | grep 'jg'`
       if [[ ${zipFile} = "jg" ]]
       then
           echo ${zipFile}
           break
   fi
   done
複製程式碼

如果不存在,則說明沒有下載加固工具,下載的命令是直接寫在了Jenkinsfile裡,也可以寫成一個shell指令碼,Jenkinsfile是用groovy語言編寫:

def download = sh(script: "wget -O jiagubao.zip -P ./ http://down.360safe.com/360Jiagu/360jiagubao_linux_64.zip", returnStdout: true).toString().trim()
echo "download=${download}"
// 解壓到./jg目錄下
sh "sudo unzip -d ./jg ./jiagubao.zip"
// 刪除加固工具壓縮包
sh "sudo rm ./jiagubao.zip"
複製程式碼

最後是加固:

//登入360開放平臺
loginR=`sudo ${base} -login ${name} ${pw2}`
echo "loginR=${loginR}"
//匯入簽名資訊,加固後自動簽名
keystoreR=`sudo ${base} -importsign ${ccPath} ${pw} ${alias} ${pw}`
echo "keystoreR=${keystoreR}"
//修改需要的加固服務
updateR=`sudo ${base} -config -x86`
echo "updateR=${updateR}"

chmod +x ${curScriptDir}/FindDebugApk.sh
chmod +x ${curScriptDir}/FindReleaseApk.sh
//建立加固後的debug和release包儲存路徑
sudo mkdir ${debugEnhancePath} ${releaseEnhancePath}
//找到需要加固的debug包
debugApk=`${curScriptDir}/FindDebugApk.sh`
echo "debugApk=${debugApk}"
//加固debug包
enhanceDebugR=`sudo ${base} -jiagu ${debugPath}${debugApk} ${debugEnhancePath} -autosign`
echo "enhanceDebugR=${enhanceDebugR}"
////找到需要加固的release包
releaseApk=`${curScriptDir}/FindReleaseApk.sh`
echo "releaseApk=${releaseApk}"
//加固release包
enhanceReleaseR=`sudo ${base} -jiagu ${releasePath}${releaseApk} ${releaseEnhancePath} -autosign`
echo "enhanceReleaseR=${enhanceReleaseR}"
複製程式碼

至此,加固後的apk就出來了,但是現在apk是在伺服器上的,測試組或者其他人員都無法拿到這個apk,所以需要一個公共的地址,來存放這個apk。可以是自己的伺服器下載地址,也可以是其他開放的下載地址。這裡選擇的是蒲公英

  1. UploadDebugApk.sh和UploadReleaseApk.sh,上傳加固包到蒲公英的指令碼。 上傳命令
uploadResult=`curl -F "file=@${apk_path}" -F "uKey=${userKey}" -F "_api_key=${apiKey}" "http://www.pgyer.com/apiv1/app/upload"`
複製程式碼

上傳完後,我們需要做最後一步,就是通過郵件通知相關人員,比如測試人員。把蒲公英下載地址通過郵件傳送給他們。

  1. 傳送郵件,直接在Jenkinsfile裡定義函式,jenkins自帶發郵件功能:
// 發郵件
def sendMail(String to,String title, String body){
    mail([
            bcc: '',
            body: body,
            cc: '',
            from: '',
            replyTo: '',
            subject: title,
            to: to
         ]);
}
複製程式碼

到此,整個自動化構建、打包、加固apk就完成了。 Jenkinsfile的結構如下:

node('node_name'){

    stage("刪除build目錄"){
        //切換到jenkins裡配置的需要構建的分支,預設拉取的是master分支
        checkout scm
        //刪除build目錄,避免資源更換後,快取未更新,導致構建錯誤
        sh "sudo rm -rf app/build"
    }

    stage("準備映象") {
    // 不用每次都構建映象
//        sh "buildDocker.sh"
        sh "pullDocker.sh"
    }

    stage("構建包") {
        try{
            sh "assembleAPK.sh"
        }catch(Exception e){
            sendMail(mySelfEmail, 'Android安裝包構建失敗', e.message)
        }
    }

    stage("加密包"){
        try{
            // 檢測是否下載了360加固工具
            def exist360 = sh(script: "JiaGu360Exist.sh", returnStdout: true).toString().trim()
            if ("jg" != exist360){
                // 檢視wget下載工具版本,未安裝會拋異常
                def result = sh(script: "wget -V", returnStdout: true).toString().trim()
                // 下載360加固工具,重新命名為jiagubao.zip,下載到當前目錄./
                def download = sh(script: "wget -O jiagubao.zip -P ./ http://down.360safe.com/360Jiagu/360jiagubao_linux_64.zip", returnStdout: true).toString().trim()
                // 解壓到./jg目錄下
                sh "sudo unzip -d ./jg ./jiagubao.zip"
                // 刪除加固工具壓縮包
                sh "sudo rm ./jiagubao.zip"
            }
            sh "JiaGu360.sh"
        }catch(Exception e){
            sendMail(mySelfEmail, '360加密包失敗', e.message)
        }
    }
    
    stage("確認上傳測試包"){
        input message: "確認上傳測試包?"
    }
    
    stage("上傳測試包") {
        try{

            def code = sh(script: "uploadTestApk.sh", returnStdout: true).toString().trim()
            if(code == null || code != "0"){
                sendMail(mySelfEmail, 'Android測試包上傳失敗', "code=${code}")
            }else{
                //從readme檔案中讀取apk的更新內容
                def content = sh(script: "ReadUpgradeInfo.sh", returnStdout: true).toString().trim()
                sendMail(testGroupEmail, 'Android測試包下載地址', "https://www.pgyer.com ${content}")

            }
        }catch(e){
            sendMail(mySelfEmail, 'Android測試包上傳失敗', e.message)

        }
    }

    stage("確認上傳正式包"){
        input message: "確認上傳正式包?"
    }
    
    stage("上傳正式包") {
  
    }

}
// 發郵件
def sendMail(String to,String title, String body){
    mail([
            bcc: '',
            body: body,
            cc: '',
            from: '',
            replyTo: '',
            subject: title,
            to: to
         ]);
}
複製程式碼

完結。

Tips:

  1. 如裡出現Permission Denied,在執行指令碼前用 chmod +x {shell_path}給指令碼加上執行許可權。
  2. 有問題,可以給xiaoge@kuick.cn發郵件,一起探討

相關文章