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: 1 addition & 1 deletion .xlings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"workspace": {
"mcpp": { "linux": "0.0.7" }
"mcpp": { "linux": "0.0.67" }
}
}
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
namespace = "mcpplibs"
name = "xpkg"
version = "0.0.39"
description = "C++23 reference implementation of the xpkg V1 spec"
version = "0.0.42"
description = "C++23 reference implementation of the xpkg V2 spec (multi-arch)"
license = "Apache-2.0"
repo = "https://github.com/openxlings/libxpkg"

Expand Down
57 changes: 55 additions & 2 deletions src/xpkg-loader.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,11 @@ PlatformMatrix parse_xpm(lua::State* L, int pkg_idx) {
if (lua::type(L, -2) == lua::TSTRING)
version = lua::tostring(L, -2);

// Skip non-version keys (deps, inherits, etc.)
if (!version.empty() && version != "deps" && version != "inherits") {
// Skip non-version keys. `exports` is a platform-level table
// (parsed above) that would otherwise leak in as a bogus
// version entry; `deps`/`inherits` likewise.
if (!version.empty() && version != "deps"
&& version != "inherits" && version != "exports") {
PlatformResource res;
if (lua::type(L, -1) == lua::TTABLE) {
int res_idx = lua::gettop(L);
Expand All @@ -316,6 +319,56 @@ 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");

// ---- V2 multi-arch shapes ----
// Scheme C / res: `sha256` is a per-arch TABLE rather
// than a string (get_str returned "" for a table).
res.sha256_by_arch = get_str_map(L, res_idx, "sha256");
// Re-key the per-arch sha256 map to canonical arch names.
if (!res.sha256_by_arch.empty()) {
std::unordered_map<std::string, std::string> canon;
for (auto& [k, v] : res.sha256_by_arch)
canon[normalize_arch(k)] = v;
res.sha256_by_arch = std::move(canon);
}
// Optional ${arch_alias} mapping (canonical -> upstream token).
{
auto alias = get_str_map(L, res_idx, "arch_alias");
for (auto& [k, v] : alias)
res.arch_alias[normalize_arch(k)] = v;
}
res.is_res = get_bool(L, res_idx, "res");

// Scheme B: per-arch resource map. Detected when the
// entry carries no single-arch url/ref/sha256 and no
// template/res markers, but has arch-named subtables.
if (res.url.empty() && res.ref.empty() && res.sha256.empty()
&& res.sha256_by_arch.empty() && !res.is_res) {
lua::pushnil(L);
while (lua::next(L, res_idx)) {
if (lua::type(L, -2) == lua::TSTRING
&& lua::type(L, -1) == lua::TTABLE) {
std::string canon = normalize_arch(lua::tostring(L, -2));
if (canon == "x86_64" || canon == "aarch64"
|| canon == "x86") {
int arch_idx = lua::gettop(L);
ArchResource ar;
ar.url = get_str(L, arch_idx, "url");
if (ar.url.empty()) {
ar.mirrors = get_str_map(L, arch_idx, "url");
if (auto it = ar.mirrors.find("GLOBAL");
it != ar.mirrors.end())
ar.url = it->second;
else if (!ar.mirrors.empty())
ar.url = ar.mirrors.begin()->second;
}
ar.sha256 = get_str(L, arch_idx, "sha256");
res.archs[canon] = std::move(ar);
}
}
lua::pop(L, 1); // pop value, keep key for next()
}
}
} else if (lua::type(L, -1) == lua::TSTRING) {
// e.g. "XLINGS_RES" — treat as url placeholder
res.url = lua::tostring(L, -1);
Expand Down
49 changes: 47 additions & 2 deletions src/xpkg.cppm
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module;
#include <string>
#include <string_view>
#include <vector>
#include <filesystem>
#include <unordered_map>
Expand All @@ -12,11 +13,40 @@ export namespace mcpplibs::xpkg {
enum class PackageType { Package, Script, Template, Config, Subos };
enum class PackageStatus { Dev, Stable, Deprecated };

struct PlatformResource {
// Canonicalize a CPU-arch token to xlings' internal spelling.
// Accepts common aliases so recipes can use upstream names verbatim:
// amd64 / x64 / x86-64 / x86_64 -> "x86_64"
// arm64 / armv8 / aarch64 -> "aarch64"
// x86 / i386 / i686 -> "x86"
// Unknown tokens are returned lower-cased unchanged. Case-insensitive.
std::string normalize_arch(std::string_view raw);

// True when two arch tokens denote the same canonical arch (e.g. "arm64"
// and "aarch64"). Used by the install-time resolver and `package.archs`
// validation so authors and hosts can disagree on spelling.
bool arch_matches(std::string_view a, std::string_view b);

// A single arch's download resource. Lighter than PlatformResource (no
// `ref`, no nested archs) so PlatformResource::archs stays a map of a
// COMPLETE type — std::unordered_map does not support incomplete value
// types, which a recursive PlatformResource-in-PlatformResource would be.
struct ArchResource {
std::string url;
std::string sha256;
std::string ref; // version alias, e.g. "latest" -> "1.0.0"
std::unordered_map<std::string, std::string> mirrors; // region: GLOBAL/CN
};

struct PlatformResource {
std::string url; // single url | URL template ("...${arch}...") | "XLINGS_RES" | "" when `archs` used
std::string sha256; // single-arch sha256 | "" when sha256_by_arch used
std::string ref; // version alias, e.g. "latest" -> "1.0.0"
std::unordered_map<std::string, std::string> mirrors; // e.g. "GLOBAL"->url, "CN"->url
// ---- V2 multi-arch additions. All empty/false => legacy V1 resource,
// parsed and resolved exactly as before. ----
std::unordered_map<std::string, ArchResource> archs; // canonical-arch -> resource (Scheme B: per-arch map)
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
};

// What this package exposes to consumers/xlings at install/runtime.
Expand Down Expand Up @@ -131,4 +161,19 @@ PlatformMatrix::~PlatformMatrix() = default;
Package::~Package() = default;
PackageIndex::~PackageIndex() = default;
IndexRepos::~IndexRepos() = default;

std::string normalize_arch(std::string_view raw) {
std::string s;
s.reserve(raw.size());
for (char c : raw)
s += (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
if (s == "amd64" || s == "x64" || s == "x86-64" || s == "x86_64") return "x86_64";
if (s == "arm64" || s == "armv8" || s == "aarch64") return "aarch64";
if (s == "x86" || s == "i386" || s == "i686") return "x86";
return s;
}

bool arch_matches(std::string_view a, std::string_view b) {
return normalize_arch(a) == normalize_arch(b);
}
}
18 changes: 18 additions & 0 deletions tests/fixtures/pkgindex/pkgs/v/v2map.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package = {
spec = "2",
name = "v2map",
description = "V2 fixture: per-arch resource map (Scheme B)",
type = "package",
archs = {"x86_64", "aarch64"},
status = "stable",
categories = {"test"},
xpm = {
linux = {
["latest"] = { ref = "1.0.0" },
["1.0.0"] = {
x86_64 = { url = "https://ex/v2map-1.0.0-linux-x86_64.tar.gz", sha256 = "aaaa" },
aarch64 = { url = "https://ex/v2map-1.0.0-linux-aarch64.tar.gz", sha256 = "bbbb" },
},
},
},
}
21 changes: 21 additions & 0 deletions tests/fixtures/pkgindex/pkgs/v/v2res.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package = {
spec = "2",
name = "v2res",
description = "V2 fixture: XLINGS_RES with per-arch checksums (res shape)",
type = "package",
archs = {"x86_64", "aarch64"},
status = "stable",
categories = {"test"},
xpm = {
linux = {
["latest"] = { ref = "1.0.0" },
["1.0.0"] = {
res = true,
sha256 = {
x86_64 = "aaaa",
aarch64 = "bbbb",
},
},
},
},
}
22 changes: 22 additions & 0 deletions tests/fixtures/pkgindex/pkgs/v/v2tmpl.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package = {
spec = "2",
name = "v2tmpl",
description = "V2 fixture: URL template + per-arch sha256 (Scheme C)",
type = "package",
archs = {"x86_64", "aarch64"},
status = "stable",
categories = {"test"},
xpm = {
linux = {
["latest"] = { ref = "1.0.0" },
["1.0.0"] = {
url = "https://ex/${name}-${version}-${os}-${arch_alias}.${ext}",
sha256 = {
x86_64 = "aaaa",
aarch64 = "bbbb",
},
arch_alias = { x86_64 = "amd64", aarch64 = "arm64" },
},
},
},
}
81 changes: 81 additions & 0 deletions tests/test_loader.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,84 @@ TEST(LoaderTest, LoadPackage_NoExports) {
auto& xpm = result->xpm;
EXPECT_EQ(xpm.exports.find("linux"), xpm.exports.end());
}

// ---------------------------------------------------------------------------
// Arch normalization
// ---------------------------------------------------------------------------

TEST(ArchTest, NormalizesAliases) {
EXPECT_EQ(normalize_arch("amd64"), "x86_64");
EXPECT_EQ(normalize_arch("x64"), "x86_64");
EXPECT_EQ(normalize_arch("x86-64"), "x86_64");
EXPECT_EQ(normalize_arch("x86_64"), "x86_64"); // canonical passthrough
EXPECT_EQ(normalize_arch("arm64"), "aarch64");
EXPECT_EQ(normalize_arch("armv8"), "aarch64");
EXPECT_EQ(normalize_arch("aarch64"), "aarch64");
EXPECT_EQ(normalize_arch("AArch64"), "aarch64"); // case-insensitive
}

TEST(ArchTest, ArchMatchesAcrossSpellings) {
EXPECT_TRUE(arch_matches("arm64", "aarch64"));
EXPECT_TRUE(arch_matches("amd64", "x86_64"));
EXPECT_FALSE(arch_matches("x86_64", "aarch64"));
}

// ---------------------------------------------------------------------------
// V2 multi-arch xpm shapes
// ---------------------------------------------------------------------------

// Scheme B: per-arch resource map. Each arch carries its own url + sha256;
// the single-arch url/sha256 stay empty. Arch keys normalize to canonical.
TEST(LoaderTest, V2_PerArchMap_ParsesBothArches) {
auto result = load_package(PKGINDEX / "pkgs/v/v2map.lua");
ASSERT_TRUE(result.has_value()) << result.error();
auto& r = result->xpm.entries.at("linux").at("1.0.0");
EXPECT_TRUE(r.url.empty());
EXPECT_TRUE(r.sha256.empty());
ASSERT_EQ(r.archs.size(), 2u);
EXPECT_EQ(r.archs.at("x86_64").url, "https://ex/v2map-1.0.0-linux-x86_64.tar.gz");
EXPECT_EQ(r.archs.at("x86_64").sha256, "aaaa");
EXPECT_EQ(r.archs.at("aarch64").url, "https://ex/v2map-1.0.0-linux-aarch64.tar.gz");
EXPECT_EQ(r.archs.at("aarch64").sha256, "bbbb");
}

// Scheme C: a URL template plus a per-arch sha256 table and an arch_alias
// map. The template string is kept verbatim (expanded only at install time).
TEST(LoaderTest, V2_Template_ParsesShaMapAndAlias) {
auto result = load_package(PKGINDEX / "pkgs/v/v2tmpl.lua");
ASSERT_TRUE(result.has_value()) << result.error();
auto& r = result->xpm.entries.at("linux").at("1.0.0");
EXPECT_EQ(r.url, "https://ex/${name}-${version}-${os}-${arch_alias}.${ext}");
EXPECT_TRUE(r.sha256.empty()); // sha256 was a table, not a string
ASSERT_EQ(r.sha256_by_arch.size(), 2u);
EXPECT_EQ(r.sha256_by_arch.at("x86_64"), "aaaa");
EXPECT_EQ(r.sha256_by_arch.at("aarch64"), "bbbb");
EXPECT_EQ(r.arch_alias.at("x86_64"), "amd64");
EXPECT_EQ(r.arch_alias.at("aarch64"), "arm64");
EXPECT_FALSE(r.is_res);
}

// res shape: XLINGS_RES auto-URL plus per-arch checksums (closes the
// XLINGS_RES "no sha256" gap). is_res flags install-time URL synthesis.
TEST(LoaderTest, V2_Res_ParsesFlagAndShaMap) {
auto result = load_package(PKGINDEX / "pkgs/v/v2res.lua");
ASSERT_TRUE(result.has_value()) << result.error();
auto& r = result->xpm.entries.at("linux").at("1.0.0");
EXPECT_TRUE(r.is_res);
ASSERT_EQ(r.sha256_by_arch.size(), 2u);
EXPECT_EQ(r.sha256_by_arch.at("x86_64"), "aaaa");
EXPECT_EQ(r.sha256_by_arch.at("aarch64"), "bbbb");
EXPECT_TRUE(r.archs.empty()); // res shape is not a per-arch map
}

// Legacy single-arch entries must be entirely unaffected by V2 parsing.
TEST(LoaderTest, V2_LegacySingleArch_Unchanged) {
auto result = load_package(PKGINDEX / "pkgs/h/hello.lua");
ASSERT_TRUE(result.has_value()) << result.error();
auto& r = result->xpm.entries.at("linux").at("1.0.0");
EXPECT_EQ(r.url, "https://example.com/hello-1.0.0-linux.tar.gz");
EXPECT_FALSE(r.sha256.empty());
EXPECT_TRUE(r.archs.empty());
EXPECT_TRUE(r.sha256_by_arch.empty());
EXPECT_FALSE(r.is_res);
}
Loading