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-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..0ae14ff 100644 --- a/tests/test_loader.cpp +++ b/tests/test_loader.cpp @@ -337,6 +337,30 @@ 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"); } TEST(LoaderTest, SourceDefaultsAreParsedAsMetadataNotVersions) {