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
395 changes: 337 additions & 58 deletions src/lua-stdlib/xim/libxpkg/elfpatch.lua

Large diffs are not rendered by default.

57 changes: 52 additions & 5 deletions src/xpkg-executor.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,35 @@ namespace fs = std::filesystem;

export namespace mcpplibs::xpkg {

// Slim per-dep export info pre-resolved by xlings: paths are already
// joined with the dep's install_dir, so the Lua side gets ready-to-use
// absolute paths. Only the fields elfpatch.lua actually needs are
// surfaced — additional `exports.*` capabilities (data, build) will be
// injected via separate _RUNTIME tables when those land.
struct DepExport {
std::string loader; // absolute path or empty
std::vector<std::string> libdirs; // absolute paths (already joined)
std::string abi; // e.g. "linux-x86_64-glibc"
};

struct ExecutionContext {
std::string pkg_name, version, platform, arch;
fs::path install_file, install_dir;
fs::path run_dir, xpkg_dir, bin_dir;
fs::path project_data_dir; // project-local data root (empty when no project config)
// `deps_list` retained as the legacy union (runtime ∪ build) for
// backward compat with old install hooks; new code should consult
// `runtime_deps_list` / `build_deps_list` to get the split.
std::vector<std::string> deps_list, args;
std::vector<std::string> runtime_deps_list;
std::vector<std::string> build_deps_list;
// Pre-resolved exports of each runtime dep. Key is the dep spec as
// it appears in runtime_deps_list (e.g. "xim:glibc@2.39"). Only
// deps that actually declare exports show up; missing entries mean
// "this dep declared nothing — fall back to convention".
std::unordered_map<std::string, DepExport> deps_exports;
// The current package's own exports (rule 2 in the predicate trigger).
DepExport self_exports;
std::string subos_sysrootdir;
};

Expand Down Expand Up @@ -320,13 +343,37 @@ void inject_context(lua::State* L, const mcpplibs::xpkg::ExecutionContext& ctx)
set_string_field(L, "project_data_dir", ctx.project_data_dir.string());
set_string_field(L, "subos_sysrootdir", ctx.subos_sysrootdir);

// deps_list as array table
auto push_string_array = [&](const std::vector<std::string>& v, const char* field) {
lua::newtable(L);
for (int i = 0; i < (int)v.size(); ++i) {
lua::pushstring(L, v[i].c_str());
lua::rawseti(L, -2, i + 1);
}
lua::setfield(L, -2, field);
};
push_string_array(ctx.deps_list, "deps_list");
push_string_array(ctx.runtime_deps_list, "runtime_deps_list");
push_string_array(ctx.build_deps_list, "build_deps_list");

// deps_exports: { [dep_spec] = { loader, libdirs, abi }, ... }
// Only deps that declared exports show up here.
lua::newtable(L);
for (int i = 0; i < (int)ctx.deps_list.size(); ++i) {
lua::pushstring(L, ctx.deps_list[i].c_str());
lua::rawseti(L, -2, i + 1);
for (auto& [dep_spec, e] : ctx.deps_exports) {
lua::newtable(L);
set_string_field(L, "loader", e.loader);
set_string_field(L, "abi", e.abi);
push_string_array(e.libdirs, "libdirs");
lua::setfield(L, -2, dep_spec.c_str());
}
lua::setfield(L, -2, "deps_list");
lua::setfield(L, -2, "deps_exports");

// self_exports: same shape as a single deps_exports entry. Empty
// strings/arrays when the current package didn't declare exports.
lua::newtable(L);
set_string_field(L, "loader", ctx.self_exports.loader);
set_string_field(L, "abi", ctx.self_exports.abi);
push_string_array(ctx.self_exports.libdirs, "libdirs");
lua::setfield(L, -2, "self_exports");

// args as array table
lua::newtable(L);
Expand Down
39 changes: 39 additions & 0 deletions src/xpkg-loader.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,45 @@ PlatformMatrix parse_xpm(lua::State* L, int pkg_idx) {
}
lua::pop(L, 1); // pop inherits field

// Parse exports block. Schema (all sub-fields optional):
// exports = {
// runtime = {
// loader = "lib64/ld-linux-x86-64.so.2", -- libc only
// libdirs = { "lib64", "lib" }, -- non-default only
// abi = "linux-x86_64-glibc", -- multi-libc only
// },
// }
// Absence of `exports` (or any sub-field) means "use the default
// convention" — no export declared, predicate falls through.
lua::getfield(L, plat_idx, "exports");
if (lua::type(L, -1) == lua::TTABLE) {
int exports_idx = lua::gettop(L);
ExportsBlock block;

lua::getfield(L, exports_idx, "runtime");
if (lua::type(L, -1) == lua::TTABLE) {
int rt_idx = lua::gettop(L);
lua::getfield(L, rt_idx, "loader");
if (lua::type(L, -1) == lua::TSTRING)
block.runtime.loader = lua::tostring(L, -1);
lua::pop(L, 1);

lua::getfield(L, rt_idx, "libdirs");
if (lua::type(L, -1) == lua::TTABLE)
block.runtime.libdirs = parse_string_array(lua::gettop(L));
lua::pop(L, 1);

lua::getfield(L, rt_idx, "abi");
if (lua::type(L, -1) == lua::TSTRING)
block.runtime.abi = lua::tostring(L, -1);
lua::pop(L, 1);
}
lua::pop(L, 1); // pop runtime sub-table

xpm.exports[platform] = std::move(block);
}
lua::pop(L, 1); // pop exports field

// Iterate version entries
lua::pushnil(L);
while (lua::next(L, plat_idx)) {
Expand Down
31 changes: 31 additions & 0 deletions src/xpkg.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,32 @@ struct PlatformResource {
std::unordered_map<std::string, std::string> mirrors; // e.g. "GLOBAL"->url, "CN"->url
};

// What this package exposes to consumers/xlings at install/runtime.
// All paths are RELATIVE to the package's install_dir; xlings joins them
// with the actual on-disk install path on the target machine to get the
// final absolute path. Per the design doc (2026-05-02-elfpatch-exports-design.md),
// providers only declare fields they actually expose — `{lib64, lib}`
// libdir convention covers 99% of packages and need not be re-declared.
struct ExportsRuntime {
// Dynamic linker (PT_INTERP) path, relative to install_dir.
// Only libc-class providers (glibc, musl) declare this.
std::string loader;
// Library search dirs (RPATH closure). Empty == fall back to the
// {lib64, lib} convention. Only declare if the package's layout
// diverges from convention.
std::vector<std::string> libdirs;
// ABI tag, used to disambiguate when multiple deps provide loaders
// (e.g. "linux-x86_64-glibc" vs "linux-x86_64-musl"). The consumer
// can request a specific abi via `elfpatch.set({interp_from = ...})`.
std::string abi;
};

struct ExportsBlock {
ExportsRuntime runtime;
// Future extension points: data (ssl_certs / locale / ...),
// build (include / cmake / pkgconfig). Not in v1.
};

struct PlatformMatrix {
// platform -> version -> resource
std::unordered_map<std::string,
Expand All @@ -37,6 +63,11 @@ struct PlatformMatrix {
// the active workspace, so the user's tool versions stay untouched.
// Install hooks access them via injected env vars / pkginfo API.
std::unordered_map<std::string, std::vector<std::string>> build_deps;
// platform -> what this package exposes (loader, libdirs, abi, ...)
// See ExportsBlock above. Empty for packages that don't declare anything;
// the predicate-driven elfpatch trigger uses these to decide whether
// a consumer needs INTERP/RPATH patching after install.
std::unordered_map<std::string, ExportsBlock> exports;
// platform inheritance, e.g. "ubuntu" -> "linux"
std::unordered_map<std::string, std::string> inherits;
// Declared outside struct body → outlined symbol in module object.
Expand Down
29 changes: 29 additions & 0 deletions tests/fixtures/pkgindex/pkgs/e/exportsfull.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- Fixture: exports block with all sub-fields populated. Used by the
-- LoaderTest.LoadPackage_ExportsFull test to verify the full parse path.
package = {
spec = "1",
name = "exportsfull",
description = "Fixture: exports.runtime with loader+libdirs+abi",
licenses = {"MIT"},
repo = "https://example.com/exportsfull",
type = "package",
archs = {"x86_64"},

xpm = {
linux = {
deps = { runtime = { "scode:something@1.0" } },
exports = {
runtime = {
loader = "lib64/ld-linux-x86-64.so.2",
libdirs = { "lib64", "lib", "usr/lib" },
abi = "linux-x86_64-glibc",
},
},
["latest"] = { ref = "1.0.0" },
["1.0.0"] = {
url = "https://example.com/exportsfull-1.0.0.tar.gz",
sha256 = "0000000000000000000000000000000000000000000000000000000000000000",
},
},
},
}
28 changes: 28 additions & 0 deletions tests/fixtures/pkgindex/pkgs/e/exportsloaderonly.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-- Fixture: exports.runtime with only `loader` declared (libdirs and abi
-- omitted — testing graceful default for partial declarations).
package = {
spec = "1",
name = "exportsloaderonly",
description = "Fixture: exports.runtime with only loader",
licenses = {"MIT"},
repo = "https://example.com/exportsloaderonly",
type = "package",
archs = {"x86_64"},

xpm = {
linux = {
deps = { runtime = {} },
exports = {
runtime = {
loader = "lib/ld-musl-x86_64.so.1",
-- libdirs / abi intentionally omitted
},
},
["latest"] = { ref = "1.0.0" },
["1.0.0"] = {
url = "https://example.com/exportsloaderonly-1.0.0.tar.gz",
sha256 = "0000000000000000000000000000000000000000000000000000000000000000",
},
},
},
}
45 changes: 45 additions & 0 deletions tests/test_loader.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,48 @@ TEST(LoaderTest, LoadPackage_DepsSplit_KeepsSeparation) {
std::vector<std::string> expectedUnion{"node", "npm", "gcc", "patchelf"};
EXPECT_EQ(un->second, expectedUnion);
}

// exports.runtime with all sub-fields populated must round-trip through the
// parser. This is the "happy path" for declarative provider metadata.
TEST(LoaderTest, LoadPackage_ExportsFull) {
auto result = load_package(PKGINDEX / "pkgs/e/exportsfull.lua");
ASSERT_TRUE(result.has_value()) << result.error();

auto& xpm = result->xpm;
auto eit = xpm.exports.find("linux");
ASSERT_NE(eit, xpm.exports.end());
auto& rt = eit->second.runtime;

EXPECT_EQ(rt.loader, "lib64/ld-linux-x86-64.so.2");
EXPECT_EQ(rt.abi, "linux-x86_64-glibc");
std::vector<std::string> expectedLibdirs{"lib64", "lib", "usr/lib"};
EXPECT_EQ(rt.libdirs, expectedLibdirs);
}

// Partial declaration: only `loader` set, libdirs/abi omitted — must parse
// without error and leave the omitted fields empty (consumers fall back to
// the {lib64, lib} convention for libdirs).
TEST(LoaderTest, LoadPackage_ExportsLoaderOnly) {
auto result = load_package(PKGINDEX / "pkgs/e/exportsloaderonly.lua");
ASSERT_TRUE(result.has_value()) << result.error();

auto& xpm = result->xpm;
auto eit = xpm.exports.find("linux");
ASSERT_NE(eit, xpm.exports.end());
auto& rt = eit->second.runtime;

EXPECT_EQ(rt.loader, "lib/ld-musl-x86_64.so.1");
EXPECT_TRUE(rt.libdirs.empty());
EXPECT_TRUE(rt.abi.empty());
}

// Packages without an `exports` block must remain valid; the platform's
// exports map entry simply doesn't exist (consumers see "no provider
// declared" and the predicate trigger falls through to no-op).
TEST(LoaderTest, LoadPackage_NoExports) {
auto result = load_package(PKGINDEX / "pkgs/h/hello.lua");
ASSERT_TRUE(result.has_value()) << result.error();

auto& xpm = result->xpm;
EXPECT_EQ(xpm.exports.find("linux"), xpm.exports.end());
}
Loading