diff --git a/.agents/docs/2026-08-17-547-and-three-merged-prs-review.md b/.agents/docs/2026-08-17-547-and-three-merged-prs-review.md index 87c9c0ec..9dece403 100644 --- a/.agents/docs/2026-08-17-547-and-three-merged-prs-review.md +++ b/.agents/docs/2026-08-17-547-and-three-merged-prs-review.md @@ -1123,7 +1123,7 @@ git 工作树,以及没有 `.git` 的 artifact 安装(`.xlings-index-version | 4 I6 放宽 | ✅ | | 5 `make_block(BlockSpec)` + provenance 二选一 | ✅ 外加 `describe_block` 的创建记录承接(方案里没有,10.1 同源的反向错误) | | 6 `SubosRuntimeUnknown` / `SubosRuntimeDrift` / `--from` warn | ✅ | -| 7 mcpp 侧文案 | ⏳ **另一个 repo,另一个 PR**(mcpp#427) | +| 7 mcpp 侧文案 | ✅ mcpp-community/mcpp#447 已合并 —— 三处,不是一处 | | §5 #551 回滚 | ✅ 但按 10.1 重新定义 | | §5 trash 出版本命名空间 + 真的清理 | ✅ | | §5 五站点收敛 | ⏳ **未做,而且不是「顺手就能做」** —— 见下 | @@ -1131,6 +1131,7 @@ git 工作树,以及没有 `.git` 的 artifact 安装(`.xlings-index-version | §6 #552 CRLF / advice 收窄 / 可测性 | ✅(并按 10.5 合并为一个读取器) | | §7 Windows CI 拆 job | ✅ | | §3 #549 控制台自检载体 | ⏳ **未做** —— D4 仍然开着 | +| 发布 + 生态验证 | ✅ 2026.8.17.1 已发布;四项产物检查 + 六项 sandbox 断言全过,见发布说明 §10–§11 | ### 11.1 为什么「五站点收敛」不是顺手的事 diff --git a/.agents/docs/2026-08-17-release-2026.8.17.1-notes.md b/.agents/docs/2026-08-17-release-2026.8.17.1-notes.md index 93d95aa8..9f6f3c2c 100644 --- a/.agents/docs/2026-08-17-release-2026.8.17.1-notes.md +++ b/.agents/docs/2026-08-17-release-2026.8.17.1-notes.md @@ -242,3 +242,99 @@ read-only(不带 `--fix`),2026.8.17.1 的二进制对着那台 39-subos 这两条**不会被 `--fix` 改写**(§3),因为哪一边是意外这里判断不了 —— 它们现在是可见的,而在此之前,`lib/libc.so.6` 那条符号链接摆在那里, 没有一行代码看过它。 + +--- + +## 10. 发布后实测(不看 job 状态,只看产物) + +`tools/verify-release.sh 2026.8.17.1`: + +| 检查 | 方法 | 结果 | +|---|---|---| +| GitHub release 资产 | `gh api .../releases` 读 `assets`(REST,不是 GraphQL) | ✅ 8/8 | +| sha256 | 自己下载后算,再比 sidecar | ✅ 4/4 逐个相同 | +| 索引 `latest`(三平台) | 直接读 xim-pkgindex **main** 上的 `pkgs/x/xlings.lua` | ✅(**见下**) | +| CN 镜像 | 完整下载后与 GitHub 副本逐字节比 | ✅ 4/4 相同 | + +**第 3 条第一次跑是红的,而这正是它存在的理由。** `bump-index` 报告 **success**, +开了 PR #642,**没有合并** —— 索引的 `latest` 还指着 2026.8.14.1。合并 #642 之后复验通过。 +这条在 `project_release_verification_traps` 里记了两次,这是第三次,**每次都是同样的形状**。 + +顺带一次独立交叉验证:bot 写进 recipe 的四个 sha256,与本机从下载产物算出来的**逐字相同**。 + +`tools/mirror-latest.sh xlings`:16 个 URL(GitHub + GitCode × 4 资产 × 归档+sidecar) +全部 OK,本次 `mirror-binaries` 真的传上去了,本地 gtc 是幂等 no-op。 + +## 11. 生态验证(sandbox,跑的是安装后的入口二进制) + +`xlings subos use --sandbox --cmd "xlings …"` 里的 `xlings` 是 +`~/.xlings/bin/xlings`,不是任何构建目录里的东西。所以这一步**必须在发布之后**做: +隔离 home 证明代码对,只有这个能证明**用户真正调用的那个东西**对。 + +| # | 断言 | 结果 | +|---|---|---| +| 0 | 沙箱里 `xlings --version` == 2026.8.17.1 | ✅ | +| 1 | `subos new --runtime glibc@2.39` 记 2.39(内置默认 2.44),写 `created_at`、不写 `described_at` | ✅ | +| 2 | **抹掉块**后在沙箱内 `self doctor --fix` → `glibc@2.39` + `described_at`,**无伪造的 `created_at`** | ✅ | +| 3 | 已描述的 subos 既不报 drift 也不报 unknown | ✅ | +| 4 | 真机 `mcpp-test` 仍报 declaration/sysroot 不一致 | ✅ | +| 5 | 一次性 subos 清理干净 | ✅ | + +**第 2 条就是 #547 本身**,跑在真实 home 上、通过沙箱、用发布出去的那个二进制。 + +### 11.1 这台真机上,32 个未描述 subos 会被描述成什么 + +按代码同一套优先级算出来: + +- **17 个**拿到真实 runtime(全部 `glibc@2.39`,来源:workspace 记录) +- **15 个**如实记 `runtime` 缺席(既无记录,sysroot 里也没有 libc 链接) +- 2 个连 `.xlings.json` 都没有 + +改之前:走 `xim install` 那条路径这 32 个**全部**会被声明成 `glibc@2.44`; +走 `doctor --fix` 那条 17 个正确、15 个被编造。 + +## 12. 八个角度,各自的结论 + +| 角度 | 这轮做了什么 | 在哪 | +|---|---|---| +| **架构** | 「这个 subos 的 runtime 是什么」从 6 个各自决定收敛成 1 个函数 + 1 个参数(`Intent`);`preserved_runtime` 同 commit 删除,让「没归队」变成编译错误 | §3 | +| **一致性** | 那 1 个参数是唯一允许的分歧:常量只在**人可能说过话**的路径上出现,`DEFAULT_RUNTIME` 的作用域因此字面等于它注释里的 `Scope: NEW subos only` | §3 优先级表 | +| **稳定性** | #551 的回滚会删掉它挪走的文件 → 因「回滚到未触碰」不可达而重新定义为「残留必须 stamp 成 incomplete」;trash 移出版本命名空间并有了真的清理时机 | §4 | +| **优雅简洁** | 方案 8 项 → 7 项,**零新命令零新标志**;4 个 Describe 站点收敛到 `describe_block` 一行 | §3 | +| **用户体验** | 两条新 finding(drift 报而不修、unknown 是 Notice 不计退出码);三条消息**渲染出来看过**,改掉了重复列仓库、超长行、名字对不上文件 | §11、消息相关 commit | +| **兼容性** | 无 schema bump;升级方向安全;**降级方向不安全并且写下来了** | §7 | +| **跨平台** | libc++ 只给 `operator==(default_sentinel_t)`;本地 `mcpp build --toolchain llvm@20.1.7` 双向验证后才推 | §6 | +| **无感升级** | 迁移走**本来就在跑的三个入口**;`subos use` 被明确否决(`XLINGS_ACTIVE_SUBOS` 是继承的环境变量) | §3 | + +**没覆盖的角度,说清楚**:性能没有量。`sysroot_runtime` 每次 Describe 最多 28 次 +`symlink_status`,在写路径上,没测过也不认为值得测 —— 哪天它出现在热路径上, +这句话就是那次调查的起点。 + +## 13. 跨仓库 + +| 仓库 | 变更 | 状态 | +|---|---|---| +| openxlings/xlings | #553,本次发布 | ✅ 已合并、已发布、已验证 | +| mcpp-community/mcpp | #447 —— 三处补救文案改指一条**真的能修**的命令 | ✅ 已合并 | +| openxlings/xim-pkgindex | #642 bump `latest` | ✅ 已合并(bot 只开不合,手动) | + +## 14. 仍然开着的 + +- **`subos remove` / `self uninstall` 的 `remove_all` 收敛** —— 见分析文档 §11.1, + 不是替换一行能做的(trash 根需要一般化;`self uninstall` 删的就是 trash 的落脚点) +- **#549 的 D4 控制台自检载体** —— 管道那半量到了且从来没坏,控制台那半仍无载体 +- **openxlings/xlings#554(发布当天量到的新缺陷)** —— `xlings self update` 在装过 + `local:` 版本的 home 上**退出 0、报告成功、什么都没升**。逐步隔离到第三步: + `xlings use xlings latest` 是 **provider-sticky** 的 —— 从 `2026.8.17.1` 活跃出发 + 它选 `2026.8.17.1`,从 `local:0.4.51` 活跃出发它选 `local:0.4.51`,同一条命令、 + 同一份 workspace,结果取决于运行前谁是活跃的。`cmd_update` 只看 `rc != 0`, + 而 `use` 成功激活了*某个*东西,所以返回 0。 + + **这是本文 §1 那张表的第七行**,而且落在这轮到处引用的那条补救命令上 —— + `self update` 唯一的承诺是「跑完之后它是新的」,而它从不检查这件事。 + 是在验证本次发布的过程中撞到的:为了让 sandbox 跑到发布产物,得先把 home 升上去, + 而它升不上去。 + +- **xim-pkgindex #587** —— 「Windows 上必须真的卸载成功」这条门禁开着并且已经陈旧 + (`consumer-smoke (linux)` 红,run 停在 2026-08-09)。**它正是会端到端压到本次 + `remove_payload_dir` 加固的那条门** —— 本次没动它,但它现在有理由重跑了。 diff --git a/mcpp.toml b/mcpp.toml index c0fe309f..a33c7472 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "xlings" -version = "2026.8.17.1" +version = "2026.8.17.2" description = "Universal package management infrastructure tool with SubOS isolation" license = "Apache-2.0" repo = "https://github.com/openxlings/xlings" diff --git a/src/core/config.cppm b/src/core/config.cppm index 252ecb0e..2006fbde 100644 --- a/src/core/config.cppm +++ b/src/core/config.cppm @@ -10,7 +10,7 @@ import xlings.core.xvm.db; namespace xlings { export struct Info { - static constexpr std::string_view VERSION = "2026.8.17.1"; + static constexpr std::string_view VERSION = "2026.8.17.2"; static constexpr std::string_view REPO = "https://github.com/openxlings/xlings"; }; diff --git a/src/core/xself/update.cpp b/src/core/xself/update.cpp index be82d050..7f7d7bd3 100644 --- a/src/core/xself/update.cpp +++ b/src/core/xself/update.cpp @@ -1,11 +1,23 @@ module xlings.core.xself.update; import std; +import xlings.core.config; +import xlings.core.entry_binary; +import xlings.core.xvm.db; import xlings.core.log; import xlings.platform; namespace xlings::xself { +bool update_landed_on_index_build(std::string_view activeVersion) { + // Empty is NOT a failure: it means nothing recorded an active version, + // which is a different defect and one this command must not claim to have + // diagnosed. Same rule `version_of` follows -- no observation is not a + // verdict. + if (activeVersion.empty()) return true; + return activeVersion.find(':') == std::string_view::npos; +} + int cmd_update() { log::info("updating package index..."); platform::set_env_variable("XLINGS_INDEX_PIN", "newest"); @@ -57,6 +69,45 @@ int cmd_update() { return rc; } + // Did the update actually land on the index build? (#554) + // + // `use ... latest` resolves WITHIN the currently active provider, which is + // defensible on its own -- switching provider for an ambiguous name behind + // someone's back is worse. But it means that on a home which has ever + // carried a `local:` build, `latest` keeps resolving to that build: + // + // from 2026.8.17.1 active -> xlings -> 2026.8.17.1 + // from local:0.4.51 active -> xlings -> local:0.4.51 + // + // `use` returns 0 either way, because it did activate something. So this + // command reported success and left the user on 0.4.51, silently and + // forever -- measured on a real home the day 2026.8.17.1 shipped. + // + // The test is the PROVIDER, not the version. "Did the version change" is + // the obvious check and it is wrong: on an already-current home nothing + // changes and that is success, so it would fail every no-op update. What + // this command means by "updated" is "running the build the index just + // handed us", and a namespaced active version (`local:0.4.51`) is exactly + // the statement that it is not -- an index install records a bare version. + if (const auto active = + xvm::get_active_version(Config::effective_workspace(), "xlings"); + !update_landed_on_index_build(active)) { + const auto entry = + entry_binary::version_of(entry_binary::path_of(Config::paths().homeDir)); + log::error("nothing was upgraded: xlings is still active at '{}'{}", + active, + entry.empty() ? std::string{} + : std::format(" (the entry binary reports {})", + entry)); + log::error(" `latest` resolves within the provider that is already " + "active, so a `{}` build keeps winning it", + active.substr(0, active.find(':'))); + log::error(" run: xlings list xlings (see what is installed)"); + log::error(" then: xlings use xlings (a version with no " + "`:` prefix)"); + return 1; + } + // The migration nudge, printed rather than performed. // // This function is running the OLD binary -- it has just replaced itself diff --git a/src/core/xself/update.cppm b/src/core/xself/update.cppm index 18c106fa..c0f7680f 100644 --- a/src/core/xself/update.cppm +++ b/src/core/xself/update.cppm @@ -12,4 +12,18 @@ namespace xlings::xself { // (xim.commands and xvm.commands both import xlings.core.xself). export int cmd_update(); +// Did `use xlings latest` land on the build the index handed us? +// +// The test is the PROVIDER, not the version (#554). "Did the version change" +// is the obvious check and it is wrong: on an already-current home nothing +// changes and that IS success, so it fails every no-op update. +// +// What this command means by "updated" is "running the build the index just +// gave us", and a namespaced active version -- `local:0.4.51` -- is precisely +// the statement that it is not. An index install records a bare version. +// +// Exported so the rule has a test. It was inline, and inline is why the +// original had no check at all: there was nothing to write a test against. +export bool update_landed_on_index_build(std::string_view activeVersion); + } // namespace xlings::xself diff --git a/tests/unit/test_self_repair.cpp b/tests/unit/test_self_repair.cpp index bc4e9380..ea9e4040 100644 --- a/tests/unit/test_self_repair.cpp +++ b/tests/unit/test_self_repair.cpp @@ -11,6 +11,7 @@ import std; import xlings.core.xself.repair; +import xlings.core.xself.update; using xlings::xself::RepairKind; using xlings::xself::RepairPolicy; @@ -344,3 +345,38 @@ TEST(SelfRepairShellSafety, AcceptsTheShapesRealPackagesUse) { EXPECT_FALSE(is_shell_safe_token("--yes")); EXPECT_FALSE(is_shell_safe_token("a\nb")); } + +// ── `self update` must not report success without updating (#554) ──────── +// +// Measured on a real home the day 2026.8.17.1 shipped: `self update` exited 0 +// and left the user on 0.4.51. `use xlings latest` resolves WITHIN the +// currently active provider, so a home that ever carried a `local:` build +// keeps re-picking it -- and `use` returns 0 because it did activate +// something. +// +// The rule is about the PROVIDER, not the version. Both directions matter and +// the wrong implementation passes only one of them. +TEST(SelfUpdateLanding, ABareVersionIsTheIndexBuild) { + EXPECT_TRUE(xlings::xself::update_landed_on_index_build("2026.8.17.1")); + EXPECT_TRUE(xlings::xself::update_landed_on_index_build("0.4.51")); +} + +TEST(SelfUpdateLanding, ANamespacedVersionIsNot) { + EXPECT_FALSE(xlings::xself::update_landed_on_index_build("local:0.4.51")); + EXPECT_FALSE(xlings::xself::update_landed_on_index_build("scode:1.0")); +} + +// The false positive that the obvious implementation ships with: an +// already-current home changes nothing, and that is success. A check on "did +// the version move" fails every no-op update. +TEST(SelfUpdateLanding, AnAlreadyCurrentHomeIsNotAFailure) { + EXPECT_TRUE(xlings::xself::update_landed_on_index_build("2026.8.17.1")) + << "running `self update` twice must not start failing the second time"; +} + +// No observation is not a verdict -- the same rule entry_binary::version_of +// follows. A workspace that records no active version has a different defect, +// and this command must not claim to have diagnosed it. +TEST(SelfUpdateLanding, NoRecordedActiveVersionIsNotAVerdict) { + EXPECT_TRUE(xlings::xself::update_landed_on_index_build("")); +}