Files
orisys-sdk/docs/publishing.md
T

12 KiB
Raw Blame History

发布流程(构建 → 校验 → 发布)

本文档说明如何构建 orisys 的 Nuitka 二进制 wheel、如何在发布前用一条命令做完整门禁,以及当前仍需人工执行的发布步骤。

历史说明:README.md 与 build/wheel/docker/run.ps1 早就引用了本文件,但它此前并不存在。本文件补齐该引用。


0. 环境准备

推荐用 py310 conda 环境执行构建与校验(它同时具备 pytest / twine / numba / Nuitka / open3d / PyQt5):

conda activate py310
python -m pytest -m "not slow and not hardware"   # 开发期快速回归

py311、py312 仅作为 Nuitka 各 ABI 的编译环境按需调用(build/wheel/win.bat 会依次使用 py310/py311/py312)。

不要在同一个环境里既装旧版 orisys 又从源码目录跑测试:site-packages 里的旧包会静默遮蔽 src/orisys。 tests/test_harness_guards.py 会显式报错提示执行 pip uninstall -y orisys。


1. 构建 wheel

# Windows:py310 / py311 / py312 三个 ABI
python build/run.py wheel-win

# 单个解释器(当前环境)
python build/run.py wheel

# Linux manylinux(Docker Desktop,Linux 容器模式)
python build/run.py manylinux
python build/run.py wheel-manylinux -PythonVersions 3.10

产物落点:

  • SDK/<version>/ — 交付用 wheel(git 忽略)
  • dist/repaired/ — 上传候选(git 忽略)

2. 发布前门禁:release preflight

# --version 可省略:默认取 pyproject 的 dynamic version(即 src/orisys/__init__.py 里的 __version__)
python build/run.py release preflight

按顺序执行 8 个步骤,任一步失败即整体失败(--keep-going 可改为收集全部失败):

步骤 检查内容
repo-state 是 git 仓库、工作树干净、记录 HEAD(--allow-dirty 可豁免,用于提交前预检)
version-consistency src/orisys/__init__.py::__version__ == pyproject 的 dynamic attr == SDK/<version>/ 中 wheel 文件名里的版本
version-strings 追踪的文本文件里没有与当前版本不符的版本字面量(见 §5.3)
duplicate-guard 该版本是否已经发布过:本地 git tag(v<version> / <version>,含 sibling orisys-sdk-release 仓库)命中即拒绝;--check-pypi 时再查 PyPI。SDK/<version>/ 里已有 wheel 只作信息展示——那是 preflight 的正常输入,不是错误
tests pytest -m "not slow and not hardware"(--longrun 时包含 slow)
bundle-precheck 每个 [[tool.orisys.bundle.items]] 至少解析出 1 个文件;finger 必需资产存在
wheel-inspection 逐个 wheel 做静态断言:文件名/版本/METADATA 字段/RECORD 哈希/压缩包路径/体积上限/无 pyc 与缓存/无 orisys/**/*.py 源码泄漏/与源码树逐文件字节一致
wheel-runtime 每个 wheel 一个 venv,在里面依次做:导入冒烟 → 内置 configs/sensors 资产可加载 → 对真实视频跑 40 帧公开流水线(无需相机、无 GUI)

正常顺序是先构建、再 preflight,不需要额外开关:

python build/run.py wheel-win
python build/run.py manylinux
python build/run.py release preflight --all-wheel-pythons --check-pypi

提交前想先看结果(此时工作树通常还是脏的):加 --allow-dirty --keep-going。

产物报告:

  • build/release/reports/<version>/preflight.json — 机器可读,含每个 wheel 的 sha256,供后续发布步骤复用
  • build/release/reports/<version>/preflight.txt — 人工阅读
  • build/release/reports/<version>/junit.xml — pytest 结果

(build/release/reports/ 已加入 .gitignore。)

常用参数

参数 作用
--wheel-dir DIR 校验指定目录下的 wheel(默认 SDK/<version>)
--all-wheel-pythons cp310/cp311/cp312 各自在对应 conda 环境里独立校验
--video PATH / --frames N / --config NAME 覆盖运行时校验输入(默认 data/sample/s1_480p.mp4 / 40 帧 / box)
--offline 复用父环境的 numpy/opencv/numba,不去 PyPI 装依赖
--check-pypi 唯一一处网络调用:只读 PyPI,防止重复发布
--skip-tests / --no-runtime / --no-assets 显式跳过并在报告中留痕;跳过的运行不能作为发布依据
--report-dir DIR 覆盖报告根目录

wheel-runtime 不会静默跳过:找不到视频时直接失败,提示 --video / ORISYS_VERIFY_VIDEO。

单体体检:release audit

对单个 wheel 做静态断言(+ 可选运行时校验),不需要版本上下文。适合体检历史产物:

python build/run.py release audit SDK\<version>\orisys-<version>-cp312-cp312-win_amd64.whl
python build/run.py release audit dist\repaired\orisys-<version>-cp310-cp310-win_amd64.whl --strict-bundle

# 追加运行时校验(--assets 与 --runtime 同时给时共用一个 venv,只装一次依赖)
python build/run.py release audit <wheel> --assets --runtime --video data/sample/s1_480p.mp4

默认对「内置文件与当前源码树不一致」只告警(历史 wheel 与当前分支不一致是正常的);加 --strict-bundle 改成失败(刚构建完的 wheel 应当一致)。

旧的导入冒烟测试仍然可用,行为不变:

python build/run.py verify <wheel>
python build/run.py verify <wheel> --contents          # 静态断言
python build/run.py verify <wheel> --assets --runtime  # 资产 + 真实帧
python build/run.py verify --gui-dist --product ory_finger

3. 发布(目前仍需人工)

阶段一不包含自动发布。当前人工步骤:

  1. 用 release preflight 拿到 PASS(并保留 preflight.json)。
  2. 把 SDK/<version>/ 同步到 orisys-sdk-release 仓库的 release/<version>/。
  3. GitHub:打 tag v<version> 并创建 Release,上传 6 个 wheel 作为附件。
    • 注意历史 tag 里有一个 typo:0.50(与 v0.5.1 指向同一 commit)。
  4. PyPI:先 TestPyPI 再正式仓库。
python -m twine upload --repository testpypi SDK/<version>/*.whl
python -m twine upload SDK/<version>/*.whl

不要把 API token 写进仓库。release preflight --check-pypi 只做只读查询,不需要 token。

已知的历史发布空洞

版本 本地 SDK/ PyPI GitHub Release
0.4.2 — 有 有
0.4.3 — 有 有
0.4.4 有 无 无
0.5.0 有 无 无
0.5.1 有 有 有(tag v0.5.1 + typo tag 0.50)
0.5.2 有 待发布 待发布(release preflight --check-pypi 确认 PyPI 尚无 0.5.2)

本表按 2026 年 0.5.2 构建时的实际状态填写。0.5.1 这一行曾被一次全局替换误改成 0.5.2 并丢失,请勿再用「全局把 0.5.1 替换成新版本号」的方式改本文件——历史事实不应该被改。


4. 环境变量

变量 用途
ORISYS_TEST_VIDEO pytest 用的视频;默认自动取 data/sample/s1_480p.mp4
ORISYS_TEST_CONFIG pytest 用的配置名;默认自动挑一个含 stitcher.box_size 的配置
ORISYS_VERIFY_VIDEO 运行时校验用的视频
ORISYS_VERIFY_CONFIG 运行时校验用的配置名(默认 box)
ORISYS_LONGRUN_BACKEND / ORISYS_LONGRUN_FRAMES slow 长稳测试的参数
CONDA_ENVS_ROOT conda envs 根目录(--all-wheel-pythons 用它解析 py310/py311/py312)

5. 三个必须记住的坑

5.1 Nuitka 编译包的 __file__ 是假的

Nuitka 把整个 orisys 编译成单个扩展模块,其 __init__ 会安装 nuitka_module_loader。于是:

>>> import orisys
>>> orisys.__file__          # 合成的、磁盘上并不存在的路径
'...\\site-packages\\orisys\\orisys\\__init__.py'
>>> orisys.__path__          # 第二项是 Nuitka 的哨兵值
['...\\site-packages\\orisys\\orisys', '\\not_existing']
>>> orisys.__loader__.__class__.__name__
'nuitka_module_loader'

后果:在 Nuitka wheel 里不能靠 orisys.__file__ 或 importlib.resources.files("orisys") 定位内置数据——它们会指向不存在的目录,静默返回「文件不存在」。请改用发行包定位:

from importlib.metadata import distribution
root = distribution("orisys").locate_file("orisys")   # site-packages/orisys

build/common/verify/wheel.py 的内置资产校验就是这么做的,并且会显式断言该目录位于校验用 venv 内,避免被父环境里的旧 orisys 顶替。

5.2 --offline 会被父环境的脏安装污染

--offline 通过 venv 的 --system-site-packages 复用父环境的 numpy/numba 等依赖,这也顺带暴露了父环境里的 orisys。若父环境残留了半卸载的 editable 安装(典型症状是 pip 提示 Ignoring invalid distribution -risys,且 site-packages/orisys/__init__.py 是一个指向死路径的 shim),校验就会读到它。

处置:在构建环境里执行 pip uninstall -y orisys,或改用不带 --offline 的完全隔离 venv(会联网安装依赖)。

5.3 版本号只有一个地方,其他都是推导出来的

版本号的唯一来源是 src/orisys/__init__.py::__version__。以下全部由它推导,不需要手改:

产物 推导方式
pyproject 的 [project] version [tool.setuptools.dynamic] version = {attr = "orisys.__version__"}
wheel 文件名里的版本 build/wheel/build.py 调 get_version()
wheel METADATA 的 Version: 生成的 setup.py 里写入同一个 version
SDK/<version>/ 目录名 同上
importlib.metadata.version("orisys") 从 wheel METADATA 读出

因此不要再往文档、示例、测试里写具体版本号:

  • 用法示例写 SDK/<version>/...、--version <version>,不要写具体版本
  • 文档里不要写「版本:x.y.z」;需要时让用户用 python -c "import orisys; print(orisys.__version__)" 自己看
  • 示例脚本的 docstring 不要再写 SDK version: x.y.z
  • 测试里需要「一个版本」时用明显是假的版本号(如 1.x.y 这种不会与真实版本混淆的值),不要用当前版本 —— 否则每次发版都要改测试,而且一次全局替换就会悄悄改坏断言

release preflight 的 version-strings 步骤强制这条规则:扫描所有被追踪的文本文件,任何与当前版本不符的版本字面量都会导致失败(tests/ 与生成物除外;依赖约束如 numpy>=1.24.4 由正则排除)。

历史记录属于例外,需显式登记在 build/release/version.py::VERSION_STRING_ALLOWLIST 里——登记本身就是「该字面量故意不是包版本」的文档。不要用全局替换修改含历史版本的文件:本文件的发布记录表就曾被一次全局替换改错(0.50 typo tag 被挂到错误的版本上,0.5.1 那一行被覆盖丢失)。

5.4 wheel 里有什么、没有什么

打包内容由 pyproject.toml 的 [[tool.orisys.bundle.items]] 单点定义:

内容 是否进 wheel 说明
config/*.json 是 orisys/configs/,运行时配置解析要用
sensors/** 是 orisys/sensors/,含 finger 的 npz/npy 资产,运行时与 GUI 都要用
examples/** 是 orisys/examples/,用户装完就能跑示例
docs/** 否 只随 GitHub 仓库与 PyPI 项目页提供。docs/publishing.md 是内部流程文档;docs/pypi_readme.md 仅在构建时被读取作为 long description

因为 docs 不进 wheel,改文档不会让已构建的 wheel 失效,不需要重新构建。(改 examples/、config/、sensors/ 会——它们确实在 wheel 里,wheel-inspection 的 bundle-fidelity 会报出来。)

如果以后需要离线 API 参考,加一个只包含 docs/api.md 的窄条目即可,不要整个 docs/ 都塞进去。