Android坑坑之旅Android开发半栈工程师

Android Jenkins的定制化打包平台构建

2017-10-10  本文已影响324人  imesong

Jenkins安装、插件安装 、项目构建

本篇文章的前提是已经完成Jenkins的自动化构建平台搭建,能够完成项目的自动化构建,如果对此还有疑问,请参考下面的链接,搭建Jenkins自动化构建平台。
Android Jenkins自动化构建之路 for Linux
Android Jenkins自动化构建之路 for Windows
Android Jenkins自动化构建之路 for MacOS

参数化构建流程

  1. Jenkins 定制参数,通过Shell脚本写入构建配置文件buildconfig.txt
  2. 新建 buildSrc 构建工程
  3. 解析buildconfig.txt 配置文件
  4. 解析的内容分为以下几种
  1. 业务代码应用配置文件处理之后的内容。

其中 构建工程对buildconfig.txt的解析 是重点和难点。

如果项目构建已经完成,我们的构建过程是这个样子


参数化构建配置完成.png

左边的Build会变成 Build with Parameters,我们在构建项目的时候,会多出一些选项。
这些选项的定义和解析,和项目的业务有关,这里列举了常用的几个。

Jenkins 参数化配置

  1. 选中 Config 选项,进入配置界面
配置界面.png

勾选 This project is parameterized ,下面会有一个 Add Parameter 的下拉箭头选项。

点击 Add Parameter ,如下

Add Parameter.png

这里列举了Jenkins支持的参数类型,基本满足我们所有的需求,最常用的有

添加一个 String Parameter ,如下

String Parameter.png

Name 就是 key 值,获取的时候就用这个对应的名称,VERSION_NAME
Default Value 是默认值,如果构建时不填,就使用默认值。

添加一个 File Parameter

File Parameter.png

SPLASH_RES 用来获取上传文件的文件名使用。

  1. 读取参数
    我们在上面配置了很多参数,怎么读取这些参数,并把这些参数保存成配置文件呢?
读取配置参数.png

Shell 脚本文件内容


Execute shell.png

这段脚本文件,会在项目根目录下生成一个 buildconfig.txt 文件,每次构建,都会写入下面的内容,通过 $ 获取参数内容
生成的配置文件内容如下

PKG_NAME=com.abc2345
APP_NAME=天气
VERSION_NAME=5
VERSION_CODE=5
IS_USE_ALARM=true
IS_CREATE_ALARM_SHORTCUT=true
IS_USE_WIDGET=true
SPLASH_RES=SPLASH.zip
ICON_RES=ICON.zip

完成了参数化构建的配置文件生成,下面就是怎么通过配置工程,解析并使用这些配置文件了。

新建配置文件解析工程

创建配置文件解析工程时,有很多坑需要注意,这里使用的是取巧的一种方法。

  1. 创建 一个Android Library 工程,命名为 buildSrc ,必须是这个名称,不然会有各种各样的问题。
  2. 修改工程目录结构,如下。
groovy model.png

这个目录结构和普通的Android 结构有些区别,src/main 下存放代码文件和资源文件,groovy用来存放 groovy代码。resources下存放入口配置文件。

  1. 修改 buildSrc 中 build.gradle 文件。
    将 build.gradle 中内容修改为如下
apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
    compile gradleApi()
    compile localGroovy()

}

repositories {
    mavenCentral()
}

  1. 配置 gradle.plugin
    在 groovy 下新建一个类,如 AssembleBuildConfigPlugin,实现 Plugin<Project> 接口
    重写 apply(Project project) 方法,
@override
void apply(Project project){
    // 输出一行日志
   println "buildsrc apply"
}
  1. 创建 .properties 文件
implementation-class=com.abc123.AssembleBuildConfigPlugin

等号的右边填写完整路径名称。

  1. 应用 buildSrc 工程。
    在 App Model 的 build.gradle 中,应用 buildSrc 工程
apply plugin: 'com.abc123'

com.abc123 就是我们构建工程的包名。

  1. 编译工程


    编译 buildSrc.png

选中 右侧 buildSrc 中的 build 任务,在gradle console 中可以看到执行情况。
下面是构建过程截图

buildSrc构建过程.png

解析 buildconfig.txt

buildSrc 的入口类就是我们定义的AssembleBuildConfigPlugin
我们定义一个 resolveProfile 的方法,groovy相关的语法大家自己了解,groovy可以兼容java。

    /**
     * 解析配置信息
     * @param project 项目目录
     */
    def resolveProfile(Project rootProject) {

        def ext = rootProject.extensions.findByName('ext')
        def config = ext.getAt("config")
        def android = ext.getAt("android")

        println("show default \tt ext==" + ext + "\t config:" + config + "\t android:" + android)

        def profileMap = readJenkinsProfile()

        println "return map:" + profileMap

        // 包名
        String PKG_NAME = profileMap.getProperty("PKG_NAME")
        println "PKGNAME===" + PKG_NAME
        if (!TextUtils.isEmpty(PKG_NAME)) {
            android.applicaitonId = PKG_NAME
            println("applicationId == " + PKG_NAME)
        }
        // 内部版本号

        //版本号
        String VERSION_CODE = profileMap.getProperty("VERSION_CODE")
        if (VERSION_CODE != null && VERSION_CODE.length() > 0) {
            android.versionCode = Integer.parseInt(VERSION_CODE)
            println "resolve from config VERSION_CODE: " + VERSION_CODE
        }

        // 外部版本号
        String VERSION_NAME = profileMap.getProperty("VERSION_NAME")
        if (!TextUtils.isEmpty(VERSION_NAME)) {
            android.versionName = VERSION_NAME
            println("VERSION_NAME ==" + VERSION_NAME)
        }

        //应用名称
        String APP_NAME = profileMap.getProperty("APP_NAME")
        if (!TextUtils.isEmpty(APP_NAME)) {
            config.APP_NAME = APP_NAME
            println("APP_NAME ==" + APP_NAME)
        }
        // 是否开启闹钟模块
        String IS_USE_ALARM = profileMap.getProperty("IS_USE_ALARM")
        if (!TextUtils.isEmpty(IS_USE_ALARM)) {
            config.IS_USE_ALARM = IS_USE_ALARM

            println "IS_USE_ALARM == " + IS_USE_ALARM
        }

        // 是否开启创建桌面闹钟功能
        String IS_CREATE_ALARM_SHORTCUT = profileMap.getProperty("IS_CREATE_ALARM_SHORTCUT")
        if (!TextUtils.isEmpty(IS_CREATE_ALARM_SHORTCUT)) {
            config.IS_CREATE_ALARM_SHORTCUT = IS_CREATE_ALARM_SHORTCUT
            println "IS_CREATE_ALARM_SHORTCUT ==" + IS_CREATE_ALARM_SHORTCUT
        }

        // 是否开启小组件功能
        String IS_USE_WIDGET = profileMap.getProperty("IS_USE_WIDGET")
        if (!TextUtils.isEmpty(IS_USE_WIDGET)) {
            config.IS_USE_WIDGET = IS_USE_WIDGET
            println "IS_USE_WIDGET==" +IS_USE_WIDGET
        }

        // 替换启动页资源
        String SPLASH_RES = profileMap.getProperty("SPLASH_RES")
        println "SPLASH_RES ==="+SPLASH_RES
        if (!TextUtils.isEmpty(SPLASH_RES)){
            String resZipPath = "SPLASH_RES.zip"
            def splashFile = new File(resZipPath);
            println "iconFile ==="+splashFile.absolutePath
            if (splashFile.exists()) {
                println "res file exist and start replace res"
                config.SPLASH_RES=SPLASH_RES
                replaceRes(rootProject, resZipPath, "main")
            } else {
                println "use default splash res"
            }
        }

        // 替换icon 资源
        String ICON_RES = profileMap.getProperty("ICON_RES")
        println "ICON_RES ==="+ICON_RES
        if (!TextUtils.isEmpty(ICON_RES)){
            String iconPath = "ICON_RES.zip"

            def iconFile = new File(iconPath)
            if (iconFile.exists()) {
                println "res file exist and start replace res"
                config.ICON_RES = ICON_RES
                replaceRes(rootProject,iconPath,"main")
            } else {
                println "use default icon res"
            }
        }

        println("show end \tt ext==" + ext + "\t config:" + config + "\t android:" + android)

    }

读取jenkins 配置文件方法

    /**
     * 读取jenkins 配置
     */
    def readJenkinsProfile() {
        def props = new Properties()
        def profile = new File(BUILD_PROFILE)

        if (profile.exists()) {
            profile.withInputStream {
                stream -> props.load(new InputStreamReader(stream, "UTF-8"))
            }
        }
        println "readProfile : " + props
        return props
    }

替换资源文件的相关方法

    //替换资源文件
    def replaceRes(Project rootProject, String resZipPath, String product) {
        println "#################### replace res start ####################"
        def appProject = rootProject.findProject(":app");
        File srcFile = appProject.file("src")
        File productFile = new File(srcFile, product)
        println "productFile==="+productFile.absolutePath
        if (!productFile.exists()){
            productFile.mkdir()
        }

        ZipFileUtil.replaceResFromZip(resZipPath, productFile.absolutePath)
    }

zip 文件解压操作

 def static replaceResFromZip(String path, String toPath) throws IOException {
        def count = -1;
        def index = -1;
        def file = null;
        def is = null;
        def fos = null;
        def bos = null;

        println "path===="+path+"\t toPaht===="+toPath
        ZipFile zipFile = new ZipFile(path);
        Enumeration<?> entries = zipFile.entries();

        while (entries.hasMoreElements()) {
            def buf = new byte[2048];
            ZipEntry entry = (ZipEntry) entries.nextElement();
            def filename = entry.getName();

            filename = toPath + "/" + filename;
            println "fileName=="+filename
            File file2 = new File(filename.substring(0, filename.lastIndexOf("/")));
            println "file2====="+file2.absolutePath
            if (!file2.exists()) {
                file2.mkdirs();
            }
            // 过滤掉文件夹 和 macOS 上的特殊文件 __MASOSX 和隐藏文件
            if (!filename.endsWith("/") && !filename.startsWith("_") && !filename.startsWith(".")) {

                file = new File(filename);
                file.createNewFile();
                is = zipFile.getInputStream(entry);
                fos = new FileOutputStream(file);
                bos = new BufferedOutputStream(fos, 2048);

                while ((count = is.read(buf)) > -1) {
                    bos.write(buf, 0, count);
                }

                bos.flush();

                fos.close();
                is.close();

            }
        }

        zipFile.close();
        println "####################replaceResFromZip end  ########################"
    }

以上这些基本就是构建定制化打包平台的主要工作,具体的细节,需要通过代码不断调试。

上一篇 下一篇

猜你喜欢

热点阅读