This commit is contained in:
luojian 2025-10-28 10:04:16 +08:00
parent b8ddea3a9a
commit eb9da1489d
17 changed files with 596 additions and 322 deletions

View File

@ -9,6 +9,11 @@ from .project_proguard import ProjectProguard
from .project_res_md5 import ProjectResMd5 from .project_res_md5 import ProjectResMd5
from .project_res_string import ProjectResString from .project_res_string import ProjectResString
from .project_update import ProjectUpdate from .project_update import ProjectUpdate
from .project_update_config import ProjectUpdateConfig
from .project_update_game_res import ProjectUpdateGameRes
from .project_update_icon import ProjectUpdateIcon
from .project_update_image import ProjectUpdateImage
from .project_update_keystore import ProjectUpdateKeystore
from .project_upload import ProjectUpload from .project_upload import ProjectUpload
@ -18,6 +23,11 @@ def run(context: Context):
ProjectInit(context), ProjectInit(context),
ProjectCopy(context), ProjectCopy(context),
ProjectResMd5(context), ProjectResMd5(context),
ProjectUpdateKeystore(context),
ProjectUpdateConfig(context),
ProjectUpdateIcon(context),
ProjectUpdateImage(context),
ProjectUpdateGameRes(context),
ProjectUpdate(context), ProjectUpdate(context),
ProjectResString(context), ProjectResString(context),
ProjectInterface(context), ProjectInterface(context),

View File

@ -1,9 +1,12 @@
import os
from dataclasses import dataclass from dataclasses import dataclass
import json import json
@dataclass @dataclass
class Context: class Context:
project_original_path: str
local_repo_commit: str
repo_url: str = "" repo_url: str = ""
repo_branch: str = "" repo_branch: str = ""
repo_commit: str = "" repo_commit: str = ""
@ -18,6 +21,7 @@ class Context:
project_original_path: str = "project/original" project_original_path: str = "project/original"
temp_project_path: str = "" temp_project_path: str = ""
temp_project_config_path: str = ""
# 本地的版本号 # 本地的版本号
local_repo_branch: str = "" local_repo_branch: str = ""
local_repo_commit: str = "" local_repo_commit: str = ""
@ -44,6 +48,24 @@ class Context:
string: dict = None string: dict = None
update_code: bool = True
update_config: bool = True
update_keystore: bool = True
update_res_img: bool = True
update_res_icon: bool = True
update_res_unity: bool = True
config_path: str = ""
keystore_path: str = ""
res_img_path: str = ""
res_icon_path: str = ""
res_unity_path: str = ""
config_config_md5: str = ""
config_keystore_md5: str = ""
config_res_img_md5: str = ""
config_res_icon_md5: str = ""
config_res_unity_md5: str = ""
@classmethod @classmethod
def from_json(cls, json_str: str): def from_json(cls, json_str: str):
data = json.loads(json_str) data = json.loads(json_str)
@ -58,3 +80,37 @@ class Context:
if self.app_name: if self.app_name:
return self.app_name return self.app_name
return self.get_config("app_name") return self.get_config("app_name")
def get_cache_config(self) -> dict[str, str]:
try:
path = os.path.join(self.temp_project_config_path, "config.json")
args = json.load(open(path, "r", encoding="utf-8"))
return args
except:
return {}
def get_cache_config_from_key(self, key: str, default: str = "") -> str:
try:
return self.get_cache_config().get(key, default)
except:
return default
def save_cache_config(self, key: str, value: str):
path = os.path.join(self.temp_project_config_path, "config.json")
config = self.get_cache_config()
config[key] = value
json.dump(config, open(path, "w", encoding="utf-8"), indent=4)
def get_map(self) -> dict[str, str]:
try:
path = os.path.join(self.temp_project_config_path, "map.json")
return json.load(open(path, "r", encoding="utf-8"))
except:
return {}
def save_map(self, map: dict[str, str]):
path = os.path.join(self.temp_project_config_path, "map.json")
json.dump(map, open(path, "w", encoding="utf-8"), indent=4)
def get_map_from_key(self, file_name) -> str:
return self.get_map().get(file_name, file_name)

View File

@ -39,7 +39,7 @@ class ProjectBuild(Task):
def execute(self): def execute(self):
self.init() self.init()
# self.save_project() # self.save_project()
self.build_apk() # self.build_apk()
self.build_aab() self.build_aab()
self.copy_to_out() self.copy_to_out()
pass pass
@ -54,12 +54,12 @@ class ProjectBuild(Task):
def copy_to_out(self): def copy_to_out(self):
app_logger().debug(f"copy_to_out start.") app_logger().debug(f"copy_to_out start.")
target = f"{self.context.temp_project_path}{os.sep}build{os.sep}outputs{os.sep}apk{os.sep}lawnWithQuickstepPlay{os.sep}debug" # target = f"{self.context.temp_project_path}{os.sep}build{os.sep}outputs{os.sep}apk{os.sep}lawnWithQuickstepPlay{os.sep}debug"
FileUtils.copy(find_path(target, "apk"), self.context.out_debug_apk) # FileUtils.copy(find_path(target, "apk"), self.context.out_debug_apk)
app_logger().debug(f"copy_to_out debug apk = {self.context.out_debug_apk}") # app_logger().debug(f"copy_to_out debug apk = {self.context.out_debug_apk}")
target = f"{self.context.temp_project_path}{os.sep}build{os.sep}outputs{os.sep}apk{os.sep}lawnWithQuickstepPlay{os.sep}release" # target = f"{self.context.temp_project_path}{os.sep}build{os.sep}outputs{os.sep}apk{os.sep}lawnWithQuickstepPlay{os.sep}release"
FileUtils.copy(find_path(target, "apk"), self.context.out_release_apk) # FileUtils.copy(find_path(target, "apk"), self.context.out_release_apk)
app_logger().debug(f"copy_to_out release apk = {self.context.out_release_apk}") # app_logger().debug(f"copy_to_out release apk = {self.context.out_release_apk}")
target = f"{self.context.temp_project_path}{os.sep}build{os.sep}outputs{os.sep}bundle{os.sep}lawnWithQuickstepPlayRelease" target = f"{self.context.temp_project_path}{os.sep}build{os.sep}outputs{os.sep}bundle{os.sep}lawnWithQuickstepPlayRelease"
FileUtils.copy(find_path(target, "aab"), self.context.out_release_aab) FileUtils.copy(find_path(target, "aab"), self.context.out_release_aab)
app_logger().debug(f"copy_to_out end.") app_logger().debug(f"copy_to_out end.")

View File

@ -1,21 +1,63 @@
import os.path
import shutil
from git import Repo
from scripts.task import Task from scripts.task import Task
from utils import FileUtils from utils import FileUtils
from utils import TimeUtils
from utils.logger_utils import app_logger from utils.logger_utils import app_logger
def clean_workspace(repo):
# 1. 放弃工作区所有已跟踪文件的修改(包括恢复被删除的已跟踪文件)
# - checkout 命令会将工作区文件重置为当前 HEAD 版本的状态
repo.git.checkout('.')
# 2. 删除工作区所有未跟踪的文件和目录(包括空目录)
# - -f强制删除避免询问
# - -d同时删除未跟踪的目录
repo.git.clean('-fd')
print("工作空间已清空,恢复到当前分支最新提交状态")
class ProjectCopy(Task): class ProjectCopy(Task):
def execute(self): def execute(self):
self.init() if os.path.exists(self.context.temp_project_path):
temp_path = os.path.join(self.context.temp_project_path, "build", "outputs")
if os.path.exists(temp_path):
shutil.rmtree(temp_path)
temp_path = os.path.join(self.context.temp_project_path, "build", "generated")
if os.path.exists(temp_path):
shutil.rmtree(temp_path)
app_logger().debug("project '{}' to '{}'".format(self.context.temp_project_path, "项目已经存在了"))
repo = Repo(self.context.temp_project_path)
temp_repo_commit = repo.head.commit.hexsha[:10]
app_logger().debug(
f"project '{self.context.temp_project_path}' , local '{temp_repo_commit}' remote '{self.context.local_repo_commit}")
if temp_repo_commit != self.context.local_repo_commit:
app_logger().debug("本地代码有变动,需要更新临时的目录")
# 本地分支和远程的分支不一样
clean_workspace(repo)
origin = repo.remote("origin")
origin.fetch()
repo.git.checkout(self.context.local_repo_commit)
self.context.update_code = True
self.context.update_config = True
self.context.update_keystore = True
self.context.update_res_img = True
self.context.update_res_icon = True
self.context.update_res_unity = True
else:
self.context.update_code = False
app_logger().debug("本地的代码没有变动,无需更新")
return
self.context.update_code = True
result = FileUtils.copy(self.context.project_original_path, self.context.temp_project_path) result = FileUtils.copy(self.context.project_original_path, self.context.temp_project_path)
app_logger().debug("Copied project '{}' to '{}'".format(self.context.project_original_path, result)) app_logger().debug("Copied project '{}' to '{}'".format(self.context.project_original_path, result))
pass pass
def init(self):
self.context.temp_project_path = self.context.project_original_path.replace(
"original", self.context.package_name.replace(".", "_") + TimeUtils.get_formatted_time(format_str="%H%M%S")
)
# self.context.temp_project_path = self.context.project_original_path
pass

View File

@ -4,5 +4,5 @@ from utils import FileUtils
class ProjectEnd(Task): class ProjectEnd(Task):
def execute(self): def execute(self):
FileUtils.delete(self.context.temp_project_path, True) # FileUtils.delete(self.context.temp_project_path, True)
pass pass

View File

@ -1,6 +1,11 @@
import os
import time import time
from git import Repo, RemoteProgress from git import Repo, RemoteProgress
from utils import FileUtils
from utils.logger_utils import app_logger
from .context import Context
from .task import Task from .task import Task
@ -10,7 +15,72 @@ def progress(op_code, cur_count, max_count=None, message=''):
print(f"操作: {op_code}, 进度: {cur_count}/{max_count}, 消息: {message}") print(f"操作: {op_code}, 进度: {cur_count}/{max_count}, 消息: {message}")
def check_config_exists(*file_paths):
# game_config/com.diy.emoticon.free.zcaqf.game.launcher/com.diy.emoticon.free.zcaqf.game.launcher_android.zip
# game_config/com.emoticon.diy.znfav.launcher.free/com.emoticon.diy.znfav.launcher.free_android.zip
# 遍历检查每个文件
if file_paths is None:
file_paths = []
for file_path in file_paths:
if not os.path.exists(file_path):
# 抛出异常并说明缺失的文件路径
raise FileNotFoundError(f"配置文件不存在: {file_path}")
class ProjectInit(Task): class ProjectInit(Task):
def __init__(self, context: Context):
super().__init__(context)
self.context.temp_project_path = self.context.project_original_path.replace("original",
"V1_" + self.context.package_name.replace(
".", "_"))
self.context.temp_project_config_path = self.context.temp_project_path + "_config"
if not os.path.exists(self.context.temp_project_config_path):
os.makedirs(self.context.temp_project_config_path)
config_md5 = context.get_cache_config_from_key("config", "")
keystore_md5 = context.get_cache_config_from_key("keystore", "")
res_img_md5 = context.get_cache_config_from_key("res_img", "")
res_icon_md5 = context.get_cache_config_from_key("res_icon", "")
res_unity_md5 = context.get_cache_config_from_key("res_unity", "")
app_logger().debug(f"temp project res md5 , "
f"config : {config_md5} , "
f"keystore : {keystore_md5} , "
f"res_img : {res_img_md5} , "
f"res_icon : {res_icon_md5} , "
f"res_unity : {res_unity_md5}")
config_root_path = os.path.join("game_config", self.context.package_name)
self.context.config_path = os.path.join(config_root_path, f"{self.context.package_name}_android.zip")
self.context.keystore_path = ""
for i in os.listdir(config_root_path):
if i.endswith(".keystore"):
self.context.keystore_path = os.path.join(config_root_path, i)
self.context.res_img_path = os.path.join(config_root_path, "drawable-xxhdpi.zip")
self.context.res_icon_path = os.path.join(config_root_path, "icon.zip")
self.context.res_unity_path = os.path.join(config_root_path, "unityLibrary.zip")
check_config_exists(self.context.config_path,
self.context.keystore_path,
self.context.res_img_path,
self.context.res_icon_path,
self.context.res_unity_path)
self.context.config_config_md5 = FileUtils.get_md5(self.context.config_path)
self.context.config_keystore_md5 = FileUtils.get_md5(self.context.keystore_path)
self.context.config_res_img_md5 = FileUtils.get_md5(self.context.res_img_path)
self.context.config_res_icon_md5 = FileUtils.get_md5(self.context.res_icon_path)
self.context.config_res_unity_md5 = FileUtils.get_md5(self.context.res_unity_path)
self.context.update_config = config_md5 != self.context.config_config_md5
self.context.update_keystore = keystore_md5 != self.context.config_keystore_md5
self.context.update_res_img = res_img_md5 != self.context.config_res_img_md5
self.context.update_res_icon = res_icon_md5 != self.context.config_res_icon_md5
self.context.update_res_unity = res_unity_md5 != self.context.config_res_unity_md5
def execute(self): def execute(self):
try: try:
repo = Repo(self.context.project_original_path) repo = Repo(self.context.project_original_path)
@ -45,12 +115,13 @@ class ProjectInit(Task):
local_branch.set_tracking_branch(repo.remotes[remote_name].refs[branch_name]) # 设置跟踪 local_branch.set_tracking_branch(repo.remotes[remote_name].refs[branch_name]) # 设置跟踪
local_branch.checkout() # 切换到该分支 local_branch.checkout() # 切换到该分支
# 拉取最新代码
repo.remotes.origin.pull()
self.context.local_repo_branch = repo.active_branch.name self.context.local_repo_branch = repo.active_branch.name
self.context.local_repo_commit = repo.head.commit.hexsha[:10] self.context.local_repo_commit = repo.head.commit.hexsha[:10]
# 拉取最新代码 print("当前分支:" + repo.active_branch.name, self.context.local_repo_commit)
repo.remotes.origin.pull()
print("当前分支:" + repo.active_branch.name)
pass pass
else: else:
raise Exception(f"No commit to {self.context.repo_commit}") raise Exception(f"No commit to {self.context.repo_commit}")

View File

@ -4,6 +4,7 @@ import os
import hashlib import hashlib
from utils import FileUtils from utils import FileUtils
from utils.logger_utils import app_logger
def string_to_md5(text): def string_to_md5(text):
@ -65,6 +66,9 @@ class ProjectInterface(Task):
f_out.write(line) f_out.write(line)
def execute(self): def execute(self):
if not self.context.update_code:
app_logger().info("代码没有更新,不需要处理 unity 接口")
return
self.unity_proxy_api_file = os.path.join(self.context.temp_project_path, self.unity_proxy_api_file = os.path.join(self.context.temp_project_path,
"launcher-game/src/com/game/hachisdk/unity/UnityProxyApi.java".replace( "launcher-game/src/com/game/hachisdk/unity/UnityProxyApi.java".replace(

View File

@ -276,6 +276,10 @@ class ProjectProguard(Task):
pass pass
def execute(self): def execute(self):
if not self.context.update_code:
app_logger().info("No update project proguard found")
self.context.proguard_dict = self.context.get_map()
return
self.root = self.context.temp_project_path self.root = self.context.temp_project_path
self.module_path = os.path.join(self.root, "launcher-game") self.module_path = os.path.join(self.root, "launcher-game")
self.code_path = os.path.join(self.module_path, "src") self.code_path = os.path.join(self.module_path, "src")
@ -324,6 +328,7 @@ class ProjectProguard(Task):
encrypt_xml_resources(self.string_path, False, string_to_md5(self.context.package_name).upper()) encrypt_xml_resources(self.string_path, False, string_to_md5(self.context.package_name).upper())
self.context.save_map(self.context.proguard_dict)
app_logger().info(json.dumps(self.context.proguard_dict, indent=4)) app_logger().info(json.dumps(self.context.proguard_dict, indent=4))
# if __name__ == '__main__': # if __name__ == '__main__':

View File

@ -4,6 +4,8 @@ import hashlib
from PIL import Image from PIL import Image
import random import random
from utils.logger_utils import app_logger
def get_md5(file_path): def get_md5(file_path):
"""计算文件的MD5值""" """计算文件的MD5值"""
@ -64,6 +66,9 @@ def modify_one_pixel(input_path, output_path):
class ProjectResMd5(Task): class ProjectResMd5(Task):
def execute(self): def execute(self):
if not self.context.update_code:
app_logger().info("不需要更新res md5")
return
path = f"launcher-game/res/" path = f"launcher-game/res/"
root_path = os.path.join(self.context.temp_project_path, path) root_path = os.path.join(self.context.temp_project_path, path)
for i in os.listdir(root_path): for i in os.listdir(root_path):

View File

@ -37,6 +37,9 @@ class ProjectResString(Task):
app_logger().debug("路径不存,不操作了,后续可以给他创建出来:" + string_path + "\t" + json.dumps(res, indent=4)) app_logger().debug("路径不存,不操作了,后续可以给他创建出来:" + string_path + "\t" + json.dumps(res, indent=4))
def execute(self): def execute(self):
if not self.context.update_code:
app_logger().info("代码没有更新,不需要处理资源")
return
for key in self.context.string.keys(): for key in self.context.string.keys():
launcher = key.replace('base', '') launcher = key.replace('base', '')
if launcher: if launcher:

View File

@ -1,86 +1,11 @@
import os.path import os.path
from pathlib import Path
import re 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.context import Context
from scripts.task import Task from scripts.task import Task
from utils import FileUtils
from utils.logger_utils import app_logger 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): def update_gradle_variable(content, variable_name, new_value):
""" """
更新 Gradle 文件中的 final def 变量值 更新 Gradle 文件中的 final def 变量值
@ -166,12 +91,8 @@ def update_gradle_property(content, key, new_value):
return updated_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) 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) 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): class ProjectUpdate(Task):
@ -181,6 +102,9 @@ class ProjectUpdate(Task):
self.build_gradle_path = None self.build_gradle_path = None
def update_package_name(self): def update_package_name(self):
if not self.context.update_config:
app_logger().info("配置文件没有变动package不需要更新")
return
""" """
更新包名 更新包名
:return: :return:
@ -203,209 +127,6 @@ class ProjectUpdate(Task):
pass pass
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): def update_gradle_config(self):
""" """
更新gradle里面的版本号 更新gradle里面的版本号
@ -427,6 +148,9 @@ plugins {
pass pass
def update_string(self): def update_string(self):
if not self.context.update_config:
app_logger().info("配置文件没有变动string不需要更新")
return
privacy = self.context.get_config("TkA_Url_Privacy") privacy = self.context.get_config("TkA_Url_Privacy")
if not privacy or privacy == "": if not privacy or privacy == "":
raise Exception("配置文件中没有配置 TkA_Url_Privacy") raise Exception("配置文件中没有配置 TkA_Url_Privacy")
@ -440,29 +164,11 @@ plugins {
text = text.replace("https://doanvanquy.com/TermsOfUse.html", text = text.replace("https://doanvanquy.com/TermsOfUse.html",
privacy.replace("privacy.html", "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) 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 pass
def execute(self): 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.build_gradle_path = os.path.join(self.context.temp_project_path, "build.gradle")
self.update_package_name() 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_gradle_config()
self.update_string() self.update_string()
pass pass

View File

@ -0,0 +1,71 @@
import os
import shutil
import javaproperties
from scripts.task import Task
from utils import FileUtils
from utils.logger_utils import app_logger
class ProjectUpdateConfig(Task):
def update_config(self):
if not self.context.update_config:
app_logger().info("配置文件没有更新")
return
"""
更新配置文件
:return:
"""
target_path = self.context.config_path
dst = os.path.join(self.context.temp_project_path, os.path.basename(target_path).replace(".zip", ""))
if not self.context.update_config:
app_logger().info("No update config found")
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")
return
if os.path.exists(dst):
shutil.rmtree(dst)
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), True)
pass
def execute(self):
self.update_config()
self.context.save_cache_config("config", self.context.config_config_md5)
pass

View File

@ -0,0 +1,170 @@
import os
import re
import xml.etree.ElementTree as ET
from xml.dom import minidom
from scripts.task import Task
from utils import FileUtils
from utils.logger_utils import app_logger
GAME_ACTIVITY_PATH = f"launcher-game/src/com/game/launcher/activity/GLGameActivity.kt".replace("/", os.sep)
ANDROID_MANIFEST_PATH = f"launcher-game/AndroidManifest.xml".replace("/", os.sep)
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
class ProjectUpdateGameRes(Task):
def update_game_result(self):
if not self.context.update_res_unity:
app_logger().info("No update game res found")
return
"""
更新游戏资源
:return:
"""
if self.context.game_type == "unity_native":
res_path = self.context.res_unity_path
dst = os.path.join(self.context.temp_project_path, "unityLibrary")
temp_dst = dst + "_res"
if os.path.exists(dst):
result = FileUtils.delete(dst, True)
app_logger().info(f"删除unityLibrary结果 : {result}")
if os.path.exists(temp_dst):
result = FileUtils.delete(temp_dst, True)
app_logger().info(f"删除temp unityLibrary结果 : {result}")
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 execute(self):
self.update_game_result()
self.context.save_cache_config("res_unity", self.context.config_res_unity_md5)
pass

View File

@ -0,0 +1,39 @@
import os
import shutil
from scripts.task import Task
from utils import FileUtils
from utils.logger_utils import app_logger
class ProjectUpdateIcon(Task):
def update_icon(self):
"""
更新游戏Icon
:return:
"""
if not self.context.update_res_icon:
app_logger().info("No update res icon found")
return
tag = "res_icon_resources"
dst = os.path.join(self.context.temp_project_path, tag)
if os.path.exists(dst):
shutil.rmtree(dst)
FileUtils.decompress(self.context.res_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 execute(self):
self.update_icon()
self.context.save_cache_config("res_icon", self.context.config_res_icon_md5)

View File

@ -0,0 +1,52 @@
import os
import shutil
from pathlib import Path
from scripts.task import Task
from utils import FileUtils
from utils.logger_utils import app_logger
class ProjectUpdateImage(Task):
def update_image(self):
"""
更新游戏的资源
:return:
"""
if not self.context.update_res_img:
app_logger().info("No update image found")
return
dst = os.path.join(self.context.temp_project_path, "drawable_res")
if os.path.exists(dst):
shutil.rmtree(dst)
FileUtils.decompress(self.context.res_img_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
res_name = Path(file).stem
file_name = self.context.get_map_from_key(res_name)
temp_tar = os.path.join(dst, file)
temp_dst = os.path.join(target_root_path, file).replace(file_name, self.context.get_map_from_key(res_name))
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
app_logger().info(f"copy {temp_tar} => {temp_dst}")
FileUtils.copy(temp_tar, temp_dst, True)
pass
def execute(self):
self.update_image()
self.context.save_cache_config("res_img", self.context.config_res_img_md5)

View File

@ -0,0 +1,41 @@
import os.path
from scripts.task import Task
from utils import FileUtils
from utils.logger_utils import app_logger
class ProjectUpdateKeystore(Task):
def update_keystore(self):
if not self.context.update_keystore:
app_logger().info("No update_keystore")
return
name = os.path.basename(self.context.keystore_path).replace(".keystore", "")
target_path = os.path.join(self.context.temp_project_path, name + ".keystore")
if os.path.exists(target_path):
os.remove(target_path)
result = FileUtils.copy(self.context.keystore_path, target_path)
app_logger().debug(f"copy keystore result {result}")
target_path = os.path.join(self.context.temp_project_path, "keystore.properties")
if os.path.exists(target_path):
os.remove(target_path)
open(target_path, "w", encoding="utf-8").write(
f"""
keyAlias={name}
keyPassword=123456
storeFile=./{name}.keystore
storePassword=123456
"""
)
def execute(self):
self.update_keystore()
self.context.save_cache_config("keystore", self.context.config_keystore_md5)
pass

View File

@ -5,7 +5,6 @@ class Task:
def __init__(self, context: Context): def __init__(self, context: Context):
self.context = context self.context = context
pass
def execute(self): def execute(self):
pass pass