【Gradle Task】一条龙发包(打包、加固、对齐签名、多渠道)

加固使用的是乐固api;多渠道为walle方案
脚本中使用了fir上传,以拿到apk的url给乐固使用
zipalign.exe与apksigner从sdk目录/build-tools/xx.x.x(注意使用28.x的,支持v2+v1同时签名;28+的增加了v3,暂未测试)下取得

import com.tencentcloudapi.common.*
import com.tencentcloudapi.common.exception.*
import com.tencentcloudapi.common.profile.*
import com.tencentcloudapi.ms.v20180408.*
import com.tencentcloudapi.ms.v20180408.models.*
import okhttp3.OkHttpClient
import okhttp3.Request

import java.security.MessageDigest
import java.util.concurrent.TimeUnit

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath "com.squareup.okhttp3:okhttp:4.4.0"
        classpath "com.google.code.gson:gson:2.8.6"
        classpath "com.tencentcloudapi:tencentcloud-sdk-java:3.0.93"
    }
}

ext.leguConfig = [
        "SecretId"       : "乐固id",
        "SecretKey"      : "乐固key",

        //需要加固的APK url地址
        "apkUrl"         : "",

        //本地APK的路径, 用来计算MD5值
        "apkPath"        : "./app/build/outputs/apk/release/app_" + project.android.defaultConfig.versionName + ".apk",
        //加固后, 下载保存到本地路径
        "downloadApkPath": "./channel/app_" + project.android.defaultConfig.versionName,

        //每隔多少秒, 查询一次加固结果
        "pollTime"       : "10",

        //加固后, 返回的ItemId, 用来轮询结果
        "ItemId"         : "",

        //加固成功的下载地址
        "downloadUrl"    : ""
]

/**
 * task.execute()方法报错,所以暂时用dependsOn反向依赖,实际执行顺序依次为assembleRelease、uploadAPKtoFir、_leguJiaGu、_leguGetResult、multiAPK
 * 此文件大量报红正常,不影响执行
 */

//1.fir上传 拿到rul
task uploadAPKtoFir() {
    def fir_api_token = "fir-api-token"
    doFirst {
        //清空旧文件
        File outputDir = new File(rootDir.getAbsolutePath() + '\\channel\\output');
        outputDir.deleteDir();
        //清理旧的apk
        FileTree tree = fileTree(rootDir.getAbsolutePath() + "\\channel")
        tree.each { File file ->
            if (file.toString().endsWith(".apk")) {
                file.println()
                delete file
            }
        }

        println "即将上传到fir..."

        //获取fir上传凭证的各个字段
        def appInfo = ("curl -X POST -d type=android&" +
                "bundle_id=$project.android.defaultConfig.applicationId&" +
                "api_token=$fir_api_token " +
                "http://api.bq04.com/apps").execute().text

        //json解析对象拿到的是Map, 集合对应的是array, 按照这个规则取出我们需要的数据
        def appInfoBean = new groovy.json.JsonSlurper().parseText(appInfo)
        def key = appInfoBean["cert"]["binary"]["key"]
        def url = appInfoBean["cert"]["binary"]["upload_url"]
        def token = appInfoBean["cert"]["binary"]["token"]

        //执行上传命令 注意路径不能包含中文、空格
        def apkFile = project.android.applicationVariants[1].outputs.first().outputFile
        println "apk路径:" + apkFile
        def result = ("curl -X POST --form file=@$apkFile" +
                " -F token=$token" +
                " -F key=$key" +
                " -F x:version=$project.android.defaultConfig.versionName" +
                " -F x:build=$project.android.defaultConfig.versionCode" +
                " $url").execute().text
        //赋值apkUrl给乐固上传用
        leguConfig.apkUrl = new groovy.json.JsonSlurper().parseText(result)["download_url"]
        println "上传完成"
    }.dependsOn("assembleRelease")
}

//2.提交加固
task _leguJiaGu() {
    doFirst {
        Credential cred = new Credential(leguConfig.SecretId, leguConfig.SecretKey)
        HttpProfile httpProfile = new HttpProfile()
        httpProfile.setEndpoint("ms.tencentcloudapi.com")

        ClientProfile clientProfile = new ClientProfile()
        clientProfile.setHttpProfile(httpProfile)

        MsClient client = new MsClient(cred, "", clientProfile)

        def req = new CreateShieldInstanceRequest()
        def appInfo = new com.tencentcloudapi.ms.v20180408.models.AppInfo()
        appInfo.AppUrl = leguConfig.apkUrl
        appInfo.AppMd5 = getFileMd5(leguConfig.apkPath)

        println "apk路径:" + leguConfig.apkPath
        println "apkMD5:" + appInfo.AppMd5

        def serviceInfo = new com.tencentcloudapi.ms.v20180408.models.ServiceInfo()
        serviceInfo.ServiceEdition = "basic"
        serviceInfo.SubmitSource = "RDM-rdm"
        serviceInfo.CallbackUrl = ""

        req.AppInfo = appInfo
        req.ServiceInfo = serviceInfo

        CreateShieldInstanceResponse resp = client.CreateShieldInstance(req)

        leguConfig.ItemId = resp.ItemId
        //Progress任务状态: 1-已完成,2-处理中,3-处理出错,4-处理超时
        println "加固处理中:" + DescribeShieldInstancesRequest.toJsonString(resp)
    }.dependsOn("uploadAPKtoFir")
}

//3.查询加固结果
task _leguGetResult() {
    doFirst {
        def resp
        def TaskStatus = 2
        def count = 1;
        while (TaskStatus == 2) {
            println ""

            def cred = new Credential(leguConfig.SecretId, leguConfig.SecretKey)
            def httpProfile = new HttpProfile()
            httpProfile.setEndpoint("ms.tencentcloudapi.com")

            def clientProfile = new ClientProfile()
            clientProfile.setHttpProfile(httpProfile)

            def client = new MsClient(cred, "", clientProfile)

            def params = "{\"ItemId\":\"" + leguConfig.ItemId + "\"}"
            def req = DescribeShieldResultRequest.fromJsonString(params, DescribeShieldResultRequest.class)

//            println leguConfig.pollTime + "s后, 查询加固状态:" + params
            println "加固中...第" + count + "次查询"
            Thread.sleep(Integer.parseInt(leguConfig.pollTime) * 1000L)
            resp = client.DescribeShieldResult(req)

            TaskStatus = resp.TaskStatus

            leguConfig.downloadUrl = resp.ShieldInfo.AppUrl

            count++
        }

        if (TaskStatus == 1) {
            println "加固成功下载地址:" + leguConfig.downloadUrl

            println "开始下载->" + file(leguConfig.downloadApkPath + ".apk").getAbsolutePath()
            downloadFile(leguConfig.downloadUrl, leguConfig.downloadApkPath + ".apk")
        } else {
            println "加固失败"
            //TaskStatus任务状态: 1-已完成,2-处理中,3-处理出错,4-处理超时
            println DescribeShieldResultRequest.toJsonString(resp)
        }
    }.dependsOn("_leguJiaGu")
    doLast {
        println "加固结束"

        //zipalign
        println "开始zipalign对齐"
        def alignResult = ("./legu/zipalign -f -v 4 " + leguConfig.downloadApkPath + ".apk " + leguConfig.downloadApkPath + "_aligned.apk").execute().text
        println alignResult
        println "zipalign对齐完成"

        println "开始签名..."
        //sign
        def signResult = ("java -jar ./legu/apksigner.jar sign --ks " +
                "./demo.jks " +
                "--ks-key-alias demo" +
                "--ks-pass pass:demo " +
                "--ks-pass pass:demo --out " +
                leguConfig.downloadApkPath + "_aligned_signed.apk " +
                leguConfig.downloadApkPath + "_aligned.apk").execute().text
        println signResult
        println "签名完成"

        //删除中间文件 并重命名最终apk 去掉后缀
        new File(leguConfig.downloadApkPath + ".apk").delete()
        new File(leguConfig.downloadApkPath + "_aligned.apk").delete()
        new File(leguConfig.downloadApkPath + "_aligned_signed.apk").renameTo(new File(leguConfig.downloadApkPath + ".apk"))
        new File(leguConfig.downloadApkPath + "_aligned_signed.apk").delete()
    }
}

//4.打多渠道包 执行此task 自动执行 打包->上传fir(拿到url给乐固用)->加固->对齐、签名->多渠道
task multiAPK(dependsOn: [_leguGetResult]) {
    doLast {
        println "生成渠道包..."
        def multiResult = ("java -jar ./channel/walle-cli-all.jar batch -f " +
                //渠道文件
                "./channel/channels.txt " +
                //源apk(乐固加固后的包)
                leguConfig.downloadApkPath + ".apk " +
                //输出目录
                "./channel/output").execute().text
        println multiResult
        println "打包完成,输出目录:" + project.getRootDir() + "\\channel\\output"
    }
}

static def getFileMd5(filePath) {
    def FILE_READ_BUFFER_SIZE = 16 * 1024
    MessageDigest digester = MessageDigest.getInstance("MD5")
    def stream = new FileInputStream(filePath)
    int bytesRead
    byte[] buf = new byte[FILE_READ_BUFFER_SIZE]
    while ((bytesRead = stream.read(buf)) >= 0) {
        digester.update(buf, 0, bytesRead)
    }
    def md5code = new BigInteger(1, digester.digest()).toString(16)// 16进制数字
    // 如果生成数字未满32位,需要前面补0
    for (int i = 0; i < 32 - md5code.length(); i++) {
        md5code = "0" + md5code
    }
    return md5code

}

//下载加固后的文件
static def downloadFile(url, filePath) {
    def clientBuilder = new OkHttpClient.Builder()
    clientBuilder.connectTimeout(10, TimeUnit.SECONDS)
    clientBuilder.readTimeout(60, TimeUnit.SECONDS)

    OkHttpClient client = clientBuilder.build()

    def request = new Request.Builder()
            .url(url)
            .get()
            .build()

    def response = client.newCall(request).execute()

    def write = new BufferedOutputStream(new FileOutputStream(filePath, false))
    def read = new BufferedInputStream(response.body().byteStream())

    def bytes = new byte[1024]
    def bytesRead = 0
    while ((bytesRead = read.read(bytes)) != -1) {
        write.write(bytes, 0, bytesRead)
    }
    read.close()
    write.flush()
    write.close()
}

//assembleRelease注入
//gradle.taskGraph.whenReady { TaskExecutionGraph taskGraph ->
////    println('taskGraph.afterTask')
//    taskGraph.getAllTasks().each { Task task ->
//        if (task.name.startsWith('assemble') && task.name.endsWith('Release')) {
//            task.doLast {
//                File apkFile = findNewestApk()
//                File leguFile = reinforce(apkFile)
//                File zipFile = zipalignApk(leguFile)
//                File signedApkFile = signApk(zipFile)
//                String oldFileName = leguFile.getPath()
//                apkFile.delete()
//                leguFile.delete()
//                zipFile.delete()
//                signedApkFile.renameTo(new File(oldFileName.replace("_zip_sgined", "")))
//            }
//        }
////        println('>>>>==' + task.name)
//    }
//}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343