├── .gitignore ├── LICENSE ├── README.md ├── apktool.py ├── assemble.py ├── bin ├── AXMLPrinter.jar ├── VasDolly.jar └── diffuse.jar ├── channel.py ├── config ├── config.yml └── template_diff.html ├── diffuse.py ├── docs └── README-zh.md ├── files.py ├── git_tag.py ├── global_config.py ├── lanzou.py ├── logger.py ├── mailing.py ├── publish.py ├── resources.py ├── run.py ├── strengthen.py └── telegram_bot.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/* 2 | app.log 3 | config/config_product.yml 4 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auto Package 2 | 3 | [中文版](docs/README-zh.md) 4 | 5 | ## 1. What? 6 | 7 | An Android auto package script. Mainly used to 8 | 9 | - Call gradlew command to package APKs, 32 bit and 64 bit separately 10 | - Copy APKs to given directory 11 | - Diff APK with last version or output its info base on [diffuse](https://github.com/JakeWharton/diffuse) 12 | - Copy language resources to given dirctory, commit to github repo for translation cooperation 13 | - Add git tag automatically and push to remote git repo 14 | - Automatically generate APP upgrade log from git logs 15 | - Reinforce APKs by [360 Security](https://jiagu.360.cn/#/global/index) 16 | - Package APKs in multi-channel by [VasDolly](https://github.com/Tencent/VasDolly) 17 | - Upload APKs to lanzou cloud 18 | - Send APK and upgrade log to Telegram group by bot 19 | - Notify receivers when succeed by email 20 | - More in the future. 21 | 22 | ## 2. How? 23 | 24 | ### 2.1 Prepare 25 | 26 | - Python: Python3 27 | - Add `pyymal` library to your environment by: `pip install pyyaml` 28 | - And `requests` library to your environment by: `pip install requests` 29 | - And `requests_toolbelt` library to your environment by: `pip install requests_toolbelt` 30 | 31 | ### 2.2 Use 32 | 33 | - Configure [the configuration file](config.yml). 34 | - Execute `python run.py` under root directory. 35 | -------------------------------------------------------------------------------- /apktool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging, os 5 | import zipfile, traceback 6 | import xml.dom.minidom 7 | from global_config import * 8 | 9 | class ApkInfo: 10 | 11 | def __init__(self, \ 12 | source_apk_file_path: str = '', \ 13 | package: str = '', \ 14 | version_name: str = '', \ 15 | version_code: str = '' 16 | ) -> None: 17 | ''' 18 | The APK info. 19 | - source_apk_file_path: source apk path 20 | - package: package name 21 | - version_name: app version name 22 | - version_code: app version code 23 | ''' 24 | self.source_apk_file_path = source_apk_file_path 25 | self.package = package 26 | self.version_code = version_code 27 | self.version_name = version_name 28 | self.build_bit:BitConfiguration = None 29 | self.build_flavor: FlavorConfiguration = None 30 | self.output_apk_directory = '' 31 | self.output_apk_file_path = '' 32 | 33 | def is_valid(self) -> bool: 34 | '''Is given APKInfo valid.''' 35 | return len(self.source_apk_file_path) > 0 36 | 37 | def __str__(self) -> str: 38 | return "ApkInfo([%s][%s][%s][%s])" % (self.source_apk_file_path, self.package, self.version_name, self.version_code) 39 | 40 | def parse_apk_info(apk_file_path: str) -> ApkInfo: 41 | ''' 42 | Parse APK info. 43 | - apk_file_path: path of APK file. 44 | ''' 45 | logging.info("Trying to get apk info from [%s]" % apk_file_path) 46 | zipFile = zipfile.ZipFile(apk_file_path) 47 | zipFile.extract("AndroidManifest.xml", '.') 48 | zipFile.close() 49 | # out = os.popen("java -jar ../bin/AXMLPrinter.jar %s" % ('AndroidManifest.xml')).read().strip() 50 | out = os.popen("java -jar bin/AXMLPrinter.jar %s" % ('AndroidManifest.xml')).read().strip() 51 | try: 52 | root = xml.dom.minidom.parseString(out) 53 | collection = root.documentElement 54 | vcode = collection.getAttribute('android:versionCode') 55 | vname = collection.getAttribute('android:versionName') 56 | pkg = collection.getAttribute('package') 57 | return ApkInfo(apk_file_path, pkg, vname, vcode) 58 | except Exception: 59 | logging.error("Failed to parse Android manifest: %s" % traceback.format_exc()) 60 | finally: 61 | os.remove('AndroidManifest.xml') 62 | return ApkInfo() 63 | -------------------------------------------------------------------------------- /assemble.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from files import * 5 | from apktool import * 6 | from global_config import * 7 | from logger import * 8 | from channel import * 9 | 10 | def assemble(bit: BitConfiguration, flavor: FlavorConfiguration) -> ApkInfo: 11 | '''Assemble APK with bit and flavor and copy APK and mapping files to destination.''' 12 | # ./gradlew assembleNationalDebug -Pbuild_ndk_type=ndk_32 -Pversion_name=3.8.0 13 | assemble_command = "cd %s && ./gradlew clean %s -Pbuild_ndk_type=%s" \ 14 | % (config.gradlew_location, flavor.get_gradlew_command(), bit.get_gradlew_bit_param_value()) 15 | if len(config.gradle_java_home) > 0: 16 | assemble_command = assemble_command + (" -Dorg.gradle.java.home=\"%s\"" % config.gradle_java_home) 17 | if len(build_config.version) != 0: 18 | assemble_command = assemble_command + " -Pversion_name=" + build_config.version 19 | logi("Final gradlew command is [%s]" % assemble_command) 20 | os.system(assemble_command) 21 | info = _find_apk_under_given_directory(bit, flavor) 22 | _copy_apk_to_directory(info) 23 | _copy_mapping_file_to_directory(info, flavor) 24 | _package_apk_channels(info) 25 | return info 26 | 27 | def _find_apk_under_given_directory(bit: BitConfiguration, flavor: FlavorConfiguration) -> ApkInfo: 28 | '''Get destination directory name.''' 29 | apk_output_directory = flavor.get_apk_output_directory() 30 | files = os.listdir(apk_output_directory) 31 | for f in files: 32 | if f.endswith('apk'): 33 | path = os.path.join(apk_output_directory, f) 34 | info = parse_apk_info(path) 35 | break 36 | # Fill bit and flavor configuration. 37 | if info is not None: 38 | info.build_bit = bit 39 | info.build_flavor = flavor 40 | return info 41 | 42 | def _copy_apk_to_directory(info: ApkInfo): 43 | '''Copy APK from one directory to another.''' 44 | if not info.is_valid(): 45 | loge("The APK info is invalid.") 46 | return 47 | output_apk_directory = os.path.join(config.output_apk_directory, '%s_%s' % (info.version_name, info.version_code)) 48 | if not os.path.exists(output_apk_directory): 49 | os.makedirs(output_apk_directory) 50 | apk_file_base_name = os.path.basename(info.source_apk_file_path) 51 | output_apk_file_path = os.path.join(output_apk_directory, apk_file_base_name) 52 | copy_to(info.source_apk_file_path, output_apk_file_path) 53 | # Fill in APk info. 54 | info.output_apk_directory = output_apk_directory 55 | info.output_apk_file_path = output_apk_file_path 56 | 57 | def _copy_mapping_file_to_directory(info: ApkInfo, flavor: FlavorConfiguration): 58 | '''Copy mapping file to given directory.''' 59 | mapping_file_name = "%s_mapping.txt" % flavor.get_name() 60 | mapping_file_copy_to = os.path.join(info.output_apk_directory, mapping_file_name) 61 | copy_to(config.mapping_file_path, mapping_file_copy_to) 62 | 63 | def _package_apk_channels(info: ApkInfo): 64 | '''Package App with different channels.''' 65 | generate_apk_channels(info.source_apk_file_path, config.output_channels, info.output_apk_directory) 66 | -------------------------------------------------------------------------------- /bin/AXMLPrinter.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/autopackage/55a873fd95c49cd71d6bbc0ff1d887511de4bf90/bin/AXMLPrinter.jar -------------------------------------------------------------------------------- /bin/VasDolly.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/autopackage/55a873fd95c49cd71d6bbc0ff1d887511de4bf90/bin/VasDolly.jar -------------------------------------------------------------------------------- /bin/diffuse.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/autopackage/55a873fd95c49cd71d6bbc0ff1d887511de4bf90/bin/diffuse.jar -------------------------------------------------------------------------------- /channel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | from logger import * 6 | 7 | def generate_apk_channels(apk_path: str, channels: [str], output_dir): 8 | '''Generate APK with channels.''' 9 | if len(channels) == 0: 10 | logi('No channels to generate, ignored ...') 11 | return True 12 | channels_str = ','.join(channels) 13 | os.system("java -jar bin/VasDolly.jar put -c \"%s\" %s %s" % (channels_str, apk_path, output_dir)) 14 | -------------------------------------------------------------------------------- /config/config.yml: -------------------------------------------------------------------------------- 1 | # APK Build Configurations. 2 | build: 3 | # Gradlew file location directory. 4 | gradlew_location: ../ 5 | # Gradlew build commands. 6 | assemble_command: 7 | national: resguardNationalRelease 8 | oversea: resguardInternationalRelease 9 | # Gradlew APK output diectories. 10 | apk_output_directory: 11 | national: ../app/build/outputs/apk/national/release 12 | oversea: ../app/build/outputs/apk/international/release 13 | # The mapping file location. 14 | mapping_file_path: ../app/mapping.txt 15 | # Custom JDK path 16 | gradle_java_home: 17 | # APK Strength Configurations. 18 | strengthen: 19 | enable: true 20 | jiagu_360: 21 | # 360 jiaguo executor jar file location. 22 | executor_path: xxxxxxxxxx 23 | account: xxxxxxxxxx 24 | password: xxxxxxxxxx 25 | # Final Output Configurations. 26 | output: 27 | # APK files store directory. 28 | apk_directory: D:/codes/other/LeafNote-resources/apks 29 | # Language files store directory. 30 | languages_directory: D:/codes/other/LeafNote-Community/languages/app 31 | # APK Info Output Email. 32 | mail: 33 | title: xxxxxxxxxx 34 | receivers: 35 | - xxxxxxxxxx 36 | - xxxxxxxxxx 37 | user: xxxxxxxxxx 38 | password: xxxxxxxxxx 39 | # Git log store file path. 40 | git_log_store_file: D:/codes/other/LeafNote-Community/GITLOG.md 41 | # Channels to package. 42 | channels: 43 | - xxxxxxxxx 44 | - xxxxxxxxx 45 | # APK Publish Configurations. 46 | publish: 47 | # LanZou Cloud Info. 48 | lanzou: 49 | username: xxxxxxxxxx 50 | password: xxxxxxxxxx 51 | ylogin: xxxxxxxxxx 52 | phpdisk_info: xxxxxxxxxx 53 | # Telegram Chat Info. 54 | telegram: 55 | chat_id: xxxxxxxxxx 56 | token: xxxxxxxxxx 57 | -------------------------------------------------------------------------------- /config/template_diff.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 8 | 9 | 10 |
11 |
12 | diff_result_content
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/diffuse.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import logging
5 | from apktool import *
6 | from files import *
7 | from global_config import *
8 |
9 | DIFF_RESULT_HTML_TEMPLATE = "config/template_diff.html"
10 | DIFF_RESULT_HTML_PLACEHOLER = "diff_result_content"
11 |
12 | def diff_apk(info: ApkInfo) -> str:
13 | '''Diff APK or output its information.'''
14 | last_version_directory = _find_last_version_directory(info)
15 | logd("Found last version directory: %s" % last_version_directory)
16 | diff_result = ''
17 | if len(last_version_directory) == 0:
18 | diff_result = _output_current_apk_information(info)
19 | else:
20 | diff_result = _output_apk_diff_result(info, last_version_directory)
21 | # Format diff result as a html content.
22 | html_diff_content = read_file(DIFF_RESULT_HTML_TEMPLATE)
23 | html_diff_content = html_diff_content.replace(DIFF_RESULT_HTML_PLACEHOLER, diff_result)
24 | return html_diff_content
25 |
26 | def _find_last_version_directory(info: ApkInfo) -> str:
27 | '''Find last APK version by APK output directory.'''
28 | last_version = 0
29 | last_version_directory = ''
30 | output_apk_directory = config.output_apk_directory
31 | logd("finding last version under %s" % (output_apk_directory))
32 | for directory in os.listdir(output_apk_directory):
33 | logd("finding last version under %s" % (directory))
34 | directory_parts = directory.split('_')
35 | if directory != ".DS_Store" and len(directory_parts) > 1:
36 | version_code = directory_parts[1]
37 | if not info.version_code == version_code and int(version_code) > last_version:
38 | last_version = int(version_code)
39 | last_version_directory = directory
40 | if len(last_version_directory) == 0:
41 | return ''
42 | return os.path.join(output_apk_directory, last_version_directory)
43 |
44 | def _output_current_apk_information(info: ApkInfo) -> str:
45 | '''Output current APK information.'''
46 | diff_result_content = os.popen("java -jar bin/diffuse.jar info %s" % info.output_apk_file_path).read().strip()
47 | diff_result_file_name = os.path.join(info.output_apk_directory, \
48 | "diff_result_%s_%s_info.txt" % (info.build_flavor.get_name(), info.build_bit.get_name()))
49 | write_file(diff_result_file_name, diff_result_content)
50 | return diff_result_content
51 |
52 | def _get_last_version_apk_file_path(info: ApkInfo, last_version_directory: str) -> str:
53 | '''Get last version APK file path.'''
54 | bit_name = info.build_bit.get_name()
55 | flavor_name = info.build_flavor.get_name()
56 | for file_name in os.listdir(last_version_directory):
57 | if file_name.endswith(".apk") and file_name.find(bit_name) >= 0 and file_name.find(flavor_name) >= 0:
58 | return os.path.join(last_version_directory, file_name)
59 | return ''
60 |
61 | def _output_apk_diff_result(info: ApkInfo, last_version_directory: str) -> str:
62 | '''Output APKs diff result.'''
63 | last_version_apk_file_path = _get_last_version_apk_file_path(info, last_version_directory)
64 | if len(last_version_apk_file_path) == 0:
65 | logi("Last version APK file not found!")
66 | return _output_current_apk_information(info)
67 | logi("Found last version APK file: %s" % last_version_apk_file_path)
68 | diff_result_content = os.popen("java -jar bin/diffuse.jar diff %s %s" % \
69 | (last_version_apk_file_path, info.output_apk_file_path)).read().strip()
70 | diff_result_file_name = os.path.join(info.output_apk_directory, \
71 | "diff_result_%s_%s_info.txt" % (info.build_flavor.get_name(), info.build_bit.get_name()))
72 | write_file(diff_result_file_name, diff_result_content)
73 | return diff_result_content
74 |
--------------------------------------------------------------------------------
/docs/README-zh.md:
--------------------------------------------------------------------------------
1 | # Android 自动打包脚本
2 |
3 | [English](README.md)
4 |
5 | ## 1. 关于
6 |
7 | 该项目是适用于 Android 的自动打包脚本,主要功能:
8 |
9 | - 使用 gradle 指令自动打包,区分 32 位和 64 位
10 | - 打包完成之后将 APK 拷贝到指定的目录
11 | - 使用 [diffuse](https://github.com/JakeWharton/diffuse) 输出相对于上一个版本的 APK 版本差异报告
12 | - 拷贝多语言资源到指定的目录,并自动提交到 Github 仓库以便于协助翻译
13 | - 自动打 tag 并提交到远程仓库
14 | - 根据 Git 提交记录自动生成更新日志
15 | - 使用 [360 加固](https://jiagu.360.cn/#/global/index) 对上述 APK 进行加固并输出到指定的目录
16 | - 基于 [VasDolly](https://github.com/Tencent/VasDolly) 进行多渠道打包
17 | - 上传打包 APK 到蓝奏云
18 | - 通过 Telegram bot 将打包完成的渠道包和更新日志信息发送到 Telegram 群组
19 | - 完成上述操作之后使用邮件通知打包结果
20 | - 未来,更多功能...
21 |
22 | ## 2. 使用
23 |
24 | ### 2.1 环境
25 |
26 | - Python: Python3
27 | - 添加 `pyymal` 库: `pip install pyyaml`
28 | - 添加 `requests` 库: `pip install requests`
29 | - 添加 `requests_toolbelt` 库: `pip install requests_toolbelt`
30 |
31 | ### 2.2 使用
32 |
33 | - 编辑配置文件 [the configuration file](config.yml)
34 | - 在根目录使用 `python run.py` 执行脚本
35 |
--------------------------------------------------------------------------------
/files.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import yaml, logging, traceback, shutil
5 | import xml.sax
6 | from typing import *
7 |
8 | ANDROID_STRING_ELEMENT_TAG_NAME_STRING = "string"
9 | ANDROID_STRING_ELEMENT_TAG_NAME_PLURALS = "plurals"
10 | ANDROID_STRING_ELEMENT_TAG_NAME_STRING_ARRAY = "string-array"
11 |
12 | def read_yaml(yaml_file: str):
13 | '''Read YAML.'''
14 | with open(yaml_file, 'r', encoding="utf-8") as f:
15 | return yaml.load(f, Loader=yaml.FullLoader)
16 |
17 | def read_file(fname) -> str:
18 | '''Read text from file.'''
19 | with open(fname, 'r', encoding="utf-8") as f:
20 | return f.read()
21 |
22 | def write_file(fname, content:str):
23 | '''Write content to file.'''
24 | with open(fname, 'w', encoding='utf-8') as f:
25 | f.write(content)
26 |
27 | def copy_to(f: str, t: str):
28 | '''Copy file from one plcae to another.'''
29 | try:
30 | shutil.copyfile(f, t)
31 | except Exception:
32 | logging.error("Error while copy file: %s" % traceback.format_exc())
33 |
34 | def parse_xml(xmlfile: str) -> Dict[str, List[str]]:
35 | '''parse xml'''
36 | parser = xml.sax.make_parser()
37 | handler = AndroidStringHandler()
38 | parser.setContentHandler(handler)
39 | parser.parse(xmlfile)
40 | return handler.resources
41 |
42 | class AndroidStringHandler(xml.sax.ContentHandler):
43 | def __init__(self) -> None:
44 | super().__init__()
45 | self.resources = {}
46 | self.element_name = ''
47 | self.element_values = []
48 |
49 | def startElement(self, name, attrs):
50 | if name == ANDROID_STRING_ELEMENT_TAG_NAME_STRING\
51 | or name == ANDROID_STRING_ELEMENT_TAG_NAME_PLURALS\
52 | or name == ANDROID_STRING_ELEMENT_TAG_NAME_STRING_ARRAY:
53 | self.element_name = attrs["name"]
54 | return super().startElement(name, attrs)
55 |
56 | def endElement(self, name):
57 | if name == ANDROID_STRING_ELEMENT_TAG_NAME_STRING\
58 | or name == ANDROID_STRING_ELEMENT_TAG_NAME_PLURALS\
59 | or name == ANDROID_STRING_ELEMENT_TAG_NAME_STRING_ARRAY:
60 | element_values = []
61 | element_values.extend(self.element_values)
62 | self.resources[self.element_name] = element_values
63 | self.element_name = ''
64 | self.element_values = []
65 | return super().endElement(name)
66 |
67 | def characters(self, content: str):
68 | if len(self.element_name) > 0:
69 | content_stripped = content.strip()
70 | if len(content_stripped) > 0:
71 | self.element_values.append(content_stripped)
72 | return super().characters(content)
73 |
74 | if __name__ == "__main__":
75 | value = parse_xml("/Users/wangshouheng/desktop/repo/github/leafnote/app/src/main/res/values/strings.xml")
76 | print(value)
77 |
--------------------------------------------------------------------------------
/git_tag.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import logging, os
5 | from apktool import *
6 | from files import *
7 | from logger import config_logging
8 | from pathlib import Path
9 |
10 | def add_new_tag(info: ApkInfo):
11 | '''Add current new tag.'''
12 | os.system("cd %s && git tag v%s && git push origin --tags"
13 | % (config.gradlew_location, info.version_name))
14 |
15 | def gen_git_log(info: ApkInfo):
16 | '''Generate git tag logs.'''
17 | # Get last commit log pattern.
18 | last_git_tag = _find_last_git_tag()
19 | if len(last_git_tag) == 0:
20 | logi("Last git tag not found.")
21 | return
22 | _append_gitlog_to_markdown(info, last_git_tag)
23 |
24 | def _append_gitlog_to_markdown(info: ApkInfo, last_git_tag: str):
25 | '''Append gitlog to markdown gitlog file.'''
26 | po = os.popen("cd %s && git log %s..HEAD --oneline" % (config.gradlew_location, last_git_tag))
27 | git_logs = po.buffer.read().decode('utf-8').strip()
28 | markdown_git_logs = ('## %s \n - ' % info.version_name) + '\n- '.join(git_logs.split('\n'))
29 | content = ''
30 | if os.path.exists(config.output_gitlog_store_file):
31 | content = read_file(config.output_gitlog_store_file)
32 | content = markdown_git_logs + '\n' + content
33 | write_file(config.output_gitlog_store_file, content)
34 | _commit_git_log_change_event(info)
35 | return git_logs
36 |
37 | def _commit_git_log_change_event(info: ApkInfo):
38 | '''Commit git log file to community repo.'''
39 | gitlog_store_file_path = Path(config.output_gitlog_store_file)
40 | gitlog_store_file_dir = gitlog_store_file_path.parent
41 | gitlog_store_file_name = os.path.basename(config.output_gitlog_store_file)
42 | os.system("cd %s\
43 | && git pull \
44 | && git add %s\
45 | && git commit -m \"feat: add upgrade logs for %s\"\
46 | && git push"
47 | % (gitlog_store_file_dir, gitlog_store_file_name, info.version_name))
48 |
49 | def _find_last_git_tag() -> str:
50 | '''Get last version git tag'''
51 | last_version_name = os.popen(
52 | "cd %s && git describe --abbrev=0 --tags" % (config.gradlew_location)).read().strip()
53 | return last_version_name
54 |
55 | if __name__ == "__main__":
56 | config_logging()
57 | print(gen_git_log(ApkInfo(version_name="test")))
58 |
--------------------------------------------------------------------------------
/global_config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | from logger import *
5 | from files import *
6 | from enum import Enum
7 | from typing import Dict
8 |
9 | YAML_CONFIGURATION_FILE_PATH = "config/config.yml"
10 |
11 | class BuildConfiguration:
12 | '''Build Environment Configuration.'''
13 | def __init__(self):
14 | # Build target script
15 | self.target_script = YAML_CONFIGURATION_FILE_PATH
16 | # Build APK version
17 | self.version = ''
18 | # Build APK channels.
19 | self.channels = ''
20 |
21 | class BitConfiguration(Enum):
22 | '''Assemble APK package bit configuration, namely, 32 bit, 64 bit and 64 + 32 bit packages.'''
23 | BIT_32 = 0
24 | BIT_64 = 1
25 | BIT_32_64 = 2
26 |
27 | def get_name(self) -> str:
28 | '''Get bit configuration name.'''
29 | if self == BitConfiguration.BIT_32:
30 | return 'ndk_32'
31 | elif self == BitConfiguration.BIT_32_64:
32 | return 'ndk_32_64'
33 | else:
34 | return 'ndk_64'
35 |
36 | def get_gradlew_bit_param_value(self) -> str:
37 | '''Get gradlew build parameter for given ndk/bit configuration.'''
38 | if self == BitConfiguration.BIT_32:
39 | return 'ndk_32'
40 | elif self == BitConfiguration.BIT_32_64:
41 | return 'ndk_32_64'
42 | else:
43 | return 'ndk_64'
44 |
45 | class FlavorConfiguration(Enum):
46 | '''Assemble APK package flavor configuration, namely, national and international packages.'''
47 | NATIONAL = 0
48 | OVERSEA = 1
49 |
50 | def get_name(self) -> str:
51 | '''Get flavor name.'''
52 | if self == FlavorConfiguration.OVERSEA:
53 | return "oversea"
54 | return "national"
55 |
56 | def get_gradlew_command(self) -> str:
57 | '''Get gradlew command configuration base on current flavor.'''
58 | if self == FlavorConfiguration.OVERSEA:
59 | return config._assemble_command_oversea
60 | return config._assemble_command_national
61 |
62 | def get_apk_output_directory(self) -> str:
63 | '''Get APK output directory base on current flavor.'''
64 | if self == FlavorConfiguration.OVERSEA:
65 | return config._apk_output_directory_oversea
66 | return config._apk_output_directory_national
67 |
68 | class GlobalConfig:
69 | def parse(self):
70 | self._configurations = read_yaml(build_config.target_script)
71 | logd(str(self._configurations))
72 | # Gradlew Build Configurations.
73 | self.gradlew_location = self._read_key("build.gradlew_location")
74 | self._assemble_command_national = self._read_key("build.assemble_command.national")
75 | self._assemble_command_oversea = self._read_key("build.assemble_command.oversea")
76 | self._apk_output_directory_national = self._read_key('build.apk_output_directory.national')
77 | self._apk_output_directory_oversea = self._read_key('build.apk_output_directory.oversea')
78 | self.mapping_file_path = self._read_key("build.mapping_file_path")
79 | self.gradle_java_home = self._read_key("build.gradle_java_home")
80 | # APK Strength Configurations.
81 | self.strengthen_enable = self._read_key('strengthen.enable')
82 | self.strengthen_jiagu_360_executor_path = self._read_key('strengthen.jiagu_360.executor_path')
83 | self.strengthen_jiagu_360_account = self._read_key('strengthen.jiagu_360.account')
84 | self.strengthen_jiagu_360_password = self._read_key('strengthen.jiagu_360.password')
85 | # Final Output Configurations.
86 | self.output_apk_directory = self._read_key('output.apk_directory')
87 | self.output_languages_directory = self._read_key('output.languages_directory')
88 | self.output_mail_title = self._read_key('output.mail.title')
89 | self.output_mail_receivers = self._read_key('output.mail.receivers')
90 | self.output_mail_user = self._read_key('output.mail.user')
91 | self.output_mail_password = self._read_key('output.mail.password')
92 | self.output_gitlog_store_file = self._read_key('output.git_log_store_file')
93 | self.output_channels = self._read_key('output.channels')
94 | # APK Publish Configurations.
95 | self.publish_lanzou_username = self._read_key('publish.lanzou.username')
96 | self.publish_lanzou_password = self._read_key('publish.lanzou.password')
97 | self.publish_lanzou_ylogin = self._read_key('publish.lanzou.ylogin')
98 | self.publish_lanzou_phpdisk_info = self._read_key('publish.lanzou.phpdisk_info')
99 | self.publish_telegram_chat_id = self._read_key('publish.telegram.chat_id')
100 | self.publish_telegram_token = self._read_key('publish.telegram.token')
101 |
102 | def _read_key(self, key: str):
103 | '''Read key from configurations.'''
104 | parts = key.split('.')
105 | value = self._configurations
106 | for part in parts:
107 | value = value[part.strip()]
108 | return value
109 |
110 | # Global Build Configuration.
111 | build_config = BuildConfiguration()
112 |
113 | # Global Configuration From Target Script.
114 | config = GlobalConfig()
115 |
116 | if __name__ == "__main__":
117 | config_logging()
118 | print(config._assemble_command_national)
119 | print(config.output_mail_receivers)
120 |
--------------------------------------------------------------------------------
/lanzou.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import logging, os, urllib, random, requests
5 | from apktool import *
6 | from files import *
7 | from global_config import *
8 | from logger import config_logging
9 | from typing import List
10 | from requests_toolbelt.multipart.encoder import MultipartEncoder
11 |
12 | fileup_url = r'http://up.woozooo.com/fileup.php'
13 |
14 | def upload(name: str, path: str):
15 | '''Upload file to lanzou cloud.'''
16 | logging.info("Uploading to lanzou cloud: %s" % f)
17 | name = urllib.parse.quote(name)
18 | fileup_headers = {
19 | "Accept": "* / *",
20 | "Accept - Encoding": "gzip, deflate, br",
21 | "DNT": "1",
22 | "Origin": "https://up.woozooo.com",
23 | "Referer": "https://up.woozooo.com/mydisk.php?item=files&action=index",
24 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"
25 | }
26 | multipart_encoder = MultipartEncoder(
27 | fields={
28 | "task": "1",
29 | "folder_id": "-1",
30 | "id": "WU_FILE_0",
31 | "name": f,
32 | "type": "application/octet-stream",
33 | # "lastModifiedDate": "Thu Jun 27 2019 12:11:16 GMT 0800 (中国标准时间)",
34 | # "size": "185",
35 | 'upload_file': (name, open(path, 'rb'), 'application/octet-stream')
36 | },
37 | boundary='-----------------------------' + str(random.randint(1e28, 1e29 - 1))
38 | )
39 | fileup_headers['Content-Type'] = multipart_encoder.content_type
40 | fileup_json = session.post(url = fileup_url, data=multipart_encoder, headers=fileup_headers).json()
41 | if fileup_json['zt'] == 1:
42 | logging.inf("Succeed to upload: %s" % name)
43 | return fileup_json
44 |
45 | def a_upload(name: str, path: str):
46 | '''
47 | - ylogin: the ylogin cookie from lanzou cloud, get cookie from 'chrome dev tools -> security -> cookie'
48 | - name: file name should have the extension info, and it should be supported your lanzou cloud account
49 | - path: the file path to upload
50 | '''
51 | if len(config.publish_lanzou_ylogin) == 0:
52 | logging.error("lanzou cloud ylogin filed required!")
53 | return
54 | if len(config.publish_lanzou_phpdisk_info) == 0:
55 | logging.error("lanzou cloud phpdisk_info filed required!")
56 | return
57 | url_upload = "https://up.woozooo.com/fileup.php"
58 | headers = {
59 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36 Edg/89.0.774.45',
60 | 'Accept-Language': 'zh-CN,zh;q=0.9',
61 | 'Referer': f'https://up.woozooo.com/mydisk.php?item=files&action=index&u={config.publish_lanzou_ylogin}'
62 | }
63 | post_data = {
64 | "task": "1",
65 | "folder_id": "-1",
66 | "id": "WU_FILE_0",
67 | "name": name,
68 | }
69 | cookie = {
70 | 'ylogin': config.publish_lanzou_ylogin,
71 | 'phpdisk_info': config.publish_lanzou_phpdisk_info
72 | }
73 | files = {'upload_file': (name, open(path, "rb"), 'application/octet-stream')}
74 | multipart_encoder = MultipartEncoder(
75 | fields={
76 | "task": "1",
77 | "folder_id": "-1",
78 | "id": "WU_FILE_0",
79 | "name": name,
80 | "type": "application/octet-stream",
81 | # "lastModifiedDate": "Thu Jun 27 2019 12:11:16 GMT 0800 (中国标准时间)",
82 | # "size": "185",
83 | 'upload_file': (name, open(path, 'rb'), 'application/octet-stream')
84 | },
85 | boundary='-----------------------------' + str(random.randint(1e28, 1e29 - 1))
86 | )
87 | # headers['Content-Type'] = multipart_encoder.content_type
88 | # res = requests.post(url_upload, data=post_data, headers=headers, cookies=cookie, timeout=120).json()
89 | res = requests.post(url_upload, data=post_data, files=files, headers=headers, cookies=cookie, timeout=120).json()
90 | # res = requests.session().post(url = fileup_url, data=multipart_encoder, headers=headers).json()
91 | logging.info(res)
92 | return res['zt'] == 1
93 |
94 | def login(username: str, passed: str):
95 | '''Log in lanzou cloud.'''
96 | session = requests.session()
97 |
98 | login_url = r'https://up.woozooo.com/account.php'
99 |
100 | login_data = {
101 | "action": "login",
102 | "task": "login",
103 | "ref": "https://up.woozooo.com/",
104 | "formhash": "0af1aa15",
105 | "username": username,
106 | "password": passed,
107 | }
108 |
109 | mydisk_data = {
110 | "task": "5",
111 | "folder_id": "-1",
112 | "pg": "1",
113 | }
114 |
115 | headers = {
116 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"
117 | }
118 |
119 | login_json = session.post(url=login_url, data=login_data, headers=headers).text
120 | return login_json
121 |
122 | def a_login(ylogin: str, phpdisk_info: str):
123 | headers = {
124 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36 Edg/89.0.774.45',
125 | 'Accept-Language': 'zh-CN,zh;q=0.9',
126 | 'Referer': 'https://pc.woozooo.com/account.php?action=login'
127 | }
128 | cookie = {
129 | 'ylogin': ylogin,
130 | 'phpdisk_info': phpdisk_info
131 | }
132 | url_account = "https://pc.woozooo.com/account.php"
133 | if cookie['phpdisk_info'] is None:
134 | logging.error('ERROR: phpdisk_info in Cookie is required!')
135 | return False
136 | if cookie['ylogin'] is None:
137 | logging.error('ERROR: ylogin in Cookie is required!')
138 | return False
139 | res = requests.get(url_account, headers=headers, cookies=cookie, verify=True)
140 | if '网盘用户登录' in res.text:
141 | log('ERROR: Failed to login lanzou cloud!')
142 | return False
143 | else:
144 | logging.info("Succeed to login lanzou cloud!")
145 | return True
146 |
147 | if __name__ == "__main__":
148 | config_logging()
149 | # ret = a_login(ylogin, phpdisk_info)
150 | # if ret:
151 | ret = a_upload("test2.apk", "D:\\codes\\other\\LeafNote-resources\\apks\\3.5.1_261\\32BIT-prod-release-3.5.1-261.apk")
152 | print(ret)
153 |
--------------------------------------------------------------------------------
/logger.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import logging
5 |
6 | def config_logging(filename: str = 'app.log'):
7 | '''Config logging library globaly.'''
8 | log_format = "%(asctime)s - %(levelname)s - %(message)s"
9 | date_format = "%m/%d/%Y %H:%M:%S %p"
10 | logging.basicConfig(filename=filename, filemode='a', level=logging.DEBUG, format=log_format, datefmt=date_format)
11 | logging.FileHandler(filename=filename, encoding='utf-8')
12 |
13 | def logd(msg: str):
14 | '''D level log.'''
15 | print(msg)
16 | logging.debug(msg)
17 |
18 | def loge(msg: str):
19 | '''E level log.'''
20 | print(msg)
21 | logging.error(msg)
22 |
23 | def logi(msg: str):
24 | '''I level log.'''
25 | print(msg)
26 | logging.info(msg)
27 |
--------------------------------------------------------------------------------
/mailing.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import smtplib, traceback
5 | from email.header import Header
6 | from email import encoders
7 | from email.header import Header
8 | from email.mime.base import MIMEBase
9 | from email.mime.text import MIMEText
10 | from email.mime.multipart import MIMEMultipart
11 | from email.utils import parseaddr, formataddr
12 | from typing import *
13 | from logger import config_logging
14 | from global_config import *
15 |
16 | def send_email(
17 | receivers: List[str],
18 | subject: str,
19 | message: str,
20 | mail_type: str = 'plain',
21 | filename = None
22 | ):
23 | '''Send email tool.'''
24 | from_ = "%s<%s>" % (subject, config.output_mail_user)
25 | to_ = ';'.join(['Manager<%s>' % email_ for email_ in receivers])
26 | msg = MIMEMultipart()
27 | msg['From'] = _format_addr(from_)
28 | msg['To'] = _format_addr(to_)
29 | msg['Subject'] = Header(subject, 'utf-8').encode()
30 | msg.attach(MIMEText(message, mail_type, 'utf-8'))
31 | if filename is not None:
32 | with open(filename, 'rb') as f:
33 | mime = MIMEBase('text', 'txt', filename='error.log')
34 | mime.add_header('Content-Disposition', 'attachment', filename='error.log')
35 | mime.add_header('Content-ID', '<0>')
36 | mime.add_header('X-Attachment-Id', '0')
37 | mime.set_payload(f.read())
38 | encoders.encode_base64(mime)
39 | msg.attach(mime)
40 | try:
41 | smtpObj = smtplib.SMTP_SSL('smtp.qq.com')
42 | smtpObj.login(config.output_mail_user, config.output_mail_password)
43 | smtpObj.sendmail(config.output_mail_user, receivers, msg.as_string())
44 | logi("Succeed to send email.")
45 | except BaseException as e:
46 | loge("Failed to send email:\n%s" % traceback.format_exc())
47 |
48 | def _format_addr(s):
49 | name, addr = parseaddr(s)
50 | return formataddr((Header(name, 'utf-8').encode(), addr))
51 |
52 | if __name__ == "__main__":
53 | '''Test entry.'''
54 | config_logging()
55 | logd(config.output_mail_receivers)
56 | logd(config.output_mail_user)
57 | logd(config.output_mail_password)
58 | send_email(config.output_mail_receivers, "测试", "测试")
59 |
--------------------------------------------------------------------------------
/publish.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import os
5 | from global_config import *
6 | from logger import *
7 | from telegram_bot import send_apk
8 | from lanzou import a_upload
9 | from files import *
10 | from git_tag import COMMUNITY_LOGS_DIR
11 |
12 | def publish(app: str, ver: str):
13 | '''
14 | Publish APKs.
15 | - app: APP name
16 | - ver: the version of APKs to publish
17 | '''
18 | if ver is None or len(ver) == 0:
19 | loge("Failed: version code is invalid!")
20 | return
21 | dest_dir = ''
22 | for dir in os.listdir(config.output_apk_directory):
23 | if dir.startswith(ver + "_"):
24 | dest_dir = os.path.join(config.output_apk_directory, dir)
25 | break
26 | if len(dest_dir) == 0:
27 | loge("Failed to find APK of version: [%s]" % ver)
28 | return
29 | found_tg = 0
30 | found_lz = 0
31 | channels_dir = dest_dir + '/channels'
32 | for apk in os.listdir(channels_dir):
33 | if apk.endswith(".apk") and apk.find("telegram") != -1:
34 | found_tg = found_tg + 1
35 | path = os.path.join(channels_dir, apk)
36 | _publish_to_tg(app, path, ver)
37 | elif apk.endswith(".apk") and apk.find("lanzou") != -1:
38 | found_lz = found_lz + 1
39 | path = os.path.join(channels_dir, apk)
40 | _publish_to_lanzou(path, ver)
41 | logi("Published APKs for tG: %d" % (found_tg))
42 | logi("Published APKs for Lanzou cloud: %d" % (found_lz))
43 |
44 | def _publish_to_tg(app: str, apk: str, ver:str):
45 | '''Publish APK to TG.'''
46 | logd("Publishing [%s] to TG." % (apk))
47 | msg = 'Hello friends! There\'s a new version [%s] of our %s for %s!\n'
48 | suffix = ''
49 | if apk.find("32BIT") != -1:
50 | suffix = '32bit'
51 | msg = msg % (ver, app, '32 bit')
52 | else:
53 | suffix = '64bit'
54 | msg = msg % (ver, app, '64 bit')
55 | upgrade_log = _read_upgrade_log(ver)
56 | if upgrade_log is None or len(upgrade_log) > 0:
57 | msg = msg + "\nUpgrade log (auto generated):\n" + upgrade_log
58 | msg = msg + "\n\nThis message is send by bot. If you have any questions, please contact the admin for help :P"
59 | send_apk(apk, "%s_v%s_%s.apk" % (app, ver, suffix), msg)
60 |
61 | def _publish_to_lanzou(apk: str, ver:str):
62 | '''Publish APK to Lanzou cloud.'''
63 | logd("Publishing %s to LZ." % (apk))
64 | name = os.path.basename(apk)
65 | a_upload(name, apk)
66 |
67 | def _read_upgrade_log(ver: str) -> str:
68 | '''Read upgrade log generated!'''
69 | path = COMMUNITY_LOGS_DIR + "/" + ver + ".txt"
70 | if not os.path.exists(path):
71 | loge("Upgrade log not found: [%s]!" % path)
72 | return
73 | return read_file(path)
74 |
75 | if __name__ == "__main__":
76 | '''Test entry.'''
77 | config_logging()
78 | # print(_read_upgrade_log("3.5"))
79 | publish("LeafNote", "3.5")
80 |
--------------------------------------------------------------------------------
/resources.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import shutil, os
5 | from logger import *
6 | from global_config import *
7 | from pathlib import Path
8 | from files import parse_xml
9 |
10 | ORIGINAL_COPY_TO_DIRECTORY_NAME = "original"
11 | ORIGINAL_COPY_TO_INFO_FILE_NAME = ".info"
12 |
13 | def copy_language_resources(version_name: str):
14 | '''Copy language resources to community repo and push to github.'''
15 | # Compare and then copy language resources.
16 | _compare_language_resources(version_name)
17 | _copy_origin_language_resources(version_name)
18 |
19 | def _compare_language_resources(version_name: str):
20 | '''Compare current language resources with last version.'''
21 | resoueces_directory = '%sapp/src/main/res/' % config.gradlew_location
22 | copied_language_directory = os.path.join(config.output_languages_directory, ORIGINAL_COPY_TO_DIRECTORY_NAME)
23 | copied_to_info_file_path = os.path.join(copied_language_directory, ORIGINAL_COPY_TO_INFO_FILE_NAME)
24 | last_version = version_name
25 | if os.path.exists(copied_to_info_file_path):
26 | last_version = read_file(copied_to_info_file_path).strip()
27 | diff_directory_name = "%s_to_%s" % (last_version, version_name)
28 | diff_directory_path = os.path.join(config.output_languages_directory, diff_directory_name)
29 | for resource_file_name in os.listdir(resoueces_directory):
30 | # Ignore directories don't start with 'values'.
31 | if not resource_file_name.startswith('values'):
32 | continue
33 | resource_file_path = os.path.join(resoueces_directory, resource_file_name)
34 | if Path(resource_file_path).is_dir():
35 | # Compare in the child directory. Here we only compare two level directories!
36 | resource_directory_path = resource_file_path
37 | for resource_child_file_name in os.listdir(resource_directory_path):
38 | resource_child_file_path = os.path.join(resource_directory_path, resource_child_file_name)
39 | if Path(resource_child_file_path).is_file() and resource_child_file_name.endswith('.xml'):
40 | compare_from = resource_child_file_path
41 | compare_to = os.path.join(copied_language_directory, resource_file_name, resource_child_file_name)
42 | result_file_name = resource_child_file_name.replace(".xml", ".txt")
43 | write_to_directory = os.path.join(diff_directory_path, resource_file_name)
44 | write_to = os.path.join(write_to_directory, result_file_name)
45 | _compare_language_resource_file_and_output(compare_from, compare_to, write_to_directory, write_to)
46 | elif Path(resource_file_path).is_file() and resource_file_name.endswith('.xml'):
47 | compare_from = resource_file_path
48 | compare_to = os.path.join(copied_language_directory, resource_file_name)
49 | result_file_name = resource_file_name.replace(".xml", ".txt")
50 | write_to = os.path.join(diff_directory_path, result_file_name)
51 | _compare_language_resource_file_and_output(compare_from, compare_to, diff_directory_path, write_to)
52 | # Commit diff result.
53 | _commit_langauge_resources_diff_result(diff_directory_path, version_name)
54 |
55 | def _commit_langauge_resources_diff_result(diff_directory_path: str, version_name: str):
56 | '''Commit langauge resorces diff result.'''
57 | if os.path.exists(diff_directory_path):
58 | os.system("cd %s \
59 | && git pull \
60 | && git add . \
61 | && git commit -m \"feat: add version %s language resources diff result\" \
62 | && git push" % (diff_directory_path, version_name))
63 |
64 | def _compare_language_resource_file_and_output(compare_from: str, compare_to: str, write_to_directory: str, write_to: str):
65 | '''Compare language resource file and output the diff result.'''
66 | logd("comparing language resource file from [%s] to [%s]" % (compare_from, compare_to))
67 | from_dict = parse_xml(compare_from)
68 | to_dict = []
69 | if os.path.exists(compare_to):
70 | to_dict = parse_xml(compare_to)
71 | compare_result: Dict[str, List[str]] = {}
72 | # Make a diff between string resources and old resources.
73 | for name, value in from_dict.items():
74 | if name not in to_dict or "\n".join(value) != "\n".join(to_dict[name]):
75 | compare_result[name] = value
76 | # Build compare result file content and write to file.
77 | if len(compare_result) > 0:
78 | compare_content = ''
79 | for name, value in compare_result.items():
80 | compare_content += (":name> " + name + "\n")
81 | compare_content += (":text> " + "\n".join(value) + "\n\n")
82 | if not os.path.exists(write_to_directory):
83 | os.makedirs(write_to_directory)
84 | write_file(write_to, compare_content)
85 |
86 | def _copy_origin_language_resources(version_name: str):
87 | '''Copy origin language resources.'''
88 | app_language_directory = os.path.join(config.output_languages_directory, ORIGINAL_COPY_TO_DIRECTORY_NAME)
89 | any_new_resources = False
90 | resoueces_directory = '%sapp/src/main/res/' % config.gradlew_location
91 | if not os.path.exists(resoueces_directory):
92 | loge("Resources directory doesn't exist or not match 'app/src/main/res/' pattern.")
93 | return
94 | for name in os.listdir(resoueces_directory):
95 | # Ignore directories don't start with 'values'.
96 | if name.startswith('values'):
97 | copy_from_directory = os.path.join(resoueces_directory, name)
98 | copy_to_directory = os.path.join(app_language_directory, name)
99 | if Path(copy_from_directory).is_dir():
100 | # Delete the copy to directory if it exists before make a copy!
101 | if os.path.exists(copy_to_directory):
102 | shutil.rmtree(copy_to_directory)
103 | shutil.copytree(copy_from_directory, copy_to_directory)
104 | any_new_resources = _remove_not_language_files_under_directory(copy_to_directory)
105 | if any_new_resources:
106 | copy_to_info_file_path = os.path.join(app_language_directory, ORIGINAL_COPY_TO_INFO_FILE_NAME)
107 | write_file(copy_to_info_file_path, version_name)
108 | os.system("cd %s \
109 | && git pull \
110 | && git add . \
111 | && git commit -m \"feat: version %s language resources\" \
112 | && git push" % (app_language_directory, version_name))
113 |
114 | def _remove_not_language_files_under_directory(directory: str) -> bool:
115 | '''Remove none langauge files under given directory.'''
116 | copy_to_directory_files = [directory]
117 | any_new_resources = False
118 | while len(copy_to_directory_files) > 0:
119 | copy_to_directory_file = copy_to_directory_files.pop()
120 | if Path(copy_to_directory_file).is_dir():
121 | copy_to_directory_files.extend([os.path.join(copy_to_directory_file, part) for part in os.listdir(copy_to_directory_file)])
122 | else:
123 | if not copy_to_directory_file.endswith(".xml")\
124 | or len(parse_xml(copy_to_directory_file)) == 0:
125 | os.remove(copy_to_directory_file)
126 | else:
127 | any_new_resources = True
128 | return any_new_resources
129 |
130 | if __name__ == "__main__":
131 | config_logging()
132 | config.parse()
133 | copy_language_resources("3.8.3.5")
134 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import os, shutil, logging, getopt, sys
5 | from assemble import assemble
6 | from diffuse import diff_apk
7 | from files import *
8 | from apktool import ApkInfo
9 | from logger import config_logging
10 | from git_tag import *
11 | from global_config import *
12 | from strengthen import jiagu_360
13 | from mailing import send_email
14 | from resources import *
15 |
16 | command_info = "\
17 | Options: \n\
18 | -h[--help] Help info\n\
19 | -c[--script] Target build script path\n\
20 | -v[--version] Build APK version\n\
21 | -c[--channels] Build APK channels, split by ',' for multiple channels, for example 'oversea,national'"
22 |
23 | def _build_apk(bit: BitConfiguration, flavor: FlavorConfiguration) -> ApkInfo:
24 | '''Execute APK build flow.'''
25 | print(">>>> Beginning to build APK for flavor [%s] bit [%s]" % (flavor.get_name(), bit.get_name()))
26 | info = assemble(bit, flavor)
27 | diff = diff_apk(info)
28 | if config.strengthen_enable:
29 | jiagu_file_directory = os.path.join(info.output_apk_directory, 'strengthen')
30 | jiagu_360(info.output_apk_file_path, jiagu_file_directory)
31 | mail_subject = "%s(%s,%s)" % (config.output_mail_title, flavor.get_name(), bit.get_name())
32 | send_email(config.output_mail_receivers, mail_subject, diff, 'html')
33 | return info
34 |
35 | def _show_invalid_command(info: str):
36 | '''Show command invalid info.'''
37 | print('Error: Unrecognized command: %s' % info)
38 | print(command_info)
39 |
40 | def _parse_command(argv):
41 | '''Parse command.'''
42 | try:
43 | opts, args = getopt.getopt(argv, "-h:-s:-v:-c:", ["help", "script=", 'version=', 'channels='])
44 | except BaseException as e:
45 | _show_invalid_command(str(e))
46 | sys.exit(2)
47 | for opt, arg in opts:
48 | print(arg)
49 | if opt in ('-s', '--script'):
50 | build_config.target_script = arg
51 | elif opt in ("-v", "--version"):
52 | build_config.version = arg
53 | elif opt in ("-c", "--channels"):
54 | build_config.channels = arg
55 | elif opt in ('-h', '--help'):
56 | print(command_info)
57 | logi("Build Info: target script[%s], version[%s] and channels[%s]"
58 | % (build_config.target_script, build_config.version, build_config.channels))
59 |
60 | def _run_main():
61 | '''Run main program.'''
62 | if len(build_config.channels) == 0:
63 | # _build_apk(BitConfiguration.BIT_32, FlavorConfiguration.NATIONAL)
64 | info = _build_apk(BitConfiguration.BIT_64, FlavorConfiguration.NATIONAL)
65 | info = _build_apk(BitConfiguration.BIT_64, FlavorConfiguration.OVERSEA)
66 | else:
67 | has_national = build_config.channels.find(FlavorConfiguration.NATIONAL.get_name()) >= 0
68 | has_oversea = build_config.channels.find(FlavorConfiguration.OVERSEA.get_name()) >= 0
69 | if has_national:
70 | _build_apk(BitConfiguration.BIT_32, FlavorConfiguration.NATIONAL)
71 | info = _build_apk(BitConfiguration.BIT_64, FlavorConfiguration.NATIONAL)
72 | if has_oversea:
73 | _build_apk(BitConfiguration.BIT_32, FlavorConfiguration.OVERSEA)
74 | info = _build_apk(BitConfiguration.BIT_64, FlavorConfiguration.OVERSEA)
75 | if not has_national and not has_oversea:
76 | print(">>>>> failed! due to no channels match!")
77 | return
78 | copy_language_resources(info.version_name)
79 | gen_git_log(info)
80 | add_new_tag(info)
81 |
82 | if __name__ == "__main__":
83 | ''' python3 run.py -s config/config_product.yml -v 4.0.3 '''
84 | config_logging()
85 | _parse_command(sys.argv[1:])
86 | config.parse()
87 | _run_main()
88 |
--------------------------------------------------------------------------------
/strengthen.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import os
5 | from global_config import *
6 | from logger import config_logging
7 |
8 | def jiagu_360(apk_file_path: str, output_directory: str):
9 | '''Reinforce the APP.'''
10 | if not os.path.exists(output_directory):
11 | os.mkdir(output_directory)
12 | os.system("java -jar %s -login %s %s\
13 | && java -jar %s -jiagu %s %s -autosign -automulpkg"\
14 | % (config.strengthen_jiagu_360_executor_path\
15 | , config.strengthen_jiagu_360_account\
16 | , config.strengthen_jiagu_360_password\
17 | , config.strengthen_jiagu_360_executor_path\
18 | , apk_file_path\
19 | , output_directory
20 | )
21 | )
22 |
23 | if __name__ == "__main__":
24 | config_logging()
25 | config.parse()
26 | apk_path = "D:\\codes\\other\\LeafNote-resources\\apks\\3.5.1_261\\32BIT-prod-release-3.5.1-261.apk"
27 | out_dir = "D:\\codes\\other\\LeafNote-resources\\apks\\3.5.1_261"
28 | jiagu_360(apk_path, out_dir)
29 | # info = os.stat()
30 | # print(info.st_atime)
31 |
--------------------------------------------------------------------------------
/telegram_bot.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | # import telebot
5 | import requests
6 | from global_config import *
7 | from logger import *
8 |
9 | # def send_message(tb: telebot.TeleBot, chat_id, msg):
10 | # tb.send_message(chat_id, msg)
11 | # print("Message Sent!")
12 |
13 | TG_SEND_DOCUMENT_URL = 'https://api.telegram.org/bot%s/sendDocument'
14 |
15 | def send_apk(path: str, name: str, msg: str):
16 | '''Send APK to TG channel by bot.'''
17 | if config.publish_telegram_chat_id is None or config.publish_telegram_chat_id == 0:
18 | loge("Failed to send APK to TG: chat id required!")
19 | return
20 | if config.publish_telegram_token is None or len(config.publish_telegram_token) == 0:
21 | loge("Failed to send APK to TG: chat token required!")
22 | return
23 | url = TG_SEND_DOCUMENT_URL % (config.publish_telegram_token)
24 | files = {
25 | 'document': (name, open(path, 'rb'))
26 | }
27 | ret = requests.post(url, data={
28 | 'chat_id': config.publish_telegram_chat_id,
29 | 'caption': msg
30 | }, files=files)
31 | logi(ret.text)
32 |
33 | if __name__ == "__main__":
34 | '''Test entry.'''
35 | config_logging()
36 | config.parse()
37 | # tb = telebot.TeleBot("Bot")
38 | # tb.config['api_key'] = token
39 | # send_message(tb, chat_id, "This is funny. I'm using telegram bot to send message to you :D")
40 | send_apk('config.yml', "Hello!")
41 |
--------------------------------------------------------------------------------