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
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:

- name: Install xlings
env:
XLINGS_VERSION: 0.4.25
XLINGS_VERSION: 0.4.69
run: |
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
curl -fsSL -o "/tmp/${tarball}" \
Expand All @@ -23,22 +23,22 @@ jobs:
"/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"

- name: Install workspace tools (.xlings.json → mcpp 0.0.7)
- name: Install workspace tools (.xlings.json → mcpp 0.0.109)
run: xlings install -y

# Cache mcpp's self-bootstrapped sandbox (musl-gcc + binutils +
# glibc + ninja + patchelf, ~800 MB). Toolchain set is pinned by
# mcpp 0.0.7, so a fixed key suffices.
# glibc + ninja + patchelf, ~800 MB). Toolchain set is pinned by the
# mcpp version, so a fixed key per version suffices.
- name: Cache mcpp sandbox
uses: actions/cache@v4
with:
path: ~/.xlings/data/xpkgs/xim-x-mcpp/0.0.7/registry
key: mcpp-sandbox-${{ runner.os }}-mcpp0.0.7
path: ~/.xlings/data/xpkgs/xim-x-mcpp/0.0.109/registry
key: mcpp-sandbox-${{ runner.os }}-mcpp0.0.109

- name: Build with mcpp
run: mcpp build

# mcpp 0.0.7 auto-prepends sandbox PATH (patchelf, ninja) for
# mcpp auto-prepends sandbox PATH (patchelf, ninja) for
# test binaries, so Linux elfpatch tests run without manual PATH
# setup. Only macOS-specific tests (need install_name_tool) are
# filtered — they can't run on a Linux runner.
Expand Down
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.67" }
"mcpp": { "linux": "0.0.109" }
}
}
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.46"
version = "0.0.47"
description = "C++23 reference implementation of the xpkg V2 spec (multi-arch)"
license = "Apache-2.0"
repo = "https://github.com/openxlings/libxpkg"
Expand Down
46 changes: 45 additions & 1 deletion src/xpkg-executor.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,37 @@ struct XvmOp {
std::string version;
std::string bindir;
std::string alias;
std::string type; // "program" | "lib"
std::string type; // "program" | "lib" | "files"
std::string filename;
std::string binding;
std::string includedir; // for headers/remove_headers ops

// type = "files": one asset the package places into the subos.
//
// Both ends are relative, and that is a requirement rather than a
// convention. A payload is shared between subos and reference-counted,
// so an absolute destination recorded against it would be wrong for
// every subos but the one that installed it. `src` is relative to the
// payload root, `dst` to the subos root; the consumer resolves them and
// rejects anything absolute or escaping.
//
// Exists because `includedir` can only say "this one directory becomes
// sysroot include". It cannot express a destination, an asset that is
// not a header, or a source and destination that differ in name --
// openssl's `lib64/` -> `usr/lib/` is all three at once. Without a way
// to say it, package indexes grow their own file-placing helpers, and
// the tool managing versions cannot see or undo any of them.
std::string src;
std::string dst;

// Arguments injected ahead of the user's own when a program shim
// dispatches. Separate from `alias` on purpose: the only way to inject
// anything today is to append it to the alias string, which consumers
// then split on the first space. That breaks on any path containing one,
// and it makes every reader of `alias` -- version listings, diagnostics
// -- report a command line where a name belongs.
std::vector<std::string> args;

std::vector<std::pair<std::string, std::string>> envs; // environment variables
};

Expand Down Expand Up @@ -794,6 +821,23 @@ public:
op.filename = read_field("filename");
op.binding = read_field("binding");
op.includedir = read_field("includedir");
op.src = read_field("src");
op.dst = read_field("dst");

// Read args array (ordered; empty when absent)
lua::getfield(L_, -1, "args");
if (lua::type(L_, -1) == lua::TTABLE) {
for (int i = 1;; ++i) {
lua::rawgeti(L_, -1, i);
if (lua::type(L_, -1) != lua::TSTRING) {
lua::pop(L_, 1);
break;
}
op.args.emplace_back(lua::tostring(L_, -1));
lua::pop(L_, 1);
}
}
lua::pop(L_, 1);

// Read envs table (key-value pairs)
lua::getfield(L_, -1, "envs");
Expand Down
59 changes: 59 additions & 0 deletions src/xpkg-lua-stdlib.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,30 @@ end

_XVM_OPS = _XVM_OPS or {}

--- Register one entry with the version manager.
-- @param name target name
-- @param opt table with:
-- type "program" (default) | "lib" | "files"
-- version defaults to the package version
-- bindir directory holding the artifact (default: install_dir)
-- filename artifact name inside bindir
-- alias name it is exposed under
-- binding "<root>@<version>" -- which release this belongs to
-- args list of arguments injected ahead of the user's own when the
-- shim dispatches. Use this rather than appending to `alias`:
-- consumers split the alias on its first space, so a path with
-- one in it breaks, and every reader of `alias` then shows a
-- command line where a name belongs.
-- envs environment variables
--
-- For type = "files", the entry describes an asset placed into the subos
-- instead of an artifact to dispatch:
-- src source, relative to the payload root
-- dst destination, relative to the subos root
--
-- Both must be relative. A payload is shared between subos and
-- reference-counted, so an absolute destination recorded against it would
-- be correct for exactly one subos and wrong for the rest.
function M.add(name, opt)
opt = opt or {}
local entry = {
Expand All @@ -734,13 +758,48 @@ function M.add(name, opt)
type = opt.type or "",
filename = opt.filename or "",
binding = opt.binding or "",
src = opt.src or "",
dst = opt.dst or "",
args = opt.args or nil,
envs = opt.envs or nil,
}
local log = _get_log()
if log then log.debug("xvm add %s version=%s", name, entry.version) end
table.insert(_XVM_OPS, entry)
end

--- Declare an asset this package places into the subos.
--
-- Sugar over `M.add` for the case where the entry is a file rather than
-- something to dispatch, so the caller does not have to invent a target
-- name: one is derived from the package name. A release may declare several,
-- and each call adds one.
--
-- @param opt src / dst (both relative, see M.add), plus binding
function M.files(opt)
opt = opt or {}
if not opt.src or opt.src == "" then
error("xvm.files: src is required")
end
if not opt.dst or opt.dst == "" then
error("xvm.files: dst is required")
end
local owner = opt.name
or (_RUNTIME and _RUNTIME.pkg_name)
or "xvm"
-- Derived rather than caller-supplied so two declarations from one
-- package cannot collide on the same target name.
_XVM_FILES_SEQ = (_XVM_FILES_SEQ or 0) + 1
local target = string.format("%s.files.%d", owner, _XVM_FILES_SEQ)
M.add(target, {
type = "files",
src = opt.src,
dst = opt.dst,
version = opt.version,
binding = opt.binding,
})
end

function M.remove(name, version)
local log = _get_log()
if log then log.debug("xvm remove %s %s", name, version or "") end
Expand Down
110 changes: 110 additions & 0 deletions tests/test_executor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1105,3 +1105,113 @@ TEST(ExecutorTest, ApplyInstallStamp_IsIdempotent) {

fs::remove_all(temp);
}

// ============================================================
// files assets and injected args
//
// `includedir` could only say "this one directory becomes sysroot include".
// It could not express a destination, an asset that is not a header, or a
// source and destination that differ in name -- openssl's `lib64/` ->
// `usr/lib/` is all three at once. With no way to say it, package indexes
// grow their own file-placing helpers and the tool managing versions can
// neither see nor undo them.
//
// `args` is separate from `alias` because the only way to inject anything
// used to be appending it to the alias string, which consumers then split on
// the first space -- broken by any path containing one, and it makes every
// reader of `alias` report a command line where a name belongs.
// ============================================================

namespace {

// Write a recipe whose config() hook is `body`, and return its ops.
std::vector<XvmOp> ops_from_config(const fs::path& dir, const char* body) {
fs::create_directories(dir);
auto pkg = dir / "opsfixture.lua";
std::string lua =
"package = { spec = \"1\", name = \"opsfixture\", type = \"package\",\n"
" xpm = { linux = { [\"1.0.0\"] = {} },\n"
" macosx = { [\"1.0.0\"] = {} },\n"
" windows = { [\"1.0.0\"] = {} } } }\n"
"import(\"xim.libxpkg.xvm\")\n"
"function config()\n";
lua += body;
lua += "\n return true\nend\n";
std::ofstream(pkg) << lua;

auto exec = create_executor(pkg.string());
EXPECT_TRUE(exec.has_value());
if (!exec) return {};
auto ctx = make_context(dir, "linux");
ctx.pkg_name = "opsfixture";
auto hook = exec->run_hook(HookType::Config, ctx);
EXPECT_TRUE(hook.success) << hook.error;
return exec->xvm_operations();
}

} // namespace

TEST(ExecutorTest, XvmAdd_CarriesSrcAndDstForFilesAssets) {
auto dir = fs::temp_directory_path() / "libxpkg_files_assets";
fs::remove_all(dir);
auto ops = ops_from_config(dir,
" xvm.add(\"pkg.files.1\", { type = \"files\",\n"
" src = \"include/openssl\", dst = \"usr/include/openssl\" })");

ASSERT_EQ(ops.size(), 1u);
EXPECT_EQ(ops[0].type, "files");
EXPECT_EQ(ops[0].src, "include/openssl");
EXPECT_EQ(ops[0].dst, "usr/include/openssl");
fs::remove_all(dir);
}

TEST(ExecutorTest, XvmAdd_CarriesInjectedArgsInOrder) {
auto dir = fs::temp_directory_path() / "libxpkg_args";
fs::remove_all(dir);
auto ops = ops_from_config(dir,
" xvm.add(\"clang\", { args = { \"-isystem\", \"/a b/include\",\n"
" \"--sysroot=/root\" } })");

ASSERT_EQ(ops.size(), 1u);
ASSERT_EQ(ops[0].args.size(), 3u);
EXPECT_EQ(ops[0].args[0], "-isystem");
// A path containing a space survives, which it cannot when arguments are
// smuggled through `alias` and split on the first one.
EXPECT_EQ(ops[0].args[1], "/a b/include");
EXPECT_EQ(ops[0].args[2], "--sysroot=/root");
EXPECT_TRUE(ops[0].alias.empty()) << "args must not leak into alias";
fs::remove_all(dir);
}

TEST(ExecutorTest, XvmFiles_DerivesADistinctTargetPerDeclaration) {
auto dir = fs::temp_directory_path() / "libxpkg_files_sugar";
fs::remove_all(dir);
auto ops = ops_from_config(dir,
" xvm.files({ src = \"include\", dst = \"usr/include\" })\n"
" xvm.files({ src = \"lib64\", dst = \"usr/lib\" })");

ASSERT_EQ(ops.size(), 2u);
EXPECT_EQ(ops[0].type, "files");
EXPECT_EQ(ops[1].type, "files");
EXPECT_EQ(ops[0].src, "include");
EXPECT_EQ(ops[1].src, "lib64");
// Names are derived, not caller-supplied, so two declarations from one
// package cannot collide.
EXPECT_NE(ops[0].name, ops[1].name);
fs::remove_all(dir);
}

TEST(ExecutorTest, XvmAdd_OmittingTheNewFieldsLeavesThemEmpty) {
auto dir = fs::temp_directory_path() / "libxpkg_no_new_fields";
fs::remove_all(dir);
// An existing recipe must be completely unaffected.
auto ops = ops_from_config(dir,
" xvm.add(\"tool\", { bindir = \"bin\", binding = \"root@1.0.0\" })");

ASSERT_EQ(ops.size(), 1u);
EXPECT_TRUE(ops[0].src.empty());
EXPECT_TRUE(ops[0].dst.empty());
EXPECT_TRUE(ops[0].args.empty());
EXPECT_EQ(ops[0].binding, "root@1.0.0");
fs::remove_all(dir);
}
Loading