Skip to content

Commit 93398f4

Browse files
authored
feat: support shared soname runtime aliases
Add shared library soname runtime aliases and isolate host tool subprocesses from target runtime library paths.
1 parent cf2b02b commit 93398f4

17 files changed

Lines changed: 452 additions & 11 deletions

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [0.0.46] — 2026-06-03
7+
8+
### 新增
9+
10+
- 共享库 target 支持声明 `soname`,Linux 构建会传递 `-Wl,-soname,...`,
11+
并在运行产物目录生成 ABI 名称 alias,供下游 `DT_NEEDED` / `dlopen()`
12+
以标准 SONAME 加载。
13+
14+
### 修复
15+
16+
- `mcpp run` / `mcpp test` 会把工具链 runtime 目录加入进程库搜索环境。
17+
这修复了 GLX/OpenGL driver 这类经由 `dlopen()` 加载的库无法找到自身
18+
`DT_NEEDED` 闭包的问题。
19+
620
## [0.0.45] — 2026-06-02
721

822
### 修复

docs/05-mcpp-toml.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,15 @@ kind = "lib"
7171
# 共享库
7272
[targets.mylib]
7373
kind = "shared"
74+
soname = "libmylib.so.1" # 可选: ELF/Mach-O ABI 名称,运行时会生成同名 alias
7475
```
7576

77+
`soname` 用于共享库的 ABI 名称,类似 Autotools/CMake 中的
78+
`SOVERSION`/`SONAME`。在 Linux 上,mcpp 会向链接器传递
79+
`-Wl,-soname,<name>`,并在输出目录生成 `<name> -> lib<target>.so` alias,
80+
让下游程序可通过标准 ABI 名称 `DT_NEEDED``dlopen()` 加载该库。
81+
该字段只对 `kind = "shared"` 有效,值必须是文件名 basename。
82+
7683
### 2.3 `[build]` — 构建配置
7784

7885
```toml

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.45"
3+
version = "0.0.46"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/ninja_backend.cppm

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ std::string join_flags(const std::vector<std::string>& flags) {
102102
return out;
103103
}
104104

105+
std::string shared_soname_flag(const LinkUnit& lu) {
106+
if (lu.kind != LinkUnit::SharedLibrary || lu.soname.empty()) return "";
107+
#if defined(__APPLE__)
108+
return "-Wl,-install_name,@rpath/" + lu.soname;
109+
#elif defined(__linux__)
110+
return "-Wl,-soname," + lu.soname;
111+
#else
112+
return "";
113+
#endif
114+
}
115+
105116
void write_file(const std::filesystem::path& p, std::string_view content) {
106117
std::filesystem::create_directories(p.parent_path());
107118
std::ofstream os(p);
@@ -369,9 +380,17 @@ std::string emit_ninja_string(const BuildPlan& plan) {
369380
append(" description = AR $out\n\n");
370381

371382
append("rule cxx_shared\n");
372-
append(" command = $cxx -shared $in -o $out $ldflags $unit_ldflags\n");
383+
append(" command = $cxx -shared $in -o $out $ldflags $soname_flag $unit_ldflags\n");
373384
append(" description = SHARED $out\n\n");
374385

386+
append("rule runtime_alias\n");
387+
if constexpr (mcpp::platform::is_windows) {
388+
append(" command = powershell -NoProfile -Command \"Copy-Item -Force '$in' -Destination '$out'\"\n");
389+
} else {
390+
append(" command = mkdir -p $$(dirname $out) && rm -f $out && ln -s $$(basename $in) $out\n");
391+
}
392+
append(" description = ALIAS $out\n\n");
393+
375394
if (dyndep) {
376395
// Scan rule: produce P1689 .ddi for one TU.
377396
// GCC: built-in -fdeps-format=p1689r5 flags during preprocessing.
@@ -618,16 +637,27 @@ std::string emit_ninja_string(const BuildPlan& plan) {
618637
std::string out_line = std::format("build {} : {}{}{}\n",
619638
escape_ninja_path(lu.output), rule, ins,
620639
implicit.empty() ? std::string{} : " |" + implicit);
640+
if (auto flag = shared_soname_flag(lu); !flag.empty())
641+
out_line += " soname_flag = " + flag + "\n";
621642
if (auto flags = join_flags(lu.linkFlags); !flags.empty())
622643
out_line += " unit_ldflags =" + flags + "\n";
623644
append(std::move(out_line));
645+
646+
for (auto const& alias : lu.runtimeAliases) {
647+
append(std::format("build {} : runtime_alias {}\n",
648+
escape_ninja_path(alias),
649+
escape_ninja_path(lu.output)));
650+
}
624651
}
625652
append("\n");
626653

627654
if (!plan.linkUnits.empty()) {
628655
std::string defaults;
629656
for (auto& lu : plan.linkUnits) {
630657
defaults += " " + escape_ninja_path(lu.output);
658+
for (auto const& alias : lu.runtimeAliases) {
659+
defaults += " " + escape_ninja_path(alias);
660+
}
631661
}
632662
append("default" + defaults + "\n");
633663
}
@@ -720,6 +750,9 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
720750
std::fputs(out.c_str(), stdout);
721751
for (auto& lu : plan.linkUnits) {
722752
r.producedArtifacts.push_back(plan.outputDir / lu.output);
753+
for (auto const& alias : lu.runtimeAliases) {
754+
r.producedArtifacts.push_back(plan.outputDir / alias);
755+
}
723756
}
724757
} else {
725758
auto prefixes = command_prefixes(flags, plan);

src/build/plan.cppm

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ struct LinkUnit {
3333
std::vector<std::filesystem::path> implicitInputs; // relative to plan.outputDir
3434
std::vector<std::string> linkFlags; // per-link edge flags
3535
std::filesystem::path output; // relative to plan.outputDir
36+
std::string soname; // ABI name for shared libraries
37+
std::vector<std::filesystem::path> runtimeAliases; // relative aliases, e.g. bin/libfoo.so.1
3638
std::optional<std::filesystem::path> entryMain; // src path of main.cpp for bin
3739
};
3840

@@ -133,6 +135,20 @@ std::filesystem::path target_output(const mcpp::manifest::Target& t) {
133135
std::format("{}{}", t.name, mcpp::platform::exe_suffix);
134136
}
135137

138+
std::vector<std::filesystem::path> runtime_aliases_for_target(
139+
const mcpp::manifest::Target& t) {
140+
std::vector<std::filesystem::path> aliases;
141+
if (t.kind != mcpp::manifest::Target::SharedLibrary || t.soname.empty()) {
142+
return aliases;
143+
}
144+
145+
auto output = target_output(t);
146+
if (t.soname != output.filename().string()) {
147+
aliases.push_back(output.parent_path() / t.soname);
148+
}
149+
return aliases;
150+
}
151+
136152
bool is_implementation_source(const std::filesystem::path& source) {
137153
auto ext = source.extension();
138154
return ext == ".cpp" || ext == ".cc" || ext == ".cxx" || ext == ".c" || ext == ".m";
@@ -207,6 +223,16 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
207223
dir.is_absolute() ? dir : package.root / dir);
208224
}
209225
}
226+
// The same private runtime directories embedded as executable RUNPATH are
227+
// also needed in the process environment for libraries reached only via
228+
// dlopen(), because their own DT_NEEDED closure does not consult the main
229+
// executable's RUNPATH.
230+
for (auto const& dir : tc.linkRuntimeDirs) {
231+
append_unique_path(plan.runtimeLibraryDirs, dir);
232+
}
233+
if (tc.payloadPaths) {
234+
append_unique_path(plan.runtimeLibraryDirs, tc.payloadPaths->glibcLib);
235+
}
210236

211237
// 1a. Detect basename collisions (both cross-package AND intra-package:
212238
// ftxui ships dom/color.cpp + screen/color.cpp, for instance).
@@ -375,6 +401,8 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
375401
lu.targetName = dep.target.name;
376402
lu.kind = LinkUnit::SharedLibrary;
377403
lu.output = dep.output;
404+
lu.soname = dep.target.soname;
405+
lu.runtimeAliases = runtime_aliases_for_target(dep.target);
378406
append_package_objects(lu, dep.packageName);
379407
append_direct_shared_deps(lu, dep.packageIndex);
380408
plan.linkUnits.push_back(std::move(lu));
@@ -399,6 +427,8 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
399427
} else if (t.kind == mcpp::manifest::Target::SharedLibrary) {
400428
lu.kind = LinkUnit::SharedLibrary;
401429
lu.output = target_output(t);
430+
lu.soname = t.soname;
431+
lu.runtimeAliases = runtime_aliases_for_target(t);
402432
} else if (t.kind == mcpp::manifest::Target::TestBinary) {
403433
lu.kind = LinkUnit::TestBinary;
404434
lu.output = target_output(t);

src/manifest.cppm

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ struct Target {
5555
std::string name;
5656
enum Kind { Library, Binary, SharedLibrary, TestBinary } kind;
5757
std::string main; // for binary / test
58+
std::string soname; // ABI name for shared libraries, e.g. libfoo.so.1
5859
};
5960

6061
// `DependencySpec` and `kDefaultNamespace` have moved to mcpp.pm.dep_spec.
@@ -304,6 +305,25 @@ ManifestError error(const std::filesystem::path& origin,
304305
return ManifestError{msg, origin, pos.line, pos.column};
305306
}
306307

308+
bool is_basename(std::string_view value) {
309+
return !value.empty()
310+
&& value.find('/') == std::string_view::npos
311+
&& value.find('\\') == std::string_view::npos;
312+
}
313+
314+
std::optional<std::string> validate_target_soname(const Target& t,
315+
std::string_view targetPath) {
316+
if (t.soname.empty()) return std::nullopt;
317+
if (t.kind != Target::SharedLibrary) {
318+
return std::format("{}soname is only valid for shared targets", targetPath);
319+
}
320+
if (!is_basename(t.soname)) {
321+
return std::format("{}soname must be a library basename, got '{}'",
322+
targetPath, t.soname);
323+
}
324+
return std::nullopt;
325+
}
326+
307327
} // namespace
308328

309329
std::expected<CppStandardConfig, std::string> normalize_cpp_standard(std::string_view raw) {
@@ -481,6 +501,16 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
481501
}
482502
t.main = mit->second.as_string();
483503
}
504+
if (auto sit = tt.find("soname"); sit != tt.end()) {
505+
if (!sit->second.is_string()) {
506+
return std::unexpected(error(origin,
507+
std::format("targets.{}.soname must be a string", tname)));
508+
}
509+
t.soname = sit->second.as_string();
510+
}
511+
if (auto msg = validate_target_soname(t, std::format("targets.{}.", tname))) {
512+
return std::unexpected(error(origin, *msg));
513+
}
484514
m.targets.push_back(std::move(t));
485515
}
486516
} // close `if (targets_table && !targets_table->empty())`
@@ -1620,6 +1650,8 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
16201650
|| k == "so" || k == "shlib") t.kind = Target::SharedLibrary;
16211651
} else if (sub == "main") {
16221652
t.main = cur.read_string();
1653+
} else if (sub == "soname") {
1654+
t.soname = cur.read_string();
16231655
} else {
16241656
// unknown subfield — skip its value
16251657
cur.skip_ws_and_comments();
@@ -1629,6 +1661,9 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
16291661
cur.skip_ws_and_comments();
16301662
}
16311663
cur.consume('}');
1664+
if (auto msg = validate_target_soname(t, std::format("targets.{}.", tname))) {
1665+
return std::unexpected(ManifestError{*msg, m.sourcePath, 0, 0});
1666+
}
16321667
m.targets.push_back(std::move(t));
16331668
cur.skip_ws_and_comments();
16341669
}

src/platform/env.cppm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ private:
4040

4141
std::string path_list_separator();
4242
std::string runtime_library_path_key();
43+
std::string host_tool_runtime_library_path_key();
4344
std::string prepend_path_list(std::string_view key,
4445
std::span<const std::filesystem::path> dirs);
4546

@@ -123,6 +124,16 @@ std::string runtime_library_path_key() {
123124
#endif
124125
}
125126

127+
std::string host_tool_runtime_library_path_key() {
128+
#if defined(__APPLE__)
129+
return "DYLD_LIBRARY_PATH";
130+
#elif defined(__linux__)
131+
return "LD_LIBRARY_PATH";
132+
#else
133+
return "";
134+
#endif
135+
}
136+
126137
std::string prepend_path_list(std::string_view key,
127138
std::span<const std::filesystem::path> dirs) {
128139
if (key.empty() || dirs.empty()) return "";

src/platform/linux.cppm

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ export namespace mcpp::platform::linux_ {
1919
std::string build_ld_library_path_prefix(
2020
const std::vector<std::filesystem::path>& dirs);
2121

22+
// Build an LD_LIBRARY_PATH shell prefix for toolchain host processes.
23+
// Unlike build_ld_library_path_prefix(), this does not append inherited
24+
// LD_LIBRARY_PATH, which may contain target-program runtime directories.
25+
std::string build_clean_ld_library_path_prefix(
26+
const std::vector<std::filesystem::path>& dirs);
27+
2228
// Return Linux toolchain runtime library directories.
2329
std::vector<std::filesystem::path>
2430
runtime_lib_dirs(const std::filesystem::path& toolchain_root);
@@ -29,16 +35,24 @@ runtime_lib_dirs(const std::filesystem::path& toolchain_root);
2935

3036
namespace mcpp::platform::linux_ {
3137

32-
std::string build_ld_library_path_prefix(
33-
const std::vector<std::filesystem::path>& dirs)
34-
{
35-
#if defined(__linux__)
36-
if (dirs.empty()) return "";
38+
namespace {
39+
40+
std::string join_dirs(const std::vector<std::filesystem::path>& dirs) {
3741
std::string joined;
3842
for (auto& d : dirs) {
3943
if (!joined.empty()) joined += ':';
4044
joined += d.string();
4145
}
46+
return joined;
47+
}
48+
49+
} // namespace
50+
51+
std::string build_ld_library_path_prefix(
52+
const std::vector<std::filesystem::path>& dirs) {
53+
#if defined(__linux__)
54+
if (dirs.empty()) return "";
55+
auto joined = join_dirs(dirs);
4256
return std::format("env LD_LIBRARY_PATH={}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}} ",
4357
mcpp::platform::shell::quote(joined));
4458
#else
@@ -47,6 +61,19 @@ std::string build_ld_library_path_prefix(
4761
#endif
4862
}
4963

64+
std::string build_clean_ld_library_path_prefix(
65+
const std::vector<std::filesystem::path>& dirs) {
66+
#if defined(__linux__)
67+
if (dirs.empty()) return "";
68+
auto joined = join_dirs(dirs);
69+
return std::format("env LD_LIBRARY_PATH={} ",
70+
mcpp::platform::shell::quote(joined));
71+
#else
72+
(void)dirs;
73+
return "";
74+
#endif
75+
}
76+
5077
std::vector<std::filesystem::path>
5178
runtime_lib_dirs(const std::filesystem::path& toolchain_root) {
5279
std::vector<std::filesystem::path> dirs;

src/platform/process.cppm

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ module;
3030
export module mcpp.platform.process;
3131

3232
import std;
33+
import mcpp.platform.env;
3334

3435
export namespace mcpp::platform::process {
3536

@@ -42,6 +43,11 @@ struct RunResult {
4243
// On POSIX, stdin is automatically redirected from /dev/null.
4344
RunResult capture(std::string_view command);
4445

46+
// Run a host tool while clearing target runtime library search variables.
47+
// This prevents target/program LD_LIBRARY_PATH from poisoning system tools
48+
// such as sha256sum, compiler probes, env, or the shell itself.
49+
RunResult capture_host_tool(std::string_view command);
50+
4551
// Run `command` with extra environment variables (additive).
4652
// Windows: _putenv_s (mutates calling process env).
4753
// POSIX: prefixes command with VAR=val tokens (no mutation).
@@ -126,6 +132,14 @@ RunResult capture(std::string_view command) {
126132
return result;
127133
}
128134

135+
RunResult capture_host_tool(std::string_view command) {
136+
auto key = mcpp::platform::env::host_tool_runtime_library_path_key();
137+
std::optional<mcpp::platform::env::ScopedEnv> runtime_env;
138+
if (!key.empty())
139+
runtime_env.emplace(key, std::nullopt);
140+
return capture(command);
141+
}
142+
129143
RunResult capture_with_env(
130144
std::string_view command,
131145
const std::vector<std::pair<std::string, std::string>>& env)

src/pm/publisher.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ std::string sha256_of_file(const std::filesystem::path& file) {
206206
if (!std::filesystem::exists(file)) return {};
207207
auto cmd = std::format("sha256sum {} 2>/dev/null",
208208
mcpp::platform::shell::quote(file.string()));
209-
auto r = mcpp::platform::process::capture(cmd);
209+
auto r = mcpp::platform::process::capture_host_tool(cmd);
210210
if (r.exit_code != 0) return {};
211211
// sha256sum format: "<64-hex> <filename>\n"
212212
auto sp = r.output.find(' ');

0 commit comments

Comments
 (0)