Android 自动构建 - 签名信息及文件拷贝

1,145 阅读2分钟
原文链接: www.jianshu.com

根据项目需求,现要在团队内部搭建一个统一的打包平台,实现对Android项目的打包。而且为了方便团队内部的测试包分发。

打包平台使用的是Jenkins,在构建前面我们面临的两个问题就是:

  • 签名文件如何配置(开发人员是不可以见的)
  • 打包出来的apk如何的再发布和出问题如何回滚到上一次的apk

TODO

环境搭建

自行在查询,因为大家的平台可能都不同,现在这类的文章已经很多了,这里就不进行展开了。

签名文件管理

这个需要达到两个目标且不能去修改配置相关信息和使用简单

  • 开发人员可以使用自己的签名信息替换
  • 配置人员只需要配置一次

设计思路:
通过在打包的机器添加一个环境变量来判断是否是打包机器,这样就可以使用试用两份配置,部分代码如下(后面会提供全部):

File propFile = file(isJenkins ?  configPath : 'apk.properties');
if (propFile.exists()) {
    def Properties props = new Properties()
    props.load(new FileInputStream(propFile))
    if (props.containsKey('STORE_FILE') && props.containsKey('STORE_PASSWORD') &&
            props.containsKey('KEY_ALIAS') && props.containsKey('KEY_PASSWORD')) {

        //配置签名信息
    } else {
        throw new IllegalArgumentException('apk.properties')
    }
} else {
    println "apk.properties not exist"
    android.buildTypes.release.signingConfig = null
}

apk存储

每天、每次构建出来的apk都有记录,方便获取当前软件最新的apk,所以一共保存了三个地方。

  • apk存储的根目录, 永远都是最新的apk,方便集成发布工具
  • apk存储的根目录/日期 , 每日构建的apk,方便查询指定日期的apk
  • apk存储的根目录/日期/项目名称, 当前项目每次构建的apk,完整的记录。

完整脚本信息如下:

打包运行:

gradle clean publishRelease
APK_NAME=test.apk //apk输入名称
APK_DIR=d:/test   //保存目录
MINIFY_ENABLED=true //是否混淆
STORE_FILE=u2020.keystore
STORE_PASSWORD=android
KEY_ALIAS=android
KEY_PASSWORD=android
def storeDir = System.getenv("STORE_ROOT") //配置文件存储路径
def os = System.getProperty("os.name")     // 系统类型
def isJenkins = "true".equals(System.getenv("IS_JENKINS")) //是否是打包机
def projectRootName = project.rootProject.name


if (isJenkins && storeDir == null){
    throw new NullPointerException('storeDir is null, you need add it in system env ' +
            'with key: STORE_ROOT')
}

project.ext{
    projectName = projectRootName
    apkName = projectName + ".apk"
    apkRootDir = null
    outputPath = null
}

/**
 * 默认打包完把apk存储的目录
 * win :d:/
 * mac :/Download
 * linux : /opt
 */

if (project.ext.apkRootDir == null){
    if (os == null || os.indexOf('windows')){
        project.ext.apkRootDir = 'd:/'
    }else if (os.indexOf('linux') > 0){
        project.ext.apkRootDir = '/opt'
    }else if (os.indexOf('windows')){
        project.ext.apkRootDir = '/Download'
    }
}


def configPath = storeDir + project.ext.projectName + "/apk.properties"
println "config path: " + configPath

File propFile = file(isJenkins ?  configPath : 'apk.properties');
if (propFile.exists()) {
    def Properties props = new Properties()
    props.load(new FileInputStream(propFile))
    if (props.containsKey('STORE_FILE') && props.containsKey('STORE_PASSWORD') &&
            props.containsKey('KEY_ALIAS') && props.containsKey('KEY_PASSWORD')) {

        android.signingConfigs.release.storeFile = file(props['STORE_FILE'])
        android.signingConfigs.release.storePassword = props['STORE_PASSWORD']
        android.signingConfigs.release.keyAlias = props['KEY_ALIAS']
        android.signingConfigs.release.keyPassword = props['KEY_PASSWORD']

        android.buildTypes.release.minifyEnabled = Boolean.parseBoolean(props['MINIFY_ENABLED', 'false'])

        project.ext.apkName = props['APK_NAME']
        project.ext.apkRootDir = props['APK_DIR']
        if (hasProperty('PROJECT_NAME')){
            project.ext.projectName = PROJECT_NAME
        }

        println "apkName: " + project.ext.apkName
        println "apk store dir: " + project.ext.apkRootDir
        println "projectName: " + project.ext.projectName
    } else {
        throw new IllegalArgumentException('apk.properties')
    }
} else {
    println "apk.properties not exist"
    android.buildTypes.release.signingConfig = null
}


def buildDay = new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("GMT+8"))
def buildTime = new Date().format("yyyy-MM-dd HH.mm.ss", TimeZone.getTimeZone("GMT+8"))


def publish = project.tasks.create("publishRelease")
android.applicationVariants.all { variant ->
    if (variant.buildType.name == 'release') {
        variant.outputs.each { output ->
            def taskOnly = project.tasks.create("publishOnly${variant.name}Apk", Copy)
            def taskDay = project.tasks.create("publishDay${variant.name}Apk", Copy)
            def taskTimes = project.tasks.create("publishTimes${variant.name}Apk", Copy)

            //构建最新的,固定位置,方便拷贝
            def onlyApkDir = project.ext.apkRootDir + "/" ;
            def onlyApk = onlyApkDir + project.ext.projectName + ".apk";
            //每日保存最新的一个
            def dayApkDir = project.ext.apkRootDir + "/" + buildDay + "/";
            def dayApk = dayApkDir + project.ext.projectName + ".apk";
            //保留每次打包的apk
            def timesApkDir = project.ext.apkRootDir + "/" + buildDay + "/" + project.ext.projectName + "/";

            def srcApk = output.outputFile.getAbsolutePath();
            def srcName = output.outputFile.getName()
            //clean
            File dk = new File(dayApk)
            if (dk.exists()){
                dk.delete()
            }

            File ok = new File(onlyApk)
            if (ok.exists()){
                ok.delete()
            }

            taskOnly.from(srcApk)
            taskOnly.into(onlyApkDir)
            taskOnly.rename(srcName, project.ext.apkName )

            taskDay.from(srcApk)
            taskDay.into(dayApkDir)
            taskDay.rename(srcName, project.ext.apkName )

            taskTimes.from(srcApk)
            taskTimes.into(timesApkDir)
            taskTimes.rename(srcName, buildTime + ".apk")

            taskOnly.dependsOn variant.assemble
            taskDay.dependsOn variant.assemble
            taskTimes.dependsOn variant.assemble
            taskOnly.dependsOn taskDay
            taskOnly.dependsOn taskTimes
            publish.dependsOn taskOnly
        }
    }
}

看到这里,点个赞吧