Android自動化打包記錄--Jenkins+Docker+WSL2

語言: CN / TW / HK

theme: smartblue

前言

在面向海外的專案組辛勤耕耘了兩年,今年被調到了國內的開發組,很多東西突然感覺有些陌生了起來。首先接到的第一個任務就是打包自動化的工作,因為國內的專案組目前有多個app在同時開發,提測的時候人工打完測試包上傳到三方平臺,然後傳送釘釘通知告知測試人員。到生產環境的時還需要打包、加固、重簽名,再處理多渠道問題,最後還需要手動上傳mapping檔案到Bugly等平臺,整個一套流程夠複雜,並且也相當浪費時間,多個app處理起來更是繁瑣。

所以,把這件事交給機器去做就是我們的終極目的。其實這件事情整體做下來更像是運維的工作,但是呢,作為一個開發工程師我學(卷)一點運維的內容不過分吧。整體內容圍繞Jenkins + Docker來進行闡述,如有紕漏或錯誤,還請各位幫忙斧正。

注: 由於編寫該文件時,360加固免費版還是支援命令列的方式使用的,但是現在免費版已經不支援命令列的操作了,如果使用則需要購買加固專業版,或者成為企業版使用者。所以現在情況下,我又寫了一個桌面端的工具來完成後續步驟,文章參考《使用ComposeDesktop開發一款桌面端多功能APK工具》。

自動化流程

先把我們前述的需求分類整理下,大致分為測試環境和生產環境,具體的流程步驟如下:

測試環境

在測試環境下,主要流程如下:

  1. 打出測試包
  2. 上傳到公司內網伺服器
  3. 生成apk的下載二維碼(qrencode)
  4. 獲取Git日誌的提交記錄
  5. 最後使用釘釘機器人通知到群即可

測試環境的整個自動化過程還是相對簡單的。

注:在之前我們是上傳到fir.im或者蒲公英這樣的應用內測託管平臺上的,然而由於最近一段時間貌似稽核比較嚴重,動不動新的app就會被提示違規然後不給下載,所以正好藉此機會捨棄了三方平臺,轉而使用內網伺服器。

生產環境

生產環境的流程就複雜多了,主要流程如下:

  1. 打出生產包
  2. 加固(加固方案這裡示例的是360加固,其他比如騰訊樂固等,大家可以自行選擇)
  3. 重簽名(可以使用360命令列重簽名,也可以自行重簽名)
  4. 生成渠道包(這一步採用的是VasDolly方案,其他如Walle等,大家可以自行選擇)
  5. 渠道包生產完畢後就可以將渠道包儲存到伺服器上或上傳到其他三方平臺上提供下載連結了
  6. 將生成的mapping檔案上傳到崩潰分析的平臺即可,這裡是騰訊的bugly
  7. 傳送釘釘通知告知相關人員

自動化原理

整體的流程已經分析完了,那麼如何實現呢?

Jenkins!在Jenkins中我們可以編寫pipeline指令碼來處理上述步驟,免去了人工操作的煩惱。Jenkins的格言:

使開發者從繁雜的整合中解脫出來,專注於更為重要的業務邏輯實現上

接下來我們先著重看下打包這個步驟,光是打包我們就需要配置java環境、gradle環境、android sdk/ndk等等,如果這套自動化的工具單部署到一臺伺服器上還則罷了,要是再多部署幾臺,那光是配置這一套環境就能把人逼瘋了,怎麼處理呢?。

Docker!Docker允許我們把這些配置的內容統統封裝起來,做成映象檔案。哪裡有需要就下載這個映象,然後在容器中執行該映象,這樣就能提供出來一套跟開發一模一樣的環境,然後在其中使用正常的gradle打包命令就可以了。

接下來我們就在Linux的環境下,安裝Jenkins和Docker來一步步實現我們的自動化流程。

Windows下安裝Ubuntu

因為我的電腦系統是Windows 11,為了方便我直接採用了WSL2的方案。

安裝步驟

首先在搜尋中輸入“啟用或關閉Windows功能”,然後再彈框中勾選如下兩項,然後最好重啟電腦: Snipaste_2022-05-30_23-48-00.png 開啟Microsoft Store,搜尋ubuntu,這裡我選擇Ubuntu 20.04.4 LTS版本進行了安裝。

安裝完畢後開啟Ubuntu過程中可能會遇到各種奇奇怪怪的問題,如果有,請參考下文相關方案。

相關error處理

error: 0x8007019e

Installing, this may take a few minutes... WslRegisterDistribution failed with error: 0x8007019e The Windows Subsystem for Linux optional component is not enabled. Please enable it and try again. See https://aka.ms/wslinstall for details. Press any key to continue...

以管理員許可權開啟Window PowerShell,輸入以下程式碼,然後按 Y 確定,重啟系統: Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

error: 0x800701bc、0x80370102

Installing, this may take a few minutes... WslRegisterDistribution failed with error: 0x800701bc Error: 0x800701bc WSL 2 ?????????????????? https://aka.ms/wsl2kernel

Press any key to continue...

前往微軟WSL官網下載安裝適用於 x64 計算機的最新 WSL2 Linux 核心更新包安裝即可。 https://docs.microsoft.com/zh-cn/windows/wsl/install-manual#step-4---download-the-linux-kernel-update-package

WSL訪問Windows

主要是mnt,表示掛載: ```shell //進入Windows下E盤

cd /mnt/e ```

WSL訪問內網

需要設定埠轉發: ```groovy //設定埠轉發 netsh interface portproxy add v4tov4 listenport=【宿主機windows平臺監聽埠】 listenaddress=0.0.0.0 connectport=【wsl2平臺監聽埠】 connectaddress=【wsl2平臺ip】

//刪除埠轉發
netsh interface portproxy delete v4tov4 listenport=【宿主機windows平臺監聽埠】 listenaddress=0.0.0.0

//檢視埠轉發狀態 netsh interface portproxy show all ```


Ubuntu下Docker內容

官方網址:https://docs.docker.com/desktop/linux/install/

安裝

如果按照官方步驟執行失敗的話,可以參考如下步驟: 首先更新軟體包索引,然後新增新的HTTPS軟體源: shell sudo apt update sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common

然後匯入源倉庫的GPG key: shell curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

將Docker APT軟體源新增到系統: shell sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

現在可以檢視Docker軟體源中的所有可用版本了: shell apt list -a docker-ce

安裝: ```shell //安裝最新版本 sudo apt install docker-ce docker-ce-cli containerd.io

//安裝指定版本 sudo apt install docker-ce= docker-ce-cli= containerd.io ```

驗證安裝,如果成功輸出docker的版本號,表示安裝成功: shell docker -v

執行HelloWorld

在執行hello-world之前需要先啟動docker服務,否則報錯如下:

docker: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/create": dial unix /var/run/docker.sock: connect: permission denied. See 'docker run --help'.

啟動docker命令如下: shell sudo service docker start 然後執行docker的hello-world,驗證是否安裝成功: shell sudo docker run hello-world

拉取映象

以拉取jdk8映象為例: shell sudo docker pull openjdk:8-jdk-oracle

映象和容器命令

```shell //顯示出所有的映象 sudo docker images

//-it表示以互動式執行該映象 sudo docker run -it 映象ID

//列出所有的容器 sudo docker ps -a

//啟動停止容器 sudo docker start/stop 容器ID

//列出正在執行的容器 sudo docker ps

//以互動式進入正在執行的容器 sudo docker exec -it 容器ID /bin/bash

```

建立映象

瞭解瞭如何使用映象後,我們現在可以嘗試建立自己所需要的映象了,根據上文的流程我們先從簡單的映象建立說起,然後再一步步建立Android打包所需的複雜的映象。建立映象需要我們編寫Dockerfile指令碼,一些常用的指令碼指令可以在官網中找到,請參考《編寫Dockerfile的最佳實踐》。

為了方便建立映象,我在Windows上也安裝並啟動了Docker然後使用IntelliJ IDEA組織相關程式碼和資源,同時IDEA還需要安裝一下Docker外掛。一切準備就緒後我們這就開始製作映象了。 image.png

建立VasDolly映象

VasDolly需要在JDK8的環境下使用,那麼有兩種方式:

  • 使用ubuntu作為基礎映象,自行安裝jdk並配置環境
  • 直接使用jdk8的基礎映象

我們使用第一種方式做為演示,首先工程結構如下所示: image.png 在VasDolly資料夾下,我們有jdk-8u333-linux-x64.tar.gz以及VasDolly.jar、Dockerfile檔案。

Dockerfile指令碼的內容如下: ```dockerfile

指定基礎映象

FROM ubuntu:20.04

新增檔案到容器中

ADD jdk-8u333-linux-x64.tar.gz /home/jdk/ ADD VasDolly.jar /home/vasdolly/

JDK會自動解壓,直接配置環境變數

ENV JAVA_HOME /home/jdk/jdk1.8.0_333 ENV JRE_HOME $JAVA_HOME/jre ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH ENV PATH $JAVA_HOME/bin:$PATH

傳送釘釘機器人訊息所需

RUN apt update && apt install -y curl

執行指令

CMD ["java", "-jar", "/home/vasdolly/VasDolly.jar", "help"] ``` 我們以ubuntu20.04版本作為基礎映象,然後新增JDK和VasDolly檔案到映象中,並配置JDK的相關環境變數,最後又安裝了傳送釘釘訊息所需的curl元件。

注意執行指令的區別:

  • CMD 在Docker Run 時執行。
  • RUN 在 Docker Build時執行。

Dockerfile指令碼編寫完畢後我們就可以,執行指令碼來建立映象了,這裡也有兩種方式可以建立映象:

  • 直接點選Dockerfile中的執行按鈕
  • 在VasDolly資料夾下執行docker的建立映象指令

第一種方式很簡單了,點選按鈕等待建立映象就好了。如果想練習Docker指令,那麼切換到VasDolly目錄下執行建立映象的指令即可。注意:注意最後一個引數是上下文路徑,由於我們有拷貝檔案的操作,所以用點則表示當前資料夾路徑。 ```shell sudo docker build -t [映象名]:[映象TAG] [上下文路徑]

例如

sudo docker build -t vasdolly:0.1 . ``` 然後映象打包成功後,我們可以用互動式命令執行該映象,當容器啟動後就可以看到控制檯輸出的VasDolly的幫助資訊了。

建立Bugly映象

該映象主要用於將mapping符號表上傳到bugly後臺,其同樣要求是基於JDK8版本,官方文件見《Bugly Android 符號表配置》 。那麼這次呢我們就使用DockerHub上的openjdk:8-jdk-oracle基礎映象,免去了自行配置JDK環境的煩惱。Dockerfile檔案如下,編寫完畢後執行建立映象指令即可,對比上面的真是非常的簡單粗暴且好使: ```dockerfile

指定基礎映象

FROM openjdk:8-jdk-oracle

新增檔案到容器中

ADD buglyqq-upload-symbol.jar /home/Bugly/

執行指令(Bugly沒有該指令,執行會出現報錯資訊,僅僅為驗證映象的正確性)

CMD ["java", "-jar", "/home/Bugly/buglyqq-upload-symbol.jar", "help"] ```

建立Android打包映象

Android打包需要JDK、Gradle、Android SDK、NDK(非必須)等工具,所以我們需要將這些東西統統打包進映象中。

本來使用的基礎映象是ubuntu:20.04,然後自己手動配置上述環境,但是後面發現這種方式比較麻煩,而且映象體積也比較大,所以後續採用了官方的grale-jdk作為了基礎映象,然後我們只需要配置Android SDK就好了。從官網下載cmdlinetools檔案:https://developer.android.google.cn/studio/ 。然後使用sdkmanager安裝build-tools以及platforms等檔案。注意,需要使用--sdk_root來指定SDK儲存的路徑。

還需要注意的一個問題就是,Gradle下載依賴後的快取問題,參考文章:https://zwbetz.com/reuse-the-gradle-dependency-cache-with-docker/ 。官方文章:https://docs.docker.com/develop/develop-images/multistage-build/ 。Docker映象是一個很純淨的環境,所以每次執行如果不快取依賴檔案,那麼每次執行都會重新下載,非常耗費時間。在製作映象時,我們建立gradle等的快取目錄,然後在Docker中掛載到本地目錄。

```dockerfile

指定基礎映象

FROM gradle:6.5.0-jdk11

安裝需要的元件,解壓

RUN apt update && apt install -y zip \ && apt install -y curl \ && apt install -y qrencode \ && apt install -y lftp \ && mkdir -p /usr/mylib/cmdlinetools \ && mkdir -p /usr/mylib/androidsdkhome \ && chmod 777 /usr/mylib/androidsdkhome \ && mkdir -p /usr/mylib/androidsdkroot \ && chmod 777 /usr/mylib/androidsdkroot \ && mkdir -p /usr/mylib/gradlecache \ && chmod 777 /usr/mylib/gradlecache

新增檔案到容器中

ADD cmdline-tools.zip /usr/mylib/cmdlinetools

配置SDK環境變數

ENV ANDROID_SDK_HOME /usr/mylib/androidsdkhome ENV PATH $ANDROID_SDK_HOME:$PATH ENV ANDROID_SDK_ROOT /usr/mylib/androidsdkroot ENV PATH $ANDROID_SDK_ROOT:$PATH

配置Gradle的環境變數,配置快取路徑(如果不進行配置,會在專案的根目錄下建立?資料夾,可能導致編譯異常)

ENV GRADLE_USER_HOME /usr/mylib/gradlecache ENV PATH $GRADLE_USER_HOME:$PATH

Android命令列工具解壓,配置環境,否則無法使用sdkmanager命令

WORKDIR /usr/mylib/cmdlinetools RUN unzip cmdline-tools.zip \ && chmod 777 cmdline-tools/bin/sdkmanager \ && rm cmdline-tools.zip ENV PATH /usr/mylib/cmdlinetools/cmdline-tools/bin:$PATH

下載平臺工具 (目前platform28,buildtool29)

RUN yes | sdkmanager --sdk_root=/usr/mylib/androidsdkroot "build-tools;29.0.2" \ && yes | sdkmanager --sdk_root=/usr/mylib/androidsdkroot "platforms;android-28"

執行指令

CMD ["gradle", "-v"] ```

上傳映象

如果你想上傳到官方的Docker Hub交友網站也是可以的,這裡為了減少網路環境的影響,還是直接白嫖了阿里雲。

首先我們需要註冊一個阿里雲賬號,記錄賬號密碼,然後開通映象容器服務(免費的),建立映象名稱空間,準備好後就可以上傳我們製作好的映象了(這裡一筆帶過了,相信對大家都不是問題,如果具體流程不清楚的可以百度): ```shell

登入阿里雲賬號,回車後需要輸入密碼

sudo docker login --username=賬號名 registry.cn-hangzhou.aliyuncs.com

建立TAG

sudo docker tag 映象ID registry.cn-hangzhou.aliyuncs.com/阿里雲映象名稱空間/映象名:版本號 sudo docker tag 52f503ef1474 registry.cn-hangzhou.aliyuncs.com/vsdragon/vasdolly:0.1

上傳映象

sudo docker push registry.cn-hangzhou.aliyuncs.com/阿里雲映象名稱空間/映象名:版本號 sudo docker push registry.cn-hangzhou.aliyuncs.com/vsdragon/vasdolly:0.1 ```

相關問題

WSL2下Ubunt無法啟動Docker

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

解決方案:在Windows中以管理員身份執行ubuntu。

Windows下 absolute path問題

Failed to run image 'xxx'. Error: docker: Error response from daemon: the working directory'C:\Users\xxx.jenkins\workspace\sample' is invalid, it needs to be an absolute path. See 'docker run --help'.

在Windows下執行Jenkins帶docker的指令碼,報錯如上。

參考方案:參考https://github.com/jenkinsci/docker-workflow-plugin/pull/184 ,但是對我來說還沒有辦法解決,所以採用的是Windows下WSL2的方案。

Gradle快取位置

image.png 製作Android打包映象時,如果不指定Gradle的快取目錄,那麼在執行Pipeline指令碼的時候,Gradle下載的依賴快取位置則為Jenkins Job下根目錄的【?】資料夾中!大部分的依賴可能沒有問題,但是有些情況下,讀取這個問號就出現了轉義的情況:

net.lingala.zip4j.exception.ZipException: java.io.FileNotFoundException: /var/lib/jenkins/workspace/Sample/%3F/.gradle/caches/modules-2/files-2.1/......

問號被轉換為了%3F,這時候讀取某些依賴就失敗了,進而導致專案編譯失敗。

解決方案:製作映象的時候務必手動指定下gradle的快取目錄,即配置GRADLE_USER_HOME環境變數,注意不要帶特殊符號等,不要給自己找麻煩!!!

引起的其他問題:當進行上述處理後,在後續進行gradle的編譯時,因為使用的是Docker,每次都會重新下載快取,所以我們還需要在pipeline的指令碼中指定本機的目錄掛載到上述的快取目錄。示例指令碼如下: shell agent { docker { image 'registry.cn-hangzhou.aliyuncs.com/vsdragon/android-builder:0.7' //掛載本地目錄 args '-v /usr/mylib/gradlecache:/usr/mylib/gradlecache' } } 然後本機目錄也要賦予讀寫許可權,否則報錯如下:

  • What went wrong: Gradle could not start your build. Could not initialize native services.
    Failed to load native library 'libnative-platform.so' for Linux amd64.

Ubuntu下Jenkins內容

官方網址 :https://www.jenkins.io/ linux下安裝方案: https://www.jenkins.io/doc/book/installing/linux/

安裝Java環境

Jenkins需要java的環境,所以需要先安裝java: ```shell //安裝JDK sudo apt update sudo apt install openjdk-8-jre java -version

//解除安裝JDK sudo dpkg --list | grep -i jdk sudo apt-get purge jdk sudo apt-get purge icedtea- jdk-* ```

安裝Jenkins

官方指令碼如果有問題,請使用如下指令碼安裝: ```shell //匯入Jenkins軟體源相關 wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -

//新增軟體源到系統中 sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'

//升級apt軟體包列表,並安裝最新版本Jenkins sudo apt update sudo apt install jenkins ```

安裝完畢後,瀏覽器開啟 localhost:8080,此時會讓你輸入管理員密碼,檢視密碼: shell sudo cat /var/lib/jenkins/secrets/initialAdminPassword 顯示的結果就是密碼,輸入下方即可:

image.png

安裝預設的社群外掛

第一步執行完畢後安裝Jenkins外掛步驟,建議直接安裝社群外掛即可:

image.png

建立Jenkins使用者

image.png

例項配置

預設8080埠即可 image.png

配置映象源

配置國內映象源,這樣下載速度會有一定的提升,先到映象源站點檢視可用的映象源:http://mirrors.jenkins-ci.org/status.html 。 在外掛管理中 -> 高階 選項頁面下,替換升級站點的URL,如下所示: image.png 換用清華的映象源: image.png

安裝Docker相關外掛

要使用Docker功能,首先Linux上需要安裝Docker,然後Jenkins中需要安裝相關Docker外掛。Docker的安裝請上一章節,現在我們需要安裝如下Docker外掛: image.png

配置訪問Docker的許可權

檢視本機上的使用者,等安裝完畢Docker後執行 ```shell grep bash /etc/passwd

//例如我機器上的使用者如下 //root:x:0:0:root:/root:/bin/bash //drag:x:1000:1000:,,,:/home/drag:/bin/bash //jenkins:x:112:119:Jenkins,,,:/var/lib/jenkins:/bin/bash

shell //檢視當前機器上的使用者 cat /etc/group ```

啟動服務,如果想要以非root使用者執行Docker命令,那麼需要將當前使用者新增到docker使用者組,給其執行docker的許可權,該使用者組在安裝Docker過程中被建立: ```shell

新增docker使用者組(安裝docker後就會存在,這一步當作驗證即可)

sudo groupadd docker

將當前使用者加入到docker使用者組中(如果是在jenkins中執行,還要把jenkins使用者加入進去)

sudo gpasswd -a $USER docker

更新使用者組

newgrp - docker

測試當前使用者是否可以直接執行docker命令

docker ps
```

WebHook處理

這裡主要說明下Jenkins專案的“構建觸發器”,我們想要達到當提交程式碼到相關分支上後,能夠自動觸發專案的構建。所以需要配合GitLab或者Github做一些關聯。

如果使用Jenkins自帶的構建觸發器,如下配置token: image.png

在GitLab中,找到 “設定”-> “匯入所有倉庫”,然後配置Jenkins專案地址,後面拼上 /build?token=Jenkins專案中設定的TOKEN,然就點選確認按鈕即可。 image.png

這時候我們可以點選剛剛建立的 webhook,點選測試: image.png 如果沒有遇到錯誤,頁面顯示成功,然後Jenkins任務也觸發並執行了,那麼恭喜你沒有踩坑。

但是不那麼幸運的小夥伴可能就會跟我一樣遇到錯誤如下: image.png 此時,參照可以參照官方提供的解決方案,地址https://plugins.jenkins.io/build-token-root/#documentation: 首先需要在Jenkins中搜索然後安裝 【Build Authorization Token Root Plugin】外掛: image.png 外掛安裝完畢後在Jenkins的“系統管理”->“安全”->“全域性安全配置”中進行設定如下即可: image.png 此時按照官方的解決方案,WebHook配置的URL地址也需要進行一絲變動: ```shell //原來是 http://JENKINS_URL/JOB_NAME/build?token=TOKEN

//現在則變為了 http://JENKINS_URL/generic-webhook-trigger/invoke?job=JOB_NAME&&token=TOKEN ``` 配置完新的WebHook地址後,此時測試的話,應該是沒有問題了,如果有請Google,注意一定是Google。

相關問題

未安裝Docker相關外掛導致的錯誤

/var/jenkins_home/workspace/image-run@tmp/durable-19c2e384/script.sh: 1: docker: not found

解決方案:安裝上文所述的相關Docker外掛。

Docker 許可權的錯誤

docker inspect -f . registry.cn-hangzhou.aliyuncs.com/vsdragon/vasdolly:0.1
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/registry.cn-hangzhou.aliyuncs.com/vsdragon/vasdolly:0.1/json": dial unix /var/run/docker.sock: connect: permission denied

jenkins是由jenkins使用者啟動執行的,docker是需要以docke使用者啟動執行的,而當前使用者沒有執行docker的許可權。

解決方案:檢視上文,然後將jenkins使用者加入docker組。

加固命令相關

加固採用的是360加固的方案,見三六零天御官網。其他方案有apk大小或者其他限制,相比來說360的方案限制稍微小一些(然而現在免費版的已經無法使用該命令列的方式了):

常用命令

```groovy //登入 ./java/bin/java -jar jiagu.jar -login [賬號] [密碼]

//設定簽名 ./java/bin/java -jar jiagu.jar -importsign [keystore檔案路徑] [keystore密碼] [alias] [alias密碼]

//加固、重簽名 ./java/bin/java -jar jiagu.jar -jiagu [源apk的路徑] [儲存到資料夾的路徑] [-autosign(可選)] [-automulpkg(可選)]


//檢視是否簽名 ./java/bin/keytool -list -printcert -jarfile [apk路徑]

```

360多渠道示例

UMENG_CHANNEL google 1 UMENG_CHANNEL wandoujia 2 一共三列,依次為統計平臺、市場名稱、渠道編號,中間用空格隔開 ,以下為相關名詞說明:

  • 統計平臺

統計平臺:即Android Name,應用中整合的資料分析sdk的公司名稱,例:UMENG_CHANNEL。

  • 市場名稱

各大安卓應用分發市場(下拉列表裡提供了Top20的市場供選擇),以幫助開發者區分不同渠道包特徵上傳相對應市場。

  • 渠道編號

即Android Value,一般填寫相關Channel id。使用者可自行定義區分各大市場的關鍵字,可以是英文、數字、漢字等。

注意事項

  • 必須進入到jiagu資料夾中執行相關命令
  • 必須使用360提供的java命令:./java/bin/java -jar jiagu.jar xxx
  • 使用多渠道檔案後會在輸出資料夾中得到所有的渠道包,以及一個加固包

但是,目前多渠道打包用的是VasDolly的方案,請檢視下文!!!

VasDolly命令相關

目前的多渠道方案為騰訊的VasDolly方案,GitHub地址 https://github.com/Tencent/VasDolly

常用命令

```groovy //通過help檢視具體命令 java -jar VasDolly.jar help

//獲取指定APK的簽名方式 java -jar VasDolly.jar get -s [apkPath]

//獲取指定APK的渠道資訊 java -jar VasDolly.jar get -c [apkPath]

//刪除指定APK的渠道資訊 java -jar VasDolly.jar remove -c [apkPath]

//通過指定渠道字串新增渠道資訊 java -jar VasDolly.jar put -c "channel1,channel2" [apkPath] [outputDir]

//通過指定某個渠道字串新增渠道資訊到目標APK java -jar VasDolly.jar put -c "channel1" [apkPath] [dstApkPath]

//通過指定渠道檔案新增渠道資訊 java -jar VasDolly.jar put -c [channelTextPath] [apkPath] [outputDir]


//為基於V1的多渠道打包添加了多執行緒支援,滿足渠道較多的使用場景 java -jar VasDolly.jar put -mtc channel.txt [apkPath] [outputDir]

//提供了FastMode,生成渠道包時不進行強校驗,速度可提升10倍以上 java -jar VasDolly.jar put -c channel.txt -f [apkPath] [outputDir] ```

Bugly命令相關

Bugly也提供了上傳mapping檔案的工具,官方文件地址《Bugly Android 符號表配置》。

常用命令

shell java -jar buglyqq-upload-symbol.jar -appid <APP ID> -appkey<APP KEY> -bundleid <App BundleID> -version <App Version> -platform <App Platform> -inputSymbol <Original Symbol File Path> -inputMapping <mapping file>

引數說明

  • appid

在Bugly平臺產品對應的appid

  • appkey

在Bugly平臺產品對應的appkey

  • bundleid

Android平臺是包名、iOS平臺叫bundle id

  • version

App版本號 (PS:注意版本號裡不要有特殊字串,比如( ),不然執行可能會報錯) 如果上報包含mapping檔案,那麼此處的版本號必須和要還原的堆疊所屬的app的實際版本號一致,因為一個版本下的App是對應唯一的mapping.txt,不對齊則無法還原對應的堆疊。具體的版本號可以參考bugly.qq.com上堆疊資訊。 如果只是上傳so或者dsym,那麼不要求版本號必須和要還原的堆疊所屬的app版本號一樣,因為so和dsym還原堆疊的時候是通過模組UUID來匹配的,但是仍然推薦填寫一個app的真實版本號。

  • platform

平臺型別,當前支援的選項分別是 Android、IOS,注意大小寫要正確

  • inputSymbol

原始符號表[dsym、so]所在資料夾目錄地址,如果是Android平臺同時包含mapping和so,此處輸入兩個原始符號表儲存的共同父目錄

  • inputMapping

mapping所在資料夾目錄地址[Android平臺特有,ios忽略]

測試環境pipeline指令碼

上述工作全部準備完畢後我們終於可以編寫Jenkins的pipeline指令碼了:

```shell /* * 打包指令碼 /

/* * GitLab倉庫地址 / def GIT_URL = "YOUR_GIT_REPOSTORY_URL"

/* * GitLab下載程式碼的祕鑰 / def GIT_CREDENTIALS_ID = "YOUR_CRENENTALS_ID"

/* * 全域性變數內容 / class PkgInfo {

/**
 * App的型別
 */
static APP_TYPE_MAP = [
        "APP名稱1"  : "appFlavor1",
        "APP名稱2"  : "appFlavor2",
]

/**
 * 獲取支援的App型別資料
 */
static def getSupportAppList() {
    String str = ""
    for (element in APP_TYPE_MAP) {
        str += "${element.key}\n"
    }
    return str
}

/**
 * app的環境
 */
static APP_ENV_MAP = [
        "測試": "test",
        "生產": "prod",
        "市場": "market",
]

/**
 * 獲取支援的環境型別資料
 */
static def getSupportEnvList() {
    String str = ""
    for (element in APP_ENV_MAP) {
        str += "${element.key}\n"
    }
    return str
}

/**
 * 打包成功情況下通知到的群組
 */
static DING_SUCCESS_MAP = [
        "釘釘群組名稱": "釘釘群組中機器人token",
]

/**
 * 獲取支援的打包成功通知到的群組資料
 */
static def getSupportDingSuccessList() {
    String str = ""
    for (element in DING_SUCCESS_MAP) {
        str += "${element.key}\n"
    }
    return str
}

/**
 * 打包失敗情況下通知到的群組
 */
static DING_FAILURE_MAP = [
        "釘釘群組名稱"  : "釘釘群組中機器人token",
]

/**
 * 獲取支援的打包失敗通知到的群組資料
 */
static def getSupportDingFailureList() {
    String str = ""
    for (element in DING_FAILURE_MAP) {
        str += "${element.key}\n"
    }
    return str
}

/**
 * Apk檔案的輸出目錄
 */
static APK_OUTPUT_DIR = "app/build/myApks/"

/**
 * 獲取當前App的Flavor
 */
static def getFlavorName(String appKey) {
    return APP_TYPE_MAP.get(appKey)
}

/**
 * 獲取當前App的Flavor
 */
static def getEnvName(String envKey) {
    return APP_ENV_MAP.get(envKey)
}

/**
 * 獲取執行成功通知到的群組
 */
static def getDingSuccessToken(String key) {
    return DING_SUCCESS_MAP.get(key)
}

/**
 * 獲取執行失敗通知到的群組
 */
static def getDingFailureToken(String key) {
    return DING_FAILURE_MAP.get(key)
}

/**
 * 獲取打包的gradle指令碼
 */
static def getAssembleCmd(String appKey, String envKey) {
    def flavor = getFlavorName(appKey)
    def env = getEnvName(envKey)
    return "gradle --no-daemon clean app:assemble${firstCharToUpperCase(flavor)}${firstCharToUpperCase(env)}Release"
}

/**
 * 將字串的首字母大寫
 */
static def firstCharToUpperCase(String str) {
    def firstStr = str.charAt(0).toString().toUpperCase()
    def otherStr = str.substring(1, str.length())
    return "${firstStr}${otherStr}"
}

/**
 * 是否需要上傳mapping檔案到伺服器
 */
static def needUploadMappingToServer(String envName) {
    return envName == "market" || envName == "prod"
}

/**
 * 是否需要上傳mapping檔案到bugly
 */
static def needUploadMappingToBugly(String appKey, String envKey) {
    def flavor = getFlavorName(appKey)
    def env = getEnvName(envKey)

    return flavor == "psd" && (env == "prod" || env == "market")
}

/**
 * 獲取mapping檔案的路徑
 */
static def getMappingDir(String flavorName, String envName) {
    return "app/build/outputs/mapping/${flavorName}${firstCharToUpperCase(envName)}Release"
}

}

/* * 返回App的基本資訊 * info[0] app名(同Flavor) * info[1] app版本名 * info[2] app版本號 / static def getAppInfo(def script, def flavorName) { return script.readFile("app/build/myApksInfo/${flavorName}.txt").readLines() }

/* * 獲取當前的格式化時間 / static def getCurrentTime(def script) { return script.sh(script: "echo date '+%Y_%m%d_%H%M'", returnStdout: true).trim() }

/* * 上傳檔案 / static def upload(def script, String flavorName, String envName) { def appInfo = getAppInfo(script, "${flavorName}") def sourceApkDir = PkgInfo.APK_OUTPUT_DIR script.echo "當前app的資訊:${appInfo}"

def versionCode = appInfo[2]

//要上傳到的伺服器資料夾的地址 (根目錄在psd-android資料夾下)
def time = getCurrentTime(script)
def uploadApkDir = "${envName}/${versionCode}/${flavorName}/${time}"
script.println("要上傳到的資料夾目錄:${uploadApkDir}")

//儲存到的資料夾網址
def dirUrl = "http://內網地址:內網埠/${uploadApkDir}"

//獲取apk名稱
def apkName = script.sh(returnStdout: true, script: "ls -1 ${sourceApkDir}").split()[0]
def qrName = "qr.png"
script.println("當前apk的名字:${apkName}")

//製作apk檔案的二維碼,儲存到輸出的apk目錄中
def apkUrl = "${dirUrl}/${apkName}"
def qrUrl = "${dirUrl}/${qrName}"
script.sh "qrencode -o ${sourceApkDir}${qrName} '${apkUrl}'"

uploadApksToServer(script,
        "${sourceApkDir}",
        "${uploadApkDir}"
)

//正式環境和市場環境都上傳mapping檔案到伺服器
if (PkgInfo.needUploadMappingToServer(envName)) {
    def uploadMappingDir = "${uploadApkDir}/mapping"
    def sourceMappingDir = PkgInfo.getMappingDir(flavorName, envName)

    uploadMappingToServer(
            script,
            "$sourceMappingDir",
            "$uploadMappingDir"
    )
}

return [apkUrl, qrUrl]

}

/ * 上傳apk檔案以及二維碼圖片到伺服器 / static def uploadApksToServer(def script, def sourceApkDir, def uploadApkDir) { script.sh "cd $sourceApkDir && lftp -u 賬戶名,賬戶密碼 內網地址 -e \"cd androidApks; mkdir -p $uploadApkDir; cd $uploadApkDir; mput ; exit\"" } / * 上傳mapping資料夾到伺服器 / static def uploadMappingToServer(def script, def sourceMappingDir, def uploadMappingDir) { script.sh "cd $sourceMappingDir && lftp -u 賬戶名,賬戶密碼 內網地址 -e \"cd androidApks; mkdir -p $uploadMappingDir; cd $uploadMappingDir; mput .txt; exit\"" }

/* * 上傳mapping檔案到bugly / static def uploadMappingToBugly(def script, def versionName, def sourceMappingDir) { //去除字串中的v字,只保留類似 1.2.3 字樣 def realVersionName = versionName.replace("v", "")

script.sh "java -jar /home/bugly/buglyqq-upload-symbol-334.jar" +
        " -appid 你的APPID" +
        " -appkey 你的APPKEY" +
        " -bundleid 包名" +
        " -version ${realVersionName}" +
        " -platform Android" +
        " -inputMapping ${sourceMappingDir}"

}

/* * 獲取git提交日誌資訊 / static def getGitLogs(def script) {

def gitLogCount = 5

/**
 * |sed 's/\"//g'
 * 該命令表示去除字串中的雙引號,如果不去除引號的話會導致傳送釘釘指令碼語法錯亂
 */
script.sh "git log --no-merges --pretty=format:\"%cn: %s\" -${gitLogCount} | sed 's/\\\"//g' > log.txt"

def gitLogs = ""
def lines = script.readFile("./log.txt").readLines()
for (line in lines) {
    gitLogs = gitLogs + "\n- " + line.trim()
}
return gitLogs

}

/* * 傳送釘釘成功訊息 * @param script * @return / static def sendDingSuccessMessage(def script, String flavorKey, String envKey, String dingSuccessKey, def urls, def showGitLog) {

def flavorName = PkgInfo.getFlavorName(flavorKey)

def appInfo = getAppInfo(script, flavorName)
script.echo "當前app的資訊:${appInfo}"

def versionName = appInfo[1]
def versionCode = appInfo[2]


if (showGitLog) {

    def logs = getGitLogs(script)

    script.sh "curl 'https://oapi.dingtalk.com/robot/send?access_token=${PkgInfo.getDingSuccessToken(dingSuccessKey)}'" +
            " -H 'Content-Type: application/json'" +
            " -d '{" +
            "\"msgtype\": \"markdown\"," +
            "\"markdown\": {" +
            "\"title\":\"打包成功的通知\"," +
            "\"text\":" +
            "\"" +
            "## ${envKey}包:${flavorName}_${versionCode}_${versionName}" +
            "\n-----" +
            "\n**注意**:僅支援內網環境" +
            "\n- [歷史APK目錄](http://內網地址:內網埠)" +
            "\n- [點選下載APK](${urls[0]})" +
            "\n- [點選顯示二維碼](${urls[1]})" +
            "\n-----" +
            "\n**更新日誌**" +
            "\n${logs}" +
            "\"" +
            "}}'"
} else {
    script.sh "curl 'https://oapi.dingtalk.com/robot/send?access_token=${PkgInfo.getDingSuccessToken(dingSuccessKey)}'" +
            " -H 'Content-Type: application/json'" +
            " -d '{" +
            "\"msgtype\": \"markdown\"," +
            "\"markdown\": {" +
            "\"title\":\"打包成功的通知\"," +
            "\"text\":" +
            "\"" +
            "## ${envKey}包:${flavorName}_${versionCode}_${versionName}" +
            "\n-----" +
            "\n注意:僅支援內網環境" +
            "\n- [歷史APK目錄](http://內網地址:內網埠)" +
            "\n- [點選下載APK](${urls[0]})" +
            "\n- [點選顯示二維碼](${urls[1]})" +
            "\"" +
            "}}'"
}

}

/* * 傳送釘釘失敗訊息 / static def sendDingFailureMessage(def script, String dingFailureKey) { script.sh "curl 'https://oapi.dingtalk.com/robot/send?access_token=${PkgInfo.getDingFailureToken(dingFailureKey)}'" + " -H 'Content-Type: application/json'" + " -d '{\"at\":{\"atMobiles\":[\"15757126424\"]},\"markdown\":{\"title\":\"打包失敗通知\",\"text\":\"### 打包失敗辣,快來人處理! \n@被艾特人手機號\"},\"msgtype\":\"markdown\"}'" }

pipeline { agent none

parameters {
    string name: 'PARAM_GIT_BRANCH', defaultValue: 'auto_pkg_test', description: '輸入Git分支,預設如上', trim: true
    choice name: 'PARAM_APP_TYPE', choices: "${PkgInfo.getSupportAppList()}", description: '選擇App的型別,預設如上'
    choice name: 'PARAM_APP_ENV', choices: "${PkgInfo.getSupportEnvList()}", description: '選擇App的環境,預設如上'
    choice name: 'PARAM_DING_SUCCESS', choices: "${PkgInfo.getSupportDingSuccessList()}", description: '選擇執行成功通知到的群,預設如上'
    choice name: 'PARAM_DING_FAILURE', choices: "${PkgInfo.getSupportDingFailureList()}", description: '選擇執行失敗通知到的群,預設如上'
    booleanParam name: 'PARAM_SHOW_GIT_LOG', defaultValue: false, description: '是否列印Git提交日誌,預設false'
}

stages {

    stage('Package') {

        agent {
            docker {
                image 'registry.cn-hangzhou.aliyuncs.com/vsdragon/android-builder:1.1'
                //做一下Gradle快取目錄的掛載
                args '-v /usr/mylib/gradlecache:/usr/mylib/gradlecache'
            }
        }

        steps {
            echo "==================================================>>Stage_1"

            echo "==================================================>>下載原始碼"
            git branch: "$PARAM_GIT_BRANCH", credentialsId: "${GIT_CREDENTIALS_ID}", url: "${GIT_URL}"

            script {
                echo "==================================================>>開始打包"
                sh PkgInfo.getAssembleCmd("$PARAM_APP_TYPE", "$PARAM_APP_ENV")


                echo "==================================================>>上傳APK"
                def urls = upload(this,
                        PkgInfo.getFlavorName("$PARAM_APP_TYPE"),
                        PkgInfo.getEnvName("$PARAM_APP_ENV")
                )

                echo "==================================================>>傳送群通知"
                sendDingSuccessMessage(this,
                        "$PARAM_APP_TYPE",
                        "$PARAM_APP_ENV",
                        "$PARAM_DING_SUCCESS",
                        urls,
                        Boolean.valueOf("$PARAM_SHOW_GIT_LOG"))
            }
        }

        post {
            failure {
                script {
                    sendDingFailureMessage(this, "$PARAM_DING_FAILURE")
                }
            }
        }
    }

    /**
     * 上傳APK以及Mapping檔案
     * 注意:bugly:0.3版本帶lftp命令
     */
    stage("Upload To Bugly") {
        agent {
            docker {
                image 'registry.cn-hangzhou.aliyuncs.com/vsdragon/bugly:0.4'
            }
        }

        steps {
            script {
                echo "==================================================>>Stage2"
                if (PkgInfo.needUploadMappingToBugly("$PARAM_APP_TYPE", "$PARAM_APP_ENV")) {
                    echo "==================================================>>上傳mapping檔案到bugly"
                    def flavorName = PkgInfo.getFlavorName("$PARAM_APP_TYPE")
                    def envName = PkgInfo.getEnvName("$PARAM_APP_ENV")

                    def appInfo = getAppInfo(this, flavorName)
                    def versionName = appInfo[1]

                    def sourceMappingDir = PkgInfo.getMappingDir(flavorName, envName)

                    uploadMappingToBugly(this, versionName, sourceMappingDir)
                }
            }
        }

        post {
            failure {
                script {
                    sendDingFailureMessage(this, "$PARAM_DING_FAILURE")
                }
            }
        }
    }
}

} ```

以上程式碼是後來更改過的指令碼了,採用了引數化構建的方式,允許選擇App的型別,環境等進行打包。

總結

目前來說帶加固那一套的指令碼已經失效了,現在能做到的就是打包、儲存、上傳apk及mapping檔案的功能了,多渠道包的功能也從中剝離了。但整體的思路都在上文基本表述出來了,如有疏漏之處還請大家多多指教。