From 98e9e9387dd924c1a791612dd64505c6fba86984 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 16 Oct 2026 23:58:55 +0900 Subject: [PATCH 1/2] fix(loader): a platform-level ref or inherits names the platform it inherits, and the loader fills it in (0.0.64) `xpm.inherits` was parsed and never used, and a platform written in the V1 form `macosx = { ref = "linux" }` became a version named "ref" whose url was "linux" (xlings: `xim:luban-fonts@ref ... Only HTTPS is supported` on macOS and Windows). Both spellings now record inheritance; after parsing, each inheriting platform receives the versions it lacks and, when it has none of its own, the deps, exports and source of the platform it inherits, along the chain, stopping at a cycle or an unknown platform. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- docs/architecture.md | 1 + mcpp.toml | 2 +- src/xpkg-loader.cppm | 52 +++++++++++++++++-- tests/fixtures/pkgindex/pkgs/v/v2inherits.lua | 23 ++++++++ tests/test_loader.cpp | 25 +++++++++ 5 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/pkgindex/pkgs/v/v2inherits.lua diff --git a/docs/architecture.md b/docs/architecture.md index 62e12ef..8f3373b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -75,6 +75,7 @@ graph TD - `build_index(repo_dir)` — 扫描 `pkgs/` 目录构建 `PackageIndex` - `build_index(repo_dir, ns, BuildOutput)` — 同上;索引自带的 `pkgindex-build.lua` 用 `io.write` / `print` 写出的内容原样、按顺序交给 `BuildOutput`,不再写到进程的 stdout。构建脚本用 `\r[i/n] …\033[K` 画进度,只有调用方知道自己的输出去向(终端、文件还是管道),所以由它决定怎么显示(0.0.59) - `load_index_db(path)` / `save_index_db(index, path)` — JSON 格式索引持久化 +- 平台继承:平台段的 `inherits = "<平台>"` 与 V1 写法 `{ ref = "<平台>" }` 都表示继承。加载时沿继承链补齐:本平台缺少的版本取自被继承的平台,`deps`、`exports`、`source` 在本平台未声明时同样继承;遇到环或不存在的平台即停止。平台级的 `ref` 不再生成名为 `ref` 的版本(0.0.64) ### mcpplibs.xpkg.index — 索引层 diff --git a/mcpp.toml b/mcpp.toml index 8465cce..f37d247 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "xpkg" -version = "0.0.63" +version = "0.0.64" description = "C++23 reference implementation of the xpkg V2 spec (multi-arch)" license = "Apache-2.0" repo = "https://github.com/openxlings/libxpkg" diff --git a/src/xpkg-loader.cppm b/src/xpkg-loader.cppm index 94b42c9..f61f1aa 100644 --- a/src/xpkg-loader.cppm +++ b/src/xpkg-loader.cppm @@ -237,6 +237,41 @@ PackageStatus parse_status(const std::string& s) { return PackageStatus::Dev; } +// A platform that inherits another holds what it does not declare itself: +// the versions it lacks, and its deps, exports and source when it has none. +// The chain is followed (ubuntu -> debian -> linux); a cycle or an unknown +// platform ends it. Done once, here, so every reader of `entries` sees the +// platform the client looks up as complete. +void resolve_inheritance(PlatformMatrix& xpm) { + for (auto& [platform, first] : xpm.inherits) { + std::unordered_set seen{platform}; + for (auto parent = first; !parent.empty() && seen.insert(parent).second;) { + // Copied before inserting: an insertion may rehash the map. + if (auto from = xpm.entries.find(parent); from != xpm.entries.end()) { + auto versions = from->second; + auto& own = xpm.entries[platform]; + for (auto& [version, resource] : versions) + own.try_emplace(version, std::move(resource)); + } + auto inherit = [&](auto& field) { + if (auto from = field.find(parent); + from != field.end() && !field.contains(platform)) { + auto value = from->second; + field.emplace(platform, std::move(value)); + } + }; + inherit(xpm.deps); + inherit(xpm.runtime_deps); + inherit(xpm.build_deps); + inherit(xpm.exports); + inherit(xpm.platform_sources); + inherit(xpm.platform_source_mirrors); + auto next = xpm.inherits.find(parent); + parent = next == xpm.inherits.end() ? std::string{} : next->second; + } + } +} + // Parse xpm platform matrix from the package table at idx PlatformMatrix parse_xpm(lua::State* L, int pkg_idx) { PlatformMatrix xpm; @@ -381,12 +416,22 @@ PlatformMatrix parse_xpm(lua::State* L, int pkg_idx) { } lua::pop(L, 1); // pop deps field - // Parse inherits if present + // Inheritance: `inherits = "linux"`, or the V1 spelling + // `macosx = { ref = "linux" }` (xpackage V1 spec, "platform + // reference"). A platform-level `ref` names a platform, never a + // version; before 0.0.64 it became a version named "ref" whose + // url was "linux" (luban-fonts on macOS and Windows, 2026-10-16). lua::getfield(L, plat_idx, "inherits"); if (lua::type(L, -1) == lua::TSTRING) { xpm.inherits[platform] = lua::tostring(L, -1); } lua::pop(L, 1); // pop inherits field + if (!xpm.inherits.contains(platform)) { + lua::getfield(L, plat_idx, "ref"); + if (lua::type(L, -1) == lua::TSTRING) + xpm.inherits[platform] = lua::tostring(L, -1); + lua::pop(L, 1); // pop ref field + } // Parse exports block. Schema (all sub-fields optional): // exports = { @@ -439,8 +484,8 @@ PlatformMatrix parse_xpm(lua::State* L, int pkg_idx) { // (parsed above) that would otherwise leak in as a bogus // version entry; `deps`/`inherits` likewise. if (!version.empty() && version != "deps" - && version != "inherits" && version != "exports" - && version != "source") { + && version != "inherits" && version != "ref" + && version != "exports" && version != "source") { PlatformResource res; if (lua::type(L, -1) == lua::TTABLE) { int res_idx = lua::gettop(L); @@ -518,6 +563,7 @@ PlatformMatrix parse_xpm(lua::State* L, int pkg_idx) { } lua::pop(L, 1); // pop xpm table + resolve_inheritance(xpm); return xpm; } diff --git a/tests/fixtures/pkgindex/pkgs/v/v2inherits.lua b/tests/fixtures/pkgindex/pkgs/v/v2inherits.lua new file mode 100644 index 0000000..19222e5 --- /dev/null +++ b/tests/fixtures/pkgindex/pkgs/v/v2inherits.lua @@ -0,0 +1,23 @@ +package = { + spec = "1", + name = "v2inherits", + xpm = { + linux = { + deps = { "xim:zlib" }, + ["latest"] = { ref = "1.0.0" }, + ["1.0.0"] = { url = "https://example.test/1.0.0.tar.gz", sha256 = "aa" }, + }, + -- V1 platform reference + macosx = { ref = "linux" }, + -- inherits, with a version of its own + windows = { + inherits = "linux", + ["2.0.0"] = { url = "https://example.test/2.0.0.zip", sha256 = "bb" }, + }, + -- a chain, and a cycle + ubuntu = { ref = "debian" }, + debian = { inherits = "linux" }, + a = { ref = "b" }, + b = { ref = "a" }, + }, +} diff --git a/tests/test_loader.cpp b/tests/test_loader.cpp index 24df87d..851c358 100644 --- a/tests/test_loader.cpp +++ b/tests/test_loader.cpp @@ -760,3 +760,28 @@ TEST(LoaderBoundaryTest, IndexBuildAndEveryRecipeCrossTheirOwnBoundaries) { EXPECT_FALSE(fs::exists(marker)); fs::remove_all(dir); } + +TEST(LoaderTest, PlatformReferenceIsInheritanceNotAVersion) { + auto result = load_package(PKGINDEX / "pkgs/v/v2inherits.lua"); + ASSERT_TRUE(result.has_value()) << result.error(); + const auto& entries = result->xpm.entries; + // `macosx = { ref = "linux" }` holds linux's versions, not one named "ref". + ASSERT_TRUE(entries.contains("macosx")); + EXPECT_FALSE(entries.at("macosx").contains("ref")); + EXPECT_EQ(entries.at("macosx").at("latest").ref, "1.0.0"); + EXPECT_EQ(entries.at("macosx").at("1.0.0").url, "https://example.test/1.0.0.tar.gz"); + EXPECT_EQ(result->xpm.deps.at("macosx"), std::vector{"xim:zlib"}); + EXPECT_EQ(result->xpm.inherits.at("macosx"), "linux"); +} + +TEST(LoaderTest, InheritingPlatformKeepsItsOwnVersionsAndFollowsChains) { + auto result = load_package(PKGINDEX / "pkgs/v/v2inherits.lua"); + ASSERT_TRUE(result.has_value()) << result.error(); + const auto& entries = result->xpm.entries; + EXPECT_EQ(entries.at("windows").at("2.0.0").url, "https://example.test/2.0.0.zip"); + EXPECT_EQ(entries.at("windows").at("1.0.0").url, "https://example.test/1.0.0.tar.gz"); + EXPECT_FALSE(entries.contains("linux") && entries.at("linux").contains("2.0.0")); + EXPECT_EQ(entries.at("ubuntu").at("1.0.0").sha256, "aa"); + // A cycle ends the chain; nothing is invented. + EXPECT_FALSE(entries.contains("a") && !entries.at("a").empty()); +} From 4f0195d0b7f5752ef3bbf7cd7ac7cd7b50aa8bce Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 17 Oct 2026 00:12:10 +0900 Subject: [PATCH 2/2] feat(loader): a version entry names the oldest client that can use it (min_client) `["2026.10.16.1"] = { min_client = "2026.10.16.1" }` is parsed into PlatformResource::min_client, so a client resolving a package with no version named can pass over the versions it cannot use, before fetching anything (xlings: a Luban edition newer than the client). Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- docs/architecture.md | 1 + src/xpkg-loader.cppm | 1 + src/xpkg.cppm | 4 ++++ tests/fixtures/pkgindex/pkgs/v/v2inherits.lua | 1 + tests/test_loader.cpp | 8 ++++++++ 5 files changed, 15 insertions(+) diff --git a/docs/architecture.md b/docs/architecture.md index 8f3373b..94ffadb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -76,6 +76,7 @@ graph TD - `build_index(repo_dir, ns, BuildOutput)` — 同上;索引自带的 `pkgindex-build.lua` 用 `io.write` / `print` 写出的内容原样、按顺序交给 `BuildOutput`,不再写到进程的 stdout。构建脚本用 `\r[i/n] …\033[K` 画进度,只有调用方知道自己的输出去向(终端、文件还是管道),所以由它决定怎么显示(0.0.59) - `load_index_db(path)` / `save_index_db(index, path)` — JSON 格式索引持久化 - 平台继承:平台段的 `inherits = "<平台>"` 与 V1 写法 `{ ref = "<平台>" }` 都表示继承。加载时沿继承链补齐:本平台缺少的版本取自被继承的平台,`deps`、`exports`、`source` 在本平台未声明时同样继承;遇到环或不存在的平台即停止。平台级的 `ref` 不再生成名为 `ref` 的版本(0.0.64) +- 版本条目的 `min_client`:能使用该版本的最旧客户端版本,解析为 `PlatformResource::min_client`。未指定版本时,客户端据此跳过自己不能使用的版本(0.0.64) ### mcpplibs.xpkg.index — 索引层 diff --git a/src/xpkg-loader.cppm b/src/xpkg-loader.cppm index f61f1aa..ca70865 100644 --- a/src/xpkg-loader.cppm +++ b/src/xpkg-loader.cppm @@ -500,6 +500,7 @@ PlatformMatrix parse_xpm(lua::State* L, int pkg_idx) { res.sha256 = get_str(L, res_idx, "sha256"); res.ref = get_str(L, res_idx, "ref"); res.revision = get_nonneg_int(L, res_idx, "revision"); + res.min_client = get_str(L, res_idx, "min_client"); // ---- V2 multi-arch shapes ---- // Scheme C / res: `sha256` is a per-arch TABLE rather diff --git a/src/xpkg.cppm b/src/xpkg.cppm index 3bee0cb..3446acb 100644 --- a/src/xpkg.cppm +++ b/src/xpkg.cppm @@ -52,6 +52,10 @@ struct PlatformResource { // states none. A client records the revision it installed and treats a // different recipe revision as not installed (xpkg V2 spec, "revision"). int revision = 0; + // The oldest client that can use this version (`min_client = + // "2026.10.16.1"`): a client resolving a package with no version named + // passes over the versions it cannot use. Empty when the entry states none. + std::string min_client; }; // What this package exposes to consumers/xlings at install/runtime. diff --git a/tests/fixtures/pkgindex/pkgs/v/v2inherits.lua b/tests/fixtures/pkgindex/pkgs/v/v2inherits.lua index 19222e5..3fc6d70 100644 --- a/tests/fixtures/pkgindex/pkgs/v/v2inherits.lua +++ b/tests/fixtures/pkgindex/pkgs/v/v2inherits.lua @@ -6,6 +6,7 @@ package = { deps = { "xim:zlib" }, ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = { url = "https://example.test/1.0.0.tar.gz", sha256 = "aa" }, + ["1.1.0"] = { url = "https://example.test/1.1.0.tar.gz", sha256 = "cc", min_client = "2026.10.16.2" }, }, -- V1 platform reference macosx = { ref = "linux" }, diff --git a/tests/test_loader.cpp b/tests/test_loader.cpp index 851c358..337c73b 100644 --- a/tests/test_loader.cpp +++ b/tests/test_loader.cpp @@ -785,3 +785,11 @@ TEST(LoaderTest, InheritingPlatformKeepsItsOwnVersionsAndFollowsChains) { // A cycle ends the chain; nothing is invented. EXPECT_FALSE(entries.contains("a") && !entries.at("a").empty()); } + +TEST(LoaderTest, AVersionNamesTheOldestClientThatCanUseIt) { + auto result = load_package(PKGINDEX / "pkgs/v/v2inherits.lua"); + ASSERT_TRUE(result.has_value()) << result.error(); + EXPECT_EQ(result->xpm.entries.at("linux").at("1.1.0").min_client, "2026.10.16.2"); + EXPECT_EQ(result->xpm.entries.at("macosx").at("1.1.0").min_client, "2026.10.16.2"); + EXPECT_TRUE(result->xpm.entries.at("linux").at("1.0.0").min_client.empty()); +}