Jenkins自动化构建Android APK并上传到fir.im
Jenkins自动化构建的作用:实现了Git仓库下所有的代码分支管理,创建多分支流水线项目后,只要Git目标仓库分支的代码有更新,就会去自动触发打包上传apk流程,所有的分支只需一个pipeline编写的脚本文件jenkinsFile 加上构建参数配置文件config.yaml就可以管理Git分支的自动化构建打包上传步骤。首先说明完成这个流程需要用到哪些工具:
一 .准备工具
1.JDK(8/11) , SDK ,Gradle ,Git 下载并配置环境变量
2.jenkin 安装
windows版本的
然后直接下一步到安装完成,浏览器打开http://localhost:8080/ 显示jenkins主页。
①配置 Global Tool Configuration 相关项目的 JDK,SDK,Gradle ;配置Gradle环境变量时要注意一个坑,gradle版本要与编译的项目的Gradle版本相同,否则会出现时SDK License 问题;
②安装BlueOcean所有的插件方便图形化界面管理
③安装所有推荐的插件
④创建项目 ,选择多分支流水线 ,配置git仓库地址和登录凭证,设置构建相关参数等
⑤配置完运行报错找不到资源,这里有个坑,就是需要手动的修改jenkins全局项目的workspaceDir,因为jenkins安装的路径在系统盘路径超长,运行时会报错找不到资源,这是windows系统最长文件路径有限制,我们需要修改jenkins的config.xml文件里的workspaceDir路径,然后在Manager jenkins 里点击Reload Configuration from Disk应用配置生效就可以了
3.了解PipeLine语法,
pipeline
编写jenkinsFile文件,jenkinsFile文件里决定了项目构建的各个步骤,比如Git仓库各个分支在代码提交后是否触发自动化构建时,打包方式,打包完成后执行gradle脚本任务等
4.Python
安装python环境 安装pip 安装Python requests库, Requests是一常用的http请求库,它使用python语言编写,可以方便地发送http请求,如果还没有安装pip,这个链接 Properly Installing Python 详细介绍了在各种平台下如何安装python以及setuptools,pip,virtualenv等常用的python工具,可以使用下面的命令来进行安装:
pip install requests
这些都是为了成功执行python脚本完成自动化上传apk到fir.im 准备的,这里也有个坑,当使用python 命令执行脚本时,jenkins报错找不到资源文件,其实就是jenkins不识别配置的python环境变量,只能把python指定为全路径后才可以。
5.fir.im 账号
获取api_token,在以后的脚本调用上传APK接口时,fir.im的域名换了注意得用最新的api.bq04.com,否则接口报错无法上传apk
6.插件pipeline utility steps 插件安装
为了在jenkinsFile读取yaml文件配置,可项目中公用一套jenkins文件然后根据config.yaml中的配置构建apk
二.编写JenkinsFile与config.yaml
如下是Android demo项目的JenkinsFile文件的代码:
def loadValuesYaml(x){//读取项目根目录下的config.yaml 配置
def valuesYaml = readYaml (file: 'config.yaml')
return valuesYaml[x];
}
pipeline {
//agent节点 多个构建从节点 有的只配置了Android环境用于执行Android项目构建,有的只能执行iOS项目构建,有的是用于执行Go项目
//通过对节点声明不同的标签label,然后在我们的构建中指定标签,这样Jenkins就会找到有对应标签的节点去执行构建了
agent any
options {//超时了,就会终止这次的构建 options还有其他配置,比如失败后重试整个pipeline的次数:retry(3)
timeout(time: 1, unit: 'HOURS')
}
environment{
//一组全局的环境变量键值对 用在stages 使用在“调用方式为${MARKET}” 注意只能在“ ”中识别
//这里的变量声明跟在jenkins 服务器配置的全局环境变量是一样的
MARKET = loadValuesYaml('market')
BUILD_TYPE = loadValuesYaml('buildType')
}
stages {//这里我们已经有默认的检出代码了 开始执行构建和发布
//可以根据分支配置构建参数 最好的方式时从一个yaml文件中获取对应的配置文件
stage('readYaml'){
steps{
script{
println MARKET
println BUILD_TYPE
}
}
}
stage('Build master APK') {
when {
branch 'master'//分支名
}
steps {
//gradle 命令 windows 使用bat ,linux 使用sh 执行
bat "./gradlew clean assemble${MARKET}${BUILD_TYPE}"//打包
}
post {
failure {
echo "Build master APK Failure!"
}
success {
echo "Build master APK Success!"
}
}
}
stage('Build dev APK') {
when {
branch 'dev-hcc'
}
steps {
bat "./gradlew clean assemble${MARKET}${BUILD_TYPE}"
}
post {
failure {
echo "Build dev APK Failure!"
}
success {
echo "Build dev APK Success!"
}
}
}
stage('ArchiveAPK') {//存储的apk
steps {
archiveArtifacts(artifacts: 'app/build/outputs/apk/**/*.apk', fingerprint: true, onlyIfSuccessful: true)
}
post {
failure {
echo "Archive Failure!"
}
success {
echo "Archive Success!"
}
}
}
stage('Report') {//显示提交信息
steps {
echo getChangeString()
}
}
stage('Publish'){//发布fir.im
steps{
bat './gradlew apkToFir'
}
post {
failure {
echo "Publish Failure!"
}
success {
echo "Publish Success!"
}
}
}
}
}
//report 提交日志
@NonCPS
def getChangeString() {
MAX_MSG_LEN = 100
def changeString = ""
echo "Gathering SCM Changes..."
def changeLogSets = currentBuild.changeSets
for (int i = 0; i < changeLogSets.size(); i++) {
def entries = changeLogSets[i].items
for (int j = 0; j < entries.length; j++) {
def entry = entries[j]
truncated_msg = entry.msg.take(MAX_MSG_LEN)
changeString += "[${entry.author}] ${truncated_msg}\n"
}
}
if (!changeString) {
changeString = " - No Changes -"
}
return changeString
}
如下是config.yaml的内容
market: Google
buildType: Debug
三.编写模块下的build.gradle 里的Task
放在android{}代码块内
task apkToFir {
//dependsOn 'assembleDebug'
doLast {
//def upUrl = "http://api.fir.im/apps"
def upUrl = "http://api.bq04.com/apps"
def appName = "jenkinsDemo"
def bundleId = project.android.defaultConfig.applicationId
def verName = project.android.defaultConfig.versionName
def apiToken = "******************"
def iconPath = "app/src/main/res/mipmap-hdpi/ic_launcher.png"
def apkPath = "app/build/outputs/apk/myAPP.apk"
def buildNumber = project.android.defaultConfig.versionCode
def changeLog = "版本更新日志"
//执行Python脚本
def pythonPath = "c:\\users\\xxx\\appdata\\local\\programs\\python\\python39\\python.exe"
def process = "${pythonPath} upToFir.py ${upUrl} ${appName} ${bundleId} ${verName} ${apiToken} ${iconPath} ${apkPath} ${buildNumber} ${changeLog}".execute()
println("开始上传至fir")
//获取Python脚本日志,便于出错调试
ByteArrayOutputStream result = new ByteArrayOutputStream()
def inputStream = process.getInputStream()
byte[] buffer = new byte[1024]
int length
while ((length = inputStream.read(buffer)) != -1) {
result.write(buffer, 0, length)
}
println(result.toString("UTF-8"))
println "上传结束 "
}
}
四.Python脚本执行上传fir.im任务
以下是upToFir.py的代码:
# coding=utf-8
# encoding = utf-8
import requests
import sys
def upToFir():
# 打印传递过来的参数数组长度,便于校验
upUrl = sys.argv[1]
appName = sys.argv[2]
bundleId = sys.argv[3]
verName = sys.argv[4]
apiToken = sys.argv[5]
print (apiToken)
iconPath = sys.argv[6]
apkPath = sys.argv[7]
buildNumber = sys.argv[8]
changeLog = sys.argv[9]
print(apkPath)
queryData = {'type': 'android', 'bundle_id': bundleId, 'api_token': apiToken}
iconDict = {}
binaryDict = {}
# 获取上传信息
try:
response = requests.post(url=upUrl, data=queryData)
json = response.json()
iconDict = (json["cert"]["icon"])
binaryDict = (json["cert"]["binary"])
except Exception as e:
print(e.message)
# 上传apk
try:
file = {'file': open(apkPath, 'rb')}
param = {"key": binaryDict['key'],
'token': binaryDict['token'],
"x:name": appName,
"x:version": verName,
"x:build": buildNumber,
"x:changelog": changeLog}
req = requests.post(url=binaryDict['upload_url'], files=file, data=param, verify=False)
print(req.status_code)
except Exception as e:
print(e.message)
# 上传logo
try:
file = {'file': open(iconPath, 'rb')}
param = {"key": iconDict['key'],
'token': iconDict['token']}
req = requests.post(url=iconDict['upload_url'], files=file, data=param, verify=False)
print(req.status_code)
except Exception as e:
print(e.message)
if __name__ == '__main__':
upToFir()
经过以上的配置后,当我们提交代码到git仓库时,就具备了构建的条件了,我们需要配置下Scan 多分支流水线 触发器检查时间间隔,只要发现代码有更新,就会触发构建APK并上传到Fir.im了,我们还可以在BlueOcean里看到流水线的相关步骤执行耗时等,如图
五.local.properties参数配置
我们的项目会把一些配置等放在本地的local.properties,如项目准备编译的模块,渠道,编译特定模块的开关等。为了能够在每个分支都能够获取这些配置的参数 ,我们在Jenkins上通过系统配置全局属性环境变量的方式同步配置local.properties参数。然后再通过jenkinsFile获取到这个变量,最后通过gradle传参的方式,再gradle构建中去动态获取这些参数。
在JenkinsFile 里获取配置的环境变量如:env.IS_JENKINS,env.LEGEND_KEY_ALIAS,env.LEGEND_KEY_PASSWORD,示例代码如下:
pipeline {
agent any
options {//超时了,就会终止这次的构建 options还有其他配置,比如失败后重试整个pipeline的次数:retry(3)
timeout(time: 1, unit: 'HOURS')
}
environment{//一组全局的环境变量键值对 用在stages 使用在“调用方式为${MARKET}” 注意只能在“ ”中识别
MARKET = loadValuesYaml('market')
BUILD_FAV = loadValuesYaml('buildFav')
BUILD_TYPE = loadValuesYaml('buildType')
BUILD_ENVIRONMENT = loadValuesYaml('build.environment')
BUILD_MODULE = loadValuesYaml('build.module')
COMPILE_SENSORS_SDK = loadValuesYaml('compileSensorsSdk')
BUILD_NUM = loadValuesYaml('build.num')
BUILD_SWITCH = loadValuesYaml('build.switch')
BUILD_ALL_DEBUG = loadValuesYaml('buildAllDebug')
}
stages {//这里我们已经有默认的检出代码了 开始执行构建和发布
//可以根据分支配置构建参数 最好的方式时从一个yaml文件中获取对应的配置文件
stage('readYaml'){
steps{
script{
println MARKET
println BUILD_FAV
println BUILD_TYPE
println BUILD_ENVIRONMENT
println BUILD_MODULE
println COMPILE_SENSORS_SDK
println BUILD_NUM
println BUILD_SWITCH
println BUILD_ALL_DEBUG
}
}
}
stage('Report') {//显示提交信息
steps {
echo getChangeString()
}
}
stage('Build Apk') {
when{
allOf {
branch 'dev';
environment name:'BUILD_SWITCH',value:'open'
environment name:'BUILD_All_DEBUG',value:'false'
}
}
steps {
sh "chmod +x gradlew"
sh "./gradlew clean assemble${BUILD_FAV}${BUILD_TYPE} -DisJenkinsParam=${env.IS_JENKINS} -DbuildTypeParam=${BUILD_TYPE} -DbuildModuleParam=${BUILD_MODULE} -DmarketParam=${MARKET} -DcompileSensorParam=${COMPILE_SENSORS_SDK} -DbuildEnvironmentParam=${BUILD_ENVIRONMENT} -DbuildNumParam=${BUILD_NUM} -DxsdqKeyAlias=${env.XSDQ_KEY_ALIAS} -DxsdqKeyPwd=${env.XSDQ_KEY_PASSWORD} -DxsdqKeyStorePwd=${env.XSDQ_KEYSTORE_PASSWORD}"//env.LEGEND_KEY_ALIAS env.LEGEND_KEY_PASSWORD env.LEGEND_KEYSTORE_PASSWORD
}
post {
failure {
echo "dev APK Failure!"
}
success {
echo "dev Success!"
}
}
}
stage('Publish xsdq'){//发布fir.im
when{
allOf {
branch 'dev';
environment name:'BUILD_SWITCH',value:'open'
environment name:'BUILD_All_DEBUG',value:'false'
}
}
steps{
sh "chmod +x gradlew"
sh "./gradlew apkToFir -DisJenkinsParam=${env.IS_JENKINS} -DbuildTypeParam=${BUILD_TYPE} -DbuildModuleParam=${BUILD_MODULE} -DmarketParam=${MARKET} -DcompileSensorParam=${COMPILE_SENSORS_SDK} -DbuildEnvironmentParam=${BUILD_ENVIRONMENT} -DbuildNumParam=${BUILD_NUM} -DxsdqKeyAlias=${env.XSDQ_KEY_ALIAS} -DxsdqKeyPwd=${env.XSDQ_KEY_PASSWORD} -DxsdqKeyStorePwd=${env.XSDQ_KEYSTORE_PASSWORD}"
}
post {
failure {
echo "Publish Failure!"
}
success {
echo "Publish Success!"
}
}
}
}
}
jenkinsFile传参为:
sh "./gradlew apkToFir -DisJenkinsParam=${env.IS_JENKINS}"
在项目的gradle文件中获取参数的方式为
def isJenkins = System.getProperty("isJenkinsParam")
每条gradle命令都必须包含所有需要传递的参数否则会拿不到某些参数
六.根据config.yaml修改服务器的local.properties 文件
这里只说可行性,一般我们不会这么去做,服务器并没有local.properties文件,如果我们从本地的项目里copy一份local.properties到jenkins服务器对应的workSpace项目的根路径下,然后通过JenkinsFile读取config.yaml的参数,再把对应的键值写入到local.properties,也能够实现动态配置,麻烦的是服务器每个分支代码都得拷贝这个文件,非常繁琐。不想了解的可以跳过以下步骤。 下面会介绍如何实现这个流程:
1.首先我们会使用Pipeline自带的readFile和writeFile
思路是这样的,先通过readFile方法获取到原来文件的内容,返回是一个字符串对象。然后根据换行符把字符串对象给切片,拿到一个list对象,遍历这个list,用if判断,根据Key找到这行,然后改写这行。由于这我们只是在内存改写,所以需要提前定义一个list对象来把改写和没有改写的都给add到新list,然后定义一个空字符串,遍历新list,每次拼接一个list元素都加上换行符。这样得到字符串就是一个完整内容,然后把这个内容利用writeFile方法写回到原来的config文件,实现此需求的editFile.groovy代码如下:
import hudson.model.*;
def setKeyValue(key, value, file_path) {
// read file, get string object
file_content_old = readFile file_path
println file_content_old
//遍历每一行,判断,然后替换字符串
lines = file_content_old.tokenize("\n")
new_lines = []
lines.each { line ->
if(line.trim().startsWith(key)) {
line = key + "=" + value
new_lines.add(line)
}else {
new_lines.add(line)
}
}
// write into file
file_content_new = ""
new_lines.each{line ->
file_content_new += line + "\n"
}
writeFile file: file_path, text: file_content_new, encoding: "UTF-8"
}
return this
2.local.properties 配置:
sdk.dir=C\:\\Users\\xxx\\AppData\\Local\\Android\\Sdk
market=google
build.module = test
build.environment=product
compileSensorsSdk = false
3.config.yaml配置:
market: Google
buildType: Debug
build.environment: test
build.module: myapp
4.JenkinsFile修改后代码:
def loadValuesYaml(x){
def valuesYaml = readYaml (file: 'config.yaml')
return valuesYaml[x];
}
pipeline {
//agent节点 多个构建从节点 有的只配置了Android环境用于执行Android项目构建,有的只能执行iOS项目构建,有的是用于执行Go项目
//那这么多不同的节点怎么管理及分配呢?
//那就是通过对节点声明不同的标签label,然后在我们的构建中指定标签,这样Jenkins就会找到有对应标签的节点去执行构建了
//agent { label 'Android'}
agent any
options {//超时了,就会终止这次的构建 options还有其他配置,比如失败后重试整个pipeline的次数:retry(3)
timeout(time: 1, unit: 'HOURS')
}
environment{//一组全局的环境变量键值对 用在stages 使用在“调用方式为${MARKET}” 注意只能在“ ”中识别
MARKET = loadValuesYaml('market')
BUILD_TYPE = loadValuesYaml('buildType')
BUILD_ENVIRONMENT = loadValuesYaml('build.environment')
BUILD_MODULE = loadValuesYaml('build.module')
}
stages {//这里我们已经有默认的检出代码了 开始执行构建和发布
//可以根据分支配置构建参数 最好的方式时从一个yaml文件中获取对应的配置文件
stage('readYaml'){
steps{
script{
println MARKET
println BUILD_TYPE
}
}
}
stage('set local properties'){
steps{
script{
editFile = load env.WORKSPACE + "/editFile.groovy"
config_file = env.WORKSPACE + "/local.properties"
try{
editFile.setKeyValue("market", "${MARKET}", config_file)
editFile.setKeyValue("build.module", "${BUILD_MODULE}", config_file)
editFile.setKeyValue("build.environment", "${BUILD_ENVIRONMENT}", config_file)
file_content = readFile config_file
println file_content
}catch (Exception e) {
error("Error editFile :" + e)
}
}
}
}
stage('Build master APK') {
when {
branch 'master'
}
steps {
bat "./gradlew clean assemble${MARKET}${BUILD_TYPE}"
}
post {
failure {
echo "Build master APK Failure!"
}
success {
echo "Build master APK Success!"
}
}
}
stage('Build dev APK') {
when {
branch 'dev-hcc'
}
steps {
bat "./gradlew clean assemble${MARKET}${BUILD_TYPE}"
}
post {
failure {
echo "Build dev APK Failure!"
}
success {
echo "Build dev APK Success!"
}
}
}
stage('ArchiveAPK') {//存储的apk
steps {
archiveArtifacts(artifacts: 'app/build/outputs/apk/**/*.apk', fingerprint: true, onlyIfSuccessful: true)
}
post {
failure {
echo "Archive Failure!"
}
success {
echo "Archive Success!"
}
}
}
stage('Report') {//显示提交信息
steps {
echo getChangeString()
}
}
stage('Publish'){//发布fir.im
steps{
bat './gradlew apkToFir'
}
post {
failure {
echo "Publish Failure!"
}
success {
echo "Publish Success!"
}
}
}
}
}
//report 提交日志
@NonCPS
def getChangeString() {
MAX_MSG_LEN = 100
def changeString = ""
echo "Gathering SCM Changes..."
def changeLogSets = currentBuild.changeSets
for (int i = 0; i < changeLogSets.size(); i++) {
def entries = changeLogSets[i].items
for (int j = 0; j < entries.length; j++) {
def entry = entries[j]
truncated_msg = entry.msg.take(MAX_MSG_LEN)
changeString += "[${entry.author}] ${truncated_msg}\n"
}
}
if (!changeString) {
changeString = " - No Changes -"
}
return changeString
}
七.总结
Jenkins 多分支流水线自动化构建,可以将构建过程都交给config.yaml去管理,通过pipeLine语法,写jenkinsFile设置项目整体构建步骤,jenkins读取yaml配置文件 ,调用groovy脚本修改本地文件local.properties,执行打包流程,groovy调用python脚本上传fir.im. 通过这些步骤实现各个分支的自动构建apk上传到fir.im.