android-automation

Introduction: 利用 Jenkins 实现 Android 自动打包发包
More: Author   ReportBugs   
Tags:

利用 Jenkins 玩转 Android 自动打包发包

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

请尊重原创,转载请注明出处:http://blog.csdn.net/mabeijianxi/article/details/52680283

先看一眼效果图:

功能描述:

可以选择不同的环境与不同的渠道,可以输入显示在 App 上的版本号,打包完成后可自动上传并且生成安装二维码

总体步骤可为:

  1. 下载新版 Jenkins 挂载到 Tomcat
  2. 编写 Python 脚本拉取蒲公英上 Apk 二维码(可选)
  3. 配置项目 build.gradle 里面的脚本
  4. 安装 Jenkins 里面需要用到的一些插件并且配置 Jenkins 参数
脚本配置地址:https://github.com/mabeijianxi/android-automation

正式开撸

一、下载新版 Jenkins 挂载到 Tomcat:

Jenkins下载适合的版本后点击安装,之后在 Tomcat 的 webapps 目录下新建一个 Jenkins 目录,再把刚安装好的 Jenkins 目录打开找到 war 目录,拷贝目录下全部数据到 webapps 下新建的 Jenkins 目录中。还有种启动方式是通过命令启动 jenkins.war 就行了,java -jar jenkins_located_path/jenkins.war。我们可以先为工作空间配置个环境变量。

我使用的是第一种方式,再启动 Tomcat 后访问http://localhost:8080/jenkins就会进入引导进入页面,如果有选择安装插件界面进去后点击安装就行,一会儿就能进入主界面。

二、安装 Jenkins 里面需要用到的一些插件:

这里也没什么技术含量,系统管理->插件管理->管理插件->可选插件: 勾选如下插件:

  1. Branch API Plugin
  2. build timeout plugin
  3. build-name-setter
  4. Credentials Binding Plugin
  5. description setter plugin
  6. Dynamic Parameter Plug-in
  7. Environment Injector Plugin
  8. fir-plugin(可选)
  9. Git plugin(可选)
  10. GIT server Plugin(可选)
  11. Gradle Plugin
  12. Pipeline: Basic Steps
  13. Pipeline: Build Step
  14. Pipeline: Input Step
  15. Pipeline: Nodes and Processes
  16. Pipeline: Stage Step
  17. Post-Build Script Plug-in
  18. SSH Slaves plugin
  19. Subversion Release Manager plugin(可选)
  20. Timestamper
  21. Workspace Cleanup Plugin
  22. Subversion Plug-in(可选)
由于有些插件之间有依赖关系所以没全部列出来,如过有我问题可以对比我的截图。

三、配置 build.gradle:

上面都是枯燥的准备工作,好玩的才刚刚开始,下面的参数传入很有意思,也是我踩了很多坑以后想到的。 进入 Android Studio->打开主 Module 的 build.gradle:

  1. 配置签名信息,不然打的包就没意义了
  2. 以参数化构建你想动态写入的数据。我自己需要动态写入的参数有 versionName、打包时间戳、是来自 Jenkjins 运行还是我们本地打包的标识符,这里很重要,如果是来自 Jenkins 打包那么我们的生成路径肯定是服务器上的路径,否则是我们本地电脑上的路径。把这三个参数与其在本地的默认值定义在 gradle.properties 中,然后在 build.gradle 变能引用。
  3. 动态修改生成的 Apk 路径与名称,因为如果是 Jenkins 打包发包那么名称必须是 Jenkins 传递过来的,不能写死,且唯一,不然没法上传
废话不多说,程序员就哪需要看这么多文字,直接干代码(一下是项目中部分代码):

gradle.build:

def getDate() {
    def date = new Date()
    def formattedDate = date.format('yyyyMMddHHmm')
    return formattedDate
}
def verCode = 14
android {
    compileSdkVersion 22
    buildToolsVersion "23.0.3"
    signingConfigs {
        signingConfig {
            keyAlias 'xxx'
            keyPassword 'xxx'
            storeFile file('xxx')
            storePassword 'xxx'
        }
    }

    defaultConfig {
        applicationId "com.henanjianye.soon.communityo2o"
        minSdkVersion 14
        targetSdkVersion 22
        multiDexEnabled true
        versionCode verCode
        versionName APP_VERSION
        resValue("string", 'app_version', APP_VERSION)
        buildConfigField "boolean", "LEO_DEBUG", "true"
        buildConfigField 'String', 'API_SERVER_URL', RELEASE_API_SERVER_URL
        buildConfigField 'String', 'API_SERVER_URL_MALL', RELEASE_API_SERVER_URL_MALL
        signingConfig signingConfigs.signingConfig
    }
    buildTypes {
        release {
            buildConfigField 'String', 'API_SERVER_URL', RELEASE_API_SERVER_URL
            buildConfigField 'String', 'API_SERVER_URL_MALL', RELEASE_API_SERVER_URL_MALL
            buildConfigField 'String', 'IM_SERVER_HOST', RELEASE_IM_SERVER_HOST
            buildConfigField 'int', 'IM_SERVER_PORT', RELEASE_IM_SERVER_PORT
            buildConfigField "boolean", "LEO_DEBUG", RELEASE_LEO_DEBUG
            minifyEnabled true
            zipAlignEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            buildConfigField 'String', 'API_SERVER_URL', RELEASE_API_SERVER_URL
            buildConfigField 'String', 'API_SERVER_URL_MALL', RELEASE_API_SERVER_URL_MALL
            buildConfigField 'String', 'IM_SERVER_HOST', RELEASE_IM_SERVER_HOST
            buildConfigField 'int', 'IM_SERVER_PORT', RELEASE_IM_SERVER_PORT
            buildConfigField "boolean", "LEO_DEBUG", RELEASE_LEO_DEBUG
        }
    }
    dexOptions {
        javaMaxHeapSize "2g"
    }


    //渠道 Flavors,我这里写了一些常用的
    productFlavors {
        commonsoon {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "commonsoon"]
        }
        zhushou91 {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "zhushou91"]
        }
    }
    allprojects {
        repositories {
            mavenCentral()
            maven { url "https://jitpack.io" }
        }
    }
    //修改生成的 apk 名字
    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def newName
            def timeNow
            def oldFile = output.outputFile
            def outDirectory = oldFile.parent

            if ("true".equals(IS_JENKINS)) {
                timeNow = JENKINS_TIME
                outDirectory = 'G:/Tomcat/webapps/jenkins/apk'
                newName = 'yj-android-v' +
                        APP_VERSION + '-' + variant.productFlavors[0].name + timeNow + '-' + variant.buildType.name + '.apk'
            } else {
                timeNow = getDate()
                if (variant.buildType.name.equals('debug')) {
                    newName = "yj-android-v${APP_VERSION}-debug.apk"
                } else {
                    newName = 'yj-android-v' +
                            APP_VERSION + '-' + variant.productFlavors[0].name + timeNow + '-' + variant.buildType.name + '.apk'
                }
            }

            output.outputFile = new File(outDirectory, newName)

        }
    }
}


gradle.properties:


RELEASE_API_SERVER_URL="xxx"
RELEASE_API_SERVER_URL_MALL="xxx"
RELEASE_IM_SERVER_HOST="xxx"
RELEASE_IM_SERVER_PORT=5222
RELEASE_LEO_DEBUG=false

INSIDE_TEST_API_SERVER_URL="xxx"
INSIDE_TEST_API_SERVER_URL_MALL="xxx"
INSIDE_TEST_IM_SERVER_HOST="xxx"
INSIDE_TEST_IM_SERVER_PORT=5222
INSIDE_TEST_LEO_DEBUG=true

APP_VERSION=2.4.0
IS_JENKINS=false
JENKINS_TIME=''

四、配置 Jenkins 参数

回到主界面->系统管理->Global Tool Configuration: 配置好 JDK 与 Gradle。由于我本地已安装好 JDK 与 Gradle 所以只需为其指定路径即可。

然后回到主界面->新建->构建一个自由风格的项目->ok:

  1. 勾选上参数化构建过程,先点击 Choice 可为其配置可选参数。我的项目需要配置的可选参数有 API 环境、打包渠道、是否来自 Jenkins 打包的标识。
  2. 点击 String Parameter,让使用者可以自定义显示在 App 上的版本号,方便测试。可以再添加一个可输入的标签,最后把这个标签加到构建页的构建名称中。
  3. 点击 Dynamic Parameter,注入 Groovy 脚本,主要是生成时间戳。
  4. 如果是 SVN 用户可选择 List SubVersion tags,然后写地址帐号密码等,这个选项可以让你选择打包 SVN 上不同版本的代码。如果是 Git 用户可参照http://birdinroom.blog.51cto.com/7740375/1404930 上面的配置来拉取不同的版本。
  5. 勾选源码管理中自己所用的管理工具 Git 或者 Subversion,填写好相关信息,当然我们的地址是动态的,所以需要引用上一步所选择的版本,如图。(如果是 Linux 后面得加上/,不然拉取的其目录下的内容不包含 TAG_SVN 这个目录)
  6. 在构建环境中勾选上 Set Build Name,主要是动态生成每次显示在构建页上的名称方便查看。
  7. 在构建栏里面选择 Invoke Gradle Script->选择配置好的 Gradle Version->在 Tasks 中输入 Gradle 命令(没了解过的建议先看下 Gradle 的基本命令),我们先执行一个 clean,然后开始编译,这时候就可以用引用上面配置的一些参数了,这里可以用-P 命令把参数传入,也可以更方便的把下面的 Pass jod parameters as Gradle properties 勾选上,其实内部也是用-Pkey=Value 的命令->在 Root Build script 中动态指定你项目的目录,为什么是动态的呢?这是因为我们上面提供了可选版本的功能,所以拉取的每个版本所放目录是不一样的,想要编译相应的版本就得动态指定编译目录。
  8. 上面的操作如果成功那么其实这时候构建已经可以在 build.gradle 所配置的输出路径中找到相应的 Apk 了,但是这还不够酷,我想把生成的 Apk 放到托管平台,一般用 fir.im 与蒲公英。如果选择 fir.im 那么本步骤略过,我个人推荐蒲公英,因为其可用命令行上传比较方(装)便(B)。下面是蒲公英上传的步骤。
    1)简单的实现方式:参照官方文档https://www.pgyer.com/doc/view/jenkins 。这种方式可以可以简单的通过命令行上传,方便使用,且后续可以动态拿到 Apk 的下载连接,与最新版本的二维码。但是想要拿到每个版本的二维码确实不行的。其实上传成功后返回的 json 数据中 Apk 的下载地址与二维码地址是写死的一个短网址,后续说明解决办法。这里按照官方的步骤,如果是 Linux 那么在增加构建步骤中选择 Execute Shell,而 Windows 环境的需要先下载 curl 工具,然后选择 Execute Windows batch command。get 地址与使用方法都在里面http://www.2cto.com/os/201205/131164.html

    需要注意这里的上传文件的名称也是动态引用的方式,因为每次生成的名称是唯一的。如果是 Shell 那么引用方式是${NAME},而 batch 引用方式是%NAME%。
    2)比较装逼的方式:我不只想要每个版本的下载长链接,我还想要每个版本二维码的长链接。但是就算通过浏览器打开最新上传的 Apk 地址,里面的二维码也只有短链接,不管是 Apk 的短链接还是二维码的它们其实是写死的,永远指向最新版的地址,这就有个问题,当在 Jenkins 中打开非最新版本的 Apk 地址或者二维码图片地址那么都会指向最新的,这就比较蛋疼了。经过仔细观察发现蒲公英 Apk 长链接生成的规则其实是固定字符串+appKey。Apk 在返回的 json 串中可以找到。于是可以利用 python 脚本来执行上传,然后正则配置到 AppKey,然后再拼上静态字符串就拿到了本次存放 Apk 的长链接。当然如果只是为了 Apk 安装长链接用第一种方式即可。现在需要的是二维码的长链接,用 Apk 的长链接发起 Http 请求然后就可以拿到页面数据,通过正则匹配可以匹配到里面二维码的 img 标签地址,但是是短地址,蒲公英只有非最新 Apk 才会在这个页面的 img 标签中返回长地址。于是我打了一个包名与签名同上传 Apk 相同的 Apk,里面没东西,大小只有几十 K。在第一个 Apk 上传完成后马上上传这个小的 Apk,这样再去拉取页面里面的 img 标签就可以得到二维码的长网址了。具体实现看下面 python 代码。

    coding: utf-8

    import os import re import requests import sys

    if len(sys.argv) == 2: mainpath = sys.argv[-1] else: exit()

    temppath = r'G:\mydownload\GETUI_ANDROID_SDK\GETUI_ANDROID_SDK\Demo\PushDemo\yijia\temp.apk'

    cmd = 'curl -F "file=@{path}" -F "uKey=xxx" -F "_api_key=xxx" http://www.pgyer.com/apiv1/app/upload' content = os.popen(cmd.format(path=mainpath)).read() print(content)

    match = re.search('"appKey":"(\w+)"', content)

    if match: appkey = match.group(1)

    content = os.popen(cmd.format(path=temppath)).read()
    
    url = 'http://static.pgyer.com/' + appkey
    html = requests.get(url).text
    
    match = re.search('http://static.pgyer.com/app/qrcodeHistory/\w+', html)
    if match:
        print('appKey#{soonAppkey}#appKeysoon#{soon}#soon'.format(soonAppkey=appkey,soon=match.group()))
    else:
        print('no qrcode')
    

    命令中执行如图代码:
  9. 经过上面的操作后大业马上就成了,接下来就是收集成果的时候了,在增加构建后操作步骤中选择 Set build description,在 Regular expression 中填写正则,然后 Description 中可以引用,这里去匹配的是构建日志中的内容,Description 的内容将显示到构建页面。我们这里如果需要插入下载链接或者二维码的话那么就需要用到 Html 标签,这时候需要去先设置下。步骤:系统管理->Configure Global Security-> Markup Formatter->Safe HTML。如果是选择上面的简单实现方式那么 Regular expression 的正则可以用:"appKey":"(.)","userKey",如果用我的 pyhton 脚本上传,那么正则可以是:appKey#(.)#appKeysoon#(.*)#soon,然后在 Description 中通过\1 或者\2 引用即可。

四、打包喝咖啡

到这里那么最感动的时刻来了,轻轻的点击下保存按钮,这时候你会来到构建界面,轻击 Build with Parameters,就会出现这个界面

配置好参数,按下时代性的构建。从这一刻起谁向你要包你可以点根烟淡淡的说道:妈的,撸着呢,一边自己打去。我反正是受够了要包岁月。

注意事项:

1、参数传递的时候 Jenkins 里面的参数名称需要与 build.gradle 里面的一致,不然没什么卵用。如:

2、在 Linux 下需要注意一些编译工具的权限问题,如 aapt。
3、在服务器上的 SDK 最好与你本地的一致,不然指不定编译的时候会少东西。如果服务器的环境与你本地环境不同就不能直接 copy 过去了,最好的办法是下载一个服务器环境的 SDKManager 或者直接用 Android Studio,然后对照你本地 SDK 里面的数据挨个下载,完了再 copy 到服务器。

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools