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 mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "xpkg"
version = "0.0.43"
version = "0.0.44"
description = "C++23 reference implementation of the xpkg V2 spec (multi-arch)"
license = "Apache-2.0"
repo = "https://github.com/openxlings/libxpkg"
Expand Down
9 changes: 9 additions & 0 deletions src/xpkg-compat.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ std::expected<ResolvedResource, std::string> resolve_resource(

const auto arch = normalize_arch(context.arch);
const auto arch_it = resource->archs.find(arch);
if (!resource->archs.empty() && arch_it == resource->archs.end())
return std::unexpected(
"no resource for arch '" + arch + "' in version "
+ resolved_version);
if (!resource->sha256_by_arch.empty()
&& !resource->sha256_by_arch.contains(arch))
return std::unexpected(
"no checksum for arch '" + arch + "' in version "
+ resolved_version);
const ArchResource* arch_resource =
arch_it == resource->archs.end() ? nullptr : &arch_it->second;

Expand Down
42 changes: 37 additions & 5 deletions src/xpkg-loader.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ namespace mcpplibs::xpkg::loader_detail {
// globals. This gives the loader a self-contained pure Lua 5.4 environment
// that does not depend on any xmake runtime. Stubs ensure legacy or
// third-party packages can still be loaded without crashing.
void register_loader_sandbox(lua::State* L) {
void register_loader_sandbox(
lua::State* L,
std::string_view platform,
std::string_view arch) {
lua::pushstring(L, std::string(platform).c_str());
lua::setglobal(L, "__xpkg_platform__");
lua::pushstring(L, std::string(arch).c_str());
lua::setglobal(L, "__xpkg_arch__");
lua::L_dostring(L,
// no-op import: returns a deep proxy and sets it as a global
// (matches xmake behavior where import("platform") sets _G.platform)
Expand Down Expand Up @@ -43,11 +50,25 @@ void register_loader_sandbox(lua::State* L) {
"end\n"

// non-standard global stubs
"function is_host() return false end\n"
"function is_host(...) "
" for i = 1, select('#', ...) do "
" if select(i, ...) == __xpkg_platform__ then return true end "
" end "
" return false "
"end\n"
"function is_arch(...) "
" for i = 1, select('#', ...) do "
" if select(i, ...) == __xpkg_arch__ then return true end "
" end "
" return false "
"end\n"
"is_plat = is_host\n"
"_RUNTIME = { platform = __xpkg_platform__, arch = __xpkg_arch__ }\n"
"format = string.format\n"

// os extensions (safe defaults)
"os.host = os.host or function() return 'unknown' end\n"
"os.host = function() return __xpkg_platform__ ~= '' and __xpkg_platform__ or 'unknown' end\n"
"os.arch = function() return __xpkg_arch__ ~= '' and __xpkg_arch__ or 'unknown' end\n"
"os.isfile = os.isfile or function() return false end\n"
"os.isdir = os.isdir or function() return false end\n"
"os.scriptdir = os.scriptdir or function() return '.' end\n"
Expand Down Expand Up @@ -580,8 +601,13 @@ bool run_pkgindex_build(const fs::path& repo_dir) {

export namespace mcpplibs::xpkg {

struct LoaderContext {
std::string platform;
std::string arch;
};

std::expected<Package, std::string>
load_package(const fs::path& pkg_path) {
load_package(const fs::path& pkg_path, const LoaderContext& context) {
if (!fs::exists(pkg_path))
return std::unexpected("file not found: " + pkg_path.string());

Expand All @@ -590,7 +616,8 @@ load_package(const fs::path& pkg_path) {
lua::L_openlibs(L);

// Register loader sandbox (pure Lua 5.4, no xmake dependency)
loader_detail::register_loader_sandbox(L);
loader_detail::register_loader_sandbox(
L, context.platform, normalize_arch(context.arch));

if (lua::L_dofile(L, pkg_path.string().c_str()) != lua::OK) {
std::string err = lua::tostring(L, -1);
Expand Down Expand Up @@ -631,6 +658,11 @@ load_package(const fs::path& pkg_path) {
return p;
}

std::expected<Package, std::string>
load_package(const fs::path& pkg_path) {
return load_package(pkg_path, {});
}

std::expected<PackageIndex, std::string>
build_index(const fs::path& repo_dir, const std::string& namespace_ = "") {
PackageIndex index;
Expand Down
19 changes: 19 additions & 0 deletions tests/fixtures/pkgindex/pkgs/v/v2platform_context.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package = {
spec = "2",
name = "v2platform-context",
xpm = {},
}

if is_host("linux") then
package.xpm.linux = {
["1.0.0"] = {
url = "https://example.test/linux-" .. os.arch() .. ".tar.gz",
},
}
elseif is_host("macosx") then
package.xpm.macosx = {
["1.0.0"] = {
url = "https://example.test/macosx-" .. os.arch() .. ".tar.gz",
},
}
end
49 changes: 49 additions & 0 deletions tests/test_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,24 @@ TEST(LoaderTest, SourceDefaultsAreParsedAsMetadataNotVersions) {
EXPECT_FALSE(result->xpm.entries.contains("source"));
}

TEST(LoaderTest, PlatformContextEvaluatesLegacyHostConditionals) {
auto linux = load_package(
PKGINDEX / "pkgs/v/v2platform_context.lua",
LoaderContext{.platform = "linux", .arch = "x86_64"});
ASSERT_TRUE(linux.has_value()) << linux.error();
EXPECT_EQ(
linux->xpm.entries.at("linux").at("1.0.0").url,
"https://example.test/linux-x86_64.tar.gz");

auto macos = load_package(
PKGINDEX / "pkgs/v/v2platform_context.lua",
LoaderContext{.platform = "macosx", .arch = "aarch64"});
ASSERT_TRUE(macos.has_value()) << macos.error();
EXPECT_EQ(
macos->xpm.entries.at("macosx").at("1.0.0").url,
"https://example.test/macosx-aarch64.tar.gz");
}

TEST(CompatTest, XlingsResSourceFollowsRefAndSelectsArchHash) {
auto package = load_package(PKGINDEX / "pkgs/v/v2source_res.lua");
ASSERT_TRUE(package.has_value()) << package.error();
Expand Down Expand Up @@ -414,3 +432,34 @@ TEST(CompatTest, RefCyclesAreRejected) {
ASSERT_FALSE(resolved.has_value());
EXPECT_NE(resolved.error().find("cycle"), std::string::npos);
}

TEST(CompatTest, MissingPerArchResourceFailsClosed) {
PlatformMatrix matrix;
matrix.source = "https://fallback.test/${arch}.tar.gz";
matrix.entries["linux"]["1.0.0"].archs["x86_64"] = {
.url = "https://example.test/x86_64.tar.gz",
.sha256 = "x86-hash",
};
auto resolved = resolve_resource(matrix, {
.name = "tool",
.version = "1.0.0",
.platform = "linux",
.arch = "aarch64",
});
ASSERT_FALSE(resolved.has_value());
EXPECT_NE(resolved.error().find("no resource for arch"), std::string::npos);
}

TEST(CompatTest, MissingPerArchChecksumFailsClosed) {
PlatformMatrix matrix;
matrix.source = "xlings-res";
matrix.entries["linux"]["1.0.0"].sha256_by_arch["x86_64"] = "x86-hash";
auto resolved = resolve_resource(matrix, {
.name = "tool",
.version = "1.0.0",
.platform = "linux",
.arch = "aarch64",
});
ASSERT_FALSE(resolved.has_value());
EXPECT_NE(resolved.error().find("no checksum for arch"), std::string::npos);
}
Loading