# build.py - 构建脚本(重构后,逻辑保持不变) import platform import os import shutil import subprocess import sys import asyncio from pathlib import Path from module_bank import PythonToSQLite from module_bank.encryption import Encryption from sqlite_vfs.core import SQLiteVFS from sqlite_vfs.folder_packer import FolderPacker is_windows = platform.system() == "Windows" # --------------------------------------------------------------------------- # 前端构建 # --------------------------------------------------------------------------- def build_vue_web(): """构建 Vue 前端并打包为 static.svfs""" dist_dir = "./src/chongming-web/dist" output_db = "./build/static.svfs" fs_name = "frontend" # 1. 执行 npm run build cmd = ["npm", "run", "build"] result = subprocess.run( cmd, cwd="./src/chongming-web", capture_output=True, text=True, shell=is_windows, encoding="utf-8", 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) # 2. 检查 dist 目录 if not os.path.exists(dist_dir): print(f"错误: dist 目录不存在: {dist_dir}") return # 3. 打包到 svfs vfs = SQLiteVFS(output_db, compress=False) packer = FolderPacker( sqlite_vfs=vfs, exclude_patterns=[], fs_name=fs_name, ) try: packer.pack_folder(dist_dir) print(f"✅ 打包完成,文件保存为 {output_db}") finally: vfs.close() # --------------------------------------------------------------------------- # PyArmor 混淆 # --------------------------------------------------------------------------- def run_pyarmor_obfuscate(): """对关键文件执行 PyArmor 混淆""" obfuscate_files = [ ("main.py", "./"), ("server.py", "./"), ("utils/launch.py", "./utils"), ("utils/config.py", "./utils"), ] for file, output in obfuscate_files: cmd = [ "pyarmor", "gen", f"--output={output}", file, ] result = subprocess.run(cmd, cwd="./build", capture_output=True, text=True) if result.returncode != 0: print(f"打包文件: {file} 失败") print(result.stderr) return else: print(result.stdout) print(f"打包文件: {file}") # --------------------------------------------------------------------------- # PyInstaller 打包 # --------------------------------------------------------------------------- def run_pyinstaller(): """使用 PyInstaller 打包成可执行文件""" try: if is_windows: cmd = [ sys.executable, "-m", "PyInstaller", "--onefile", "--name", "chongming", "--distpath", ".", "--workpath", "./build_temp", "--specpath", "./specs", "--clean", "--hidden-import", "utils", "--hidden-import", "utils.launch", "--hidden-import", "utils.config", "--hidden-import", "server", "--hidden-import", "module_bank", "--hidden-import", "fastapi", "--hidden-import", "fastapi.staticfiles", "--hidden-import", "fastapi.middleware.cors", "--hidden-import", "uvicorn", "--hidden-import", "sqlmodel", "--hidden-import", "aiosqlite", "--hidden-import", "aiofiles", "--hidden-import", "apscheduler", "--hidden-import", "diskcache", "--hidden-import", "passlib", "--hidden-import", "passlib.context", "--hidden-import", "passlib.handlers.bcrypt", "--hidden-import", "sqlite_vfs", "--hidden-import", "python_multipart", "--hidden-import", "sqlite_vfs.core", "--hidden-import", "jwt", "--icon", "../../public/chongming.ico", "main.py", ] else: cmd = [ sys.executable, "-m", "PyInstaller", "--onefile", "--name", "chongming", "--distpath", ".", "--workpath", "./build_temp", "--specpath", "./specs", "--clean", "--hidden-import", "utils", "--hidden-import", "utils.launch", "--hidden-import", "utils.config", "--hidden-import", "server", "--hidden-import", "module_bank", "--hidden-import", "fastapi", "--hidden-import", "fastapi.staticfiles", "--hidden-import", "fastapi.middleware.cors", "--hidden-import", "uvicorn", "--hidden-import", "sqlmodel", "--hidden-import", "aiosqlite", "--hidden-import", "aiofiles", "--hidden-import", "apscheduler", "--hidden-import", "diskcache", "--hidden-import", "passlib", "--hidden-import", "passlib.context", "--hidden-import", "passlib.handlers.bcrypt", "--hidden-import", "sqlite_vfs", "--hidden-import", "python_multipart", "--hidden-import", "sqlite_vfs.core", "--hidden-import", "jwt", # gunicorn 相关 "--hidden-import", "gunicorn.glogging", "--hidden-import", "gunicorn.app", "--hidden-import", "gunicorn.app.wsgiapp", "--icon", "../../public/chongming.ico", "main.py", ] print(f"正在运行 PyInstaller: {' '.join(cmd)}") result = subprocess.run(cmd, cwd="./build", capture_output=True, text=True) if result.returncode == 0: print("PyInstaller 打包成功!") print(result.stdout) else: print("PyInstaller 打包失败!") print("错误信息:", result.stderr) except FileNotFoundError: print("错误:未找到 PyInstaller,请先安装 PyInstaller:pip install pyinstaller") except Exception as e: print(f"PyInstaller 执行出错: {e}") # --------------------------------------------------------------------------- # 数据库初始化 # --------------------------------------------------------------------------- async def init_database(): """构建环境下的数据库初始化(创建表、管理员、权限)""" from sqlalchemy.ext.asyncio import ( create_async_engine, AsyncSession, async_sessionmaker, ) from sqlmodel import SQLModel, text from sqlalchemy.exc import OperationalError from ..app.core.config import get_config from ..app.task.dev_init_db import init_permission, dev_init_admin database = get_config()["database"] database_type = database["type"] database_config = database.get(database_type, None) if database_config is None: raise ValueError("配置不存在") engine = create_async_engine( "sqlite+aiosqlite:///./build/database.db", **database_config ) async_session_maker = async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False ) async with engine.begin() as conn: if database_type == "sqlite": await conn.execute(text("PRAGMA journal_mode=WAL")) await conn.execute(text("PRAGMA synchronous=NORMAL")) try: await conn.run_sync( lambda conn: SQLModel.metadata.create_all(bind=conn, checkfirst=True) ) except OperationalError as e: if "already exists" in str(e): raise async with async_session_maker() as session: await init_permission(session) await dev_init_admin(session) # --------------------------------------------------------------------------- # 构建主流程 # --------------------------------------------------------------------------- async def main(): # ======================================================== # 1. 生成密钥并创建 resources 目录 # ======================================================== key = Encryption.generate_key() resources_path = Path("resources") resources_path.mkdir(exist_ok=True) # 2. 打包 Python 模块到 .mbank app_packer = PythonToSQLite("./resources/app.mbank", key) app_packer.pack_directory("src/chongming/app", "app") app_packer.verify_package_structure() app_packer.delete_source_code(None) plugin_packer = PythonToSQLite("./resources/plugins.mbank", key) plugin_packer.pack_directory("src/plugins", "plugins") plugin_packer.verify_package_structure() plugin_packer.delete_source_code(None) # 3. 准备 build 目录 if Path("build").exists() and Path("build").is_dir(): shutil.rmtree("build") os.makedirs("build", exist_ok=True) # 复制 resources 到 build shutil.copytree(resources_path, Path("build") / "resources", dirs_exist_ok=True) print(f"已复制 resources 文件夹到 build/resources") # 复制 public 中的文件到 build shutil.copy("public/main.py", "build") shutil.copy("public/server.py", "build") shutil.copy("public/config.toml", "build") # 复制 utils(先复制 public/utils,后续会处理 launch.py 中的 key) utils_source = Path("public/utils") if utils_source.exists(): shutil.copytree(utils_source, Path("build") / "utils", dirs_exist_ok=True) print(f"已复制 utils 文件夹到 build/utils") print("模块打包完成") # 4. 替换密钥 launch_file = Path("./build/utils/launch.py") launch_content = launch_file.read_text(encoding="utf-8") launch_content = launch_content.replace("key = None", f"key = '{key}'") launch_file.write_text(launch_content, encoding="utf-8") jwt_key = Encryption.generate_key() config_file = Path("./build/config.toml") config_content = config_file.read_text(encoding="utf-8") config_content = config_content.replace( "your-secret-key-change-in-production", jwt_key ) config_file.write_text(config_content, encoding="utf-8") # 5. 混淆 run_pyarmor_obfuscate() # 6. PyInstaller 打包 run_pyinstaller() # 7. 清理临时文件 for temp_dir in [ "build/build_temp", "build/specs", "build/utils", "build/pyarmor_runtime_000000", ]: shutil.rmtree(temp_dir, ignore_errors=True) for temp_file in ["build/main.py", "build/server.py"]: try: os.remove(temp_file) except FileNotFoundError: pass print("构建完成") # 8. 初始化数据库(构建输出目录下的 database.db) await init_database() # 9. 构建前端并生成 static.svfs build_vue_web() def build(): """构建入口""" asyncio.run(main())