前言

在面向海外的项目组辛勤耕耘了两年,本年被调到了国内的开发组,很多东西忽然感觉有些陌生了起来。首要接到的第一个任务便是打包自动化的作业,由于国内的项目组现在有多个app在一起开发,提测的时分人工打完测验包上传到三方途径,然后发送钉钉告诉告知测验人员。到出产环境的时还需求打包、加固、重签名,再处理多途径问题,最终还需求手动上传mapping文件到Bugly等途径,整个一套流程够复杂,并且也相当浪费时刻,多个app处理起来更是繁琐。

所以,把这件事交给机器去做便是咱们的终极意图。其实这件作业全体做下来更像是运维的作业,可是呢,作为一个开发工程师我学(卷)一点运维的内容不过分吧。全体内容环绕Jenkins + Docker来进行论述,如有疏忽或过错,还请各位帮忙指正。

注: 由于编写该文档时,360加固免费版仍是支撑指令行的方法运用的,可是现在免费版现已不支撑指令行的操作了,假如运用则需求购买加固专业版,或许成为企业版用户。所以现在情况下,我又写了一个桌面端的东西来完结后续过程,文章参阅《运用ComposeDesktop开发一款桌面端多功用APK东西》。

自动化流程

先把咱们前述的需求分类整理下,大致分为测验环境和出产环境,详细的流程过程如下:

测验环境

在测验环境下,首要流程如下:

  1. 打出测验包
  2. 上传到公司内网服务器
  3. 生成apk的下载二维码(qrencode)
  4. 获取Git日志的提交记载
  5. 最终运用钉钉机器人告诉到群即可

测验环境的整个自动化进程仍是相对简略的。

注:在之前咱们是上传到fir.im或许蒲公英这样的运用内测托管途径上的,但是由于最近一段时刻貌似审核比较严重,动不动新的app就会被提示违规然后不给下载,所以正好借此机会舍弃了三方途径,转而运用内网服务器。

Android自动化打包记录--Jenkins+Docker+WSL2

出产环境

出产环境的流程就复杂多了,首要流程如下:

  1. 打出出产包
  2. 加固(加固计划这儿示例的是360加固,其他比方腾讯乐固等,咱们能够自行挑选)
  3. 重签名(能够运用360指令行重签名,也能够自行重签名)
  4. 生成途径包(这一步选用的是VasDolly计划,其他如Walle等,咱们能够自行挑选)
  5. 途径包出产结束后就能够将途径包存储到服务器上或上传到其他三方途径上供给下载链接了
  6. 将生成的mapping文件上传到溃散剖析的途径即可,这儿是腾讯的bugly
  7. 发送钉钉告诉告知相关人员

Android自动化打包记录--Jenkins+Docker+WSL2

自动化原理

全体的流程现已剖析完了,那么怎么完成呢?

Jenkins!在Jenkins中咱们能够编写pipeline脚原本处理上述过程,免去了人工操作的烦恼。Jenkins的格言:

使开发者从繁杂的集成中摆脱出来,专心于更为重要的事务逻辑完成上

接下来咱们先着重看下打包这个过程,光是打包咱们就需求装备java环境、gradle环境、android sdk/ndk等等,假如这套自动化的东西单布置到一台服务器上还则算了,要是再多布置几台,那光是装备这一套环境就能把人逼疯了,怎么处理呢?。

Docker!Docker答应咱们把这些装备的内容通通封装起来,做成镜像文件。哪里有需求就下载这个镜像,然后在容器中运转该镜像,这样就能供给出来一套跟开发一模一样的环境,然后在其中运用正常的gradle打包指令就能够了。

接下来咱们就在Linux的环境下,装置Jenkins和Docker来一步步完成咱们的自动化流程。

Windows下装置Ubuntu

由于我的电脑体系是Windows 11,为了便利我直接选用了WSL2的计划。

装置过程

首要在查找中输入“启用或封闭Windows功用”,然后再弹框中勾选如下两项,然后最好重启电脑:

Android自动化打包记录--Jenkins+Docker+WSL2
翻开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 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 ?????????????????? aka.ms/wsl2kernel

Press any key to continue…

前往微软WSL官网下载装置适用于 x64 计算机的最新 WSL2 Linux 内核更新包装置即可。 docs.microsoft.com/zh-cn/windo…

WSL拜访Windows

首要是mnt,表明挂载:

//进入Windows下E盘
cd /mnt/e

WSL拜访内网

需求设置端口转发:

//设置端口转发
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内容

官方网址:docs.docker.com/desktop/lin…

装置

假如依照官方过程履行失利的话,能够参阅如下过程: 首要更新软件包索引,然后增加新的HTTPS软件源:

sudo apt update
sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common

然后导入源库房的GPG key:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

将Docker APT软件源增加到体系:

sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

现在能够检查Docker软件源中的一切可用版别了:

apt list -a docker-ce

装置:

//装置最新版别
sudo apt install docker-ce docker-ce-cli containerd.io
//装置指定版别
sudo apt install docker-ce=<VERSION> docker-ce-cli=<VERSION> containerd.io

验证装置,假如成功输出docker的版别号,表明装置成功:

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指令如下:

sudo service docker start

然后运转docker的hello-world,验证是否装置成功:

sudo docker run hello-world

拉取镜像

以拉取jdk8镜像为例:

sudo docker pull openjdk:8-jdk-oracle

镜像和容器指令

//显示出一切的镜像
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插件。一切预备就绪后咱们这就开端制造镜像了。

Android自动化打包记录--Jenkins+Docker+WSL2

创立VasDolly镜像

VasDolly需求在JDK8的环境下运用,那么有两种方法:

  • 运用ubuntu作为根底镜像,自行装置jdk并装备环境
  • 直接运用jdk8的根底镜像

咱们运用第一种方法做为演示,首要工程结构如下所示:

Android自动化打包记录--Jenkins+Docker+WSL2
在VasDolly文件夹下,咱们有jdk-8u333-linux-x64.tar.gz以及VasDolly.jar、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目录下履行创立镜像的指令即可。留意:留意最终一个参数是上下文途径,由于咱们有拷贝文件的操作,所以用点则表明当时文件夹途径。

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文件如下,编写结束后履行创立镜像指令即可,比照上面的真是十分的简略粗犷且好使:

#指定根底镜像
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文件:developer.android.google.cn/studio/ 。然后运用sdkmanager装置build-tools以及platforms等文件。留意,需求运用–sdk_root来指定SDK存储的途径。

还需求留意的一个问题便是,Gradle下载依靠后的缓存问题,参阅文章:zwbetz.com/reuse-the-g… 。官方文章:docs.docker.com/develop/dev… 。Docker镜像是一个很纯净的环境,所以每次履行假如不缓存依靠文件,那么每次履行都会从头下载,十分耗费时刻。在制造镜像时,咱们创立gradle等的缓存目录,然后在Docker中挂载到本地目录。

#指定根底镜像
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交友网站也是能够的,这儿为了削减网络环境的影响,仍是直接白嫖了阿里云。

首要咱们需求注册一个阿里云账号,记载账号暗码,然后开通镜像容器服务(免费的),创立镜像命名空间,预备好后就能够上传咱们制造好的镜像了(这儿一笔带过了,信任对咱们都不是问题,假如详细流程不清楚的能够百度):

#登录阿里云账号,回车后需求输入暗码
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的脚本,报错如上。

参阅计划:参阅github.com/jenkinsci/d… ,可是对我来说还没有方法处理,所以选用的是Windows下WSL2的计划。

Gradle缓存方位

Android自动化打包记录--Jenkins+Docker+WSL2
制造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的脚本中指定本机的目录挂载到上述的缓存目录。示例脚本如下:

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内容

官方网址 :www.jenkins.io/ linux下装置计划: www.jenkins.io/doc/book/in…

装置Java环境

Jenkins需求java的环境,所以需求先装置java:

//装置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

官方脚本假如有问题,请运用如下脚本装置:

//导入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,此刻会让你输入管理员暗码,检查暗码:

sudo cat /var/lib/jenkins/secrets/initialAdminPassword

显示的结果便是暗码,输入下方即可:

Android自动化打包记录--Jenkins+Docker+WSL2

装置默许的社区插件

第一步履行结束后装置Jenkins插件过程,建议直接装置社区插件即可:

Android自动化打包记录--Jenkins+Docker+WSL2

创立Jenkins用户

Android自动化打包记录--Jenkins+Docker+WSL2

实例装备

默许8080端口即可

Android自动化打包记录--Jenkins+Docker+WSL2

装备镜像源

装备国内镜像源,这样下载速度会有一定的提升,先到镜像源站点检查可用的镜像源:mirrors.jenkins-ci.org/status.html 。 在插件管理中 -> 高级 选项页面下,替换晋级站点的URL,如下所示:

Android自动化打包记录--Jenkins+Docker+WSL2
换用清华的镜像源:
Android自动化打包记录--Jenkins+Docker+WSL2

装置Docker相关插件

要运用Docker功用,首要Linux上需求装置Docker,然后Jenkins中需求装置相关Docker插件。Docker的装置请上一章节,现在咱们需求装置如下Docker插件:

Android自动化打包记录--Jenkins+Docker+WSL2

装备拜访Docker的权限

检查本机上的用户,等装置结束Docker后履行

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
//检查当时机器上的用户
cat /etc/group

发动服务,假如想要以非root用户履行Docker指令,那么需求将当时用户增加到docker用户组,给其履行docker的权限,该用户组在装置Docker进程中被创立:

#增加docker用户组(装置docker后就会存在,这一步当作验证即可)
sudo groupadd docker

#将当时用户参加到docker用户组中(假如是在jenkins中运转,还要把jenkins用户参加进去)
sudo gpasswd -a $USER docker

#更新用户组
newgrp - docker

#测验当时用户是否能够直接履行docker指令
docker ps    

WebHook处理

这儿首要阐明下Jenkins项意图“构建触发器”,咱们想要到达当提交代码到相关分支上后,能够自动触发项意图构建。所以需求配合GitLab或许Github做一些相关。

假如运用Jenkins自带的构建触发器,如下装备token:

Android自动化打包记录--Jenkins+Docker+WSL2

在GitLab中,找到 “设置”-> “导入一切库房”,然后装备Jenkins项目地址,后边拼上 /build?token=Jenkins项目中设置的TOKEN,然就点击承认按钮即可。

Android自动化打包记录--Jenkins+Docker+WSL2

这时分咱们能够点击刚刚创立的 webhook,点击测验:

Android自动化打包记录--Jenkins+Docker+WSL2
假如没有遇到过错,页面显示成功,然后Jenkins任务也触发并履行了,那么恭喜你没有踩坑。

可是不那么幸运的小伙伴或许就会跟我一样遇到过错如下:

Android自动化打包记录--Jenkins+Docker+WSL2
此刻,参照能够参照官方供给的处理计划,地址plugins.jenkins.io/build-token…: 首要需求在Jenkins中查找然后装置 **【Build Authorization Token Root Plugin】**插件:
Android自动化打包记录--Jenkins+Docker+WSL2
插件装置结束后在Jenkins的“体系管理”->“安全”->“大局安全装备”中进行设置如下即可:
Android自动化打包记录--Jenkins+Docker+WSL2
此刻依照官方的处理计划,WebHook装备的URL地址也需求进行一丝变动:

//原来是
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的计划约束稍微小一些(但是现在免费版的现已无法运用该指令行的方法了):

常用指令

//登录
./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地址** **github.com/Tencent/Vas…

常用指令

//经过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 符号表装备》。

常用指令

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脚本了:

/**
 * 打包脚本
 */
/**
 * 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文件的功用了,多途径包的功用也从中剥离了。但全体的思路都在上文基本表述出来了,如有疏漏之处还请咱们多多指教。