Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1123,14 +1123,15 @@ 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 五站点收敛 | ⏳ **未做,而且不是「顺手就能做」** —— 见下 |
| §4 删 `forSearch` | ✅ |
| §6 #552 CRLF / advice 收窄 / 可测性 | ✅(并按 10.5 合并为一个读取器) |
| §7 Windows CI 拆 job | ✅ |
| §3 #549 控制台自检载体 | ⏳ **未做** —— D4 仍然开着 |
| 发布 + 生态验证 | ✅ 2026.8.17.1 已发布;四项产物检查 + 六项 sandbox 断言全过,见发布说明 §10–§11 |

### 11.1 为什么「五站点收敛」不是顺手的事

Expand Down
96 changes: 96 additions & 0 deletions .agents/docs/2026-08-17-release-2026.8.17.1-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name> --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` 加固的那条门** —— 本次没动它,但它现在有理由重跑了。
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/core/config.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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";
};

Expand Down
51 changes: 51 additions & 0 deletions src/core/xself/update.cpp
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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 <version> (a version with no "
"`<provider>:` prefix)");
return 1;
}

// The migration nudge, printed rather than performed.
//
// This function is running the OLD binary -- it has just replaced itself
Expand Down
14 changes: 14 additions & 0 deletions src/core/xself/update.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 36 additions & 0 deletions tests/unit/test_self_repair.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import std;
import xlings.core.xself.repair;
import xlings.core.xself.update;

using xlings::xself::RepairKind;
using xlings::xself::RepairPolicy;
Expand Down Expand Up @@ -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(""));
}
Loading