# xlings 2026.7.27.0 — 发布说明

> **版本号改为日期式，`x.y.z` 停用。** 格式 `年.月.日.当日序号`，**月日不补零**。
> 适用于所有位置：`mcpp.toml`、`Info::VERSION`、git tag、索引条目键、发布标题、文档。
>
> **对老客户端安全 —— 已验证。** `self update` 展开为 `install xlings@latest` +
> `use xlings latest`，后者走 `version_key_greater`：按 `.` 分段数值比较，
> `[2026,7,27,0]` 对 `[0,4,68]` 首段就分出大小，所以日期式版本恒大于任何 `0.x`。
> 单元测试 `XvmDateVersioning` 对真实比较器钉住了这一点。
>
> **不补零的真正理由**（写测试时才发现的）：各段数值全等时比较器会退到字符串 tiebreak
> 以保持全序（`sort` 需要）。所以 `2026.07.27.0` 与 `2026.7.27.0` **不是一个键的两种写法，
> 是两个数值相等、靠字节排序的不同键** —— 库里同时存在两者，`latest` 解析到哪个取决于字符串比较。

**日期**: 2026-07-27
**类型**: 发布记录 (release notes)
**集成分支**: `release/0.4.70`
**关联**: `2026-07-26-xvm-pr384-review.md`、`2026-07-26-xvm-group-transaction-design.md`、
`2026-07-26-xvm-group-transaction-plan.md`

---

## 一句话

**`xlings use` 现在要么把整个工具链发布版本切换到位，要么什么都不改并说明原因。**

在此之前它可能把 `gcc` 切到 15、把 `g++` 留在 16、把头文件留在第三个版本上，而且不报任何错。

---

## 用户可见的变化

### 修复

| 变化 | 之前 |
|---|---|
| `xlings use` 切换整个发布版本 | 只切换入口 target，其余成员留在原处 |
| 从任一成员进入结果一致 | 从 `gcc` 和从 `g++` 进入可能得到不同结果 |
| 切换失败时磁盘零改动 | 中途失败留下 sysroot 与 workspace 互相矛盾 |
| 删除后整组一致回退 | 逐 target 独立挑选，可能 `gcc` 用 GCC 而 `g++` 用 musl |
| 删除后幸存版本的头文件被放回 | sysroot 可能一个头文件都不剩 |
| 安装非激活版本不再覆盖 sysroot | 覆盖了 active 版本的头文件，且 `use` 修不回来 |
| 状态文件原子替换 | 写入中断会**清空** `~/.xlings.json`（整个版本库）或用户的 `~/.bashrc` |
| `install`/`remove`/`use` 互斥 | 并发运行时后写的抹掉先写的 |
| `subos` / `config` / `self install` 也走同一把锁 | 它们读改写同一个 `~/.xlings.json`，`subos new` 在 mkfs 期间会把并发 install 的 `versions` 写回旧值 |
| 一个发布声明的**每个**头文件目录都随切换移动 | 只记住最后一个，切换后 sysroot 里混着两个版本的头文件 |
| **库真正随发布版本切换** | `xlings use` 对库是**静默 no-op** —— 规划器读的 `VData::libdir` 全代码库没有写入者 |
| 安装非激活版本不再覆盖激活版本的**库** | 覆盖了，而头文件没动 → sysroot 同时holding两个版本，编译期与链接期不是同一个 |
| `use` 与 install 写同一个 sysroot lib 目录 | `use` 写 `usr/lib`、install 写 `lib` —— 两边不一致（此前因 `use` 那侧是死代码而未暴露） |
| `self doctor` 不再把 release 锚点报成 broken payload | 库-only 包的锚点节点被当成缺失的可执行文件，给出的修复建议修不了任何东西 |
| 失败时给出 code 与可执行 hint | 只有一句 `install failed` |
| `self doctor` 能看见 binding 状态 | 只看 shim 和 payload |
| `self doctor --fix` 可停用不一致的发布 | 无法处理 |
| **`self doctor --fix` 可剪除悬空的 legacy binding 边** | 一条指向不存在版本的旧边会让整个发布拒绝切换，而 doctor 看不见、`--fix` 修不了，只能重装 |
| **`type = "files"` 资产声明** | recipe 只能说"这个目录变成 sysroot 的 include"，别的都表达不了 —— 索引因此长出 7 种各自为政的写文件方式，其中 4 种 xlings 完全看不见 |

### 行为变化（可能被感知为倒退）

**一批过去静默通过的操作现在会明确失败。** 这是有意的 —— 它们此前在静默地损坏状态。

| 情况 | 现在 |
|---|---|
| 同一 exact `(target, version)` 已被别的包拥有 | 拒绝安装，提示先卸载拥有它的包 |
| recipe 声明 headers 但无法判定归属 | 拒绝安装，提示在 header 上指定 group |
| 无版本的删除且存在多个匹配 | 拒绝，要求给出带 namespace 的完整版本 |

每一条都带稳定的 `code` 与一条 hint。发布前已对钉住的官方索引做过审计：**113 个 recipe、89 个使用 xvm，零拒绝**。

其它变化：

- 指向状态文件的**硬链接不再跟随更新**（原子替换换掉了 inode，这正是安全性的来源）
- 在**不可写目录**中改写已存在的状态文件现在会失败（暂存文件需要在该目录创建）
- `XLINGS_NO_LOCK=1` 可绕过状态锁（仅用于诊断卡住的锁，会告警）
- 另一个 xlings 正在改这个安装时，`xlings subos new/rm/use -g`、`xlings config`、
  `xlings self install` 会**明确失败**并提示等待，而不再静默覆盖对方的写入。
  被拒绝的 `subos new` 已经建好了目录 —— 它在注册前是惰性的，重试会直接复用
  （锁只覆盖提交，不覆盖 mkfs，否则隔壁终端的一次普通 install 会被拖到超时）

---

## 升级

**`xlings self update` 即可，无需任何手工步骤。**

- 0.4.69 的 `~/.xlings.json` 直接可读，**无格式迁移**
- 已有 shim 继续工作，不需要重建
- 升级前激活的工具链，升级后仍然激活且版本不变
- **回滚到 0.4.69 是安全的** —— 本版本未改变磁盘格式

---

## 已知限制

诚实列出，不以"可恢复"一笔带过。

| 限制 | 影响 | 计划 |
|---|---|---|
| **元数据损坏的条目仍是死路** | 若 `~/.xlings.json` 中某条目的 `bindingGroup` 字段畸形，`use` 会拒绝，而 `doctor --fix` 修不了它 —— 需要手工编辑 `versions.json` | 0.4.71 |
| `use` 的文件系统副作用非事务 | 决策已与执行分离，失败不再半切换；但提交点之后的文件系统操作仍非原子 | 0.4.71 |
| 无物化台账 | 无法识别 sysroot 中不属于任何包的文件 | 0.4.71 |
| `profile rollback` 不覆盖 xvm 状态 | 回滚不会还原激活的工具链 | 0.4.71 |

### 从 0.4.68/0.4.69 升级后如果某个 `use` 被拒绝

先跑 **`xlings self doctor --fix`**。

老版本写下的 binding 是"成对边"形式，其中可能有指向已不存在版本的悬空边 ——
例如 `gcc@15.1.0` 指向只注册了 16.1.0 的 `xim-gnu-gcc`。0.4.70 的 fail-closed 解析
会因此拒绝整个发布（`xvm-binding-version-missing`），而这在 0.4.68 上是能切的。

这类边描述不出任何可切换的成员，只会造成拒绝，所以 `--fix` 直接剪除，不需要重装。
在一台真实机器上剪除 20 条后 `xlings use gcc 15.1.0` 恢复可用，且 16.1.0
仍然作为完整发布解析。

**元数据损坏的自救方式**：`xlings self doctor` 会打印出错条目与精确到字段的 JSON Pointer
（例如 `/bindingGroup/rootVersion`），据此手工编辑 `~/.xlings.json` 后即可恢复。

---

## 内部变化（不影响使用）

- 存储格式仍是 #384 的 root-manifest 形态。group 归一化为顶层表推迟到 0.4.71，
  届时需要一个兼容读窗口（详见 plan §0.15 的决策与代价记录）。
- 头文件物化改为读 group 声明的 `bindingHeaders`（原先只读单个 `includedir`）。
  `includedir` 仍然写入，作为降级到 0.4.69 的兼容镜像 —— 0.4.69 不认识
  `bindingHeaders`，不写它会让回滚静默停止搬头文件。
  注意：当前官方索引里**没有**任何 recipe 声明头文件目录，`xvm.setup` 每次调用
  也只能声明一个（调用两次会被判为重复注册）。所以这条修的是模型允许、公开
  API 目前还产生不出来的状态 —— 但它已经被持久化、已经会 round-trip，
  留着不消费就是下一个"写了没人读"的字段。
- 六个 CI workflow 现在也在 `release/**` 分支上运行。
- CI 不再在恢复 mcpp 缓存后立即删除其中的 payload —— 此前每次运行都必须重新从各上游
  主机拉取全部传递依赖，任何一个不可达就全线失败。
- 六个 CI workflow 现在也在 `release/**` 分支上运行。

## 从源码构建的贡献者需要注意

**构建工具链要求提高：mcpp `0.0.67` → `0.0.109`。**

不影响使用 xlings 的人 —— 二进制发布不需要 mcpp。只影响从源码构建的人：

```console
$ xlings update && xlings install mcpp@0.0.109 -y && xlings use mcpp 0.0.109
```

原因不是本次改动需要新特性，而是 mcpplibs 索引把客户端下限提到了 `min_mcpp = 0.0.108`。
低于该下限的 mcpp 无法从索引解析任何包 —— 而 0.0.85 之前的版本连这个门禁都不认识，
只会抛出一条**看起来像下载失败**的错误（`error: fetch '<pkg>' failed`），极具误导性。

`.xlings.json` 与六个 workflow 的 `XIM_PKGINDEX_REF` 必须同步升级；索引自己的注释也写明
"Bump min_mcpp ONLY together with the CI MCPP_VERSION pin"。

---

## 验证

| 项 | 结果 |
|---|---|
| 单元测试 | 11 个测试二进制全绿 |
| E2E-32 工具链整组切换 | 5 个场景全过（切换 / 成员入口 / 一致回退 / 拒绝时零改动） |
| 索引审计 | 113 recipe、零拒绝、3 条人工确认无害 |
| 三平台 CI | 见最终 PR |
