#!/usr/bin/python # -*- coding: utf-8 -*- from logging import debug, root from struct import pack from Common import Setting, Debug, PathConvert, StringFormat Setting.SetDefaultencodingUTF8() import zipfile, sys, os, traceback import shutil import platform import subprocess import xml.dom.minidom as minidom import ConfigParser CUR_PLATFORM_NAME = platform.system() CUR_PATH = os.path.abspath(sys.path[0]) APK_TOOLS_PATH = os.path.abspath(CUR_PATH + '/../APKTools') APK_TOOL_JAR_PATH = os.path.abspath(APK_TOOLS_PATH + '/apktool.jar') APK_SIGNER_JAR_PATH = os.path.abspath(APK_TOOLS_PATH + '/apksigner.jar') if CUR_PLATFORM_NAME == 'Windows': ZIP_ALIGN_PATH = os.path.abspath(APK_TOOLS_PATH + '/zipalign.exe') elif CUR_PLATFORM_NAME == 'Linux': ZIP_ALIGN_PATH = os.path.abspath(APK_TOOLS_PATH + '/zipalign.exe') elif CUR_PLATFORM_NAME == 'Darwin': ZIP_ALIGN_PATH = os.path.abspath(APK_TOOLS_PATH + '/zipalign_mac') else: ZIP_ALIGN_PATH = os.path.abspath(APK_TOOLS_PATH + '/zipalign.exe') TEMP_OUT_PATH = r"\apkTemp" VALID_CMD_ARGV = {"-help", "-rulePath", "-configPath", "-resPath", "-outPath", "-inPath", "-keyStorePath", "-keyAlias", "-keyStorePwd", "-keyAliasPwd", "-keySignVer"} def PrintHelp(): Debug.Log('') Debug.Log('') Debug.Log('命令参数说明') Debug.Log('') Debug.Log('-help 查看帮助') Debug.Log('') Debug.Log('') Debug.Log('-configPath 根据INI配置文件来打包') Debug.Log('') Debug.Log('') Debug.Log('-inPath 原始Apk路径') Debug.Log('-outPath 生成的新Apk路径') Debug.Log('-resPath 作为资源的Apk文件') Debug.Log('-rulePath 指定更新资源规则,可找对应程序获得当前版本的规则') Debug.Log(' 为.apk文件里的资源路径') Debug.Log(' 如:assets/AssetsAndroid') Debug.Log('-keyStorePath 签名证书路径') Debug.Log('-keyAlias 签名证书alias') Debug.Log('-keyStorePwd 签名证书密码') Debug.Log('-keyAliasPwd 签名证书alias密码') Debug.Log('-keySignVer (可选)签名版本, 默认为v1') Debug.Log('') Debug.Log('') Debug.Log('') def ValidJava(): try: cmds = ["java", "-version"] sp = subprocess.Popen(cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE) contents = sp.communicate() sp.wait() version = None for content in contents: lines = content.split("\n") for i in range(len(lines)): line = lines[i].strip() if line.startswith("java version "): version = line.replace("java version ", "") version = version.replace("\"", "") if not version: Debug.LogError("无法检测JDK版本 ") return False else: if version.startswith("1.8"): return True else: Debug.LogError("JDK版本不符,需要使用1.8版本 当前版本 " + version) return False except Exception as exc: Debug.LogError(exc) Debug.Log("") Debug.LogError("无法找到有效JDK, 请检查环境 !!! ") return False return True def ToolsChmodEXE(): if CUR_PLATFORM_NAME != 'Linux' or CUR_PLATFORM_NAME != 'Darwin': return Debug.LogSuccess("赋予%s目录可执行权限开始" % APK_TOOLS_PATH) import stat for root, dirs, files in os.walk(APK_TOOLS_PATH): for file in files: os.chmod(file, os.stat(file).st_mode | stat.S_IEXEC) Debug.LogSuccess("赋予%s目录可执行权限成功" % APK_TOOLS_PATH) def ExecConfig(configPath): if not os.path.exists(StringFormat.FormatStringPlatform(configPath)): Debug.LogError(configPath + "不存在") return config = ConfigParser.ConfigParser() config.read(StringFormat.FormatStringPlatform(configPath)) keyStorePathD = config.get('signinfodefault', 'keyStorePath') keyAliasD = config.get('signinfodefault', 'keyAlias') keyStorePwdD = config.get('signinfodefault', 'keyStorePwd') keyAliasPwdD = config.get('signinfodefault', 'keyAliasPwd') keySignVerD = config.get('signinfodefault', 'keySignVer') packs = [] for k in config.sections(): if k[:4] == 'pack': packs.append(int(k[4:])) packs.sort() success = [] errors = [] for i in packs: oneConfig = 'pack' + str(i) inPath = config.get(oneConfig, 'inPath') outPath = config.get(oneConfig, 'outPath') rulePath = config.get(oneConfig, 'rulePath') resPath = config.get(oneConfig, 'resPath') if config.has_option(oneConfig, "keyStorePath"): keyStorePath = config.get(oneConfig, 'keyStorePath') keyAlias = config.get(oneConfig, 'keyAlias') keyStorePwd = config.get(oneConfig, 'keyStorePwd') keyAliasPwd = config.get(oneConfig, 'keyAliasPwd') keySignVer = config.get(oneConfig, 'keySignVer') res = ExecOne(inPath, outPath, rulePath, resPath, keyStorePath, keyAlias, keyStorePwd, keyAliasPwd, keySignVer) else: res = ExecOne(inPath, outPath, rulePath, resPath, keyStorePathD, keyAliasD, keyStorePwdD, keyAliasPwdD, keySignVerD) if res: success.append(i) else: errors.append(i) successNum = len(success) if successNum > 0: Debug.Log("") Debug.Log("") Debug.Log("") Debug.LogSuccess("执行成功的有:") for i in success: oneConfig = 'pack' + str(i) Debug.LogSuccess(config.get(oneConfig, 'inPath')) Debug.LogSuccess("\t to " + config.get(oneConfig, 'outPath')) else: Debug.Log("") Debug.Log("") errorNum = len(errors) if errorNum > 0: Debug.Log("") Debug.LogError("执行失败的有:", True) for i in errors: oneConfig = 'pack' + str(i) Debug.LogError(config.get(oneConfig, 'inPath')) Debug.LogError("\t to " + config.get(oneConfig, 'outPath')) Debug.Log("") Debug.Log("") sys.exit(2) else: Debug.Log("") Debug.Log("") def ExecOne(inputPath, outPath, rulePath, resPath, keyStorePath, keyAlias, keyStorePwd, keyAliasPwd, keySignVer): if not os.path.exists(StringFormat.FormatStringPlatform(inputPath)): Debug.LogError(inputPath + "不存在") return False if not outPath or outPath == "": Debug.LogError(outPath + "目标目录无效") return False if not os.path.exists(StringFormat.FormatStringPlatform(rulePath)): Debug.LogError(rulePath + "不存在") return False if not os.path.exists(StringFormat.FormatStringPlatform(resPath)): Debug.LogError(resPath + "不存在") return False if not os.path.exists(StringFormat.FormatStringPlatform(keyStorePath)): Debug.LogError(keyStorePath + "不存在") return False dirOutPath = os.path.dirname(outPath) tempOutPath = dirOutPath + TEMP_OUT_PATH if os.path.exists(StringFormat.FormatStringPlatform(tempOutPath)): Debug.LogError(tempOutPath + "临时解压目录已存在,请先删除") return False result = ExecOneApk(inputPath, outPath, rulePath, resPath, keyStorePath, keyAlias, keyStorePwd, keyAliasPwd, keySignVer, tempOutPath) if os.path.exists(StringFormat.FormatStringPlatform(tempOutPath)): shutil.rmtree(u'' + tempOutPath) if os.path.exists(StringFormat.FormatStringPlatform(outPath + ".idsig")): os.remove(StringFormat.FormatStringPlatform(outPath + ".idsig")) return result def ExecOneApk(inputPath, outPath, rulePath, resPath, keyStorePath, keyAlias, keyStorePwd, keyAliasPwd, keySignVer, tempOutPath): if keySignVer: if isinstance(keySignVer, str): if len(keySignVer) > 1: if (keySignVer[:1] == 'v' or keySignVer == 'V'): keySignVer = keySignVer[1:] keySignVer = int(keySignVer) else: keySignVer = 1 rules = ConfigParser.ConfigParser() rules.read(StringFormat.FormatStringPlatform(rulePath)) covers = rules.items('cover') if not covers or len(covers) < 0: Debug.LogError(rulePath + ' 规则文件[cover]内容不能为空') filters = rules.items('filter') try: # 解包原始资源 Debug.Log("解包原始资源") Debug.Log("") cmds = ["java", "-jar", StringFormat.FormatStringPlatform(APK_TOOL_JAR_PATH), "d", StringFormat.FormatStringPlatform(inputPath), "-o", StringFormat.FormatStringPlatform(tempOutPath)] res = subprocess.Popen(cmds, shell=True, stdout=subprocess.PIPE) res.wait() if res.returncode != 0: Debug.LogError("无法解包 " + inputPath) return False # 解包新资源 Debug.Log("解包新资源") Debug.Log("") cmds = ["java", "-jar", StringFormat.FormatStringPlatform(APK_TOOL_JAR_PATH), "d-axml", StringFormat.FormatStringPlatform(resPath), "-o", StringFormat.FormatStringPlatform(tempOutPath + "/AndroidManifest-new.xml")] res = subprocess.Popen(cmds, shell=False, stdout=subprocess.PIPE) res.wait() if res.returncode != 0: Debug.LogError("无法解包 " + resPath) return False newVersionCode = None newVersionName = None newLebianVerCode = None domTree = minidom.parse(StringFormat.FormatStringPlatform(tempOutPath + "/AndroidManifest-new.xml")) root = domTree.documentElement newVersionCode = root.getAttribute('platformBuildVersionCode') newVersionName = root.getAttribute('platformBuildVersionName') applications = root.getElementsByTagName('application') for app in applications: metadatas = app.getElementsByTagName('meta-data') for metadata in metadatas: if metadata.getAttribute('android:name') == "LEBIAN_VERCODE": newLebianVerCode = metadata.getAttribute('android:value') break if newLebianVerCode: break if not newVersionCode or not newVersionName or not newLebianVerCode: Debug.LogError("无法识别" + resPath + "里的数据信息") return False domTree = minidom.parse(StringFormat.FormatStringPlatform(tempOutPath + "/AndroidManifest.xml")) root = domTree.documentElement # 修改AndroidManifest.xml Debug.Log("修改AndroidManifest.xml") Debug.Log("") Debug.LogSuccess("BuildVersionCode " + root.getAttribute('platformBuildVersionCode') + " -> " + newVersionCode) root.setAttribute('platformBuildVersionCode', newVersionCode) Debug.LogSuccess("BuildVersionCode " + root.getAttribute('platformBuildVersionName') + " -> " + newVersionName) root.setAttribute('platformBuildVersionName', newVersionName) applications = root.getElementsByTagName('application') for app in applications: metadatas = app.getElementsByTagName('meta-data') for metadata in metadatas: if metadata.getAttribute('android:name') == "LEBIAN_VERCODE": Debug.LogSuccess("LEBIAN_VERCODE " + metadata.getAttribute('android:value') + " -> " + newLebianVerCode) metadata.setAttribute('android:value', newLebianVerCode) newLebianVerCode = None break if not newLebianVerCode: break Debug.Log("") with open(StringFormat.FormatStringPlatform(tempOutPath + "/AndroidManifest.xml"), "w") as f: f.write(domTree.toxml(encoding = "utf-8")) os.remove(StringFormat.FormatStringPlatform(tempOutPath + "/AndroidManifest-new.xml")) # 更新资源 Debug.Log("更新资源") Debug.Log("") with zipfile.ZipFile(StringFormat.FormatStringPlatform(resPath)) as resFile: for data in resFile.infolist(): for cover in covers: fileName = data.filename if fileName == cover or fileName.startswith(cover): if not filters or fileName in filters: resFile.extract(data, tempOutPath) break # 编译新APK Debug.Log("编译新APK") Debug.Log("") cmds = ["java", "-jar", StringFormat.FormatStringPlatform(APK_TOOL_JAR_PATH), "b", StringFormat.FormatStringPlatform(tempOutPath)] res = subprocess.Popen(cmds, shell=False, stdout=subprocess.PIPE) res.wait() if res.returncode != 0: Debug.LogError("无法编译包 " + tempOutPath) return # zip对齐apk Debug.Log("zip对齐apk") Debug.Log("") cmds = [StringFormat.FormatStringPlatform(ZIP_ALIGN_PATH), "-p", "-f", "4", StringFormat.FormatStringPlatform(tempOutPath + "/dist/" + os.path.basename(inputPath)), StringFormat.FormatStringPlatform(outPath)] res = subprocess.Popen(cmds, shell=False, stdout=subprocess.PIPE) res.wait() if res.returncode != 0: Debug.LogError("无法zip对齐APK " + outPath) return # 签名apk Debug.Log("签名apk") Debug.Log("") cmds = ["java", "-jar", StringFormat.FormatStringPlatform(APK_SIGNER_JAR_PATH), "sign", "--ks", StringFormat.FormatStringPlatform(keyStorePath), "--ks-key-alias", keyAlias, "--ks-pass", "pass:" + keyStorePwd, "--key-pass", "pass:" + keyAliasPwd] if keySignVer >= 4: cmds.append('--v4-signing-enabled') cmds.append('true') if keySignVer >= 3: cmds.append('--v3-signing-enabled') cmds.append('true') if keySignVer >= 2: cmds.append('--v2-signing-enabled') cmds.append('true') cmds.append('--v1-signing-enabled') cmds.append('true') cmds.append(StringFormat.FormatStringPlatform(outPath)) res = subprocess.Popen(cmds, shell=False, stdout=subprocess.PIPE) res.wait() if res.returncode != 0: Debug.LogError("无法签名APK " + outPath) return False Debug.LogSuccess("执行成功" + inputPath + " -> " + outPath) Debug.Log("") Debug.Log("") return True except Exception as exc: Debug.LogError(exc) Debug.Log("") return False def CMDArgvIsNull(dic, keyName): if (not keyName in dic): PrintHelp() Debug.Log('') Debug.Log('') Debug.LogError(keyName + ' 不能为空') Debug.Log('') Debug.Log('') return True return False def Main(): args = sys.argv[1:] dic = {} num = len(args) for i in range(num): arg = StringFormat.FormatStdinString(args[i]) if arg.startswith('-'): if not arg in VALID_CMD_ARGV: Debug.LogError(arg + " 不是有效的参数") PrintHelp() return if i + 1 >= num: Debug.LogSuccess(arg) dic[arg] = '' else: nextArg = StringFormat.FormatStdinString(args[i + 1]) if nextArg.startswith('-'): Debug.LogSuccess(arg) dic[arg] = '' else: Debug.LogSuccess(arg + " " + nextArg) dic[arg] = nextArg if len(dic) <= 0: PrintHelp() return if ('-help' in dic): PrintHelp() return if not ValidJava(): return ToolsChmodEXE() if ('-configPath' in dic): ExecConfig(dic['-configPath']) return if (CMDArgvIsNull(dic, '-inPath')): return if (CMDArgvIsNull(dic, '-outPath')): return if (CMDArgvIsNull(dic, '-rulePath')): return if (CMDArgvIsNull(dic, '-resPath')): return if (CMDArgvIsNull(dic, '-keyStorePath')): return if (CMDArgvIsNull(dic, '-keyAlias')): return if (CMDArgvIsNull(dic, '-keyStorePwd')): return if (CMDArgvIsNull(dic, '-keyAliasPwd')): return Debug.Log('') Debug.Log('') keySignVer = None if ('-keySignVer' in dic): keySignVer = dic['-keySignVer'] ExecOne(dic['-inPath'], dic['-outPath'], dic['-rulePath'], dic['-resPath'], dic['-keyStorePath'], dic['-keyAlias'], dic['-keyStorePwd'], dic['-keyAliasPwd'], keySignVer) if __name__ == '__main__': try: Main() except Exception as e: Debug.LogError(e) Debug.LogError(traceback.format_exc(), True) sys.exit(1)