← 返回 Day 181 正文

Day 181 · 图解

Day 181 — 原理图解

五张图,从「版本号怎么比大小」到「一个文件怎么变成 PyPI 上的一行」。


图 1 · 语义化版本的优先级阶梯

  优先级 低 ───────────────────────────────────────────────────▶ 高

  1.0.0-alpha
      │  数字段按数值比,字母段按字典比
      │  前缀相同的,段数少的更小
      ▼
  1.0.0-alpha.1
      ▼
  1.0.0-alpha.beta
      ▼
  1.0.0-beta
      ▼
  1.0.0-beta.2
      ▼
  1.0.0-beta.11      ← 注意:11 > 2(数值比较),不是字典序
      ▼
  1.0.0-rc.1
      ▼
  1.0.0              ← 正式版永远高于自己的任何预发布

  ┌────────────────────────────────────────────┐
  │ 1.0.0+build.1  ==  1.0.0+build.999          │
  │ 构建元数据不参与比较(规范第 10 条)         │
  └────────────────────────────────────────────┘

一句话:预发布是「还没好」,+build 只是备注。比较时只看前三段 + 预发布。


图 2 · 六步发布链路(哪一步是不可逆的)

   ① 打 tag            git tag v1.2.3
        │
        ▼
   ② 校验一致性        tag 版本 == pyproject version ?
        │               ✗ 不一致 → 就此终止(连构建都不做)
        ▼
   ③ 构建              python -m build  →  dist/*.whl + dist/*.tar.gz
        │
        ▼
   ④ 校验产物          文件名 / METADATA / wheel 与 sdist 版本一致 / twine check
        │               ✗ 任一失败 → 就此终止
        ▼
   ⑤ 上传              POST /legacy/  (multipart + Basic __token__)
        │
        ═════════════════════════════  ← 分水线:从这里开始不可逆
        │
        ▼
   ⑥ 索引生效          PyPI 页面 / JSON API / pip install

   为什么第 ⑤ 步是分水线:
   PyPI 不允许覆盖同名文件 → 传错的 1.2.3 收不回来
   → 只能在 1.2.4 里修 → 所以校验必须全部发生在 ⑤ 之前

一句话:流水线的形状不是随便定的,是被「不可逆」这个约束逼出来的。


图 3 · 前端、后端与两种产物

                     源码(pyproject.toml + 包目录)
                                │
                                │  谁来执行构建?
                                ▼
        ┌───────────────────────────────────────────────┐
        │  python -m build   ← PEP 517 前端              │
        │    1. 建隔离环境(可选,--no-isolation 可跳)   │
        │    2. 装 build-system.requires 里的后端         │
        │    3. 调用后端的钩子                            │
        └───────────────────┬───────────────────────────┘
                            │ 调
                            ▼
        ┌───────────────────────────────────────────────┐
        │  setuptools.build_meta  ← PEP 517 后端         │
        │    build_sdist()   build_wheel()               │
        │                                                │
        │  ★ 前端不在时,本日脚本直接调这里(降级路径)    │
        │    实测:后端直调 513 ms vs 前端 1921 ms        │
        └───────────────────┬───────────────────────────┘
                            │
                ┌───────────┴────────────┐
                ▼                        ▼
      demo_release_pkg-1.2.3.tar.gz   demo_release_pkg-1.2.3-py3-none-any.whl
        (sdist:源码 + 构建配置)        (wheel:已构建,装了就能用)
        安装时需现场构建                  安装时直接解压
        实测 1222 字节(浮动 ±20)         实测 1458 字节(稳定)

  注意:文件名是可解析的元数据
    {归一化名}-{版本}(-{build})?-{python}-{abi}-{platform}.whl     (PEP 427)
    {归一化名}-{版本}.tar.gz                                       (PEP 625)
    归一化 = 小写 + 连续 - _ . 压成单个 -                            (PEP 503)

一句话:前端是壳,后端是干活的;产物文件名本身就是契约的一部分。


图 4 · 一条提交怎么变成 CHANGELOG 的一行

  git 提交(唯一的「人写」环节)
  ┌──────────────────────────────────────────────────────┐
  │ 8b7a6f5 feat(cli)!: 把 --output 参数改名为 --format    │
  │                                                       │
  │ BREAKING CHANGE: --output 已移除,请改用 --format      │
  └──────────────────────────────────────────────────────┘
        │
        │ ① 按行首模式切分(7-40 位十六进制 sha + 主题)
        │    ★ 不能按空行切:git log 在提交之间不加空行
        ▼
  ┌───────────────────────────────────────────────┐
  │ type=feat  scope=cli  bang=! (正文另有 BREAKING)│
  └───────────────┬───────────────────────────────┘
                  │
      ┌───────────┴────────────┐
      ▼                        ▼
  ② 分类                    ③ 版本决策
  breaking 优先 → Changed    出现过 breaking → MAJOR +1
  feat → Added               否则有 feat     → MINOR +1
  fix  → Fixed               否则            → PATCH +1
  docs/chore/ci → 丢弃       (0.x 项目另按惯例)
      │                        │
      └───────────┬────────────┘
                  ▼
  ④ 渲染
  ## [2.0.0] - 2026-10-16
  ### BREAKING CHANGES
  - 把 --output 参数改名为 --format (8b7a6f5)
  ### Added
  - **api**: 新增分页参数 page/page_size (a1b2c3d)
  ### Changed / ### Fixed …
  [2.0.0]: https://…/compare/v1.1.4...v2.0.0      ← compare 链接

  实测:7 条提交 → 跳过 2 条(docs/chore)→ 1.1.4 决策为 2.0.0

一句话:提交守规矩,CHANGELOG 和版本号就都能算出来;! 与 BREAKING CHANGE: 两种写法都要认。


图 5 · 上传协议时序(客户端 ↔ 索引)

   客户端(twine / 本日 04 脚本)              索引(PyPI / 本地桩)
        │                                          │
        │  ① POST /legacy/                         │
        │     Content-Type: multipart/form-data    │
        │     Authorization: Basic                 │
        │        base64("__token__:pypi-****")     │
        ├─────────────────────────────────────────▶│
        │                                          │  ② 认证头解析
        │                                          │     user=__token__ ?
        │                                          │     secret 以 pypi- 开头 ?
        │                                          │        ✗ → 403(实测拿到过)
        │                                          │
        │     multipart 字段:                      │  ③ 解析 multipart
        │       :action=file_upload                 │     取 metadata / content
        │       protocol_version=1                  │
        │       metadata=<core metadata 文本>       │  ④ 算 sha256(content)
        │       sha256_digest=<摘要>                │     与 sha256_digest 比
        │       content=<文件字节>                  │        ✗ → 400(桩实测过)
        │                                          │
        │  ⑤ 200 {"message": "OK"}                 │  ⑤ 落库 + 生成索引条目
        │◀─────────────────────────────────────────│
        │                                          │
        │  ⑥ GET /pypi/<name>/json  ← 发布后校验    │
        │     查文件名、size、sha256 对不对          │
        ├─────────────────────────────────────────▶│
        │                                          │

  实测边界:假 token → 403;本地桩篡改摘要 → 400;本地桩正常上传 → 200
  未实测:真实 token 的成功上传(见 README 实验 6)

一句话:上传不是「发个文件就完了」——认证、完整性、协议字段三关, 任何一关不过都必须在「不可逆」之前暴露出来。


一句话速记

图 一句话
1 预发布低于正式版;+build 不参与比较;数字段按数值比
2 校验必须全部在「上传」之前,因为上传不可逆
3 build 是前端、setuptools 是后端;文件名就是元数据
4 提交守规矩 → CHANGELOG 与版本号都能自动推导
5 认证 → 完整性 → 落库,三关都在不可逆点之前