Skip to content

Commit 4c9905f

Browse files
committed
feat: support shared soname runtime aliases
1 parent cf2b02b commit 4c9905f

10 files changed

Lines changed: 341 additions & 3 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/toolchain/fingerprint.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "0.0.45";
21+
inline constexpr std::string_view MCPP_VERSION = "0.0.46";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env bash
2+
# requires: elf
3+
# Shared libraries can declare an ABI SONAME. Consumers may load that ABI name
4+
# through dlopen(), and mcpp run must provide the runtime alias automatically.
5+
set -e
6+
7+
TMP=$(mktemp -d)
8+
trap "rm -rf $TMP" EXIT
9+
10+
cd "$TMP"
11+
mkdir -p depShared/src app/src
12+
13+
cat > depShared/src/dep.c <<'EOF'
14+
int dep_shared_answer(void) {
15+
return 42;
16+
}
17+
EOF
18+
19+
cat > depShared/mcpp.toml <<'EOF'
20+
[package]
21+
name = "depShared"
22+
version = "0.1.0"
23+
24+
[build]
25+
sources = ["src/*.c"]
26+
27+
[targets.depShared]
28+
kind = "shared"
29+
soname = "libdepShared.so.1"
30+
EOF
31+
32+
cat > app/src/main.cpp <<'EOF'
33+
#include <dlfcn.h>
34+
35+
using answer_fn = int (*)();
36+
37+
int main() {
38+
void* handle = dlopen("libdepShared.so.1", RTLD_NOW);
39+
if (!handle) {
40+
return 10;
41+
}
42+
auto answer = reinterpret_cast<answer_fn>(dlsym(handle, "dep_shared_answer"));
43+
if (!answer) {
44+
dlclose(handle);
45+
return 11;
46+
}
47+
int result = answer();
48+
dlclose(handle);
49+
return result == 42 ? 0 : 12;
50+
}
51+
EOF
52+
53+
cat > app/mcpp.toml <<'EOF'
54+
[package]
55+
name = "app"
56+
version = "0.1.0"
57+
58+
[build]
59+
sources = ["src/*.cpp"]
60+
ldflags = ["-ldl"]
61+
62+
[targets.app]
63+
kind = "bin"
64+
main = "src/main.cpp"
65+
66+
[dependencies.depShared]
67+
path = "../depShared"
68+
EOF
69+
70+
cd app
71+
"$MCPP" build > build.log 2>&1 || {
72+
cat build.log
73+
echo "build failed"
74+
exit 1
75+
}
76+
77+
so="$(find target -name 'libdepShared.so' | head -1)"
78+
alias="$(find target -name 'libdepShared.so.1' | head -1)"
79+
[[ -n "$so" && -n "$alias" ]] || {
80+
cat build.log
81+
find target -path '*/bin/*' -maxdepth 4 -type f -o -type l 2>/dev/null || true
82+
echo "expected shared library and ABI soname alias were not produced"
83+
exit 1
84+
}
85+
86+
readelf -d "$so" | grep -q 'Library soname: \[libdepShared.so.1\]' || {
87+
readelf -d "$so" || true
88+
echo "shared library missing requested SONAME"
89+
exit 1
90+
}
91+
92+
"$MCPP" run > run.log 2>&1 || {
93+
cat run.log
94+
echo "run failed"
95+
exit 1
96+
}
97+
98+
echo "OK"

0 commit comments

Comments
 (0)