仓库初始化

This commit is contained in:
lwt
2026-05-22 00:16:08 +08:00
commit 95c96dce42
118 changed files with 23005 additions and 0 deletions
View File
+116
View File
@@ -0,0 +1,116 @@
@echo off
REM ASCII-only batch: reliable under cmd.exe from PowerShell/Python (no UTF-8 BOM issues).
chcp 65001 >nul 2>&1
setlocal EnableExtensions EnableDelayedExpansion
title Server Manager - build
for %%I in ("%~dp0..\..") do set "ROOT=%%~fI"
set "SRC=%ROOT%\src"
set "RESOURCE_CONFIG=%ROOT%\resources\config"
pushd "%SRC%" || (
echo [ERROR] src directory not found: %SRC%
exit /b 1
)
echo ============================================================
echo Server Manager - build EXE
echo ============================================================
echo.
python --version >nul 2>&1
if errorlevel 1 (
echo [ERROR] Python not found. Install Python 3.8+
pause
exit /b 1
)
echo [1/6] pip deps...
pip install PyQt6 pyinstaller -q 2>nul
pip install pymysql Pillow -q 2>nul
python -c "import PyQt6" 2>nul
if errorlevel 1 (
echo [ERROR] pip install PyQt6
pause
exit /b 1
)
python -c "import PyInstaller" 2>nul
if errorlevel 1 (
echo [ERROR] pip install pyinstaller
pause
exit /b 1
)
echo [2/6] icon...
python "%ROOT%\resources\icon\convert_icon.py"
if not exist "%ROOT%\resources\icon\icon.ico" (
echo [WARN] no icon.ico, using --icon NONE
set "ICON_PARAM=--icon NONE"
) else (
set "ICON_PARAM=--icon %ROOT%\resources\icon\icon.ico"
)
echo [3/6] clean dist\build...
if exist "dist" rd /s /q dist
if exist "build" rd /s /q build
if exist "*.spec" del /q *.spec 2>nul
REM Pre-create work dir for PyInstaller warn-main.txt on some Windows setups
if not exist "build\main" mkdir "build\main"
echo [4/6] PyInstaller main (onedir)...
REM Single line: no ^ line-continuation (avoids trailing-space / encoding breakage)
pyinstaller --noconfirm --onedir --console --name main !ICON_PARAM! --add-data "%ROOT%\resources\version.json;." --add-data "%ROOT%\resources\config.json;." --add-data "%ROOT%\resources\icon;icon" --hidden-import PyQt6.QtWidgets --hidden-import PyQt6.QtCore --hidden-import PyQt6.QtGui --hidden-import pymysql --hidden-import entrypoints.cli.main --hidden-import entrypoints.cli.extended --hidden-import entrypoints.gui.bootstrap --hidden-import mcp.server --hidden-import mcp.tools --hidden-import models.app_config --hidden-import services.commands --hidden-import services.server_creator --hidden-import services.hot_update --hidden-import services.rg_search --hidden-import bsdiff4 --collect-all PyQt6 --collect-all bsdiff4 --hidden-import requests entrypoints\main.py
if errorlevel 1 (
echo [ERROR] main PyInstaller failed
pause
exit /b 1
)
echo [4b/6] copy rg.exe...
if exist "%ROOT%\resources\bin\rg.exe" (
copy /y "%ROOT%\resources\bin\rg.exe" "dist\main\rg.exe" >nul
echo copied resources\bin\rg.exe to dist\main\rg.exe
) else (
echo [WARN] resources\bin\rg.exe missing, search will not use ripgrep
)
echo [4c/6] copy resources\config templates to dist\main\config ^(same as installer {app}\config^)...
if exist "%RESOURCE_CONFIG%" (
if not exist "dist\main\config" mkdir "dist\main\config"
xcopy "%RESOURCE_CONFIG%\*" "dist\main\config\" /E /I /Y /Q >nul
echo copied ..\resources\config to dist\main\config
) else (
echo [WARN] ..\resources\config missing, Inno install will fail if resources\config is absent at compile time
)
echo [5/6] PyInstaller mini_updater...
pyinstaller --noconfirm --onefile --windowed --name mini_updater --distpath dist --workpath build_mini --specpath build_mini "%ROOT%\scripts\update\mini_updater.py"
if errorlevel 1 (
echo [ERROR] mini_updater PyInstaller failed
pause
exit /b 1
)
echo [6/6] done.
if not exist "dist\main\config" mkdir "dist\main\config"
if exist "config\tool.config" copy /y "config\tool.config" "dist\main\config\tool.config" >nul
rd /s /q build 2>nul
rd /s /q build_mini 2>nul
del /q *.spec 2>nul
del /q build_mini\*.spec 2>nul
echo.
echo ============================================================
echo OK: dist\main\ dist\mini_updater.exe
echo ============================================================
echo.
if /i not "%~1"=="nopause" pause
popd
endlocal
exit /b 0
+96
View File
@@ -0,0 +1,96 @@
; ============================================================
; ServerManager - Inno Setup 安装包脚本
; 用于生成外层安装包、卸载程序,支持静默更新
; ============================================================
; 本地测试包:使用 scripts\package\pack_local_test.bat 或 ISCC /DLOCAL_TEST=1
; 输出到 build\output_local\ServerManager_Setup_LOCAL.exe,独立 AppId,与正式安装并存、不覆盖 build\output\
#ifdef LOCAL_TEST
#define MyAppName "ServerManager (本地测试)"
#define MyAppId "{{B2C3D4E5-F6A7-8901-BCDE-F123456789ABC}"
#define MyOutputDir "..\..\build\output_local"
#define MyOutputBase "ServerManager_Setup_LOCAL"
#else
#define MyAppName "ServerManager"
#define MyAppId "{{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
#define MyOutputDir "..\..\build\output"
#define MyOutputBase "ServerManager_Setup"
#endif
#define MyAppVersion "1.1.3"
#define MyAppExeName "main.exe"
#define MyAppPublisher "ServerManager"
#define MyAppURL "http://your-server.com/"
[Setup]
; 基础信息
AppId={#MyAppId}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
; 输出(正式:build\output\;本地测试:build\output_local\
OutputDir={#MyOutputDir}
OutputBaseFilename={#MyOutputBase}
SetupIconFile=
Compression=lzma2/ultra64
SolidCompression=yes
; 静默安装由命令行参数控制:/VERYSILENT /SUPPRESSMSGBOXES /FORCECLOSEAPPLICATIONS
UninstallDisplayIcon={app}\{#MyAppExeName}
; 控制面板中显示
UninstallDisplayName={#MyAppName} {#MyAppVersion}
; 安装完成后运行主程序
CloseApplications=force
RestartApplications=no
; 静默参数说明:
; /VERYSILENT - 完全静默,不显示安装向导
; /SUPPRESSMSGBOXES - 抑制消息框
; /FORCECLOSEAPPLICATIONS - 强制关闭正在运行的程序
[Languages]
Name: "default"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "创建桌面快捷方式"; GroupDescription: "附加图标:"; Flags: unchecked
Name: "quicklaunchicon"; Description: "创建快速启动栏快捷方式"; GroupDescription: "附加图标:"; Flags: unchecked
[Files]
; 主程序目录模式(src\dist\main\ 含 main.exe 与 _internal 等依赖,无 _MEI 解压,避免 DLL 加载失败)
Source: "..\..\src\dist\main\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; 独立进程外更新器:与 main.exe 同目录
Source: "..\..\src\dist\mini_updater.exe"; DestDir: "{app}"; Flags: ignoreversion
; 版本与更新配置
Source: "..\..\resources\version.json"; DestDir: "{app}"; Flags: ignoreversion
; 默认配置模板(default.kv、sys_*.config.example 等,与 resources\config 同源;build.bat 会同步拷入 dist\main\config
Source: "..\..\resources\config\*"; DestDir: "{app}\config"; Flags: ignoreversion recursesubdirs createallsubdirs
[Dirs]
; 安装后展开软件所需目录:配置与日志
Name: "{app}\config"; Flags: uninsneveruninstall
Name: "{app}\logs"; Flags: uninsneveruninstall
[Icons]
; 开始菜单
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\卸载 {#MyAppName}"; Filename: "{uninstallexe}"
; 桌面(根据用户选择)
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
; 快速启动栏
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon
[Run]
; 安装完成后自动运行主程序(含静默更新后也启动,实现“退出并重启”)
Filename: "{app}\{#MyAppExeName}"; Description: "运行 {#MyAppName}"; Flags: nowait postinstall
[UninstallDelete]
; 删除安装目录下的所有内容(Inno 默认会删除 {app},此处列出以防需要额外清理)
Type: filesandordirs; Name: "{app}"
; 彻底清理应用产生的缓存目录
Type: filesandordirs; Name: "{localappdata}\{#MyAppName}"
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Server Manager - Linux 下用 PyInstaller 打包 main(与 build.bat 的 onedir 主程序对应)
# 用法: chmod +x build_linux.sh && ./build_linux.sh
# 产物: src/dist/main/main (可将该目录加入 PATH,或设置 SERVER_MANAGER_MAIN 供 scripts/run/run.sh 自动调用)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
SRC="$ROOT/src"
RESOURCE_CONFIG="$ROOT/resources/config"
cd "$SRC"
echo "============================================================"
echo " Server Manager - build Linux (PyInstaller onedir)"
echo "============================================================"
echo
if ! command -v python3 >/dev/null 2>&1; then
echo "[ERROR] Need python3"
exit 1
fi
echo "[1/5] pip deps..."
python3 -m pip install -q PyQt6 pyinstaller pymysql Pillow requests
echo "[2/5] icon..."
python3 "$ROOT/resources/icon/convert_icon.py" || true
ICON_ARG=(--icon NONE)
if [[ -f "$ROOT/resources/icon/icon.ico" ]]; then
ICON_ARG=(--icon "$ROOT/resources/icon/icon.ico")
fi
echo "[3/5] clean dist/build..."
rm -rf dist build ./*.spec 2>/dev/null || true
echo "[4/5] PyInstaller main (onedir)..."
# add-data 在 Linux 下用冒号分隔
python3 -m PyInstaller --noconfirm --onedir --name main \
"${ICON_ARG[@]}" \
--add-data "$ROOT/resources/version.json:." \
--add-data "$ROOT/resources/config.json:." \
--add-data "$ROOT/resources/icon:icon" \
--hidden-import PyQt6.QtWidgets \
--hidden-import PyQt6.QtCore \
--hidden-import PyQt6.QtGui \
--hidden-import pymysql \
--hidden-import entrypoints.cli.main \
--hidden-import entrypoints.cli.extended \
--hidden-import entrypoints.gui.bootstrap \
--hidden-import mcp.server \
--hidden-import mcp.tools \
--hidden-import models.app_config \
--hidden-import services.commands \
--hidden-import services.server_creator \
--hidden-import services.hot_update \
--hidden-import services.rg_search \
--hidden-import bsdiff4 \
--collect-all bsdiff4 \
--hidden-import requests \
entrypoints/main.py
echo "[5/5] done."
mkdir -p dist/main/config
if [[ -f config/tool.config ]]; then
cp -f config/tool.config dist/main/config/tool.config || true
fi
if [[ -d "$RESOURCE_CONFIG" ]]; then
cp -a "$RESOURCE_CONFIG/." dist/main/config/
echo " copied resources/config -> dist/main/config"
else
echo " [WARN] resources/config missing"
fi
rm -rf build 2>/dev/null || true
rm -f ./*.spec 2>/dev/null || true
echo
echo "============================================================"
echo "OK: dist/main/main"
echo "CLI: dist/main/main list"
echo "GUI: dist/main/main"
echo "或设置 SERVER_MANAGER_MAIN 指向该文件,配合 scripts/run/run.sh 使用"
echo "============================================================"
echo
+116
View File
@@ -0,0 +1,116 @@
@echo off
chcp 65001 >nul
title Server Manager - 打包便携版
for %%I in ("%~dp0..\..") do set "ROOT=%%~fI"
set "SRC=%ROOT%\src"
pushd "%SRC%" || (
echo [错误] 未找到 src 目录: %SRC%
exit /b 1
)
echo ============================================================
echo Server Manager - 打包便携版 EXE
echo ============================================================
echo.
:: 检查 Python 是否安装
python --version >nul 2>&1
if errorlevel 1 (
echo [错误] 未找到 Python,请先安装 Python 3.8+
pause
exit /b 1
)
echo [1/5] 检查并安装依赖...
pip install PyQt6 pyinstaller Pillow -q
if errorlevel 1 (
echo [错误] 安装依赖失败
pause
exit /b 1
)
echo [2/5] 生成图标文件...
python "%ROOT%\resources\icon\convert_icon.py"
if not exist "%ROOT%\resources\icon\icon.ico" (
echo [警告] 无法生成 ICO 图标,将使用默认图标
set "ICON_PARAM="
) else (
set "ICON_PARAM=--icon %ROOT%\resources\icon\icon.ico"
)
echo [3/5] 清理旧的构建文件...
if exist "dist" rd /s /q dist
if exist "build" rd /s /q build
if exist "main.spec" del /q "main.spec"
echo [4/5] 开始打包(单文件版本)...
pyinstaller --noconfirm --onefile --windowed ^
--name "main" ^
%ICON_PARAM% ^
--add-data "%ROOT%\resources\icon;icon" ^
--hidden-import "PyQt6.QtWidgets" ^
--hidden-import "PyQt6.QtCore" ^
--hidden-import "PyQt6.QtGui" ^
--hidden-import "entrypoints.cli.main" ^
--hidden-import "entrypoints.cli.extended" ^
--hidden-import "entrypoints.gui.bootstrap" ^
--hidden-import "mcp.server" ^
--hidden-import "mcp.tools" ^
--hidden-import "models.app_config" ^
--hidden-import "services.commands" ^
--hidden-import "services.server_creator" ^
--hidden-import "services.hot_update" ^
--hidden-import "services.rg_search" ^
--hidden-import "requests" ^
--hidden-import "bsdiff4" ^
--collect-all "PyQt6" ^
--collect-all "bsdiff4" ^
entrypoints\main.py
if errorlevel 1 (
echo [错误] 打包失败,尝试使用目录模式...
pyinstaller --noconfirm --onedir --windowed ^
--name "main" ^
%ICON_PARAM% ^
--add-data "%ROOT%\resources\icon;icon" ^
--hidden-import "PyQt6.QtWidgets" ^
--hidden-import "PyQt6.QtCore" ^
--hidden-import "PyQt6.QtGui" ^
--hidden-import "entrypoints.cli.main" ^
--hidden-import "entrypoints.cli.extended" ^
--hidden-import "entrypoints.gui.bootstrap" ^
--hidden-import "mcp.server" ^
--hidden-import "mcp.tools" ^
--hidden-import "models.app_config" ^
--hidden-import "services.commands" ^
--hidden-import "services.server_creator" ^
--hidden-import "services.hot_update" ^
--hidden-import "services.rg_search" ^
--hidden-import "requests" ^
--hidden-import "bsdiff4" ^
--collect-all "bsdiff4" ^
entrypoints\main.py
if errorlevel 1 (
echo [错误] 打包失败
pause
exit /b 1
)
)
echo [5/5] 打包完成!
echo.
echo ============================================================
if exist "dist\main.exe" (
echo 打包成功!单文件版本: dist\main.exe
) else if exist "dist\main\main.exe" (
echo 打包成功!目录版本: dist\main\main.exe
)
echo ============================================================
echo.
pause
popd
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Remove generated packaging intermediates after installer builds."""
from __future__ import annotations
import shutil
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def _is_inside_root(path: Path) -> bool:
try:
path.resolve().relative_to(ROOT.resolve())
return True
except ValueError:
return False
def _remove_dir(path: Path) -> None:
if path.exists() and path.is_dir() and _is_inside_root(path):
shutil.rmtree(path, ignore_errors=True)
print(f" removed dir: {path.relative_to(ROOT)}")
def _remove_file(path: Path) -> None:
if path.exists() and path.is_file() and _is_inside_root(path):
try:
path.unlink()
print(f" removed file: {path.relative_to(ROOT)}")
except OSError:
pass
def main() -> int:
print("[cleanup] removing generated packaging intermediates...")
for rel in (
"src/dist",
"src/build",
"src/build_mini",
"build/output/config",
):
_remove_dir(ROOT / rel)
for pycache in ROOT.rglob("__pycache__"):
_remove_dir(pycache)
for pattern in ("*.pyc", "*.pyo", "*.spec"):
for item in ROOT.rglob(pattern):
_remove_file(item)
print("[cleanup] done.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+72
View File
@@ -0,0 +1,72 @@
@echo off
chcp 65001 >nul
setlocal
title Server Manager - pack all to build\output
:: 发版前请先打本地测试包验证: scripts\package\pack_local_test.bat 或 python scripts/package/pack_local_test.py
for %%I in ("%~dp0..\..") do set "ROOT=%%~fI"
set "ISCC=C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
set "OUTPUT_DIR=%ROOT%\build\output"
echo(============================================================
echo( Server Manager - 一键打包
echo( EXE + Inno Setup 安装包
echo(============================================================
echo(
:: Step 1: PyInstaller 打包 main.exe(在 src 目录执行)
echo([Step 1/2] PyInstaller 打包 main.exe ...
echo(
call "%ROOT%\scripts\package\build.bat" nopause
if errorlevel 1 (
echo(
echo([失败] PyInstaller 打包未成功
pause
exit /b 1
)
echo(
:: Step 2: Inno Setup 编译安装包
echo([Step 2/2] Inno Setup 编译安装包 ...
echo(
if not exist "%ISCC%" goto :missing_iscc
"%ISCC%" "%ROOT%\scripts\package\build_installer.iss"
if errorlevel 1 goto :iscc_failed
:: 将正式产物放在 build\output
if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"
copy /y "%ROOT%\resources\version.json" "%OUTPUT_DIR%\version.json" >nul
if not exist "%OUTPUT_DIR%\patches" mkdir "%OUTPUT_DIR%\patches"
echo( 已复制: version.json -> build\output\version.json
echo( 增量包目录: build\output\patches\
echo(
echo([清理] 删除打包中间产物 ...
python "%ROOT%\scripts\package\clean_artifacts.py"
echo(
echo(============================================================
echo(一键打包完成!
echo( 新发布目录:
echo( - build\output\ServerManager_Setup.exe 完整安装包
echo( - build\output\version.json 版本与更新配置
echo( - build\output\patches\*.patch 增量补丁(可选)
echo(============================================================
echo(
pause
exit /b 0
:missing_iscc
echo([错误] 未找到 Inno Setup 编译器: %ISCC%
echo( 请安装 Inno Setup 6 或修改本脚本中的 ISCC 路径
pause
exit /b 1
:iscc_failed
echo(
echo([失败] Inno Setup 编译未成功
pause
exit /b 1
+62
View File
@@ -0,0 +1,62 @@
@echo off
chcp 65001 >nul
setlocal
title Server Manager - 本地测试包(不发版、不覆盖 build\output
for %%I in ("%~dp0..\..") do set "ROOT=%%~fI"
set "ISCC=C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
set "OUTPUT_LOCAL_DIR=%ROOT%\build\output_local"
echo ============================================================
echo Server Manager - 本地测试包
echo build\output_local\ServerManager_Setup_LOCAL.exe
echo 不修改版本号,不覆盖正式目录 build\output\
echo 测试通过后请再执行 scripts\package\release.bat 或 scripts\package\pack_all.bat
echo ============================================================
echo(
echo([Step 1/3] PyInstaller 打包 main + mini_updater ...
call "%ROOT%\scripts\package\build.bat" nopause
if errorlevel 1 (
echo(
echo([失败] PyInstaller 打包未成功
echo(若在本终端乱码/解析失败,请改用 CMD 窗口双击本脚本,或执行: python scripts/package/pack_local_test.py
pause
exit /b 1
)
echo(
echo([Step 2/3] Inno Setup 编译【本地测试】安装包 ^(LOCAL_TEST^) ...
if not exist "%ISCC%" goto :missing_iscc
"%ISCC%" /DLOCAL_TEST=1 "%ROOT%\scripts\package\build_installer.iss"
if errorlevel 1 goto :iscc_failed
echo(
echo([Step 3/3] 复制 version.json 到 build\output_local ...
if not exist "%OUTPUT_LOCAL_DIR%" mkdir "%OUTPUT_LOCAL_DIR%"
copy /y "%ROOT%\resources\version.json" "%OUTPUT_LOCAL_DIR%\version.json" >nul
echo(
echo([清理] 删除打包中间产物 ...
python "%ROOT%\scripts\package\clean_artifacts.py"
echo.
echo ============================================================
echo 本地测试包已生成,请安装验证
echo %OUTPUT_LOCAL_DIR%\ServerManager_Setup_LOCAL.exe
echo 验证通过后,再执行正式发版
echo ============================================================
echo.
pause
exit /b 0
:missing_iscc
echo([错误] 未找到 Inno Setup: %ISCC%
pause
exit /b 1
:iscc_failed
echo([失败] Inno Setup 编译未成功
pause
exit /b 1
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""
本地测试包:与 scripts/package/pack_local_test.bat 相同流程,适合在 PowerShell / CI 中调用。
不修改版本号,输出到 build\output_local\,不覆盖正式 build\output\
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
OUTPUT_LOCAL_DIR = ROOT / "build" / "output_local"
ISCC = os.environ.get(
"INNO_SETUP_ISCC",
r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
)
def main() -> int:
if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
print("用法: python scripts/package/pack_local_test.py")
print("生成 build\\output_local\\ServerManager_Setup_LOCAL.exe,不修改正式 build\\output\\")
return 0
build_script = ROOT / "scripts" / "package" / "build.bat"
if not build_script.exists():
print("错误: 未找到 scripts\\package\\build.bat", file=sys.stderr)
return 1
if not Path(ISCC).is_file():
print(f"错误: 未找到 Inno Setup 编译器: {ISCC}", file=sys.stderr)
print("可设置环境变量 INNO_SETUP_ISCC 指向 ISCC.exe", file=sys.stderr)
return 1
print("[1/3] PyInstaller: scripts\\package\\build.bat nopause ...")
# Use cmd /d /c call ... so exit /b propagates.
r = subprocess.run(
["cmd.exe", "/d", "/c", "call", str(build_script), "nopause"],
cwd=str(ROOT),
shell=False,
)
if r.returncode != 0:
print("[失败] PyInstaller 步骤退出码:", r.returncode, file=sys.stderr)
return r.returncode
print("[2/3] Inno Setup: LOCAL_TEST -> build\\output_local\\ ...")
r = subprocess.run(
[ISCC, "/DLOCAL_TEST=1", str(ROOT / "scripts" / "package" / "build_installer.iss")],
cwd=str(ROOT),
)
if r.returncode != 0:
print("[失败] Inno Setup 退出码:", r.returncode, file=sys.stderr)
return r.returncode
out_local = OUTPUT_LOCAL_DIR
out_local.mkdir(parents=True, exist_ok=True)
vj = ROOT / "resources" / "version.json"
if vj.exists():
shutil.copy2(vj, out_local / "version.json")
print("[3/3] 已复制 version.json -> build\\output_local\\")
cleanup_script = ROOT / "scripts" / "package" / "clean_artifacts.py"
if cleanup_script.exists():
print("[cleanup] 删除打包中间产物 ...")
subprocess.run([sys.executable, str(cleanup_script)], cwd=str(ROOT), check=False)
exe = out_local / "ServerManager_Setup_LOCAL.exe"
print()
print("本地测试包:", exe)
print("验证通过后,再执行正式发版(scripts\\package\\release.bat / scripts\\package\\pack_all.bat + scripts\\package\\release.py)。")
return 0
if __name__ == "__main__":
sys.exit(main())
+84
View File
@@ -0,0 +1,84 @@
@echo off
chcp 65001 >nul
setlocal
for %%I in ("%~dp0..\..") do set "ROOT=%%~fI"
set "ISCC=C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
title Server Manager - 一键发版
echo(============================================================
echo( Server Manager - 一键发版
echo( 顺序:PyInstaller 打包 ^> 改版本号并同步 dist ^> Inno ^> 收尾
echo( 默认全量更新,产物统一输出到 build\output
echo(============================================================
echo( 提示:发版前请先打本地测试包验证 ^(scripts\package\pack_local_test.bat^)
echo(============================================================
echo(
set "NEW_VER=%~1"
set "OUTPUT_DIR=%ROOT%\build\output"
if "%NEW_VER%"=="" (
set /p "NEW_VER=请输入新版本号 (x.y.z,例如 1.0.4): "
)
if "%NEW_VER%"=="" (
echo([错误] 未输入版本号
pause
exit /b 1
)
echo(
echo([阶段 1/4] PyInstaller 打包 main.exe ...
call "%ROOT%\scripts\package\build.bat" nopause
if errorlevel 1 (
echo([失败] PyInstaller 打包未成功
pause
exit /b 1
)
echo(
echo([阶段 2/4] 更新版本号、全量更新配置并同步 version.json 到 dist ...
python "%ROOT%\scripts\package\release.py" "%NEW_VER%"
if errorlevel 1 (
echo([失败] 版本号更新失败
pause
exit /b 1
)
echo(
echo([阶段 3/4] Inno Setup 编译安装包 ...
if not exist "%ISCC%" goto :missing_iscc
"%ISCC%" "%ROOT%\scripts\package\build_installer.iss"
if errorlevel 1 goto :iscc_failed
if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"
if not exist "%OUTPUT_DIR%\patches" mkdir "%OUTPUT_DIR%\patches"
echo(
echo([阶段 4/4] 复制 version.json 至 build\output ...
python "%ROOT%\scripts\package\release.py" --post-build
echo(
echo([清理] 删除打包中间产物 ...
python "%ROOT%\scripts\package\clean_artifacts.py"
echo(
echo(============================================================
echo(一键发版完成!
echo( 新产出目录 build\output\
echo( - ServerManager_Setup.exe 完整安装包
echo( - version.json 版本与更新配置
echo( - patches\*.patch 增量补丁(结构未变时才手动启用)
echo(============================================================
echo(
pause
exit /b 0
:missing_iscc
echo([错误] 未找到 Inno Setup: %ISCC%
pause
exit /b 1
:iscc_failed
echo([失败] Inno Setup 编译未成功
pause
exit /b 1
+277
View File
@@ -0,0 +1,277 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
发版与版本号:推荐顺序见 scripts/package/release.bat — 先 PyInstaller 打包,再更新版本号并同步到 dist,再 Inno Setup,最后 --post-build。
单独用法:
仅改版本号(通常在 build.bat 成功之后): python scripts/package/release.py 1.0.4 [--notes "说明"]
打包完成后: python scripts/package/release.py --post-build [--old-exe 上一版main.exe]
本地测试包: scripts/package/pack_local_test.bat 或 python scripts/package/pack_local_test.py(见 RELEASE.md
"""
import argparse
import hashlib
import json
import re
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
VERSION_JSON = ROOT / "resources" / "version.json"
ROOT_VERSION_JSON = ROOT / "version.json"
OUTPUT_DIR = ROOT / "build" / "output"
PUBLIC_MANIFEST_PATH = "server_manager/build/output/version.json"
PUBLIC_INSTALLER_PATH = "server_manager/build/output/ServerManager_Setup.exe"
def read_text(p: Path) -> str:
return p.read_text(encoding="utf-8")
def write_text(p: Path, s: str) -> None:
p.write_text(s, encoding="utf-8")
def read_version_from_json(p: Path) -> str:
if not p.exists():
return ""
try:
data = json.loads(read_text(p))
except Exception:
return ""
return (data.get("version") or "").strip()
def get_current_version() -> str:
"""读取当前已发布版本,以 build/output/version.json 为准。"""
return read_version_from_json(OUTPUT_DIR / "version.json")
def _set_url_path(url: str, api_kind: str, public_path: str) -> str:
fallback = f"http://172.18.180.94:3000/api/{api_kind}?path={public_path}"
if not url:
return fallback
url = url.strip()
url = url.replace("/api/file?", f"/api/{api_kind}?")
url = url.replace("/api/download?", f"/api/{api_kind}?")
if "path=" in url:
return re.sub(r"path=[^&]+", lambda _m: f"path={public_path}", url, count=1)
return fallback
def ensure_compat_manifest(data: dict) -> dict:
"""补齐客户端更新字段,并固定到 build/output 发布路径。"""
seed = (
data.get("update_url")
or data.get("full_installer_url")
or data.get("download_url")
or ""
)
data["update_url"] = _set_url_path(seed, "file", PUBLIC_MANIFEST_PATH)
full_url = _set_url_path(
data.get("full_installer_url") or data.get("download_url") or seed,
"download",
PUBLIC_INSTALLER_PATH,
)
data["full_installer_url"] = full_url
data["download_url"] = full_url
return data
def write_version_json(data: dict) -> None:
data = ensure_compat_manifest(data)
text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
write_text(VERSION_JSON, text)
write_text(ROOT_VERSION_JSON, text)
def set_version_pre(new_version: str, release_notes: str = None, with_delta: bool = False) -> str:
"""更新版本号并同步 manifest。默认全量更新;显式 --with-delta 才生成 delta 项。"""
vj = VERSION_JSON
if not vj.exists():
print("错误: 未找到 version.json", file=sys.stderr)
sys.exit(1)
data = json.loads(read_text(vj))
old_version = read_version_from_json(OUTPUT_DIR / "version.json")
if with_delta and not old_version:
print("错误: 未找到 build/output/version.json 或其中无 version 字段", file=sys.stderr)
sys.exit(1)
if old_version:
print(f" [i] 旧版本以 build/output/version.json 为准: {old_version}")
# build_installer.iss
iss = ROOT / "scripts" / "package" / "build_installer.iss"
if iss.exists():
t = read_text(iss)
t = re.sub(r'(#define MyAppVersion\s+")[^"]+(")', r'\g<1>' + new_version + r'\g<2>', t)
write_text(iss, t)
print(f" [OK] build_installer.iss -> {new_version}")
# Tk demo/updater examples
for rel in ("src/entrypoints/tk_update_demo.py", "scripts/update/tk_updater.py"):
main_py = ROOT / rel
if not main_py.exists():
continue
t = read_text(main_py)
t = re.sub(r'(LOCAL_VERSION\s*=\s*")[^"]+(")', r'\g<1>' + new_version + r'\g<2>', t)
write_text(main_py, t)
print(f" [OK] {rel} -> LOCAL_VERSION {new_version}")
# version.json
data["version"] = new_version
if release_notes is not None:
data["release_notes"] = release_notes
else:
data["release_notes"] = data.get("release_notes", "") or f"Server Manager {new_version} 更新。"
data = ensure_compat_manifest(data)
full_url = data.get("full_installer_url", "")
if with_delta:
# delta_updates: 只保留 上一版本 -> 当前版本 一项。
# 目录模式下 delta 只替换 main.exe;只有确认依赖目录无需变化时才启用。
patch_path = f"patches/v{old_version}_to_v{new_version}.patch"
if full_url and "build/output/" in full_url:
patch_url = re.sub(r"build/output/[^?]+", "build/output/" + patch_path, full_url)
elif full_url and "output/" in full_url:
patch_url = re.sub(r"output/[^?]+", "build/output/" + patch_path, full_url)
else:
patch_url = full_url.replace("ServerManager_Setup.exe", patch_path) if full_url else ""
data["delta_updates"] = {
old_version: {
"patch_url": patch_url,
"new_exe_sha256": "",
}
}
print(f" [OK] version.json -> version {new_version}, delta_updates[{old_version}] 已添加")
else:
data["delta_updates"] = {}
data.pop("delta", None)
print(f" [OK] version.json -> version {new_version},本次使用全量更新")
write_version_json(data)
print(" [OK] 已同步 version.json -> resources/version.json 与根目录 version.json")
# 若已打过 PyInstaller,把新版本同步进 dist,避免安装包内仍是旧 version.json
for rel in ("src/dist/main/_internal/version.json", "src/dist/main/version.json"):
dest = ROOT / rel
if dest.parent.is_dir():
shutil.copy2(vj, dest)
print(f" [OK] 已同步 version.json -> {dest.relative_to(ROOT)}")
return old_version
def set_version_post(old_exe_path: str = None) -> None:
"""打包完成后:若提供 --old-exe(上一版 main.exe 路径)则生成 patch 并填 new_exe_sha256;否则仅复制 version.json。本地不保存 exe。"""
vj = VERSION_JSON
if not vj.exists():
return
data = json.loads(read_text(vj))
data = ensure_compat_manifest(data)
new_version = (data.get("version") or "").strip()
delta_updates = data.get("delta_updates") or {}
if not isinstance(delta_updates, dict):
return
# 找到需要填 new_exe_sha256 的那一项(上一版本)
old_version = None
for k, v in delta_updates.items():
if isinstance(v, dict) and not (v.get("new_exe_sha256") or "").strip():
old_version = k.strip()
break
if not old_version:
print(" [i] 无需生成增量补丁(new_exe_sha256 已填或无 delta")
write_version_json(data)
_copy_version_to_output()
return
# 支持目录模式 (dist/main/main.exe) 与单文件 (dist/main.exe)
new_exe = ROOT / "src" / "dist" / "main" / "main.exe"
if not new_exe.exists():
new_exe = ROOT / "src" / "dist" / "main.exe"
if not new_exe.exists():
print(" [i] 未找到 src/dist/main.exe 或 src/dist/main/main.exe,跳过增量包")
write_version_json(data)
_copy_version_to_output()
return
# 仅当显式传入上一版 exe 路径时生成 patch(与 Cursor 一致,本地不保存 exe)
old_exe = Path(old_exe_path).resolve() if old_exe_path else None
if not old_exe_path or not old_exe or not old_exe.is_file():
print(f" [i] 未提供上一版 exe(使用 --old-exe 指定路径可生成增量包),仅全量")
write_version_json(data)
_copy_version_to_output()
return
try:
import bsdiff4
except ImportError:
print(" [!] 未安装 bsdiff4,跳过增量包。可执行: pip install bsdiff4", file=sys.stderr)
_copy_version_to_output()
return
patch_dir = OUTPUT_DIR / "patches"
patch_dir.mkdir(parents=True, exist_ok=True)
patch_file = patch_dir / f"v{old_version}_to_v{new_version}.patch"
old_data = old_exe.read_bytes()
new_data = new_exe.read_bytes()
patch_data = bsdiff4.diff(old_data, new_data)
patch_file.write_bytes(patch_data)
print(f" [OK] 已生成: {patch_file}")
new_sha = hashlib.sha256(new_data).hexdigest().lower()
delta_updates[old_version]["new_exe_sha256"] = new_sha
data["delta_updates"] = delta_updates
write_version_json(data)
print(f" [OK] version.json -> delta_updates[\"{old_version}\"].new_exe_sha256 = {new_sha[:16]}...")
_copy_version_to_output()
print(" 发版准备完成。")
def _copy_version_to_output() -> None:
src = VERSION_JSON
dst = OUTPUT_DIR / "version.json"
if src.exists() and dst.parent.exists():
shutil.copy2(src, dst)
print(" [OK] 已复制 version.json -> build/output/")
if src.exists():
shutil.copy2(src, ROOT_VERSION_JSON)
print(" [OK] 已同步 version.json -> 根目录")
def clean_generated_artifacts() -> None:
cleanup_script = ROOT / "scripts" / "package" / "clean_artifacts.py"
if cleanup_script.exists():
subprocess.run([sys.executable, str(cleanup_script)], cwd=str(ROOT), check=False)
def main():
parser = argparse.ArgumentParser(description="一键发版:更新版本号 / 打包后生成增量并填 SHA256")
parser.add_argument("version", nargs="?", help="新版本号,如 1.0.4")
parser.add_argument("--post-build", action="store_true", help="打包完成后执行:复制 version.json;若提供 --old-exe 则生成增量并填 SHA256")
parser.add_argument("--old-exe", default=None, help="上一版 main.exe 路径,用于生成增量 patch(与 Cursor 一致,本地不保存 exe)")
parser.add_argument("--with-delta", action="store_true", help="生成 main.exe 增量项;仅在确认依赖目录无需变化时使用")
parser.add_argument("--notes", "-n", default=None, help="可选:release_notes 更新说明")
args = parser.parse_args()
if args.post_build:
set_version_post(old_exe_path=args.old_exe)
clean_generated_artifacts()
return
if not args.version or not re.match(r"^\d+\.\d+\.\d+$", args.version.strip()):
print("用法: python scripts/package/release.py <版本号> 例如: python scripts/package/release.py 1.0.4", file=sys.stderr)
sys.exit(1)
new_ver = args.version.strip()
old_ver = set_version_pre(new_ver, args.notes, with_delta=args.with_delta)
if args.with_delta:
print(f"\n 当前版本 {old_ver} -> 新版本 {new_ver}。若使用 scripts/package/release.bat,接下来将编译安装包并执行 --post-build。")
else:
print(f"\n 新版本 {new_ver} 将通过 build/output 下的完整安装包更新客户端。")
if __name__ == "__main__":
main()