The first two articles automated our packaging hardening and uploading platform for internal testing, saving developers time.

But there are still some problems that are not solved. For example, when we are writing code and the test suddenly asks you to package it, we have to pause the current work and execute the package command for the corresponding branch. During the package, we can only wait and not continue to write the code.

At this point, Jenkins server will come in handy. We can configure the packaging command on Jenkins server, and let testers choose the branch version they want to package and improve the efficiency of both sides.

There are a lot of articles on the Internet about Jenkins, please do your own research. This article only explains how to configure the following things on Jenkins:

  • Harden using the bang-bang hardening command line
  • The hardened package is signed
  • Use VasDolly for fast multi-channel packing
  • Upload the installation package to dandelion

Configure the Gradle script

Resigning and VasDolly multichannel packing commands were added because 360 hardening was replaced with Bong-Bong hardening, and the bong-bong hardening command line tool did not support autosigning and local multichannel package generation.

Reinforce the script

Complete config.gradle configuration:

ext {
    curGitCommit = rootProject.properties['GIT_COMMIT'] = =null ? getGitCommit() : rootProject.properties['GIT_COMMIT']
    curTime = rootProject.properties["BUILD_TIMESTAMP"] = =null ? getCurTime() : rootProject.properties["BUILD_TIMESTAMP"]
    backupPath = ".. /buildBackup/"

    if (rootProject.properties["isJenkins"].toBoolean()) {
        androidHome = rootProject.properties['ANDROID_HOME']}else {
        Properties properties = new Properties()
        properties.load(project.rootProject.file('local.properties').newDataInputStream())
        androidHome = properties.getProperty('sdk.dir')}// Signature file configuration
    signing = [keyAlias : 'xxxxx'.keyPassword : 'xxxxx'.storeFile : '.. /sign.keystore'.               storePassword: 'xxxxxx']

    // The dandelion configuration
    pgy = [apiKey : "xxxx".           uploadUrl: "https://www.pgyer.com/apiv2/app/upload"]

    // Hardening configuration
    jiagu = [
            app1Online : "${backupPath}${curGitCommit}/${curTime}/app1Online_jiagu/".app1Admin : "${backupPath}${curGitCommit}/${curTime}/app1Admin_jiagu/".app2Online : "${backupPath}${curGitCommit}/${curTime}/app2Online_jiagu/".app2Admin : "${backupPath}${curGitCommit}/${curTime}/app2Admin_jiagu/".            channelConfigPath: '.. /jiagu/Channel.txt'.VasDollyJar : ".. /jiagu/VasDolly.jar".banbanJar : ".. /jiagu/secapi.jar".banbanName : rootProject.properties['banbanName'] = =null ? "banbanName" : rootProject.properties['banbanName'].banbanApiKey : rootProject.properties['banbanApiKey'] = =null ? "banbanApiKey" : rootProject.properties['banbanApiKey'].banbanSecretKey : rootProject.properties['banbanSecretKey'] = =null ? "banbanSecretKey" : rootProject.properties['banbanSecretKey'].banbanIp : rootProject.properties['banbanIp'] = =null ? "banbanIp" : rootProject.properties['banbanIp'],
    ]

    android = [compileSdkVersion: 28.               buildToolsVersion: "29.0.3".minSdkVersion : 19.targetSdkVersion : 28]

    apksignerDir = "${androidHome}/build-tools/${android['buildToolsVersion']}/"

    // Version number management
    APP1_VERSION_NAME = "2.0.2"
    APP1_TEST_NUM = "0001"
    APP2_VERSION_NAME = "1.0.5."
    APP2_TEST_NUM = "0005"

    dependencies = [
            "androidx-appcompat"       : "Androidx. Appcompat: appcompat: 1.1.0." "."androidx-constraintlayout": "Androidx. Constraintlayout: constraintlayout: 1.1.3."."VasDolly-helper"          : "Com. Leon. Channel: helper: the 2.0.3"]},static def getCurTime() {
    return new Date().format("yyyy-MM-dd_HH-mm-ss")}static def getGitCommit() {
    def exceptionStr = Git rev-parse HEAD failed
    try {
        def cmd = 'git rev-parse HEAD'
        def gitCommit = cmd.execute().text.trim()
        if (gitCommit.isEmpty()) {
            throw new GradleException(exceptionStr)
        }
        return gitCommit
    } catch (Exception e) {
        throw new GradleException(exceptionStr, e)
    }

}
Copy the code

Implementing hardened signature and multichannel package logic in Jiagu. gradle:

import org.apache.tools.ant.taskdefs.condition.Os


/** **@paramConfig Configure hardening policies *@paramApkPath File path *@paramOutputPath outputPath *@paramChannelName channelName. If it is Null, read rootproject.ext. jiagu["channelConfigPath"] multi-channel configuration file */
def jiaGu(String config, String apkPath, String outputPath, String channelName) {
    println(\nconfig :$config \napkPath:$apkPath \noutputPath:$outputPath \nchannelName:$channelName")
    exec {
        executable = 'java'
        args = ['-jar', rootProject.ext.jiagu["banbanJar"].'-i', rootProject.ext.jiagu["banbanIp"].'-u', rootProject.ext.jiagu["banbanName"].'-a', rootProject.ext.jiagu["banbanApiKey"].'-c', rootProject.ext.jiagu["banbanSecretKey"].'-f'.'0'.'-t', config,
                '-p', apkPath,
                '-d', outputPath,
        ]
    }

    println Hardened file path: ${apkPath}
    println Hardened file path: ${outputPath}

    def signApkPath = apkSigner(outputPath)

    generatingMultipleChannels(signApkPath, outputPath, channelName)

  	// When running on Jenkins, back up the build artifacts to the hard drive root directory,
    if (isJenkins.toBoolean()) {
        String sourceDir = file(outputPath).parentFile.absolutePath
        String destinationDir = "/Users/buildBackup/${rootProject.properties["JOB_NAME"]}/${rootProject.ext.curGitCommit}/${rootProject.ext.curTime}/"
        new groovy.util.AntBuilder().copy(todir: destinationDir) {
            fileset(dir: sourceDir)
        }
        println "Back up build artifacts to: ${destinationDir}"}}def apkSigner(String apkDir) {
    def apkFile = getApkFile(apkDir)

    if (apkFile == null| |! apkFile.exists()) {throw GradleException("Hardened APK file does not exist")}def signDir = new File(apkDir, 'sign')
    if(! signDir.exists()) { signDir.mkdirs() }def outputApkPath = new File(signDir, apkFile.name)

    def storeFile = rootProject.ext.signing["storeFile"]
    def storeFilePath = file(storeFile).absolutePath
    // Compatible with Windows, Mac, and Linux
    def execuTable = Os.isFamily(Os.FAMILY_WINDOWS) ? 'cmd' : 'sh'
    def apksigner = Os.isFamily(Os.FAMILY_WINDOWS) ? 'apksigner.bat' : './apksigner'

    println("Sign the hardening package")

    def parameter = [apksigner, 'sign'.'--ks', storeFilePath,
                     '--ks-key-alias', rootProject.ext.signing["keyAlias"].'--ks-pass'."pass:${rootProject.ext.signing["storePassword"]}".'--key-pass'."pass:${rootProject.ext.signing["keyPassword"]}".'--out', outputApkPath, apkFile.absolutePath]
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        parameter.add(0.'/c')
    }
    exec {
        workingDir = rootProject.ext.apksignerDir
        executable = execuTable
        args = parameter
    }
    println "Apk signature successful:" + outputApkPath
    return outputApkPath
}

/** * Generate multi-channel package *@paramSignApkPath Base package *@paramOutputDir Output directory *@paramChannelName channelName. If it is Null, read rootproject.ext. jiagu["channelConfigPath"] multi-channel configuration file */
private void generatingMultipleChannels(signApkPath, outputDir, channelName) {
    def channelDir = new File(outputDir, 'channels')
    if(! channelDir.exists()) { channelDir.mkdirs() } println("Generate multi-channel packages")
    def channel = channelName == null ? rootProject.ext.jiagu["channelConfigPath"] : channelName
    exec {
        executable = 'java'
        args = ['-jar', rootProject.ext.jiagu["VasDollyJar"].'put'.'-c', channel,
                signApkPath, channelDir.absolutePath
        ]
    }
    println("Generate multi-channel package completed")}def getApkPath(String flavor) {
    if ("app1Online" == flavor) {
        return "${projectDir.absolutePath}/build/outputs/apk/production/release/${getApkName(rootProject.ext.android["versionName"])}"
    } else {
        return "${projectDir.absolutePath}/build/outputs/apk/${flavor}/release/${getApkName(getTestVersionName(flavor))}"}}private def getApkFile(String fileDir) {
    def dir = file(fileDir)
    if(! dir.exists()) { println"dir not exists:" + dir.path
        return null
    }
    File[] files = dir.listFiles(new FileFilter() {
        @Override
        boolean accept(File file) {
            return file.isFile() && file.name.endsWith(".apk")}})if (files == null || files.size() == 0) {
        println "files == null || files.size() == 0"
        return null
    }
    return files[0]}private static void checkOutputDir(File apkOutputFile) {
    if (apkOutputFile.exists()) {
        File[] files = apkOutputFile.listFiles()
        if(files ! =null) {
            for (File file : files) {
                file.delete()
            }
        }
    } else {
        apkOutputFile.mkdirs()
    }
}

Run./gradlew releaseApp1 */./gradlew releaseApp1
task releaseApp1(dependsOn: ['assembleApp1OnlineRelease']) {
    group = "publish"
    doLast {
        def apkOutputFile = file(rootProject.ext.jiagu["app1OnlineOutputPath"])
        checkOutPutDir(apkOutputFile)
        def apkFile = file(getApkPath("App1Online"))
        if(! apkFile.exists()) { println("apk file is not exists:" + apkFile.absolutePath)
            return
        }
        jiaGu("1", apkFile.absolutePath, apkOutputFile.absolutePath, null)}}Copy the code

We simply need to execute./gradlew releaseApp1 from the command line to compile – > harden – > sign – > generate multichannel packages

The code of uploading dandelion is the same as above, only need to change the APK path, take the generated channel package to upload.

Jenkins configuration

First we create the task, configure the task command parameters:

Configure the Gradle plugin to execute the commands selected above and configure the Project properties:

If the task of uploading dandelion is performed, we want to display the two-dimensional code of dandelion directly, and add the following Set Build Description configuration:

We save the configuration and execute the task:

conclusion

There is not much explanation in this article, but it is simple and rough to configure the pictures on the code. If you are confused about Gradle, please read the previous articles.

The configuration of Jenkins is only briefly covered in this paper. I believe that you can get started quickly by simply understanding the relevant concepts and principles. After passing the basic process, you can achieve more functions by drawing on one example from another.

Finally, the demo address: github.com/imliujun/Gr…

reading

  • Improved efficiency – automatically harden and upload to dandelion
  • Use Gradle to implement a set of code to develop multiple applications