Skip to content

Commit 3820caf

Browse files
committed
feat: 添加打包构建和远程调试支持
1 parent fcca67d commit 3820caf

File tree

6 files changed

+239
-13
lines changed

6 files changed

+239
-13
lines changed

build.bat

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
@echo off
2+
chcp 65001 >nul 2>&1
3+
4+
set OUT=dist\app
5+
set DEST=%OUT%\metaminsweeper
6+
7+
echo [Clean] Removing old builds...
8+
if exist "%OUT%\metaminsweeper" rmdir /s /q "%OUT%\metaminsweeper"
9+
if exist "%OUT%\plugin_manager" rmdir /s /q "%OUT%\plugin_manager"
10+
11+
echo.
12+
echo [1/3] metaminsweeper.exe
13+
pyinstaller --noconfirm --name metaminsweeper --windowed --distpath %OUT% src\main.py
14+
15+
echo.
16+
echo [2/3] plugin_manager.exe
17+
pyinstaller --noconfirm --name plugin_manager --windowed --hidden-import code --hidden-import xmlrpc --hidden-import xmlrpc.server --hidden-import xmlrpc.client --hidden-import http.server --hidden-import socketserver --hidden-import email --hidden-import email.utils --distpath %OUT% src\plugin_manager\_run.py
18+
19+
echo.
20+
echo [3/3] Copy resources to metaminsweeper\
21+
copy /y "%OUT%\plugin_manager\plugin_manager.exe" "%DEST%\"
22+
xcopy /e /y /i "%OUT%\plugin_manager\_internal" "%DEST%\_internal" >nul
23+
xcopy /e /y /i "src\plugin_manager" "%DEST%\plugin_manager" >nul
24+
xcopy /e /y /i "src\plugins" "%DEST%\plugins" >nul
25+
xcopy /e /y /i "src\shared_types" "%DEST%\shared_types" >nul
26+
if exist "%DEST%\plugin_manager\_run.py" del /f /q "%DEST%\plugin_manager\_run.py"
27+
28+
echo [4/4] Copy debugpy from venv to _internal\
29+
set SP=.venv\Lib\site-packages
30+
xcopy /e /y /i "%SP%\debugpy" "%DEST%\_internal\debugpy" >nul
31+
xcopy /e /y /i "%SP%\msgspec" "%DEST%\_internal\msgspec" >nul 2>nul
32+
xcopy /e /y /i "%SP%\setuptools" "%DEST%\_internal\setuptools" >nul 2>nul
33+
34+
echo.
35+
echo Done! Both in: %OUT%\
36+
pause

hook-debugpy-pyinstaller.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""
2+
PyInstaller runtime hook for debugpy compatibility.
3+
在 import debugpy 之前修复路径问题,确保 _vendored 目录可被找到。
4+
"""
5+
import os
6+
import sys
7+
8+
# PyInstaller 打包后 __file__ 指向临时目录或 zip 内,
9+
# debugpy/_vendored/__init__.py 用 os.path.abspath(__file__) 定位资源会失败。
10+
# 此 hook 在任何代码执行前将 debugpy 的真实解压路径注入 sys._MEIPASS 搜索逻辑。
11+
12+
def _fix_debugpy_paths():
13+
# PyInstaller 运行时,已解压的文件在 sys._MEIPASS 下
14+
meipass = getattr(sys, "_MEIPASS", None)
15+
if not meipass:
16+
return # 非打包环境,不需要处理
17+
18+
debugpy_dir = os.path.join(meipass, "debugpy")
19+
vendored_dir = os.path.join(debugpy_dir, "_vendored")
20+
21+
if os.path.isdir(debugpy_dir):
22+
# 确保 debugpy 在 sys.path 中靠前(PyInstaller 已处理,但保险起见)
23+
if debugpy_dir not in sys.path:
24+
sys.path.insert(0, debugpy_dir)
25+
26+
_fix_debugpy_paths()

src/main.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,41 @@ def cli_check_file(file_path: str) -> int:
156156

157157
# ── 启动 ZMQ Server + 插件管理器 ──
158158
game_server = GameServerBridge(ui)
159-
plugin_process = subprocess.Popen(
160-
[sys.executable, "-m", "plugin_manager", "--mode", "tray"],
161-
cwd=os.path.dirname(os.path.abspath(__file__)),
162-
env=get_env_for_subprocess(),
163-
)
159+
160+
# 打包后直接调用 plugin_manager.exe,开发模式用 python -m
161+
if getattr(sys, 'frozen', False):
162+
base_dir = os.path.dirname(sys.executable)
163+
plugin_exe = os.path.join(base_dir, "plugin_manager.exe")
164+
if not os.path.exists(plugin_exe):
165+
QtWidgets.QMessageBox.warning(
166+
mainWindow, "Plugin Manager",
167+
f"plugin_manager.exe not found:\n{plugin_exe}\n\nPlugins will be disabled.",
168+
)
169+
plugin_process = None
170+
else:
171+
cmd = [plugin_exe, "--mode", "tray"]
172+
cwd = base_dir
173+
try:
174+
plugin_process = subprocess.Popen(
175+
cmd, cwd=cwd, env=get_env_for_subprocess(),
176+
)
177+
except Exception as e:
178+
QtWidgets.QMessageBox.warning(
179+
mainWindow, "Plugin Manager",
180+
f"Failed to start plugin_manager:\n{e}",
181+
)
182+
plugin_process = None
183+
else:
184+
cmd = [sys.executable, "-m", "plugin_manager", "--mode", "tray"]
185+
cwd = os.path.dirname(os.path.abspath(__file__))
186+
try:
187+
plugin_process = subprocess.Popen(
188+
cmd, cwd=cwd, env=get_env_for_subprocess(),
189+
)
190+
except Exception as e:
191+
print(f"[WARN] Failed to start plugin_manager: {e}")
192+
plugin_process = None
193+
164194
ui._plugin_process = plugin_process # 保存引用,防止被 GC
165195

166196
# 连接信号:插件发来的新游戏指令 → 主线程处理
@@ -180,7 +210,7 @@ def cli_check_file(file_path: str) -> int:
180210

181211
def _cleanup():
182212
game_server.stop()
183-
if plugin_process.poll() is None:
213+
if plugin_process is not None and plugin_process.poll() is None:
184214
plugin_process.terminate()
185215
plugin_process.wait(timeout=5)
186216

src/plugin_manager/_run.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""
2+
插件管理器独立入口(供 PyInstaller 打包使用)
3+
4+
替代 __main__.py,避免相对导入问题。
5+
python -m plugin_manager 仍走 __main__.py(开发模式)
6+
打包后使用此脚本作为入口。
7+
"""
8+
9+
import argparse
10+
import sys
11+
12+
13+
def main() -> int:
14+
parser = argparse.ArgumentParser(description="Solvable-Minesweeper 插件管理器")
15+
parser.add_argument(
16+
"--endpoint",
17+
default="tcp://127.0.0.1:5555",
18+
help="ZMQ Server 地址 (默认: tcp://127.0.0.1:5555)",
19+
)
20+
parser.add_argument(
21+
"--mode",
22+
choices=["window", "tray"],
23+
default="window",
24+
help="GUI 模式: window=显示主窗口, tray=系统托盘 (默认: window)",
25+
)
26+
parser.add_argument(
27+
"--no-gui",
28+
action="store_true",
29+
default=False,
30+
help="不显示界面(后台模式)",
31+
)
32+
parser.add_argument(
33+
"--debug",
34+
action="store_true",
35+
default=False,
36+
help="启用 debugpy 远程调试,等待 VS Code 附加 (端口 5678)",
37+
)
38+
parser.add_argument(
39+
"--debug-port",
40+
type=int,
41+
default=5678,
42+
help="debugpy 监听端口 (默认: 5678)",
43+
)
44+
45+
args = parser.parse_args()
46+
47+
# 可选:启动 debugpy 等待远程调试附加
48+
if args.debug:
49+
try:
50+
import debugpy
51+
# in_process_debug_adapter=True: 不启动子进程,直接在当前进程中运行 adapter
52+
# 解决 PyInstaller 打包后子进程找不到 Python/debugpy 的问题
53+
debugpy.listen(("0.0.0.0", args.debug_port), in_process_debug_adapter=True)
54+
print(f"[debug] Waiting for debugger attach on port {args.debug_port}...")
55+
debugpy.wait_for_client()
56+
print("[debug] Debugger attached, continuing...")
57+
except ImportError as e:
58+
print(f"[WARN] --debug set but debugpy import failed: {e}")
59+
60+
from plugin_manager.app_paths import get_log_dir
61+
from plugin_manager.logging_setup import init_logging
62+
63+
init_logging(get_log_dir(), console=True, level="DEBUG")
64+
65+
from plugin_manager import run_plugin_manager_process
66+
67+
show_main_window = not args.no_gui and args.mode == "window"
68+
69+
return run_plugin_manager_process(
70+
endpoint=args.endpoint,
71+
with_gui=not args.no_gui,
72+
show_main_window=show_main_window,
73+
)
74+
75+
76+
if __name__ == "__main__":
77+
sys.exit(main())

src/plugin_manager/app_paths.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,16 @@ def get_user_plugin_dirs() -> list[Path]:
9595
"""
9696
获取用户自定义插件目录(外部,用户自行添加的插件)
9797
98-
- 开发模式: <project>/src/user_plugins/(可选)
99-
- 打包模式: <exe所在目录>/user_plugins/
98+
- 开发模式: <项目根目录>/src/plugins/, <项目根目录>/src/user_plugins/
99+
- 打包模式: <exe所在目录>/plugins/, <exe所在目录>/user_plugins/
100100
"""
101101
base = get_executable_dir()
102-
user_dir = base / "user_plugins"
103-
if user_dir.is_dir():
104-
return [user_dir]
105-
return []
102+
result: list[Path] = []
103+
for name in ("plugins", "user_plugins"):
104+
d = base / name
105+
if d.is_dir():
106+
result.append(d)
107+
return result
106108

107109

108110
def get_all_plugin_dirs() -> list[Path]:

src/plugin_manager/main_window.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,11 +485,17 @@ def _setup_ui(self) -> None:
485485
self._list = lst
486486
left_layout.addWidget(lst)
487487

488-
# 刷新按钮
488+
# 刷新 + 调试按钮行
489489
btn_row = QHBoxLayout()
490490
self._refresh_btn = QPushButton(self.tr("刷新"))
491491
btn_row.addWidget(self._refresh_btn)
492492
btn_row.addStretch()
493+
494+
self._debug_btn = QPushButton("🐛 Debug")
495+
self._debug_btn.setCheckable(True)
496+
self._debug_btn.setToolTip(self.tr("开启/关闭远程调试 (debugpy)"))
497+
btn_row.addWidget(self._debug_btn)
498+
493499
left_layout.addLayout(btn_row)
494500

495501
left_panel.setMaximumWidth(200)
@@ -585,6 +591,9 @@ def _connect_signals(self) -> None:
585591
self._list.customContextMenuRequested.connect(self._on_list_context_menu)
586592
self.connection_changed.connect(self._on_conn_changed)
587593

594+
# 调试开关
595+
self._debug_btn.toggled.connect(self._toggle_debug)
596+
588597
# ── 连接状态 ────────────────────────────────────────
589598

590599
def set_connected(self, ok: bool) -> None:
@@ -613,6 +622,52 @@ def _poll_connection_status(self) -> None:
613622
except Exception:
614623
pass
615624

625+
# ── 远程调试 ────────────────────────────────────────
626+
627+
_debug_active: bool = False
628+
629+
def _toggle_debug(self, enabled: bool) -> None:
630+
"""开启/关闭 debugpy 远程调试"""
631+
if enabled:
632+
self._start_debug()
633+
else:
634+
self._stop_debug()
635+
636+
def _start_debug(self) -> None:
637+
"""启动 debugpy 监听"""
638+
try:
639+
import debugpy
640+
# in_process_debug_adapter=True: 不启动子进程,直接在当前进程中运行 adapter
641+
# 解决 PyInstaller 打包后子进程找不到 Python/debugpy 的问题
642+
debugpy.listen(("0.0.0.0", 5678), in_process_debug_adapter=True)
643+
PluginManagerWindow._debug_active = True
644+
self._debug_btn.setText("🐛 Listening...")
645+
self._debug_btn.setStyleSheet("background: #4caf50; color: white; font-weight: bold;")
646+
self.statusBar().showMessage(self.tr("Debug server listening on port 5678, waiting for VS Code attach..."))
647+
logger.info("Debug server started on port 5678")
648+
except ImportError as e:
649+
self._debug_btn.setChecked(False)
650+
QMessageBox.warning(
651+
self, "Debug",
652+
f"debugpy import failed:\n{e}",
653+
)
654+
except Exception as e:
655+
self._debug_btn.setChecked(False)
656+
QMessageBox.warning(self, "Debug", f"Failed to start debugger:\n{e}")
657+
658+
def _stop_debug(self) -> None:
659+
"""停止 debugpy"""
660+
try:
661+
import debugpy
662+
debugpy.stop_listen()
663+
except Exception:
664+
pass
665+
PluginManagerWindow._debug_active = False
666+
self._debug_btn.setText("🐛 Debug")
667+
self._debug_btn.setStyleSheet("")
668+
self.statusBar().showMessage(self.tr("Debug stopped"))
669+
logger.info("Debug server stopped")
670+
616671
# ── 插件列表 ────────────────────────────────────────
617672

618673
# ── 插件列表 ────────────────────────────────────────

0 commit comments

Comments
 (0)