Files
chongming_lite/src/chongming/scripts/build.py
T
2026-04-26 17:12:35 +08:00

383 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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())