"""用 Nuitka 打包(真编译成 C 再编译成 exe)。 用法:: uv run python packaging/build_nuitka.py # 单文件(默认) uv run python packaging/build_nuitka.py --standalone # 目录模式 uv run python packaging/build_nuitka.py --console # 带控制台,排查启动问题 uv run python packaging/build_nuitka.py --msvc # 指定 MSVC(默认自动挑) uv run python packaging/build_nuitka.py --low-memory # 编译器内存不够时用 uv run python packaging/build_nuitka.py --clean uv run python packaging/build_nuitka.py --no-selftest 产物:``dist/nuitka/`` 与 PyInstaller 的区别见 ``packaging/README.md``:Nuitka 构建慢得多、需要一个 C 编译器,换来的是**启动更快、体积更小、更难被反编译**。 构建前后同样有"私钥守卫 + 产物自检"两道关卡(与 PyInstaller 脚本一致)。 """ from __future__ import annotations import argparse import os import shutil import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import _common as common # noqa: E402 # 明显用不到的重型模块,让 Nuitka 不要跟进去(Qt 的裁减由 pyside6 插件负责) NOFOLLOW = [ "tkinter", "PySide6.QtWebEngineCore", "PySide6.QtWebEngineWidgets", "PySide6.QtWebEngineQuick", "PySide6.Qt3DCore", "PySide6.Qt3DRender", "PySide6.Qt3DExtras", "PySide6.QtCharts", "PySide6.QtDataVisualization", "PySide6.QtQuick", "PySide6.QtQml", "PySide6.QtMultimedia", "PySide6.QtDesigner", "PySide6.QtPdf", ] # PostgreSQL 驱动(可选功能)。在 Python 3.13+ 上 Nuitka 只能用 MSVC,而 MSVC # 编译 psycopg.errors 生成的 2.8 万行 C 文件时会 C1002(编译器堆空间不足)—— # 这是编译器上限,不是代码问题。因此提供"不带 PG"的构建:界面会如实提示 # "未安装 PostgreSQL 驱动(可选功能)",其余功能完全不受影响。 POSTGRES_MODULES = ["psycopg", "psycopg_binary", "psycopg_c", "psycopg_pool"] def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="用 Nuitka 打包 chongming 授权工具") mode = parser.add_mutually_exclusive_group() mode.add_argument("--onefile", action="store_true", default=True, help="打成单个 exe(默认)") mode.add_argument("--standalone", action="store_true", help="打成目录(启动更快)") compiler = parser.add_mutually_exclusive_group() compiler.add_argument("--msvc", action="store_true", help="用 MSVC 编译(默认自动挑选编译器)") compiler.add_argument("--mingw64", action="store_true", help="用 MinGW64 编译(默认自动挑选编译器)") parser.add_argument("--jobs", type=int, default=0, help="并行编译任务数(默认交给 Nuitka 决定)") parser.add_argument("--low-memory", action="store_true", default=True, help="降低编译器内存占用(默认开启)") parser.add_argument("--no-low-memory", dest="low_memory", action="store_false", help="关闭低内存模式。默认开启是有原因的:本项目的代码经 Nuitka " "展开后,MSVC 编译最大的那个模块会因堆空间不足报 C1002/LNK1257") parser.add_argument("--lto", choices=("yes", "no"), default=None, help="链接期优化。默认交给 Nuitka(开着,链接很慢);" "--lto=no 可显著缩短构建时间,产物略大") parser.add_argument("--no-postgres", dest="postgres", action="store_false", default=True, help="构建不带 PostgreSQL 支持的版本。Python 3.13+ 上 " "Nuitka 只能用 MSVC,而 MSVC 编译 psycopg.errors 会 C1002;" "此时工具会提示「未安装驱动」,本地台账照常可用") parser.add_argument("--console", action="store_true", help="保留控制台窗口(默认无控制台)") parser.add_argument("--clean", action="store_true", help="构建前清理并删除中间产物") parser.add_argument("--no-selftest", action="store_true", help="跳过构建后的产物自检") parser.add_argument("--allow-sensitive-files", action="store_true", help="源码树里有私钥/.lic/台账库时仍继续构建(默认中止)") return parser.parse_args() def build_command(args: argparse.Namespace, *, icon: Path | None, onefile: bool, low_memory: bool, with_postgres: bool = True) -> list[str]: dist_dir = common.DIST / "nuitka" version = common.project_version() command = [ sys.executable, "-m", "nuitka", f"--mode={'onefile' if onefile else 'standalone'}", # PySide6 插件负责把 Qt 的 DLL 与插件(platforms/styles/imageformats/ # iconengines)收集齐全 —— 少了 imageformats 里的 svg,图标与复选框 # 指示器会变空白(构建后的自检会验证这一点) "--enable-plugins=pyside6", # 注意:Nuitka 的值型选项必须写成 --opt=value,空格分隔会报 # "requires an argument with '--output-dir='" f"--output-dir={dist_dir}", f"--output-filename={common.APP_NAME}.exe", f"--windows-console-mode={'force' if args.console else 'disable'}", # 不下载 ccache / depends.exe 之类就别想跑通,脚本里没法应答交互提示 "--assume-yes-for-downloads", # exe 属性:公司/产品/版本(右键属性里看得到) "--company-name=chongming", f"--product-name={common.APP_NAME}", f"--file-version={version}", f"--product-version={version}", "--file-description=chongming 机器码授权工具(厂商侧)", ] if icon: command.append(f"--windows-icon-from-ico={icon}") if args.msvc: command.append("--msvc=latest") elif args.mingw64: command.append("--mingw64") if args.jobs > 0: command.append(f"--jobs={args.jobs}") if args.lto: command.append(f"--lto={args.lto}") if low_memory: # Nuitka 生成的 C 文件极大(单个模块上万行),MSVC 会因堆空间不足报 # C1002/LNK1257。这个开关让 Nuitka 少塞内联代码、把模块拆得更细。 command.append("--low-memory") if args.clean: command.append("--remove-output") for module in NOFOLLOW: command.append(f"--nofollow-import-to={module}") if not with_postgres: for module in POSTGRES_MODULES: command.append(f"--nofollow-import-to={module}") command.append(str(common.PACKAGING / "launcher.py")) return command def main() -> int: args = parse_args() common.require("nuitka", "nuitka") onefile = not args.standalone dist_dir = common.DIST / "nuitka" log_path = common.PACKAGING / "build" / "nuitka-build.log" print("=" * 60) print(f"Nuitka 打包({'单文件' if onefile else '目录模式'})") print("=" * 60) # 1) 安全闸门 common.guard_no_secrets(allow=args.allow_sensitive_files) # 2) 图标 icon = common.build_app_icon() if args.clean and dist_dir.exists(): shutil.rmtree(dist_dir) print(f"已清理 {dist_dir.relative_to(common.ROOT)}") # 3) 构建(首次很慢,日志落盘) env = dict(os.environ) # 让 Nuitka 能解析本包:直接给源码树,不依赖 editable 安装 env["PYTHONPATH"] = str(common.ROOT / "src") + os.pathsep + env.get("PYTHONPATH", "") # 与 PyInstaller 同理:PATH 里的同名 DLL 会被误收集(见 _common 的说明) env.update(common.build_env_overrides(env)) low_memory = args.low_memory with_postgres = args.postgres command = build_command(args, icon=icon, onefile=onefile, low_memory=low_memory, with_postgres=with_postgres) print("\n$ " + " ".join(command)) print("\n提示:Nuitka 是**真编译**,首次构建约 10 分钟起(后续有缓存会快很多)。") print(f"构建日志:{log_path.relative_to(common.ROOT)}\n") started = time.monotonic() code, log_text = common.run_build(command, log_path, env) # 兜底:万一还是撞上 MSVC 堆空间不足(例如用了 --no-low-memory),自动降内存重试 if code != 0 and not low_memory and common.is_compiler_out_of_memory(log_text): print("检测到编译器堆空间不足(C1002/LNK1257)—— 自动加 --low-memory 重试一次…\n") low_memory = True command = build_command(args, icon=icon, onefile=onefile, low_memory=True) code, log_text = common.run_build(command, log_path, env) elapsed = time.monotonic() - started if code != 0: print(f"❌ Nuitka 构建失败(退出码 {code})\n") print(common.log_tail(log_text, 30)) print(f"\n完整日志:{log_path.relative_to(common.ROOT)}") if common.is_compiler_out_of_memory(log_text): print("失败原因是编译器内存不足。先用 --low-memory(默认已开);" "仍失败就改用 PyInstaller(build_pyinstaller.py)," "或用 --no-postgres 构建不带 PG 的版本。") else: print("常见原因:缺少 C 编译器。Windows 上装 Visual Studio 的「C++ 生成工具」,") print("或用 --mingw64(需 Nuitka 支持的 mingw64 版本,msys2 自带的那个不行)。") return code # 4) 找到产物并验证 artifact = common.find_artifact(dist_dir) if artifact is None: print(f"\n❌ 构建结束但没在 {dist_dir} 里找到 exe") return 1 stray = common.guard_artifact_no_secrets(artifact.parent) if stray: print(f"\n❌ 产物目录里出现敏感文件,请检查后再分发:{stray}") return 1 if not args.no_selftest: if not common.verify_artifact(artifact, "Nuitka 产物"): print("\n❌ 产物自检未通过 —— 先别分发,见上方输出") return 1 common.report_success("Nuitka", artifact, elapsed) return 0 if __name__ == "__main__": sys.exit(main())