美文网首页Android坑坑之旅Android开发半栈工程师
Android Jenkins的定制化打包平台构建

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

作者: imesong | 来源:发表于2017-10-10 17:30 被阅读324次

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. 解析的内容分为以下几种
  • BuildConfig 中相关开关,如是否开启某一个模块功能代码
  • 普通字符串,如包名,内部版本号,外部版本号
  • 资源文件替换,如启动页,桌面icon
  • xml文件修改,主要指AndroidManifest.xml
  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
  • Boolean Parameter
  • Choice Parameter
  • File Parameter

添加一个 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 文件
  • 在 resources 目录下,新建 META-INF 文件,然后在文件夹中再创建 gradle-plugins 文件,一定要分两次创建两层文件夹。
  • 在 gradle-plugins 中,创建 com.abc123.properties 文件,其中文件名为构建工程的包名,后缀为.properties。
  • 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  ########################"
    }

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

相关文章

网友评论

    本文标题:Android Jenkins的定制化打包平台构建

    本文链接:https://www.haomeiwen.com/subject/fnpvyxtx.html