在面向海外的项目组辛勤耕耘了两年,今年被调到了国内的开发组,很多东西突然感觉有些陌生了起来。首先接到的第一个任务就是打包自动化的工作,因为国内的项目组目前有多个app在同时开发,提测的时候人工打完测试包上传到三方平台,然后发送钉钉通知告知测试人员。到生产环境的时还需要打包、加固、重签名,再处理多渠道问题,最后还需要手动上传mapping文件到Bugly等平台,整个一套流程够复杂,并且也相当浪费时间,多个app处理起来更是繁琐。
所以,把这件事交给机器去做就是我们的终极目的。其实这件事情整体做下来更像是运维的工作,但是呢,作为一个开发工程师我学(卷)一点运维的内容不过分吧。整体内容围绕Jenkins + Docker来进行阐述,如有纰漏或错误,还请各位帮忙斧正。
注: 由于编写该文档时,360加固免费版还是支持命令行的方式使用的,但是现在免费版已经不支持命令行的操作了,如果使用则需要购买加固专业版,或者成为企业版用户。所以现在情况下,我又写了一个桌面端的工具来完成后续步骤,文章参考《使用ComposeDesktop开发一款桌面端多功能APK工具》。
先把我们前述的需求分类整理下,大致分为测试环境和生产环境,具体的流程步骤如下:
在测试环境下,主要流程如下:
测试环境的整个自动化过程还是相对简单的。
注:在之前我们是上传到fir.im或者蒲公英这样的应用内测托管平台上的,然而由于最近一段时间貌似审核比较严重,动不动新的app就会被提示违规然后不给下载,所以正好借此机会舍弃了三方平台,转而使用内网服务器。
生产环境的流程就复杂多了,主要流程如下:
整体的流程已经分析完了,那么如何实现呢?
Jenkins!在Jenkins中我们可以编写pipeline脚本来处理上述步骤,免去了人工操作的烦恼。Jenkins的格言:
使开发者从繁杂的集成中解脱出来,专注于更为重要的业务逻辑实现上
接下来我们先着重看下打包这个步骤,光是打包我们就需要配置java环境、gradle环境、android sdk/ndk等等,如果这套自动化的工具单部署到一台服务器上还则罢了,要是再多部署几台,那光是配置这一套环境就能把人逼疯了,怎么处理呢?。
Docker!Docker允许我们把这些配置的内容统统封装起来,做成镜像文件。哪里有需要就下载这个镜像,然后在容器中运行该镜像,这样就能提供出来一套跟开发一模一样的环境,然后在其中使用正常的gradle打包命令就可以了。
接下来我们就在Linux的环境下,安装Jenkins和Docker来一步步实现我们的自动化流程。
因为我的电脑系统是Windows 11,为了方便我直接采用了WSL2的方案。
首先在搜索中输入“启用或关闭Windows功能”,然后再弹框中勾选如下两项,然后最好重启电脑:
打开Microsoft Store,搜索ubuntu,这里我选择Ubuntu 20.04.4 LTS版本进行了安装。
安装完毕后打开Ubuntu过程中可能会遇到各种奇奇怪怪的问题,如果有,请参考下文相关方案。
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
Installing, this may take a few minutes…
WslRegisterDistribution failed with error: 0x800701bc
Error: 0x800701bc WSL 2 ??? https://aka.ms/wsl2kernelPress 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
主要是mnt,表示挂载:
//进入Windows下E盘
cd /mnt/e
需要设置端口转发:
//设置端口转发
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
官方网址:https://docs.docker.com/desktop/linux/install/
如果按照官方步骤执行失败的话,可以参考如下步骤:
首先更新软件包索引,然后添加新的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
在运行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插件。一切准备就绪后我们这就开始制作镜像了。
VasDolly需要在JDK8的环境下使用,那么有两种方式:
我们使用第一种方式做为演示,首先工程结构如下所示:
在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组件。
注意运行指令的区别:
Dockerfile脚本编写完毕后我们就可以,运行脚本来创建镜像了,这里也有两种方式可以创建镜像:
第一种方式很简单了,点击按钮等待创建镜像就好了。如果想练习Docker指令,那么切换到VasDolly目录下执行创建镜像的指令即可。注意:注意最后一个参数是上下文路径,由于我们有拷贝文件的操作,所以用点则表示当前文件夹路径。
sudo docker build -t [镜像名]:[镜像TAG] [上下文路径]
#例如
sudo docker build -t vasdolly:0.1 .
然后镜像打包成功后,我们可以用交互式命令运行该镜像,当容器启动后就可以看到控制台输出的VasDolly的帮助信息了。
该镜像主要用于将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打包需要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中挂载到本地目录。
#指定基础镜像
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
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
解决方案:在Windows中以管理员身份运行ubuntu。
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的方案。
制作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.
官方网址 :https://www.jenkins.io/
linux下安装方案: https://www.jenkins.io/doc/book/installing/linux/
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软件源相关
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
显示的结果就是密码,输入下方即可:
第一步执行完毕后安装Jenkins插件步骤,建议直接安装社区插件即可:
配置国内镜像源,这样下载速度会有一定的提升,先到镜像源站点查看可用的镜像源:http://mirrors.jenkins-ci.org/status.html 。
在插件管理中 -> 高级 选项页面下,替换升级站点的URL,如下所示:
换用清华的镜像源:
要使用Docker功能,首先Linux上需要安装Docker,然后Jenkins中需要安装相关Docker插件。Docker的安装请上一章节,现在我们需要安装如下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
这里主要说明下Jenkins项目的“构建触发器”,我们想要达到当提交代码到相关分支上后,能够自动触发项目的构建。所以需要配合GitLab或者Github做一些关联。
如果使用Jenkins自带的构建触发器,如下配置token:
在GitLab中,找到 “设置”-> “导入所有仓库”,然后配置Jenkins项目地址,后面拼上 /build?token=Jenkins项目中设置的TOKEN,然就点击确认按钮即可。
这时候我们可以点击刚刚创建的 webhook,点击测试:
如果没有遇到错误,页面显示成功,然后Jenkins任务也触发并执行了,那么恭喜你没有踩坑。
但是不那么幸运的小伙伴可能就会跟我一样遇到错误如下:
此时,参照可以参照官方提供的解决方案,地址https://plugins.jenkins.io/build-token-root/#documentation:
首先需要在Jenkins中搜索然后安装 **【Build Authorization Token Root Plugin】**插件:
插件安装完毕后在Jenkins的“系统管理”->“安全”->“全局安全配置”中进行设置如下即可:
此时按照官方的解决方案,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。
/var/jenkins_home/workspace/image-run@tmp/durable-19c2e384/script.sh: 1: docker: not found
解决方案:安装上文所述的相关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路径]
UMENG_CHANNEL google 1
UMENG_CHANNEL wandoujia 2
一共三列,依次为统计平台、市场名称、渠道编号,中间用空格隔开 ,以下为相关名词说明:
统计平台:即Android Name,应用中集成的数据分析sdk的公司名称,例:UMENG_CHANNEL。
各大安卓应用分发市场(下拉列表里提供了Top20的市场供选择),以帮助开发者区分不同渠道包特征上传相对应市场。
即Android Value,一般填写相关Channel id。用户可自行定义区分各大市场的关键字,可以是英文、数字、汉字等。
但是,目前多渠道打包用的是VasDolly的方案,请查看下文!!!
目前的多渠道方案为腾讯的VasDolly方案,GitHub地址** **https://github.com/Tencent/VasDolly
//通过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也提供了上传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>
在Bugly平台产品对应的appid
在Bugly平台产品对应的appkey
Android平台是包名、iOS平台叫bundle id
App版本号 (PS:注意版本号里不要有特殊字符串,比如( ),不然运行可能会报错)
如果上报包含mapping文件,那么此处的版本号必须和要还原的堆栈所属的app的实际版本号一致,因为一个版本下的App是对应唯一的mapping.txt,不对齐则无法还原对应的堆栈。具体的版本号可以参考bugly.qq.com上堆栈信息。
如果只是上传so或者dsym,那么不要求版本号必须和要还原的堆栈所属的app版本号一样,因为so和dsym还原堆栈的时候是通过模块UUID来匹配的,但是仍然推荐填写一个app的真实版本号。
平台类型,当前支持的选项分别是 Android、IOS,注意大小写要正确
原始符号表[dsym、so]所在文件夹目录地址,如果是Android平台同时包含mapping和so,此处输入两个原始符号表存储的共同父目录
mapping所在文件夹目录地址[Android平台特有,ios忽略]
上述工作全部准备完毕后我们终于可以编写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文件的功能了,多渠道包的功能也从中剥离了。但整体的思路都在上文基本表述出来了,如果有需要可以按照文章进行尝试。