diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1efcdf..0ede5fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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}" \ @@ -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. diff --git a/.xlings.json b/.xlings.json index 9a6e5d9..a7b8d67 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": { "linux": "0.0.67" } + "mcpp": { "linux": "0.0.109" } } } diff --git a/mcpp.toml b/mcpp.toml index 6577c1a..8d4ef00 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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" diff --git a/src/xpkg-executor.cppm b/src/xpkg-executor.cppm index a64354b..854b4d1 100644 --- a/src/xpkg-executor.cppm +++ b/src/xpkg-executor.cppm @@ -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 args; + std::vector> envs; // environment variables }; @@ -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"); diff --git a/src/xpkg-lua-stdlib.cppm b/src/xpkg-lua-stdlib.cppm index def0db5..d0ef2fd 100644 --- a/src/xpkg-lua-stdlib.cppm +++ b/src/xpkg-lua-stdlib.cppm @@ -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 "@" -- 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 = { @@ -734,6 +758,9 @@ 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() @@ -741,6 +768,38 @@ function M.add(name, opt) 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 diff --git a/tests/test_executor.cpp b/tests/test_executor.cpp index 610792c..4c11828 100644 --- a/tests/test_executor.cpp +++ b/tests/test_executor.cpp @@ -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 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); +}