Metadata-Version: 2.4
Name: kkpack
Version: 0.1.3
Summary: 把 Python 项目打包成自带解释器的 exe，第三方依赖在首次运行时自动安装
Author: Python卡皮巴拉
License-Expression: MIT
Keywords: packaging,exe,nuitka,pyinstaller,freeze,windows,kkpack
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: Chinese (Simplified)
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: System :: Software Distribution
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Provides-Extra: release
Requires-Dist: build>=1.0; extra == "release"
Requires-Dist: twine>=5.0; extra == "release"
Dynamic: license-file

# kkpack

把 Python 项目打包成 **Windows 上自带解释器的 exe**，第三方依赖在目标机首次运行时按需安装。

```bash
pip install kkpack
kkpack main.py
```

使用者电脑上不需要装 Python，也不需要装任何依赖。

---

## 它解决什么问题

传统打包（Nuitka / PyInstaller 一把梭）有两个痛点：**体积** —— 把 numpy、torch 这类重型库全编进 exe，产物动辄几百 MB、编译半小时起步；**更新** —— 改一行代码就要重编整棵依赖树。

kkpack 的做法是：**你的代码 + Python 运行时 + 标准库编进 exe，第三方依赖留到运行时按需安装。**
依赖版本在打包时冻结成一份完整清单（含间接依赖），所以目标机装出来的版本永远和你开发时一致。

---

## 30 秒上手

就是最普通的项目结构，不需要为打包改任何代码：

```
myapp/
├── main.py
├── mypkg/
│   ├── __init__.py
│   └── core.py
└── requirements.txt      # requests==2.31.0
```

```bash
pip install kkpack        # 第 1 步：安装 kkpack
cd myapp
kkpack doctor             # 第 2 步：体检环境（可选，第一次建议跑）
kkpack main.py            # 第 3 步：打包
```

`doctor` 会告诉你：有没有 C 编译器、后端装没装、`requirements.txt` 在不在，以及哪些依赖没写版本、哪些用了范围约束（两者都允许，只是打包时取到的版本不同）。其中 **后端没装、编译器没找到、没有 `requirements.txt` 都不算阻塞** —— 后端构建时自动 `pip install`；编译器由 Nuitka 自己解决（见「环境要求」）；没有依赖清单就是 **零依赖**（目标机不安装任何依赖，详见下文）。

**第 4 步：把产物整个目录拷给别人**

```
dist/main.dist/main.exe          # 双击即可运行
dist/main.dist/requirements.txt  # 依赖清单，随程序分发
```

`main.exe` 首次运行会把依赖装到 exe 同级的 `_deps/` 目录里，之后每次启动都不再联网。
**注意：`main.dist` 整个目录要一起拷，不能只拷 exe。**

---

## 命令行

```
kkpack [入口文件] [选项]
kkpack init           生成带注释的配置文件
kkpack doctor         检查当前环境的打包能力
```

| 参数 | 作用 | 默认 |
|---|---|---|
| `entry` | 入口 py 文件，如 `main.py`（不写则从配置读 `tool.entry`） | — |
| `--backend nuitka\|pyinstaller` | 打包后端，没装会自动 pip 安装 | `nuitka` |
| `--backend-version VER` | 后端版本：留空 = kkpack 验证过的那一版；`any` = 不锁也不检查 | 验证过的版本 |
| `-y, --yes` | 后端版本与验证版本不一致时不再询问，直接继续 | 会问（非交互环境只提示） |
| `--compiler auto\|msvc\|mingw64` | C 编译器：`auto` 交给 Nuitka 自己挑（缺了会自动下载 MinGW64） | `auto` |
| `--compiler-dir DIR` | 指定 C 工具链根目录（GCC 家族），让 Nuitka 用你这份而不是它自己下载的 winlibs | — |
| `--mode runtime\|offline\|all` | 依赖处理方式 | `runtime` |
| `--onefile` / `--no-onefile` | 单 exe / 目录形式（目录启动更快，推荐） | 目录形式 |
| `--exe-only` / `--no-exe-only` | 只交付升级包：把 exe 复制到 `<产物目录>/upgrade/`，覆盖已部署目录里的旧 exe 即完成升级 | 关 |
| `--windowed` / `--console` | 是否显示控制台黑窗口（GUI 程序用 `--windowed`） | `--console` |
| `--out DIR` | 产物输出目录 | `dist` |
| `--exe-name NAME` | 产物（exe）名，不用写 `.exe` | 入口文件名 |
| `--icon PATH.ico` | 程序图标，只支持 `.ico` | 系统默认图标 |
| `--resource PATH` | 追加一项随产物分发的资源（文件/目录），可重复；`"源 -> 目标"` 可改名 | — |
| `--index URL` | 追加镜像源，可重复（`--index A --index B`） | 阿里云→清华→PyPI |
| `--stdlib precise\|full\|none` | 标准库包含策略 | `full` |
| `--jobs N` | 并行编译进程数，内存小就调小 | `2` |
| `--callable NAME` | 模块模式下要调用的函数名 | — |
| `-c, --config PATH` | 指定配置文件（默认自动找 `kkpack.toml` / `pyfrost.toml`） | 自动查找 |
| `--clean` | 清空自动生成的构建文件后重编（保留 wheel 下载缓存） | — |
| `--quiet` | 只输出关键结果 | — |

优先级：**命令行参数 > 配置文件 > 默认值**。

构建过程里唯一的询问是「后端版本与 kkpack 验证过的版本不一致，用你装着的这一版继续吗」。
它的默认值就是继续：**敲回车即当 Y**，只有明确输入 `n` / `no` 才取消；管道 / CI 等
非交互环境不询问，直接继续（想完全免掉这段提示：`[tool] backend_version = "any"`）。

---

## 配置文件（可选，不写也能跑）

```bash
kkpack init          # 生成一份带注释的样板
```

默认查找当前目录下的 `kkpack.toml`（本工具前身用过的 `pyfrost.toml` 同样接受）。
下面这份 **全部可以省略**：

```toml
[tool]
backend = "nuitka"        # nuitka | pyinstaller
compiler = "auto"         # C 编译器：auto 交给 Nuitka 挑；也可强制 msvc / mingw64
compiler_dir = ""         # C 工具链根目录（含 bin/gcc.exe）；填了就用你这份，不再下载 winlibs
entry = "main.py"         # 命令行给了入口就以命令行优先
exe_name = "MyApp"        # 产物（exe）名，不用写 .exe；默认取入口文件名
icon = "assets/app.ico"   # 程序图标，只支持 .ico；不写用系统默认图标
logo = "assets/logo.png"  # 首次装依赖的进度窗口顶部的品牌图片，png / gif；不写就不显示
resources = [             # 随产物复制到 exe 同级的资源（文件/目录），见「资源文件随 exe 分发」
    "assets",
    "config.ini",
]
onefile = false           # 目录形式启动更快
exe_only = false          # 只交付升级包（一个 exe 完成升级），见「只交付 exe 的升级包」
console = true            # GUI 程序改成 false，不弹黑窗口
output_dir = "dist"
jobs = 2                  # 并行编译数，内存不够就调小

[bundle]
mode = "runtime"          # runtime | offline | all，见「依赖处理的三种模式」
include = []              # 强制编译进 exe 的包
exclude = []              # 强制留到运行时的包

[runtime]
check_update = false      # 是否在运行时检查依赖更新（会联网）
progress = true           # 首次安装时弹 tkinter 进度条
download_jobs = 4         # 并发下载数
clear_console = true      # 依赖装完后清屏，黑窗口只剩程序自己的输出

[index]
urls = [
    "https://mirrors.aliyun.com/pypi/simple/",
    "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/",
    "https://pypi.org/simple/",
]
timeout = 30

[stdlib]
# full（默认）：整个标准库打进 exe，含 tkinter / sqlite3 / asyncio
# precise     ：只补真正用到的标准库，体积更小；代价见「依赖处理的三种模式」
include_mode = "full"

[version]                 # 见「版本信息与代码签名」
company = "某某科技有限公司"

[sign]                    # 见「版本信息与代码签名」
certificate = ""          # 留空 = 不签名
```

配置文件里写了 kkpack 不认识的段或键，构建时会明确提示被忽略，不会出现"改了没生效"。

---

## requirements.txt 是唯一的依赖来源

依赖 **只写在 `requirements.txt` 里**，不要在配置文件里重复声明一遍 —— 避免两处版本号不一致这种最难查的问题。

**零依赖也支持**：不需要任何第三方包时，这个文件可以不存在、为空、或全是注释 —— 产物照样是一份完整的 exe（只带 Python 运行时与标准库）。相应地，exe 里也就 **没有依赖清单**，目标机首次运行不会安装任何依赖；之后要加包，就写进 `requirements.txt` 重新打包，或把一份 `requirements.txt` 放到 exe 同级（运行期会读它，`exe_only` 的升级包正是这么加依赖的）。

版本 **不强制锁定**，三种写法都收：

| 写法 | 构建期行为 | 建议 |
|---|---|---|
| `pyserial` | 取 **当时的最新版** | 允许。构建日志会给出该补的锁定行 |
| `pyserial>=3.4` | 取满足约束的最新版 | 允许。约束在构建期有效 |
| `pyserial==3.5` | 就用这一版 | **推荐** —— 每次构建结果完全一致 |

三种写法最终都会被 **解析成一个具体版本** 并冻结进产物：`dist/*.dist/requirements.txt` 与运行期清单 `REQUIREMENTS` 里都是 `name==version`。所以 **目标机装到的永远是同一份依赖**，差别只在"下一次构建会不会得到另一个版本"。构建日志会直接给出可粘贴的锁定行：

```text
没写版本，按最新解析：pyserial==3.5
推荐锁定（写回 requirements.txt 即可让每次构建结果一致）：pyserial==3.5  pika==1.3.2
```

打包时会把它 **展开成完整依赖树**（包括 `urllib3` / `certifi` 这类间接依赖），展开后的清单随 exe 分发 —— 你在开发机看到的依赖，就是目标机实际跑的那一份。不支持 `-r other.txt`、`git+...`、VCS 与本地路径依赖。

---

## 依赖处理的三种模式

| 模式 | 产物体积 | 目标机首次启动 | 适用场景 |
|---|---|---|---|
| `runtime` | 最小 | 需要联网安装依赖 | 默认，绝大多数情况 |
| `offline` | 中（多了 `offline/` 目录） | 不联网，从随包 wheel 安装 | 内网 / 客户机不能联网 |
| `all` | 最大 | 不联网，依赖已在 exe 里 | 极端自包含需求（编译很慢，实测不划算） |

**构建机至少要联网一次** —— 三种模式的差别只在 **目标机**，打包时必须先展开依赖树（`urllib3` / `certifi` 这类间接依赖也要一起锁死）。`runtime` 模式下这一步 **只取元数据、不下载 wheel**，结果缓存在 `.kkpack/` 下，`--clean` 也不会删。

三种情况例外，会真把 wheel 取到本地（构建日志里会写明是哪一条）：

| 配置 | 为什么非下不可 |
|---|---|
| `[stdlib] include_mode = "precise"` | precise 要扫 wheel 里的 import，才知道该补哪些标准库模块 |
| `mode = "offline"` | wheel 要随程序一起分发出去 |
| `mode = "all"` | wheel 要解压出来编译进 exe |

**精细控制**：默认全部依赖走"运行时安装"，用 `[bundle] include / exclude` 可以单独指定谁进 exe。优先级 `exclude > include > mode`；include 里写 `"*"` 等价于 `mode = "all"`。

```toml
[bundle]
include = ["pillow"]      # pillow 编译进 exe，其余运行时安装
exclude = ["heavy-tool"]  # heavy-tool 一定不进 exe
```

### `runtime` 模式为什么也会下载 wheel：`precise` 的代价

`full`（默认）把整个标准库塞进 exe，**不需要知道你的代码用了哪些标准库模块**，只看依赖元数据就够，一个 wheel 都不用下载。
`precise` 要算出"只补真正用到的那些"，就必须知道 **每个第三方包自己 import 了什么** —— 这些信息只存在于 wheel 的源码里，于是 kkpack 只能先把 wheel 取到 `.kkpack/wheels/`，再逐个解压、AST 扫描（实现见 `backends.ast_stdlib_modules`）。

实测典型工程 `PySide6 + pyqtgraph + scipy`（Python 3.9 / win_amd64）改成 `precise` 后，`.kkpack/wheels/` 里会一次性出现约 **267 MB**：

| wheel | 大小 | 它 import 的标准库（实测片段） |
|---|---|---|
| `PySide6_Addons` | 123.0 MB | `asyncio`、`contextvars`、`concurrent` |
| `PySide6_Essentials` | 78.9 MB | `logging`、`argparse`、`ast` |
| `scipy` | 46.2 MB | `itertools`、`warnings`、`math` |
| `numpy` | 15.9 MB | `subprocess`、`zipfile`、`operator` |
| `pyqtgraph` / `shiboken6` / `PySide6` | 1.9 / 1.1 / 0.5 MB | `weakref`、`marshal`、`base64` |

这不是"`runtime` 失效了"，也不是重复下载，而是 `precise` 换取更小 exe 的 **必要成本**。怎么区分正常与异常：日志里有一行 `必须把 wheel 取到本地：…` 就是正常；看到 `pip download` 的输出却 **没有** 这一行，那才是真出了问题。wheel 下过一次就留在 `.kkpack/wheels/`（`--clean` 也不删），重复构建不会重下。

想省掉这次下载只有把 `include_mode` 改回 `full`（代价是 exe 里带着整个标准库）。**"exe 更小"与"构建期不下 wheel"目前只能二选一。**

---

## GUI 程序（没有黑窗口）

```bash
kkpack main.py --windowed
```

或在配置里 `[tool] console = false`。这会转成 Nuitka 的 `--windows-disable-console` / PyInstaller 的 `--noconsole`。

关掉黑窗口后 `print` 都不再可见，所以 kkpack 在 **依赖安装这一段时间** 用 tkinter 弹一个进度条窗口：下载（显示已下载 MB 数与包进度）→ 安装（逐个包）→ 装完自动关闭，接着启动你的程序。

| 位置 | 说明 |
|---|---|
| `[runtime] progress = true/false` | 是否弹进度条（默认 true） |
| 环境变量 `KK_PROGRESS=0` | 单次运行临时关掉（`0` / `no` / `off` / `false` 都认） |
| `[runtime] clear_console = true/false` | 依赖装完是否清屏（默认 true） |
| 环境变量 `KK_CLEAR=0` | 单次运行临时把安装过程的输出留在屏幕上 |
| tkinter 不可用 | 自动退回控制台输出，不影响安装 |

两点容易误会：

- **进度条和有没有黑窗口无关。** `console = true`（默认）时 **一样会弹** —— 黑窗口里是逐行文字输出，进度条窗口是图形反馈，两者同时存在、互不冲突。有黑窗口又不想弹窗，就把 `[runtime] progress` 设成 `false`。
- **依赖已经装好时不弹。** `_deps/` 里存着安装记录就整段跳过，既不弹窗也不联网 —— 也就是说 **只有"确实有包要装"的那一次运行才会弹**，之后每次启动都是安静的。

> 想确认弹框到底有没有生效：`[tool] console = true` 打成黑窗口版再跑一次首次安装，能同时看到 tkinter 进度条和它前面的 `[kkpack] 需要安装 N 个包…`；如果看到的是 `[kkpack] 依赖已就绪：N 个包已安装，跳过网络请求`，说明依赖早就装好了，本来就不该弹框。
> 实测参照：**Windows x64 + Python 3.9 + Nuitka 2.7.13**，`console = true` 的 PE 子系统为 Console(3) 时进度条照常显示（窗口类 `TkTopLevel`）。

### 打包 Qt / PySide6 程序

PySide6、PyQt5/6、pyqtgraph 这类 Qt 应用不需要额外参数，`kkpack main.py` 直接可用。下面三件事 kkpack 已经替你处理好了，每一条都是真机踩出来的：

1. **`--nofollow-import-to` 的模块名不能带斜杠**。PySide6 官方 wheel 的 `top_level.txt` 里写的是路径（`PySide6/Qt3DCore`），原样传给 Nuitka 会直接 `FATAL: ... not directory path`。kkpack 会统一规范成点号（`PySide6.Qt3DCore`）。
2. **不能把 built-in / frozen 模块当成 `--include-module`**。`_abc` / `_winapi` / `zipimport` 这类由解释器本体提供、没有文件可 include；旧版 Nuitka（< 2.7）收到会在解析 include 列表时内部崩溃（`os.path.abspath(None)`）。kkpack 按 `_imp.is_builtin / is_frozen` 先把它们摘掉。
3. **产物根目录要带 `python3.dll`**。`cp39-abi3` 这类轮子（PySide6 / shiboken6）的扩展链接的是稳定 ABI 转发层 `python3.dll`，**不是** `python39.dll`；目标机上没有 Python，缺它就报 `ImportError: DLL load failed while importing Shiboken: 找不到指定的模块`。kkpack 会从构建机把它一并带进产物。

实测项目：**PySide6 6.7.2 + pyqtgraph 0.13.7 + numpy 2.0.2 + pygame + pyserial + pika**，打包后运行 exe，Qt 窗口正常显示、事件循环正常退出。

> Qt 的插件（`platforms/qwindows.dll` 等）随 wheel 一起装在 `_deps/` 里，所以 **别把 `_deps/` 只当缓存随手清**（删了下次启动会重新联网装一遍）。

### 进度条文案可配置

`[progress]` 段可以按项目改语言；占位符写错会自动退回默认文案，不会让安装崩掉：

```toml
[progress]
title = "Downloading components"
downloading = "Downloading {done}/{total}..."
installing = "Installing..."
format = "{mb:.1f} MB downloaded ({done}/{total})"
installed = "Installed {name}"
```

可用键：`title` / `prepare` / `downloading` / `installing` / `format` / `percent` / `installed` / `package` / `cancel` / `cancelling` / `cancelled` / `failed_title` / `failed_body` / `sources_tried` / `missing_files` / `offline_hint` / `enter_to_exit`。
带占位符的四个：`format`（`{mb}` 已下载 MB、`{done}`、`{total}`）、`percent`（`{value}` 百分比、`{done}`、`{total}`）、`installed`（`{name}` wheel 文件名）、`package`（`{name}`、`{version}`）。

### 进度窗口里放上自己的 logo 和标题

首次运行装依赖的那个窗口，顶部可以显示你自己的品牌图片和标题。两样都 **不配就不显示，也不占位置** —— 没配的窗口和你以前看到的一模一样：

```toml
[tool]
logo = "assets/logo.png"     # png / gif，相对项目根目录解析

[progress]
title = "某某工具箱 v2.0"     # 配了才在窗口里显示；留着默认文案等于没配
```

排版是固定的：**图在上、标题在下，两者水平居中**，底部留一段固定间距接状态文字和进度条。"只有图 / 只有标题 / 两样都有"三种情况都长得规整，不会一个靠左一个靠右，也不会留下一条空白带。

- 图片只接受 **png / gif**：进度窗口用 tkinter 的 `PhotoImage` 显示，它不认 jpg / bmp / svg。写错格式会在 **构建一开始** 就报错并提示转换 —— 喂给 Tk 的坏格式不报错，只是窗口上什么都不显示，那种失败最难查。
- 超过 **320 × 120** 的图会在运行期按整数分之一自动缩小（Tk 没有平滑缩放，只能丢像素），所以给一张大图也不会把窗口撑变形、更不会超出屏幕。
- 图片坏了、丢了、格式不认，一律 **静默跳过**：只显示标题，绝不让依赖装不上。
- 标题和窗口标题栏共用 `[progress] title` —— 配了它，标题栏和窗口内顶部就都是它。

### 依赖安装中途能取消

进度窗口右下角有一个 **取消** 按钮，它和窗口右上角的 **X** 是同一件事：立刻停掉下载、结束进程。

- 已经装好的包 **保持有效**（记录在 `_deps/<py版本-平台>/_installed.json` 里），下次启动只补没装完的那些。
- 中断的那一刻正在解压的包不会留下记录，下次启动会重新解压它 —— 解压本身可重复，不会留下坏状态。
- 退出码是 **130**（128 + SIGINT），和控制台里按 Ctrl-C 的约定一致。
- 没有进度窗口时（`progress = false` 或 `KK_PROGRESS=0`），在控制台按 **Ctrl-C** 走的是同一条路。
- 取消是"直接结束进程"，而不是慢慢等它停下：取消的那一刻下载线程多半正卡在 socket 的 `recv` 上（下大 wheel 时是常态），而 `concurrent.futures` 注册了 atexit，正常退出会去 join 这些线程 —— 实测能一直拖到下载超时（几十秒）。kkpack 的做法是先给工作线程 1 秒自己收尾，超时就直接退出。

---

## 产物名与图标

```toml
[tool]
exe_name = "MyApp"          # 产物（exe）名，不用写 .exe
icon = "assets/app.ico"     # 只支持 .ico，相对路径按项目根目录解析
```

等价命令行：`kkpack main.py --exe-name MyApp --icon assets/app.ico`。

**产物名** 默认取入口文件名（`main.py` → `main.exe`），可以随便起，**含连字符、空格、点、中文都能用**：`My-App`、`app.v2`、`我的工具` 都没问题。会被拒绝的只有 Windows 本身不允许的：`\ / : * ? " < > |`、保留设备名（`CON` / `PRN` / `AUX` / `NUL` / `COM1`-`COM9` / `LPT1`-`LPT9`）、首尾是点号的名字。这些在构建一开始就明确报错，不会等编译器抛一堆看不懂的日志出来。

> **为什么含 `-` 的名字要特殊处理？** Nuitka 在 **目录形式**（`onefile = false`）下会忽略 `--output-filename`，产物名只能取自入口文件名，而这个文件名同时会被解析成模块名，`My-App` 会直接解析失败。kkpack 的做法是：编译期用安全模块名，构建成功后再把目录里那个 `<模块名>.exe` 改名为你要的名字。所以产物名完全不受模块命名规则限制，你只需要在构建日志里看到一行"产物已按 exe_name 改名"就知道生效了。

改名是 **四种组合统一** 的（Nuitka / PyInstaller × 单文件 / 目录）：

| 形态 | 编译后改名 |
|---|---|
| 单文件 | `dist/<模块名>.exe` → `dist/<exe_name>.exe` |
| 目录形式 | `dist/<模块名>.dist/<模块名>.exe` → `dist/<模块名>.dist/<exe_name>.exe` |

**只改 exe 的名字，产物目录名不变。** 目录名保持后端的模块名（`<模块名>.dist`）—— 那是后端自己覆盖、自己维护的目录，改它只会多出一份"上一轮残留"要处理；而目录名对使用者没有意义，拷给别人的是目录里的那个 exe。

**图标** 必须是 `.ico`（Windows 可执行文件的图标格式）。给 png 之类的文件会在构建开始时报错并提示转换，不会等到编译结束才失败。图标会被编译进 PE 资源区。

**首次运行的下载进度窗口也会用这个图标。** tkinter 的窗口图标只能从一个 `.ico` 文件加载（它拿不到 exe 自己的 PE 资源），所以构建时会 **再复制一份图标到 exe 同级**：

```text
dist/main/
├── main.exe          # 图标已编进 PE 资源
└── app.ico           # 同一份图标，供进度窗口使用
```

单文件（`onefile = true`）也一样会多出这个 `.ico` —— 这是让进度窗口不顶着 Tk 默认羽毛图标的唯一办法（onefile 的临时解压目录会被运行期主动排除，打进 exe 反而找不到）。不配置就用系统默认图标，进度窗口也不设图标，不影响其它功能。

**进度窗口顶部的 logo 是另一份文件**（`[tool] logo`），同样会被复制到 exe 同级 —— 它不是 exe 的图标，只是首次装依赖那个窗口上的一张品牌图片，所以格式要求不同（png / gif）。两个都配了就是：

```text
dist/main/
├── main.exe
├── app.ico           # 进度窗口的窗口图标（也是 exe 的图标，已编进 PE 资源）
└── logo.png          # 进度窗口顶部的品牌图片
```

---

## 资源文件随 exe 分发

程序要读的图片、模型、JSON 配置这些 **不是你代码的一部分**，后端不会替你带上。写在 `[tool] resources` 里，kkpack 会在编译完成后把它们复制到 **exe 同级目录**：

```toml
[tool]
resources = [
    "assets",               # 目录：整个递归复制
    "config.ini",           # 文件
    "src/lang -> lang",     # 改名 / 换层级
]
```

等价命令行（可重复，且与配置里的 **合并**、不是覆盖）：`kkpack main.py --resource assets --resource config.ini`。

**目标名默认取源路径的最后一段** —— 于是 `assets/` 落成 exe 同级的 `assets/`，`src/lang` 落成 `lang/`（不会平白多出一层 `src`）。要保持层级、或把两处并到一处，就在 `->` 右边写清楚：

| 配置 | 源 | 复制到（exe 同级） |
|---|---|---|
| `"assets"` | `assets/` | `assets/` |
| `"config.ini"` | `config.ini` | `config.ini` |
| `"src/lang"` | `src/lang/` | `lang/` |
| `"src/lang -> lang/zh"` | `src/lang/` | `lang/zh/` |
| `"C:/models/net.onnx"` | 绝对路径 | `net.onnx` |

需要 `->` 的典型场合是同名撞车（`a/logo.png` 和 `b/logo.png` 都想叫 `logo.png`）—— 撞车会在构建一开始就报错并指出是哪两项，不会静默覆盖掉前一个。每一项的落点也会逐条打印在构建日志里（`资源（目录）：assets -> dist/main.dist/assets`），不用猜。

**为什么复制到 exe 同级，而不是像 `--add-data` 那样打进 exe？** 单文件产物每次运行都要把自己解压到一个临时目录（`_MEIxxxx`），那个路径程序自己都拿不稳，打进去的资源在单文件模式下反而找不到。exe 同级是 **目录形式与单文件形式都成立** 的位置：

```text
dist/
├── main.dist/           # 目录形式（onefile = false）
│   ├── main.exe
│   ├── assets/          # ← 资源在这里
│   └── config.ini
└── main.exe             # 单文件形式（onefile = true），资源与它并排
```

程序里这样定位（上面两种形式都对）：

```python
import os
import sys

base = os.path.dirname(os.path.abspath(sys.executable))
config = os.path.join(base, "config.ini")
```

别用 `__file__` 去拼 —— 冻结之后它指向哪里取决于后端与产物形态（单文件下指向临时解压目录），而 `dirname(sys.executable)` 在四种组合下都是同一个地方；也别用当前工作目录（双击启动、从别处启动、被计划任务拉起时各不相同）。

路径写错 **不会拖到编译完才报**：源不存在、目标重复、目标写成绝对路径或带 `..` 跑出产物目录 —— 这几种在构建第 1 步就明确报错并指出是哪一条。复制过程不做任何过滤（不跳 `__pycache__`、不跳隐藏文件）：静默少拷一个文件比多拷一个文件难查得多，多拷的当场看得见，少拷的要等使用者运行到那一行。

---

## 只交付 exe 的升级包

第一次部署发的是完整产物目录（几百兆），之后每次改代码都重发一遍太浪费。`[tool] exe_only` 把交付物收敛成一个 exe：

```toml
[tool]
exe_only = true
```

等价命令行：`kkpack main.py --exe-only`。

编译完成后，除了照常生成完整产物目录，kkpack 会 **额外** 把这次该覆盖的文件单独放进 `<产物目录>/upgrade/`：

```text
dist/
├── main.dist/               # 完整产物目录：首次部署发这一个
│   ├── main.exe
│   ├── python39.dll         # 运行库
│   ├── _ssl.pyd             # C 扩展
│   └── requirements.txt
└── upgrade/                 # ← 升级包：这次要拷给使用者的东西
    ├── main.exe             # 覆盖已部署目录里的同名 exe
    └── requirements.txt     # 依赖清单（见下）
```

使用者的升级动作就是 **把 `upgrade/main.exe` 覆盖进已部署的 `main.dist/` 目录**（同名覆盖）—— 运行库、C 扩展、标准库、已经装好的依赖（`_deps/`）全都不用动。

### exe 里到底有什么

| 内容 | 位置 | 升级时要不要重发 |
|---|---|---|
| 你写的源码（模块编译产物） | **exe 里** | 要，这就是升级 |
| 用到的标准库模块（编译产物） | **exe 里** | 随 exe 一起变，不用单独管 |
| 运行库 `python39.dll`、C 扩展 `_ssl.pyd` | 产物目录 | 不用（没变） |
| 第三方依赖 | 目标机的 `_deps/` | 不用（缺了会自动装） |

所以 **只有 exe 会因为你改代码而变化**，这就是它能一个文件顶一次升级的原因。只有三种情况必须整目录重发：

- 换了 Python 版本（`python39.dll` 的名字与 ABI 都变了）；
- 换了打包后端（Nuitka ↔ PyInstaller）；
- 新代码 import 了 **旧目录里没有的 C 扩展**（比如以前没用过 `sqlite3` / `_tkinter`）。默认的 `[stdlib] include_mode = "full"` 会把标准库整套收进产物，正常遇不到；用过 `precise` 的项目碰到这种就整目录重发一次。

### 依赖清单也一起拷

`upgrade/requirements.txt` 是这次构建冻结下来的依赖列表。运行期它与 **编进 exe 的清单合并**（同名以 exe 里冻结的版本为准）：

- 拷过去：新增的依赖照常会装；想手工补一个依赖，改这个文件就行，下次启动就装；
- **忘了拷也没关系**：exe 内的清单兜底，程序该装的一个都不会漏。

`exe_only` 与 `onefile` 不能同时用 —— 单文件产物本身就是「一个 exe 就是全部」，而且它里面带着运行库和标准库，正是这个开关想避开的。同时打开会直接报错，由你选一个。

工作流：

1. **首次部署**：`exe_only` 不写或写 `false`，把 `dist/main.dist/` 整个目录发给使用者；
2. **之后升级**：`exe_only = true` 构建，把 `dist/upgrade/main.exe` 覆盖过去（依赖有变化就把 `requirements.txt` 一起覆盖）。

---

## 版本信息与代码签名

这两件事只为一个目的：**让 Windows 和安全软件知道"这个 exe 是谁做的"**。系统里能读到这些信息的只有两处 ——「属性 → 详细信息」里的版本资源，和数字签名。两处都空着的未签名 exe，在安全软件的评分模型里就是个高分可疑文件（见下一节）。

### `[version]`：写进 exe 的 PE 版本资源

```toml
[version]
company = "某某科技有限公司"            # CompanyName
product = "某某工具"                    # ProductName，省略 = exe 名
description = "某某工具主程序"          # FileDescription，省略 = exe 名
version = "1.2.0"                       # FileVersion / ProductVersion
copyright = "版权所有 (C) 2026 某某科技有限公司"
trademark = ""
```

不写也能构建，兜底值保证这一栏 **不会是空的**：产品名和描述退回 exe 名，版本退回 `0.0.0`。唯一不编造的是公司名 —— 在别人的程序里塞一个不存在的公司名，比这一栏空着危险得多；所以完全没配时，构建日志里会有一条提示让你补 `company`。

- `version` 最多 4 段数字（`1.2` / `1.2.0` / `1.2.0.4`），可以带 `v` 前缀。Windows 的版本资源本来就是 4 个 16 位整数，所以 `1.2.3-beta` 只能写进去 `1.2.3` —— 丢掉的后缀会 **单独提示出来**，不静默截断。
- 两个后端写法不同、结果一致：Nuitka 逐项传 `--windows-company-name` 这类参数；PyInstaller 走一个自动生成的 `VSVersionInfo` 文件（`.kkpack/version_info.txt`，纯 ASCII、中文按 `\uXXXX` 转义，免得不同版本的 PyInstaller 在文件编码上打架）。
- 只在 Windows 上生效 —— 其它平台本来就没有这一栏。

### `[sign]`：构建末尾自动签名

```toml
[sign]
certificate = "C:/certs/app.pfx"           # 留空 = 不签名（默认）
password = ""                              # 留空则读环境变量 KK_SIGN_PASSWORD
timestamp_url = "http://timestamp.digicert.com"
signtool = ""                              # 留空自动找 PATH 和 Windows SDK
```

`certificate` 填了，打包的 **最后一步** 就会自动签名；留空就整段跳过、不碰签名。三种写法对应 signtool 的三个参数：

| 写法 | 传给 signtool | 什么时候用 |
|---|---|---|
| `C:/certs/app.pfx` | `/f` + `/p` | 有证书文件（相对路径按项目根目录解析） |
| `CN=某某科技有限公司` | `/n` | 证书已装进「个人」证书存储（硬件令牌、云签名客户端都在那） |
| 40 位十六进制指纹 | `/sha1` | 存储里同名证书有多张，需要精确指定 |

几个刻意的设计：

- **签名在改名之后。** 产物名是编译完后改出来的（见上一节），签名必须落在最终产物上。
- **失败会让构建失败。** 静默发出一份"以为已经签好"的 exe 才是最坏的结果，所以签不上会明确报错并说清原因。临时要跳过就设 `KK_SIGN=0`，或把 `certificate` 留空。
- **密码为空也会显式传 `/p ""`。** signtool 缺 `/p` 会弹一个 GUI 密码输入窗口，在无人值守的构建里就是永久挂起；真挂住了还有超时兜底会报错退出。
- **签名后自动校签**（`signtool verify /pa`）。自签名证书或证书链没装全时校签会报错，但这不影响签名本身有效 —— 所以校签不过只提示、不判失败。
- **时间戳别忘了。** 不签时间戳的话，证书一过期，之前签过的所有版本签名会同时失效。
- 需要 `signtool.exe`，它随「Windows SDK」的 Signing Tools 组件安装（几十 MB，不必装整个 SDK 的编译器）。找不到时会提示装哪个、或把路径写进 `signtool`。云签名（Azure Trusted Signing 等）一般也提供 signtool 兼容的调用方式。

---

## 杀软误报怎么办

exe 被 Defender / 360 / 火绒拦下甚至直接删掉，绝大多数情况不是程序有问题，而是 **它的形态和恶意软件太像**。按下面的顺序从便宜到贵地处理。

先分清是哪种：**被隔离/被删**（提示 `Trojan:Win32/xxx!ml` 这类，看「Windows 安全中心 → 保护历史记录」）是杀软误报；**弹窗问是否允许联网** 是防火墙在问，两者对策完全不同。检测名带 `!ml` 结尾说明是机器学习打的分、不是特征库命中 —— 这种最好治。

**1. 改打包配置（零成本，今天就能做）**。kkpack 的默认值是 **通用** 的，不是 **最不容易被误杀** 的。按收益从高到低：

| 措施 | 为什么 | 代价 |
|---|---|---|
| `onefile = false` | 单文件每次运行都要把自己解压到 `%TEMP%` 再执行 —— 这正是杀软眼里的"释放器"行为 | 分发变成目录 |
| `mode = "offline"` / `all` | `runtime` 模式会在 **首次运行时** 联网下载、解压、再 import，这条"下载即执行"链是行为监控最敏感的 | 产物变大 |
| 填 `[version] company` | 没有公司名的未签名 exe 只能靠文件内容猜 | 一行配置 |
| 填 `[version] version` | `0.0.0` 比一个正经版本号更像没人维护的野程序 | 一行配置 |
| 换 `backend` | Nuitka 的 stub 与 PyInstaller 的 bootloader 被打包器连坐拉黑时，换一个往往直接绕开 | 重编一次 |

`console` / `progress` 这些和误报无关，别在这上面花时间。

**2. 误报申诉（免费，几小时到几天）**。微软：<https://www.microsoft.com/en-us/wdsi/filesubmission>，选 software developer → false positive，会回复检测名和处理结论。国内 360 安全开放平台、腾讯电脑管家、火绒各有独立入口，要分别提。

> **别把样本传 VirusTotal 求"清白"。** VT 会把样本共享给各家引擎，常见效果是让更多杀软更快把这一版拉黑。它适合查已有哈希，不适合提交自己的新版本。

**3. 代码签名（治本）**。见上一节。预期要说清：**签名不是立刻免死金牌** —— OV 证书要靠下载量攒 SmartScreen 信誉，EV 证书才是即时信任；个人开发者申请 OV/EV 通常需要企业资质。但签了之后，"未签名"这个最强的负分项就消失了。

**4. 换个分发形态**。用 Inno Setup / NSIS 把 `xxx.dist/` 打成安装包（装到 `Program Files`、写卸载项）。行为画像立刻从"从临时目录跑起来的裸 exe"变成"正常安装的软件"，顺带解决单文件往 `%TEMP%` 释放文件的观感问题。

---

## 目标机上的行为

```
xxx.dist/
├── xxx.exe
├── requirements.txt          # 冻结的依赖清单
├── _deps/                    # 首次运行时生成（按 py版本-平台 分目录）
│   └── py39-win32-amd64/
└── offline/                  # 仅 offline 模式有：随包 wheel
```

- 依赖装进 `_deps/<py版本-平台>/`，**不会污染目标机的 Python 环境**（也没有 Python 环境）。
- C 扩展自带的 DLL 目录（如 `numpy.libs`）会自动注册，multiprocessing 子进程也会继承。
- 全部依赖都下不到时，会打印（或弹窗）**缺哪些包 + sha256 + 直链 + 离线目录**，把 wheel 放进 `offline/` 目录重启即可，不会悄悄崩掉。
- **首次装依赖的那一串输出，装完会清屏**：那次运行要联网下载安装，步骤提示与进度都打在控制台上，不清掉就和程序自己的输出连成一片。只在真控制台上清 —— 重定向到文件或管道时不动它；零依赖或依赖早就装好时也不会清，那时屏幕上全是程序自己的输出。想留着看就设
`[runtime] clear_console = false` 或 `KK_CLEAR=0`。

---

## 多进程（multiprocessing）

`spawn` / `Pool` 的写法可以直接打包，不用改代码，也不用自己调 `freeze_support()`：

```python
import multiprocessing as mp

def _worker(n):                       # 定义在入口文件里也没问题
    import numpy as np
    return int(np.arange(1, n + 1).sum())

def main():
    ctx = mp.get_context("spawn")     # Windows 上没有 fork，用 spawn 最稳
    with ctx.Pool(processes=2) as pool:
        print(pool.map(_worker, [4, 5]))

if __name__ == "__main__":
    main()
```

冻结后，spawn 的子进程会以 `exe --multiprocessing-fork` 的形式重启 exe。kkpack 在运行期初始化时识别出它，自动完成三件事：

1. **跳过依赖安装** —— 沿用父进程算好的 `_deps` 目录（否则 onefile 每次解压目录不同，子进程会装到又一个新地方）。
2. **重新注册 DLL 目录** —— `os.add_dll_directory` 是进程级的、不会继承，numpy 这类带 `.libs` 的 C 扩展必须在子进程里再注册一次。
3. **把入口文件里的函数与类补回 `__main__`** —— Windows 冻结后 multiprocessing 不会重建 `__main__`（见 multiprocessing.spawn 里的 WINEXE 分支），少了这一步子进程会报 `Can't get attribute '_worker' on <module '__main__' (built-in)>`。

唯一约定：worker 函数要放在 **能被 import 的模块里或入口文件顶层**，不要藏在 `if __name__ == "__main__":` 内部 —— pickle 按引用序列化时会取不到它。

---

## 运行期环境变量（排障/临时覆盖用）

| 变量 | 作用 |
|---|---|
| `KK_INDEXES` | 逗号分隔的镜像源，覆盖打包时配置的源列表 |
| `KK_OFFLINE` | 指向离线 wheel 目录 |
| `KK_PROGRESS` | 设为 `0` 关闭进度条窗口 |
| `KK_CLEAR` | 设为 `0` 保留装依赖时的控制台输出（不清屏） |
| `KK_TRACE` | 设为 `1` 把每次 bootstrap 写进 `_kk_trace.log`，用来查子进程有没有重跑 |

---

## 常见问题

**Q：必须要 `requirements.txt` 吗？**
不必。它是依赖的唯一来源 —— 但 **零依赖也是合法输入**：文件为空、只有注释、甚至完全
没有这个文件，都按零依赖打包，产物是一份只带 Python 运行时与标准库的完整 exe（构建
日志里会写明）。

**代价是 exe 里不含依赖清单**，目标机首次运行不会安装任何依赖。之后要加第三方包，把
包名写进 `requirements.txt` 重新打包；也可以事后把一份 `requirements.txt` 放到 exe
同级 —— 运行期会照它安装（`exe_only` 的升级包就是这么工作的）。

**Q：`runtime` 模式打包时会下载 wheel 吗？**
默认不会（`.kkpack/wheels/` 是空的）——`[bundle] include` 里的包例外，那些必须
下载、要解压出来编译进 exe。解析完整依赖树靠 pip 的 `--dry-run` / `--report`：
只取每个包的 `.metadata`（KB 级），直链与 sha256 由 pip 的 report 给出，
再按文件名从你配的镜像页补一份镜像直链。
两个前提 kkpack 自己兜住了：构建环境的 pip 低于 22.2 时，另外装一份新的到
`.kkpack/pipenv` 供解析使用（**不升级构建环境本身的 pip** —— 那份还要用来跑你的
构建，就地升级在 Windows 上会把环境 pip 装成半个）；源不支持 PEP 658 元数据时
（阿里云、清华目前都不支持）自动用官方 PyPI 兜底解析，**下载仍然优先走你配的镜像**。
两件事都可以在 `[index]` 里关掉：`upgrade_pip = false` / `pypi_fallback = false`，
关掉后 pip 太老或源不支持就只能退回"把 wheel 下载到本地"。

**Q：改成 `include_mode = "precise"` 之后，`runtime` 模式为什么又下载了 200+ MB wheel？**
因为 `precise` 要拆开每个 wheel 看它 import 了哪些标准库模块，才能算出该往 exe 里补哪些，
所以必须先把 wheel 取到 `.kkpack/wheels/`。这是 `precise` 的固有代价，不是 `runtime`
失效，也不是重复下载。详见 [`runtime` 模式为什么也会下载 wheel](#runtime-模式为什么也会下载-wheelprecise-的代价)。
想省掉这次下载就改回 `include_mode = "full"`。

**Q：首次运行时进度窗口关不掉 / 想中断下载？**
现在 **可以**：窗口右下角的 **取消** 按钮和右上角的 **X** 是同一个动作 ——
立即停掉下载并结束进程（退出码 130）。已经装好的包保持有效，下次启动只补没装完的
那些；没有进度窗口时（`progress = false` 或 `KK_PROGRESS=0`）在控制台按 **Ctrl-C**，
走的是同一条路。详见[依赖安装中途能取消](#依赖安装中途能取消)。

**Q：Nuitka 报"没有 C 编译器"？**
正常路径下 **不需要你自己装编译器**：Nuitka 找不到编译器时会自动下载 winlibs MinGW64
（约 200 MB，只下一次），kkpack 已经把 `--assume-yes-for-downloads` 传给它，不会再弹
交互确认。仍然失败的话有两条路：装 MSVC 或 MinGW64 后重跑；或
`kkpack main.py --backend pyinstaller`（不需要 C 编译器，但没有 Nuitka 快、体积也更大）。

kkpack 自己 **不会** 因为"探测不到编译器"就拒绝构建 —— 探测结果只用于提示，选哪个编译器
由 Nuitka 决定（它还会找注册表里的 VS、自己下载过的 winlibs，范围比任何 PATH 检查都宽）。
探测不准时点名即可：`[tool] compiler = "msvc"` / `"mingw64"`，或 `--compiler msvc`。

**Q：Nuitka 说 `Non downloaded winlibs-gcc '...' is being ignored`，转头去下载 winlibs 又失败？**
Nuitka 默认 **只认它自己下载的那份** winlibs MinGW64 —— 你机器上 PATH 里的 `gcc.exe`
会被它主动丢掉（日志里那句 `is being ignored` 就是这个意思）。GitHub 不通或网络不稳时，
它下载的 zip 还可能损坏（`Problem with the downloaded zip file, deleting it.`），构建直接
FATAL，而你的工具链明明就在手边。

配上 `[tool] compiler_dir = "D:/mingw64"`（或 `--compiler-dir D:/mingw64`）即可：
kkpack 会把该目录的 `bin` 顶到构建进程 PATH 的最前面，并传
`--experimental=force-accept-windows-gcc` 让 Nuitka 接受这份非下载的 gcc。

**只有配了它才管用。** 实测（Nuitka 2.7.13，同一份 gcc 16.1.0）：把它放进 PATH 并不会
被采用 —— Nuitka 用的仍是自己下载的 gcc 14.2.0，日志里照旧一句 `is being ignored`；
连设 `CC=gcc` 也一样。那个例外开关是唯一的入口，而 kkpack 只在配了 `compiler_dir`
时才传它 —— 没配的用户继续受 Nuitka 默认那层保护，不会被 PATH 上的野 gcc 悄悄接管。

填的是 **解压后能看见 `bin/` 的那一层**（`bin` 的直接上一层；直接指到 `bin` 本身也认）。
适用于 GCC 家族：MinGW64 / TDM-GCC / w64devkit / msys2 的 ucrt64。三条边界：

- 例外开关只对 gcc 生效，所以工具链必须是 GCC 家族；目录里找不到 `gcc.exe` / `clang.exe`
  时，构建会在 **编译之前** 停下来并说明找过哪些位置，而不是留下一屏日志再去编译。
- 必须让编译器落在 mingw64 —— 机器上装了 MSVC 时 Nuitka 照样优先 MSVC，你那份就白配了。
  kkpack 会自动补 `--mingw64`；你显式写了 `compiler = "msvc"` 时它会直接报错，不假装生效。
- **MSVC 不能用目录指定**：Nuitka 靠注册表和 vswhere 找它，给目录没用。填了会得到解释。
- 那个例外开关 **不是所有 Nuitka 版本都真的认**。2.3.2 里它只让 `is being ignored` 那句
  提示闭嘴 —— 源码里 `compiler_path = None` 缩进挂在了开关判断的外面，PATH 上那份 gcc
  照样被丢掉（实测同一份 gcc 16.1.0：2.3.2 打印的仍是它自己下载的 `gcc 13.2.0`，
  2.7.13 打印 `gcc 16.1.0`）。这种版本上 kkpack 会直接说 `compiler_dir` 不会生效、
  要它生效就升级 Nuitka，而不是让你看着"已加进构建 PATH"以为配好了。

**Q：打包出来的 exe 读不到我项目里的图片 / 配置文件？**
后端只带代码，资源要显式声明 —— 写进 `[tool] resources`（或 `--resource`），
kkpack 会把它们复制到 exe 同级；程序里用 `os.path.dirname(sys.executable)` 定位。
详见[资源文件随 exe 分发](#资源文件随-exe-分发)。

**Q：改了一行代码，要重新发几百兆的整个目录吗？**
不用。打开 `[tool] exe_only`（或 `--exe-only`），kkpack 会把这次要覆盖的文件单独放进
`<产物目录>/upgrade/`：一个 exe，外加 `requirements.txt`。把它覆盖进使用者机器上已部署
的目录就完成了升级，运行库和已经装好的依赖都不用动。
详见[只交付 exe 的升级包](#只交付-exe-的升级包)。

**Q：`--only-binary` 相关的下载失败？**
kkpack 用 `pip download --only-binary=:all:` 取 wheel，只有源码包（sdist）的库会失败。
解决办法：换一个提供了 wheel 的版本，或改用 `--backend pyinstaller` + `mode = "all"` / `include`。

**Q：编译很久 / 内存爆了？**
`--jobs 1` 或 `2`；`include` 里少放包；`mode = "runtime"`（默认）比 `all` 快得多。LTO 是默认关闭的，别打开。

**Q：项目放在中文目录下，Nuitka 编译明明成功了却报 `UnicodeDecodeError`？**
ccache 是原生程序，日志按系统 ANSI 代码页写（中文 Windows 就是 GBK），而 Nuitka
（2.7 以前）用 UTF-8 读这个日志，于是在收尾统计时崩：
`UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd7 ...`。
kkpack 会在构建路径含非 ASCII 字符时自动加 `--disable-ccache`（Nuitka 只在启用 ccache
时才写这个日志，关掉即可），并在日志里打一行提示 —— 中文路径下只是失去 ccache 的
重复编译加速，功能与产物不受影响。

**Q：构建崩在 `Detecting used DLLs`，看到一个路径里全是 `\x04` 的 `AssertionError`？**
同一类 **"Python 装在中文路径下"** 的坑，只是崩在更早的阶段：Nuitka 在 x86_64 上默认用
`depends.exe`（Dependency Walker，2006 年的工具）扫扩展模块的 DLL 依赖，而它处理不了非
ASCII 路径 —— 写出的 `.depends` 文本里 `D:\桌面文件夹\...` 会退化成一串 `0x04` 占位字节，
Nuitka 又用 `latin1` 读这个文件，`os.path.isfile()` 随之断言失败、构建终止。它跟 C 编译器
无关，也跟 `distutils` 无关。

**这条 kkpack 已经替你修好了**：构建路径含非 ASCII 时自动改用 Nuitka **自带的内联 PE 解析器**
（`--experimental=force-dependencies-pefile`，Nuitka 2.6 起提供，arm64 上本来就走这条路），
不额外下载、不引入新依赖；装着的 Nuitka 早于 2.6 时会在构建日志里提示升级，而不是硬崩。

而下面那条 `cannot find -lpython311`，kkpack 也不必让你去搬 Python：MinGW 那条
`-L<Python 目录>\libs` 我们改不了，但 gcc 会把环境变量 `LIBRARY_PATH` 当成 **额外的 `-L`** 交给
`ld` —— 于是 kkpack 把该目录下的 `*.lib` 镜像到一个纯 ASCII 路径再交给链接器。见下面那条 FAQ。

**Q：编译到最后报 `cannot find -lpython311`？**
这是 **Python 本身装在含中文（非 ASCII）的路径下** 导致的，与装没装编译器、Nuitka 是哪个
版本都无关。Nuitka 在 MinGW 模式下要从 `<Python 目录>\libs` 取 `python311.lib`，再把它作为
`-L<那个目录>` 交给链接器（`addWin32PythonLib`）—— 路径里有中文就传不过去，于是走到链接
阶段才报这一句。实测同一个 gcc、同一份 `python311.lib`，只把 `-L` 指向的目录换成英文就
从 rc=1 变 rc=0。

**这条 kkpack 也替你修好了**：gcc 会把环境变量 `LIBRARY_PATH` 当成 **额外的 `-L`** 交给 `ld`，
所以 kkpack 先把 `<Python 目录>\libs` 下的 `*.lib` 镜像到一个纯 ASCII 目录（优先
`<项目>\.kkpack\pylibs`，项目路径本身含中文就退到 `%TEMP%\kkpack-pylibs-<hash>`；同名同大小
的文件不重复复制），再把该目录前置进构建进程的 `LIBRARY_PATH`。构建日志第 4 步会写出来。
镜像做不成（目录不可写等）时才退回原来的两个办法：① 装 Visual Studio 的
**「使用 C++ 的桌面开发」**（Nuitka 优先用 MSVC，`link.exe` 不走这条路）；② 把 Python 装到
纯英文路径（如 `D:\Python311`）后重建虚拟环境。

还有两点值得知道：**虚拟环境会遮住这个问题** —— `sys.prefix` 是英文、`sys.base_prefix` 才是
中文，命令提示符下看着"我全英文了"其实并没有，所以 kkpack 看的是 `base_prefix`；
**`[tool] compiler_dir` 与换编译器版本都解决不了它**，因为问题出在链接器收到的那条 Python
`libs` 路径上，不在编译器装在哪。

**Q：编译时看到 `failure to detect name of distribution ... corruption of its installation`？**
这是 **这台机器的环境坏了**，跟被编译的程序、跟 kkpack 都没关系，而且 **不影响编译**。
Nuitka 启动时会遍历解释器里的发行版元数据（建"顶层包名 → 发行版"映射），碰上 `*.dist-info`
里缺 `METADATA` 的那一份就报这一句，然后跳过它继续。最常见的原因是一次 **被打断的升级**：
实测某个 3.9 环境里 `setuptools-82.0.1.dist-info` 只剩下 `INSTALLER` / `RECORD` / `REQUESTED`，
`METADATA` 与 `WHEEL` 都没了 —— 于是 `setuptools` 的代码是 82.0.1、`pip list` 却报 44.1.1
（另一份完好的旧 dist-info 还在），`pip check` 会给出
`... has requirement setuptools>=70.1.0, but you have setuptools 44.1.1` 这种自相矛盾的结论。

修复就是在那个环境里重装一次：`python -m pip install --force-reinstall setuptools`，并删掉同名的
旧 `*.dist-info`（`pip list` 报的版本以它为准）。kkpack 会在构建日志第 4 步把环境里缺 `METADATA`
的 dist-info 列出来，省得你对着这条 WARNING 猜是谁的问题。

**Q：能不能不用我机器上装的那个后端版本？**
能，而且 kkpack 默认就这么干：**后端版本是构建输入的一部分**，不是"无所谓的环境细节"。
上面两条崩溃在不同 Nuitka 版本上的表现就不一样，同一份配置会一边成功一边失败，
而报错跟版本毫无表面关联 —— 所以 kkpack 声明了自己 **验证过的版本**：

- 后端由 kkpack 安装时，装的就是验证过的那一版（`nuitka==2.7.13` / `pyinstaller==6.22.3`），
  同一个 kkpack 版本在任何机器上都得到同一套后端行为（该版本装不上时退回最新版并说明）；
- 机器上已有别的版本时，构建日志说明差异、给出对齐命令，并在真终端里问一句
  「继续打包？[Y/n]」—— 管道 / CI 里不打断，只提示，绝不把构建挂住。

`[tool] backend_version`（或 `--backend-version`）可以写具体版本号，写 `"any"` 则回到
"不锁也不检查"的老行为；`-y / --yes` 跳过那次询问。`kkpack doctor` 里能直接看到
已装版本与验证版本的对照。

**Q：目标平台和构建平台必须一致吗？**
目前 yes —— wheel 是按构建时的平台/Python 版本下载的（默认 `win_amd64`）。在目标平台上打包，或用对应平台的机器各打一份。

**Q：动态 import 的模块能被打包吗？**
kkpack 会用 AST 扫描你项目里所有 `.py`，识别 `importlib.import_module("xxx")` 并登记，
同时对包用 `--include-package` 收整棵子树。实在扫不到的，构建时会提示"动态导入未解析"，
在 `[bundle] include` 里显式声明即可。

**Q：用了 multiprocessing，报 `Can't get attribute 'xxx' on <module '__main__'>`？**
把 worker 函数从 `if __name__ == "__main__":` 里挪到文件顶层（或独立模块）。
pickle 只能记录函数的引用位置，藏在守卫块里的对象子进程还原不出来。

**Q：`exe_name` 能用中文或连字符吗？**
能，`My-App`、`我的工具` 都可以。产物名只受 Windows 文件名字符限制，不受 Python
模块命名规则限制——kkpack 会用安全模块名编译，结束时再把产物改成你要的名字。
（详见 [产物名与图标](#产物名与图标)。）

**Q：改了 `exe_name` 之后，之前打包的产物还在？**
同名重复构建会 **直接覆盖**（旧产物被整个换掉），所以反复构建不会越堆越多。
但如果把 `exe_name` 换成了新名字，旧名字那份产物不会自动清理，建议先删掉
`output_dir`（默认 `dist/`），否则新旧两份同时存在、容易拷错。

---

## 已知限制

- 打包机需要能访问 PyPI 或镜像源（`runtime` 模式也一样）；元数据解析需要一个支持
  PEP 658 的源，默认用官方 PyPI 兜底（`[index] pypi_fallback = false` 可关）
- 跨平台构建暂不支持（需目标平台 + 目标 Python 版本）
- `exe_only` 的升级包要求目标机上已有一份完整部署，且 Python 版本与后端不变
- 不支持 VCS / 本地路径依赖（`git+https://...`）
- `--windowed` 下运行期无控制台输出，排查请配合 `KK_TRACE=1`

## 环境要求

| 项 | 要求 |
|---|---|
| 操作系统 | **仅 Windows**（依赖 wheel 按 `win_amd64` 取，其它平台不做承诺） |
| Python | 3.8 及以上 |
| 打包后端 | 未安装时构建会自动 `pip install`，nuitka / PyInstaller 都支持 |
| 打包后端版本 | 默认用 kkpack **验证过的版本**（`nuitka==2.7.13` / `pyinstaller==6.22.3`）；你机器上装了别的版本时，构建会说明差异并请你确认（`[tool] backend_version = "any"` 可关掉） |
| C 编译器 | Nuitka 需要 MSVC 或 MinGW64；都没有时它会自动下载 MinGW64（约 200 MB，需联网）。PyInstaller 完全不需要编译器。机器上已有 GCC 家族工具链、但下不动 winlibs 时，用 `[tool] compiler_dir` 指向它即可 |
| kkpack 自身 | 零第三方依赖 |

不确定环境行不行就先跑 `kkpack doctor`，上面这几项它一次体检完 —— 缺后端、缺编译器都
只提示、不判死：后端构建时会自动装，编译器 Nuitka 会自己解决（离线机器除外）。

---

## License

MIT。许可证原文随包分发：sdist 根目录的 `LICENSE`，以及 wheel 里的
`.dist-info/licenses/LICENSE`。

## 关于作者

微信公众号：Python卡皮巴拉

🌟【Python卡皮巴拉】—— 你的Python修炼秘籍，代码界的“神兽”驾到！🌟
