# 安卓游戏打包脚本 # 1.用apktool解包 # 2.复制res资源、assets资源、jniLib资源 # 3.修改包名、app名、渠道文件 # 4.添加app icon、合并AndroidManifest文件 # 5.添加app启动图,修改AndroidManifest文件 # 6.生成新的R文件 # 7.将新的R文件编译,生成dex,再用baksmali生成smali,替换旧的R文件 # 8.将jar资源打包成dex,再用baksmali生成smali,拷贝到smali目录下 # 9.统计方法量,并分割dex(将smali文件拷贝到目录smali_classes2) # 10.用apktool回包 # 11.重新签名 import file_utils import xml_utils import smali_utils import config_utils import game_utils import os import os.path import json import sys import importlib import uuid ignoreLauncher = ['jm_ysdk', 'jm_yijie', 'jm_quick', 'jm_beiyu', 'jm_xq_jrtt'] def pack(game, sdk, config): config['cache'] = uuid.uuid1() subChannel = config['subChannel'] # 解包 ret = decomplie(game, sdk, subChannel, config) if ret: return ret # 删除旧代码 ret = removeOldCode(game, sdk, subChannel, config) if ret: return ret # 删除一些不支持的属性 ret = removeNoSupportAttr(game, sdk, subChannel, config) if ret: return ret # 删除一些不支持的配置 ret = fixUnSupportConfig(game, sdk, subChannel, config) if ret: return ret # 合并Drawable-v4目录 ret = mergeDrawableRes(game, sdk, subChannel, config) if ret: return ret # 移除相同的资源 ret = removeSameRes(game, sdk, subChannel, config) if ret: return ret # 复制res资源 ret = copyRes(game, sdk, subChannel, config) if ret: return ret # 合并主文件 ret = mergeManifestRes(game, sdk, subChannel, config) if ret: return ret # 替换占位符 ret = changePlaceholders(game, sdk, subChannel, config) if ret: return ret # 添加meta-data ret = addMetaData(game, sdk, subChannel, config) if ret: return ret # 增加配置文件 ret = addConfig(game, sdk, subChannel, config) if ret: return ret # 复制app res资源 ret = copyAppRes(game, sdk, subChannel, config) if ret: return ret # 更改包名 if 'packageName' in config and config['packageName'] != '': ret = changePackageName(game, sdk, subChannel, config) if ret: return ret # 更改app名 if 'name' in config and config['name'] != '': ret = changeAppName(game, sdk, subChannel, config) if ret: return ret # 更改app icon if config['changeIcon']: ret = changeAppIcon(game, sdk, subChannel, config) if ret: return ret # 添加启动图操作 if config['addLauncher']: ret = addLauncher(game, sdk, subChannel, config) if ret: return ret # 添加多图标 #ret = addMoreIcon(game, sdk, subChannel, config) #if ret: # return ret # 打包lib依赖 ret = packJar(game, sdk, subChannel, config) if ret: return ret # sdk脚本处理 ret = doSDKPostScript(game, sdk, config) if ret: return ret # 乐变sdk的特殊处理 ret = game_utils.sdkLebianChange(game, sdk, config) if ret: return ret # 游戏脚本处理 ret = doGamePostScript(game, sdk, config) if ret: return ret # log sdk if 'logSdk' in config: for log in config['logSdk']: ret = addLogSdk(game, sdk, subChannel, config, log) if ret: return ret # 生成R文件 ret = generateNewRFile(game, sdk, subChannel, config) if ret: return ret # 添加MultiDex支持 if config['splitDex']: ret = splitDex(game, sdk, subChannel, config) if ret: return ret # 更改版本号 ret = changeVersion(game, sdk, subChannel, config) if ret: return ret # 回编译 ret = recomplie(game, sdk, subChannel, config) if ret: return ret # 对齐apk ret = alignApk(game, sdk, subChannel, config) if ret: return ret # 签名 ret = apksignerApk(game, sdk, subChannel, config) if ret: return ret # 添加渠道信息 ret = addChannel(game, sdk, subChannel, config) if ret: return ret # 清理产生的中间文件 if config['clearCache']: clearTemp(game, sdk, subChannel, config) return 0 def decomplie(game, sdk, subChannel, config): ''' 解包 ''' apktoolPath = file_utils.getApkToolPath() gamePath = file_utils.getFullGameApk(game) decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) if os.path.exists(decompliePath): print('delete decomplie folder...') file_utils.deleteFolder(decompliePath) print('decomplie apk...') return file_utils.execJarCmd(apktoolPath, 'd -f "%s" -o "%s"' % (gamePath, decompliePath)) def removeOldCode(game, sdk, subChannel, config): ''' 删除旧代码 ''' decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) codePath = os.path.join(decompliePath, 'smali', 'com', 'jmhy', 'sdk') file_utils.deleteFolder(codePath) return 0 def copyRes(game, sdk, subChannel, config): ''' 复制res资源 ''' # 拷贝sdk资源 print('copy res...') decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) sdkPath = file_utils.getFullSDKPath(sdk) resPath = os.path.join(sdkPath, 'res') decomplieResPath = file_utils.getFullPath(decompliePath, 'res') for d in os.listdir(resPath): copyResWithType(resPath, decomplieResPath, d) # 拷贝assets print('copy assets...') assetsPath = file_utils.getFullPath(sdkPath, 'assets') decomplieAssetsPath = file_utils.getFullPath(decompliePath, 'assets') if os.path.exists(assetsPath): ret = file_utils.copyFileAllDir(assetsPath, decomplieAssetsPath) if ret: return ret # 拷贝jniLib print('copy jniLibs...') jniPath = file_utils.getFullPath(sdkPath, 'jniLibs') decomplieJniPath = file_utils.getFullPath(decompliePath, 'lib') abiFilters = [] if os.path.exists(decomplieJniPath): for abi in os.listdir(decomplieJniPath): if abi == 'armeabi-v7a' or abi == 'armeabi': abiFilters.append(abi) else: abiFilters = ['armeabi-v7a'] if os.path.exists(jniPath): ret = file_utils.copyFileAllDir(jniPath, decomplieJniPath, False, abiFilters) if ret: return ret return 0 def mergeDrawableRes(game, sdk, subChannel, config): ''' 合并Drawable-v4目录 ''' print('merge drawable path...') decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) resPath = os.path.join(decompliePath, 'res') for path in os.listdir(resPath): if path.startswith('drawable') and path.endswith('-v4'): v4DrawablePath = os.path.join(resPath, path) drawablePath = os.path.join(resPath, path[:-3]) if os.path.exists(drawablePath): ret = file_utils.copyFileAllDir(v4DrawablePath, drawablePath, True) if ret: return ret else: os.rename(v4DrawablePath, drawablePath) return 0 def removeSameRes(game, sdk, subChannel, config): ''' 移除相同的资源 ''' # 读取sdk的资源 print('remove same res...') sdkPath = file_utils.getFullSDKPath(sdk) sdkResPath = os.path.join(sdkPath, 'res') resList = [] for path in os.listdir(sdkResPath): if not path.startswith('values'): continue absPath = os.path.join(sdkResPath, path) for resFile in os.listdir(absPath): '''if not resFile.startswith('jm_'): continue''' resList = xml_utils.readAllRes(os.path.join(absPath, resFile), resList) if len(resList) == 0: print('no same res found') return 0 # 移除相同的资源 decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) resPath = os.path.join(decompliePath, 'res') for path in os.listdir(resPath): if not path.startswith('values'): continue absPath = os.path.join(resPath, path) for resFile in os.listdir(absPath): xml_utils.removeSameRes(os.path.join(absPath, resFile), resList) return 0 def mergeManifestRes(game, sdk, subChannel, config): ''' 合并主文件 ''' print('merge AndroidManifest...') decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) manifest = os.path.join(decompliePath, 'AndroidManifest.xml') sdkPath = file_utils.getFullSDKPath(sdk) libManifest = file_utils.getFullPath(sdkPath, 'manifest.xml') return xml_utils.mergeManifestRes(manifest, libManifest) def copyAppRes(game, sdk, subChannel, config): ''' 拷贝app的资源,比如app icon、启动图等 ''' channelPath = file_utils.getSubChannelPath(game, sdk, subChannel) decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) # assets print('copy assets...') assetsPath = file_utils.getFullPath(channelPath, 'assets') decomplieAssetsPath = file_utils.getFullPath(decompliePath, 'assets') if os.path.exists(assetsPath): ret = file_utils.copyFileAllDir(assetsPath, decomplieAssetsPath) if ret: return ret # icon print('copy icon...') ret = copyAppResWithType(decompliePath, channelPath, 'icon') if ret: return ret # 启动图 print('copy splash...') ret = copyAppResWithType(decompliePath, channelPath, 'splash') if ret: return ret # 其他图片 print('copy image...') ret = copyAppResWithType(decompliePath, channelPath, 'image') if ret: return ret return 0 def copyAppResWithType(decompliePath, channelPath, typeName): decomplieResPath = os.path.join(decompliePath, 'res') iconPath = os.path.join(channelPath, typeName) if not os.path.exists(iconPath): print('dir "%s" not exists' % iconPath) return 0 for d in os.listdir(iconPath): ret = copyResWithType(iconPath, decomplieResPath, d) if ret: return ret return 0 def copyResWithType(resPath, decomplieResPath, typeName): # appt的打包目录会带-v4后缀 resDir = os.path.join(resPath, typeName) target = os.path.join(decomplieResPath, typeName) targetV4 = os.path.join(decomplieResPath, typeName + '-v4') if not os.path.exists(target) and not os.path.exists(targetV4): os.makedirs(target) return file_utils.copyFileAllDir(resDir, target, False) elif not os.path.exists(target): return file_utils.copyFileAllDir(resDir, targetV4, False) else: return file_utils.copyFileAllDir(resDir, target, False) def removeNoSupportAttr(game, sdk, subChannel, config): ''' 删除一些不支持的属性 ''' decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) manifest = os.path.join(decompliePath, 'AndroidManifest.xml') xml_utils.removeRootAttr(manifest, 'compileSdkVersion') xml_utils.removeRootAttr(manifest, 'compileSdkVersionCodename') return 0 def fixUnSupportConfig(game, sdk, subChannel, config): ''' 删除一些不支持的配置 ''' # 检查minSdkVersion decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) yml = os.path.join(decompliePath, 'apktool.yml') minSdkVersion = 15 file_utils.changeMinSdkVersion(yml, minSdkVersion) resPath = os.path.join(decompliePath, 'res') tag = '-v' for res in os.listdir(resPath): print('res = ' + res) if res.startswith('values') and tag in res: start = res.index(tag) version = res[start+len(tag):] if not version.isdigit(): continue version = int(version) print('version = %d' % version) if version < minSdkVersion: unSopportPath = os.path.join(resPath, res) print('unSopportPath = ' + unSopportPath) file_utils.deleteFolder(unSopportPath) print('deleteFolder = ' + unSopportPath) return 0 def changePackageName(game, sdk, subChannel, config): ''' 更改包名 ''' # 全局替换AndroidManifest里面的包名 newPackageName = config['packageName'] decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) manifest = os.path.join(decompliePath, 'AndroidManifest.xml') packageName = xml_utils.getPackageName(manifest) xml_utils.changePackageName(manifest, newPackageName) print('change package name %s --> %s' % (packageName, newPackageName)) return 0 def changeAppName(game, sdk, subChannel, config): ''' 更改app名 ''' # 生成string.xml文件 name = config['name'] resName = 'common_sdk_name' if 'outName' in config: resName = resName + '_' + config['outName'] decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) stringFile = os.path.join(decompliePath, 'res', 'values', 'sdk_strings.xml') content = '%s' % (resName, name) file_utils.createFile(stringFile, content) # 修改主文件的app名的值 manifest = os.path.join(decompliePath, 'AndroidManifest.xml') xml_utils.changeAppName(manifest, '@string/%s' % resName) print('change app name %s' % name) return 0 def changeAppIcon(game, sdk, subChannel, config): ''' 更改app icon ''' print('change app icon...') resName = 'common_sdk_icon' decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) manifest = os.path.join(decompliePath, 'AndroidManifest.xml') xml_utils.changeAppIcon(manifest, '@mipmap/%s' % resName) return 0 def addMetaData(game, sdk, subChannel, config): ''' 添加meta-data ''' if 'metaData' not in config: return 0 print('add meta-data...') decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) manifest = os.path.join(decompliePath, 'AndroidManifest.xml') xml_utils.addMetaData(manifest, config['metaData']) return 0 def addConfig(game, sdk, subChannel, config): ''' 添加config.json ''' if 'configData' not in config: return 0 print('add config.json...') decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) configJson = os.path.join(decompliePath, 'assets', 'jmhy_config.json') jsonText = json.dumps(config['configData'], ensure_ascii=False) file_utils.createFile(configJson, jsonText) return 0 def changePlaceholders(game, sdk, subChannel, config): ''' 处理掉占位符 ''' if 'placeholders' not in config: return 0 decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) manifest = os.path.join(decompliePath, 'AndroidManifest.xml') placeholders = config['placeholders'] for placeholder in placeholders: oldText = '${%s}' % placeholder newText = placeholders[placeholder] print('change placeholder %s -> %s' % (oldText, newText)) file_utils.replaceContent(manifest, oldText, newText) return 0 def addLauncher(game, sdk, subChannel, config): ''' 添加启动图 ''' # ysdk的特殊处理 if sdk in ignoreLauncher: return 0 channelPath = file_utils.getSubChannelPath(game, sdk, subChannel) splashPath = os.path.join(channelPath, 'splash') if len(os.listdir(splashPath)) == 0: print('dir splash is empty') return 0 print('add launcher...') # 添加关联资源 decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) internalPath = file_utils.getFullInternalPath() ret = copyAppResWithType(decompliePath, internalPath, 'launcher_res') if ret: return ret # 拷贝代码 print('copy launcher code...') codePath = os.path.join(internalPath, 'launcher_code', 'smali') smaliPath = file_utils.getFullPath(decompliePath, 'smali') ret = file_utils.copyFileAllDir(codePath, smaliPath) if ret: return ret # 修改主文件信息 print('change launcher config...') manifest = os.path.join(decompliePath, 'AndroidManifest.xml') orientation = xml_utils.getScreenOrientation(manifest) activity = xml_utils.removeLauncherActivity(manifest) xml_utils.addLauncherActivity(manifest, orientation, 'com.jmhy.sdk.template.LauncherActivity') # 修改跳转的 launcherActivity = os.path.join(decompliePath, 'smali', 'com', 'jmhy', 'sdk', 'template', 'LauncherActivity.smali') file_utils.replaceContent(launcherActivity, '{class}', activity) print('change launcher %s to %s' % (activity, 'com.jmhy.sdk.template.LauncherActivity')) # config['oldLauncher'] = activity if 'launcherTime' in config: timeHex = formatHex(config['launcherTime']) file_utils.replaceContent(launcherActivity, '0x0BB8', timeHex) return 0 def addMoreIcon(game, sdk, subChannel, config): ''' 添加多个图标 ''' print('add more icon support...') decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) icon = '@mipmap/common_sdk_icon' if not config['changeIcon']: manifest = os.path.join(decompliePath, 'AndroidManifest.xml') icon = xml_utils.getApplicationAttr(manifest, 'icon') switchIcon = icon if config['switchIcon']: switchIcon = '@mipmap/common_sdk_icon2' manifest = os.path.join(decompliePath, 'AndroidManifest.xml') return xml_utils.addMoreIcon(manifest, icon, switchIcon) def formatHex(millisecond): ''' 将毫秒转为16进制,4位格式 ''' timeHex = str(hex(millisecond)).upper() timeHex = timeHex[2:] formatHex = '' if len(timeHex) == 3: formatHex = '0x0' + timeHex elif len(timeHex) == 4: formatHex = '0x' + timeHex else: formatHex = '0x0BB8' return formatHex def doSDKPostScript(game, sdk, config): ''' 执行sdk相关特殊处理脚本 ''' sdkPath = file_utils.getFullSDKPath(sdk) scriptPath = os.path.join(sdkPath, 'script') targetScript = os.path.join(scriptPath, 'sdk_script.py') if not os.path.exists(targetScript): print('sdk_script no exists') return 0 print('doSDKPostScript...') sys.path.append(scriptPath) module = importlib.import_module('sdk_script') ret = module.execute(game, sdk, config) sys.path.remove(scriptPath) return ret def doGamePostScript(game, sdk, config): ''' 执行游戏相关特殊处理脚本 ''' channelPath = file_utils.getFullGamePath(game) scriptPath = os.path.join(channelPath, 'script') targetScript = os.path.join(scriptPath, 'game_script.py') if not os.path.exists(targetScript): print('game_script no exists') return 0 print('doGamePostScript...') sys.path.append(scriptPath) module = importlib.import_module('game_script') ret = module.execute(game, sdk, config) sys.path.remove(scriptPath) return ret def addLogSdk(game, sdk, subChannel, config, logSdk): # 拷贝jniLibs decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) sdkPath = file_utils.getFullLogSDKPath(logSdk) print('copy log jniLibs...') jniPath = file_utils.getFullPath(sdkPath, 'jniLibs') decomplieJniPath = file_utils.getFullPath(decompliePath, 'lib') abiFilters = [] if os.path.exists(decomplieJniPath): for abi in os.listdir(decomplieJniPath): if abi == 'armeabi-v7a' or abi == 'armeabi': abiFilters.append(abi) else: abiFilters = ['armeabi-v7a'] if os.path.exists(jniPath): ret = file_utils.copyFileAllDir(jniPath, decomplieJniPath, False, abiFilters) if ret: return ret print('merge log AndroidManifest...') libManifest = file_utils.getFullPath(sdkPath, 'manifest.xml') if os.path.exists(libManifest): manifest = os.path.join(decompliePath, 'AndroidManifest.xml') ret = xml_utils.mergeManifestRes(manifest, libManifest) if ret: return ret return packLogJar(game, sdk, subChannel, config, logSdk) def generateNewRFile(game, sdk, subChannel, config): ''' 生成新的R文件 ''' decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) aapt = file_utils.getAAPTPath() androidPlatforms = file_utils.getAndroidCompileToolPath() manifest = os.path.join(decompliePath, 'AndroidManifest.xml') decomplieResPath = file_utils.getFullPath(decompliePath, 'res') compliePath = file_utils.getFullPath(decompliePath, 'gen') if not os.path.exists(compliePath): os.makedirs(compliePath) ret = file_utils.getExecPermission(aapt) if ret: return ret # 生成R文件 print('create R.java ...') createRCmd = '"%s" p -f -m -J "%s" -S "%s" -I "%s" -M "%s"' % (aapt, compliePath, decomplieResPath, androidPlatforms, manifest) ret = file_utils.execFormatCmd(createRCmd) if ret: return ret # 编译R文件 print('complie R.java ...') packageName = xml_utils.getPackageName(manifest) packagePath = file_utils.getPackagePath(compliePath, packageName) RSourceFile = os.path.join(packagePath, 'R.java') complieRCmd = 'javac -source 1.7 -target 1.7 -encoding UTF-8 "%s"' % RSourceFile ret = file_utils.execFormatCmd(complieRCmd) if ret: return ret # 生成dex print('dex R.class ...') dx = file_utils.getDxPath() outDex = os.path.join(compliePath, 'classes.dex') ret = file_utils.execJarCmd(dx, '--dex --output="%s" "%s"' % (outDex, compliePath)) if ret: return ret # 反向dex生成smali # 存放在out目录 print('baksmali classes.dex ...') baksmaliPath = file_utils.getBaksmaliPath() outPath = file_utils.getFullPath(decompliePath, 'out') ret = file_utils.execJarCmd(baksmaliPath, '-o "%s" "%s"' % (outPath, outDex)) if ret: return ret # 将生成的文件拷贝到目标目录 print('copy R.smali ...') smaliPath = file_utils.getFullPath(decompliePath, 'smali') file_utils.copyFileAllDir(outPath, smaliPath) return 0 def packJar(game, sdk, subChannel, config): ''' 打包所有的jar ''' splitDex = config['splitDex'] decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) outPath = file_utils.getFullPath(decompliePath, 'gen') dx = file_utils.getDxPath() if not os.path.exists(outPath): os.makedirs(outPath) # --no-warning dexCmd = '--dex --multi-dex --no-warning --output="%s"' % outPath # 找到所有lib依赖 sdkPath = file_utils.getFullSDKPath(sdk) libs = os.path.join(sdkPath, 'libs') libConfig = os.path.join(libs, 'config.json') # 存在配置文件 if os.path.exists(libConfig): jsonText = file_utils.readFile(libConfig) libConf = json.loads(jsonText) if 'libConfig' in config and config['libConfig'] in libConf: conf = config['libConfig'] libList = libConf[conf] for jar in libList: dexCmd += ' ' + os.path.join(libs, jar) else: for jar in os.listdir(libs): if not jar.endswith('.jar'): continue dexCmd += ' ' + os.path.join(libs, jar) else: for jar in os.listdir(libs): if not jar.endswith('.jar'): continue dexCmd += ' ' + os.path.join(libs, jar) # multidex.jar if splitDex: dexCmd += ' ' + file_utils.getMultiDexPath() # sdk实现类 print('packageing all jar ...') dexCmd += ' ' + os.path.join(sdkPath, '%s.jar' % sdk) ret = file_utils.execJarCmd(dx, dexCmd) if ret: return ret # 反向dex生成smali # 存放在out目录 print('baksmali classes.dex ...') outDex = os.path.join(outPath, 'classes.dex') baksmaliPath = file_utils.getBaksmaliPath() outPath = file_utils.getFullPath(decompliePath, 'out') ret = file_utils.execJarCmd(baksmaliPath, '-o "%s" "%s"' % (outPath, outDex)) if ret: return ret # 将生成的文件拷贝到目标目录 print('copy all smali ...') smaliPath = file_utils.getFullPath(decompliePath, 'smali') ret = file_utils.copyFileAllDir(outPath, smaliPath, True) if ret: return ret return 0 def packLogJar(game, sdk, subChannel, config, logSdk): ''' 打包所有的jar ''' decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) outPath = file_utils.getFullPath(decompliePath, 'gen') dx = file_utils.getDxPath() if not os.path.exists(outPath): os.makedirs(outPath) # --no-warning dexCmd = '--dex --multi-dex --no-warning --output="%s"' % outPath # 找到所有lib依赖 sdkPath = file_utils.getFullLogSDKPath(logSdk) libs = os.path.join(sdkPath, 'libs') for jar in os.listdir(libs): if not jar.endswith('.jar'): continue dexCmd += ' ' + os.path.join(libs, jar) # sdk实现类 print('packageing all log jar ...') dexCmd += ' ' + os.path.join(sdkPath, '%s.jar' % logSdk) ret = file_utils.execJarCmd(dx, dexCmd) if ret: return ret # 反向dex生成smali # 存放在out目录 print('baksmali classes.dex ...') outDex = os.path.join(outPath, 'classes.dex') baksmaliPath = file_utils.getBaksmaliPath() outPath = file_utils.getFullPath(decompliePath, 'out') ret = file_utils.execJarCmd(baksmaliPath, '-o "%s" "%s"' % (outPath, outDex)) if ret: return ret # 将生成的文件拷贝到目标目录 print('copy all log smali ...') smaliPath = file_utils.getFullPath(decompliePath, 'smali') ret = file_utils.copyFileAllDir(outPath, smaliPath, True) if ret: return ret return 0 def splitDex(game, sdk, subChannel, config): ''' 分割dex ''' # 判断是否已经存在application # 存在,则往原application添加内容 # 不存在,则拷贝一个默认的android.support.multidex.MultiDexApplication print('add MultiDex support...') decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) manifest = os.path.join(decompliePath, 'AndroidManifest.xml') application = xml_utils.getApplicationAttr(manifest, 'name') if application is None: ret = xml_utils.changeApplicationAttr(manifest, 'name', 'android.support.multidex.MultiDexApplication') if ret: return ret else: smaliPath = os.path.join(decompliePath, 'smali') applicationFile = file_utils.getPackagePath(smaliPath, application) applicationFile += '.smali' ret = changeApplicationDex(applicationFile) if ret: return ret return splitSmali(game, sdk, subChannel, config, application) def changeApplicationDex(file): ''' 修改application的smali文件,增加MultiDex操作 ''' index = file_utils.getApplicationSmaliIndex(file) file_utils.insertApplicationSmali(file, index) return 0 def splitSmali(game, sdk, subChannel, config, application): ''' 如果函数上限超过限制,自动拆分smali,以便生成多个dex文件 ''' print('splitSmali...') decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) smaliPath = os.path.join(decompliePath, 'smali') appPackage = None if application: appPackage = application[:application.rfind('.')] appPackage = appPackage.replace('.', '/') allFiles = [] allFiles = file_utils.list_files(smaliPath, allFiles) #print('file count is %d' % len(allFiles)) #maxFuncNum = 65535 # 留一点空间,防止计算误差 maxFuncNum = 64000 currFucNum = 0 totalFucNum = 0 currDexIndex = 1 allRefs = [] #保证Application等类在第一个classex.dex文件中 for f in allFiles: f = f.replace('\\', '/') if (appPackage and appPackage in f) or '/android/support/multidex' in f: currFucNum += smali_utils.get_smali_method_count(f, allRefs) totalFucNum = currFucNum for f in allFiles: f = f.replace('\\', '/') if not f.endswith('.smali'): continue if (appPackage and appPackage in f) or '/android/support/multidex' in f: continue thisFucNum = smali_utils.get_smali_method_count(f, allRefs) totalFucNum += thisFucNum #print('%d # %d ==> %s' % (thisFucNum, currDexIndex, f)) #print('totalFucNum is %d' % totalFucNum) if currFucNum + thisFucNum >= maxFuncNum: currFucNum = thisFucNum currDexIndex += 1 newDexPath = os.path.join(decompliePath, 'smali_classes%d' % currDexIndex) os.makedirs(newDexPath) else: currFucNum += thisFucNum if currDexIndex > 1: newDexPath = os.path.join(decompliePath, 'smali_classes%d' % currDexIndex) targetFile = newDexPath + f[len(smaliPath):] file_utils.copyFile(f, targetFile, True) return 0 def changeVersion(game, sdk, subChannel, config): ''' 更改版本号 ''' decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) yml = os.path.join(decompliePath, 'apktool.yml') versionCode = None versionName = None targetSdkVersion = None if 'versionCode' in config: versionCode = config['versionCode'] if 'versionName' in config: versionName = config['versionName'] if 'targetSdkVersion' in config: targetSdkVersion = config['targetSdkVersion'] return file_utils.changeVersion(yml, versionCode, versionName, targetSdkVersion) def recomplie(game, sdk, subChannel, config): ''' 回编译 ''' print('recomplie apk...') apktoolPath = file_utils.getApkToolPath() decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) outApk = file_utils.getOutApkPath(game, sdk, subChannel, config['cache']) return file_utils.execJarCmd(apktoolPath, 'b -f "%s" -o "%s"' % (decompliePath, outApk)) def alignApk(game, sdk, subChannel, config): ''' 对齐apk ''' print('align apk...') outApk = file_utils.getOutApkPath(game, sdk, subChannel, config['cache']) alignApk = file_utils.getAlignApkPath(game, sdk, subChannel, config['cache']) alignapkTool = file_utils.getAlignPath() if os.path.exists(alignApk): os.remove(alignApk) ret = file_utils.getExecPermission(alignapkTool) if ret: return ret # zipalign.exe -v -p 4 input.apk output.apk return file_utils.execFormatCmd('"%s" -f -p 4 "%s" "%s"' % (alignapkTool, outApk, alignApk)) def apksignerApk(game, sdk, subChannel, config): ''' 签名apk ''' print('sign apk...') path = os.path.join(file_utils.getCurrentPath(), 'keystore', 'key.json') jsonText = file_utils.readFile(path) signConfig = json.loads(jsonText) keystore = {} if game in signConfig: if sdk in signConfig[game] and subChannel in signConfig[game][sdk]: keystore = signConfig[game][sdk][subChannel] else: keystore = signConfig['default'] else: keystore = signConfig['default'] print('storeFile is "%s"' % keystore['storeFile']) apksigner = file_utils.getApksignerPath() alignApk = file_utils.getAlignApkPath(game, sdk, subChannel, config['cache']) signedApk = file_utils.getSignApkPath(game, sdk, subChannel, config['cache']) storeFile = os.path.join(file_utils.getCurrentPath(), 'keystore', keystore['storeFile']) if 'outName' in config and 'outPath' in config: if not os.path.exists(config['outPath']): os.makedirs(config['outPath']) signedApk = os.path.join(config['outPath'], config['outName'] + '.apk') elif 'outName' in config: signedApk = file_utils.getRenameApkPath(game, sdk, config['cache'], config['outName']) # java -jar apksigner.jar sign --ks key.jks --ks-key-alias releasekey --ks-pass pass:pp123456 --key-pass pass:pp123456 --out output.apk input.apk v2disable = '' if 'v2disable' in config and config['v2disable']: v2disable = ' --v2-signing-enabled=false' return file_utils.execJarCmd(apksigner, 'sign%s --ks "%s" --ks-key-alias %s --ks-pass pass:%s --key-pass pass:%s --out "%s" "%s"' % (v2disable, storeFile, keystore['keyAlias'], keystore['storePassword'], keystore['keyPassword'], signedApk, alignApk)) def addChannel(game, sdk, subChannel, config): ''' 添加渠道信息 ''' if 'v2disable' in config and config['v2disable']: return 0 walle = file_utils.getWallePath() signedApk = file_utils.getSignApkPath(game, sdk, subChannel, config['cache']) if 'outName' in config and 'outPath' in config: signedApk = os.path.join(config['outPath'], config['outName'] + '.apk') elif 'outName' in config: signedApk = file_utils.getRenameApkPath(game, sdk, config['cache'], config['outName']) properties = config['properties'] appid = '' appkey = '' if 'appid' in properties: appid = properties['appid'] if 'appkey' in properties: appkey = properties['appkey'] return file_utils.execJarCmd(walle, 'put -e version=%s,agent=%s,appid=%s,appkey=%s "%s" "%s"' % (config_utils.getDate(), properties['agent'], appid, appkey, signedApk, signedApk)) def clearTemp(game, sdk, subChannel, config): ''' 清空中间产生的文件 ''' print('clear temp...') alignApk = file_utils.getAlignApkPath(game, sdk, subChannel, config['cache']) outApk = file_utils.getOutApkPath(game, sdk, subChannel, config['cache']) if os.path.exists(alignApk): os.remove(alignApk) if os.path.exists(outApk): os.remove(outApk) decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) file_utils.deleteFolder(decompliePath) print('clear temp end') def packConsoleInput(): ''' 控制台打包 ''' if len(sys.argv) < 3: print('argument is missing') return 1 # 校验参数 game = sys.argv[1] sdk = sys.argv[2] # 可选参数,没有则默认打全部渠道 subChannel = None if len(sys.argv) > 3: subChannel = sys.argv[3] return packConsole(game, sdk, subChannel) def packConsole(game, sdk, subChannel): ''' 控制台打包 ''' if not os.path.exists(file_utils.getFullGameApk(game)): print('game "%s" not exists' % game) return 1 if not os.path.exists(file_utils.getFullSDKPath(sdk)): print('sdk "%s" not exists' % sdk) return 1 # 读取配置 channelPath = file_utils.getChannelPath(game, sdk) configPath = os.path.join(channelPath, 'config.json') if not os.path.exists(configPath): print('%s not exists' % configPath) return 1 jsonText = file_utils.readFile(configPath) config = json.loads(jsonText) # 检查参数 if not config_utils.checkConfig(config): return 1 # 处理参数 config_utils.replaceArgs(config) successCount = 0 failureCount = 0 if type(config) == dict: if subChannel is None or config['subChannel'] == subChannel: ret = pack(game, sdk, config) if ret: failureCount += 1 else: successCount += 1 else: print('subChannel "%s" no found' % subChannel) return 1 elif type(config) == list: found = False for itemConfig in config: if subChannel is None or itemConfig['subChannel'] == subChannel: found = True ret = pack(game, sdk, itemConfig) if ret: failureCount += 1 else: successCount += 1 if not found: print('subChannel "%s" no found' % subChannel) return 1 print('success %d, failure %d' % (successCount, failureCount)) return 0