Skip to content

Commit 0cd8b59

Browse files
authored
Add runtime library dirs for run and test
Add manifest runtime metadata and inject package runtime library paths for run and test.
1 parent 4ae6dbe commit 0cd8b59

6 files changed

Lines changed: 380 additions & 0 deletions

File tree

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# mcpp: GL Runtime Closure Plan
2+
3+
> 状态: active
4+
> 分支: `codex/gl-runtime-closure-mcpp`
5+
> PR: pending
6+
> Last updated: 2026-06-03
7+
> 目标: 让 mcpp 以标准工具链方式表达、解析、诊断并注入运行时闭包,使 GLFW/OpenGL 这类通过 `dlopen` 加载的运行库不再依赖用户手写环境变量。
8+
9+
## Scope
10+
11+
This repository owns the tool behavior. It should not hard-code one OpenGL
12+
vendor or one package index workaround. The expected model is closer to
13+
Conan/vcpkg run environments plus Nix-style runtime closure diagnostics:
14+
15+
- package metadata can declare runtime library directories, `dlopen` library
16+
names, and required system capabilities;
17+
- dependency resolution carries runtime requirements separately from compile
18+
includes and link flags;
19+
- `mcpp run`, `mcpp test`, `mcpp doctor`, and `mcpp pack` consume the same
20+
runtime model;
21+
- missing system capabilities produce actionable errors before a user only sees
22+
a failed GUI window.
23+
24+
## Current Problem
25+
26+
- `mcpp run` builds the selected binary and executes it directly.
27+
- Build/link propagation covers link-time shared libraries, but `dlopen`
28+
libraries such as `libGLX.so.0` and `libGL.so.1` do not appear in `DT_NEEDED`.
29+
- `mcpp pack` already has runtime closure logic, but it is oriented around
30+
loader-visible ELF dependencies. GLX/EGL/Mesa/vendor-driver cases need an
31+
explicit runtime metadata path rather than guessing from one executable.
32+
33+
## Proposed Manifest Surface
34+
35+
Keep compile, link, and runtime requirements separate. Initial names can be
36+
adjusted during implementation, but the semantics should remain stable:
37+
38+
```toml
39+
[runtime]
40+
library_dirs = ["relative/or/generated/runtime/lib"]
41+
dlopen_libs = ["libGLX.so.0", "libGL.so.1", "libGL.so"]
42+
capabilities = ["x11.display", "opengl.glx.driver"]
43+
```
44+
45+
For package descriptors coming from an index, the same data should be accepted
46+
from the package `mcpp` table:
47+
48+
```lua
49+
mcpp = {
50+
runtime = {
51+
library_dirs = {"mcpp_generated/runtime/lib"},
52+
dlopen_libs = {"libGLX.so.0", "libGL.so.1", "libGL.so"},
53+
capabilities = {"x11.display", "opengl.glx.driver"},
54+
},
55+
}
56+
```
57+
58+
Compatibility rule: packages that do not declare runtime metadata keep current
59+
behavior.
60+
61+
## Implementation Plan
62+
63+
- [x] Create this repository-level plan checkpoint.
64+
- [x] Add manifest/runtime metadata parsing and validation.
65+
- Candidate files: `src/manifest.cppm`, manifest tests.
66+
- Invalid entries should fail early: empty library name, absolute path in
67+
package metadata unless explicitly allowed, duplicate capability strings.
68+
- [x] Carry runtime requirements through the resolved package graph.
69+
- Candidate files: dependency resolution and `PackageRoot`/graph structures.
70+
- Runtime requirements must not be mixed into public include usage.
71+
- [x] Teach `mcpp run` and `mcpp test` to build a run environment.
72+
- 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.
75+
- Linux: prepend resolved runtime directories to `LD_LIBRARY_PATH`.
76+
- macOS: use `DYLD_LIBRARY_PATH` only for local tool execution where allowed,
77+
otherwise prefer rpath/install-name behavior.
78+
- Windows: prepend resolved runtime directories to `PATH`.
79+
- [ ] Add runtime diagnostics.
80+
- Candidate commands: `mcpp self doctor`, or a new target-aware runtime
81+
doctor path if the existing command shape supports it.
82+
- Diagnostics should list the target, the package that required the runtime
83+
item, unresolved `dlopen` names, and missing capabilities.
84+
- [ ] Extend `mcpp pack` to consume runtime metadata.
85+
- Candidate file: `src/pack/pack.cppm`.
86+
- `pack` should include declared runtime directories/files when the mode
87+
requests a runnable bundle.
88+
- Keep system capabilities explicit; do not silently bundle host GPU drivers
89+
unless a package declares a redistributable runtime.
90+
- [x] Add regression coverage with a small `dlopen` fixture.
91+
- Test should prove that a library loaded only via `dlopen` is found through
92+
mcpp runtime metadata during `mcpp run`.
93+
- A second pack-oriented test should prove runtime metadata is represented in
94+
the bundled executable environment.
95+
- [ ] Update docs.
96+
- Candidate files: `docs/02-pack-and-release.md`,
97+
`docs/05-mcpp-toml.md`, README snippets if needed.
98+
99+
## Verification
100+
101+
- [x] `mcpp build`
102+
- [x] `mcpp run -- --version`
103+
- [x] `mcpp test`
104+
- [ ] `MCPP=<built-mcpp> bash tests/e2e/run_all.sh`
105+
- [x] Focused runtime metadata e2e for `dlopen` resolution
106+
- [ ] Focused pack e2e for runtime metadata inclusion
107+
108+
## PR / CI / Merge Notes
109+
110+
- [x] Commit this plan as the first checkpoint.
111+
- [ ] Open a PR with sanitized paths and no local machine details.
112+
- [ ] Include a test plan in the PR body.
113+
- [ ] Wait for Linux/macOS/Windows CI.
114+
- [ ] Squash merge after required checks pass.
115+
116+
## Cross-Repository Dependencies
117+
118+
- `mcpp-index` can only fully validate `compat.glfw` GLX runtime metadata after
119+
this repository supports runtime requirements in `mcpp run`.
120+
- `imgui-m` should not own tool runtime behavior; it only consumes the fixed
121+
behavior through its minimal window example.
122+
- `xim-pkgindex` participates only after a released mcpp version is needed by
123+
xlings or users.

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"

0 commit comments

Comments
 (0)