auto_build_launcher/scripts/project_update.py

469 lines
19 KiB
Python
Raw Normal View History

2025-07-07 13:52:21 +00:00
import os.path
from pathlib import Path
import re
2025-09-07 14:43:06 +00:00
from xml.dom import minidom
2025-07-07 13:52:21 +00:00
import javaproperties
2025-07-07 14:25:45 +00:00
import requests
from lxml import etree
2025-09-07 14:43:06 +00:00
import xml.etree.ElementTree as ET
2025-07-07 13:52:21 +00:00
from scripts.context import Context
2025-07-07 03:27:26 +00:00
from scripts.task import Task
2025-07-07 13:52:21 +00:00
from utils import FileUtils
from utils.logger_utils import app_logger
2025-09-07 14:43:06 +00:00
def process_manifest(input_path, output_path):
# 解析XML文件
tree = ET.parse(input_path)
root = tree.getroot()
# 定义命名空间映射
android_namespace = 'http://schemas.android.com/apk/res/android'
ET.register_namespace('android', android_namespace)
namespaces = {'android': android_namespace}
# 处理application标签移除所有属性
application = root.find('application')
if application is not None:
# 保存所有子节点
children = list(application)
# 清除application标签
root.remove(application)
# 创建新的application标签无属性
new_application = ET.Element('application')
# 添加回所有子节点
for child in children:
new_application.append(child)
# 将新的application标签添加回root
root.append(new_application)
# 查找并删除指定的activity节点
activity_to_remove = new_application.find(
".//activity[@android:name='com.unity3d.player.UnityPlayerActivity']",
namespaces=namespaces
)
if activity_to_remove is not None:
new_application.remove(activity_to_remove)
# 保存处理后的XML保留android命名空间前缀
rough_string = ET.tostring(root, 'utf-8')
reparsed = minidom.parseString(rough_string)
pretty_xml = reparsed.toprettyxml(indent=" ", encoding='utf-8')
# 去除空行
lines = pretty_xml.decode('utf-8').split('\n')
non_empty_lines = [line for line in lines if line.strip() != '']
pretty_xml = '\n'.join(non_empty_lines).encode('utf-8')
with open(output_path, 'wb') as f:
f.write(pretty_xml)
print(f"处理完成,结果已保存到 {output_path}")
2025-07-07 14:25:45 +00:00
2025-07-07 13:52:21 +00:00
def uncomment_line(content, line_pattern):
"""
取消指定行的注释
:param content: 文件内容
:param line_pattern: 要取消注释的行内容不含前导//和空格
:return: 更新后的内容
"""
# 匹配以//开头,后跟任意空格,然后是目标行内容
pattern = rf'^(\s*)//\s*({re.escape(line_pattern)}\s*)$'
# 替换为去注释版本(保留原有缩进)
replacement = rf'\1\2'
updated_content = re.sub(pattern, replacement, content, flags=re.MULTILINE)
return updated_content
def update_gradle_variable(content, variable_name, new_value):
"""
更新 Gradle 文件中的 final def 变量值
:param content: Gradle 文件内容
:param variable_name: 变量名 ( "releaseName")
:param new_value: 新值 (字符串或数字)
:return: 更新后的内容
"""
# 处理字符串值(带引号)
if isinstance(new_value, str):
# 匹配带引号的字符串赋值
pattern = rf'(final\s+def\s+{re.escape(variable_name)}\s*=\s*["\'])(.*?)(["\'])'
replacement = rf'\g<1>{new_value}\g<3>'
else:
# 匹配数字值(不带引号)
pattern = rf'(final\s+def\s+{re.escape(variable_name)}\s*=\s*)(\d+)'
replacement = rf'\g<1>{new_value}'
updated_content = re.sub(pattern, replacement, content)
return updated_content
2025-09-07 14:43:06 +00:00
def modify_text_with_regex(original_text, key, new_value):
"""
使用正则表达式修改文本中指定key的值支持带引号和数字类型
参数:
original_text -- 原始文本内容
key -- 要修改的键名
new_value -- 新的值
返回:
修改后的文本内容
"""
# 改进的正则模式:更精确地匹配带引号的值和数字值
# 匹配带引号的值(单引号或双引号)或数字值
pattern = re.compile(
r'(final def ' + re.escape(key) + r' = )' # 前缀部分
r'(?:' # 非捕获组,用于分组不同情况
r'([\'"])(.*?)\2' # 带引号的值(单引号或双引号)
r'|' # 或者
r'(\d+)' # 数字值(整数)
r')'
)
# 查找匹配
match = pattern.search(original_text)
if not match:
print(f"未找到键: {key}")
return original_text
# 检查是带引号的情况还是数字情况
quote_type = match.group(2) # 引号类型(单引号或双引号)
is_number = match.group(4) is not None # 是否是数字类型
# 构造替换字符串
if quote_type:
# 有引号的情况,保持原有引号类型
replacement = f'\g<1>{quote_type}{new_value}{quote_type}'
elif is_number:
# 数字类型,直接替换数字
replacement = f'\g<1>{new_value}'
else:
# 其他情况,保持原样替换
replacement = f'\g<1>{new_value}'
# 执行替换,只替换第一个匹配项
modified_text = pattern.sub(replacement, original_text, count=1)
return modified_text
2025-07-07 13:52:21 +00:00
def update_gradle_property(content, key, new_value):
# 匹配两种格式:
# 1. resValue "string", "key", "value"
# 2. resValue("string", "key", "value")
pattern = rf'(resValue\s*\(?\s*["\']string["\']\s*,\s*["\']{re.escape(key)}["\']\s*,\s*["\'])(.*?)(["\']\s*\)?)'
# 替换为新值
updated_content = re.sub(pattern, rf'\g<1>{new_value}\g<3>', content)
return updated_content
2025-08-01 02:14:00 +00:00
LAUNCHER_CODE_PATH = f"LauncherCode/src/com/launchercode".replace("/", os.sep)
2025-09-07 14:43:06 +00:00
GAME_ACTIVITY_PATH = f"launcher-game/src/com/game/launcher/activity/GLGameActivity.kt".replace("/", os.sep)
2025-08-01 02:14:00 +00:00
GAME_ACTIVITY_PATH_2 = f"LauncherCode/src/com/launchercode/activity/GameActivity.kt".replace("/", os.sep)
2025-09-07 14:43:06 +00:00
ANDROID_MANIFEST_PATH = f"launcher-game/AndroidManifest.xml".replace("/", os.sep)
STRING_PATH = f"launcher-game/res/values/strings.xml".replace("/", os.sep)
2025-07-11 06:37:26 +00:00
LAUNCER_STRING_PATH = f"LauncherCode/src/com/launchercode/LauncherStringsValue.kt".replace("/", os.sep)
2025-07-07 03:27:26 +00:00
class ProjectUpdate(Task):
2025-07-07 13:52:21 +00:00
def __init__(self, context: Context):
super().__init__(context)
self.build_gradle_path = None
def update_package_name(self):
"""
更新包名
:return:
"""
2025-09-07 14:43:06 +00:00
build_gradle_path = os.path.join(self.context.temp_project_path, "ad.gradle")
2025-07-07 13:52:21 +00:00
text = open(build_gradle_path, "r", encoding="utf-8").read()
2025-09-07 14:43:06 +00:00
text = text.replace(self.context.original_package, self.context.package_name)
2025-07-07 13:52:21 +00:00
open(build_gradle_path, "w", encoding="utf-8").write(text)
xml_path = os.path.join(self.context.temp_project_path, "lawnchair/res/xml")
for root, dirs, files in os.walk(xml_path):
for file in files:
temp_xml_path = os.path.join(root, file)
text = open(temp_xml_path, "r", encoding="utf-8").read()
2025-09-07 14:43:06 +00:00
text = text.replace(self.context.original_package, self.context.package_name)
2025-07-07 13:52:21 +00:00
open(temp_xml_path, "w", encoding="utf-8").write(text)
pass
pass
def update_keystore(self):
root_dir = os.path.join("game_config", self.context.package_name)
for file in os.listdir(root_dir):
if file.endswith(".keystore"):
name = file.replace(".keystore", "")
FileUtils.copy(os.path.join(root_dir, file), os.path.join(self.context.temp_project_path, file))
open(os.path.join(self.context.temp_project_path, "keystore.properties"), "w", encoding="utf-8").write(
f"""
keyAlias={name}
keyPassword=123456
storeFile=./{name}.keystore
storePassword=123456
"""
)
return
raise Exception("keystore not found")
def update_config(self):
"""
更新配置文件
:return:
"""
root_dir = os.path.join("game_config", self.context.package_name)
config_path = list(
filter(lambda f: f.endswith(".zip") and f.startswith(self.context.package_name), os.listdir(root_dir)))
if len(config_path) <= 0:
raise Exception("config not found")
target_path = os.path.join(root_dir, config_path[0])
dst = os.path.join(self.context.temp_project_path, config_path[0].replace(".zip", ""))
result = FileUtils.decompress(target_path, dst)
app_logger().debug(f"{target_path} -> {dst} , 解压结果: {result}")
mainly_path = os.path.join(dst, "mainly")
2025-10-10 12:53:00 +00:00
if not os.path.exists(mainly_path):
mainly_path = os.path.join(dst, "appConfig")
2025-07-07 13:52:21 +00:00
google_services_json_path = os.path.join(dst, "google-services.json")
2025-10-10 12:53:00 +00:00
if not os.path.exists(google_services_json_path):
google_services_json_path = os.path.join(dst, "appConfig", "google-services.json")
2025-07-07 13:52:21 +00:00
FileUtils.copy(google_services_json_path,
os.path.join(self.context.temp_project_path, "google-services.json"),
True)
2025-09-07 14:43:06 +00:00
dst_path = os.path.join(self.context.temp_project_path, f"launcher-game{os.sep}assets")
2025-07-07 13:52:21 +00:00
2025-09-07 14:43:06 +00:00
for file in list(filter(lambda f: not (f == "google_fonts.json" or f == "pag_gl_slide.pag"),
2025-08-01 02:14:00 +00:00
os.listdir(dst_path))):
2025-07-07 13:52:21 +00:00
FileUtils.delete(os.path.join(dst_path, file), True)
pass
for file in list(filter(lambda f: f.find(".") <= 0, os.listdir(mainly_path))):
FileUtils.copy(os.path.join(mainly_path, file), os.path.join(dst_path, file))
2025-10-10 12:53:00 +00:00
with open(os.path.join(dst, "tkg_config_mainly.properties"), 'rb') as f:
2025-07-07 13:52:21 +00:00
self.context.config = javaproperties.load(f)
2025-07-15 14:03:08 +00:00
# 不打admob
2025-07-24 11:10:16 +00:00
if self.context.admob_app_id is None or self.context.admob_app_id == "":
self.context.admob_app_id = self.context.get_config("admob_id")
2025-07-07 13:52:21 +00:00
pass
def update_icon(self):
"""
更新游戏Icon
:return:
"""
target_icon_path = os.path.join("game_config", self.context.package_name, "icon.zip")
tag = "res_icon_resources"
dst = os.path.join(self.context.temp_project_path, tag)
FileUtils.decompress(target_icon_path, dst)
for root, dirs, files in os.walk(dst):
for file in files:
temp_tart_path = os.path.join(root, file)
if temp_tart_path.find("__MACOSX") > 0:
continue
2025-09-07 14:43:06 +00:00
temp_dst = temp_tart_path.replace(tag, "launcher-game" + os.sep + "res")
2025-07-07 13:52:21 +00:00
app_logger().debug(f"copy icon = {temp_tart_path} -> {temp_dst}")
FileUtils.copy(temp_tart_path, temp_dst, True)
pass
def update_game_result(self):
"""
更新游戏资源
:return:
"""
root_dir = os.path.join("game_config", self.context.package_name)
if self.context.game_type == "unity_native":
res_path = os.path.join(root_dir, "unityLibrary.zip")
if not os.path.exists(res_path):
raise Exception("unity library not found")
dst = os.path.join(self.context.temp_project_path, "unityLibrary")
temp_dst = dst + "_res"
if os.path.exists(dst):
FileUtils.delete(dst, True)
2025-09-07 14:43:06 +00:00
if os.path.exists(temp_dst):
FileUtils.delete(temp_dst, True)
2025-07-07 13:52:21 +00:00
FileUtils.decompress(res_path, temp_dst)
2025-07-09 06:52:30 +00:00
build_path = os.path.join(temp_dst, "build")
if os.path.exists(build_path):
FileUtils.delete(build_path, True)
2025-07-07 13:52:21 +00:00
if os.listdir(temp_dst).index("unityLibrary") >= 0:
FileUtils.copy(os.path.join(temp_dst, "unityLibrary"), dst)
else:
FileUtils.copy(temp_dst, dst)
2025-09-07 14:43:06 +00:00
android_manifest_xml_path = os.path.join(dst, "src", "main", "AndroidManifest.xml")
process_manifest(android_manifest_xml_path, android_manifest_xml_path)
2025-07-07 13:52:21 +00:00
text = open(os.path.join(dst, "build.gradle"), "r", encoding="utf-8").read()
text = text.replace("implementation", "api")
2025-09-07 14:43:06 +00:00
if not 'namespace "com.unity3d.player"' in text:
text = text.replace("compileSdkVersion", """
namespace "com.unity3d.player"
compileSdkVersion""")
2025-07-09 06:52:30 +00:00
text = text.replace("unityStreamingAssets.tokenize(', ')",
'[".unity3d", ".bundle", ".version", ".bytes", ".hash"]')
2025-07-09 06:18:40 +00:00
text = text.replace("apply plugin: 'com.android.library'", """
plugins {
id 'com.android.library'
}
""")
2025-07-07 13:52:21 +00:00
open(os.path.join(dst, "build.gradle"), "w", encoding="utf-8").write(text)
2025-09-07 14:43:06 +00:00
lines = open(os.path.join(dst, "build.gradle"), "r", encoding="utf-8").readlines()
new_lines = []
for line in lines:
if line.find("com.game:hachisdk") > 0:
continue
new_lines.append(line)
open(os.path.join(dst, "build.gradle"), "w", encoding="utf-8").writelines(new_lines)
2025-07-07 13:52:21 +00:00
# 引用Unity项目
2025-09-07 14:43:06 +00:00
text = open(os.path.join(self.context.temp_project_path, "ad.gradle"), "r", encoding="utf-8").read()
2025-07-07 13:52:21 +00:00
text = uncomment_line(text, "implementation projects.unityLibrary")
2025-09-07 14:43:06 +00:00
open(os.path.join(self.context.temp_project_path, "ad.gradle"), "w", encoding="utf-8").write(text)
2025-07-07 13:52:21 +00:00
2025-09-07 14:43:06 +00:00
text = open(os.path.join(self.context.temp_project_path, "settings.gradle"), "r", encoding="utf-8").read()
text = uncomment_line(text, "include ':unityLibrary'")
open(os.path.join(self.context.temp_project_path, "settings.gradle"), "w", encoding="utf-8").write(text)
2025-07-07 13:52:21 +00:00
2025-09-07 14:43:06 +00:00
# launcher 引用 unityActivity
2025-07-07 13:52:21 +00:00
text = open(os.path.join(self.context.temp_project_path, GAME_ACTIVITY_PATH), "r", encoding="utf-8").read()
2025-09-07 14:43:06 +00:00
text = text.replace("GLGameWebActivity", "com.unity3d.player.UnityPlayerActivity")
2025-07-07 13:52:21 +00:00
open(os.path.join(self.context.temp_project_path, GAME_ACTIVITY_PATH), "w", encoding="utf-8").write(text)
2025-07-07 14:25:45 +00:00
text = open(os.path.join(self.context.temp_project_path, ANDROID_MANIFEST_PATH), "r",
encoding="utf-8").read()
2025-07-10 16:10:25 +00:00
text = text.replace("@style/LauncherGameIntroTheme", "@style/UnityThemeSelector")
2025-07-07 13:52:21 +00:00
open(os.path.join(self.context.temp_project_path, ANDROID_MANIFEST_PATH), "w", encoding="utf-8").write(text)
else:
raise Exception(f"不支持的游戏类型 : {self.context.game_type}")
pass
def update_image(self):
"""
更新游戏的资源
:return:
"""
root_dir = os.path.join("game_config", self.context.package_name)
drawable_path = os.path.join(root_dir, "drawable-xxhdpi.zip")
if not os.path.exists(drawable_path):
raise Exception("drawable not found")
dst = os.path.join(self.context.temp_project_path, "drawable_res")
FileUtils.decompress(drawable_path, dst)
if os.path.join(dst, "drawable-xxhdpi"):
dst = os.path.join(dst, "drawable-xxhdpi")
target_root_path = os.path.join(self.context.temp_project_path,
2025-09-07 14:43:06 +00:00
f"launcher-game{os.sep}res{os.sep}drawable-xxhdpi")
2025-07-07 13:52:21 +00:00
image_list = list(map(lambda f: Path(f).stem, os.listdir(target_root_path)))
for file in os.listdir(dst):
2025-08-01 02:14:00 +00:00
if file == ".DS_Store":
continue
2025-07-07 13:52:21 +00:00
temp_tar = os.path.join(dst, file)
temp_dst = os.path.join(target_root_path, file)
file_name = Path(file).stem
if file_name in image_list:
FileUtils.delete(os.path.join(target_root_path, file_name + ".png"))
FileUtils.delete(os.path.join(target_root_path, file_name + ".jpg"))
pass
FileUtils.copy(temp_tar, temp_dst, True)
pass
def update_gradle_config(self):
"""
更新gradle里面的版本号
:return:
"""
2025-09-07 14:43:06 +00:00
build_gradle_path = os.path.join(self.context.temp_project_path, "ad.gradle")
2025-07-07 13:52:21 +00:00
text = open(build_gradle_path, "r", encoding="UTF-8").read()
2025-09-07 14:43:06 +00:00
text = modify_text_with_regex(text, "admob_app_id", self.context.admob_app_id)
text = modify_text_with_regex(text, "game_services_project_id", self.context.game_services_project_id)
text = modify_text_with_regex(text, "facebook_app_id", self.context.facebook_app_id)
text = modify_text_with_regex(text, "facebook_client_token", self.context.facebook_client_token)
text = modify_text_with_regex(text, "appName", self.context.get_app_name())
text = modify_text_with_regex(text, "appVersionName", self.context.version_display_name)
text = modify_text_with_regex(text, "appVersionCode", self.context.version_code)
2025-07-07 13:52:21 +00:00
open(build_gradle_path, "w", encoding="UTF-8").write(text)
pass
2025-07-08 05:51:30 +00:00
def update_string(self):
privacy = self.context.get_config("TkA_Url_Privacy")
if not privacy or privacy == "":
raise Exception("配置文件中没有配置 TkA_Url_Privacy")
tkg_custom = self.context.get_config("tkg_custom")
if not tkg_custom or tkg_custom == "":
raise Exception("配置文件中没有配置 tkg_custom")
text = open(os.path.join(self.context.temp_project_path, STRING_PATH), "r", encoding="utf-8").read()
2025-09-07 14:43:06 +00:00
text = text.replace("https://doanvanquy.com/privacy.html", privacy)
text = text.replace("https://doanvanquy.com/TermsOfUse.html",
2025-07-08 05:51:30 +00:00
privacy.replace("privacy.html", "TermsOfUse.html"))
open(os.path.join(self.context.temp_project_path, STRING_PATH), "w", encoding="utf-8").write(text)
2025-07-11 06:37:26 +00:00
2025-09-07 14:43:06 +00:00
# text = open(os.path.join(self.context.temp_project_path, LAUNCER_STRING_PATH), "r", encoding="utf-8").read()
# text = text.replace("https://harmonitun.com/privacy.html", privacy)
# text = text.replace("https://harmonitun.com/TermsOfUse.html",
# privacy.replace("privacy.html", "TermsOfUse.html"))
# text = text.replace("harmounitun@outlook.com", tkg_custom)
# open(os.path.join(self.context.temp_project_path, LAUNCER_STRING_PATH), "w", encoding="utf-8").write(text)
2025-08-01 02:14:00 +00:00
pass
2025-07-07 03:27:26 +00:00
def execute(self):
2025-08-01 02:14:00 +00:00
global GAME_ACTIVITY_PATH
path = os.path.join(self.context.temp_project_path, GAME_ACTIVITY_PATH)
if not os.path.exists(path):
GAME_ACTIVITY_PATH = GAME_ACTIVITY_PATH_2
# GAME_ACTIVITY_PATH = GAME_ACTIVITY_PATH_2
pass
2025-07-07 13:52:21 +00:00
self.build_gradle_path = os.path.join(self.context.temp_project_path, "build.gradle")
self.update_package_name()
self.update_keystore()
self.update_config()
self.update_icon()
self.update_image()
self.update_game_result()
self.update_gradle_config()
2025-07-08 05:51:30 +00:00
self.update_string()
2025-07-07 03:27:26 +00:00
pass