云端 Mac 上的 Python wheel 发布验收:标签与原生库

云端 Mac 上的 Python wheel 发布验收:标签与原生库

你在 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 缺文件的问题。应在临时目录的新虚拟环境中安装,并使用隔离模式验证实际导入路径。

把任务交给独享的云端 Mac

租用一台独享物理 Mac mini,按你的工作安排选择节点与计费周期。开通后在控制台查看连接信息;使用中遇到问题,可在控制台提交工单。

选择云端 Mac 方案

按你的工作安排选择节点

先核对配置与周期,再开通你自己的独享物理 Mac mini。