Files
2026-09-22 16:08:12 +08:00

227 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""用 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())