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 在不在,以及哪些依赖没写版本、哪些用了范围约束(两者都允许,只是打包时取到的版本不同)。其中 后端没装、编译器没找到、没有 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")。
配置文件(可选,不写也能跑)
kkpack init # 生成一份带注释的样板
默认查找当前目录下的 kkpack.toml(本工具前身用过的 pyfrost.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。所以 目标机装到的永远是同一份依赖,差别只在"下一次构建会不会得到另一个版本"。构建日志会直接给出可粘贴的锁定行:
没写版本,按最新解析: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 都认) |
[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 已经替你处理好了,每一条都是真机踩出来的:
--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})。
进度窗口里放上自己的 logo 和标题
首次运行装依赖的那个窗口,顶部可以显示你自己的品牌图片和标题。两样都 不配就不显示,也不占位置 —— 没配的窗口和你以前看到的一模一样:
[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 秒自己收尾,超时就直接退出。
产物名与图标
[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 反而找不到)。不配置就用系统默认图标,进度窗口也不设图标,不影响其它功能。
进度窗口顶部的 logo 是另一份文件([tool] logo),同样会被复制到 exe 同级 —— 它不是 exe 的图标,只是首次装依赖那个窗口上的一张品牌图片,所以格式要求不同(png / gif)。两个都配了就是:
dist/main/
├── main.exe
├── app.ico # 进度窗口的窗口图标(也是 exe 的图标,已编进 PE 资源)
└── logo.png # 进度窗口顶部的品牌图片
资源文件随 exe 分发
程序要读的图片、模型、JSON 配置这些 不是你代码的一部分,后端不会替你带上。写在 [tool] resources 里,kkpack 会在编译完成后把它们复制到 exe 同级目录:
[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 同级是 目录形式与单文件形式都成立 的位置:
dist/
├── main.dist/ # 目录形式(onefile = false)
│ ├── main.exe
│ ├── assets/ # ← 资源在这里
│ └── config.ini
└── main.exe # 单文件形式(onefile = true),资源与它并排
程序里这样定位(上面两种形式都对):
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:
[tool]
exe_only = true
等价命令行:kkpack main.py --exe-only。
编译完成后,除了照常生成完整产物目录,kkpack 会 额外 把这次该覆盖的文件单独放进 <产物目录>/upgrade/:
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 就是全部」,而且它里面带着运行库和标准库,正是这个开关想避开的。同时打开会直接报错,由你选一个。
工作流:
- 首次部署:
exe_only不写或写false,把dist/main.dist/整个目录发给使用者; - 之后升级:
exe_only = true构建,把dist/upgrade/main.exe覆盖过去(依赖有变化就把requirements.txt一起覆盖)。
版本信息与代码签名
这两件事只为一个目的:让 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/目录重启即可,不会悄悄崩掉。 - 首次装依赖的那一串输出,装完会清屏:那次运行要联网下载安装,步骤提示与进度都打在控制台上,不清掉就和程序自己的输出连成一片。只在真控制台上清 —— 重定向到文件或管道时不动它;零依赖或依赖早就装好时也不会清,那时屏幕上全是程序自己的输出。想留着看就设
[runtime] clear_console = false或KK_CLEAR=0。
多进程(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_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。
想省掉这次下载就改回 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 分发。
Q:改了一行代码,要重新发几百兆的整个目录吗?
不用。打开 [tool] exe_only(或 --exe-only),kkpack 会把这次要覆盖的文件单独放进
<产物目录>/upgrade/:一个 exe,外加 requirements.txt。把它覆盖进使用者机器上已部署
的目录就完成了升级,运行库和已经装好的依赖都不用动。
详见只交付 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修炼秘籍,代码界的“神兽”驾到!🌟
Metadata
Release files for kkpack 0.1.3
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.3.tar.gz | 278.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| kkpack-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 418.9 kB
Release files / kkpack-0.1.3.tar.gz
| Download URL | kkpack-0.1.3.tar.gz |
|---|---|
| Size | 278.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3238561066b1f197ced36df09d5ce9e05358a395d091f8e5dc15547f91641ecc
|
|
BLAKE2b-256 checksum How to use checksums |
c1f22dc5fa7921bb54740f1553571364b9aae14b27ef8916dd992b87e83d0abe
|
| 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.3-py3-none-any.whl
| Download URL | kkpack-0.1.3-py3-none-any.whl |
|---|---|
| Size | 140.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f83577cef972495bd9ee7859293e86f487737838945e7db931e16c46472f223c
|
|
BLAKE2b-256 checksum How to use checksums |
02ea301a2633e169706204e4dada7fdca50829aa814cc4a861d2eacc1d5e4a27
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.2
|