modify: 规范打包脚本

This commit is contained in:
2026-05-04 20:06:19 +08:00
parent 9da50e182c
commit 425d2f3ff9
10 changed files with 434 additions and 354 deletions
+4 -4
View File
@@ -16,7 +16,7 @@ dependencies = [
"diskcache", "diskcache",
"fastapi[uvicorn]", "fastapi[uvicorn]",
"gunicorn", "gunicorn",
"module-bank==0.2.4", "module-bank==0.3.1",
"pyjwt", "pyjwt",
"python-multipart", "python-multipart",
"sqlite-vfs", "sqlite-vfs",
@@ -26,14 +26,14 @@ dependencies = [
"uvicorn", "uvicorn",
] ]
[[tool.uv.index]]
url = "https://pypi.org/simple"
[[tool.uv.index]] [[tool.uv.index]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple" url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true default = true
[[tool.uv.index]]
url = "https://pypi.org/simple"
[[tool.uv.index]] [[tool.uv.index]]
url = "https://mirrors.ustc.edu.cn/pypi/web/simple/" url = "https://mirrors.ustc.edu.cn/pypi/web/simple/"
+1
View File
@@ -0,0 +1 @@
VITE_API_BASE_URL = "http://localhost:8000"
+1
View File
@@ -0,0 +1 @@
VITE_API_BASE_URL = "/"
+9
View File
@@ -1 +1,10 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
+11 -3
View File
@@ -15,9 +15,17 @@ import { useAuthStore } from './stores/auth'
* 打包时取消该注释 * 打包时取消该注释
*/ */
import { client } from "./api/generated/client.gen" import { client } from "./api/generated/client.gen"
client.setConfig({ // client.setConfig({
baseUrl: "/" // baseUrl: "/"
}) // })
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
if (API_BASE_URL) {
client.setConfig({
baseUrl: API_BASE_URL
})
}
const app = createApp(App) const app = createApp(App)
+17
View File
@@ -1,3 +1,13 @@
# import sys
# used_modules = set()
# def audit_hook(event, args):
# if event == "import":
# used_modules.add(args[0])
# sys.addaudithook(audit_hook)
import time import time
from pathlib import Path from pathlib import Path
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@@ -25,12 +35,14 @@ cache = get_cache()
logger = get_logger("app") logger = get_logger("app")
try: try:
env = config["default"]["env"] env = config["default"]["env"]
except KeyError: # pragma: no cover except KeyError: # pragma: no cover
raise ValueError("配置不存在") raise ValueError("配置不存在")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
""" """
@@ -99,6 +111,11 @@ async def lifespan(app: FastAPI):
await task_service.start(init_tasks_callback) await task_service.start(init_tasks_callback)
yield yield
# if env == "development":
# with open("actually_used_stdlib.txt", "w") as f:
# for name in sorted(used_modules):
# if not name.split('.')[0] in {"chongming", "plugins"}:
# f.write(name + "\n")
await task_service.shutdown(wait=True) await task_service.shutdown(wait=True)
await engine.dispose() await engine.dispose()
+2 -1
View File
@@ -3,6 +3,7 @@
""" """
from typing import Optional from typing import Optional
from uuid import UUID
from fastapi import Request from fastapi import Request
import bcrypt import bcrypt
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -162,7 +163,7 @@ class AuthService:
user_id = validation["user_id"] user_id = validation["user_id"]
# 获取用户 # 获取用户
user = await UserService.get_user_by_id(user_id, session) user = await UserService.get_user_by_id(UUID(user_id), session)
if not user: if not user:
return None return None
+381 -340
View File
@@ -1,456 +1,497 @@
import os """
构建脚本 - 将 chongming 应用打包为可执行文件
功能阶段:
1. 模块打包(module-bank): 加密打包 Python 模块
2. 资源准备: 创建 build 目录, 复制必要文件
3. 混淆与打包: pyarmor 混淆 + PyInstaller/Nuitka 打包
4. 数据库初始化: 创建表结构和默认数据
5. 前端构建: 编译 Vue 项目并打包为 VFS
用法:
python -m chongming.scripts.build [pyinstaller|nuitka]
"""
import platform import platform
import shutil import shutil
import subprocess import subprocess
import sys import sys
import asyncio import asyncio
import time
from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from module_bank import PythonToSQLite from module_bank import PythonToSQLite
from module_bank.encryption import Encryption from module_bank.encryption import Encryption
from sqlite_vfs.core import SQLiteVFS from sqlite_vfs.core import SQLiteVFS
from sqlite_vfs.folder_packer import FolderPacker from sqlite_vfs.folder_packer import FolderPacker
from ..app.core.config import get_config from ..app.core.config import get_config, Config
# ── 常量 ────────────────────────────────────────────────────────────────────
PYINSTALLER = "pyinstaller" PYINSTALLER = "pyinstaller"
NUITKA = "nuitka" NUITKA = "nuitka"
MODES = (PYINSTALLER, NUITKA)
is_windows = platform.system() == "Windows" is_windows = platform.system() == "Windows"
build_mode = PYINSTALLER
if len(sys.argv) == 2:
build_mode = sys.argv[1]
if build_mode not in [PYINSTALLER, NUITKA]: # ── 构建结果数据结构 ─────────────────────────────────────────────────────────
raise ValueError(f"不支持{build_mode}打包模式")
@dataclass
class BuildStepResult:
"""单个构建步骤的执行结果"""
name: str # 步骤名称
success: bool # 是否成功
duration: float # 耗时(秒)
message: str = "" # 附加信息(如包大小)
def build_vue_web(): @dataclass
""" class BuildReport:
打包 Vue Web 前端项目 """构建总结报告"""
mode: str
start_time: float
end_time: float = 0.0
steps: list[BuildStepResult] = field(default_factory=list)
该函数执行以下操作: @property
1. 运行 npm build 命令构建 Vue 项目 def total_duration(self) -> float:
2. 将构建产物打包到 SQLite VFS 虚拟文件系统中 return self.end_time - self.start_time if self.end_time else 0.0
Returns: @property
None: 无返回值,通过打印信息反馈执行结果 def all_success(self) -> bool:
""" return all(s.success for s in self.steps)
# 构建 Vue Web 命令
dist_dir = "./src/chongming-web/dist" def add_step(self, name: str, success: bool, duration: float, message: str = ""):
output_db = "./build/static.svfs" self.steps.append(BuildStepResult(name, success, duration, message))
fs_name = "frontend"
cmd = [ def print_summary(self):
"npm", """打印结构化的构建总结"""
"run", sep = "=" * 60
"build", print(f"\n{sep}")
] print(f" 🏗️ CHONGMING 构建报告")
result = subprocess.run( print(f"{sep}")
print(f" 构建模式 : {self.mode}")
print(f" 总耗时 : {self.total_duration:.2f}s")
print(f" 构建状态 : {'✅ 成功' if self.all_success else '❌ 失败'}")
print(f"{sep}")
print(f" 步骤明细:")
for i, step in enumerate(self.steps, 1):
icon = "✅" if step.success else "❌"
msg = f" ({step.message})" if step.message else ""
print(f" {icon} [{i}/{len(self.steps)}] {step.name:<20s} {step.duration:>7.2f}s{msg}")
print(f"{sep}\n")
# ── 工具函数 ────────────────────────────────────────────────────────────────
def _run_cmd(
cmd: list[str],
cwd: str | None = None,
capture: bool = True,
shell: bool | None = None,
) -> subprocess.CompletedProcess:
"""统一执行外部命令"""
if shell is None:
shell = is_windows
return subprocess.run(
cmd, cmd,
cwd="./src/chongming-web", cwd=cwd,
capture_output=True, capture_output=capture,
text=True, text=True,
shell=is_windows, shell=shell,
encoding="utf-8", encoding="utf-8",
errors="replace", errors="replace",
) )
if result.returncode != 0:
print("打包 Vue Web 失败")
print("标准输出:", result.stdout)
print("错误输出:", result.stderr)
return
else:
print("Vue Web 打包成功")
if result.stdout:
print(result.stdout)
# 检查 dist 目录是否存在 def _format_size(path: str | Path) -> str:
if not os.path.exists(dist_dir): """获取文件可读大小"""
print(f"错误: dist 目录不存在: {dist_dir}") size = Path(path).stat().st_size
return for unit in ("B", "KB", "MB", "GB"):
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024.0
return f"{size:.2f} TB"
vfs = SQLiteVFS(output_db, compress=False) def _ensure_dir(path: str | Path):
packer = FolderPacker( """确保目录存在"""
sqlite_vfs=vfs, Path(path).mkdir(parents=True, exist_ok=True)
exclude_patterns=[],
fs_name=fs_name, def _resolve_mode() -> str:
"""解析构建模式"""
mode = PYINSTALLER
if len(sys.argv) >= 2:
mode = sys.argv[1].lower()
if mode not in MODES:
print(f" ❌ 不支持的构建模式: {mode},可选: {', '.join(MODES)}")
sys.exit(1)
return mode
# ── 阶段 1: 模块打包 (module-bank) ──────────────────────────────────────────
def _stage_pack_modules(config: Config) -> str:
"""
使用 module-bank 加密打包 Python 模块为 .mbank 文件。
返回打包过程中的提示信息。
"""
_ensure_dir("resources")
mbank_list = config.get("production", {}).get("module_system", {}).get("path", [])
if not mbank_list:
return "⚠️ 未配置 module_system.path,跳过模块打包"
messages = []
for output_path, source_dir, _ in mbank_list:
key = Encryption.generate_key()
packer = PythonToSQLite(
output_path,
key,
pyarmor_obfuscate=True,
) )
packer.pack_directory(source_dir, "app" if "app" in source_dir else "plugins")
packer.verify_package_structure()
packer.packer_key(key)
packer.delete_source_code(None)
size = _format_size(output_path)
messages.append(f"{Path(output_path).name} ({size})")
return " - " + "\n - ".join(messages)
try: def _stage_prepare_build_dir(config: Config, mode: str) -> str:
packer.pack_folder(dist_dir) """准备 build 目录并复制必要文件"""
print(f"✅ 打包完成,文件保存为 {output_db}") build_path = Path("build")
finally: if build_path.exists():
vfs.close() shutil.rmtree(str(build_path))
_ensure_dir("build")
# 复制 utils
utils_src = Path("public/utils")
if utils_src.exists():
shutil.copytree(utils_src, build_path / "utils")
def run_pyarmor_obfuscate(): # 复制 resources
""" resources_src = Path("resources")
使用 PyArmor 对关键 Python 文件进行代码混淆 if resources_src.exists():
shutil.copytree(resources_src, build_path / "resources")
该函数遍历预定义的文件列表,对每个文件执行 pyarmor gen 命令进行混淆处理, # 复制核心文件
以保护源代码不被轻易反编译。 for f in ("main.py", "server.py", "config.toml"):
shutil.copy(f"public/{f}", f"build/{f}")
处理的文件包括: # 注入 JWT 密钥
- main.py (主入口文件) jwt_key = Encryption.generate_key()
- server.py (服务器文件) cfg_path = build_path / "config.toml"
- utils/launch.py (启动工具) content = cfg_path.read_text(encoding="utf-8")
- utils/config.py (配置工具) content = content.replace("your-secret-key-change-in-production", jwt_key)
cfg_path.write_text(content, encoding="utf-8")
Returns: return "资源准备完成"
None: 无返回值,遇到错误时提前返回
""" # ── 阶段 3: PyArmor 混淆 ───────────────────────────────────────────────────
# pyarmor obfuscate --output=obfuscated_script.py original_script.py
obfuscate_files = [ def _stage_pyarmor_obfuscate() -> str:
"""对关键脚本执行 PyArmor 混淆"""
files = [
("main.py", "./"), ("main.py", "./"),
("server.py", "./"), ("server.py", "./"),
("utils/launch.py", "./utils"), ("utils/launch.py", "./utils"),
("utils/config.py", "./utils"), ("utils/config.py", "./utils"),
] ]
for file, output in obfuscate_files: failed = []
cmd = [ for filename, output in files:
"pyarmor", result = _run_cmd(
"gen", ["pyarmor", "gen", f"--output={output}", filename],
f"--output={output}", cwd="build",
file, )
]
result = subprocess.run(cmd, cwd="./build", capture_output=True, text=True)
if result.returncode != 0: if result.returncode != 0:
print(f"打包文件: {file} 失败") failed.append(filename)
print(result.stderr) if failed:
return return f"❌ 混淆失败: {', '.join(failed)}"
else: return "✅ 所有文件混淆成功"
print(result.stdout)
print(f"打包文件: {file}")
def run_nuitka(): # ── 阶段 4: PyInstaller 打包 ────────────────────────────────────────────────
"""
兼容旧版 Nuitka 的打包函数(不使用任何可能不支持的参数)
编译后手动 strip + upx 压缩
"""
import shutil
config = get_config() def _stage_pyinstaller(config: Config) -> tuple[str, str]:
build_import_base = config \ """执行 PyInstaller 打包,返回 (状态消息, 可执行文件路径)"""
.get("pyinstaller_build_import", {}) \ base_imports = config.get("pyinstaller_build_import", {})
.get("base", []) build_imports = list(base_imports.get("base", []))
linux_extra = config \ build_imports.extend(
.get("pyinstaller_build_import", {}) \ base_imports.get("windows", []) if is_windows
.get("linux", []) else base_imports.get("linux", [])
windows_extra = config \ )
.get("pyinstaller_build_import", {}) \
.get("windows", [])
all_imports = list(build_import_base) icon = Path("../../public/chongming.ico")
cmd = [
sys.executable, "-m", "PyInstaller",
"--onefile", "--noconfirm",
"--optimize", "2", "--strip",
"--name", "chongming",
"--distpath", ".",
"--workpath", "./build_temp",
"--specpath", "./specs",
"--clean",
]
if icon.exists():
cmd.extend(["--icon", str(icon)])
for imp in build_imports:
cmd.extend(["--hidden-import", imp])
cmd.append("main.py")
result = _run_cmd(cmd, cwd="build", capture=True)
if result.returncode != 0:
return f"❌ PyInstaller 失败", ""
exe_path = Path("build/chongming")
if is_windows: if is_windows:
all_imports.extend(windows_extra) exe_path = exe_path.with_suffix(".exe")
else:
all_imports.extend(linux_extra) suffix = ".exe" if is_windows else ""
exe = Path("build") / f"chongming{suffix}"
if exe.exists():
size = _format_size(exe)
return f"✅ PyInstaller 打包成功 ({size})", str(exe)
return "✅ PyInstaller 打包完成 (但未找到可执行文件)", ""
# ── 阶段 5: Nuitka 打包 ────────────────────────────────────────────────────
def _stage_nuitka(config: Config) -> tuple[str, str]:
"""执行 Nuitka 打包,返回 (状态消息, 可执行文件路径)"""
base_imports = config.get("pyinstaller_build_import", {}).get("base", [])
extra = config.get("pyinstaller_build_import", {}).get(
"windows" if is_windows else "linux", []
)
packages = [ packages = [
"module_bank", "sqlite_vfs", "fastapi", "uvicorn", "gunicorn", "module_bank", "sqlite_vfs", "fastapi", "uvicorn", "gunicorn",
"diskcache", "bcrypt", "apscheduler", "aiofiles", "aiosqlite", "diskcache", "bcrypt", "apscheduler", "aiofiles", "aiosqlite",
"sqlmodel", "jwt", "python_multipart", "tomli", "typing_extensions" "sqlmodel", "jwt", "python_multipart", "tomli", "typing_extensions",
] ]
# 最基础的参数(兼容所有 Nuitka 版本)
cmd = [ cmd = [
sys.executable, "-m", "nuitka", sys.executable, "-m", "nuitka",
"--standalone", "--standalone", "--onefile",
"--onefile",
"--follow-imports", "--follow-imports",
"--static-libpython=no", "--static-libpython=no",
"--remove-output", "--remove-output",
"--assume-yes-for-downloads", "--assume-yes-for-downloads",
"--lto=yes",
"--disable-plugin=tk-inter",
"--disable-plugin=numpy",
"--output-dir=.", "--output-filename=chongming",
"main.py",
"--jobs=2",
] ]
# 尝试添加 LTO(如果不支持,会在此阶段失败,我们在下方会用 try/except 回退)
cmd.append("--lto=yes")
# 尝试启用 UPX 插件(旧版可能不支持)
if shutil.which("upx"): if shutil.which("upx"):
cmd.append("--enable-plugin=upx") cmd.append("--enable-plugin=upx")
for pkg in packages: for pkg in packages:
cmd.append(f"--include-package={pkg}") cmd.append(f"--include-package={pkg}")
for mod in list(base_imports) + extra:
for mod in all_imports:
cmd.append(f"--include-module={mod}") cmd.append(f"--include-module={mod}")
cmd.append("--disable-plugin=tk-inter")
cmd.append("--disable-plugin=numpy")
cmd += ["--output-dir=.", "--output-filename=chongming", "main.py"]
cmd.append("--jobs=2")
if is_windows: if is_windows:
icon_path = Path("../../public/chongming.ico") icon = Path("../../public/chongming.ico")
if icon_path.exists(): if icon.exists():
cmd.append(f"--windows-icon-from-ico={icon_path}") cmd.append(f"--windows-icon-from-ico={icon}")
print("正在运行 Nuitka 编译...")
try: try:
subprocess.run(cmd, cwd="./build", check=True, capture_output=False, text=True) _run_cmd(cmd, cwd="build", capture=False)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
# 如果因为 --lto=yes 或 --enable-plugin=upx 失败,则用更基础的命令重试 # 降级重试(移除可能不兼容的参数)
print("检测到参数不支持,移除 --lto=yes 和 --enable-plugin=upx 重新编译...") fallback = [
fallback_cmd = [arg for arg in cmd if arg not in ("--lto=yes", "--enable-plugin=upx")] arg for arg in cmd
subprocess.run(fallback_cmd, cwd="./build", check=True, capture_output=False, text=True) if arg not in ("--lto=yes", "--enable-plugin=upx")
print("Nuitka 编译完成!开始后处理瘦身...")
exe_path = Path("./build/chongming")
if not is_windows and exe_path.exists():
# 手动 strip 去掉符号表
subprocess.run(["strip", str(exe_path)], check=False)
print("已执行 strip,去除调试符号")
if shutil.which("upx") and exe_path.exists():
# 手动 UPX 极限压缩
subprocess.run(["upx", "--best", str(exe_path)], check=False)
print("已执行 UPX 压缩")
# 清理临时入口文件
shutil.rmtree("build/utils", ignore_errors=True)
os.remove("build/main.py")
os.remove("build/server.py")
def run_pyinstaller():
"""
使用 PyInstaller 将应用打包成单文件可执行程序
该函数根据操作系统平台(Windows/非Windows)构建不同的 PyInstaller 命令,
包含所有必要的隐藏导入以确保依赖库被正确打包。
Windows 和非 Windows 平台的主要区别:
- 非 Windows 平台额外包含 gunicorn 相关模块
Returns:
None: 无返回值,通过打印信息反馈执行结果
"""
config = get_config()
try:
# 构建 PyInstaller 命令
build_import_base = config \
.get("pyinstaller_build_import", {}) \
.get("base", [])
cmd = [
sys.executable,
"-m",
"PyInstaller",
"--onefile",
"--noconfirm",
"--optimize",
"2",
"--strip",
"--name",
"chongming",
"--distpath",
".",
"--workpath",
"./build_temp",
"--specpath",
"./specs",
"--clean",
# 其他选项
"--icon",
"../../public/chongming.ico",
"main.py",
] ]
if is_windows: _run_cmd(fallback, cwd="build", capture=False)
windows_build_import = config \
.get("pyinstaller_build_import", {}) \
.get("windows", []) + build_import_base
for build_import in windows_build_import: exe = Path("build/chongming")
cmd.append("--hidden-import") if exe.exists():
cmd.append(build_import) # 后处理瘦身
else: if not is_windows:
linux_build_import = config \ subprocess.run(["strip", str(exe)], check=False)
.get("pyinstaller_build_import", {}) \ if shutil.which("upx"):
.get("linux", []) + build_import_base subprocess.run(["upx", "--best", str(exe)], check=False)
for build_import in linux_build_import: size = _format_size(exe)
cmd.append("--hidden-import") return f"✅ Nuitka 编译完成 ({size})", str(exe)
cmd.append(build_import)
print(f"正在运行 PyInstaller: {' '.join(cmd)}") return "❌ Nuitka 编译失败 (未生成可执行文件)", ""
result = subprocess.run(cmd, cwd="./build", capture_output=True, text=True)
if result.returncode == 0: # ── 阶段 6: 清理临时文件 ────────────────────────────────────────────────────
print("PyInstaller 打包成功!")
print(result.stdout)
else:
print("PyInstaller 打包失败!")
print("错误信息:", result.stderr)
except FileNotFoundError: def _stage_cleanup(mode: str) -> str:
print("错误:未找到 PyInstaller,请先安装 PyInstaller:pip install pyinstaller") """清理构建过程中的临时文件"""
except Exception as e: build = Path("build")
print(f"PyInstaller 执行出错: {e}") dirs_to_remove = [
"build_temp", "specs", "utils",
"pyarmor_runtime_000000",
"__pycache__",
]
files_to_remove = ["main.py", "server.py"]
for d in dirs_to_remove:
target = build / d
if target.exists():
shutil.rmtree(str(target), ignore_errors=True)
async def init_database(): for f in files_to_remove:
""" target = build / f
异步初始化数据库 if target.exists():
target.unlink(missing_ok=True)
该函数执行以下操作: return "临时文件已清理"
1. 从配置中读取数据库连接信息
2. 创建异步数据库引擎和会话工厂
3. 设置 SQLite 优化参数(WAL 模式和同步策略)
4. 创建所有 SQLModel 定义的数据库表
5. 初始化权限系统和默认管理员账户
Raises: # ── 阶段 7: 数据库初始化 ────────────────────────────────────────────────────
ValueError: 当数据库配置不存在时抛出
OperationalError: 当数据库操作出现异常时抛出(已存在表的情况除外)
Returns: async def _stage_init_database():
None: 无返回值 """初始化数据库表结构和默认数据"""
"""
from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel import SQLModel, text from sqlmodel import SQLModel, text
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from ..app.task.dev_init_db import (
from ..app.task.dev_init_db import init_permission, init_default_roles, dev_init_admin init_permission,
init_default_roles,
dev_init_admin,
)
database = get_config()["database"] database = get_config()["database"]
database_type = database["type"] db_type = database["type"]
database_config = database.get(database_type, None) db_config = database.get(db_type, {})
if database_config is None: if db_config is None:
raise ValueError("配置不存在") return "❌ 数据库配置不存在"
os.makedirs("./build/data", exist_ok=True)
_ensure_dir("build/data")
engine = create_async_engine( engine = create_async_engine(
"sqlite+aiosqlite:///./build/data/database.db", **database_config "sqlite+aiosqlite:///./build/data/database.db",
) **db_config,
async_session_maker = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
) )
session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with engine.begin() as conn: async with engine.begin() as conn:
if database_type == "sqlite": if db_type == "sqlite":
await conn.execute(text("PRAGMA journal_mode=WAL")) await conn.execute(text("PRAGMA journal_mode=WAL"))
await conn.execute(text("PRAGMA synchronous=NORMAL")) await conn.execute(text("PRAGMA synchronous=NORMAL"))
try: try:
await conn.run_sync( await conn.run_sync(
lambda conn: SQLModel.metadata.create_all(bind=conn, checkfirst=True) lambda c: SQLModel.metadata.create_all(bind=c, checkfirst=True)
) )
except OperationalError as e: except OperationalError as e:
if "already exists" in str(e): if "already exists" not in str(e):
raise raise
# 添加管理员用户
async with async_session_maker() as session: async with session_maker() as session:
# await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
await init_permission(session) await init_permission(session)
# 再初始化默认角色及权限关联
await init_default_roles(session) await init_default_roles(session)
# 最后初始化管理员用户
await dev_init_admin(session) await dev_init_admin(session)
db_size = _format_size("build/data/database.db")
return f"✅ 数据库初始化完成 ({db_size})"
# ── 阶段 8: Vue 前端构建 ────────────────────────────────────────────────────
def _stage_build_vue() -> str:
"""构建 Vue 前端并打包为 SQLite VFS"""
dist_dir = "./src/chongming-web/dist"
output_db = "./build/static.svfs"
fs_name = "frontend"
# 1. npm build
result = _run_cmd(
["npm", "run", "build"],
cwd="./src/chongming-web",
capture=True,
)
if result.returncode != 0:
err = (result.stderr or "").strip()[:200]
return f"❌ Vue 构建失败: {err}"
# 2. 检查产物
if not Path(dist_dir).exists():
return "❌ Vue 构建成功但未找到 dist 目录"
# 3. 打包为 VFS
vfs = SQLiteVFS(output_db, compress=False)
packer = FolderPacker(sqlite_vfs=vfs, exclude_patterns=[], fs_name=fs_name)
try:
packer.pack_folder(dist_dir)
finally:
vfs.close()
size = _format_size(output_db)
return f"✅ Vue 打包完成 ({size})"
# ── 主流程 ──────────────────────────────────────────────────────────────────
async def main(): async def main():
""" """规范化构建主流程"""
主构建流程函数 mode = _resolve_mode()
report = BuildReport(mode=mode, start_time=time.time())
config = get_config()
该函数协调整个应用的构建过程,按顺序执行以下步骤: async def run_step(name: str, fn, *args, **kwargs) -> str:
1. 加密打包 Python 模块(app 和 plugins)到 SQLite 数据库文件 """执行一个构建步骤并记录结果(支持同步和异步函数)"""
2. 创建并准备 build 目录结构 start = time.time()
3. 复制必要的源文件和配置文件到 build 目录 try:
4. 注入动态生成的加密密钥到配置文件 result = fn(*args, **kwargs)
5. 对关键文件进行代码混淆 if asyncio.iscoroutine(result):
6. 使用 PyInstaller 打包成可执行文件 msg = await result
7. 清理临时构建文件 else:
8. 初始化数据库 msg = result
9. 构建 Vue 前端项目 elapsed = time.time() - start
report.add_step(name, success=True, duration=elapsed, message=msg or "")
print(f" ✅ {name:<25s} {elapsed:>7.2f}s {msg}")
return msg
except Exception as e:
elapsed = time.time() - start
err_msg = str(e).strip()[:200]
report.add_step(name, success=False, duration=elapsed, message=err_msg)
print(f" ❌ {name:<25s} {elapsed:>7.2f}s {err_msg}")
return ""
Returns: print("\n" + "=" * 60)
None: 无返回值 print(" 🏗️ CHONGMING 构建开始")
""" print(f" 模式: {mode} | 平台: {platform.system()}")
# 打包模块 print("=" * 60 + "\n")
resources_path = Path("resources")
if not resources_path.exists():
resources_path.mkdir()
mbank_list = get_config()["production"].get("module_system", {}).get("path", []) # 阶段 1: 模块打包
for mbank in mbank_list: await run_step("模块打包(module-bank)", _stage_pack_modules, config)
key = Encryption.generate_key()
app_packer = PythonToSQLite(mbank[0], key)
app_packer.pack_directory(mbank[1], mbank[2])
app_packer.verify_package_structure()
app_packer.packer_key(key)
app_packer.delete_source_code(None)
# 创建 build 目录 # 阶段 2: 准备构建目录
build_path = Path("build") await run_step("准备构建目录", _stage_prepare_build_dir, config, mode)
if build_path.exists() and build_path.is_dir():
shutil.rmtree("build")
os.makedirs("build", exist_ok=True) # 阶段 3+4: 混淆 + 打包
if mode == PYINSTALLER:
await run_step("PyArmor 混淆", _stage_pyarmor_obfuscate)
start = time.time()
try:
msg, exe_path = _stage_pyinstaller(config)
elapsed = time.time() - start
success = "❌" not in msg
report.add_step("PyInstaller 打包", success, elapsed, msg.replace("✅ PyInstaller ", ""))
print(f" {'✅' if success else '❌'} PyInstaller 打包{' ' * 12} {elapsed:>7.2f}s {msg}")
except Exception as e:
elapsed = time.time() - start
report.add_step("PyInstaller 打包", False, elapsed, str(e)[:200])
print(f" ❌ PyInstaller 打包 {elapsed:>7.2f}s {str(e)[:200]}")
else:
await run_step("Nuitka 编译", lambda: _stage_nuitka(config)[0])
# 复制 utils 文件夹到 build 文件夹 # 阶段 5: 清理
utils_path = Path("utils") await run_step("清理临时文件", _stage_cleanup, mode)
if utils_path.exists():
target_utils_path = Path("build") / "utils"
shutil.copytree(utils_path, target_utils_path, dirs_exist_ok=True)
print(f"已复制 utils 文件夹到 {target_utils_path}")
# 复制 resources 文件夹到 build 文件夹 # 阶段 6: 数据库初始化
resources_path = Path("resources") await run_step("初始化数据库", _stage_init_database)
if resources_path.exists():
target_resources_path = Path("build") / "resources"
shutil.copytree(resources_path, target_resources_path, dirs_exist_ok=True)
print(f"已复制 resources 文件夹到 {target_resources_path}")
print("模块打包完成") # 阶段 7: Vue 前端构建
await run_step("Vue 前端构建", _stage_build_vue)
# 复制 main.py 到 build 文件夹 # 总结
shutil.copy("public/main.py", "build") report.end_time = time.time()
shutil.copy("public/server.py", "build") report.print_summary()
# 复制 config.toml 到 build 文件夹
shutil.copy("public/config.toml", "build")
# 复制 utils 文件夹到 build 文件夹
shutil.copytree("public/utils", "build/utils")
jwt_key = Encryption.generate_key() if not report.all_success:
with open("./build/config.toml", "r", encoding="utf-8") as f: sys.exit(1)
context = f.read()
context = context.replace("your-secret-key-change-in-production", jwt_key)
with open("build/config.toml", "w", encoding="utf-8") as f:
f.write(context)
if build_mode == PYINSTALLER:
# 混淆
run_pyarmor_obfuscate()
# 运行 PyInstaller
run_pyinstaller()
# 清理临时文件
shutil.rmtree(r"build/build_temp", ignore_errors=True)
shutil.rmtree(r"build/specs", ignore_errors=True)
shutil.rmtree(r"build/utils", ignore_errors=True)
shutil.rmtree(r"build/pyarmor_runtime_000000", ignore_errors=True)
os.remove(r"build/main.py")
os.remove(r"build/server.py")
elif build_mode == NUITKA:
run_nuitka()
# Nuitka 编译后清理临时文件
shutil.rmtree(r"build/utils", ignore_errors=True)
# os.remove(r"build/main.py")
# os.remove(r"build/server.py")
print("构建完成")
# 初始化数据库
await init_database()
# 构建 vue 项目
build_vue_web()
def build(): def build():
""" """构建入口"""
构建入口函数
该函数作为整个构建流程的入口点,通过 asyncio.run() 启动异步主构建流程。
Returns:
None: 无返回值
"""
asyncio.run(main()) asyncio.run(main())
if __name__ == "__main__":
build()
+2
View File
@@ -28,6 +28,8 @@ def generate_markdown_from_py_files(directory, output_file):
file_path = os.path.join(root, file) file_path = os.path.join(root, file)
if file == "output.md" or file == "package-lock.json": if file == "output.md" or file == "package-lock.json":
continue continue
if "generated" in file_path:
continue
md_file.write(f"`{file_path}`\n") md_file.write(f"`{file_path}`\n")
md_file.write("```\n") md_file.write("```\n")
with open(file_path, "r", encoding="utf-8") as py_file: with open(file_path, "r", encoding="utf-8") as py_file:
Generated
+4 -4
View File
@@ -471,7 +471,7 @@ requires-dist = [
{ name = "diskcache" }, { name = "diskcache" },
{ name = "fastapi", extras = ["uvicorn"] }, { name = "fastapi", extras = ["uvicorn"] },
{ name = "gunicorn" }, { name = "gunicorn" },
{ name = "module-bank", specifier = "==0.2.4" }, { name = "module-bank", specifier = "==0.3.1" },
{ name = "pyjwt" }, { name = "pyjwt" },
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "sqlite-vfs" }, { name = "sqlite-vfs" },
@@ -938,14 +938,14 @@ wheels = [
[[package]] [[package]]
name = "module-bank" name = "module-bank"
version = "0.2.4" version = "0.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cryptography" }, { name = "cryptography" },
{ name = "pyarmor" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/d3/e7/417d918069be3bcf74f2e37f93195ebd4e9e975e1ec690dcd04b902a8d74/module_bank-0.2.4.tar.gz", hash = "sha256:074cfed70a391762d5a2a560b581826fb599d63bee899c46ab03d3f221acdccf", size = 68693, upload-time = "2026-05-01T15:32:24.387Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/59/ba/480feb93002b8b0139c61357f4cea4c75fb478b5e4095f1ad2421e83b033/module_bank-0.2.4-py3-none-any.whl", hash = "sha256:255ac8b5378b2c3073eb3fe805c67e9ce759352d407deebf11bfa1277eceb62a", size = 12415, upload-time = "2026-05-01T15:32:22.081Z" }, { url = "https://files.pythonhosted.org/packages/31/f8/81015ad652132bccc33089c81e6f45ff8537e68cffefe789d213f7d45ca5/module_bank-0.3.1-py3-none-any.whl", hash = "sha256:29f32ec55b4effe5ccd8e6414e80e92a1c8584cfe90f731b3677e389c3af448b", size = 19037, upload-time = "2026-05-04T11:58:33.114Z" },
] ]
[[package]] [[package]]