diff --git a/src/chongming_cli/build.py b/src/chongming_cli/build.py index 5dd9e9e..9e937fa 100644 --- a/src/chongming_cli/build.py +++ b/src/chongming_cli/build.py @@ -27,14 +27,16 @@ 做 PyArmor 混淆。**默认关闭**(产物为明文,默认只适合内部 调试;对外分发必须加这个参数) --license 打开授权:注入厂商授权公钥(缺公钥则构建失败)、跑授权私钥 - 泄漏守卫。**默认关闭**(不注入、不守卫、缺公钥也不报错, - 产物无法校验任何授权;对外分发必须加这个参数) + 泄漏守卫。**默认关闭**(license 是可选构建项:不加则产物为 + "非授权版"——不校验授权、插件照常加载,与开发态一致; + 加上则产物为"授权版"——强制校验,现场改配置/环境变量关不掉。 + 两者由构建期烘焙进产物,见 chongming/_build_flags.py) 示例: uv run build --only mbank # 依赖未变时,仅重新打包 .mbank 模块文件 uv run build nuitka --only mbank,prepare - uv run build --encrypt --license # 对外分发:加密模块 + 注入授权公钥 - uv run build # 内部调试:明文模块、不带授权 + uv run build --encrypt --license # 对外分发:加密模块 + 授权版产物 + uv run build # 内部/无授权版:明文模块、不校验授权 依赖收集(--only pack 生效): 打包收集清单 = 根 config.toml [pyinstaller_build_import](网关本体依赖) @@ -99,7 +101,7 @@ class BuildOptions: """构建开关(命令行可选参数,默认全部关闭 = 明文调试产物)""" encrypt: bool = False # --encrypt: 打开 mbank 加密与 PyArmor 混淆 - license: bool = False # --license: 打开授权公钥注入与私钥泄漏守卫 + license: bool = False # --license: 授权版产物(注入公钥 + 烘焙强制校验);默认非授权版 @dataclass @@ -387,33 +389,46 @@ def _stage_prepare_build_dir(config: Config, mode: str, options: BuildOptions) - jwt_key = Encryption.generate_key() content = content.replace("your-secret-key-change-in-production", jwt_key) - # 注入厂商授权公钥(机器码授权)—— 默认不做,加 --license 才注入 + # 注入厂商授权公钥(机器码授权)—— 默认不做,加 --license 才注入; + # 两条路径都会把"本产物是否为授权版"烘焙进 chongming/_build_flags.py + # (编译进产物、部署侧不可改)—— 未加 --license 的产物是不校验授权、 + # 插件照常加载的正常版本,而不是"拒绝所有插件"的砖头包。 if options.license: pubkey = _resolve_license_public_key() if pubkey: + from chongming_license import public_key_fingerprint + content = _set_license_pubkey(content, pubkey) + _write_build_flags(enforce=True, public_key=pubkey) + print( + f" ✓ 已注入厂商授权公钥(指纹 {public_key_fingerprint(pubkey)}):" + "产物为授权版,强制校验授权(现场改配置/环境变量关不掉)" + ) elif os.environ.get(ALLOW_NO_KEY_ENV) == "1": - # 公钥缺失时打包产物会拒绝**全部**插件(没人能证明被授权)= 砖头设备, - # 故默认让构建失败;仅内部演示允许显式放行。 + # 无公钥 = 谁都证明不了被授权。此时**不再**烘焙"授权版"(那会拒绝 + # 全部插件 = 砖头设备),而是烘焙非授权版:插件照常加载、不校验授权。 + _write_build_flags(enforce=False) print( f" ⚠️ 未提供授权公钥且设置了 {ALLOW_NO_KEY_ENV}=1:" - "本产物无法校验任何授权(所有插件都不会加载),仅限内部演示" + "本产物为非授权版(不校验授权,插件照常加载),仅限内部演示" ) else: raise RuntimeError( - "缺少厂商授权公钥:打包产物会拒绝加载所有插件(设备变砖)。\n" + "缺少厂商授权公钥:无法产出可校验授权的产物。\n" f" 请用环境变量 {LICENSE_PUBKEY_ENV}= 或 " 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 强制放行。" + " 不需要授权校验的内部/演示构建请**去掉 --license**" + f"(产物不校验、插件照常加载),或设 {ALLOW_NO_KEY_ENV}=1 强制放行。" ) else: - # 默认不注入:产物不带厂商授权信任根(模板里已写着的公钥也不动) + # 非授权版:明确烘焙"不校验授权"(产物内编译,部署侧改不了)—— + # 打包不含 license 因此始终是合法选项,产物与开发态一致、插件照常加载。 + _write_build_flags(enforce=False) print( - " ⚠️ 未注入厂商授权公钥(默认):本产物无法校验任何授权," - "对外分发请加 --license" + " ⚠️ 未注入厂商授权公钥(默认):本产物为非授权版(不校验授权," + "插件照常加载),对外分发请加 --license" ) cfg_path.write_text(content, encoding="utf-8") @@ -457,6 +472,50 @@ def _resolve_license_public_key() -> str: return "" +def _write_build_flags(enforce: bool, public_key: str = "") -> Path: + """把"本产物是否为授权版"烘焙成 ``chongming/_build_flags.py``(随代码编译)。 + + 格式契约与网关读取端 ``chongming.app.core.licensing`` 对齐(常量名一致): + 该模块在打包 venv 的网关包内生成,编译进产物后**部署侧改不了** —— 所以 + "打包不配 license" 可以是合法选项(烘焙 False = 不校验、插件照常加载), + 而授权版产物的强制校验也不会被现场改 config.toml / 环境变量关掉。 + + 授权版同时把公钥烘焙进代码:现场即便把 config.toml 里的公钥换成自己的 + 并自签一份 license,也过不了内置公钥的验签(配置公钥只在未烘焙时兜底)。 + """ + import importlib.util + + from chongming_license import load_public_key, public_key_fingerprint + + fingerprint = "" + if public_key: + # 失败即构建期配置错误:趁早报错,避免打出一个"谁都验不过"的授权版产物 + load_public_key(public_key) + fingerprint = public_key_fingerprint(public_key) + + spec = importlib.util.find_spec("chongming") + if spec is None or not spec.submodule_search_locations: + raise RuntimeError( + "未找到 chongming 网关包,无法写入构建期授权标记(_build_flags.py)。\n" + " 请确认构建 venv 已安装 chongming-gateway(uv sync)" + ) + target = Path(next(iter(spec.submodule_search_locations))) / "_build_flags.py" + target.write_text( + "\n".join([ + '"""构建期烘焙的授权决策 —— 由打包脚本生成,请勿手改。', + "", + "编译进产物后部署侧不可修改:部署目录的 config.toml / 环境变量都影响不了", + "这份决策。读取端:chongming.app.core.licensing。", + '"""', + f"LICENSE_ENFORCE = {enforce!r}", + f'LICENSE_PUBLIC_KEY = "{public_key}"', + f'PUBLIC_KEY_FINGERPRINT = "{fingerprint}"', + ]) + "\n", + encoding="utf-8", + ) + return target + + def _set_license_pubkey(text: str, pubkey: str) -> str: """把公钥写进 [license] 段的 public_key,保留其余注释与排版。