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
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 — 索引层

Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
53 changes: 50 additions & 3 deletions src/xpkg-loader.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> 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;
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down
4 changes: 4 additions & 0 deletions src/xpkg.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions tests/fixtures/pkgindex/pkgs/v/v2inherits.lua
Original file line number Diff line number Diff line change
@@ -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" },
},
}
33 changes: 33 additions & 0 deletions tests/test_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string>{"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());
}
Loading