前言
本篇是接着上篇juejin.cn/post/721920… ,对于在使用Docker搭建Android打包平台时遇到的问题做一个总结。上一篇写的是使用Docker运行jenkins的过程,然而启动jenkins之后,还是没法直接打Android包的,下面会做一个叙述。
步骤
1、Android环境配置
上图是启动Jenkins之后的页面,我们来添加一个仓库,打包看看会遇到什么。SO我们新建一个任务,添加构建(具体流程参考juejin.cn/post/721920…),不出意外,当checkout完代码之后,会报错,即can not found SDK,找不到Android SDK,很明显,我们没有配置Android的环境,所以我们配置一下,OK,怎么配置,如果在本机,直接把Android的路径填入环境变量文件(.bash_profile)即可,但是在Docker里面要稍微麻烦些。下面写几种方法:
1、给现有的docker容器添加环境变量
docker exec -i CONTAINER_ID /bin/bash -c "export VAR1=VAL1 && export VAR2=VAL2 && your_cmd"
试了,没成功,哈哈
2、在创建docker容器的时候,添加参数
docker run --env ANDROID_HOME=/opt/androidhome/sdk --env PATH=/opt/androidhome/sdk/emulator:$PATH --env PATH=/opt/androidhome/sdk/tools:$PATH --env PATH=/opt/androidhome/sdk/tools/bin:$PATH --env PATH=/opt/androidhome/sdk/platform-tools:$PATH --name jenkins-agent -d -p 8008:8080 -p 50000:50000 jenkins/jenkins
这个试了,容器能创建,但是跑不起来,因为我是使用jenkins/jenkins镜像,直接添加参数,好像就找不到Java的环境了,折腾了一会后还是放弃了。
3、使用dockerfile来创建镜像
这个的思路就是From现在的镜像,在里面添加自己的配置。
# 基于已有的镜像 jenkins/jenkins 来构建新镜像
FROM jenkins/jenkins
# 设置变量
ENV USR_LOCAL="/usr/local" \
ANDROID_HOME="${USR_LOCAL}/AndroidSdk" \
SDK_TOOL_URL="https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip"
# 创建android sdk目录,并下载 sdkmanager
RUN mkdir -p ${ANDROID_HOME} \
&& cd $ANDROID_HOME \
&& curl -o sdk.zip $SDK_TOOL_URL \
&& unzip sdk.zip \
&& rm sdk.zip
# 安装android sdk其他package, 输入y是因为此处会有一个licence,需要用户同意后才会安装
RUN echo y | ${ANDROID_HOME}/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3"
# 设置环境变量: 把 android sdk 路径加入到 PATH 中
ENV PATH ${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools:${PATH}
FROM: 表是基于某个镜像
MAINTAINER: 用户信息
ENV: 添加变量
RUN:执行命令
其他的语法大家可以自行查找。以上dockerfile就是下载sdkmanage,然后通过sdkmanage来加载对应的sdk,为啥不直接下载sdk呢?我记得现在没有对应的版本下载链接,所以一般通过sdkmanage来下载。下载的逻辑分三步:
1、下载sdkManage,解压。
2、执行解压后的sdkmanage,从而下载对应的sdk。
3、设置环境变量。
运行时,报错,发现没有操作文件的权限,在dockerfile加入USER root ,即可,执行sdkmanager的命令时,发现还是报错, Exception in thread "main" java.lang.NoClassDefFoundError:javax/xml/bind/annotation/XmlSchema,一般这种基本是java版本导致的,默认环境是11的,XmlSchema在11的时候已经被删了,所以要使用这个sdkmanage需要构建8的环境。咋办,那就先构建Java8的环境,拉取完之后,再还原成11的。
#缓存当前配置
ARG CACHE_PATH=$PATH
ARG CACHE_CLASSPATH=$CLASSPATH
ARG CACHE_JAVA_HOME=$JAVA_HOME
#配置java环境变量
RUN mkdir /usr/lib/jvm
ADD jdk-8u202-linux-arm64-vfp-hflt.tar.gz /usr/lib/jvm
ENV JAVA_HOME /usr/lib/jvm/jdk1.8.0_202
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
#还原java环境变量
ENV JAVA_HOME $CACHE_JAVA_HOME
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib
这里单独说下ADD,此关键字的结构是 <ADD 本机地址文件 docker目录> 注意一下,就是本机地址文件要跟dockerfile文件放在同一层级,不然会找不到,因为ADD 只读取dockerfile文件目录上下文,然后docker目录不存在就mkdir一下,举个例子,假如我们的dockerfile文件在桌面,那我们就把本机文件也放在桌面,然后cd desktop,执行docker build xxx即可。ADD是再带解压的。
执行过程中遇到无法执行unzip,可以下加入以下命令。
# 解决unzip问题
RUN apt --fix-broken install
RUN apt-get update
RUN echo y | apt-get upgrade --fix-missing
RUN echo y | apt-get install zip
贴一下完成的dockerfile文件
# 基于已有的镜像 jenkins/jenkins 来构建新镜像
FROM jenkins/jenkins
MAINTAINER xxx
USER root
# 解决unzip问题
# RUN apt --fix-broken install
# RUN apt-get update
# RUN echo y | apt-get upgrade --fix-missing
# RUN echo y | apt-get install zip
ARG CACHE_PATH=$PATH
ARG CACHE_CLASSPATH=$CLASSPATH
ARG CACHE_JAVA_HOME=$JAVA_HOME
#配置java环境变量
RUN mkdir /usr/lib/jvm
ADD jdk-8u202-linux-arm64-vfp-hflt.tar.gz /usr/lib/jvm
ENV JAVA_HOME /usr/lib/jvm/jdk1.8.0_202
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
# 设置变量
ENV USR_LOCAL="/usr/local" \
ANDROID_HOME="${USR_LOCAL}/AndroidSdk" \
SDK_TOOL_URL="https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip?utm_source=androiddevtools&utm_medium=website"
# 创建android sdk目录,并下载 sdkmanager
RUN mkdir -p ${ANDROID_HOME} \
&& cd $ANDROID_HOME \
&& curl -o sdk.zip $SDK_TOOL_URL \
&& unzip sdk.zip \
&& rm sdk.zip
# RUN echo y | ${ANDROID_HOME}/tools/bin/sdkmanager --update
# 安装android sdk其他package, 输入y是因为此处会有一个licence,需要用户同意后才会安装
RUN echo y | ${ANDROID_HOME}/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3"
ENV PATH $CACHE_PATH
ENV CLASSPATH $CACHE_CLASSPATH
# 设置环境变量: 把 android sdk 路径加入到 PATH 中
ENV PATH ${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools:${PATH}
USER jenkins
#还原java环境变量
ENV JAVA_HOME $CACHE_JAVA_HOME
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib
创建镜像:
docker build -t android_jenkins -f android_dockerfile.dockerfile .
启动容器:
docker run -d --name my_docker -url=http://172.16.200.9:8008 --user=root -m 16384m -p 8008:8080 -p 50000:50000 android_jenkins
-m 16384m内存分配,多一些,不然打包可难受了,哈哈
就启动了
接下来在浏览器中打开,构建包时发现已经有Android环境了。
2、打包流程
以release包为例,打包简单说就是构建出一个apk文件,复杂点说,构建出原始包之后,我们要生成多渠道包,同时给它加固,重签名,然后归档出一个zip,然后因为应用市场原因,我们还要构建32位和64位的包,然后给运营,这个过程也可以通过脚本流程化。
juejin.cn/post/721920…这篇里面也说到了Pipeline,用Pipeline来描述这个流程,下面就简单叙述下。
Init
初始化,做一些简单的参数校验,或者其他的,比如的构建依赖一些参数,可以在这做一下合法性检测。
Checkout
stage('Checkout') {
steps {
git url: "${env.GIT_URL}",
branch: "${env.BRANCH_NAME}",
credentialsId: "${env.GIT_CREDENTIAL_ID}"
}
}
拉代码,没啥好说,env.GIT_URL可以获取当前的分支名,都是全局的变量,可以在下面的environment中配置。
environment {
GIT_URL = 'https://henhen@dev.azure.com/henhen/meow-interacation-fe/_git/cpp_new'
GIT_CREDENTIAL_ID = 'git_cpp'
JVM_OPTS = '-Xmx4096m'
GRADLE_OPTS = '-Xmx12288m -XX:+HeapDumpOnOutOfMemoryError -Dorg.gradle.caching=true -Dorg.gradle.configureondemand=true -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false'
}
我是用的是多分支流水,通过GIT_URL可以获取到分支名,如果不是多分支的,可以设置parameters,然后在parameters编辑自己想打包的分支,可通过params.xxx获取,xxx对应你设置的name,比如:
parameters {
text( name: 'BRANCH', description: '分支', defaultValue: env.BRANCH_NAME, trim: true)
}
打包获取分支就可以通过params.BRANCH获取。
Build
这个就是构建包了,上面我说还有加固,重签名等操作,我放在一起了,你也可以切其他的stage来处理
stage('Build') {
steps {
timeout(time: 50, unit: 'MINUTES') {
script {
if(params.GRADLE_BUILD_TYPE == "构建测试包"){
sh './gradlew buildDebug'
}else {
sh './gradlew buildRelease'
}
}
}
}
}
我自定义了task来构建包,根据parameters中配置的参数来确定构建包类型,相对而言,构建release包更全一些,下面就一release包举例。
task buildRelease{
doLast {
assembleApk("Release")
}
}
def assembleApk(String buildType) {
exec {
if (buildType == "Debug") {
xxx
} else {
commandLine "sh", "-c", "cd ~ && cd ${project.rootDir} && ./gradlew buildReleasePkg"
}
}
}
task buildReleasePkg{
doLast {
exec {
commandLine "sh", "-c", "cd ~ && cd ${project.rootDir} && chmod +x ./jenkins_release.sh && ./jenkins_release.sh"
}
}
}
task noticeRelease(){
doLast {
exec {
def branch = System.properties["branch"]
println("分支"+branch)
commandLine "sh", "-c", "cd ~ && cd ${project.rootDir} && chmod +x ./build_notice.sh && ./build_notice.sh $branch"
}
}
}
上面只是task链调用,最终走到jenkins_release.sh这个文件。
#!/bin/sh
# 移除所有apk文件,打包时已存在,则会打包失败
find ./app/build/outputs/apk -type f -name "*release*.apk" | xargs rm -f
./gradlew clean
if ./gradlew assembleRelease; then
printf "打包成功\n"
if ! find ./app/build/outputs/apk -type f -name "*release*.apk" | xargs python3 legu/legu_jenkins.py jiagu2; then
printf "\033[32m 加固失败 \033[0m\n"
return 1
fi
else
printf "\033[31m 打包失败! \033[0m\n"
fi
上述加固的流程用到了python,所以记得在容器中配置python环境,这个可以在dockerfile中配置,也可以后续再容器中通过命令手动配置,同时记得pip3 install 你所有依赖的库,贴一下在容器安装python命令
// 先执行更新软件列表
apt-get update
// 安装依赖
apt-get -y install gcc automake autoconf libtool make
apt-get -y install make*
apt-get -y install zlib*
apt-get -y install openssl libssl-dev
apt-get install sudo
// 切换到 /usr/local 目录,将下载的安装包存放在此目录里
cd /usr/local
wget https://www.python.org/ftp/python/3.8.12/Python-3.8.12.tgz
// 解压
tar -xvf Python-3.8.12.tgz
// 创建 python3 文件夹
mkdir python3
// 切换到解压后的 Python-3.8.12 目录下
cd Python-3.8.12
// 执行如下代码安装
// --prefix 选项是配置安装的路径,执行后所有资源文件放在 /usr/local/python3 的路径里。
./configure --prefix=/usr/local/python3 --enable-optimizations
make
make install
// 创建软链接
ln -s /usr/local/python3/bin/python3.8 /usr/bin/python3
ln -s /usr/local/python3/bin/pip3 /usr/bin/pip3
//查看 Python 3 是否安装成功
python3 -V
pip3 -V
// 如果上述包pip3:command not found 执行下面的命令
sudo apt-get install python3-pip
// 如果提示找不到包 就更新下
sudo apt-get update
加固以及签名的代码就不贴了,这个是个固定通用流程。因为签名需要用到Android sdk的biuild-tools工具zipalign,所以别忘了配置biuild-tools的环境变量,如果忘记了,也可以在命令上直接带上路径,比如/AndroidSdk/build-tools/32.0.0/zipalign。 加固以及签名完毕后需要压缩一下,因为有很多包,各渠道以及各种位数的有好几十个,为了方便,归档的时候就压缩一下,调用一下方法
def zip_apks() -> int:
'''
执行 生成apk 压缩文件
'''
# 根据输出需求分32 64 all目录
sign_sh = legu_dir + '/zipApks.sh {}'.format(jiagu_record)
os.chmod(legu_dir + '/zipApks.sh', stat.S_IXUSR)
ret = os.system(sign_sh)
if ret != 0:
print('apk压缩失败')
return ret
记得使用os.chmod授权一下,不然会报错,提示没有权限。
#!/bin/sh
printf "开始生成压缩文件: ${1}"
cd "legu"
zip_file="./apks.zip"
if [ -f "$zip_file" ]; then
rm -f "$zip_file"
echo "$zip_file exist"
fi
zip -r "./apks.zip" "./$1/32" "./$1/64" "./$1/all"
exit 0
ArchiveArtifacts
这个就是归档了
stage('ArchiveArtifacts'){
steps {
archiveArtifacts artifacts: 'legu/*.zip,app/build/outputs/apk/arm64/debug/*.apk,app/build/outputs/apk/arm/debug/*.apk', followSymlinks: false, onlyIfSuccessful: true
}
}
这里你也可以自定义自己的插件(www.jenkins.io/zh/doc/deve… ),自定义自己的归档逻辑,默认的归档是在本机的,一般可以自定义传到oss上去,这里只归档一个链接即可。
Notice
发通知,默认有邮件通知,配置一下
post {
always {
emailext subject: '${DEFAULT_SUBJECT}',
body: '${DEFAULT_CONTENT}',
recipientProviders: [developers(), requestor()]
}
}
不过从及时性考虑,发到群里比较可靠一些。
#!/bin/bash
######钉钉通知######
DTALK_TITLE="android-正式包更新"
DTALK_COMMIT_ID=`git log -1 --pretty=format:"%h"`
DTALK_COMMIT_AUTH=`git log -1 --pretty=format:"%an"`
DTALK_COMMIT_TIME=`git log -n1 --pretty="format:%cd" --date=format:"%Y-%m-%d %H:%M:%S"`
DTALK_COMMIT_MSG=`git log -1 --pretty=format:"%s" --no-merges`
DTALK_IMAGE="默认图"
DTALK_WEBHOOK="钉钉群链接,自行配置"
DTALK_HEADER="Content-Type: application/json"
DTALK_DOWNLOAD="归档的地址"
DTALK_BTN_TEXT="jenkins链接"
DTALK_STR="{"msgtype":"actionCard","actionCard":{"title":"${DTALK_TITLE}","text":" \n #### ${DTALK_TITLE} \n\n ${DTALK_COMMIT_ID} \n\n ${DTALK_COMMIT_AUTH} \n\n ${DTALK_COMMIT_TIME} \n\n ${DTALK_COMMIT_MSG}","singleTitle":"${DTALK_BTN_TEXT}","singleURL":"${DTALK_DOWNLOAD}"}}"
curl -X POST -H "${DTALK_HEADER}" -d "${DTALK_STR}" "${DTALK_WEBHOOK}"
最终经过漫长的等待,产物终于出来了
PS:建议给docker容器多一些内存,以及多一些磁盘空间,不然很难跑起来
3、WebHook配置
打包整体已经配置完了,还有一些场景我们也要配置一下。比如自动打包。对于一个自动化流程,我们尽量做到自动,所以打包的触发我们也希望是自动的,同时Jenkins也是支持这个操作的,只是我们需要一些配置
1、安装Generic Webhook Trigger插件
跟其他插件一样,直接在 系统设置 -> 插件管理 搜索Generic Webhook Trigger 安装即可。
2、配置Jenkins的webhook
一般我们就是直接在构建的配置中勾选配置即可
具体这些配置就是读取通知的结构的字段规则,因为我使用的是jenkinsfile文件的,所以直接贴下jenkinsfile配置:
triggers{
GenericTrigger(
genericVariables: [
[key: 'ref', value: '$.ref'],
[key: 'user_name', value: '$.user_name'],
[key: 'namespace', value: '$.path_with_namespace']
],
causeString: 'Triggered on $ref',
token: 'webhook',
printContributedVariables: true,
printPostContent: true,
silentResponse: false
)
}
上述直接贴到pipeline中即可,注意token字段,这个跟代码仓库配置的webhook的URL里面的参数要对齐。像genericVariables里面的参数就是通知内容了,说实话,我也不知道具体有哪些,因为我的场景是收到通知就打包,所以不关心了。
PS:上述jenkin分支中的配置页面,我是无法编辑的,应该是使用多分支流水的原因,构建规则就使用jenkinsfile了,这个应该就无法编辑了(我猜的),网上说的都是直接编辑这里,最终试了,还是添加在jenkinsfile中
3、配置代码仓库的webhook
以gitlee为例(gitlab也是差不多的),打开你的仓库,在“管理”下面,选择WebHooks,填入URL(就是你打包服务的地址,后面拼接上/generic-webhook-trigger/invoke?token=webhook,因为我在jenkinsfile的GenericTrigger里面填的是“webhook”,所以这里就是“token=webhook”,根据你自己的token填上就好了),密码可以不填。最后保存一下即可。
以上是基本流程,当然如果你是在本地机器搭建的jenkins服务,那这个URL填了也是没用的,因为访问不到你本地的机器,所以我们可以使用ngrok,没错,使用内网穿透,不过注意哈,有的公司不允许使用内网穿透。
简单说下ngrok使用。
1、安装
ngrok.com/download 直接去这里,因为我使用的是docker环境是linux的,所以直接用以下命令
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null && echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list && sudo apt update && sudo apt install ngrok
网址里面都有,可以自行查看。
2、配置
配置前记得注册一下,dashboard.ngrok.com/signup,注册成功登录后,就可以下看到下图
然后在本地执行以下命令:
生成ngrok.yml 配置文件
ngrok config add-authtoken <your-authtoken>
配置端口映射
ngrok http 8080
因为Jenkins的端口默认是8080,所以这里就填8080。
成功以后会出现以下页面
然后我们把Forwarding中的链接填到Jenkins的Jenkins Location中
最后测