Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [0.0.46] — 2026-06-03

### 新增

- 共享库 target 支持声明 `soname`,Linux 构建会传递 `-Wl,-soname,...`,
并在运行产物目录生成 ABI 名称 alias,供下游 `DT_NEEDED` / `dlopen()`
以标准 SONAME 加载。

### 修复

- `mcpp run` / `mcpp test` 会把工具链 runtime 目录加入进程库搜索环境。
这修复了 GLX/OpenGL driver 这类经由 `dlopen()` 加载的库无法找到自身
`DT_NEEDED` 闭包的问题。

## [0.0.45] — 2026-06-02

### 修复
Expand Down
7 changes: 7 additions & 0 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,15 @@ kind = "lib"
# 共享库
[targets.mylib]
kind = "shared"
soname = "libmylib.so.1" # 可选: ELF/Mach-O ABI 名称,运行时会生成同名 alias
```

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

### 2.3 `[build]` — 构建配置

```toml
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "0.0.45"
version = "0.0.46"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
35 changes: 34 additions & 1 deletion src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ std::string join_flags(const std::vector<std::string>& flags) {
return out;
}

std::string shared_soname_flag(const LinkUnit& lu) {
if (lu.kind != LinkUnit::SharedLibrary || lu.soname.empty()) return "";
#if defined(__APPLE__)
return "-Wl,-install_name,@rpath/" + lu.soname;
#elif defined(__linux__)
return "-Wl,-soname," + lu.soname;
#else
return "";
#endif
}

void write_file(const std::filesystem::path& p, std::string_view content) {
std::filesystem::create_directories(p.parent_path());
std::ofstream os(p);
Expand Down Expand Up @@ -369,9 +380,17 @@ std::string emit_ninja_string(const BuildPlan& plan) {
append(" description = AR $out\n\n");

append("rule cxx_shared\n");
append(" command = $cxx -shared $in -o $out $ldflags $unit_ldflags\n");
append(" command = $cxx -shared $in -o $out $ldflags $soname_flag $unit_ldflags\n");
append(" description = SHARED $out\n\n");

append("rule runtime_alias\n");
if constexpr (mcpp::platform::is_windows) {
append(" command = powershell -NoProfile -Command \"Copy-Item -Force '$in' -Destination '$out'\"\n");
} else {
append(" command = mkdir -p $$(dirname $out) && rm -f $out && ln -s $$(basename $in) $out\n");
}
append(" description = ALIAS $out\n\n");

if (dyndep) {
// Scan rule: produce P1689 .ddi for one TU.
// GCC: built-in -fdeps-format=p1689r5 flags during preprocessing.
Expand Down Expand Up @@ -618,16 +637,27 @@ std::string emit_ninja_string(const BuildPlan& plan) {
std::string out_line = std::format("build {} : {}{}{}\n",
escape_ninja_path(lu.output), rule, ins,
implicit.empty() ? std::string{} : " |" + implicit);
if (auto flag = shared_soname_flag(lu); !flag.empty())
out_line += " soname_flag = " + flag + "\n";
if (auto flags = join_flags(lu.linkFlags); !flags.empty())
out_line += " unit_ldflags =" + flags + "\n";
append(std::move(out_line));

for (auto const& alias : lu.runtimeAliases) {
append(std::format("build {} : runtime_alias {}\n",
escape_ninja_path(alias),
escape_ninja_path(lu.output)));
}
}
append("\n");

if (!plan.linkUnits.empty()) {
std::string defaults;
for (auto& lu : plan.linkUnits) {
defaults += " " + escape_ninja_path(lu.output);
for (auto const& alias : lu.runtimeAliases) {
defaults += " " + escape_ninja_path(alias);
}
}
append("default" + defaults + "\n");
}
Expand Down Expand Up @@ -720,6 +750,9 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
std::fputs(out.c_str(), stdout);
for (auto& lu : plan.linkUnits) {
r.producedArtifacts.push_back(plan.outputDir / lu.output);
for (auto const& alias : lu.runtimeAliases) {
r.producedArtifacts.push_back(plan.outputDir / alias);
}
}
} else {
auto prefixes = command_prefixes(flags, plan);
Expand Down
30 changes: 30 additions & 0 deletions src/build/plan.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ struct LinkUnit {
std::vector<std::filesystem::path> implicitInputs; // relative to plan.outputDir
std::vector<std::string> linkFlags; // per-link edge flags
std::filesystem::path output; // relative to plan.outputDir
std::string soname; // ABI name for shared libraries
std::vector<std::filesystem::path> runtimeAliases; // relative aliases, e.g. bin/libfoo.so.1
std::optional<std::filesystem::path> entryMain; // src path of main.cpp for bin
};

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

std::vector<std::filesystem::path> runtime_aliases_for_target(
const mcpp::manifest::Target& t) {
std::vector<std::filesystem::path> aliases;
if (t.kind != mcpp::manifest::Target::SharedLibrary || t.soname.empty()) {
return aliases;
}

auto output = target_output(t);
if (t.soname != output.filename().string()) {
aliases.push_back(output.parent_path() / t.soname);
}
return aliases;
}

bool is_implementation_source(const std::filesystem::path& source) {
auto ext = source.extension();
return ext == ".cpp" || ext == ".cc" || ext == ".cxx" || ext == ".c" || ext == ".m";
Expand Down Expand Up @@ -207,6 +223,16 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
dir.is_absolute() ? dir : package.root / dir);
}
}
// The same private runtime directories embedded as executable RUNPATH are
// also needed in the process environment for libraries reached only via
// dlopen(), because their own DT_NEEDED closure does not consult the main
// executable's RUNPATH.
for (auto const& dir : tc.linkRuntimeDirs) {
append_unique_path(plan.runtimeLibraryDirs, dir);
}
if (tc.payloadPaths) {
append_unique_path(plan.runtimeLibraryDirs, tc.payloadPaths->glibcLib);
}

// 1a. Detect basename collisions (both cross-package AND intra-package:
// ftxui ships dom/color.cpp + screen/color.cpp, for instance).
Expand Down Expand Up @@ -375,6 +401,8 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
lu.targetName = dep.target.name;
lu.kind = LinkUnit::SharedLibrary;
lu.output = dep.output;
lu.soname = dep.target.soname;
lu.runtimeAliases = runtime_aliases_for_target(dep.target);
append_package_objects(lu, dep.packageName);
append_direct_shared_deps(lu, dep.packageIndex);
plan.linkUnits.push_back(std::move(lu));
Expand All @@ -399,6 +427,8 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
} else if (t.kind == mcpp::manifest::Target::SharedLibrary) {
lu.kind = LinkUnit::SharedLibrary;
lu.output = target_output(t);
lu.soname = t.soname;
lu.runtimeAliases = runtime_aliases_for_target(t);
} else if (t.kind == mcpp::manifest::Target::TestBinary) {
lu.kind = LinkUnit::TestBinary;
lu.output = target_output(t);
Expand Down
35 changes: 35 additions & 0 deletions src/manifest.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ struct Target {
std::string name;
enum Kind { Library, Binary, SharedLibrary, TestBinary } kind;
std::string main; // for binary / test
std::string soname; // ABI name for shared libraries, e.g. libfoo.so.1
};

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

bool is_basename(std::string_view value) {
return !value.empty()
&& value.find('/') == std::string_view::npos
&& value.find('\\') == std::string_view::npos;
}

std::optional<std::string> validate_target_soname(const Target& t,
std::string_view targetPath) {
if (t.soname.empty()) return std::nullopt;
if (t.kind != Target::SharedLibrary) {
return std::format("{}soname is only valid for shared targets", targetPath);
}
if (!is_basename(t.soname)) {
return std::format("{}soname must be a library basename, got '{}'",
targetPath, t.soname);
}
return std::nullopt;
}

} // namespace

std::expected<CppStandardConfig, std::string> normalize_cpp_standard(std::string_view raw) {
Expand Down Expand Up @@ -481,6 +501,16 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
}
t.main = mit->second.as_string();
}
if (auto sit = tt.find("soname"); sit != tt.end()) {
if (!sit->second.is_string()) {
return std::unexpected(error(origin,
std::format("targets.{}.soname must be a string", tname)));
}
t.soname = sit->second.as_string();
}
if (auto msg = validate_target_soname(t, std::format("targets.{}.", tname))) {
return std::unexpected(error(origin, *msg));
}
m.targets.push_back(std::move(t));
}
} // close `if (targets_table && !targets_table->empty())`
Expand Down Expand Up @@ -1620,6 +1650,8 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
|| k == "so" || k == "shlib") t.kind = Target::SharedLibrary;
} else if (sub == "main") {
t.main = cur.read_string();
} else if (sub == "soname") {
t.soname = cur.read_string();
} else {
// unknown subfield — skip its value
cur.skip_ws_and_comments();
Expand All @@ -1629,6 +1661,9 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
cur.skip_ws_and_comments();
}
cur.consume('}');
if (auto msg = validate_target_soname(t, std::format("targets.{}.", tname))) {
return std::unexpected(ManifestError{*msg, m.sourcePath, 0, 0});
}
m.targets.push_back(std::move(t));
cur.skip_ws_and_comments();
}
Expand Down
11 changes: 11 additions & 0 deletions src/platform/env.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ private:

std::string path_list_separator();
std::string runtime_library_path_key();
std::string host_tool_runtime_library_path_key();
std::string prepend_path_list(std::string_view key,
std::span<const std::filesystem::path> dirs);

Expand Down Expand Up @@ -123,6 +124,16 @@ std::string runtime_library_path_key() {
#endif
}

std::string host_tool_runtime_library_path_key() {
#if defined(__APPLE__)
return "DYLD_LIBRARY_PATH";
#elif defined(__linux__)
return "LD_LIBRARY_PATH";
#else
return "";
#endif
}

std::string prepend_path_list(std::string_view key,
std::span<const std::filesystem::path> dirs) {
if (key.empty() || dirs.empty()) return "";
Expand Down
37 changes: 32 additions & 5 deletions src/platform/linux.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export namespace mcpp::platform::linux_ {
std::string build_ld_library_path_prefix(
const std::vector<std::filesystem::path>& dirs);

// Build an LD_LIBRARY_PATH shell prefix for toolchain host processes.
// Unlike build_ld_library_path_prefix(), this does not append inherited
// LD_LIBRARY_PATH, which may contain target-program runtime directories.
std::string build_clean_ld_library_path_prefix(
const std::vector<std::filesystem::path>& dirs);

// Return Linux toolchain runtime library directories.
std::vector<std::filesystem::path>
runtime_lib_dirs(const std::filesystem::path& toolchain_root);
Expand All @@ -29,16 +35,24 @@ runtime_lib_dirs(const std::filesystem::path& toolchain_root);

namespace mcpp::platform::linux_ {

std::string build_ld_library_path_prefix(
const std::vector<std::filesystem::path>& dirs)
{
#if defined(__linux__)
if (dirs.empty()) return "";
namespace {

std::string join_dirs(const std::vector<std::filesystem::path>& dirs) {
std::string joined;
for (auto& d : dirs) {
if (!joined.empty()) joined += ':';
joined += d.string();
}
return joined;
}

} // namespace

std::string build_ld_library_path_prefix(
const std::vector<std::filesystem::path>& dirs) {
#if defined(__linux__)
if (dirs.empty()) return "";
auto joined = join_dirs(dirs);
return std::format("env LD_LIBRARY_PATH={}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}} ",
mcpp::platform::shell::quote(joined));
#else
Expand All @@ -47,6 +61,19 @@ std::string build_ld_library_path_prefix(
#endif
}

std::string build_clean_ld_library_path_prefix(
const std::vector<std::filesystem::path>& dirs) {
#if defined(__linux__)
if (dirs.empty()) return "";
auto joined = join_dirs(dirs);
return std::format("env LD_LIBRARY_PATH={} ",
mcpp::platform::shell::quote(joined));
#else
(void)dirs;
return "";
#endif
}

std::vector<std::filesystem::path>
runtime_lib_dirs(const std::filesystem::path& toolchain_root) {
std::vector<std::filesystem::path> dirs;
Expand Down
14 changes: 14 additions & 0 deletions src/platform/process.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module;
export module mcpp.platform.process;

import std;
import mcpp.platform.env;

export namespace mcpp::platform::process {

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

// Run a host tool while clearing target runtime library search variables.
// This prevents target/program LD_LIBRARY_PATH from poisoning system tools
// such as sha256sum, compiler probes, env, or the shell itself.
RunResult capture_host_tool(std::string_view command);

// Run `command` with extra environment variables (additive).
// Windows: _putenv_s (mutates calling process env).
// POSIX: prefixes command with VAR=val tokens (no mutation).
Expand Down Expand Up @@ -126,6 +132,14 @@ RunResult capture(std::string_view command) {
return result;
}

RunResult capture_host_tool(std::string_view command) {
auto key = mcpp::platform::env::host_tool_runtime_library_path_key();
std::optional<mcpp::platform::env::ScopedEnv> runtime_env;
if (!key.empty())
runtime_env.emplace(key, std::nullopt);
return capture(command);
}

RunResult capture_with_env(
std::string_view command,
const std::vector<std::pair<std::string, std::string>>& env)
Expand Down
2 changes: 1 addition & 1 deletion src/pm/publisher.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ std::string sha256_of_file(const std::filesystem::path& file) {
if (!std::filesystem::exists(file)) return {};
auto cmd = std::format("sha256sum {} 2>/dev/null",
mcpp::platform::shell::quote(file.string()));
auto r = mcpp::platform::process::capture(cmd);
auto r = mcpp::platform::process::capture_host_tool(cmd);
if (r.exit_code != 0) return {};
// sha256sum format: "<64-hex> <filename>\n"
auto sp = r.output.find(' ');
Expand Down
2 changes: 1 addition & 1 deletion src/toolchain/fingerprint.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import mcpp.toolchain.detect;

export namespace mcpp::toolchain {

inline constexpr std::string_view MCPP_VERSION = "0.0.45";
inline constexpr std::string_view MCPP_VERSION = "0.0.46";

struct FingerprintInputs {
Toolchain toolchain;
Expand Down
Loading
Loading