# 发布流程(构建 → 校验 → 发布) 本文档说明如何构建 orisys 的 Nuitka 二进制 wheel、如何在发布前用一条命令做完整门禁,以及当前仍需人工执行的发布步骤。 > 历史说明:`README.md` 与 `build/wheel/docker/run.ps1` 早就引用了本文件,但它此前并不存在。本文件补齐该引用。 --- ## 0. 环境准备 推荐用 `py310` conda 环境执行构建与校验(它同时具备 pytest / twine / numba / Nuitka / open3d / PyQt5): ```powershell 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 ```powershell # 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//` — 交付用 wheel(git 忽略) - `dist/repaired/` — 上传候选(git 忽略) --- ## 2. 发布前门禁:`release preflight` ```powershell python build/run.py release preflight --version 0.5.2 ``` 按顺序执行 7 个步骤,任一步失败即整体失败(`--keep-going` 可改为收集全部失败): | 步骤 | 检查内容 | | --- | --- | | `repo-state` | 是 git 仓库、工作树干净、记录 HEAD(`--allow-dirty` 可豁免) | | `version-consistency` | `src/orisys/__init__.py::__version__` == pyproject 的 dynamic attr == `SDK//` 中 wheel 文件名里的版本 | | `duplicate-guard` | `SDK//` 已有产物则拒绝(`--force` 豁免);`--check-pypi` 时只读查询 PyPI,版本已存在则拒绝 | | `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` | 在一次性 venv 中安装该 wheel,断言内置 configs/sensors 资产可加载,并对真实视频跑 40 帧公开流水线(无需相机、无 GUI) | 产物报告: - `build/release/reports//preflight.json` — 机器可读,含每个 wheel 的 sha256,供后续发布步骤复用 - `build/release/reports//preflight.txt` — 人工阅读 - `build/release/reports//junit.xml` — pytest 结果 (`build/release/reports/` 已加入 `.gitignore`。) ### 常用参数 | 参数 | 作用 | | --- | --- | | `--wheel-dir DIR` | 校验指定目录下的 wheel(默认 `SDK/`) | | `--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 做静态断言(+ 可选运行时校验),不需要版本上下文。适合体检历史产物: ```powershell python build/run.py release audit SDK\0.5.2\orisys-0.5.2-cp312-cp312-win_amd64.whl python build/run.py release audit dist\repaired\orisys-0.5.2-cp310-cp310-win_amd64.whl --strict-bundle # 追加运行时校验 python build/run.py release audit --assets --runtime --video data/sample/s1_480p.mp4 ``` 默认对「内置文件与当前源码树不一致」只告警(历史 wheel 与当前分支不一致是正常的);加 `--strict-bundle` 改成失败(刚构建完的 wheel 应当一致)。 旧的导入冒烟测试仍然可用,行为不变: ```powershell python build/run.py verify python build/run.py verify --contents # 静态断言 python build/run.py verify --assets --runtime # 资产 + 真实帧 python build/run.py verify --gui-dist --product ory_finger ``` --- ## 3. 发布(目前仍需人工) 阶段一不包含自动发布。当前人工步骤: 1. 用 `release preflight` 拿到 PASS(并保留 `preflight.json`)。 2. 把 `SDK//` 同步到 `orisys-sdk-release` 仓库的 `release//`。 3. GitHub:打 tag **`v`** 并创建 Release,上传 6 个 wheel 作为附件。 - 注意历史 tag 里有一个 typo:`0.50`(与 `v0.5.2` 指向同一 commit)。 4. PyPI:先 TestPyPI 再正式仓库。 ```powershell python -m twine upload --repository testpypi SDK/0.5.2/*.whl python -m twine upload SDK/0.5.2/*.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.2 | 有 | 有 | 有(tag `v0.5.2` + typo tag `0.50`) | --- ## 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`。于是: ```python >>> 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")` 定位内置数据**——它们会指向不存在的目录,静默返回「文件不存在」。请改用发行包定位: ```python 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(会联网安装依赖)。