1312 lines
52 KiB
Python
1312 lines
52 KiB
Python
import os
|
||
import sys
|
||
import json
|
||
import datetime
|
||
import subprocess
|
||
import shutil
|
||
import importlib.util
|
||
import py_compile
|
||
|
||
class ProjectManager:
|
||
|
||
def __init__(self, world):
|
||
self.world = world
|
||
self.current_project_path = None
|
||
self.project_config = None
|
||
|
||
print("✓ 项目管理系统初始化完成")
|
||
|
||
def _get_repo_root(self):
|
||
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
def get_project_scripts_dir(self, project_path=None):
|
||
project_path = os.path.normpath(project_path or self.current_project_path or "")
|
||
if not project_path:
|
||
return ""
|
||
return os.path.join(project_path, "scripts")
|
||
|
||
def _ensure_project_scripts_dir(self, project_path):
|
||
scripts_dir = self.get_project_scripts_dir(project_path)
|
||
if scripts_dir:
|
||
os.makedirs(scripts_dir, exist_ok=True)
|
||
return scripts_dir
|
||
|
||
def _sync_project_script_manager(self, project_path, reload_scripts=True):
|
||
script_manager = getattr(self.world, "script_manager", None)
|
||
if not script_manager:
|
||
return ""
|
||
|
||
scripts_dir = self._ensure_project_scripts_dir(project_path)
|
||
script_manager.set_scripts_directory(
|
||
scripts_dir,
|
||
create=True,
|
||
reload_scripts=reload_scripts,
|
||
)
|
||
return scripts_dir
|
||
|
||
def _restore_project_context(self, project_path, project_config, reload_scripts=True):
|
||
normalized_project_path = os.path.normpath(project_path) if project_path else None
|
||
self.current_project_path = normalized_project_path
|
||
self.project_config = project_config
|
||
|
||
script_manager = getattr(self.world, "script_manager", None)
|
||
if not script_manager:
|
||
return
|
||
|
||
if normalized_project_path:
|
||
self._sync_project_script_manager(normalized_project_path, reload_scripts=reload_scripts)
|
||
else:
|
||
script_manager.set_scripts_directory(
|
||
"scripts",
|
||
create=True,
|
||
reload_scripts=reload_scripts,
|
||
)
|
||
|
||
def _sanitize_build_name(self, project_name):
|
||
invalid_chars = '<>:"/\\|?*'
|
||
safe_name = "".join("_" if char in invalid_chars else char for char in str(project_name or "").strip())
|
||
safe_name = safe_name.rstrip(". ")
|
||
return safe_name or "EGProject"
|
||
|
||
# ==================== 项目生命周期管理 ====================
|
||
|
||
def createNewProject(self, project_path, project_name):
|
||
"""创建新项目
|
||
|
||
Args:
|
||
project_path: 项目路径
|
||
project_name: 项目名称
|
||
|
||
Returns:
|
||
bool: 创建是否成功
|
||
"""
|
||
if not project_path or not project_name:
|
||
print("错误: 项目路径和名称不能为空")
|
||
return False
|
||
|
||
# full_project_path = os.path.join(project_path, project_name)
|
||
full_project_path = os.path.normpath(os.path.join(project_path, project_name))
|
||
print(f"full_project_path: {full_project_path}")
|
||
|
||
try:
|
||
# 创建项目文件夹结构
|
||
os.makedirs(full_project_path)
|
||
os.makedirs(os.path.join(full_project_path, "models")) # 模型文件夹
|
||
os.makedirs(os.path.join(full_project_path, "textures")) # 贴图文件夹
|
||
scenes_path = os.path.join(full_project_path, "scenes") # 场景文件夹
|
||
os.makedirs(scenes_path)
|
||
self._ensure_project_scripts_dir(full_project_path)
|
||
|
||
# 创建项目配置文件
|
||
project_config = {
|
||
"name": project_name,
|
||
"path": full_project_path,
|
||
"created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
"last_modified": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
"version": "1.0.0",
|
||
"engine_version": "1.0.0"
|
||
}
|
||
|
||
config_file = os.path.join(full_project_path, "project.json")
|
||
with open(config_file, "w", encoding="utf-8") as f:
|
||
json.dump(project_config, f, ensure_ascii=False, indent=4)
|
||
|
||
print(f"项目配置文件已创建: {config_file}")
|
||
|
||
# 清空当前场景
|
||
self._clearCurrentScene()
|
||
|
||
# 更新项目状态,确保后续保存脚本元数据时能拿到项目路径
|
||
self.current_project_path = full_project_path
|
||
self.project_config = project_config
|
||
self._sync_project_script_manager(full_project_path, reload_scripts=True)
|
||
|
||
# 自动保存初始场景
|
||
scene_file = os.path.join(scenes_path, "scene.bam")
|
||
if self.world.scene_manager.saveScene(scene_file, full_project_path):
|
||
# 更新配置文件中的场景路径
|
||
project_config["scene_file"] = os.path.relpath(scene_file, full_project_path)
|
||
with open(config_file, "w", encoding="utf-8") as f:
|
||
json.dump(project_config, f, ensure_ascii=False, indent=4)
|
||
print(f"初始场景已保存到: {scene_file}")
|
||
else:
|
||
print("初始场景保存失败")
|
||
|
||
print(f"项目 '{project_name}' 创建成功!")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"创建项目失败: {str(e)}")
|
||
return False
|
||
|
||
def openProject(self, project_path):
|
||
"""打开项目
|
||
|
||
Args:
|
||
project_path: 项目路径
|
||
|
||
Returns:
|
||
bool: 打开是否成功
|
||
"""
|
||
# print(f"\n[DEBUG] ===== 开始打开项目: {project_path} =====")
|
||
try:
|
||
if not project_path:
|
||
print("错误: 项目路径不能为空")
|
||
return False
|
||
|
||
# 检查是否是有效的项目文件夹
|
||
config_file = os.path.join(project_path, "project.json")
|
||
# print(f"[DEBUG] 项目配置文件路径: {config_file}")
|
||
# print(f"[DEBUG] 配置文件是否存在: {os.path.exists(config_file)}")
|
||
|
||
if not os.path.exists(config_file):
|
||
print("错误: 选择的不是有效的项目文件夹!")
|
||
return False
|
||
|
||
# 读取项目配置
|
||
# print(f"[DEBUG] 开始读取项目配置文件...")
|
||
with open(config_file, "r", encoding="utf-8") as f:
|
||
project_config = json.load(f)
|
||
# print(f"[DEBUG] 项目配置读取成功: {project_config.get('name', 'Unknown')}")
|
||
|
||
previous_project_path = self.current_project_path
|
||
previous_project_config = self.project_config
|
||
|
||
self.current_project_path = os.path.normpath(project_path)
|
||
self.project_config = project_config
|
||
self._sync_project_script_manager(self.current_project_path, reload_scripts=True)
|
||
|
||
# 检查场景文件
|
||
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
||
# print(f"[DEBUG] 场景文件路径: {scene_file}")
|
||
# print(f"[DEBUG] 场景文件是否存在: {os.path.exists(scene_file)}")
|
||
|
||
if os.path.exists(scene_file):
|
||
# 加载场景前的安全检查
|
||
# print(f"[DEBUG] 开始加载场景文件...")
|
||
# print(f"[DEBUG] 当前world状态检查:")
|
||
# print(f"[DEBUG] - render节点存在: {hasattr(self.world, 'render') and self.world.render is not None}")
|
||
# print(f"[DEBUG] - loader存在: {hasattr(self.world, 'loader') and self.world.loader is not None}")
|
||
# print(f"[DEBUG] - scene_manager存在: {hasattr(self.world, 'scene_manager') and self.world.scene_manager is not None}")
|
||
|
||
# 检查world的基本状态
|
||
if not hasattr(self.world, 'render') or self.world.render is None:
|
||
# print(f"[DEBUG] 错误: world.render不存在或为None")
|
||
self._restore_project_context(
|
||
previous_project_path,
|
||
previous_project_config,
|
||
reload_scripts=True,
|
||
)
|
||
return False
|
||
|
||
if not hasattr(self.world, 'loader') or self.world.loader is None:
|
||
# print(f"[DEBUG] 错误: world.loader不存在或为None")
|
||
self._restore_project_context(
|
||
previous_project_path,
|
||
previous_project_config,
|
||
reload_scripts=True,
|
||
)
|
||
return False
|
||
|
||
# 清理可能的残留状态
|
||
try:
|
||
# 清理选择状态
|
||
if hasattr(self.world, 'selection') and self.world.selection:
|
||
self.world.selection.clearSelection()
|
||
# print(f"[DEBUG] 选择状态已清理")
|
||
|
||
# 清理GUI状态
|
||
if hasattr(self.world, 'gui_elements'):
|
||
for gui_elem in self.world.gui_elements:
|
||
if gui_elem and not gui_elem.isEmpty():
|
||
gui_elem.detachNode()
|
||
self.world.gui_elements.clear()
|
||
# print(f"[DEBUG] GUI元素已清理")
|
||
|
||
# 验证BAM文件
|
||
# print(f"[DEBUG] 验证BAM文件完整性...")
|
||
if os.path.getsize(scene_file) == 0:
|
||
# print(f"[DEBUG] 错误: BAM文件为空")
|
||
self._restore_project_context(
|
||
previous_project_path,
|
||
previous_project_config,
|
||
reload_scripts=True,
|
||
)
|
||
return False
|
||
|
||
# 检查文件权限
|
||
if not os.access(scene_file, os.R_OK):
|
||
# print(f"[DEBUG] 错误: BAM文件不可读")
|
||
self._restore_project_context(
|
||
previous_project_path,
|
||
previous_project_config,
|
||
reload_scripts=True,
|
||
)
|
||
return False
|
||
|
||
# print(f"[DEBUG] BAM文件验证通过")
|
||
|
||
except Exception as e:
|
||
# print(f"[DEBUG] 清理状态时发生异常: {e}")
|
||
self._restore_project_context(
|
||
previous_project_path,
|
||
previous_project_config,
|
||
reload_scripts=True,
|
||
)
|
||
return False
|
||
|
||
if self.world.scene_manager.loadScene(scene_file):
|
||
# print(f"[DEBUG] 场景加载成功")
|
||
# 更新项目配置
|
||
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
|
||
else:
|
||
# print(f"[DEBUG] 场景加载失败")
|
||
self._restore_project_context(
|
||
previous_project_path,
|
||
previous_project_config,
|
||
reload_scripts=True,
|
||
)
|
||
return False
|
||
else:
|
||
# print(f"[DEBUG] 场景文件不存在,跳过场景加载")
|
||
pass
|
||
|
||
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
with open(config_file, "w", encoding="utf-8") as f:
|
||
json.dump(project_config, f, ensure_ascii=False, indent=4)
|
||
|
||
# 更新项目状态
|
||
self.current_project_path = os.path.normpath(project_path)
|
||
self.project_config = project_config
|
||
|
||
# print(f"[DEBUG] ===== 项目打开成功: {project_path} =====")
|
||
print("项目加载成功!")
|
||
return True
|
||
|
||
except Exception as e:
|
||
self._restore_project_context(
|
||
locals().get("previous_project_path"),
|
||
locals().get("previous_project_config"),
|
||
reload_scripts=True,
|
||
)
|
||
# print(f"[DEBUG] ===== 项目打开失败 =====")
|
||
print(f"加载项目时发生错误:{str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def openProjectForPath(self, project_path):
|
||
"""通过路径打开项目
|
||
|
||
Args:
|
||
project_path: 项目路径
|
||
|
||
Returns:
|
||
bool: 打开是否成功
|
||
"""
|
||
try:
|
||
if not project_path:
|
||
return False
|
||
|
||
previous_project_path = self.current_project_path
|
||
previous_project_config = self.project_config
|
||
|
||
# 检查是否是有效的项目文件夹
|
||
config_file = os.path.join(project_path, "project.json")
|
||
if not os.path.exists(config_file):
|
||
print("警告: 选择的不是有效的项目文件夹!")
|
||
return False
|
||
|
||
# 读取项目配置
|
||
with open(config_file, "r", encoding="utf-8") as f:
|
||
project_config = json.load(f)
|
||
|
||
self.current_project_path = os.path.normpath(project_path)
|
||
self.project_config = project_config
|
||
self._sync_project_script_manager(self.current_project_path, reload_scripts=True)
|
||
|
||
# 检查场景文件
|
||
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
||
if os.path.exists(scene_file):
|
||
# 加载场景
|
||
if self.world.scene_manager.loadScene(scene_file):
|
||
# 更新项目配置
|
||
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
|
||
else:
|
||
self._restore_project_context(
|
||
previous_project_path,
|
||
previous_project_config,
|
||
reload_scripts=True,
|
||
)
|
||
return False
|
||
|
||
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
with open(config_file, "w", encoding="utf-8") as f:
|
||
json.dump(project_config, f, ensure_ascii=False, indent=4)
|
||
|
||
# 更新项目状态
|
||
self.current_project_path = os.path.normpath(project_path)
|
||
self.project_config = project_config
|
||
|
||
print(f"项目 '{project_path}' 加载成功!")
|
||
return True
|
||
|
||
except Exception as e:
|
||
self._restore_project_context(
|
||
locals().get("previous_project_path"),
|
||
locals().get("previous_project_config"),
|
||
reload_scripts=True,
|
||
)
|
||
error_msg = f"加载项目时发生错误:{str(e)}"
|
||
print(error_msg)
|
||
return False
|
||
|
||
def saveProject(self):
|
||
"""保存项目
|
||
|
||
Returns:
|
||
bool: 保存是否成功
|
||
"""
|
||
try:
|
||
# 检查是否有当前项目路径
|
||
if not self.current_project_path:
|
||
print("错误: 请先创建或打开一个项目!")
|
||
return False
|
||
|
||
project_path = self.current_project_path
|
||
scenes_path = os.path.join(project_path, "scenes")
|
||
|
||
# 固定的场景文件名
|
||
scene_file = os.path.join(scenes_path, "scene.bam")
|
||
|
||
# 先写临时文件,成功后再替换,避免保存失败时丢失旧场景。
|
||
if self._save_scene_atomically(scene_file, project_path):
|
||
# 更新项目配置文件
|
||
config_file = os.path.join(project_path, "project.json")
|
||
project_config = self.project_config or {}
|
||
if os.path.exists(config_file):
|
||
with open(config_file, "r", encoding="utf-8") as f:
|
||
project_config = json.load(f)
|
||
|
||
# 更新最后修改时间
|
||
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
# 记录场景文件路径
|
||
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
|
||
|
||
with open(config_file, "w", encoding="utf-8") as f:
|
||
json.dump(project_config, f, ensure_ascii=False, indent=4)
|
||
|
||
# 更新项目配置
|
||
self.project_config = project_config
|
||
|
||
print("项目保存成功!")
|
||
return True
|
||
else:
|
||
print("错误: 保存场景失败!")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"保存项目时发生错误:{str(e)}")
|
||
return False
|
||
|
||
def _save_scene_atomically(self, scene_file, project_path):
|
||
"""先保存到临时文件,成功后再原子替换正式场景文件。"""
|
||
scene_file = os.path.normpath(scene_file)
|
||
scene_dir = os.path.dirname(scene_file)
|
||
scene_name, scene_ext = os.path.splitext(os.path.basename(scene_file))
|
||
temp_scene_file = os.path.join(scene_dir, f"{scene_name}.tmp{scene_ext}")
|
||
final_gui_info_file = scene_file.replace('.bam', '_gui.json')
|
||
temp_gui_info_file = temp_scene_file.replace('.bam', '_gui.json')
|
||
final_lui_info_file = scene_file.replace('.bam', '_lui.json')
|
||
temp_lui_info_file = temp_scene_file.replace('.bam', '_lui.json')
|
||
|
||
for temp_path in (temp_scene_file, temp_gui_info_file, temp_lui_info_file):
|
||
if os.path.exists(temp_path):
|
||
try:
|
||
os.remove(temp_path)
|
||
except OSError:
|
||
pass
|
||
|
||
try:
|
||
if not self.world.scene_manager.saveScene(temp_scene_file, project_path):
|
||
return False
|
||
|
||
os.replace(temp_scene_file, scene_file)
|
||
|
||
if os.path.exists(temp_gui_info_file):
|
||
os.replace(temp_gui_info_file, final_gui_info_file)
|
||
|
||
if os.path.exists(temp_lui_info_file):
|
||
os.replace(temp_lui_info_file, final_lui_info_file)
|
||
|
||
return True
|
||
except Exception as e:
|
||
print(f"原子保存场景失败: {e}")
|
||
return False
|
||
finally:
|
||
for temp_path in (temp_scene_file, temp_gui_info_file, temp_lui_info_file):
|
||
if os.path.exists(temp_path):
|
||
try:
|
||
os.remove(temp_path)
|
||
except OSError:
|
||
pass
|
||
|
||
# ==================== 项目打包功能 ====================
|
||
|
||
def buildPackage(self, build_dir):
|
||
"""将当前项目打包为最终运行程序。"""
|
||
try:
|
||
if not self.current_project_path:
|
||
print("错误: 请先创建或打开一个项目!")
|
||
return False
|
||
|
||
project_path = os.path.normpath(self.current_project_path)
|
||
project_display_name = (
|
||
(self.project_config or {}).get("name")
|
||
or os.path.basename(project_path)
|
||
or "EGProject"
|
||
)
|
||
project_name = self._sanitize_build_name(project_display_name)
|
||
|
||
if not build_dir:
|
||
print("错误: 请指定打包输出目录!")
|
||
return False
|
||
|
||
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
||
if not os.path.exists(scene_file):
|
||
print("错误: 请先保存场景!")
|
||
return False
|
||
|
||
if hasattr(self.world, "selection") and self.world.selection:
|
||
self.world.selection.clearSelection()
|
||
print("已取消场景中的物体选中状态")
|
||
|
||
self._sync_project_script_manager(project_path, reload_scripts=True)
|
||
|
||
if not self.saveProject():
|
||
print("错误: 保存场景失败!")
|
||
return False
|
||
|
||
output_root = os.path.normpath(build_dir)
|
||
staging_root = os.path.join(output_root, f".{project_name}_staging")
|
||
source_root = os.path.join(staging_root, "source")
|
||
assets_root = os.path.join(staging_root, "assets")
|
||
dist_dir = os.path.join(output_root, project_name)
|
||
|
||
if os.path.exists(staging_root):
|
||
shutil.rmtree(staging_root)
|
||
os.makedirs(source_root, exist_ok=True)
|
||
os.makedirs(assets_root, exist_ok=True)
|
||
|
||
if os.path.exists(dist_dir):
|
||
shutil.rmtree(dist_dir)
|
||
|
||
shutil.copy2(scene_file, os.path.join(assets_root, "scene.bam"))
|
||
self.copy_folder(os.path.join(project_path, "scenes", "resources"), assets_root)
|
||
self._merge_folder_contents(
|
||
os.path.join(project_path, "Resources"),
|
||
os.path.join(assets_root, "resources"),
|
||
)
|
||
if not self._saveGUIElementsToJSON(assets_root, project_path):
|
||
print("错误: 导出 GUI 数据失败!")
|
||
return False
|
||
self._saveCameraStateToJSON(assets_root)
|
||
self._copyScriptsToBuild(assets_root, project_path)
|
||
self._copyRenderPipelineRuntime(assets_root)
|
||
|
||
runtime_entry = self._createProjectRuntimeEntry(source_root, project_display_name)
|
||
if not runtime_entry:
|
||
print("错误: 生成项目运行时入口失败")
|
||
return False
|
||
|
||
success = self._executeProjectBuild(
|
||
source_root=source_root,
|
||
assets_root=assets_root,
|
||
build_root=output_root,
|
||
project_name=project_name,
|
||
)
|
||
if not success:
|
||
return False
|
||
|
||
if os.path.exists(staging_root):
|
||
shutil.rmtree(staging_root, ignore_errors=True)
|
||
|
||
exe_path = os.path.join(dist_dir, f"{project_name}.exe")
|
||
print(f"打包完成!项目运行程序: {exe_path}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"打包过程出错:{str(e)}")
|
||
return False
|
||
|
||
def _merge_folder_contents(self, source_folder, destination_folder):
|
||
"""将 source_folder 的内容合并到 destination_folder。"""
|
||
if not source_folder or not os.path.exists(source_folder):
|
||
return False
|
||
|
||
os.makedirs(destination_folder, exist_ok=True)
|
||
for root, _, files in os.walk(source_folder):
|
||
rel_root = os.path.relpath(root, source_folder)
|
||
target_root = destination_folder if rel_root == "." else os.path.join(destination_folder, rel_root)
|
||
os.makedirs(target_root, exist_ok=True)
|
||
for file_name in files:
|
||
source_file = os.path.join(root, file_name)
|
||
target_file = os.path.join(target_root, file_name)
|
||
shutil.copy2(source_file, target_file)
|
||
return True
|
||
|
||
def _load_runtime_template(self):
|
||
template_path = os.path.join(self._get_repo_root(), "templates", "main_template.py")
|
||
with open(template_path, "r", encoding="utf-8") as f:
|
||
return f.read()
|
||
|
||
def _createProjectRuntimeEntry(self, source_root, project_name):
|
||
try:
|
||
template_content = self._load_runtime_template()
|
||
safe_project_name = project_name.replace("\\", "\\\\").replace('"', '\\"')
|
||
runtime_source = template_content.replace("__EG_PROJECT_NAME__", safe_project_name)
|
||
runtime_entry = os.path.join(source_root, "main.py")
|
||
with open(runtime_entry, "w", encoding="utf-8") as f:
|
||
f.write(runtime_source)
|
||
return runtime_entry
|
||
except Exception as e:
|
||
print(f"创建项目运行时入口失败: {e}")
|
||
return ""
|
||
|
||
def _executeProjectBuild(self, source_root, assets_root, build_root, project_name):
|
||
repo_root = self._get_repo_root()
|
||
work_root = os.path.join(build_root, f".{project_name}_work")
|
||
spec_root = os.path.join(build_root, f".{project_name}_spec")
|
||
|
||
for directory in (work_root, spec_root):
|
||
if os.path.exists(directory):
|
||
shutil.rmtree(directory)
|
||
os.makedirs(directory, exist_ok=True)
|
||
|
||
pyinstaller_runner = None
|
||
pyinstaller_candidates = [
|
||
[sys.executable, "-m", "PyInstaller"],
|
||
[sys.executable, "-m", "pyinstaller"],
|
||
["pyinstaller"],
|
||
]
|
||
for candidate in pyinstaller_candidates:
|
||
pyinstaller_probe = subprocess.run(
|
||
[*candidate, "--version"],
|
||
capture_output=True,
|
||
text=True,
|
||
check=False,
|
||
)
|
||
if pyinstaller_probe.returncode == 0:
|
||
pyinstaller_runner = candidate
|
||
break
|
||
|
||
if not pyinstaller_runner:
|
||
print("错误: 当前环境未安装 PyInstaller,无法执行项目打包。")
|
||
return False
|
||
|
||
sep = ";" if os.name == "nt" else ":"
|
||
command = [
|
||
*pyinstaller_runner,
|
||
"--noconfirm",
|
||
"--clean",
|
||
"--windowed",
|
||
"--name",
|
||
project_name,
|
||
"--distpath",
|
||
build_root,
|
||
"--workpath",
|
||
work_root,
|
||
"--specpath",
|
||
spec_root,
|
||
"--paths",
|
||
source_root,
|
||
"--paths",
|
||
repo_root,
|
||
"--paths",
|
||
os.path.join(repo_root, "RenderPipelineFile"),
|
||
"--exclude-module",
|
||
"imgui_bundle",
|
||
"--exclude-module",
|
||
"p3dimgui",
|
||
"--hidden-import",
|
||
"core",
|
||
"--hidden-import",
|
||
"core.script_system",
|
||
"--hidden-import",
|
||
"core.CustomMouseController",
|
||
"--hidden-import",
|
||
"ssbo_component.runtime_importer",
|
||
"--hidden-import",
|
||
"ssbo_component.ssbo_controller",
|
||
"--hidden-import",
|
||
"rpcore",
|
||
"--collect-submodules",
|
||
"rpcore",
|
||
"--collect-submodules",
|
||
"rpplugins",
|
||
"--collect-submodules",
|
||
"rplibs",
|
||
]
|
||
|
||
panda_binary_dir = self._get_panda3d_binary_dir()
|
||
panda_display_binaries = [
|
||
"libpandagl.dll",
|
||
"libp3windisplay.dll",
|
||
"libpandadx9.dll",
|
||
"libp3tinydisplay.dll",
|
||
"cgGL.dll",
|
||
"cgD3D9.dll",
|
||
"d3dx9_43.dll",
|
||
]
|
||
if panda_binary_dir:
|
||
for binary_name in panda_display_binaries:
|
||
binary_path = os.path.join(panda_binary_dir, binary_name)
|
||
if os.path.exists(binary_path):
|
||
command.extend(["--add-binary", f"{binary_path}{sep}."])
|
||
|
||
data_mappings = [
|
||
(os.path.join(assets_root, "scene.bam"), "."),
|
||
(os.path.join(assets_root, "camera_state.json"), "."),
|
||
(os.path.join(assets_root, "gui"), "gui"),
|
||
(os.path.join(assets_root, "resources"), "resources"),
|
||
(os.path.join(assets_root, "scripts"), "scripts"),
|
||
(os.path.join(assets_root, "RenderPipelineFile"), "RenderPipelineFile"),
|
||
]
|
||
|
||
for source_path, target_path in data_mappings:
|
||
if os.path.exists(source_path):
|
||
command.extend(["--add-data", f"{source_path}{sep}{target_path}"])
|
||
|
||
command.append(os.path.join(source_root, "main.py"))
|
||
|
||
result = subprocess.run(
|
||
command,
|
||
cwd=source_root,
|
||
capture_output=True,
|
||
text=True,
|
||
check=False,
|
||
)
|
||
if result.returncode != 0:
|
||
print("项目打包失败。")
|
||
output = (result.stdout or "") + "\n" + (result.stderr or "")
|
||
print(output[-4000:])
|
||
return False
|
||
|
||
for directory in (work_root, spec_root):
|
||
if os.path.exists(directory):
|
||
shutil.rmtree(directory, ignore_errors=True)
|
||
|
||
print(f"✓ 项目已打包到: {os.path.join(build_root, project_name)}")
|
||
return True
|
||
|
||
def _get_panda3d_binary_dir(self):
|
||
try:
|
||
panda3d_spec = importlib.util.find_spec("panda3d.core")
|
||
if panda3d_spec and panda3d_spec.origin:
|
||
panda_package_dir = os.path.dirname(os.path.dirname(os.path.abspath(panda3d_spec.origin)))
|
||
candidate_dirs = [
|
||
os.path.join(panda_package_dir, "bin"),
|
||
os.path.join(os.path.dirname(panda_package_dir), "bin"),
|
||
]
|
||
for candidate_dir in candidate_dirs:
|
||
if os.path.isdir(candidate_dir):
|
||
return candidate_dir
|
||
except Exception as e:
|
||
print(f"⚠️ 获取 Panda3D 二进制目录失败: {e}")
|
||
return ""
|
||
|
||
def _createStandardBuildFiles(self, build_dir, project_path, scene_file):
|
||
"""创建标准的Panda3D打包文件"""
|
||
project_name = os.path.basename(project_path)
|
||
# 确保构建目录存在
|
||
if not os.path.exists(build_dir):
|
||
os.makedirs(build_dir)
|
||
|
||
# 复制场景文件到构建目录
|
||
shutil.copy2(scene_file, os.path.join(build_dir, "scene.bam"))
|
||
|
||
# 复制Resources文件夹到build目录
|
||
source_resources = os.path.join(project_path, "scenes", "resources")
|
||
self.copy_folder(source_resources, build_dir)
|
||
|
||
self._saveGUIElementsToJSON(build_dir, project_path)
|
||
|
||
self._copyScriptsToBuild(build_dir, project_path)
|
||
|
||
self._copyScriptSystemToBuild(build_dir)
|
||
|
||
source_render_pipeline = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"RenderPipelineFile")
|
||
dest_render_pipeline = os.path.join(build_dir,"RenderPipelineFile")
|
||
|
||
if os.path.exists(source_render_pipeline):
|
||
if os.path.exists(dest_render_pipeline):
|
||
shutil.rmtree(dest_render_pipeline)
|
||
|
||
shutil.copytree(
|
||
source_render_pipeline,
|
||
dest_render_pipeline,
|
||
ignore=shutil.ignore_patterns('__pycache__','*.pyc','.git','.vscode','*.log','samples')
|
||
)
|
||
print("✓ RenderPipelineFile文件夹已复制到build目录")
|
||
else:
|
||
print("⚠️ RenderPipelineFile文件夹未找到")
|
||
|
||
# 创建标准的应用程序入口文件
|
||
self._createAppFile(build_dir, project_name)
|
||
|
||
# 创建标准的setup.py文件
|
||
self._createStandardSetupFile(build_dir, project_name)
|
||
|
||
#创建requirements.txt文件
|
||
self._createRequirementsFile(build_dir)
|
||
|
||
def _copyScriptsToBuild(self, build_dir, project_path):
|
||
"""将项目脚本编译后复制到构建目录,避免直接暴露源码。"""
|
||
try:
|
||
scripts_dest = os.path.join(build_dir, "scripts")
|
||
scripts_src = self.get_project_scripts_dir(project_path)
|
||
if os.path.exists(scripts_dest):
|
||
shutil.rmtree(scripts_dest)
|
||
os.makedirs(scripts_dest, exist_ok=True)
|
||
|
||
if os.path.exists(scripts_src):
|
||
compiled_count = 0
|
||
fallback_count = 0
|
||
for root, _, files in os.walk(scripts_src):
|
||
rel_root = os.path.relpath(root, scripts_src)
|
||
target_root = scripts_dest if rel_root == "." else os.path.join(scripts_dest, rel_root)
|
||
os.makedirs(target_root, exist_ok=True)
|
||
|
||
for file_name in files:
|
||
if file_name.startswith("__"):
|
||
continue
|
||
|
||
source_file = os.path.join(root, file_name)
|
||
if file_name.endswith(".py"):
|
||
compiled_file = os.path.join(
|
||
target_root,
|
||
f"{os.path.splitext(file_name)[0]}.pyc",
|
||
)
|
||
try:
|
||
py_compile.compile(source_file, cfile=compiled_file, doraise=True)
|
||
compiled_count += 1
|
||
except py_compile.PyCompileError as e:
|
||
fallback_file = os.path.join(target_root, file_name)
|
||
shutil.copy2(source_file, fallback_file)
|
||
fallback_count += 1
|
||
print(f"⚠️ 编译脚本失败,已回退复制源码: {source_file} -> {fallback_file} ({e})")
|
||
elif not file_name.endswith((".pyc", ".pyo", ".log")):
|
||
shutil.copy2(source_file, os.path.join(target_root, file_name))
|
||
|
||
print(f"✓ Scripts已导出到 build/scripts,编译 {compiled_count} 个脚本")
|
||
if fallback_count:
|
||
print(f"⚠️ {fallback_count} 个脚本因编译失败保留为 .py 源码")
|
||
else:
|
||
print("⚠️ 项目中没有scripts目录")
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 复制脚本文件时出错: {str(e)}")
|
||
|
||
def _copyRenderPipelineRuntime(self, build_dir):
|
||
"""复制运行时需要的 RenderPipeline 资源,并将源码编译为 pyc。"""
|
||
try:
|
||
source_root = os.path.join(self._get_repo_root(), "RenderPipelineFile")
|
||
target_root = os.path.join(build_dir, "RenderPipelineFile")
|
||
|
||
if os.path.exists(target_root):
|
||
shutil.rmtree(target_root)
|
||
|
||
if not os.path.exists(source_root):
|
||
print("⚠️ RenderPipelineFile 文件夹未找到")
|
||
return False
|
||
|
||
blocked_dirs = {"__pycache__", ".git", ".vscode", "samples", "toolkit"}
|
||
blocked_root_files = {"setup.py", "start_daytime_editor.py", "start_plugin_configurator.py"}
|
||
blocked_relative_dirs = {
|
||
os.path.normpath(os.path.join("rplibs", "yaml", "yaml_py2")),
|
||
}
|
||
|
||
for root, dirs, files in os.walk(source_root):
|
||
relative_root = os.path.relpath(root, source_root)
|
||
normalized_relative_root = os.path.normpath(relative_root)
|
||
dirs[:] = [
|
||
name for name in dirs
|
||
if name not in blocked_dirs
|
||
and os.path.normpath(os.path.join(normalized_relative_root, name)) not in blocked_relative_dirs
|
||
]
|
||
target_dir = target_root if relative_root == "." else os.path.join(target_root, relative_root)
|
||
os.makedirs(target_dir, exist_ok=True)
|
||
|
||
for file_name in files:
|
||
if file_name.endswith((".pyc", ".pyo", ".log")):
|
||
continue
|
||
if relative_root == "." and file_name in blocked_root_files:
|
||
continue
|
||
|
||
source_file = os.path.join(root, file_name)
|
||
if file_name.endswith(".py"):
|
||
compiled_file = os.path.join(
|
||
target_dir,
|
||
f"{os.path.splitext(file_name)[0]}.pyc",
|
||
)
|
||
try:
|
||
py_compile.compile(source_file, cfile=compiled_file, doraise=True)
|
||
except py_compile.PyCompileError as e:
|
||
print(f"⚠️ RenderPipeline 脚本编译失败,保留源码: {source_file} ({e})")
|
||
shutil.copy2(source_file, os.path.join(target_dir, file_name))
|
||
else:
|
||
shutil.copy2(source_file, os.path.join(target_dir, file_name))
|
||
|
||
print("✓ RenderPipeline 运行时资源已导出")
|
||
return True
|
||
except Exception as e:
|
||
print(f"⚠️ 导出 RenderPipeline 运行时资源失败: {e}")
|
||
return False
|
||
|
||
def _copyScriptSystemToBuild(self,build_dir):
|
||
core_files = [
|
||
"script_system.py",
|
||
"CustomMouseController.py"
|
||
]
|
||
|
||
source_core_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"core")
|
||
|
||
core_dest = os.path.join(build_dir,"core")
|
||
|
||
if not os.path.exists(core_dest):
|
||
os.makedirs(core_dest)
|
||
|
||
for file_name in core_files:
|
||
source_file = os.path.join(source_core_dir,file_name)
|
||
|
||
if os.path.exists(source_file):
|
||
shutil.copy2(source_file,os.path.join(core_dest,file_name))
|
||
|
||
def _saveGUIElementsToJSON(self, build_dir, project_path):
|
||
"""保存GUI元素到JSON文件,内容与_collectGUIElementInfo保持一致"""
|
||
try:
|
||
# 创建目标gui目录
|
||
gui_dest = os.path.join(build_dir, "gui")
|
||
if not os.path.exists(gui_dest):
|
||
os.makedirs(gui_dest)
|
||
|
||
# 收集所有GUI元素信息
|
||
gui_data = []
|
||
|
||
# 获取当前场景中的GUI元素
|
||
if hasattr(self.world, 'gui_elements'):
|
||
for gui_node in self.world.gui_elements:
|
||
if gui_node and not gui_node.isEmpty():
|
||
# 使用_collectGUIElementInfo方法收集信息
|
||
gui_info = self.world.scene_manager._collectGUIElementInfo(gui_node)
|
||
if gui_info:
|
||
self._updateResourcePaths(gui_info,project_path,build_dir)
|
||
gui_data.append(gui_info)
|
||
print(f"收集GUI元素信息: {gui_info['name']}")
|
||
|
||
# 保存GUI信息到JSON文件
|
||
gui_file_path = os.path.join(gui_dest, "gui_elements.json")
|
||
with open(gui_file_path, "w", encoding="utf-8") as f:
|
||
json.dump(gui_data, f, ensure_ascii=False, indent=4)
|
||
|
||
print(f"✓ GUI元素数据已保存到 {gui_file_path},共 {len(gui_data)} 个元素")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 保存GUI元素时出错: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def _saveCameraStateToJSON(self, build_dir):
|
||
"""保存当前主相机状态,供打包运行时恢复。"""
|
||
try:
|
||
camera = getattr(self.world, "camera", None) or getattr(self.world, "cam", None)
|
||
if not camera or camera.isEmpty():
|
||
return False
|
||
|
||
camera_state = {
|
||
"position": list(camera.getPos()),
|
||
"rotation": list(camera.getHpr()),
|
||
"camera_control_enabled": bool(getattr(self.world, "camera_control_enabled", True)),
|
||
}
|
||
|
||
camera_state_path = os.path.join(build_dir, "camera_state.json")
|
||
with open(camera_state_path, "w", encoding="utf-8") as f:
|
||
json.dump(camera_state, f, ensure_ascii=False, indent=4)
|
||
|
||
print(f"✓ 主相机状态已保存到 {camera_state_path}")
|
||
return True
|
||
except Exception as e:
|
||
print(f"⚠️ 保存主相机状态失败: {e}")
|
||
return False
|
||
|
||
def _updateResourcePaths(self,gui_info,project_path,build_dir):
|
||
project_path = os.path.normpath(project_path)
|
||
project_resources_dir = os.path.join(project_path, "Resources")
|
||
scene_resources_dir = os.path.join(project_path, "scenes", "resources")
|
||
|
||
def normalize_resource_path(old_path):
|
||
if not old_path:
|
||
return old_path
|
||
|
||
old_path = str(old_path).strip()
|
||
if not old_path:
|
||
return old_path
|
||
|
||
if old_path.startswith(("http://", "https://")):
|
||
return old_path
|
||
|
||
normalized_path = os.path.normpath(old_path.replace("/", os.sep))
|
||
if normalized_path == "resources" or normalized_path.startswith(f"resources{os.sep}"):
|
||
return normalized_path.replace("\\", "/")
|
||
|
||
candidate_paths = []
|
||
if os.path.isabs(normalized_path):
|
||
candidate_paths.append(normalized_path)
|
||
else:
|
||
candidate_paths.extend(
|
||
[
|
||
os.path.join(project_resources_dir, normalized_path),
|
||
os.path.join(scene_resources_dir, normalized_path),
|
||
os.path.join(project_path, normalized_path),
|
||
]
|
||
)
|
||
|
||
resolved_path = ""
|
||
for candidate in candidate_paths:
|
||
if os.path.exists(candidate):
|
||
resolved_path = os.path.normpath(candidate)
|
||
break
|
||
|
||
if not resolved_path and os.path.isabs(normalized_path):
|
||
resolved_path = normalized_path
|
||
|
||
if resolved_path:
|
||
for base_dir in (project_resources_dir, scene_resources_dir):
|
||
try:
|
||
if os.path.exists(base_dir) and os.path.commonpath([resolved_path, base_dir]) == os.path.normpath(base_dir):
|
||
relative_path = os.path.relpath(resolved_path, base_dir)
|
||
return os.path.join("resources", relative_path).replace("\\", "/")
|
||
except ValueError:
|
||
continue
|
||
|
||
try:
|
||
relative_path = os.path.relpath(resolved_path, project_path)
|
||
if not relative_path.startswith(".."):
|
||
return os.path.join("resources", relative_path).replace("\\", "/")
|
||
except ValueError:
|
||
pass
|
||
|
||
return os.path.join("resources", os.path.basename(resolved_path)).replace("\\", "/")
|
||
|
||
return os.path.join("resources", os.path.basename(normalized_path)).replace("\\", "/")
|
||
|
||
if "image_path" in gui_info and gui_info["image_path"]:
|
||
gui_info["image_path"] = normalize_resource_path(gui_info["image_path"])
|
||
if "video_path" in gui_info and gui_info["video_path"]:
|
||
gui_info["video_path"] = normalize_resource_path(gui_info["video_path"])
|
||
if "bg_image_path" in gui_info and gui_info["bg_image_path"]:
|
||
gui_info["bg_image_path"] = normalize_resource_path(gui_info["bg_image_path"])
|
||
|
||
def _createRequirementsFile(self,build_dir):
|
||
requirements_content = """Panda3D==1.10.15
|
||
imgui-bundle
|
||
Pillow>=9.0.1
|
||
numpy>=1.24
|
||
aiohttp>=3.9
|
||
openvr==2.2.0
|
||
pyassimp>=5.2.5
|
||
"""
|
||
|
||
requirements_path = os.path.join(build_dir,"requirements.txt")
|
||
with open(requirements_path,"w",encoding="utf-8") as f:
|
||
f.write(requirements_content)
|
||
|
||
def copy_folder(self, source_folder, destination_folder):
|
||
"""将一个文件夹从源路径复制到目标路径下的resources文件夹中
|
||
|
||
Args:
|
||
source_folder (str): 源文件夹路径
|
||
destination_folder (str): 目标文件夹路径
|
||
"""
|
||
try:
|
||
# 创建resources文件夹作为目标
|
||
resources_dest = os.path.join(destination_folder, "resources")
|
||
|
||
# 确保目标目录存在
|
||
if not os.path.exists(destination_folder):
|
||
os.makedirs(destination_folder)
|
||
|
||
# 如果目标resources文件夹已存在,先删除
|
||
if os.path.exists(resources_dest):
|
||
shutil.rmtree(resources_dest)
|
||
|
||
# 检查源文件夹是否存在
|
||
if os.path.exists(source_folder):
|
||
# 复制整个文件夹到resources目录下
|
||
shutil.copytree(
|
||
source_folder,
|
||
resources_dest,
|
||
ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log')
|
||
)
|
||
print(f"✓ 文件夹已从 {source_folder} 复制到 {resources_dest}")
|
||
return True
|
||
else:
|
||
print(f"⚠️ 源文件夹不存在: {source_folder}")
|
||
# 即使源文件夹不存在,也创建空的resources目录
|
||
if not os.path.exists(resources_dest):
|
||
os.makedirs(resources_dest)
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 复制文件夹时出错: {str(e)}")
|
||
return False
|
||
|
||
def _copyResourcesToBuild(self, build_dir, project_path):
|
||
"""复制GUI资源到构建目录的resources文件夹"""
|
||
try:
|
||
# 创建目标resources目录
|
||
resources_dest = os.path.join(build_dir, "resources")
|
||
|
||
# 源Resources目录
|
||
resources_src = os.path.join(project_path, "Resources")
|
||
|
||
if os.path.exists(resources_src):
|
||
# 直接复制整个Resources目录
|
||
if os.path.exists(resources_dest):
|
||
shutil.rmtree(resources_dest)
|
||
|
||
shutil.copytree(
|
||
resources_src,
|
||
resources_dest,
|
||
ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log')
|
||
)
|
||
print("✓ Resources目录已复制到build/resources")
|
||
|
||
# 统计复制的文件数量
|
||
file_count = 0
|
||
for root, dirs, files in os.walk(resources_dest):
|
||
file_count += len(files)
|
||
print(f"✓ 共复制了 {file_count} 个资源文件")
|
||
else:
|
||
# 创建空的resources目录
|
||
if not os.path.exists(resources_dest):
|
||
os.makedirs(resources_dest)
|
||
print("⚠️ 项目中没有Resources目录")
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 复制资源文件时出错: {str(e)}")
|
||
|
||
def _collectResourceFiles(self, project_path):
|
||
"""收集项目中GUI使用的资源文件"""
|
||
resource_files = set()
|
||
|
||
try:
|
||
# 收集Resources目录中的所有文件(这是最主要的资源来源)
|
||
resources_dir = os.path.join(project_path, "Resources")
|
||
if os.path.exists(resources_dir):
|
||
for root, dirs, files in os.walk(resources_dir):
|
||
for file in files:
|
||
file_path = os.path.join(root, file)
|
||
# 收集所有文件,不仅仅是媒体文件
|
||
resource_files.add(file_path)
|
||
|
||
# 同时收集场景中引用的特定资源
|
||
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
||
if os.path.exists(scene_file):
|
||
# 从场景文件中提取资源引用
|
||
referenced_files = self._extractResourcesFromScene(scene_file, project_path)
|
||
for file_path in referenced_files:
|
||
if os.path.isabs(file_path):
|
||
if os.path.exists(file_path):
|
||
resource_files.add(file_path)
|
||
else:
|
||
# 相对路径
|
||
full_path = os.path.join(project_path, file_path)
|
||
if os.path.exists(full_path):
|
||
resource_files.add(full_path)
|
||
|
||
except Exception as e:
|
||
print(f"收集资源文件时出错: {str(e)}")
|
||
|
||
return list(resource_files)
|
||
|
||
def _extractResourcesFromScene(self, scene_file, project_path):
|
||
"""从场景文件中提取资源引用"""
|
||
referenced_files = []
|
||
|
||
try:
|
||
# 这里应该实现从BAM文件中提取贴图、视频等资源引用的逻辑
|
||
# 由于直接解析BAM文件比较复杂,我们采用间接方式
|
||
|
||
# 检查项目配置文件或其他元数据文件中可能包含的资源引用
|
||
config_file = os.path.join(project_path, "project.json")
|
||
if os.path.exists(config_file):
|
||
with open(config_file, "r", encoding="utf-8") as f:
|
||
project_config = json.load(f)
|
||
# 这里可以根据实际需要提取资源引用
|
||
|
||
except Exception as e:
|
||
print(f"从场景文件提取资源引用时出错: {str(e)}")
|
||
|
||
return referenced_files
|
||
|
||
# ==================== 辅助方法 ====================
|
||
|
||
def _clearCurrentScene(self):
|
||
"""清空当前场景"""
|
||
try:
|
||
if hasattr(self.world, 'scene_manager') and self.world.scene_manager:
|
||
self.world.scene_manager.clearScene()
|
||
print("✓ 当前场景已清空")
|
||
except Exception as e:
|
||
print(f"清空场景失败: {str(e)}")
|
||
|
||
def updateWindowTitle(self, parent_window, project_name):
|
||
"""更新窗口标题(保留方法以兼容旧代码)"""
|
||
# 这个方法现在不需要做任何事情,因为我们不再处理UI
|
||
pass
|
||
|
||
def _createAppFile(self, build_dir, project_name):
|
||
"""创建标准的应用程序入口文件"""
|
||
try:
|
||
app_content = f'''#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
{project_name} - Panda3D应用程序
|
||
自动生成的入口文件
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
|
||
# 添加当前目录到Python路径
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
try:
|
||
from panda3d.core import loadPrcFileData
|
||
|
||
# 设置基本的Panda3D配置
|
||
loadPrcFileData("", """
|
||
window-title {project_name}
|
||
sync-video false
|
||
show-frame-rate-meter true
|
||
""")
|
||
|
||
# 导入主应用程序类
|
||
# 注意:这里需要根据实际情况修改导入路径
|
||
from main import MyWorld
|
||
|
||
# 创建并运行应用程序
|
||
app = MyWorld()
|
||
app.run()
|
||
|
||
except ImportError as e:
|
||
print(f"导入错误: {{e}}")
|
||
print("请确保已正确安装Panda3D")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(f"运行错误: {{e}}")
|
||
sys.exit(1)
|
||
'''
|
||
|
||
app_file = os.path.join(build_dir, "main.py")
|
||
with open(app_file, "w", encoding="utf-8") as f:
|
||
f.write(app_content)
|
||
print(f"✓ 应用程序入口文件已创建: {app_file}")
|
||
|
||
except Exception as e:
|
||
print(f"创建应用程序入口文件失败: {str(e)}")
|
||
|
||
def _createStandardSetupFile(self, build_dir, project_name):
|
||
"""创建标准的setup.py文件用于打包"""
|
||
try:
|
||
setup_content = f'''#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
{project_name} - Panda3D项目打包配置
|
||
"""
|
||
|
||
from setuptools import setup, find_packages
|
||
import sys
|
||
|
||
# 确保Python版本兼容性
|
||
if sys.version_info < (3, 6):
|
||
sys.exit("Python 3.6或更高版本是必需的")
|
||
|
||
setup(
|
||
name="{project_name}",
|
||
version="1.0.0",
|
||
description="基于Panda3D的3D应用程序",
|
||
author="",
|
||
author_email="",
|
||
url="",
|
||
|
||
# 包含所有Python包
|
||
packages=find_packages(),
|
||
|
||
# 包含非Python文件
|
||
include_package_data=True,
|
||
package_data={{
|
||
"": ["*.bam", "*.egg", "*.png", "*.jpg", "*.jpeg", "*.ogg", "*.wav", "*.mp3"]
|
||
}},
|
||
|
||
# 依赖项
|
||
install_requires=[
|
||
"Panda3D==1.10.15",
|
||
"imgui-bundle",
|
||
"Pillow>=9.0.1",
|
||
"numpy>=1.24",
|
||
"aiohttp>=3.9",
|
||
"openvr==2.2.0",
|
||
"pyassimp>=5.2.5",
|
||
],
|
||
|
||
# Python版本要求
|
||
python_requires=">=3.11",
|
||
|
||
# 分类信息
|
||
classifiers=[
|
||
"Development Status :: 4 - Beta",
|
||
"Intended Audience :: Developers",
|
||
"License :: OSI Approved :: MIT License",
|
||
"Programming Language :: Python :: 3",
|
||
"Programming Language :: Python :: 3.6",
|
||
"Programming Language :: Python :: 3.7",
|
||
"Programming Language :: Python :: 3.8",
|
||
"Programming Language :: Python :: 3.9",
|
||
"Programming Language :: Python :: 3.10",
|
||
],
|
||
|
||
# 入口点
|
||
entry_points={{
|
||
"console_scripts": [
|
||
"{project_name}=main:main",
|
||
],
|
||
}},
|
||
)
|
||
'''
|
||
|
||
setup_file = os.path.join(build_dir, "setup.py")
|
||
with open(setup_file, "w", encoding="utf-8") as f:
|
||
f.write(setup_content)
|
||
print(f"✓ setup.py文件已创建: {setup_file}")
|
||
|
||
except Exception as e:
|
||
print(f"创建setup.py文件失败: {str(e)}")
|
||
|
||
def _executeStandardBuild(self, build_dir):
|
||
"""执行标准的Panda3D打包命令"""
|
||
try:
|
||
# 这里可以实现具体的打包逻辑
|
||
# 例如使用pdeploy或其他打包工具
|
||
|
||
print(f"✓ 项目文件已准备到: {build_dir}")
|
||
print("注意: 请使用Panda3D的pdeploy工具进行最终打包")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"执行打包命令失败: {str(e)}")
|
||
return False
|