modify: 0.3.3
This commit is contained in:
+109
-58
@@ -10,6 +10,7 @@
|
||||
|
||||
用法:
|
||||
uv run build [pyinstaller|nuitka] [--only <step>[,<step>...]]
|
||||
[--encrypt] [--license]
|
||||
|
||||
参数:
|
||||
pyinstaller|nuitka 构建模式(默认 pyinstaller)
|
||||
@@ -22,10 +23,18 @@
|
||||
license_guard 授权私钥泄漏守卫
|
||||
vue Vue 前端构建
|
||||
可传多个: --only mbank,vue 或 --only mbank --only vue
|
||||
--encrypt 打开加密与混淆:.mbank 加密存字节码并写 .key 密钥文件、
|
||||
做 PyArmor 混淆。**默认关闭**(产物为明文,默认只适合内部
|
||||
调试;对外分发必须加这个参数)
|
||||
--license 打开授权:注入厂商授权公钥(缺公钥则构建失败)、跑授权私钥
|
||||
泄漏守卫。**默认关闭**(不注入、不守卫、缺公钥也不报错,
|
||||
产物无法校验任何授权;对外分发必须加这个参数)
|
||||
|
||||
示例:
|
||||
uv run build --only mbank # 依赖未变时,仅重新打包 .mbank 模块文件
|
||||
uv run build nuitka --only mbank,prepare
|
||||
uv run build --encrypt --license # 对外分发:加密模块 + 注入授权公钥
|
||||
uv run build # 内部调试:明文模块、不带授权
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -52,13 +61,21 @@ NUITKA = "nuitka"
|
||||
MODES = (PYINSTALLER, NUITKA)
|
||||
|
||||
# 可独立执行的构建步骤(--only 参数取值)
|
||||
STEPS = ("mbank", "prepare", "obfuscate", "pack", "clean", "vue")
|
||||
STEPS = ("mbank", "prepare", "obfuscate", "pack", "clean", "license_guard", "vue")
|
||||
|
||||
is_windows = platform.system() == "Windows"
|
||||
|
||||
# ── 构建结果数据结构 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class BuildOptions:
|
||||
"""构建开关(命令行可选参数,默认全部关闭 = 明文调试产物)"""
|
||||
|
||||
encrypt: bool = False # --encrypt: 打开 mbank 加密与 PyArmor 混淆
|
||||
license: bool = False # --license: 打开授权公钥注入与私钥泄漏守卫
|
||||
|
||||
|
||||
@dataclass
|
||||
class BuildStepResult:
|
||||
"""单个构建步骤的执行结果"""
|
||||
@@ -76,6 +93,7 @@ class BuildReport:
|
||||
mode: str
|
||||
start_time: float
|
||||
end_time: float = 0.0
|
||||
options: "BuildOptions | None" = None # 构建开关(总结里回显,便于确认产物性质)
|
||||
steps: list[BuildStepResult] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
@@ -96,6 +114,9 @@ class BuildReport:
|
||||
print(" 🏗️ CHONGMING 构建报告")
|
||||
print(f"{sep}")
|
||||
print(f" 构建模式 : {self.mode}")
|
||||
if self.options is not None:
|
||||
print(f" 加密混淆 : {_switch_text(self.options.encrypt, '明文产物')}")
|
||||
print(f" 授权注入 : {_switch_text(self.options.license, '不校验授权')}")
|
||||
print(f" 总耗时 : {self.total_duration:.2f}s")
|
||||
print(f" 构建状态 : {'✅ 成功' if self.all_success else '❌ 失败'}")
|
||||
print(f"{sep}")
|
||||
@@ -172,17 +193,24 @@ def _ensure_dir(path: str | Path):
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _parse_args() -> tuple[str, set[str]]:
|
||||
"""解析命令行参数,返回 (构建模式, 需执行的步骤子集)
|
||||
def _switch_text(on: bool, off_note: str) -> str:
|
||||
"""构建开关的可读状态:开 / 关(附一句"关掉意味着什么/怎么打开")"""
|
||||
return "开" if on else f"关({off_note})"
|
||||
|
||||
|
||||
def _parse_args() -> tuple[str, set[str], BuildOptions]:
|
||||
"""解析命令行参数,返回 (构建模式, 需执行的步骤子集, 构建开关)
|
||||
|
||||
支持:
|
||||
uv run build [pyinstaller|nuitka]
|
||||
uv run build --only <step>[,<step>...] [pyinstaller|nuitka]
|
||||
only_steps 为空表示执行完整构建流程。
|
||||
uv run build [--encrypt] [--license] [pyinstaller|nuitka]
|
||||
only_steps 为空表示执行完整构建流程;开关默认全关(明文、不带授权)。
|
||||
"""
|
||||
args = sys.argv[1:]
|
||||
mode = PYINSTALLER
|
||||
only_steps: set[str] = set()
|
||||
options = BuildOptions()
|
||||
i = 0
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
@@ -197,6 +225,12 @@ def _parse_args() -> tuple[str, set[str]]:
|
||||
sys.exit(1)
|
||||
only_steps.add(step)
|
||||
i += 2
|
||||
elif arg == "--encrypt":
|
||||
options.encrypt = True
|
||||
i += 1
|
||||
elif arg == "--license":
|
||||
options.license = True
|
||||
i += 1
|
||||
elif arg.startswith("--"):
|
||||
print(f" ❌ 未知参数: {arg}")
|
||||
sys.exit(1)
|
||||
@@ -206,20 +240,32 @@ def _parse_args() -> tuple[str, set[str]]:
|
||||
print(f" ❌ 不支持的构建模式: {mode},可选: {', '.join(MODES)}")
|
||||
sys.exit(1)
|
||||
i += 1
|
||||
return mode, only_steps
|
||||
return mode, only_steps, options
|
||||
|
||||
|
||||
# ── 阶段 1: 模块打包 (module-bank) ──────────────────────────────────────────
|
||||
|
||||
|
||||
def _pack_mbank(output_path: str, source_dir: str, package_name: str, obfuscate: bool) -> str:
|
||||
"""打包单个 mbank 目标,返回文件大小描述"""
|
||||
def _pack_mbank(
|
||||
output_path: str, source_dir: str, package_name: str, obfuscate: bool, encrypt: bool
|
||||
) -> str:
|
||||
"""打包单个 mbank 目标,返回文件大小描述
|
||||
|
||||
encrypt=False(默认,加 --encrypt 才打开)时传 key=None:module-bank 存
|
||||
**明文**字节码,模块元信息里 encrypted=False,加载端不需要密钥也能导入。
|
||||
"""
|
||||
# 每次从空文件开始:旧包里的历史版本既让体积膨胀,也会留下重名/空的版本
|
||||
Path(output_path).unlink(missing_ok=True)
|
||||
|
||||
key = Encryption.generate_key()
|
||||
key_path = Path(output_path).with_suffix(".key")
|
||||
key_path.write_text(key, encoding="utf-8")
|
||||
if encrypt:
|
||||
key = Encryption.generate_key()
|
||||
key_path.write_text(key, encoding="utf-8")
|
||||
else:
|
||||
# 明文打包不再有"本包的密钥";上一次加密构建留下的 .key 与新包不配对,
|
||||
# 留着只会让人误以为它是这一包的密钥
|
||||
key = None
|
||||
key_path.unlink(missing_ok=True)
|
||||
|
||||
packer = PythonToSQLite(output_path, key, pyarmor_obfuscate=obfuscate)
|
||||
try:
|
||||
@@ -240,9 +286,9 @@ def _is_pyarmor_license_error(exc: Exception) -> bool:
|
||||
return "out of license" in msg or "license" in msg
|
||||
|
||||
|
||||
def _stage_pack_modules(config: Config) -> str:
|
||||
def _stage_pack_modules(config: Config, options: BuildOptions) -> str:
|
||||
"""
|
||||
使用 module-bank 加密打包 Python 模块为 .mbank 文件。
|
||||
使用 module-bank 打包 Python 模块为 .mbank 文件。
|
||||
返回打包过程中的提示信息。
|
||||
"""
|
||||
_ensure_dir("resources")
|
||||
@@ -250,17 +296,29 @@ def _stage_pack_modules(config: Config) -> str:
|
||||
if not mbank_list:
|
||||
return "⚠️ 未配置 module_system.path,跳过模块打包"
|
||||
|
||||
# 不加密的产物本来就是拿来读的,PyArmor 混淆只会让它更难读,一并跳过
|
||||
obfuscate = options.encrypt
|
||||
|
||||
messages = []
|
||||
for output_path, source_dir, package_name in mbank_list:
|
||||
try:
|
||||
size = _pack_mbank(output_path, source_dir, package_name, obfuscate=True)
|
||||
messages.append(f"{Path(output_path).name} ({size})")
|
||||
size = _pack_mbank(
|
||||
output_path,
|
||||
source_dir,
|
||||
package_name,
|
||||
obfuscate=obfuscate,
|
||||
encrypt=options.encrypt,
|
||||
)
|
||||
note = "" if options.encrypt else ",明文未加密未混淆"
|
||||
messages.append(f"{Path(output_path).name} ({size}{note})")
|
||||
except Exception as e:
|
||||
if not _is_pyarmor_license_error(e):
|
||||
if not obfuscate or not _is_pyarmor_license_error(e):
|
||||
raise
|
||||
# PyArmor 试用版授权额度耗尽(out of license) → 降级为不混淆重打包
|
||||
Path(output_path).unlink(missing_ok=True)
|
||||
size = _pack_mbank(output_path, source_dir, package_name, obfuscate=False)
|
||||
size = _pack_mbank(
|
||||
output_path, source_dir, package_name, obfuscate=False, encrypt=True
|
||||
)
|
||||
print(
|
||||
f" ⚠️ {Path(output_path).name}: PyArmor 授权不足(out of license),"
|
||||
f"降级为无混淆打包"
|
||||
@@ -269,7 +327,7 @@ def _stage_pack_modules(config: Config) -> str:
|
||||
return " - " + "\n - ".join(messages)
|
||||
|
||||
|
||||
def _stage_prepare_build_dir(config: Config, mode: str) -> str:
|
||||
def _stage_prepare_build_dir(config: Config, mode: str, options: BuildOptions) -> str:
|
||||
"""准备 build 目录并复制必要文件"""
|
||||
build_path = Path("build")
|
||||
if build_path.exists():
|
||||
@@ -301,25 +359,33 @@ def _stage_prepare_build_dir(config: Config, mode: str) -> str:
|
||||
jwt_key = Encryption.generate_key()
|
||||
content = content.replace("your-secret-key-change-in-production", jwt_key)
|
||||
|
||||
# 注入厂商授权公钥(机器码授权)
|
||||
pubkey = _resolve_license_public_key()
|
||||
if pubkey:
|
||||
content = _set_license_pubkey(content, pubkey)
|
||||
elif os.environ.get(ALLOW_NO_KEY_ENV) == "1":
|
||||
# 公钥缺失时打包产物会拒绝**全部**插件(没人能证明被授权)= 砖头设备,
|
||||
# 故默认让构建失败;仅内部演示允许显式放行。
|
||||
print(
|
||||
f" ⚠️ 未提供授权公钥且设置了 {ALLOW_NO_KEY_ENV}=1:"
|
||||
"本产物无法校验任何授权(所有插件都不会加载),仅限内部演示"
|
||||
)
|
||||
# 注入厂商授权公钥(机器码授权)—— 默认不做,加 --license 才注入
|
||||
if options.license:
|
||||
pubkey = _resolve_license_public_key()
|
||||
if pubkey:
|
||||
content = _set_license_pubkey(content, pubkey)
|
||||
elif os.environ.get(ALLOW_NO_KEY_ENV) == "1":
|
||||
# 公钥缺失时打包产物会拒绝**全部**插件(没人能证明被授权)= 砖头设备,
|
||||
# 故默认让构建失败;仅内部演示允许显式放行。
|
||||
print(
|
||||
f" ⚠️ 未提供授权公钥且设置了 {ALLOW_NO_KEY_ENV}=1:"
|
||||
"本产物无法校验任何授权(所有插件都不会加载),仅限内部演示"
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"缺少厂商授权公钥:打包产物会拒绝加载所有插件(设备变砖)。\n"
|
||||
f" 请用环境变量 {LICENSE_PUBKEY_ENV}=<base64 公钥> 或 "
|
||||
f"{LICENSE_PUBKEY_FILE_ENV}=<文件路径> 提供,\n"
|
||||
f" 或把公钥写入 {DEFAULT_LICENSE_PUBKEY_FILE}"
|
||||
"(由 `uv run chongming-license keygen` 输出)。\n"
|
||||
f" 仅内部演示无授权构建可去掉 --license,"
|
||||
f"或设 {ALLOW_NO_KEY_ENV}=1 强制放行。"
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"缺少厂商授权公钥:打包产物会拒绝加载所有插件(设备变砖)。\n"
|
||||
f" 请用环境变量 {LICENSE_PUBKEY_ENV}=<base64 公钥> 或 "
|
||||
f"{LICENSE_PUBKEY_FILE_ENV}=<文件路径> 提供,\n"
|
||||
f" 或把公钥写入 {DEFAULT_LICENSE_PUBKEY_FILE}"
|
||||
"(由 `uv run chongming-license keygen` 输出)。\n"
|
||||
f" 仅内部演示无授权构建可设 {ALLOW_NO_KEY_ENV}=1。"
|
||||
# 默认不注入:产物不带厂商授权信任根(模板里已写着的公钥也不动)
|
||||
print(
|
||||
" ⚠️ 未注入厂商授权公钥(默认):本产物无法校验任何授权,"
|
||||
"对外分发请加 --license"
|
||||
)
|
||||
cfg_path.write_text(content, encoding="utf-8")
|
||||
|
||||
@@ -520,20 +586,6 @@ def _stage_nuitka(config: Config) -> tuple[str, str]:
|
||||
"windows" if is_windows else "linux", []
|
||||
)
|
||||
|
||||
# packages = [
|
||||
# "module_bank",
|
||||
# "sqlite_vfs",
|
||||
# "fastapi",
|
||||
# "uvicorn",
|
||||
# "gunicorn",
|
||||
# "diskcache",
|
||||
# "chongming_cache",
|
||||
# "chongming_cache.backends",
|
||||
# "chongming_cache.backends.disk",
|
||||
# "tomli",
|
||||
# "typing_extensions",
|
||||
# ]
|
||||
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
@@ -554,12 +606,9 @@ def _stage_nuitka(config: Config) -> tuple[str, str]:
|
||||
"main.py",
|
||||
"--jobs=4",
|
||||
]
|
||||
# export LDFLAGS="-fuse-ld=lld"
|
||||
# export LDFLAGS="-fuse-ld=lld-20"
|
||||
if shutil.which("upx"):
|
||||
cmd.append("--enable-plugin=upx")
|
||||
# # for pkg in packages:
|
||||
# cmd.append(f"--include-package={pkg}")
|
||||
|
||||
for mod in list(base_imports) + extra:
|
||||
cmd.append(f"--include-module={mod}")
|
||||
|
||||
@@ -660,8 +709,8 @@ def _stage_build_vue() -> str:
|
||||
|
||||
async def main():
|
||||
"""规范化构建主流程"""
|
||||
mode, only_steps = _parse_args()
|
||||
report = BuildReport(mode=mode, start_time=time.time())
|
||||
mode, only_steps, options = _parse_args()
|
||||
report = BuildReport(mode=mode, start_time=time.time(), options=options)
|
||||
config = get_config()
|
||||
|
||||
def should_run(name: str) -> bool:
|
||||
@@ -693,20 +742,22 @@ async def main():
|
||||
print(f" 模式: {mode} | 平台: {platform.system()}")
|
||||
if only_steps:
|
||||
print(f" 仅执行步骤: {', '.join(sorted(only_steps))}")
|
||||
print(f" 加密混淆: {_switch_text(options.encrypt, '明文产物,--encrypt 打开')}")
|
||||
print(f" 授权注入: {_switch_text(options.license, '不校验授权,--license 打开')}")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
# 阶段 1: 模块打包
|
||||
if should_run("mbank"):
|
||||
await run_step("模块打包(module-bank)", _stage_pack_modules, config)
|
||||
await run_step("模块打包(module-bank)", _stage_pack_modules, config, options)
|
||||
|
||||
# 阶段 2: 准备构建目录
|
||||
if should_run("prepare"):
|
||||
await run_step("准备构建目录", _stage_prepare_build_dir, config, mode)
|
||||
await run_step("准备构建目录", _stage_prepare_build_dir, config, mode, options)
|
||||
|
||||
# 阶段 3+4: 混淆 + 打包
|
||||
if should_run("pack"):
|
||||
if mode == PYINSTALLER:
|
||||
if should_run("obfuscate"):
|
||||
if options.encrypt and should_run("obfuscate"):
|
||||
await run_step("PyArmor 混淆", _stage_pyarmor_obfuscate)
|
||||
start = time.time()
|
||||
try:
|
||||
@@ -731,7 +782,7 @@ async def main():
|
||||
await run_step("清理临时文件", _stage_cleanup, mode)
|
||||
|
||||
# 阶段 6: 授权私钥泄漏守卫(产物落地后、分发前)
|
||||
if should_run("license_guard"):
|
||||
if options.license and should_run("license_guard"):
|
||||
await run_step("授权私钥守卫", _stage_license_guard)
|
||||
|
||||
# 阶段 7: Vue 前端构建
|
||||
|
||||
@@ -15,6 +15,7 @@ def generate_markdown_from_py_files(directory, output_file):
|
||||
dirs[:] = [d for d in dirs if d != "dist"]
|
||||
dirs[:] = [d for d in dirs if d != "chongming-web"]
|
||||
dirs[:] = [d for d in dirs if d != "workers"]
|
||||
dirs[:] = [d for d in dirs if d != "target"]
|
||||
for file in files:
|
||||
if (
|
||||
file.endswith(".py")
|
||||
|
||||
Reference in New Issue
Block a user