The idea behind this Gradle automation script is to automate packaging, hardening, and adding multiple channels so that you can do everything in one click before your app is released to the market, freeing your hands and saving time. In the future, it is considered to automatically upload the packaged APK to the server or hosting platform with curl instruction, or automate the construction, packaging and uploading with Jenkins, so as to realize the automation of the whole process. However, the most popular is the use of GitLab Auto DevOps and Kubernetes cluster, to achieve sustainable integration and automatic deployment, interested in you can go to understand.

Preparation for App packaging and release

We usually App stores to the application market basically has experienced the following process, local play a release package first, then through online or download tool for reinforcement, reinforcement is obtained due to the reinforcement will eliminate signature information first, so to be signed again after reinforcement, then generate multi-channel package, so basically the whole process is over, I drew a mind map as follows:

Reinforcement is introduced

My simple understanding is to give the original apk is encrypted and jacket, and create a new apk, and then run time will decrypt related action, so the app after reinforcement generally affect the startup time, reinforce the platform online also has a lot of contrast, mainly related to the startup time, package size, compatibility, security, and so on. This study only discusses how to realize the idea of automatic reinforcement and multi-channel packaging. 360 reinforcement is not the best choice. The reinforcement is mainly to prevent the application from being decomcompiled, debugged, cracked, secondary packaging, memory interception and other threats after it goes online.

Download 360 Reinforcement insurance

The steps of this Gradle automation practice are mainly based on 360 reinforcement + Tencent’s VasDolly multi-channel packaging.

  • Manually download

Windows, Mac, and Linux are available for details: jiagu.360.cn/#/global/do…

  • Automatically download

Curl is a file transfer tool that you can use to download and transfer files from the command line. Haxx.se /download.ht…

*/ def download360jiagu() {zipFile = File (packers["zipPath"]) if (! zipFile.exists()) { if (! zipFile.parentFile.exists()) { zipFile.parentFile.mkdirs() println("packers===create parentFile jiagu ${zipFile. ParentFile. AbsolutePath} ")} / / reinforcement of download address def downloadUrl = isWindows ()? packers["jiagubao_windows"] : Def CMD = "curl -o ${packers["zipPath"]} ${downloadUrl}" println cmd cmd.execute().waitForProcessOutput(System.out, System.err) } File unzipFile = file(packers["unzipPath"]) if (! Unzipfile.exists () {// Unzip the Zip file ant.unzip(SRC: packers["zipPath"], dest: packers["unzipPath"], encoding: "GBK") println 'packers===unzip 360jiagu' // Enable read/write permission on the decompressed file to prevent Jar files from being executed without permission. If (! isWindows()) { def cmd = "chmod -R 777 ${packers["unzipPath"]}" println cmd cmd.execute().waitForProcessOutput(System.out, System.err) } } }Copy the code

Make a release package

Gradle actually provides us with a series of related tasks, as shown below

We need to get a release package before hardening, so we can use itassembleReleasePerform this operation before hardeningassembleReleaseThis Task.

DependsOn 'assembleRelease'} task packersNewRelease {group 'packers';Copy the code

Automatic hardening

The so-called automatic implementation of reinforcement, nothing more than a few lines of command, 360 reinforcement protection provides a set of command line reinforcement, details please refer to: jiagu.360.cn/#/global/he…

We have communicated with the official and they need to fix it in the next version. The current version 3.2.2.3 (2020-03-16) has the bug. Currently, the command line cannot only select piracy monitoringCopy the code
/ def packers360(File releaseApk) {println 'packers===beginning 360 jiagu' def packersFile  = file(app["packersPath"]) if (! Packersfile.exists ()) {packersfile.mkdir ()} exec { packers["jarPath"], '-login', packers["account"], Println 'packers===import 360 login'} exec { packers["jarPath"], '-importsign', signing["storeFile"], signing["storePassword"], signing["keyAlias"], Signing ["keyPassword"]] println 'packers===import 360 sign'} exec {the executable = 'Java' args = ['-jar', packers["jarPath"], Println 'packers===show 360 sign'} exec {executable = 'Java' args = ['-jar', Packers ["jarPath"], '-config'] println 'packers===init 360 services'} exec { Executable = 'Java' args = ['-jar', packers["jarPath"], '-jiagu', Releaseapk. absolutePath, app["packersPath"], '-autosign'] println 'packers===excute 360 jiagu' } println 'packers===360 jiagu finished' println "packers===360 jiagu path ${app["packersPath"]}" }Copy the code

Automatic signature

About automatic signature, in fact, 360 at the time of reinforcement provides automatic signature configuration options, if you don’t want to put the signature file to 360, after the reinforcement can choose manual signature, because it involves security problems, this version I take 360 automatic signature, if you want to manually signed, below I give a plan, We mainly use zipalign and apksigner commands, which are located in the build-tools directory in the SDK file. We need Gradle to configure the path for automatic signature.

  • Align the unsigned APK
zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk
Copy the code
  • Sign the APK using your private key
apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk
Copy the code
  • Verify that the APK has been signed
apksigner verify my-app-release.apk
Copy the code

Automatic realization of multi-channel based on hardened Apk

As for multi-channel packaging, Tencent’s VasDolly has been used in our previous projects, so we adopt the VasDolly command this time, but we need to download VasDolly. Jar first. For details, please refer to github.com/Tencent/Vas… There is no requirement as to where to put it, just need gradle to configure the path, I directly put it in the project root directory. You can also use 360 multi-channel reinforcement, in fact, the entire set of commands provided by 360 Reinforcement can be used.

Def channelFile () {File channelFile = File ("${app["channelPath"]}/new") if (! channelFile.exists()) { channelFile.mkdirs() } def cmd = "java -jar ${app["vasDollyPath"]} put -c ${".. /channel.txt"} ${outputpackersApk()} ${channelFile.absolutePath}" println cmd cmd.execute().waitForProcessOutput(System.out, System.err) println 'packers===excute VasDolly reBuildChannel' }Copy the code

Sensitive information access

As we all know, signature requires signature files, passwords, aliases and other files, and 360 hardening requires the configuration of account and password. These are sensitive information, which is not recommended by Google to be stored directly in Gradle. It is recorded in plain text in Gradle, and is recommended to be stored in the Properties file.

Def propertiesFile = rootproject.file ("release.properties") def properties = new Properties() property.load (new FileInputStream(propertiesFile)) ext {// Signing = [keyAlias: properties['RELEASE_KEY_ALIAS'], keyPassword : properties['RELEASE_KEY_PASSWORD'], storeFile : properties['RELEASE_KEYSTORE_PATH'], storePassword: Properties ['RELEASE_STORE_PASSWORD']] // app related configuration app = [// Default release apk file path, since hardening is based on release package releasePath: "${project.buildDir}/outputs/apk/release", "${project.buildDir}/outputs/packers", // "${project.buildDir}/outputs/channels", // "../VasDolly. Jar "] // 360 Hardening configuration Packers = [Account: properties['ACCOUNT360'], // Account password: Properties ['PASSWORD360'], // zipPath: "${project.rootDir}/jiagu/360jiagu.zip", // unzipPath: ${project.rootDir}/jiagu/ 360Jiagubao /", "${project. RootDir} / jiagu / 360 jiagubao jiagu/jiagu jar", / / execute commands channelConfigPath jar package path: ${project.rootDir}/jiagu/ channel. TXT ", // multichannel jiagubao_mac: "Https://down.360safe.com/360Jiagu/360jiagubao_mac.zip", / / reinforcement jiagubao_windows download MAC address: "Https://down.360safe.com/360Jiagu/360jiagubao_windows_64.zip" / / reinforcement widnows download address]Copy the code

Gradle related basics

  • References to gradle script plug-ins
apply from: "${project.rootDir}/packers.gradle"
Copy the code
  • A local variable
 def dest = "A"
Copy the code
  • Extended attributes
Ext {account = "XXXX" password = "XXXXX"}Copy the code
  • String correlation
Def name = "I am ${' I am '}" def name =" I am ${' I am '}" "Copy the code
  • Optional parentheses
Println ('A') println 'A'Copy the code
  • Closure as the last argument to the method
repositories {
    println "A"
}
repositories() { println "A" }
repositories({println "A" })
Copy the code
  • Task dependent on
TaskB {// TaskB dependsOn A // packersRelease doLast {println "B"}}Copy the code
  • The task scheduling
//taskB must always run after taskA, regardless of whether taskA and taskB are going to run tasKb. mustRunAfter(taskA) // Not as strict as mSUT tasKb. shouldRunAfter (taskA)Copy the code
  • File location
// use a relative path File configFile = File (' SRC /config.xml') // use an absolutePath configFile = File (configfile.absolutepath) // File object configFile = file(new file(' SRC /config.xml')) 'using a project pathCopy the code
  • Document traversal
Collection. each {File File -> println file.name}Copy the code
  • Copy and rename files
Copy {from source file address into destination directory address rename(" original file name ", "new file name ")}Copy the code

Automatically upload to the server

This function will be updated in the next article. We can upload it to our own server by using curl command. If you can upload it to dandelion or fir. You can also choose Jenknis to automate building, packaging, and uploading.

  • Publish the application to the fir.im hosting platform

For details, please refer to: www.betaqr.com/docs/publis…

Method 1: fir-CLI command line tool upload $FIR p path/to/application -t YOUR_FIR_TOKEN Method 2: API upload Use the curl command to invoke API 1. Curl -x "POST" "http://api.bq04.com/apps" \ -h "content-type: application/json" \ -d "{\"type\":\"android\", \"bundle_id\":\"xx.x\", \"api_token\":\"aa\"}" 2. Apk curl -f "key= XXXXXX "\ -f "token= XXXXX" \ -f "[email protected]" \ -f "x:name=aaaa" \ -f "x:version= A.B.C "\ -f "X :build=1" \ -f "x:release_type=Adhoc" \ #type=ios use -f "x:changelog=first" \ https://up.qbox.meCopy the code
  • Publish the app to Dandelion

Please refer to: www.pgyer.com/doc/api#upl…

curl -F "file=@/tmp/example.ipa" -F "uKey=" -F "_api_key=" https://upload.pgyer.com/apiv1/app/upload
Copy the code

The overall effect

Our requirement is to make two batches of packages for the old background and the new background. The packages of the old background must be prefixed with app-, so there are three taskspackersNewReleasePerform normal hardened packaging for the new backend,packersOldReleaseUsed for packaging prefixed app-name used for old background,packersReleaseThis task is used to package the old background and the new background simultaneously with one click.You can also view the output log of the packaging task on the Gradle console as follows:

Gradle automation source code

In order to let you try to automate gradle scripts to bring convenience, below I contribute my own entire Gradle source code, you can take away the need to study, if there are problems also hope to communicate more.

/**
 * @author hule
 * @date 2020/04/15 13:42
 * description:360自动加固+Vaslloy多渠道打包
 */

// 把敏感信息存放到自定义的properties文件中
def propertiesFile = rootProject.file("release.properties")
def properties = new Properties()
properties.load(new FileInputStream(propertiesFile))

ext {
    // 签名配置
    signing = [keyAlias     : properties['RELEASE_KEY_ALIAS'],
               keyPassword  : properties['RELEASE_KEY_PASSWORD'],
               storeFile    : properties['RELEASE_KEYSTORE_PATH'],
               storePassword: properties['RELEASE_STORE_PASSWORD']
    ]

    // app相关的配置
    app = [
            //默认release apk的文件路径,因为加固是基于release包的
            releasePath : "${project.buildDir}/outputs/apk/release",
            //对release apk 加固后产生的加固apk地址
            packersPath : "${project.buildDir}/outputs/packers",
            //加固后进行腾讯多渠道打包的地址
            channelPath : "${project.buildDir}/outputs/channels",
            //腾讯VasDolly多渠道打包jar包地址
            vasDollyPath: "../VasDolly.jar"
    ]

    // 360加固配置
    packers = [account          : properties['ACCOUNT360'], //账号
               password         : properties['PASSWORD360'],  //密码
               zipPath          : "${project.rootDir}/jiagu/360jiagu.zip",  //加固压缩包路径
               unzipPath        : "${project.rootDir}/jiagu/360jiagubao/",  //加固解压路径
               jarPath          : "${project.rootDir}/jiagu/360jiagubao/jiagu/jiagu.jar",  //执行命令的jar包路径
               channelConfigPath: "${project.rootDir}/jiagu/Channel.txt",  //加固多渠道
               jiagubao_mac     : "https://down.360safe.com/360Jiagu/360jiagubao_mac.zip",  //加固mac下载地址
               jiagubao_windows : "https://down.360safe.com/360Jiagu/360jiagubao_windows_64.zip" //加固widnows下载地址
    ]
}

/**
 *  360加固,适用于新后台打包
 */
task packersNewRelease {
    group 'packers'
    dependsOn 'assembleRelease'
    doLast {
        //删除加固后的渠道包
        deleteFile()
        // 下载360加固文件
        download360jiagu()
        // 寻找打包文件release apk
        def releaseFile = findReleaseApk()
        if (releaseFile != null) {
            //执行加固签名
            packers360(releaseFile)
            //对加固后的apk重新用腾讯channel构建渠道包
            reBuildChannel()
        } else {
            println 'packers===can\'t find release apk and can\'t excute 360 jiagu'
        }
    }
}

/**
 * 适用于老后台,老后台需要在渠道apk的名称增加前缀 app-
 */
task packersOldRelease {
    group 'packers'
    doLast {
        File channelFile = file("${app["channelPath"]}/new")
        if (!channelFile.exists() || !channelFile.listFiles()) {
            println 'packers==== please excute pakcersNewRelease first!'
        } else {
            File oldChannelFile = file("${app["channelPath"]}/old")
            if (!oldChannelFile.exists()) {
                oldChannelFile.mkdirs()
            }
            // 对文件集合进行迭代
            channelFile.listFiles().each { File file ->
                copy {
                    from file.absolutePath
                    into oldChannelFile.absolutePath
                    rename(file.name, "app-${file.name}")
                }
            }
            println 'packers===packersOldRelease sucess'
        }
    }
}

/**
 *  加固后,打新版本的渠道包时,同时生成老版本的渠道包
 */
task packersRelease {
    group 'packers'
    dependsOn packersNewRelease
    dependsOn packersOldRelease
    packersOldRelease.mustRunAfter(packersNewRelease)
    doLast {
        println "packers===packersRelease finished"
    }
}

/**
 *  对于release apk 进行360加固
 */
def packers360(File releaseApk) {
    println 'packers===beginning 360 jiagu'
    def packersFile = file(app["packersPath"])
    if (!packersFile.exists()) {
        packersFile.mkdir()
    }
    exec {
        // 登录360加固保
        executable = 'java'
        args = ['-jar', packers["jarPath"], '-login', packers["account"], packers["password"]]
        println 'packers===import 360 login'
    }
    exec {
        // 导入签名信息
        executable = 'java'
        args = ['-jar', packers["jarPath"], '-importsign', signing["storeFile"],
                signing["storePassword"], signing["keyAlias"], signing["keyPassword"]]
        println 'packers===import 360 sign'
    }
    exec {
        // 查看360加固签名信息
        executable = 'java'
        args = ['-jar', packers["jarPath"], '-showsign']
        println 'packers===show 360 sign'
    }
    exec {
        // 初始化加固服务配置,后面可不带参数
        executable = 'java'
        args = ['-jar', packers["jarPath"], '-config']
        println 'packers===init 360 services'
    }
    exec {
        // 执行加固
        executable = 'java'
        args = ['-jar', packers["jarPath"], '-jiagu', releaseApk.absolutePath, app["packersPath"], '-autosign']
        println 'packers===excute 360 jiagu'
    }
    println 'packers===360 jiagu finished'
    println "packers===360 jiagu path ${app["packersPath"]}"
}

/**
 * 自动下载360加固保,也可以自己下载然后放到根目录
 */
def download360jiagu() {
    // 下载360压缩包
    File zipFile = file(packers["zipPath"])
    if (!zipFile.exists()) {
        if (!zipFile.parentFile.exists()) {
            zipFile.parentFile.mkdirs()
            println("packers===create parentFile jiagu ${zipFile.parentFile.absolutePath}")
        }
        // 加固保的下载地址
        def downloadUrl = isWindows() ? packers["jiagubao_windows"] : packers["jiagubao_mac"]
        // mac自带curl命令 windows需要下载curl安装
        def cmd = "curl -o ${packers["zipPath"]} ${downloadUrl}"
        println cmd
        cmd.execute().waitForProcessOutput(System.out, System.err)
    }
    File unzipFile = file(packers["unzipPath"])
    if (!unzipFile.exists()) {
        //解压 Zip 文件
        ant.unzip(src: packers["zipPath"], dest: packers["unzipPath"], encoding: "GBK")
        println 'packers===unzip 360jiagu'
        //将解压后的文件开启读写权限,防止执行 Jar 文件没有权限执行,windows需要自己手动改
        if (!isWindows()) {
            def cmd = "chmod -R 777 ${packers["unzipPath"]}"
            println cmd
            cmd.execute().waitForProcessOutput(System.out, System.err)
        }
    }
}

/**
 * 腾讯channel重新构建渠道包
 */
def reBuildChannel() {
    File channelFile = file("${app["channelPath"]}/new")
    if (!channelFile.exists()) {
        channelFile.mkdirs()
    }
    def cmd = "java -jar ${app["vasDollyPath"]} put -c ${"../channel.txt"} ${outputpackersApk()} ${channelFile.absolutePath}"
    println cmd
    cmd.execute().waitForProcessOutput(System.out, System.err)
    println 'packers===excute VasDolly reBuildChannel'
}

/**
 *  是否是windows系统
 * @return
 */
static Boolean isWindows() {
    return System.properties['os.name'].contains('Windows')
}

/**
 * 寻找本地的release  apk
 * @return true
 */
def deleteFile() {
    delete app["channelPath"]
    delete app["packersPath"]
    println 'packers===delete all file'
}

/**
 * 首先打一个release包,然后找到当前的文件进行加固
 * @return releaseApk
 */
def findReleaseApk() {
    def apkDir = file(app["releasePath"])
    File releaseApk = apkDir.listFiles().find { it.isFile() && it.name.endsWith(".apk") }
    println "packers===find release apk ${releaseApk.name}"
    return releaseApk
}
/**
 *  加固输出并且重新命名
 * @return packersApk
 */
def outputpackersApk() {
    File oldApkDir = file(app["packersPath"])
    File oldApk = oldApkDir.listFiles().find { it.isFile() && it.name.contains("jiagu") }
    println "packers===output pacckers sourceApk ${oldApk.name}"
    copy {
        from app["packersPath"] + File.separator + oldApk.name
        into app["packersPath"]
        rename(oldApk.name, "release.apk")
        println 'packers===output pacckers renameApk release.apk'
    }
    File newApk = oldApkDir.listFiles().find { it.isFile() && it.name.equals("release.apk") }
    println "packers===output packers renameApk${newApk.absolutePath}"
    return newApk.absolutePath
}

Copy the code

conclusion

This article is based on the automatic practice of 360 reinforcement and Tencent VasDolly multi-channel packaging. It provides only one way, not limited to these two platforms. Other platforms just change the command of gradle automation reinforcement and multi-channel packaging. Your likes are my motivation to write!