kkpack
把 Python 项目打包成 Windows 上自带解释器的 exe,第三方依赖在目标机首次运行时按需安装。
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
pip install kkpack # 第 1 步:安装 kkpack
cd myapp
kkpack doctor # 第 2 步:体检环境(可选,第一次建议跑)
kkpack main.py # 第 3 步:打包
doctor 会告诉你:有没有 C 编译器、后端装没装、requirements.txt 在不在,以及哪些依赖没写版本、哪些用了范围约束(两者都允许,只是打包时取到的版本不同)。
第 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 |
--mode runtime|offline|all |
依赖处理方式 | runtime |
--onefile / --no-onefile |
单 exe / 目录形式(目录启动更快,推荐) | 目录形式 |
--windowed / --console |
是否显示控制台黑窗口(GUI 程序用 --windowed) |
--console |
--out DIR |
产物输出目录 | dist |
--exe-name NAME |
产物(exe)名,不用写 .exe |
入口文件名 |
--icon PATH.ico |
程序图标,只支持 .ico |
系统默认图标 |
--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 init # 生成一份带注释的样板
默认查找当前目录下的 kkpack.toml(本工具前身用过的 pyfrost.toml 同样接受)。
下面这份 全部可以省略:
[tool]
backend = "nuitka" # nuitka | pyinstaller
entry = "main.py" # 命令行给了入口就以命令行优先
exe_name = "MyApp" # 产物(exe)名,不用写 .exe;默认取入口文件名
icon = "assets/app.ico" # 程序图标,只支持 .ico;不写用系统默认图标
onefile = false # 目录形式启动更快
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 # 并发下载数
[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 里,不要在配置文件里重复声明一遍 —— 避免两处版本号不一致这种最难查的问题。
版本 不强制锁定,三种写法都收:
| 写法 | 构建期行为 | 建议 |
|---|---|---|
pyserial |
取 当时的最新版 | 允许。构建日志会给出该补的锁定行 |
pyserial>=3.4 |
取满足约束的最新版 | 允许。约束在构建期有效 |
pyserial==3.5 |
就用这一版 | 推荐 —— 每次构建结果完全一致 |
三种写法最终都会被 解析成一个具体版本 并冻结进产物:dist/*.dist/requirements.txt 与运行期清单 REQUIREMENTS 里都是 name==version。所以 目标机装到的永远是同一份依赖,差别只在"下一次构建会不会得到另一个版本"。构建日志会直接给出可粘贴的锁定行:
没写版本,按最新解析: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"。
[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 程序(没有黑窗口)
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 都认) |
| 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 已经替你处理好了,每一条都是真机踩出来的:
--nofollow-import-to的模块名不能带斜杠。PySide6 官方 wheel 的top_level.txt里写的是路径(PySide6/Qt3DCore),原样传给 Nuitka 会直接FATAL: ... not directory path。kkpack 会统一规范成点号(PySide6.Qt3DCore)。- 不能把 built-in / frozen 模块当成
--include-module。_abc/_winapi/zipimport这类由解释器本体提供、没有文件可 include;旧版 Nuitka(< 2.7)收到会在解析 include 列表时内部崩溃(os.path.abspath(None))。kkpack 按_imp.is_builtin / is_frozen先把它们摘掉。 - 产物根目录要带
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] 段可以按项目改语言;占位符写错会自动退回默认文案,不会让安装崩掉:
[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})。
依赖安装中途能取消
进度窗口右下角有一个 取消 按钮,它和窗口右上角的 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 秒自己收尾,超时就直接退出。
产物名与图标
[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 同级:
dist/main/
├── main.exe # 图标已编进 PE 资源
└── app.ico # 同一份图标,供进度窗口使用
单文件(onefile = true)也一样会多出这个 .ico —— 这是让进度窗口不顶着 Tk 默认羽毛图标的唯一办法(onefile 的临时解压目录会被运行期主动排除,打进 exe 反而找不到)。不配置就用系统默认图标,进度窗口也不设图标,不影响其它功能。
版本信息与代码签名
这两件事只为一个目的:让 Windows 和安全软件知道"这个 exe 是谁做的"。系统里能读到这些信息的只有两处 ——「属性 → 详细信息」里的版本资源,和数字签名。两处都空着的未签名 exe,在安全软件的评分模型里就是个高分可疑文件(见下一节)。
[version]:写进 exe 的 PE 版本资源
[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]:构建末尾自动签名
[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/目录重启即可,不会悄悄崩掉。
多进程(multiprocessing)
spawn / Pool 的写法可以直接打包,不用改代码,也不用自己调 freeze_support():
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 在运行期初始化时识别出它,自动完成三件事:
- 跳过依赖安装 —— 沿用父进程算好的
_deps目录(否则 onefile 每次解压目录不同,子进程会装到又一个新地方)。 - 重新注册 DLL 目录 ——
os.add_dll_directory是进程级的、不会继承,numpy 这类带.libs的 C 扩展必须在子进程里再注册一次。 - 把入口文件里的函数与类补回
__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_TRACE |
设为 1 把每次 bootstrap 写进 _kk_trace.log,用来查子进程有没有重跑 |
常见问题
Q:必须要 requirements.txt 吗?
是,且不能为空。它是依赖的唯一来源,没有它 kkpack 不知道目标机该装什么。
(完全没有第三方依赖的项目,暂不支持——后续版本会放开。)
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。
想省掉这次下载就改回 include_mode = "full"。
Q:首次运行时进度窗口关不掉 / 想中断下载?
现在 可以:窗口右下角的 取消 按钮和右上角的 X 是同一个动作 ——
立即停掉下载并结束进程(退出码 130)。已经装好的包保持有效,下次启动只补没装完的
那些;没有进度窗口时(progress = false 或 KK_PROGRESS=0)在控制台按 Ctrl-C,
走的是同一条路。详见依赖安装中途能取消。
Q:Nuitka 报"没有 C 编译器"?
装 MinGW64 或 MSVC,或者直接用 kkpack main.py --backend pyinstaller(PyInstaller 不需要编译器,但没有 Nuitka 快、体积也更大)。
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:目标平台和构建平台必须一致吗?
目前 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/),否则新旧两份同时存在、容易拷错。
已知限制
requirements.txt必须存在且非空- 打包机需要能访问 PyPI 或镜像源(
runtime模式也一样);元数据解析需要一个支持 PEP 658 的源,默认用官方 PyPI 兜底([index] pypi_fallback = false可关) - 跨平台构建暂不支持(需目标平台 + 目标 Python 版本)
- 不支持 VCS / 本地路径依赖(
git+https://...) --windowed下运行期无控制台输出,排查请配合KK_TRACE=1
环境要求
| 项 | 要求 |
|---|---|
| 操作系统 | 仅 Windows(依赖 wheel 按 win_amd64 取,其它平台不做承诺) |
| Python | 3.8 及以上 |
| 打包后端 | Nuitka 需要 MSVC 或 MinGW64;PyInstaller 不需要 C 编译器 |
| kkpack 自身 | 零第三方依赖 |
不确定环境行不行就先跑 kkpack doctor,上面这几项它一次体检完。
License
MIT。许可证原文随包分发:sdist 根目录的 LICENSE,以及 wheel 里的
.dist-info/licenses/LICENSE。
关于作者
微信公众号:Python卡皮巴拉
🌟【Python卡皮巴拉】—— 你的Python修炼秘籍,代码界的“神兽”驾到!🌟
Metadata
Release files for kkpack 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| kkpack-0.1.1.tar.gz | 179.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| kkpack-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 275.6 kB
Release files / kkpack-0.1.1.tar.gz
| Download URL | kkpack-0.1.1.tar.gz |
|---|---|
| Size | 179.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f42ed781bed529976a8e664d70e7a9fe760f293cd16366a355ebfff3e3ae755e
|
|
BLAKE2b-256 checksum How to use checksums |
e0ee5300be02147f9404dd8e4071de59eace1278957d6a6d451f7820d2b5d847
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.2
|
Release files / kkpack-0.1.1-py3-none-any.whl
| Download URL | kkpack-0.1.1-py3-none-any.whl |
|---|---|
| Size | 95.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fd46687046127efbeebaabb01301c63f3919dd3392c8c30e4a64ee8d921766b6
|
|
BLAKE2b-256 checksum How to use checksums |
177050fc7db9b65d1dfe9e219921d003c8d57f8392a10b9861748341fadf34b7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.2
|