modify: 规范打包脚本
This commit is contained in:
+4
-4
@@ -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/"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_BASE_URL = "http://localhost:8000"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_BASE_URL = "/"
|
||||||
Vendored
+9
@@ -1 +1,10 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_BASE_URL: string
|
||||||
|
// 更多环境变量...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+383
-342
@@ -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,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
def _resolve_mode() -> str:
|
||||||
packer.pack_folder(dist_dir)
|
"""解析构建模式"""
|
||||||
print(f"✅ 打包完成,文件保存为 {output_db}")
|
mode = PYINSTALLER
|
||||||
finally:
|
if len(sys.argv) >= 2:
|
||||||
vfs.close()
|
mode = sys.argv[1].lower()
|
||||||
|
if mode not in MODES:
|
||||||
|
print(f" ❌ 不支持的构建模式: {mode},可选: {', '.join(MODES)}")
|
||||||
|
sys.exit(1)
|
||||||
|
return mode
|
||||||
|
|
||||||
|
# ── 阶段 1: 模块打包 (module-bank) ──────────────────────────────────────────
|
||||||
|
|
||||||
def run_pyarmor_obfuscate():
|
def _stage_pack_modules(config: Config) -> str:
|
||||||
"""
|
"""
|
||||||
使用 PyArmor 对关键 Python 文件进行代码混淆
|
使用 module-bank 加密打包 Python 模块为 .mbank 文件。
|
||||||
|
返回打包过程中的提示信息。
|
||||||
该函数遍历预定义的文件列表,对每个文件执行 pyarmor gen 命令进行混淆处理,
|
|
||||||
以保护源代码不被轻易反编译。
|
|
||||||
|
|
||||||
处理的文件包括:
|
|
||||||
- main.py (主入口文件)
|
|
||||||
- server.py (服务器文件)
|
|
||||||
- utils/launch.py (启动工具)
|
|
||||||
- utils/config.py (配置工具)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None: 无返回值,遇到错误时提前返回
|
|
||||||
"""
|
"""
|
||||||
# pyarmor obfuscate --output=obfuscated_script.py original_script.py
|
_ensure_dir("resources")
|
||||||
obfuscate_files = [
|
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)
|
||||||
|
|
||||||
|
def _stage_prepare_build_dir(config: Config, mode: str) -> str:
|
||||||
|
"""准备 build 目录并复制必要文件"""
|
||||||
|
build_path = Path("build")
|
||||||
|
if build_path.exists():
|
||||||
|
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")
|
||||||
|
|
||||||
|
# 复制 resources
|
||||||
|
resources_src = Path("resources")
|
||||||
|
if resources_src.exists():
|
||||||
|
shutil.copytree(resources_src, build_path / "resources")
|
||||||
|
|
||||||
|
# 复制核心文件
|
||||||
|
for f in ("main.py", "server.py", "config.toml"):
|
||||||
|
shutil.copy(f"public/{f}", f"build/{f}")
|
||||||
|
|
||||||
|
# 注入 JWT 密钥
|
||||||
|
jwt_key = Encryption.generate_key()
|
||||||
|
cfg_path = build_path / "config.toml"
|
||||||
|
content = cfg_path.read_text(encoding="utf-8")
|
||||||
|
content = content.replace("your-secret-key-change-in-production", jwt_key)
|
||||||
|
cfg_path.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
return "资源准备完成"
|
||||||
|
|
||||||
|
# ── 阶段 3: PyArmor 混淆 ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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]]
|
||||||
|
|||||||
Reference in New Issue
Block a user