Python 脚本构建Android APK 自动加固、打渠道包并上传服务器

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()

这样就完成了整个加固、打渠道包、上传服务器的过程。

你可能感兴趣的:(Python 脚本构建Android APK 自动加固、打渠道包并上传服务器)