# 安卓游戏打包脚本 # 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 re import file_utils import xml_utils import smali_utils import config_utils import game_utils import common_utils import os import os.path import json import sys import importlib import uuid import zipfile import time import datetime ignoreLauncher = ['jm_ysdk', 'jm_yijie', 'jm_quick', 'jm_beiyu', 'jm_xq_jrtt', 'jm_zy_ysdk'] adaptApp = ['yjzx'] startTime = '' def pack(game, sdk, config): config['cache'] = uuid.uuid1() subChannel = config['subChannel'] print('game = %s, sdk = %s, subChannel = %s, ...' % (game, sdk, 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) # 复制icon图标到res ret = copyIcon(game, sdk, subChannel, config) # if ret: # return ret # 打包lib依赖 ret = packJar(game, sdk, subChannel, config) if ret: return ret # 继承JMApplication common_utils.changeApplication( game, sdk, subChannel, config, 'com.jmhy.sdk.common.JMApplication') # 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 # 游戏独立处理 ret = doGameScript(game, sdk, config) if ret: return ret # 格式化Xml # ret = formatXml(game, sdk, subChannel, 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 # oaid sdk ret = addOaidSdk(game, sdk, subChannel, config, "1.0.10") 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 # 更改HardwareAccelerated属性 ret = replaceAmHardwareAccelerated(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 ret = copyApk2OutDir(game, sdk, subChannel, config) if ret: return ret # 清理产生的中间文件 if config['clearCache']: clearTemp(game, sdk, subChannel, config) endTime = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') d1 = datetime.datetime.strptime(endTime, '%Y-%m-%d %H:%M:%S') d2 = datetime.datetime.strptime(startTime, '%Y-%m-%d %H:%M:%S') d = d1 - d2 print('开始时间:' + startTime) print('结束时间:' + endTime) print('用时:{}'.format(config_utils.getTime(d.seconds))) return 0 def decomplie(game, sdk, subChannel, config): ''' 解包 ''' apktoolPath = file_utils.getApkToolPath() # gamePath = file_utils.getFullGameApk(game) cacheGameApk = file_utils.getCacheGameApk(game, config['random'], sdk) 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"' % (cacheGameApk, 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) allFiles = [] allFiles = file_utils.list_files(codePath, allFiles) for f in allFiles: fpath, fname = os.path.split(f) # 分离文件名和路径 if fname == 'R.smali' or fname.startswith('R$'): continue os.remove(f) # print('remove %s' % f) 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' or abi == 'arm64-v8a': print('append : ' + abi) 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): # print('res path %s' % path) if (path.startswith('drawable') or path.startswith('mipmap')) and path.endswith('-v4'): # print('merge path %s' % path) 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''' if resFile.endswith('.DS_Store'): continue # print('readAllRes -- > ' + os.path.join(absPath, resFile)) 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、启动图等 ''' random = config['random'] channelPath = file_utils.getSubChannelPath(game, sdk, random, subChannel) decompliePath = file_utils.getDecompliePath( game, sdk, subChannel, config['cache']) # assets print('copy assets...') # 适配一剑斩仙 if game in adaptApp: print('适配一剑斩仙...') decomplieAssetsPath = file_utils.getFullPath(decompliePath, 'assets') skinZipPath = os.path.join(decomplieAssetsPath, 'skin.zip') skinPath = os.path.join(decomplieAssetsPath, 'skin') if os.path.exists(skinZipPath): with zipfile.ZipFile(skinZipPath) as zf: zf.extractall(decomplieAssetsPath) print('create unzip skin') assetsPath = file_utils.getFullPath(channelPath, 'assets') decomplieAssetsPath = file_utils.getFullPath( decompliePath, 'assets') if os.path.exists(assetsPath): ret = file_utils.copyFileAllDir( assetsPath, decomplieAssetsPath) with zipfile.ZipFile(skinZipPath, 'w') as z: for root, dirs, files in os.walk(skinPath): for single_file in files: filepath = os.path.join(root, single_file) print('create zip ---> ' + filepath) temPath = 'skin/' + single_file z.write(filepath, temPath) z.close() else: print('normal 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 else: print('normal 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_temp.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 random = config['random'] channelPath = file_utils.getSubChannelPath(game, sdk, random, 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']) manifest = os.path.join(decompliePath, 'AndroidManifest.xml') activity = xml_utils.getLauncherActivityName(manifest) if activity == 'com.jmhy.sdk.template.LauncherActivity': print('add launcher already exist...') return 1 # 添加关联资源 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...') 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 copyIcon(game, sdk, subChannel, config): ''' 复制icon到res ,一键登录使用 ''' if config['changeIcon']: decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) decomplieResPath = file_utils.getFullPath(decompliePath, 'res') iconPath = os.path.join(decomplieResPath, 'mipmap-xhdpi', 'common_sdk_icon.png') desPath = os.path.join( decomplieResPath, 'drawable-hdpi', 'jm_cmcc_icon.png') file_utils.copyFile(iconPath, desPath) else: decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) decomplieResPath = file_utils.getFullPath(decompliePath, 'res') desPath = os.path.join(decomplieResPath, 'drawable-hdpi', 'jm_cmcc_icon.png') manifest = os.path.join(decompliePath, 'AndroidManifest.xml') icon = xml_utils.getApplicationAttr(manifest, 'icon') tag = icon[1:].split("/") if len(tag) < 1: return hdpi = ['-xhdpi', '-xxhdpi', '-xxxhdpi'] for h in hdpi: p1 = '%s%s' % (tag[0], h) png = tag[1] + ".png" iconPath = os.path.join(decomplieResPath, p1, png) if os.path.exists(iconPath): print('iconPath = ' + iconPath) file_utils.copyFile(iconPath, desPath) break def formatHex(millisecond): ''' 将毫秒转为16进制,4位格式 ''' timeHex = str(hex(millisecond)).upper() timeHex = timeHex[2:] formatHex = '' if len(timeHex) == 3: formatHex = '0x0' + timeHexaddLauncher 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 doGameScript(game, sdk, config): ''' 执行游戏相关特殊处理脚本 ''' channelPath = file_utils.getFullPath('game_script', game) targetScript = os.path.join(channelPath, 'game_script.py') print(targetScript + "--------") if not os.path.exists(targetScript): print('game_script no exists') return 0 print('doGameScript...') sys.path.append(channelPath) module = importlib.import_module('game_script') ret = module.execute(game, sdk, config) sys.path.remove(channelPath) return ret def addLogSdk(game, sdk, subChannel, config, logSdk): # 拷贝jniLibs decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) sdkPath = '' print('add log sdk, sdk:' + sdk) if sdk == 'jm_beta_sdk' or sdk == 'beta_sdk': sdkPath = file_utils.getFullLogSDKPath(logSdk, True) else: 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' or abi == 'arm64-v8a': 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 changePlaceholders(game, sdk, subChannel, config) return packLogJar(game, sdk, subChannel, config, logSdk) def addOaidSdk(game, sdk, subChannel, config, oaidVersion): sdkPath = file_utils.getFullOaidSDKPath(oaidVersion) decompliePath = file_utils.getDecompliePath( game, sdk, subChannel, config['cache']) # 拷贝assets print('copy oaid 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 # 拷贝jniLibs print('copy oaid 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 oaid 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 packOaidJar(game, sdk, subChannel, config, oaidVersion) def generateNewRFile(game, sdk, subChannel, config): ''' 生成新的R文件 ''' decompliePath = file_utils.getDecompliePath( game, sdk, subChannel, config['cache']) 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) # 生成R文件 print('生成R文件 ...') if config['aapt2disable']: aapt = file_utils.getAAPTPath() ret = file_utils.getExecPermission(aapt) if ret: return ret 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 else: # compile aapt = file_utils.getAAPT2Path() ret = file_utils.getExecPermission(aapt) if ret: return ret print('compiled res ...') complieResPath = os.path.join(compliePath, 'compiled') complieResCmd = '"%s" compile --dir "%s" -o "%s"' % ( aapt, decomplieResPath, complieResPath) file_utils.execFormatCmd(complieResCmd) # unzip print('unzip compiled res ...') unzipResPath = os.path.join(compliePath, 'aapt2_res') with zipfile.ZipFile(complieResPath) as zf: zf.extractall(unzipResPath) print('create unzip %s' % unzipResPath) # link print('link res ...') outApk = os.path.join(compliePath, 'res.apk') linkResCmd = '"%s" link -o "%s" -I "%s" --manifest "%s" --java "%s" --auto-add-overlay' % ( aapt, outApk, androidPlatforms, manifest, compliePath) for filename in os.listdir(unzipResPath): linkResCmd += ' %s' % filename print('link cmd len is %s' % len(linkResCmd)) ret = file_utils.execFormatCmd(linkResCmd, unzipResPath) 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.8 -target 1.8 -encoding UTF-8 "%s"' % RSourceFile ret = file_utils.execFormatCmd(complieRCmd) if ret: return ret # 生成dex print('dex R.class ...') outDex = os.path.join(compliePath, 'classes.dex') if config['aapt2disable']: dx = file_utils.getDxPath() dexCmd = '--dex --no-warning --output="%s" "%s"' % ( outDex, compliePath) else: dx = file_utils.getD8Path() clazz = os.path.join(packagePath, '*.class') dexCmd = '--lib "%s" --output "%s" %s' % ( androidPlatforms, compliePath, clazz) ret = file_utils.execJarCmd(dx, dexCmd) 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, 'd "%s" -o "%s"' % (outDex, outPath)) 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') if not os.path.exists(outPath): os.makedirs(outPath) if config['aapt2disable']: dx = file_utils.getDxPath() dexCmd = '--dex --no-warning --output="%s"' % outPath else: dx = file_utils.getD8Path() androidPlatforms = file_utils.getAndroidCompileToolPath() dexCmd = '--lib "%s" --output "%s"' % (androidPlatforms, 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) elif 'default' in libConf: libList = libConf['default'] 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: print('getMultiDexPath ...') 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, 'd "%s" -o "%s"' % (outDex, outPath)) 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): ''' 打包Log jar ''' decompliePath = file_utils.getDecompliePath(game, sdk, subChannel, config['cache']) outPath = file_utils.getFullPath(decompliePath, 'gen') if not os.path.exists(outPath): os.makedirs(outPath) if config['aapt2disable']: dx = file_utils.getDxPath() dexCmd = '--dex --multi-dex --no-warning --output="%s"' % outPath else: dx = file_utils.getD8Path() androidPlatforms = file_utils.getAndroidCompileToolPath() dexCmd = '--lib "%s" --output "%s"' % (androidPlatforms, outPath) # 找到所有lib依赖 if sdk == 'jm_beta_sdk' or sdk == 'beta_sdk': sdkPath = file_utils.getFullLogSDKPath(logSdk, True) else: sdkPath = file_utils.getFullLogSDKPath(logSdk) libs = os.path.join(sdkPath, 'libs') libConfig = os.path.join(libs, 'config.json') # 存在配置文件 if os.path.exists(libConfig): jsonText = file_utils.readFile(libConfig) libList = json.loads(jsonText) for jar in libList: 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) # 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, 'd "%s" -o "%s"' % (outDex, outPath)) 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 packOaidJar(game, sdk, subChannel, config, oaidVerion): ''' 打包oaid jar ''' decompliePath = file_utils.getDecompliePath( game, sdk, subChannel, config['cache']) outPath = file_utils.getFullPath(decompliePath, 'gen') if not os.path.exists(outPath): os.makedirs(outPath) if config['aapt2disable']: dx = file_utils.getDxPath() dexCmd = '--dex --multi-dex --no-warning --output="%s"' % outPath else: dx = file_utils.getD8Path() androidPlatforms = file_utils.getAndroidCompileToolPath() dexCmd = '--lib "%s" --output "%s"' % (androidPlatforms, outPath) # 找到所有lib依赖 sdkPath = file_utils.getFullOaidSDKPath(oaidVerion) libs = os.path.join(sdkPath, 'libs') libConfig = os.path.join(libs, 'config.json') # 存在配置文件 # if os.path.exists(libConfig): # jsonText = file_utils.readFile(libConfig) # libList = json.loads(jsonText) # for jar in libList: # 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) # sdk实现类 print('packageing oaid jar ...') dexCmd += ' ' + os.path.join(libs, 'miit_mdid_%s.jar' % oaidVerion) 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, 'd "%s" -o "%s"' % (outDex, outPath)) 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'] else: targetSdkVersion = 26 print('changeVersion versionCode=%s,versionName=%s,targetSdkVersion=%s' % (versionCode, versionName, 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']) useAppt2 = ' --use-aapt2' if config['aapt2disable']: useAppt2 = '' return file_utils.execJarCmd(apktoolPath, 'b -f "%s" -o "%s"%s' % (decompliePath, outApk, useAppt2)) 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...') print('game = %s, sdk = %s, subChannel = %s, ...' % (game, sdk, subChannel)) path = os.path.join(file_utils.getCurrentPath(), 'keystore', 'key.json') jsonText = file_utils.readFile(path) signConfig = json.loads(jsonText) keystore = {} for key in signConfig.keys(): print(key) if game.find(key) > -1 or game == key: if sdk in signConfig[key]: keystore = signConfig[key][sdk] break else: keystore = signConfig['default'] else: keystore = signConfig['default'] # if game in signConfig: # if sdk in signConfig[game]: # keystore = signConfig[game][sdk] # 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.getSiginedApkPath( game, sdk, subChannel, config['cache']) storeFile = os.path.join(file_utils.getCurrentPath(), 'keystore', keystore['storeFile']) # 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.getSiginedApkPath( game, sdk, subChannel, config['cache']) walleApk = file_utils.getWalleApkPath( game, sdk, subChannel, config['cache']) properties = config['properties'] appid = '' appkey = '' host = '' if 'appid' in properties: appid = properties['appid'] if 'appkey' in properties: appkey = properties['appkey'] if 'host' in properties: host = properties['host'] return file_utils.execJarCmd(walle, 'put -e version=%s,agent=%s,appid=%s,appkey=%s,host=%s "%s" "%s"' % ( config_utils.getDate(), properties['agent'], appid, appkey, host, signedApk, walleApk)) def clearTemp(game, sdk, subChannel, config): ''' 清空中间产生的文件 ''' print('clear temp...') targetApkPath = file_utils.getTargetApkPath(game, sdk, config['cache']) if os.path.exists(targetApkPath): file_utils.deleteFolder(targetApkPath) decompliePath = file_utils.getDecompliePath( game, sdk, subChannel, config['cache']) if os.path.exists(decompliePath): file_utils.deleteFolder(decompliePath) random = config['random'] channelPath = file_utils.getChannelPath(game, random, sdk) if os.path.exists(channelPath): file_utils.deleteFolder(channelPath) 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, config, subChannel): ''' 控制台打包 ''' cache_game_apk_path = file_utils.getCacheGameApk(game, config['random'], sdk) print('cache game apk path %s' % cache_game_apk_path) if not os.path.exists(cache_game_apk_path): 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 global startTime startTime = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') # 读取配置 random = config['random'] channelPath = file_utils.getChannelPath(game, random, 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)) subChannelPath = os.path.join(channelPath, subChannel) print('------subChannelPath 目录清空:%s -------' % subChannelPath) file_utils.deleteFolder(subChannelPath) return 0 def formatXml(game, sdk, subChannel, config): decompliePath = file_utils.getDecompliePath( game, sdk, subChannel, config['cache']) manifest = os.path.join(decompliePath, 'AndroidManifest.xml') return xml_utils.formatXml(manifest) def copyApk2OutDir(game, sdk, subChannel, config): walleApk = file_utils.getWalleApkPath( game, sdk, subChannel, config['cache']) releaseApkPath = "" if 'outName' in config and 'outPath' in config: if not os.path.exists(config['outPath']): os.makedirs(config['outPath']) releaseApkPath = os.path.join( config['outPath'], config['outName'] + '.apk') elif 'outName' in config: releaseApkPath = file_utils.getRenameApkPath( game, sdk, config['cache'], config['outName']) print('------生成正式包路径 %s -------' % releaseApkPath) file_utils.copyFile(walleApk, releaseApkPath) def replaceAmHardwareAccelerated(game, sdk, subChannel, config): decompliePath = file_utils.getDecompliePath( game, sdk, subChannel, config['cache']) manifest = os.path.join(decompliePath, 'AndroidManifest.xml') lines = open(manifest).readlines() fp = open(manifest, "w") isReplaced = False for l in lines: if isReplaced == False and l.find("android:hardwareAccelerated=") >= 0: isReplace = True l = re.sub("android:hardwareAccelerated=\"false\"", "android:hardwareAccelerated=\"true\"", l) fp.write(l) fp.close() pass def openFile(file, mode): return open(file, mode, encoding='UTF-8')