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文件的功能了,多渠道包的功能也從中剝離了。但整體的思路都在上文基本表述出來了,如有疏漏之處還請大家多多指教。