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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 +
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion .xlings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"workspace": {
"mcpp": { "linux": "2026.8.4.1" }
"mcpp": { "linux": "2026.9.26.2" }
}
}
12 changes: 11 additions & 1 deletion mcpp.lock
Original file line number Diff line number Diff line change
@@ -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"

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.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"
Expand Down
4 changes: 4 additions & 0 deletions src/xpkg-compat.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ struct ResolvedResource {
std::string url;
std::string sha256;
std::unordered_map<std::string, std::string> 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<ResolvedResource, std::string> resolve_resource(
Expand Down Expand Up @@ -114,6 +117,7 @@ std::expected<ResolvedResource, std::string> 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;
Expand Down
15 changes: 15 additions & 0 deletions src/xpkg-loader.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>::max())
r = static_cast<int>(v);
}
lua::pop(L, 1);
return r;
}

std::vector<std::string> get_str_array(lua::State* L, int idx, const char* key) {
std::vector<std::string> result;
lua::getfield(L, idx, key);
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/xpkg.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ struct PlatformResource {
std::unordered_map<std::string, std::string> sha256_by_arch; // canonical-arch -> sha256 (Scheme C: template / res)
std::unordered_map<std::string, std::string> 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.
Expand Down
38 changes: 38 additions & 0 deletions tests/fixtures/pkgindex/pkgs/v/v2revision.lua
Original file line number Diff line number Diff line change
@@ -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,
},
},
},
}
51 changes: 51 additions & 0 deletions tests/test_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading