diff --git a/mcpp.toml b/mcpp.toml index ca978da..cfb78d5 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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" diff --git a/src/xpkg-compat.cppm b/src/xpkg-compat.cppm index 9ebba5e..970f7c1 100644 --- a/src/xpkg-compat.cppm +++ b/src/xpkg-compat.cppm @@ -100,6 +100,15 @@ std::expected 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; diff --git a/src/xpkg-loader.cppm b/src/xpkg-loader.cppm index d01dac9..7c84f28 100644 --- a/src/xpkg-loader.cppm +++ b/src/xpkg-loader.cppm @@ -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) @@ -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" @@ -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 -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()); @@ -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); @@ -631,6 +658,11 @@ load_package(const fs::path& pkg_path) { return p; } +std::expected +load_package(const fs::path& pkg_path) { + return load_package(pkg_path, {}); +} + std::expected build_index(const fs::path& repo_dir, const std::string& namespace_ = "") { PackageIndex index; diff --git a/tests/fixtures/pkgindex/pkgs/v/v2platform_context.lua b/tests/fixtures/pkgindex/pkgs/v/v2platform_context.lua new file mode 100644 index 0000000..226ea9d --- /dev/null +++ b/tests/fixtures/pkgindex/pkgs/v/v2platform_context.lua @@ -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 diff --git a/tests/test_loader.cpp b/tests/test_loader.cpp index e02827d..f3067df 100644 --- a/tests/test_loader.cpp +++ b/tests/test_loader.cpp @@ -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(); @@ -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); +}