383 lines
12 KiB
Python
383 lines
12 KiB
Python
# 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())
|