APKTools.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. from logging import debug, root
  4. from struct import pack
  5. from Common import Setting, Debug, PathConvert, StringFormat
  6. Setting.SetDefaultencodingUTF8()
  7. import zipfile, sys, os, traceback
  8. import shutil
  9. import platform
  10. import subprocess
  11. import xml.dom.minidom as minidom
  12. import ConfigParser
  13. CUR_PLATFORM_NAME = platform.system()
  14. CUR_PATH = os.path.abspath(sys.path[0])
  15. APK_TOOLS_PATH = os.path.abspath(CUR_PATH + '/../APKTools')
  16. APK_TOOL_JAR_PATH = os.path.abspath(APK_TOOLS_PATH + '/apktool.jar')
  17. APK_SIGNER_JAR_PATH = os.path.abspath(APK_TOOLS_PATH + '/apksigner.jar')
  18. if CUR_PLATFORM_NAME == 'Windows':
  19. ZIP_ALIGN_PATH = os.path.abspath(APK_TOOLS_PATH + '/zipalign.exe')
  20. elif CUR_PLATFORM_NAME == 'Linux':
  21. ZIP_ALIGN_PATH = os.path.abspath(APK_TOOLS_PATH + '/zipalign.exe')
  22. elif CUR_PLATFORM_NAME == 'Darwin':
  23. ZIP_ALIGN_PATH = os.path.abspath(APK_TOOLS_PATH + '/zipalign_mac')
  24. else:
  25. ZIP_ALIGN_PATH = os.path.abspath(APK_TOOLS_PATH + '/zipalign.exe')
  26. TEMP_OUT_PATH = r"\apkTemp"
  27. VALID_CMD_ARGV = {"-help", "-rulePath", "-configPath",
  28. "-resPath", "-outPath", "-inPath",
  29. "-keyStorePath", "-keyAlias", "-keyStorePwd", "-keyAliasPwd",
  30. "-keySignVer"}
  31. def PrintHelp():
  32. Debug.Log('')
  33. Debug.Log('')
  34. Debug.Log('命令参数说明')
  35. Debug.Log('')
  36. Debug.Log('-help 查看帮助')
  37. Debug.Log('')
  38. Debug.Log('')
  39. Debug.Log('-configPath 根据INI配置文件来打包')
  40. Debug.Log('')
  41. Debug.Log('')
  42. Debug.Log('-inPath 原始Apk路径')
  43. Debug.Log('-outPath 生成的新Apk路径')
  44. Debug.Log('-resPath 作为资源的Apk文件')
  45. Debug.Log('-rulePath 指定更新资源规则,可找对应程序获得当前版本的规则')
  46. Debug.Log(' 为.apk文件里的资源路径')
  47. Debug.Log(' 如:assets/AssetsAndroid')
  48. Debug.Log('-keyStorePath 签名证书路径')
  49. Debug.Log('-keyAlias 签名证书alias')
  50. Debug.Log('-keyStorePwd 签名证书密码')
  51. Debug.Log('-keyAliasPwd 签名证书alias密码')
  52. Debug.Log('-keySignVer (可选)签名版本, 默认为v1')
  53. Debug.Log('')
  54. Debug.Log('')
  55. Debug.Log('')
  56. def ValidJava():
  57. try:
  58. cmds = ["java", "-version"]
  59. sp = subprocess.Popen(cmds, stdout=subprocess.PIPE,
  60. stderr=subprocess.PIPE)
  61. contents = sp.communicate()
  62. sp.wait()
  63. version = None
  64. for content in contents:
  65. lines = content.split("\n")
  66. for i in range(len(lines)):
  67. line = lines[i].strip()
  68. if line.startswith("java version "):
  69. version = line.replace("java version ", "")
  70. version = version.replace("\"", "")
  71. if not version:
  72. Debug.LogError("无法检测JDK版本 ")
  73. return False
  74. else:
  75. if version.startswith("1.8"):
  76. return True
  77. else:
  78. Debug.LogError("JDK版本不符,需要使用1.8版本 当前版本 " + version)
  79. return False
  80. except Exception as exc:
  81. Debug.LogError(exc)
  82. Debug.Log("")
  83. Debug.LogError("无法找到有效JDK, 请检查环境 !!! ")
  84. return False
  85. return True
  86. def ToolsChmodEXE():
  87. if CUR_PLATFORM_NAME != 'Linux' or CUR_PLATFORM_NAME != 'Darwin':
  88. return
  89. Debug.LogSuccess("赋予%s目录可执行权限开始" % APK_TOOLS_PATH)
  90. import stat
  91. for root, dirs, files in os.walk(APK_TOOLS_PATH):
  92. for file in files:
  93. os.chmod(file, os.stat(file).st_mode | stat.S_IEXEC)
  94. Debug.LogSuccess("赋予%s目录可执行权限成功" % APK_TOOLS_PATH)
  95. def ExecConfig(configPath):
  96. if not os.path.exists(StringFormat.FormatStringPlatform(configPath)):
  97. Debug.LogError(configPath + "不存在")
  98. return
  99. config = ConfigParser.ConfigParser()
  100. config.read(StringFormat.FormatStringPlatform(configPath))
  101. keyStorePathD = config.get('signinfodefault', 'keyStorePath')
  102. keyAliasD = config.get('signinfodefault', 'keyAlias')
  103. keyStorePwdD = config.get('signinfodefault', 'keyStorePwd')
  104. keyAliasPwdD = config.get('signinfodefault', 'keyAliasPwd')
  105. keySignVerD = config.get('signinfodefault', 'keySignVer')
  106. packs = []
  107. for k in config.sections():
  108. if k[:4] == 'pack':
  109. packs.append(int(k[4:]))
  110. packs.sort()
  111. success = []
  112. errors = []
  113. for i in packs:
  114. oneConfig = 'pack' + str(i)
  115. inPath = config.get(oneConfig, 'inPath')
  116. outPath = config.get(oneConfig, 'outPath')
  117. rulePath = config.get(oneConfig, 'rulePath')
  118. resPath = config.get(oneConfig, 'resPath')
  119. if config.has_option(oneConfig, "keyStorePath"):
  120. keyStorePath = config.get(oneConfig, 'keyStorePath')
  121. keyAlias = config.get(oneConfig, 'keyAlias')
  122. keyStorePwd = config.get(oneConfig, 'keyStorePwd')
  123. keyAliasPwd = config.get(oneConfig, 'keyAliasPwd')
  124. keySignVer = config.get(oneConfig, 'keySignVer')
  125. res = ExecOne(inPath, outPath, rulePath, resPath,
  126. keyStorePath, keyAlias, keyStorePwd, keyAliasPwd,
  127. keySignVer)
  128. else:
  129. res = ExecOne(inPath, outPath, rulePath, resPath,
  130. keyStorePathD, keyAliasD, keyStorePwdD, keyAliasPwdD,
  131. keySignVerD)
  132. if res:
  133. success.append(i)
  134. else:
  135. errors.append(i)
  136. successNum = len(success)
  137. if successNum > 0:
  138. Debug.Log("")
  139. Debug.Log("")
  140. Debug.Log("")
  141. Debug.LogSuccess("执行成功的有:")
  142. for i in success:
  143. oneConfig = 'pack' + str(i)
  144. Debug.LogSuccess(config.get(oneConfig, 'inPath'))
  145. Debug.LogSuccess("\t to " + config.get(oneConfig, 'outPath'))
  146. else:
  147. Debug.Log("")
  148. Debug.Log("")
  149. errorNum = len(errors)
  150. if errorNum > 0:
  151. Debug.Log("")
  152. Debug.LogError("执行失败的有:", True)
  153. for i in errors:
  154. oneConfig = 'pack' + str(i)
  155. Debug.LogError(config.get(oneConfig, 'inPath'))
  156. Debug.LogError("\t to " + config.get(oneConfig, 'outPath'))
  157. Debug.Log("")
  158. Debug.Log("")
  159. sys.exit(2)
  160. else:
  161. Debug.Log("")
  162. Debug.Log("")
  163. def ExecOne(inputPath, outPath, rulePath, resPath,
  164. keyStorePath, keyAlias, keyStorePwd, keyAliasPwd,
  165. keySignVer):
  166. if not os.path.exists(StringFormat.FormatStringPlatform(inputPath)):
  167. Debug.LogError(inputPath + "不存在")
  168. return False
  169. if not outPath or outPath == "":
  170. Debug.LogError(outPath + "目标目录无效")
  171. return False
  172. if not os.path.exists(StringFormat.FormatStringPlatform(rulePath)):
  173. Debug.LogError(rulePath + "不存在")
  174. return False
  175. if not os.path.exists(StringFormat.FormatStringPlatform(resPath)):
  176. Debug.LogError(resPath + "不存在")
  177. return False
  178. if not os.path.exists(StringFormat.FormatStringPlatform(keyStorePath)):
  179. Debug.LogError(keyStorePath + "不存在")
  180. return False
  181. dirOutPath = os.path.dirname(outPath)
  182. tempOutPath = dirOutPath + TEMP_OUT_PATH
  183. if os.path.exists(StringFormat.FormatStringPlatform(tempOutPath)):
  184. Debug.LogError(tempOutPath + "临时解压目录已存在,请先删除")
  185. return False
  186. result = ExecOneApk(inputPath, outPath, rulePath, resPath,
  187. keyStorePath, keyAlias, keyStorePwd, keyAliasPwd,
  188. keySignVer, tempOutPath)
  189. if os.path.exists(StringFormat.FormatStringPlatform(tempOutPath)):
  190. shutil.rmtree(u'' + tempOutPath)
  191. if os.path.exists(StringFormat.FormatStringPlatform(outPath + ".idsig")):
  192. os.remove(StringFormat.FormatStringPlatform(outPath + ".idsig"))
  193. return result
  194. def ExecOneApk(inputPath, outPath, rulePath, resPath,
  195. keyStorePath, keyAlias, keyStorePwd, keyAliasPwd,
  196. keySignVer, tempOutPath):
  197. if keySignVer:
  198. if isinstance(keySignVer, str):
  199. if len(keySignVer) > 1:
  200. if (keySignVer[:1] == 'v' or keySignVer == 'V'):
  201. keySignVer = keySignVer[1:]
  202. keySignVer = int(keySignVer)
  203. else:
  204. keySignVer = 1
  205. rules = ConfigParser.ConfigParser()
  206. rules.read(StringFormat.FormatStringPlatform(rulePath))
  207. covers = rules.items('cover')
  208. if not covers or len(covers) < 0:
  209. Debug.LogError(rulePath + ' 规则文件[cover]内容不能为空')
  210. filters = rules.items('filter')
  211. try:
  212. # 解包原始资源
  213. Debug.Log("解包原始资源")
  214. Debug.Log("")
  215. cmds = ["java", "-jar", StringFormat.FormatStringPlatform(APK_TOOL_JAR_PATH),
  216. "d", StringFormat.FormatStringPlatform(inputPath),
  217. "-o", StringFormat.FormatStringPlatform(tempOutPath)]
  218. res = subprocess.Popen(cmds, shell=True, stdout=subprocess.PIPE)
  219. res.wait()
  220. if res.returncode != 0:
  221. Debug.LogError("无法解包 " + inputPath)
  222. return False
  223. # 解包新资源
  224. Debug.Log("解包新资源")
  225. Debug.Log("")
  226. cmds = ["java", "-jar", StringFormat.FormatStringPlatform(APK_TOOL_JAR_PATH),
  227. "d-axml", StringFormat.FormatStringPlatform(resPath),
  228. "-o", StringFormat.FormatStringPlatform(tempOutPath + "/AndroidManifest-new.xml")]
  229. res = subprocess.Popen(cmds, shell=False, stdout=subprocess.PIPE)
  230. res.wait()
  231. if res.returncode != 0:
  232. Debug.LogError("无法解包 " + resPath)
  233. return False
  234. newVersionCode = None
  235. newVersionName = None
  236. newLebianVerCode = None
  237. domTree = minidom.parse(StringFormat.FormatStringPlatform(tempOutPath + "/AndroidManifest-new.xml"))
  238. root = domTree.documentElement
  239. newVersionCode = root.getAttribute('platformBuildVersionCode')
  240. newVersionName = root.getAttribute('platformBuildVersionName')
  241. applications = root.getElementsByTagName('application')
  242. for app in applications:
  243. metadatas = app.getElementsByTagName('meta-data')
  244. for metadata in metadatas:
  245. if metadata.getAttribute('android:name') == "LEBIAN_VERCODE":
  246. newLebianVerCode = metadata.getAttribute('android:value')
  247. break
  248. if newLebianVerCode:
  249. break
  250. if not newVersionCode or not newVersionName or not newLebianVerCode:
  251. Debug.LogError("无法识别" + resPath + "里的数据信息")
  252. return False
  253. domTree = minidom.parse(StringFormat.FormatStringPlatform(tempOutPath + "/AndroidManifest.xml"))
  254. root = domTree.documentElement
  255. # 修改AndroidManifest.xml
  256. Debug.Log("修改AndroidManifest.xml")
  257. Debug.Log("")
  258. Debug.LogSuccess("BuildVersionCode " + root.getAttribute('platformBuildVersionCode') + " -> " + newVersionCode)
  259. root.setAttribute('platformBuildVersionCode', newVersionCode)
  260. Debug.LogSuccess("BuildVersionCode " + root.getAttribute('platformBuildVersionName') + " -> " + newVersionName)
  261. root.setAttribute('platformBuildVersionName', newVersionName)
  262. applications = root.getElementsByTagName('application')
  263. for app in applications:
  264. metadatas = app.getElementsByTagName('meta-data')
  265. for metadata in metadatas:
  266. if metadata.getAttribute('android:name') == "LEBIAN_VERCODE":
  267. Debug.LogSuccess("LEBIAN_VERCODE " + metadata.getAttribute('android:value') + " -> " + newLebianVerCode)
  268. metadata.setAttribute('android:value', newLebianVerCode)
  269. newLebianVerCode = None
  270. break
  271. if not newLebianVerCode:
  272. break
  273. Debug.Log("")
  274. with open(StringFormat.FormatStringPlatform(tempOutPath + "/AndroidManifest.xml"), "w") as f:
  275. f.write(domTree.toxml(encoding = "utf-8"))
  276. os.remove(StringFormat.FormatStringPlatform(tempOutPath + "/AndroidManifest-new.xml"))
  277. # 更新资源
  278. Debug.Log("更新资源")
  279. Debug.Log("")
  280. with zipfile.ZipFile(StringFormat.FormatStringPlatform(resPath)) as resFile:
  281. for data in resFile.infolist():
  282. for cover in covers:
  283. fileName = data.filename
  284. if fileName == cover or fileName.startswith(cover):
  285. if not filters or fileName in filters:
  286. resFile.extract(data, tempOutPath)
  287. break
  288. # 编译新APK
  289. Debug.Log("编译新APK")
  290. Debug.Log("")
  291. cmds = ["java", "-jar", StringFormat.FormatStringPlatform(APK_TOOL_JAR_PATH),
  292. "b", StringFormat.FormatStringPlatform(tempOutPath)]
  293. res = subprocess.Popen(cmds, shell=False, stdout=subprocess.PIPE)
  294. res.wait()
  295. if res.returncode != 0:
  296. Debug.LogError("无法编译包 " + tempOutPath)
  297. return
  298. # zip对齐apk
  299. Debug.Log("zip对齐apk")
  300. Debug.Log("")
  301. cmds = [StringFormat.FormatStringPlatform(ZIP_ALIGN_PATH),
  302. "-p", "-f", "4",
  303. StringFormat.FormatStringPlatform(tempOutPath + "/dist/" + os.path.basename(inputPath)),
  304. StringFormat.FormatStringPlatform(outPath)]
  305. res = subprocess.Popen(cmds, shell=False, stdout=subprocess.PIPE)
  306. res.wait()
  307. if res.returncode != 0:
  308. Debug.LogError("无法zip对齐APK " + outPath)
  309. return
  310. # 签名apk
  311. Debug.Log("签名apk")
  312. Debug.Log("")
  313. cmds = ["java", "-jar", StringFormat.FormatStringPlatform(APK_SIGNER_JAR_PATH), "sign",
  314. "--ks", StringFormat.FormatStringPlatform(keyStorePath), "--ks-key-alias", keyAlias,
  315. "--ks-pass", "pass:" + keyStorePwd, "--key-pass", "pass:" + keyAliasPwd]
  316. if keySignVer >= 4:
  317. cmds.append('--v4-signing-enabled')
  318. cmds.append('true')
  319. if keySignVer >= 3:
  320. cmds.append('--v3-signing-enabled')
  321. cmds.append('true')
  322. if keySignVer >= 2:
  323. cmds.append('--v2-signing-enabled')
  324. cmds.append('true')
  325. cmds.append('--v1-signing-enabled')
  326. cmds.append('true')
  327. cmds.append(StringFormat.FormatStringPlatform(outPath))
  328. res = subprocess.Popen(cmds, shell=False, stdout=subprocess.PIPE)
  329. res.wait()
  330. if res.returncode != 0:
  331. Debug.LogError("无法签名APK " + outPath)
  332. return False
  333. Debug.LogSuccess("执行成功" + inputPath + " -> " + outPath)
  334. Debug.Log("")
  335. Debug.Log("")
  336. return True
  337. except Exception as exc:
  338. Debug.LogError(exc)
  339. Debug.Log("")
  340. return False
  341. def CMDArgvIsNull(dic, keyName):
  342. if (not keyName in dic):
  343. PrintHelp()
  344. Debug.Log('')
  345. Debug.Log('')
  346. Debug.LogError(keyName + ' 不能为空')
  347. Debug.Log('')
  348. Debug.Log('')
  349. return True
  350. return False
  351. def Main():
  352. args = sys.argv[1:]
  353. dic = {}
  354. num = len(args)
  355. for i in range(num):
  356. arg = StringFormat.FormatStdinString(args[i])
  357. if arg.startswith('-'):
  358. if not arg in VALID_CMD_ARGV:
  359. Debug.LogError(arg + " 不是有效的参数")
  360. PrintHelp()
  361. return
  362. if i + 1 >= num:
  363. Debug.LogSuccess(arg)
  364. dic[arg] = ''
  365. else:
  366. nextArg = StringFormat.FormatStdinString(args[i + 1])
  367. if nextArg.startswith('-'):
  368. Debug.LogSuccess(arg)
  369. dic[arg] = ''
  370. else:
  371. Debug.LogSuccess(arg + " " + nextArg)
  372. dic[arg] = nextArg
  373. if len(dic) <= 0:
  374. PrintHelp()
  375. return
  376. if ('-help' in dic):
  377. PrintHelp()
  378. return
  379. if not ValidJava():
  380. return
  381. ToolsChmodEXE()
  382. if ('-configPath' in dic):
  383. ExecConfig(dic['-configPath'])
  384. return
  385. if (CMDArgvIsNull(dic, '-inPath')):
  386. return
  387. if (CMDArgvIsNull(dic, '-outPath')):
  388. return
  389. if (CMDArgvIsNull(dic, '-rulePath')):
  390. return
  391. if (CMDArgvIsNull(dic, '-resPath')):
  392. return
  393. if (CMDArgvIsNull(dic, '-keyStorePath')):
  394. return
  395. if (CMDArgvIsNull(dic, '-keyAlias')):
  396. return
  397. if (CMDArgvIsNull(dic, '-keyStorePwd')):
  398. return
  399. if (CMDArgvIsNull(dic, '-keyAliasPwd')):
  400. return
  401. Debug.Log('')
  402. Debug.Log('')
  403. keySignVer = None
  404. if ('-keySignVer' in dic):
  405. keySignVer = dic['-keySignVer']
  406. ExecOne(dic['-inPath'], dic['-outPath'], dic['-rulePath'], dic['-resPath'],
  407. dic['-keyStorePath'], dic['-keyAlias'], dic['-keyStorePwd'], dic['-keyAliasPwd'],
  408. keySignVer)
  409. if __name__ == '__main__':
  410. try:
  411. Main()
  412. except Exception as e:
  413. Debug.LogError(e)
  414. Debug.LogError(traceback.format_exc(), True)
  415. sys.exit(1)