@@ -96,9 +96,100 @@ acme = { url = "git@gitlab.example.com:platform/mcpp-index.git", branch = "main"
9696| 命令 | 行为 |
9797| ---| ---|
9898| ` mcpp index list ` | 列出所有索引(标注来源: project / global / built-in)。 |
99- | ` mcpp index update [<ns>...] ` | 拉取索引最新提交。` branch ` 跟踪式按 ` git pull --ff-only ` ;` rev ` /` tag ` no-op。 |
100- | ` mcpp index pin <ns> [<rev>] ` | 把当前索引解析到的 commit 写回 ` mcpp.toml ` 作为 ` rev ` 。空 ` <rev> ` = 用当前 HEAD。 |
101- | ` mcpp index unpin <ns> ` | 反向操作:删除 ` rev ` ,改回 ` branch="main" ` 。 |
99+ | ` mcpp index update [<ns>...] ` | 拉远端最新 sha,** 写回 ` mcpp.lock ` ** 。无参 = 更新所有索引;` rev ` /` tag ` 锁定的索引被显式跳过(no-op + 提示)。 |
100+ | ` mcpp index pin <ns> [<rev>] ` | 把当前索引解析到的 commit 写回 ` mcpp.toml ` 作为 ` rev ` (从"软锁"升级为"硬锁",任何机器都能复现)。空 ` <rev> ` = 用 lock 里现行 sha。 |
101+ | ` mcpp index unpin <ns> ` | 反向操作:从 ` mcpp.toml ` 删除 ` rev ` 字段(lock 仍锁,但下次 ` index update ` 又能动)。 |
102+ | ` mcpp update [<pkg>] ` | 重新解析包版本约束(等同 cargo update)。** 会先自动刷新涉及到的索引** ,再重选最高匹配版本,写回 lock。无参 = 全部 dep。 |
103+
104+ > 简记:` build / run / test / pack ` ** 永远不改** ` mcpp.lock ` ;
105+ > ` mcpp update ` / ` mcpp index update ` / ` mcpp index pin ` 是** 仅有的三个**
106+ > 改写 lock 的命令。
107+
108+ ### 2.5 升级与刷新流程的四个典型场景
109+
110+ > 对应"` mcpp.lock ` 锁住后,具体怎么动"的四个典型问题。
111+
112+ #### 场景 A:首次构建,无 lock,无 ` [indices] `
113+
114+ ```
115+ mcpp build
116+ → 内置默认 mcpp index URL,拉远端默认分支 HEAD
117+ → 写入 mcpp.lock 的 [indices.mcpp].rev = <sha>
118+ → 解析依赖,写入 lock 的 [[package]] 段
119+ ```
120+
121+ 之后所有 ` mcpp build ` 都用这个 lock,完全离线复现。
122+
123+ #### 场景 B:索引上游有了新版本(包括官方 mcpp-index 加了新包 / 新版本)
124+
125+ ** 默认行为** :` mcpp build ` ** 看不到** ——它只读 lock 里的旧 sha,旧索引
126+ 里不包含新版本,** 不会报错也不会自动升级** 。
127+
128+ 这是** 特性,不是 bug** : CI / 团队成员 / 老分支构建出来的产物完全可复现,
129+ 不会因为上游某天合了一个 PR 就改变行为。
130+
131+ 要看到新版本,** 必须主动** :
132+
133+ ``` bash
134+ mcpp index update mcpp # 拉新 sha,写回 lock
135+ # ↳ build 现在能看到新版本号了
136+ mcpp build # 但还是用 lock 里旧的 dep 版本
137+ # (因为 mcpp.toml 约束没变,旧版本仍满足)
138+ ```
139+
140+ 如果还想** 升级到新版本** :
141+
142+ ``` bash
143+ mcpp update # 重新解析所有 dep 的版本约束
144+ # 这条会自动先刷索引,再选最高匹配版本
145+ ```
146+
147+ 或单独升级一个包:
148+
149+ ``` bash
150+ mcpp update gtest # 只重选 gtest 的版本
151+ mcpp update mcpplibs:cmdline # 命名空间形式同样支持
152+ ```
153+
154+ #### 场景 C: lock 锁住的 sha 被上游删了 / force-push 没了
155+
156+ 例如官方 index 仓被 force-push,本地 lock 里那个 sha 在远端找不到了。
157+
158+ - ** 本地 ` ~/.mcpp/index/mcpp/<sha>/ ` 已经 checkout 过** :` mcpp build `
159+ 仍然离线工作——sha 在本地缓存里没事。
160+ - ** 从未拉过 / 缓存被 gc 了** :` mcpp build ` 报错:
161+ ```
162+ error: cannot fetch index 'mcpp' at sha abc123de...:
163+ remote no longer has this object.
164+ try: mcpp index update mcpp # to advance to a current sha
165+ ```
166+ ** 不静默漂移** ——必须用户决定怎么办。
167+
168+ #### 场景 D:把"软锁"升级为"硬锁"
169+
170+ ` mcpp.toml ` 没写 ` rev ` 字段时,lock 里有 sha,但 ` mcpp.toml ` 本身没有
171+ 不可变锁定信息——别的机器 clone 项目时,如果删了 lock 重 build,会拿到
172+ ** 当前最新** 的 sha,而不是当时构建时的 sha。
173+
174+ 如果想让 ` mcpp.toml ` 本身也带锁定:
175+
176+ ``` bash
177+ mcpp index pin mcpp # 把 lock 里的 sha 写到 mcpp.toml 的 [indices.mcpp].rev
178+ git add mcpp.toml mcpp.lock
179+ git commit -m " Pin mcpp index to <sha>"
180+ ```
181+
182+ 之后任何机器、任何时刻 clone 这个仓库都拿到完全一样的索引。
183+
184+ 反向(允许 ` mcpp index update ` 推动 sha 前进):
185+
186+ ``` bash
187+ mcpp index unpin mcpp # 从 mcpp.toml 删 rev,lock 仍保留 sha
188+ ```
189+
190+ > 这两条是 cargo 没有的——cargo 的 registry 是"追加式不可变",所以不
191+ > 需要在 ` Cargo.toml ` 里固定 registry 版本。mcpp 的索引是普通 git 仓
192+ > 库,有 force-push / 删 commit 的可能,所以多了"硬锁到 toml"的维度。
102193
103194## 3. 内部模型
104195
@@ -228,6 +319,67 @@ mcpp build
228319 └── 5. emit ninja, link
229320```
230321
322+ ## 4.5 不可变性的两层保证
323+
324+ ** 用户最在意的不是"今天的 gtest@1.15.2 跟明天的 gtest@1.15.2 是不是
325+ 同一个文件"——这一层 mcpp 后期的发布政策会保证不可变。真正担心的
326+ 是:索引仓库里那个 ` gtest.lua ` 描述符** 今天写的 url + sha256,明天会
327+ 不会被悄悄改成别的东西** 。两层是分开的:
328+
329+ | 层 | 保护对象 | 提供方式 | 失败时的影响 |
330+ | ---| ---| ---| ---|
331+ | ** L1:包发布不可变性** (policy) | 已发布的 ` <ns>:<name>@<ver> ` 对应的 url + sha256 + 名字空间永不改写;只能新增版本,不能修改也不能删除已发布版本。 | mcpp-index 仓库的 CI gate(已发布版本字段被改 → reject PR);` mcpp publish ` 客户端拒绝覆盖。 | 一旦违反,使用旧描述符的 build 会拿到不一致的二进制,极难追溯。 |
332+ | ** L2:索引快照锁定** (mechanism) | 当前项目"看到"的索引 commit sha 在 ` mcpp.lock ` 里固定。 | ` mcpp build ` / ` run ` / ` test ` / ` pack ` 默认离线读 lock 的 sha,不联网。 | 即使 L1 政策被违反或上游 force-push,** 已锁住的项目继续用旧索引,完全不受影响** ——只有显式 ` mcpp index update ` 才会拉到新内容。 |
333+
334+ L1 是承诺,L2 是兜底。两层的关系类似 HTTPS 里"CA 不签发流氓证书"
335+ (policy)+ " 客户端 pin 住已知证书" (mechanism)——任何一层成立,
336+ build 就稳定;两层都失效才会出问题。
337+
338+ ### L1 — 后期 mcpp-index 仓的发布政策
339+
340+ 设计文档不强制实现 L1 检查(那是 mcpp-index 仓 CI 的事),但设计层
341+ 面记下假设,给 L2 兜底逻辑做参考:
342+
343+ - **新增**(`pkgs/.../<name>.lua` 中追加 `add_versions(" 0.0.3" , ...)`)
344+ → 允许,合法 PR。
345+ - **修订已发布版本**(改 `0.0.2 ` 那行的 sha256、url、build flags)
346+ → CI gate 必须 reject。如果 sha256 真的算错了,**只能发** `0.0.2 -fix`
347+ 之类的新版本,不能就地改 `0.0.2 `。
348+ - **删除已发布版本** → reject。极端情况(法律 / 安全)走人工 yank 流
349+ 程,版本仍保留但标 yanked,新解析跳过它。
350+ - **整个包改名 / 改 namespace ** → 等同删除 + 新增,要求作者发明确新版
351+ 本号 + 旧版本保留不删。
352+
353+ > mcpp 后期可以再发一个独立 spec(类似 `xpkg-publish-policy`)详细
354+ > 列举可修字段集 / 不可修字段集,本 PR 不展开。
355+
356+ ### L2 — `mcpp.lock` 的索引 sha 锁
357+
358+ L2 不依赖 L1 是否真被遵守:**任何时刻,build 看到的索引描述符 = lock
359+ 里那个 commit 的 checkout**。所以:
360+
361+ - L1 完美遵守 → L2 锁的 sha 解析出来的描述符 = 远端最新一致(锁多余但
362+ 无害)。
363+ - L1 偶发违反(典型场景:误 force-push、维护者手抖) → L2 仍指向旧
364+ sha 的 checkout,旧描述符内容不变,build 完全不感知。
365+ - L1 故意违反(攻击) → 同上,L2 给一段缓冲时间;同时 mcpp 可在
366+ `mcpp index update` 时检测" 远端历史被改写" 并报警。
367+
368+ **对用户问题的直接回答**:
369+
370+ > " 如果包索引仓的包描述发生变化了呢?"
371+
372+ `mcpp build` 不受影响——lock 锁的 commit sha 还指向旧 checkout,
373+ 旧 checkout 里的描述符内容不变(git object 是 content-addressed,不会
374+ 被远端改写)。除非用户主动 `mcpp index update` 拉到新 sha,才会看到
375+ 新内容;即便看到了,L1 政策要求新内容只是" 新增版本" ,不会改写已选
376+ 的那个版本——所以已选 dep 仍稳定。
377+
378+ > " 对于具体的包发布,后期 mcpp 也是固定不可回退的"
379+
380+ 正是 L1 的承诺。L2 是物理层兜底,即使 L1 (人为 / 流程)出错,build
381+ 也能稳定到下一次显式 update。
382+
231383## 5 . 兼容性 / 迁移
232384
233385### 5.1 现有项目(无 `[indices]`)— 隐式默认 + 强制 lock pinning
@@ -394,11 +546,35 @@ PR-A 已经引入 `(namespace, name)`。本 PR 把 namespace 跟"索引来源"
394546
395547| 维度 | mcpp ` [indices] ` | cargo ` [source] ` | npm ` .npmrc ` |
396548| ---| ---| ---| ---|
397- | 锁定粒度 | git commit sha | crates.io 时间戳 | tarball sha512 |
549+ | 锁定粒度 | git commit sha + 包 checksum | 包版本 + 包 checksum | tarball sha512 |
398550| 多源 | namespace 路由 | replace-with 链 | scoped registry |
399551| 私有 | git URL + ssh | sparse-index + token | scoped registry + token |
400552| 离线 | 本地 path 索引 + 缓存 | ` --offline ` + 缓存 | ` --offline ` + 缓存 |
401-
402- mcpp 的优势:** git 原生** ——不需要发明索引格式 / 索引服务,clone 一
403- 个仓就是一个索引;劣势:无 fast-path 增量(每次拉全仓)。规模大到这
404- 个成为瓶颈时再考虑 sparse-index / 索引镜像。
553+ | 刷新触发 | ` mcpp index update ` / ` mcpp update ` (显式) | 每次 ` cargo build ` 自动 fetch index(可 ` --frozen ` 关) | 每次 ` npm install ` 自动查 registry(可 ` --offline ` 关) |
554+ | 已发布不可变 (L1 policy) | mcpp-index 仓 CI gate + ` mcpp publish ` 拒绝覆盖(后期承诺) | crates.io 服务端强制 | npm registry 服务端强制(标 deprecated 不算改写) |
555+ | 索引快照锁 (L2 mechanism) | ` mcpp.lock [indices.<ns>].rev = <sha> ` | 无(L1 已足够,registry 服务承诺 immutable) | 无(同上) |
556+ | 配置层硬锁 | ` mcpp.toml [indices.<ns>].rev = "<sha>" ` 把 lock 抬到 toml | 无(不需要) | 无(不需要) |
557+
558+ ** 核心差异** : cargo 由 crates.io 这一个** 受控服务** 单独承担"已发布
559+ 不可变"承诺;mcpp 选了** 双保险** ——L1 后期由 mcpp-index 仓的发布政
560+ 策 + CI gate 承担(类似 crates.io),L2 在客户端 lock 文件里再锁一层
561+ 索引 commit sha。
562+
563+ 为什么多一层 L2?因为 mcpp 的索引是普通 git 仓库——任何人 fork 一个
564+ 就是新索引,官方仓也理论上可能 force-push;不像 crates.io 那样有服务
565+ 端硬约束。L2 给两类用户兜底:
566+
567+ 1 . 用第三方 / 私有索引的项目(` [indices.acme] = ... ` )——第三方维护
568+ 方未必有 L1 级别的 CI gate,L2 让 lock 锁住自己当时 build 用的索引
569+ sha。
570+ 2 . 即使是官方索引,L1 真的偶发出错时(误 force-push、签错 sha256
571+ patch),已经在 CI / 用户机器上锁住的项目不受影响,只有显式 update
572+ 才会拉到新内容。
573+
574+ 代价:用户多了一个心智负担——"什么时候该跑 ` mcpp index update ` "。文
575+ 档里要有明显的 onboarding 提示;典型答案是"开发期跟着自己节奏更新,
576+ 合并到 main 之前 lock 跟着 PR 一起进 git,CI 必走 lock"。
577+
578+ mcpp 的工程优势:** git 原生** ——不需要发明索引格式 / 索引服务,clone
579+ 一个仓就是一个索引;劣势:无 fast-path 增量(每次拉全仓)。规模大到
580+ 这个成为瓶颈时再考虑 sparse-index / 索引镜像。
0 commit comments