auto_build_launcher/scripts/project_update.py

469 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os.path
from pathlib import Path
import re
from xml.dom import minidom
import javaproperties
import requests
from lxml import etree
import xml.etree.ElementTree as ET
from scripts.context import Context
from scripts.task import Task
from utils import FileUtils
from utils.logger_utils import app_logger
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}")
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
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
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
LAUNCHER_CODE_PATH = f"LauncherCode/src/com/launchercode".replace("/", os.sep)
GAME_ACTIVITY_PATH = f"launcher-game/src/com/game/launcher/activity/GLGameActivity.kt".replace("/", os.sep)
GAME_ACTIVITY_PATH_2 = f"LauncherCode/src/com/launchercode/activity/GameActivity.kt".replace("/", os.sep)
ANDROID_MANIFEST_PATH = f"launcher-game/AndroidManifest.xml".replace("/", os.sep)
STRING_PATH = f"launcher-game/res/values/strings.xml".replace("/", os.sep)
LAUNCER_STRING_PATH = f"LauncherCode/src/com/launchercode/LauncherStringsValue.kt".replace("/", os.sep)
class ProjectUpdate(Task):
def __init__(self, context: Context):
super().__init__(context)
self.build_gradle_path = None
def update_package_name(self):
"""
更新包名
:return:
"""
build_gradle_path = os.path.join(self.context.temp_project_path, "ad.gradle")
text = open(build_gradle_path, "r", encoding="utf-8").read()
text = text.replace(self.context.original_package, self.context.package_name)
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()
text = text.replace(self.context.original_package, self.context.package_name)
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")
if not os.path.exists(mainly_path):
mainly_path = os.path.join(dst, "appConfig")
google_services_json_path = os.path.join(dst, "google-services.json")
if not os.path.exists(google_services_json_path):
google_services_json_path = os.path.join(dst, "appConfig", "google-services.json")
FileUtils.copy(google_services_json_path,
os.path.join(self.context.temp_project_path, "google-services.json"),
True)
dst_path = os.path.join(self.context.temp_project_path, f"launcher-game{os.sep}assets")
for file in list(filter(lambda f: not (f == "google_fonts.json" or f == "pag_gl_slide.pag"),
os.listdir(dst_path))):
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))
with open(os.path.join(dst, "tkg_config_mainly.properties"), 'rb') as f:
self.context.config = javaproperties.load(f)
# 不打admob
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")
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
temp_dst = temp_tart_path.replace(tag, "launcher-game" + os.sep + "res")
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)
if os.path.exists(temp_dst):
FileUtils.delete(temp_dst, True)
FileUtils.decompress(res_path, temp_dst)
build_path = os.path.join(temp_dst, "build")
if os.path.exists(build_path):
FileUtils.delete(build_path, True)
if os.listdir(temp_dst).index("unityLibrary") >= 0:
FileUtils.copy(os.path.join(temp_dst, "unityLibrary"), dst)
else:
FileUtils.copy(temp_dst, dst)
android_manifest_xml_path = os.path.join(dst, "src", "main", "AndroidManifest.xml")
process_manifest(android_manifest_xml_path, android_manifest_xml_path)
text = open(os.path.join(dst, "build.gradle"), "r", encoding="utf-8").read()
text = text.replace("implementation", "api")
if not 'namespace "com.unity3d.player"' in text:
text = text.replace("compileSdkVersion", """
namespace "com.unity3d.player"
compileSdkVersion""")
text = text.replace("unityStreamingAssets.tokenize(', ')",
'[".unity3d", ".bundle", ".version", ".bytes", ".hash"]')
text = text.replace("apply plugin: 'com.android.library'", """
plugins {
id 'com.android.library'
}
""")
open(os.path.join(dst, "build.gradle"), "w", encoding="utf-8").write(text)
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)
# 引用Unity项目
text = open(os.path.join(self.context.temp_project_path, "ad.gradle"), "r", encoding="utf-8").read()
text = uncomment_line(text, "implementation projects.unityLibrary")
open(os.path.join(self.context.temp_project_path, "ad.gradle"), "w", encoding="utf-8").write(text)
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)
# launcher 引用 unityActivity
text = open(os.path.join(self.context.temp_project_path, GAME_ACTIVITY_PATH), "r", encoding="utf-8").read()
text = text.replace("GLGameWebActivity", "com.unity3d.player.UnityPlayerActivity")
open(os.path.join(self.context.temp_project_path, GAME_ACTIVITY_PATH), "w", encoding="utf-8").write(text)
text = open(os.path.join(self.context.temp_project_path, ANDROID_MANIFEST_PATH), "r",
encoding="utf-8").read()
text = text.replace("@style/LauncherGameIntroTheme", "@style/UnityThemeSelector")
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,
f"launcher-game{os.sep}res{os.sep}drawable-xxhdpi")
image_list = list(map(lambda f: Path(f).stem, os.listdir(target_root_path)))
for file in os.listdir(dst):
if file == ".DS_Store":
continue
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:
"""
build_gradle_path = os.path.join(self.context.temp_project_path, "ad.gradle")
text = open(build_gradle_path, "r", encoding="UTF-8").read()
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)
open(build_gradle_path, "w", encoding="UTF-8").write(text)
pass
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()
text = text.replace("https://doanvanquy.com/privacy.html", privacy)
text = text.replace("https://doanvanquy.com/TermsOfUse.html",
privacy.replace("privacy.html", "TermsOfUse.html"))
open(os.path.join(self.context.temp_project_path, STRING_PATH), "w", encoding="utf-8").write(text)
# 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)
pass
def execute(self):
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
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()
self.update_string()
pass