0代码搭建Android打包平台🐶(后篇)

2,663 阅读7分钟

前言

本篇是接着上篇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":"![screenshot](${DTALK_IMAGE}) \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中

最后测