237 lines
12 KiB
Markdown
237 lines
12 KiB
Markdown
# 发布流程(构建 → 校验 → 发布)
|
||
|
||
本文档说明如何构建 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/<version>/` — 交付用 wheel(git 忽略)
|
||
- `dist/repaired/` — 上传候选(git 忽略)
|
||
|
||
---
|
||
|
||
## 2. 发布前门禁:`release preflight`
|
||
|
||
```powershell
|
||
# --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**,不需要额外开关:
|
||
|
||
```powershell
|
||
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 做静态断言(+ 可选运行时校验),不需要版本上下文。适合体检历史产物:
|
||
|
||
```powershell
|
||
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 应当一致)。
|
||
|
||
旧的导入冒烟测试仍然可用,行为不变:
|
||
|
||
```powershell
|
||
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 再正式仓库。
|
||
|
||
```powershell
|
||
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`。于是:
|
||
|
||
```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(会联网安装依赖)。
|
||
|
||
### 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/` 都塞进去。
|
||
|