Python 脚本构建Android APK 自动加固、打渠道包并上传服务器
常规流程
打出原始apk
使用乐固工具加固并打出响应渠道包
-
将生成的渠道包上传对应服务器,生成推广链接
因为每一步都需要人工介入,尤其当渠道较多时,相当耗时,且有出错的可能,所以考虑以脚本替代人工执行步骤。
准备工作
- qshell 用于上传七牛服务器的命令行工具
- VasDolly多渠道打包工具,同时支持基于V1签名和V2签名进行多渠道打包
- apksigner.jar 用于为apk进行签名,支持v1+v2签名,这里有个坑,一开始我选择使用的是java包内的jarsigner,后发现只支持v1签名。apksigner.jar位置在android sdk的libs中,可以考虑将其复制到当前工作目录下
脚本编写
初始化
# 初始化环境
def init():
command = './qshell account'
result = os.popen(command).read()
if ('error' in result):
command = './qshell account '
os.popen(command).read()
if not os.path.exists(ORIGIN_APK):
raise RuntimeError('找不到原始 apk 文件')
# 删除临时 apk 文件
if os.path.exists(TEMP_APK):
os.remove(TEMP_APK)
# 删除上传失败日志文件
if os.path.exists(QINIU_FAILED_LIST):
os.remove(QINIU_FAILED_LIST)
# 初始化清缓存文件
fp = open(REFRESH_CONFIG,'w')
fp.write(QINIU_URL+ORIGIN_APK)
fp.close()
os.makedirs(OUTPUT_PATH)
首先我们进行qshell的设置,设置当前用户的AccessKey, SecretKey和Name, Name是用户可以任意取的名字,表示当前在本地记录的账户的名称。
ORIGIN_APK是打包出来的原始apk
TEMP_APK、QINIU_FAILED_LIST、REFRESH_CONFIG 都是运行生成的结果,在下一次运行初期进行一次清理。
上传临时APK文件到七牛中
# 上传临时APK文件到七牛
def uploadApp():
command = './qshell rput --overwrite %s %s %s ' % (
QINIU_BUCKET, ORIGIN_APK, ORIGIN_APK)
uploadResult = os.popen(command).read()
if 'success' not in uploadResult:
raise RuntimeError('上传临时APK文件到七牛失败:'+uploadResult)
refresh()
# 刷新七牛缓存
def refresh():
command = './qshell cdnrefresh -i %s' % (REFRESH_CONFIG)
os.popen(command).read()
将临时文件上传七牛,这样我们能拿到改apk的链接,方便下一步的加固工作
QINIU_BUCKET:七牛空间名称,可以为公开空间或私有空间
使用乐固进行加固并签名
# 使用乐固加固
def legu():
cred = credential.Credential(TENCENT_KEY, TENCENT_SECRET)
httpProfile = HttpProfile()
httpProfile.endpoint = "ms.tencentcloudapi.com"
clientProfile = ClientProfile()
clientProfile.httpProfile = httpProfile
client = ms_client.MsClient(cred, "ap-shanghai", clientProfile)
req = models.CreateShieldInstanceRequest()
params = '''{
"AppInfo":{
"AppUrl":"%s",
"AppMd5":"%s"
},
"ServiceInfo":{
"ServiceEdition":"basic",
"SubmitSource":"api"
}
}''' % (QINIU_URL+ORIGIN_APK, md5_file(ORIGIN_APK))
req.from_json_string(params)
#调用腾讯 API乐固接口加固
resp = client.CreateShieldInstance(req)
itemId = resp.ItemId
if not itemId:
raise RuntimeError('创建乐固实例失败:'+resp)
req = models.DescribeShieldResultRequest()
params = '{"ItemId":"%s"}' % (itemId)
req.from_json_string(params)
#下载加固结果
for num in range(20):
time.sleep(120)
print("查看乐固加固结果,第%d次尝试中..." % num)
resp = client.DescribeShieldResult(req)
if resp.TaskStatus != 2:
break
else:
raise RuntimeError('加固超时')
downloadURL = resp.ShieldInfo.AppUrl
appMD5 = resp.ShieldInfo.ShieldMd5
if downloadURL:
urllib.urlretrieve(downloadURL, TEMP_APK)
if md5_file(TEMP_APK) != appMD5:
raise RuntimeError('下载加固结果失败')
# 签名
command = sign_command(TEMP_APK)
os.popen(command).read()
# 计算文件的 md5值
def md5_file(name):
m = md5()
a_file = open(name, 'rb') # 使用二进制格式读取文件内容
m.update(a_file.read())
a_file.close()
return m.hexdigest()
#使用apksigner进行签名
def sign_command(file):
return 'java -jar apksigner.jar sign --ks %s --ks-key-alias ALIAS --ks-pass pass:PASS --key-pass pass:PASS --out %s %s'% (SIGN_KEY, file,file)
使用乐固API接口进行加固,完成加固后下载至本地并进行md5的验证,确保包的一致性,而后在本地对其进行签名
TENCENT_KEY、TENCENT_SECRET 用于乐固的腾讯key&&secret
ALIAS别名 PASS密码 SIGN_KEY秘钥所在位置
打渠道包
# 打渠道包
def channel():
command = 'java -jar VasDolly.jar put -c %s %s %s' % (
CHANNEL_CONFIG, TEMP_APK, OUTPUT_PATH)
result = os.popen(command).read()
searchObj = re.search(r'total (\d+) channel apk', result, re.M | re.I)
total = int(searchObj.group(1))
channelCount = len(open(CHANNEL_CONFIG, 'rU').readlines())
if total != channelCount:
raise RuntimeError('生成渠道包数量异常:应生成%s,实际生成%s' % (channelCount, total))
rename()
# 重命名
def rename():
files = os.listdir(OUTPUT_PATH)
for file in files:
f = os.path.join(OUTPUT_PATH, file)
#os.popen(sign_command(f))
os.rename(f, f.replace('-'+TEMP_APK, '.apk'))
不同于以往先打渠道包而后进行签名的方式,VasDolly可以让我们在不破坏签名的情况下进行渠道打包。
这边也有个坑,因为原先采用的打渠道包方式是umeng,umeng会直接修改manifest的channel字段,所以我们在包内获取渠道名的方式都是直接读这个字段。VasDolly不会去修改这个字段,因而需要改变项目内所有获取渠道名的方式,这边可以参考VasDolly的说明进行修改。
这样我们的加固及打渠道都完成了,渠道包会输出在你指定的OUTPUT_PATH。
CHANNEL_CONFIG 包含所有渠道名称 换行分隔
上传到七牛
def deploy():
uploadCmd = './qshell qupload2 --overwrite --rescan-local --src-dir=%s --bucket=%s --failure-list %s --check-hash --thread-count 10' % (
QINIU_UPLOAD_PATH, QINIU_BUCKET, QINIU_FAILED_LIST)
os.popen(uploadCmd).read()
if os.path.getsize(QINIU_FAILED_LIST):
raise RuntimeError('上传到七牛失败,失败文件列表见:' + QINIU_FAILED_LIST)
#清缓存
fp = open(REFRESH_CONFIG,'w')
files = os.listdir(OUTPUT_PATH)
fp.writelines([QINIU_URL+QINIU_UPLOAD_SUBPATH+file+'\n' for file in files])
fp.close()
refresh()
# 刷新七牛缓存
def refresh():
command = './qshell cdnrefresh -i %s' % (REFRESH_CONFIG)
os.popen(command).read()
这样就完成了整个加固、打渠道包、上传服务器的过程。