Jenkins配置-Android自动化打包-Mac版

Jenkins是一款开源CI$CD软件,用于自动化各种任务,包括构建、测试和部署软件
优点:
持续的软件版本发布、测试项目
监控外部调用执行的工作
对于移动端开发来说,使用Jenkins持续化集成,可以帮助开发人员缩短开发周期,开发人员只需要关注开发任务,像给产品、测试人员打包时,这些任务就可以交给Jenkins来做,测试人员可只需要扫描一下二维码安装即可。
先来一张效果图


效果图.jpg

安装

这里我们通过homebrew安装,如果未安装Homebrew,先安装Homebrew,详见Homebrew安装和使用
Homebrew安装完成后,执行以下命令安装Jenkins

brew install jenkins

安装完成后,执行war包

java -jar /usr/local/Cellar/jenkins/2.183/libexec/jenkins.war

这里Jenkins版本号可根据自己的Jenkins版本进行更换
另附启动和关闭Jenkins命令:
启动

jenkins -h

关闭
control + c 快捷键关闭
启动后,先不要急着打开Jenkins的web容器,先去/Library/LaunchDaemons目录下新建一个org.jenkins-ci.plist文件,文件内容如下(可直接拷贝修改JENKINS_HOME值为你自己的路径)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>StandardOutPath</key>
    <string>/var/log/jenkins/jenkins.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/jenkins/jenkins.log</string>
    <key>EnvironmentVariables</key>
    <dict>
      <key>JENKINS_HOME</key>
      <string>/Users/aladin/Documents/Jenkins/Home</string>
    </dict>
    <key>GroupName</key>
    <string>daemon</string>
    <key>KeepAlive</key>
    <true/>
    <key>Label</key>
    <string>org.jenkins-ci</string>
    <key>ProgramArguments</key>
    <array>
      <string>/bin/bash</string>
      <string>/Library/Application Support/Jenkins/jenkins-runner.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>UserName</key>
    <string>jenkins</string>
    <key>SessionCreate</key>
    <true/>
  </dict>
</plist>

启动Jenkins
为避免权限问题,先执行下面的命令:

sudo chown root /usr/local/Cellar/jenkins/2.122/homebrew.mxcl.jenkins.plist

启动完成后,打开浏览器,输入http://localhost:8080会出现如下页面:

jenkins_install_finish.jpg

稍等几分钟,会出现如下页面
unlock_jenkins.jpg

进入到Jenkins提示的目录(我这里是/Users/Shared/Jenkins/Home/secrets/initialAdminPassword)获取管理员密码,在该路径中,非Jenkins用户secrets目录和initialAdminPassword文件时没有读写权限的,将该权限改成只读或读和写:
secret.jpg

edit.jpg

获取到密码后,记得备份下,后期可能会用到

配置

1、安装插件

可以按照推荐的插件安装,也可以自己选择

20190701100537.jpg

这里我们选择安装推荐的插件
install_plugins.jpg

安装完成后,会提示我们创建用户
创建用户.jpg

创建完成后,会提示我们配置Jenkins URL,这个可以根据自身情况进行修改,这里我们先用默认设置http://localhost:8080/
Jenkins插件安装
因为公司项目托管在gitlab上,所以这里需要安装gitlab插件,同时安装Git Parameter插件和Build Name and Description Setter(用于参数化构建)
manage plugins.jpg

plugins install.jpg

安装Dynamic Parameter插件
由于Dynamic Parameter插件有漏洞,在Jenkins中搜索不到,这里给出下载地址,经测试目前只有0.1.1版本能用,该插件下载完成后需要在插件管理-高级-上传插件中进行安装
插件安装完成后,我们就来进行基本环境的配置

2、环境配置
2.1系统配置:
Manage Jenkins.jpg

增加环境变量
进入Manage Jenkins -> Configure System -> 全局属性,勾选Environment variables,增加一对键值


PATH.jpg

其中PATH的值为本机的环境变量,可以在终端执行以下命令查看,为一堆路径:

echo $PATH

Android sdk配置:


Android sdk.jpg

SDK配置中的键必须是ANDROID_HOME,值为你本机的Android SDK目录,这里要注意SDK目录的权限问题,没有权限的话,可能会导致后期构建的时候提示找不到SDK路径。
JDK、Git、Gradle配置:


Global Tool Configuration.jpg

jdk git gradle.jpg
2.2Gradle配置

接下来看下build.gradle中的部分配置

apply plugin: 'com.android.application'

def fileArray = []

def getBuildTime() {
    return new Date().format("yyyy-MM-dd-HH-mm")
}

//是否Jenkins打包
def isJenkins() {
    return "true".equals(IS_JENKINS)
}
//是否是Google渠道
def isGpChannel() {
    return "gp".equals(JENKINS_CHANNEL)
}

//获取Channel
def getJenkinsChannel() {
//    def channels = System.getenv("JENKINS_CHANNEL")
    def channels = JENKINS_CHANNEL
    println("多渠道:" + channels)
//    String channels = "yingyongbao"
    channels.toString().tokenize(',').each { channelItem ->
        android.productFlavors.create(channelItem, {
            manifestPlaceholders = [
                    APP_NAME     : APP_NAME,
                    CHANNEL_VALUE: channelItem,
                    API_DOMAIN   : API_DOMAIN
            ]
            println("当前渠道:" + channelItem)
        })
//        android.sourceSets.main.manifest.srcFile 'src/main/AndroidManifest.xml'
    }
}

android {
    compileSdkVersion 28
    buildToolsVersion "29.0.0"
    defaultConfig {
        String packageName = "com.ywd.jenkinsbuildtest"
        if (isJenkins()) {
            packageName = PACKAGE_NAME
        }
        applicationId packageName
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 100
        versionName "1.0.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        //确保所有的flavors都属于同一维度
        flavorDimensions "default"

//        sourceSets.main {
//            jni.srcDirs = []
//            //LOCAL_LDFLAGS += -fuse-ld=bfd
//            //jni.srcDirs 'src/main/jni'
//            jniLibs.srcDir 'src/main/libs'
//        }
    }

    buildTypes {
        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        preview {
            minifyEnabled false
            zipAlignEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        release {
            minifyEnabled true
            zipAlignEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    getJenkinsChannel()

//    productFlavors {
//        def channel = "gp"
//        println("productFlavors_isJenkins":isJenkins())
//        if(isJenkins()){
//            channel = CHANNEL_VALUE
//        }
//        println(channel)
//        app {
//            manifestPlaceholders = [CHANNEL_VALUE: channel]
//        }
//    }

    applicationVariants.all { variant ->
        variant.outputs.all { output ->
            def appVersion = variant.versionName //版本号
            def buildType = "" //构建类型
            def buildTime = getBuildTime() //构建时间
            println("IS_JENKINS_${IS_JENKINS}")

            if ("true".equals(IS_JENKINS)) {
                appVersion = APP_VERSION
                buildTime = BUILD_TIME
            }

            //构建类型
            if ("debug".equals(variant.buildType.name)) {
                buildType = "Debug"
            } else if ("preview".equals(variant.buildType.name)) {
                buildType = "Preview"
            } else {
                buildType = "Release"
            }

            def fileName = "${appVersion}_${variant.productFlavors[0].name}_${buildTime}_${buildType}.apk"
            def outFile = output.outputFile
            if (outFile != null && outFile.name.endsWith('.apk')) {
                outputFileName = fileName
            }
            fileArray.add(outFile.parentFile.absolutePath + File.separator + fileName)
        }
    }

    //根据不同场景配置不同的AndroidManifest.xml文件
    sourceSets {
        println("==============sourceSets==============")
        main {
            if (isGpChannel()) {
                manifest.srcFile 'src/main/gp/AndroidManifest.xml'
                println("使用Google配置")
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                println("使用默认配置")
            }
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

afterEvaluate {
    //只有Jenkins打包才复制,此处复制到文件下载路径
    if (isJenkins()) {
        assembleDebug.doLast {
            forEachFile(fileArray)
        }

        assemblePreview.doLast {
            forEachFile(fileArray)
        }

        assembleRelease.doLast {
            forEachFile(fileArray)
        }
    }
}

def forEachFile(fileArray) {
    fileArray.forEach { file ->
        renameAndMoveoutApk(file)
    }
}

def renameAndMoveoutApk(orignalFile) {
    //此处路径根据实际情况设置
    def intoFile = rootDir.parentFile.parentFile.parentFile.parentFile.getAbsolutePath() + File.separator + "Shared/apache-tomcat-9.0.21/webapps/Jenkins_apk"
    copy {
        println("开始复制:目标路径:${intoFile}")
        from orignalFile
        into intoFile
//        rename("${android.defaultConfig.versionName}_${android.defaultConfig.versionCode}_","")
    }
}

gradle.properties

IS_JENKINS = false
BUILD_TIME = 2019-09-04
APP_NAME = JenkinsBuildTest
APP_VERSION = 1.0.0
PACKAGE_NAME = com.ywd.jenkinsbuildtest1
# 渠道
JENKINS_CHANNEL = app
# 接口域名
API_DOMAIN = ""
2.3Jenkins项目配置

新建项目


create new project.jpg

填写项目名称,这里我们选择构建一个自由风格的软件项目


create 2.jpg

创建好后,进入项目配置
项目配置.jpg

选择参数化构建,添加参数GitParameter


参数化构建_git.jpg

这里变量名随意,参数类型选择Branch or Tag
select branch.jpg

添加参数,选择Choice Parameter,这里参数名为IS_JENKINS,注意这里参数名要和gradle.properties中定义的相同
IS_JENKINS.jpg

创建参数BUILD_TYPE,这里名字可以随意,参数根据自己项目中定义
BUILD_TYPE.jpg

添加Dynamic Parameter
Dynamic Parameter.jpg

创建参数BUILD_TIME,注意这里参数名要和gradle.properties中定义的相同
并且Dynamic Parameter使用的是Groovy Script


BUILD_TIME.jpg

创建
源码管理
这里我们使用的是Git
添加gitlab账号.jpg

输入仓库地址后,点击添加,添加认证
gitlab_jenkins.jpg

创建完成后,选择刚刚创建的用户凭据,并填写上面参数化构建填好的分支变量名,注意变量名前要加$
源码管理.jpg

选择构建插件
Android使用的是Gradle构建,这里我们选择之前配置好的Gradle版本,并输入以下命令
clean assemble${BUILD_TYPE} --stacktrace

然后勾选Pass all job parameters as Project properties,旧版本是勾选Pass job parameters as Gradle properties


select gradle version.jpg

配置完成后,点击保存,回到项目首页


项目列表.jpg

可以看到,原先的立即构建已经变成了Build with Parameters
Build with Parameters.jpg

配置完成后,点击开始构建,这里我们可以查看控制台输出


控制台输出.jpg

在控制台我们可以看到和Android Studio打包同样的输出结果,最后显示构建成功。
Build Successful.jpg

构建完成后,我们可以在项目目录找到打好的包
build apk.jpg

构建名称
原本的构建名称只是一个编号,对于使用人员来讲,没有辨识度,我们可以在项目的构建环境中进行配置,更改名称,具体操作如下,在构建环境中勾选Set Build Name,并填入上文配置的参数名称
set build name 0.jpg

保存后,我们再次构建查看下结果,构建名称已经改变了


set build name.jpg

经过如上配置,我们的Jenkins打包就可以正常工作了,但是构建完成的包,测试人员该怎么安装呢,不能每次打完包还要我们去项目目录下找到发给他们吧,这是不可能的,让测试区工作区自己找?可不太可能。。。接下来我们卡一下如何将打完的包生成二维码并展示
3、生成二维码并展示
3.1 Tomcat安装及配置

安装并配置Tomcat,详见Mac安装Tomcat
修改配置文件conf/web.xml

<init-param>
    <param-name>listings</param-name>
    <param-value>true</param-value>
</init-param>

把原来的false改为true,此时在webapps下新建个目录,如download,就可以通过浏览器访问里面的内容


download app.jpg
3.2 Python安装及配置

安装Python和pip详见Mac安装Python和pip
安装Pillow
输入命令sudo pip install Pillow,出现如下提示说明已经安装完成

install pillow

3.3 qrcode安装及配置

输入以下命令

pip3 install myqr
install myqr.jpg

以上配置完成后,打开Jenkins,进入Manage Jenkins -> 全局属性,然后新增属性,添加Python全局变量


python env.jpg
3.4 生成二维码

进入项目 -> 配置 -> 构建,增加构建步骤


Execute shell.jpg

填写如下命令

myqr http://172.20.41.235:8888/Jenkins_apk/${APP_VERSION}_${JENKINS_CHANNEL}_${BUILD_TIME}_${BUILD_TYPE}.apk -n ${APP_VERSION}_${JENKINS_CHANNEL}_${BUILD_TIME}_${BUILD_TYPE}.png -v 1 -l L -d /Users/Shared/apache-tomcat-9.0.21/webapps/Jenkins_apk

其中路径和${}里面的参数根据自己的实际情况进行配置
关于qrcode的详细使用,详见Github

3.5 展示二维码

通过myqr命令会在Tomcat下载目录生成一张二维码图片,接下来我们要把二维码图片显示在Jenkins上:
安装插件description setter plugin,安装好后,进入项目->配置->构建后操作,增加构建后操作步骤,选择Set build description

select set build description.jpg

我这里已经设置过了build description,所以是灰色的
选择完成后,描述可以添加HTML标签,所以我们可以将<img src='' />标签加入到描述中,不过这里有个问题,加了img标签后,Jenkins并不会显示二维码图片,这是因为Jenkins出于安全考虑,所有描述信息的Markup Formatter默认是采用Plain text模式,这种模式不会对描述信息中的HTML编码进行解析。
我们可以在Manage Jenkins -> Configure Global Security,将Markup Formatter的设置改为Safe HTML即可。
Description中的描述

<img src='http://172.20.41.235:8888/Jenkins_apk/${APP_VERSION}_${JENKINS_CHANNEL}_${BUILD_TIME}_${BUILD_TYPE}.png' height="200" width="200" /><br><a href="http://172.20.41.235:8888/Jenkins_apk/${APP_VERSION}_${JENKINS_CHANNEL}_${BUILD_TIME}_${BUILD_TYPE}.apk">下载连接</a>

其中地址和参数可根据自身实际情况进行配置。


build description.jpg
4、常见问题

1、

Caused by: java.lang.RuntimeException: The SDK directory '/Users/aladin/Library/Android/sdk' does not exist.

这个路径是SDK默认的路径,刚开始是以为jenkins没有把local.properties文件拉下来,结果这个文件拉下来之后,还是报这个错,后来估计是权限问题,然后将everyone只读权限放在Library目录下就没问题了

参考文章:
Android-解放双手告别测试-使用Jenkins自动化打包
Android使用Jenkins持续集成
Jenkins本地搭建遇到的问题 for Mac

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341