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) {