Skip to content

Commit c3b1f23

Browse files
committed
feat: add runtime library dirs for run and test
1 parent 19b622c commit c3b1f23

6 files changed

Lines changed: 268 additions & 9 deletions

File tree

.agents/docs/2026-06-03-gl-runtime-closure-plan.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,17 @@ behavior.
6161
## Implementation Plan
6262

6363
- [x] Create this repository-level plan checkpoint.
64-
- [ ] Add manifest/runtime metadata parsing and validation.
64+
- [x] Add manifest/runtime metadata parsing and validation.
6565
- Candidate files: `src/manifest.cppm`, manifest tests.
6666
- Invalid entries should fail early: empty library name, absolute path in
6767
package metadata unless explicitly allowed, duplicate capability strings.
68-
- [ ] Carry runtime requirements through the resolved package graph.
68+
- [x] Carry runtime requirements through the resolved package graph.
6969
- Candidate files: dependency resolution and `PackageRoot`/graph structures.
7070
- Runtime requirements must not be mixed into public include usage.
71-
- [ ] Teach `mcpp run` and `mcpp test` to build a run environment.
71+
- [x] Teach `mcpp run` and `mcpp test` to build a run environment.
7272
- Candidate file: `src/cli.cppm`.
73+
- Done: `mcpp run` consumes resolved runtime library directories.
74+
- Done: `mcpp test` uses the same runtime environment for test binaries.
7375
- Linux: prepend resolved runtime directories to `LD_LIBRARY_PATH`.
7476
- macOS: use `DYLD_LIBRARY_PATH` only for local tool execution where allowed,
7577
otherwise prefer rpath/install-name behavior.
@@ -85,7 +87,7 @@ behavior.
8587
requests a runnable bundle.
8688
- Keep system capabilities explicit; do not silently bundle host GPU drivers
8789
unless a package declares a redistributable runtime.
88-
- [ ] Add regression coverage with a small `dlopen` fixture.
90+
- [x] Add regression coverage with a small `dlopen` fixture.
8991
- Test should prove that a library loaded only via `dlopen` is found through
9092
mcpp runtime metadata during `mcpp run`.
9193
- A second pack-oriented test should prove runtime metadata is represented in
@@ -96,16 +98,16 @@ behavior.
9698

9799
## Verification
98100

99-
- [ ] `mcpp build`
100-
- [ ] `mcpp run -- --version`
101-
- [ ] `mcpp test`
101+
- [x] `mcpp build`
102+
- [x] `mcpp run -- --version`
103+
- [x] `mcpp test`
102104
- [ ] `MCPP=<built-mcpp> bash tests/e2e/run_all.sh`
103-
- [ ] Focused runtime metadata e2e for `dlopen` resolution
105+
- [x] Focused runtime metadata e2e for `dlopen` resolution
104106
- [ ] Focused pack e2e for runtime metadata inclusion
105107

106108
## PR / CI / Merge Notes
107109

108-
- [ ] Commit this plan as the first checkpoint.
110+
- [x] Commit this plan as the first checkpoint.
109111
- [ ] Open a PR with sanitized paths and no local machine details.
110112
- [ ] Include a test plan in the PR body.
111113
- [ ] Wait for Linux/macOS/Windows CI.

src/build/plan.cppm

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ struct BuildPlan {
5353

5454
std::vector<CompileUnit> compileUnits; // topologically sorted
5555
std::vector<LinkUnit> linkUnits;
56+
std::vector<std::filesystem::path> runtimeLibraryDirs;
5657
};
5758

5859
// Build a BuildPlan from already-validated inputs.
@@ -166,6 +167,14 @@ local_include_dirs_for_manifest(const std::filesystem::path& root,
166167
return dirs;
167168
}
168169

170+
void append_unique_path(std::vector<std::filesystem::path>& out,
171+
std::filesystem::path path)
172+
{
173+
if (path.empty()) return;
174+
if (std::find(out.begin(), out.end(), path) == out.end())
175+
out.push_back(std::move(path));
176+
}
177+
169178
} // namespace
170179

171180
BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
@@ -192,6 +201,13 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
192201
plan.stdBmiPath = stdBmiPath;
193202
plan.stdObjectPath = stdObjectPath;
194203

204+
for (auto const& package : packages) {
205+
for (auto const& dir : package.manifest.runtimeConfig.libraryDirs) {
206+
append_unique_path(plan.runtimeLibraryDirs,
207+
dir.is_absolute() ? dir : package.root / dir);
208+
}
209+
}
210+
195211
// 1a. Detect basename collisions (both cross-package AND intra-package:
196212
// ftxui ships dom/color.cpp + screen/color.cpp, for instance).
197213
// For colliding files the object path gets a per-unit prefix

src/cli.cppm

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3496,6 +3496,15 @@ int cmd_run(const mcpplibs::cmdline::ParsedArgs& parsed,
34963496
std::fflush(stdout);
34973497
std::string cmd = mcpp::platform::shell::quote(exe.string());
34983498
for (auto& a : passthrough) cmd += " " + mcpp::platform::shell::quote(a);
3499+
3500+
std::optional<mcpp::platform::env::ScopedEnv> runtimeEnv;
3501+
auto runtimeEnvKey = mcpp::platform::env::runtime_library_path_key();
3502+
auto runtimeEnvValue = mcpp::platform::env::prepend_path_list(
3503+
runtimeEnvKey, ctx->plan.runtimeLibraryDirs);
3504+
if (!runtimeEnvKey.empty() && !runtimeEnvValue.empty()) {
3505+
runtimeEnv.emplace(runtimeEnvKey, runtimeEnvValue);
3506+
}
3507+
34993508
int rc = std::system(cmd.c_str());
35003509
return mcpp::platform::process::extract_exit_code(rc) == 0 ? 0 : 1;
35013510
}
@@ -4031,6 +4040,15 @@ int cmd_test(const mcpplibs::cmdline::ParsedArgs& /*parsed*/,
40314040
int passed = 0;
40324041
int failed = 0;
40334042
std::vector<std::string> failures;
4043+
4044+
std::optional<mcpp::platform::env::ScopedEnv> runtimeEnv;
4045+
auto runtimeEnvKey = mcpp::platform::env::runtime_library_path_key();
4046+
auto runtimeEnvValue = mcpp::platform::env::prepend_path_list(
4047+
runtimeEnvKey, ctx->plan.runtimeLibraryDirs);
4048+
if (!runtimeEnvKey.empty() && !runtimeEnvValue.empty()) {
4049+
runtimeEnv.emplace(runtimeEnvKey, runtimeEnvValue);
4050+
}
4051+
40344052
for (auto& lu : ctx->plan.linkUnits) {
40354053
if (lu.kind != mcpp::build::LinkUnit::TestBinary) continue;
40364054
auto exe = ctx->outputDir / lu.output;

src/manifest.cppm

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ struct BuildConfig {
104104
std::string cStandard;
105105
};
106106

107+
// `[runtime]` — requirements needed when launching built binaries.
108+
struct RuntimeConfig {
109+
std::vector<std::filesystem::path> libraryDirs; // relative to package root
110+
std::vector<std::string> dlopenLibs; // runtime-loaded sonames
111+
std::vector<std::string> capabilities; // host/system capabilities
112+
};
113+
107114
// `[target.<triple>]` — per-target overrides.
108115
// Picked up when caller passes --target <triple> to build/run/test.
109116
struct TargetEntry {
@@ -182,6 +189,7 @@ struct Manifest {
182189

183190
Toolchain toolchain; // optional; empty == fallback
184191
BuildConfig buildConfig;
192+
RuntimeConfig runtimeConfig;
185193

186194
// [target.<triple>] tables — empty if user didn't declare any.
187195
std::map<std::string, TargetEntry> targetOverrides;
@@ -779,6 +787,15 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
779787
}
780788
}
781789

790+
// [runtime] — launch-time requirements.
791+
if (auto v = doc->get_string_array("runtime.library_dirs")) {
792+
for (auto& s : *v) m.runtimeConfig.libraryDirs.emplace_back(s);
793+
}
794+
if (auto v = doc->get_string_array("runtime.dlopen_libs"))
795+
m.runtimeConfig.dlopenLibs = *v;
796+
if (auto v = doc->get_string_array("runtime.capabilities"))
797+
m.runtimeConfig.capabilities = *v;
798+
782799
// [lib] — library root convention (cargo-style).
783800
if (auto v = doc->get_string("lib.path")) {
784801
m.lib.path = *v;
@@ -1683,6 +1700,61 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
16831700
auto v = cur.read_string();
16841701
if (!v.empty()) m.buildConfig.cStandard = v;
16851702
}
1703+
else if (key == "runtime") {
1704+
auto runtimeBody = cur.read_table_body();
1705+
LuaCursor rc { runtimeBody };
1706+
rc.skip_ws_and_comments();
1707+
while (!rc.eof()) {
1708+
auto sub = rc.read_key();
1709+
if (sub.empty()) {
1710+
rc.skip_ws_and_comments();
1711+
if (rc.eof()) break;
1712+
++rc.pos;
1713+
continue;
1714+
}
1715+
rc.skip_ws_and_comments();
1716+
if (!rc.consume('=')) {
1717+
return std::unexpected(ManifestError{
1718+
std::format("malformed runtime segment near key '{}'", sub),
1719+
m.sourcePath, 0, 0});
1720+
}
1721+
rc.skip_ws_and_comments();
1722+
auto read_string_list = [&](std::vector<std::string>& out)
1723+
-> std::expected<void, ManifestError>
1724+
{
1725+
if (!rc.consume('{')) {
1726+
return std::unexpected(ManifestError{
1727+
std::format("expected '{{' after `runtime.{} =`", sub),
1728+
m.sourcePath, 0, 0});
1729+
}
1730+
rc.skip_ws_and_comments();
1731+
while (!rc.eof() && rc.peek() != '}') {
1732+
auto s = rc.read_string();
1733+
if (!s.empty()) out.push_back(std::move(s));
1734+
rc.skip_ws_and_comments();
1735+
}
1736+
rc.consume('}');
1737+
return {};
1738+
};
1739+
if (sub == "library_dirs") {
1740+
std::vector<std::string> dirs;
1741+
if (auto r = read_string_list(dirs); !r) return std::unexpected(r.error());
1742+
for (auto& d : dirs) m.runtimeConfig.libraryDirs.emplace_back(std::move(d));
1743+
} else if (sub == "dlopen_libs") {
1744+
if (auto r = read_string_list(m.runtimeConfig.dlopenLibs); !r)
1745+
return std::unexpected(r.error());
1746+
} else if (sub == "capabilities") {
1747+
if (auto r = read_string_list(m.runtimeConfig.capabilities); !r)
1748+
return std::unexpected(r.error());
1749+
} else {
1750+
rc.skip_ws_and_comments();
1751+
if (rc.peek() == '"' || rc.peek() == '\'') (void)rc.read_string();
1752+
else if (rc.peek() == '{') rc.skip_table();
1753+
else (void)rc.read_bareword();
1754+
}
1755+
rc.skip_ws_and_comments();
1756+
}
1757+
}
16861758
else {
16871759
// Unknown key — skip the value (string / bareword / table).
16881760
cur.skip_ws_and_comments();
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env bash
2+
# requires: gcc unix-shell
3+
# Runtime library directories declared in mcpp.toml must be visible to
4+
# libraries loaded only through dlopen(), not just DT_NEEDED link deps.
5+
set -e
6+
7+
TMP=$(mktemp -d)
8+
trap "rm -rf $TMP" EXIT
9+
10+
cd "$TMP"
11+
mkdir -p app/src app/tests app/runtime
12+
13+
cat > app/runtime/plugin.c <<'EOF'
14+
int runtime_plugin_answer(void) {
15+
return 42;
16+
}
17+
EOF
18+
19+
gcc -shared -fPIC app/runtime/plugin.c -o app/runtime/libruntime_plugin.so
20+
21+
cat > app/src/main.cpp <<'EOF'
22+
#include <dlfcn.h>
23+
24+
using answer_fn = int (*)();
25+
26+
int main() {
27+
void* handle = dlopen("libruntime_plugin.so", RTLD_NOW);
28+
if (!handle) {
29+
return 10;
30+
}
31+
auto answer = reinterpret_cast<answer_fn>(dlsym(handle, "runtime_plugin_answer"));
32+
if (!answer) {
33+
dlclose(handle);
34+
return 11;
35+
}
36+
int result = answer();
37+
dlclose(handle);
38+
return result == 42 ? 0 : 12;
39+
}
40+
EOF
41+
42+
cat > app/tests/test_runtime_plugin.cpp <<'EOF'
43+
#include <dlfcn.h>
44+
45+
using answer_fn = int (*)();
46+
47+
int main() {
48+
void* handle = dlopen("libruntime_plugin.so", RTLD_NOW);
49+
if (!handle) {
50+
return 20;
51+
}
52+
auto answer = reinterpret_cast<answer_fn>(dlsym(handle, "runtime_plugin_answer"));
53+
if (!answer) {
54+
dlclose(handle);
55+
return 21;
56+
}
57+
int result = answer();
58+
dlclose(handle);
59+
return result == 42 ? 0 : 22;
60+
}
61+
EOF
62+
63+
cat > app/mcpp.toml <<'EOF'
64+
[package]
65+
name = "app"
66+
version = "0.1.0"
67+
68+
[build]
69+
sources = ["src/*.cpp"]
70+
ldflags = ["-ldl"]
71+
72+
[runtime]
73+
library_dirs = ["runtime"]
74+
75+
[targets.app]
76+
kind = "bin"
77+
main = "src/main.cpp"
78+
EOF
79+
80+
cd app
81+
"$MCPP" build > build.log 2>&1 || {
82+
cat build.log
83+
echo "build failed"
84+
exit 1
85+
}
86+
87+
"$MCPP" run > run.log 2>&1 || {
88+
cat run.log
89+
echo "run failed"
90+
exit 1
91+
}
92+
93+
"$MCPP" test > test.log 2>&1 || {
94+
cat test.log
95+
echo "test failed"
96+
exit 1
97+
}
98+
99+
echo "OK"

tests/unit/test_manifest.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,29 @@ kind = "lib"
261261
EXPECT_EQ(m->buildConfig.cStandard, "c11");
262262
}
263263

264+
TEST(Manifest, RuntimeConfig) {
265+
constexpr auto src = R"(
266+
[package]
267+
name = "x"
268+
version = "0.1.0"
269+
[runtime]
270+
library_dirs = ["runtime/lib", "plugins"]
271+
dlopen_libs = ["libGLX.so.0", "libGL.so.1"]
272+
capabilities = ["x11.display", "opengl.glx.driver"]
273+
)";
274+
auto m = mcpp::manifest::parse_string(src);
275+
ASSERT_TRUE(m.has_value()) << m.error().format();
276+
ASSERT_EQ(m->runtimeConfig.libraryDirs.size(), 2u);
277+
EXPECT_EQ(m->runtimeConfig.libraryDirs[0], "runtime/lib");
278+
EXPECT_EQ(m->runtimeConfig.libraryDirs[1], "plugins");
279+
ASSERT_EQ(m->runtimeConfig.dlopenLibs.size(), 2u);
280+
EXPECT_EQ(m->runtimeConfig.dlopenLibs[0], "libGLX.so.0");
281+
EXPECT_EQ(m->runtimeConfig.dlopenLibs[1], "libGL.so.1");
282+
ASSERT_EQ(m->runtimeConfig.capabilities.size(), 2u);
283+
EXPECT_EQ(m->runtimeConfig.capabilities[0], "x11.display");
284+
EXPECT_EQ(m->runtimeConfig.capabilities[1], "opengl.glx.driver");
285+
}
286+
264287
TEST(SynthesizeFromXpkgLua, CflagsCxxflagsLdflagsAndCStandard) {
265288
constexpr auto src = R"(
266289
package = {
@@ -291,6 +314,35 @@ package = {
291314
EXPECT_EQ(m->modules.sources[0], "*/src/*.c");
292315
}
293316

317+
TEST(SynthesizeFromXpkgLua, RuntimeConfig) {
318+
constexpr auto src = R"(
319+
package = {
320+
spec = "1",
321+
name = "glfw",
322+
xpm = { linux = { ["3.4"] = { url = "u", sha256 = "h" } } },
323+
mcpp = {
324+
sources = { "*/src/context.c" },
325+
runtime = {
326+
library_dirs = { "mcpp_generated/runtime/lib" },
327+
dlopen_libs = { "libGLX.so.0", "libGL.so.1" },
328+
capabilities = { "x11.display", "opengl.glx.driver" },
329+
},
330+
targets = { ["glfw"] = { kind = "lib" } },
331+
},
332+
}
333+
)";
334+
auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "glfw", "3.4");
335+
ASSERT_TRUE(m.has_value()) << m.error().format();
336+
ASSERT_EQ(m->runtimeConfig.libraryDirs.size(), 1u);
337+
EXPECT_EQ(m->runtimeConfig.libraryDirs[0], "mcpp_generated/runtime/lib");
338+
ASSERT_EQ(m->runtimeConfig.dlopenLibs.size(), 2u);
339+
EXPECT_EQ(m->runtimeConfig.dlopenLibs[0], "libGLX.so.0");
340+
EXPECT_EQ(m->runtimeConfig.dlopenLibs[1], "libGL.so.1");
341+
ASSERT_EQ(m->runtimeConfig.capabilities.size(), 2u);
342+
EXPECT_EQ(m->runtimeConfig.capabilities[0], "x11.display");
343+
EXPECT_EQ(m->runtimeConfig.capabilities[1], "opengl.glx.driver");
344+
}
345+
294346
TEST(SynthesizeFromXpkgLua, AppliesCurrentPlatformMcppOverlay) {
295347
constexpr auto src = R"(
296348
package = {

0 commit comments

Comments
 (0)