From 860545334215c2187a9307e0856e8cc71a465868 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 22:55:45 +0000 Subject: [PATCH] feat(model): a version entry's packaging revision (0.0.58) A version entry may state `revision`, a non-negative integer that counts changes to what the recipe installs under an unchanged upstream version. An entry that states none, and an entry whose value is not a non-negative integer, reads as revision 0. The loader reads the field from the entry table itself, so every resource shape (single url, mirror map, per-arch map, template, res) carries it the same way; a `ref` alias carries none. This is the one addition the reference implementation needs for openxlings/xlings#620. The decision that uses the field -- a recorded revision that differs from the recipe's means not installed -- belongs to the client, and the contract is stated in the xpkg V2 specification. The V2 compatibility rule makes the addition safe in both directions: a client built on 0.0.57 ignores the key, and a recipe may state it before any client reads it. resolve_resource() reports the field as ResolvedResource::revision, taken from the entry that supplies the payload: for a `ref` alias, the entry the alias resolves to. A client that resolves through the library's own resolver therefore learns the revision it will install without walking aliases itself. CI builds with mcpp 2026.9.26.2 (.xlings.json and the sandbox cache key). The index now requires mcpp 2026.9.18.3 or later, so the previous pin, 2026.8.4.1, can no longer resolve this package's dependencies. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 6 +-- .xlings.json | 2 +- mcpp.lock | 12 ++++- mcpp.toml | 2 +- src/xpkg-compat.cppm | 4 ++ src/xpkg-loader.cppm | 15 ++++++ src/xpkg.cppm | 5 ++ tests/fixtures/pkgindex/pkgs/v/v2revision.lua | 38 ++++++++++++++ tests/test_loader.cpp | 51 +++++++++++++++++++ 9 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/pkgindex/pkgs/v/v2revision.lua diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b490b30..07403a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - - name: Install workspace tools (.xlings.json → mcpp 2026.8.4.1) + - name: Install workspace tools (.xlings.json → mcpp 2026.9.26.2) run: xlings install -y # Cache mcpp's self-bootstrapped sandbox (musl-gcc + binutils + @@ -32,8 +32,8 @@ jobs: - name: Cache mcpp sandbox uses: actions/cache@v4 with: - path: ~/.xlings/data/xpkgs/xim-x-mcpp/2026.8.4.1/registry - key: mcpp-sandbox-${{ runner.os }}-mcpp2026.8.4.1 + path: ~/.xlings/data/xpkgs/xim-x-mcpp/2026.9.26.2/registry + key: mcpp-sandbox-${{ runner.os }}-mcpp2026.9.26.2 # build.mcpp regenerates the embedded Lua stdlib from src/lua-stdlib/ # into MCPP_OUT_DIR. There is no committed copy to fall out of sync with. diff --git a/.xlings.json b/.xlings.json index 3220145..018eb6f 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": { "linux": "2026.8.4.1" } + "mcpp": { "linux": "2026.9.26.2" } } } diff --git a/mcpp.lock b/mcpp.lock index 9a6c268..eb1aaf4 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -1,9 +1,19 @@ # Auto-generated by mcpp. Do not edit by hand. +# Records what this build resolved. It does not yet pin future builds: +# index dependencies are re-resolved from their constraints each time. +# dev-dependencies are excluded: only `mcpp test` resolves them, and this +# file must not change depending on which command ran last. version = 2 +[package."compat.lua"] +namespace = "compat" +version = "5.4.7" +source = "index+compat@5.4.7" +hash = "fnv1a:62ff6fb178577847" + [package."mcpplibs.capi.lua"] namespace = "mcpplibs.capi" version = "0.0.3" source = "index+mcpplibs.capi@0.0.3" -hash = "fnv1a:c8e7ef9d60a1a3a6" +hash = "fnv1a:f3031630d0b22d94" diff --git a/mcpp.toml b/mcpp.toml index 6ab31d8..a6e762a 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "xpkg" -version = "0.0.57" +version = "0.0.58" 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-compat.cppm b/src/xpkg-compat.cppm index 25b1ca3..2206d5a 100644 --- a/src/xpkg-compat.cppm +++ b/src/xpkg-compat.cppm @@ -27,6 +27,9 @@ struct ResolvedResource { std::string url; std::string sha256; std::unordered_map mirrors; + // The packaging revision of the entry that supplies the payload: for a + // `ref` alias, the revision of the entry the alias resolves to. + int revision = 0; }; std::expected resolve_resource( @@ -114,6 +117,7 @@ std::expected resolve_resource( ResolvedResource result; result.version = resolved_version; + result.revision = resource->revision; if (arch_resource != nullptr) { result.url = arch_resource->url; result.sha256 = arch_resource->sha256; diff --git a/src/xpkg-loader.cppm b/src/xpkg-loader.cppm index 46cf3d6..b5cfb6b 100644 --- a/src/xpkg-loader.cppm +++ b/src/xpkg-loader.cppm @@ -155,6 +155,20 @@ bool get_bool(lua::State* L, int idx, const char* key) { return r; } +// A non-negative integer field, or 0. Lenient like the readers above: a +// malformed value reads as absent. The index's own CI rejects it. +int get_nonneg_int(lua::State* L, int idx, const char* key) { + lua::getfield(L, idx, key); + int r = 0; + if (lua::isinteger(L, -1)) { + auto v = lua::tointeger(L, -1); + if (v >= 0 && v <= std::numeric_limits::max()) + r = static_cast(v); + } + lua::pop(L, 1); + return r; +} + std::vector get_str_array(lua::State* L, int idx, const char* key) { std::vector result; lua::getfield(L, idx, key); @@ -401,6 +415,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"); // ---- 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 57bf95a..3bee0cb 100644 --- a/src/xpkg.cppm +++ b/src/xpkg.cppm @@ -47,6 +47,11 @@ struct PlatformResource { std::unordered_map sha256_by_arch; // canonical-arch -> sha256 (Scheme C: template / res) std::unordered_map arch_alias; // canonical-arch -> upstream token for ${arch_alias} bool is_res = false; // res=true: XLINGS_RES with per-arch checksums + // Packaging revision of this version's payload: a change to what the + // recipe installs that keeps the upstream version. 0 when the entry + // 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; }; // What this package exposes to consumers/xlings at install/runtime. diff --git a/tests/fixtures/pkgindex/pkgs/v/v2revision.lua b/tests/fixtures/pkgindex/pkgs/v/v2revision.lua new file mode 100644 index 0000000..5ff7614 --- /dev/null +++ b/tests/fixtures/pkgindex/pkgs/v/v2revision.lua @@ -0,0 +1,38 @@ +package = { + spec = "2", + name = "v2revision", + description = "V2 fixture: packaging revision on version entries", + type = "package", + archs = {"x86_64", "aarch64"}, + status = "stable", + categories = {"test"}, + xpm = { + linux = { + ["latest"] = { ref = "1.2.0" }, + ["1.0.0"] = { + url = "https://ex/v2revision-1.0.0.tar.gz", + sha256 = "aaaa", + }, + ["1.1.0"] = { + url = "https://ex/v2revision-1.1.0.tar.gz", + sha256 = "bbbb", + revision = 2, + }, + ["1.2.0"] = { + x86_64 = { url = "https://ex/v2revision-1.2.0-x86_64.tar.gz", sha256 = "cccc" }, + aarch64 = { url = "https://ex/v2revision-1.2.0-aarch64.tar.gz", sha256 = "dddd" }, + revision = 1, + }, + ["1.3.0"] = { + url = "https://ex/v2revision-1.3.0.tar.gz", + sha256 = "eeee", + revision = "3", + }, + ["1.4.0"] = { + url = "https://ex/v2revision-1.4.0.tar.gz", + sha256 = "ffff", + revision = -1, + }, + }, + }, +} diff --git a/tests/test_loader.cpp b/tests/test_loader.cpp index bdcc5bd..602686a 100644 --- a/tests/test_loader.cpp +++ b/tests/test_loader.cpp @@ -337,6 +337,57 @@ TEST(LoaderTest, V2_LegacySingleArch_Unchanged) { EXPECT_TRUE(r.archs.empty()); EXPECT_TRUE(r.sha256_by_arch.empty()); EXPECT_FALSE(r.is_res); + EXPECT_EQ(r.revision, 0); +} + +// A version entry may state a packaging revision. An entry that states none +// has revision 0, and so does an entry whose value is not a non-negative +// integer. The revision does not change how the rest of the entry is read: +// a per-arch map with a revision is still a per-arch map. +TEST(LoaderTest, V2_Revision_ParsedPerVersionEntry) { + auto result = load_package(PKGINDEX / "pkgs/v/v2revision.lua"); + ASSERT_TRUE(result.has_value()) << result.error(); + auto& plat = result->xpm.entries.at("linux"); + EXPECT_EQ(plat.at("1.0.0").revision, 0); + EXPECT_EQ(plat.at("1.1.0").revision, 2); + EXPECT_EQ(plat.at("1.1.0").url, "https://ex/v2revision-1.1.0.tar.gz"); + + auto& map = plat.at("1.2.0"); + EXPECT_EQ(map.revision, 1); + ASSERT_EQ(map.archs.size(), 2u); + EXPECT_EQ(map.archs.at("x86_64").sha256, "cccc"); + + EXPECT_EQ(plat.at("1.3.0").revision, 0); // a string is not a revision + EXPECT_EQ(plat.at("1.4.0").revision, 0); // nor is a negative number + EXPECT_EQ(plat.at("latest").revision, 0); // an alias carries none + EXPECT_EQ(plat.at("latest").ref, "1.2.0"); +} + +// The resolver reports the revision of the entry that supplies the payload, +// so a client asking for an alias learns the revision it will install. +TEST(LoaderTest, V2_Revision_ResolvedThroughRef) { + auto result = load_package(PKGINDEX / "pkgs/v/v2revision.lua"); + ASSERT_TRUE(result.has_value()) << result.error(); + + auto direct = resolve_resource(result->xpm, { + .name = "v2revision", .version = "1.1.0", + .platform = "linux", .arch = "x86_64", .ext = ""}); + ASSERT_TRUE(direct.has_value()) << direct.error(); + EXPECT_EQ(direct->revision, 2); + + auto alias = resolve_resource(result->xpm, { + .name = "v2revision", .version = "latest", + .platform = "linux", .arch = "aarch64", .ext = ""}); + ASSERT_TRUE(alias.has_value()) << alias.error(); + EXPECT_EQ(alias->version, "1.2.0"); + EXPECT_EQ(alias->revision, 1); + EXPECT_EQ(alias->sha256, "dddd"); + + auto none = resolve_resource(result->xpm, { + .name = "v2revision", .version = "1.0.0", + .platform = "linux", .arch = "x86_64", .ext = ""}); + ASSERT_TRUE(none.has_value()) << none.error(); + EXPECT_EQ(none->revision, 0); } TEST(LoaderTest, SourceDefaultsAreParsedAsMetadataNotVersions) {