diff --git a/docs/architecture.md b/docs/architecture.md index 62e12ef..94ffadb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -75,6 +75,8 @@ 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) +- 版本条目的 `min_client`:能使用该版本的最旧客户端版本,解析为 `PlatformResource::min_client`。未指定版本时,客户端据此跳过自己不能使用的版本(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..ca70865 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); @@ -455,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 @@ -518,6 +564,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/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 new file mode 100644 index 0000000..3fc6d70 --- /dev/null +++ b/tests/fixtures/pkgindex/pkgs/v/v2inherits.lua @@ -0,0 +1,24 @@ +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" }, + ["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" }, + -- 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..337c73b 100644 --- a/tests/test_loader.cpp +++ b/tests/test_loader.cpp @@ -760,3 +760,36 @@ 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()); +} + +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()); +}