你在 Windows 上改 Python 项目,把带原生扩展的包交给云端 Mac 构建,日志已经显示 wheel 生成成功;同事安装后却遇到架构不匹配或动态库找不到。问题通常不在压缩包能否生成,而在包里装了什么、依赖指向哪里,以及测试时是否偷偷借用了源码目录。
下面以包含 pyproject.toml 的现有项目为起点,建立标签检查、原生库检查和隔离安装三道验收。不涉及上传仓库,也不把本机通过当成所有系统都兼容。
一、先固定解释器与验收范围
在远程 Mac 的项目根目录执行以下命令。使用已经安装、符合项目要求的 Python;不要在验收期间顺手升级解释器。
python3 -m venv .wheel-build
.wheel-build/bin/python -m pip --version
.wheel-build/bin/python -c \
'import platform, sys; print(sys.version); print(platform.machine()); print(sys.executable)'
sw_vers
xcodebuild -version
把输出与提交号一起保存。解释器版本、进程架构和系统版本都是验收输入;机器采用 Apple Silicon,并不代表当前 Python 进程一定运行于 arm64。
明确你准备支持哪个 Python 版本、哪个最低 macOS 版本,以及是否包含原生扩展。纯 Python 包也可能依赖原生包,不能只检查顶层项目。
wheel 文件名里的平台标签是兼容性声明,不是转换器。把 arm64 文件改名为 universal2,不会让二进制多出另一种架构。
二、从一次明确的构建开始
先确保 dist 中没有上次留下的包,再生成本次待验收文件。构建后端及其版本应通过项目采用的约束机制固定;默认构建隔离不会自动锁定所有构建依赖。
mkdir -p dist
find dist -maxdepth 1 -name '*.whl' -print
.wheel-build/bin/python -m pip wheel \
--no-deps --wheel-dir dist .
如果第一条检查列出了旧文件,先把它们移到独立归档目录,不要混着验收。--no-deps 仅表示不为运行时依赖生成 wheel,并不禁止构建阶段下载依赖。
以下检查要求目录内恰好只有一个 wheel,并输出内部元数据:
.wheel-build/bin/python - <<'PY'
from pathlib import Path
from zipfile import ZipFile
wheels = list(Path("dist").glob("*.whl"))
if len(wheels) != 1:
raise SystemExit("Expected exactly one wheel")
wheel = wheels[0]
print(wheel.name)
with ZipFile(wheel) as archive:
metadata = [
name for name in archive.namelist()
if name.endswith(".dist-info/WHEEL")
]
if len(metadata) != 1:
raise SystemExit("Invalid wheel metadata")
print(archive.read(metadata[0]).decode("utf-8"))
PY
核对 Tag 与文件名声明是否符合交付目标。py3-none-any 通常用于平台无关包;若包内藏着原生可执行文件,就需要重新审查打包方式,而不是直接相信标签。
三、追查原生库是否借用了构建机
将 wheel 解到临时目录,检查扩展模块与动态库。下面是检查代码,不会执行包内程序:
inspect_dir="$(mktemp -d)"
.wheel-build/bin/python - "$inspect_dir" <<'PY'
from pathlib import Path
from zipfile import ZipFile
import sys
wheel, = Path("dist").glob("*.whl")
with ZipFile(wheel) as archive:
archive.extractall(sys.argv[1])
PY
find "$inspect_dir" -type f \
\( -name '*.so' -o -name '*.dylib' \) \
-exec file {} \; -exec otool -L {} \;
对项目自己的受信产物使用这段解包流程;不要拿未知来源压缩包直接解到工作目录。
重点找用户主目录、构建工作区及本机包管理器前缀。这些绝对路径在另一台机器上往往不存在。系统库路径不能一概当成错误;@rpath、@loader_path 也不自动代表正确,还要检查实际打包的库及其加载路径配置。
修改后必须重新构建再验收。不要只在解包目录修补文件,却交付未经修改的原始 wheel。检查完成后删除本次临时目录。
四、离开源码目录做真实安装
先把 wheel 和全部运行时依赖准备到独立目录。此步骤可能联网;依赖版本应由项目的发布约束控制,并保存最终下载清单。
mkdir -p wheelhouse
.wheel-build/bin/python -m pip download \
--only-binary=:all: --dest wheelhouse dist/*.whl
若某个依赖没有匹配的二进制包,这里应失败。不要悄悄退回源码构建,否则验收目标已经改变。
下面在源码之外创建环境;执行时输入项目真实的导入名,它可能与发行包名不同:
read -r -p "Import module: " module
wheelhouse="$(pwd)/wheelhouse"
wheel="$(find "$(pwd)/dist" -maxdepth 1 -name '*.whl' -print)"
sandbox="$(mktemp -d)"
python3 -m venv "$sandbox/venv"
(
cd "$sandbox"
./venv/bin/python -m pip install \
--no-index --find-links "$wheelhouse" "$wheel"
./venv/bin/python -m pip check
./venv/bin/python -I - "$module" <<'PY'
import importlib
import sys
module = importlib.import_module(sys.argv[1])
print(getattr(module, "__file__", None))
print(list(getattr(module, "__path__", [])))
PY
)
检查导入位置属于新环境,而不是源码树。pip check 只检查依赖声明关系;它不会验证原生库加载,更不会调用懒加载功能。因此还要执行一次真正触发扩展模块的业务操作,例如处理一个固定的小样本并断言结果。
五、把通过条件写进发布记录
每次至少保留提交号、解释器信息、wheel 文件名、摘要值、依赖清单与业务冒烟结果。用 shasum -a 256 dist/*.whl 记录摘要,确保交付的是刚刚测试的同一个文件。
检查失败时按层处理:标签不符回到构建目标,动态库丢失回到依赖收集,只有源码目录内能导入则检查包发现规则与资源声明。不要用扩大搜索路径来掩盖缺文件。
最后,在声明支持的最低系统及其他目标解释器上重复安装与运行。较新系统上的一次成功,只证明当前组合通过;把验证范围写清,比给包贴一个更宽泛的标签可靠。
常见问题
在 Apple Silicon 上构建成功就能标成 universal2 吗?
不能。universal2 需要相关原生二进制包含 arm64 与 x86_64 架构,还要分别验证运行行为;不能通过重命名 wheel 获得兼容性。
为什么导入测试必须离开源码目录?
源码目录可能让 Python 直接加载本地模块,掩盖 wheel 缺文件的问题。应在临时目录的新虚拟环境中安装,并使用隔离模式验证实际导入路径。