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.49"
version = "0.0.50"
description = "C++23 reference implementation of the xpkg V2 spec (multi-arch)"
license = "Apache-2.0"
repo = "https://github.com/openxlings/libxpkg"
Expand Down
30 changes: 25 additions & 5 deletions src/lua-stdlib/xim/libxpkg/elfpatch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -608,17 +608,37 @@ function M.closure_lib_paths(opt)
or (_RUNTIME and (_RUNTIME.runtime_deps_list or _RUNTIME.deps_list))
or {}
local deps_exports = _RUNTIME and _RUNTIME.deps_exports or {}
-- Two sources, both already ABSOLUTE and both decided by the resolver:
-- what the dep declared (deps_exports), else what the resolver filled in
-- by convention (resolved_deps.libdirs).
--
-- What used to be here is gone: a branch that took the dep's NAME, asked
-- pkginfo to find it again, and tried {lib64, lib} against whatever came
-- back. That was a second, independent resolution — and with two versions
-- of one package installed it answered differently from the one that
-- chose the interpreter, producing a binary whose loader and libc came
-- from different payloads. It segfaults before main, and the error names
-- a GLIBC_PRIVATE symbol rather than anything about versions.
--
-- The convention itself did not go away; it moved to the single place
-- that is entitled to apply it. See
-- xlings/.agents/docs/2026-08-05-dependency-resolution-single-source.md
local resolved = (_RUNTIME and type(_RUNTIME.resolved_deps) == "table")
and _RUNTIME.resolved_deps or {}
for _, dep_spec in ipairs(deps_list) do
local declared = deps_exports[dep_spec]
local rec = resolved[dep_spec]
if declared and declared.libdirs and #declared.libdirs > 0 then
for _, d in ipairs(declared.libdirs) do _push(d) end
else
elseif rec and rec.libdirs and #rec.libdirs > 0 then
for _, d in ipairs(rec.libdirs) do _push(d) end
elseif _LIBXPKG_MODULES and _LIBXPKG_MODULES.pkginfo then
-- Only a client that predates resolved_deps reaches this. Kept so
-- an older xlings keeps working, and warned so the degraded path
-- is never silent.
local dep_name = dep_spec:gsub("@.*", ""):gsub("^.+:", "")
local dep_version = dep_spec:find("@", 1, true) and dep_spec:match("@(.+)") or nil
local dep_dir
if _LIBXPKG_MODULES and _LIBXPKG_MODULES.pkginfo then
dep_dir = _LIBXPKG_MODULES.pkginfo.dep_install_dir(dep_name, dep_version)
end
local dep_dir = _LIBXPKG_MODULES.pkginfo.dep_install_dir(dep_name, dep_version)
if dep_dir then
for _, sub in ipairs({"lib64", "lib"}) do
local libdir = path.join(dep_dir, sub)
Expand Down
55 changes: 53 additions & 2 deletions src/lua-stdlib/xim/libxpkg/pkginfo.lua
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,61 @@ local function _resolve_dep_via_xvm(dep_name, dep_version)
return nil
end

-- The resolver's record for a dependency, if this client sends one.
--
-- type(), not truthiness: an unknown _RUNTIME field is nil here, but the same
-- probe written as `if _RUNTIME.resolved_deps then` on a module proxy is true
-- everywhere — a trap this repo has fallen into twice (subos.env,
-- xim.pkgindex.sysroot).
--
-- Matched by spec first, because that is the key; then by bare name, because
-- callers reach this function from several directions and not all of them
-- still have the original spec string in hand.
function M.resolved_dep(dep_name, dep_version)
local t = _RUNTIME and _RUNTIME.resolved_deps
if type(t) ~= "table" then return nil end
if dep_version and dep_version ~= "" then
local exact = t[dep_name .. "@" .. dep_version]
if exact then return exact end
end
local _, bare = _parse_namespace(dep_name)
for spec, rec in pairs(t) do
local sname = spec:gsub("@.*", "")
local _, sbare = _parse_namespace(sname)
if sname == dep_name or sbare == bare then return rec end
end
return nil
end

-- Where a dependency actually lives.
--
-- The resolver already decided this. Everything below the first branch is a
-- SECOND answer to a question that has one — kept only for callers with no
-- install context (tool scripts, offline queries), and noisy on purpose so
-- that "we guessed" is never silent.
--
-- Two independent answers is exactly how a binary ends up with its INTERP
-- from one glibc and its RUNPATH from another, which segfaults before main
-- with no diagnostic. See
-- xlings/.agents/docs/2026-08-05-dependency-resolution-single-source.md
function M.dep_install_dir(dep_name, dep_version)
local rec = M.resolved_dep(dep_name, dep_version)
if rec and rec.install_dir and rec.install_dir ~= "" then
return rec.install_dir
end

local result = _resolve_dep_via_scan(dep_name, dep_version)
if result then return result end
return _resolve_dep_via_xvm(dep_name, dep_version)
if not result then
result = _resolve_dep_via_xvm(dep_name, dep_version)
end
local log = _get_log()
if log and _RUNTIME and _RUNTIME.install_dir then
-- Inside an install, a miss means the client predates resolved_deps.
-- Outside one there is nothing to miss, so no warning.
log.warn("dep_install_dir(%s): no resolver record, fell back to a "
.. "scan -> %s", tostring(dep_name), tostring(result))
end
return result
end

function M.install_dir(pkgname, pkgversion)
Expand Down
40 changes: 40 additions & 0 deletions src/xpkg-executor.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ struct DepExport {
std::string abi; // e.g. "linux-x86_64-glibc"
};

// One runtime dependency, as the RESOLVER settled it — not as the recipe
// spelled it.
//
// This is the record that makes "which version is this dependency" have one
// answer. `DepExport` below cannot serve: it carries only what a dep
// explicitly declared, so a dep that declared nothing is absent from it, and
// absence was defined to mean "fall back to convention" — which is a second
// answerer wearing a different hat. Every runtime dep appears here, declared
// or not.
struct ResolvedDep {
std::string spec; // the recipe's own text, e.g. "xim:glibc@>=2.38"
std::string name; // canonical, e.g. "xim:glibc"
std::string version; // what it resolved TO, e.g. "2.44"
std::string install_dir; // absolute payload directory — the authority
std::vector<std::string> libdirs; // absolute; convention-filled when the
// dep declared none, so no consumer
// has to re-derive it
std::string source; // why this one: "plan" | "pinned-active" | ...
};

struct ExecutionContext {
std::string pkg_name, version, platform, arch;
fs::path install_file, install_dir;
Expand All @@ -38,6 +58,10 @@ struct ExecutionContext {
// 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;
// Keyed by the same spec string as deps_exports, but TOTAL: every runtime
// dep is here whether or not it declared exports. Empty only when the
// client predates it — libxpkg then degrades to scanning and says so.
std::unordered_map<std::string, ResolvedDep> resolved_deps;
// The current package's own exports (rule 2 in the predicate trigger).
DepExport self_exports;
std::string subos_sysrootdir;
Expand Down Expand Up @@ -641,6 +665,22 @@ void inject_context(lua::State* L, const mcpplibs::xpkg::ExecutionContext& ctx)
}
lua::setfield(L, -2, "deps_exports");

// resolved_deps: { [spec] = { name, version, install_dir, libdirs, source } }
// Total, unlike deps_exports. A hook that finds a dep missing from HERE is
// running on a client that does not send it, not looking at a dep that
// declared nothing — the two used to be indistinguishable.
lua::newtable(L);
for (auto& [dep_spec, r] : ctx.resolved_deps) {
lua::newtable(L);
set_string_field(L, "name", r.name);
set_string_field(L, "version", r.version);
set_string_field(L, "install_dir", r.install_dir);
set_string_field(L, "source", r.source);
push_string_array(r.libdirs, "libdirs");
lua::setfield(L, -2, dep_spec.c_str());
}
lua::setfield(L, -2, "resolved_deps");

// self_exports: same shape as a single deps_exports entry. Empty
// strings/arrays when the current package didn't declare exports.
lua::newtable(L);
Expand Down
Loading