加固使用的是乐固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)
// }
//}