From 4c9905f745160a73f9ca06fd2524ab7c2deac69d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 3 Jun 2026 07:03:08 +0800 Subject: [PATCH 1/2] feat: support shared soname runtime aliases --- CHANGELOG.md | 14 +++ docs/05-mcpp-toml.md | 7 ++ mcpp.toml | 2 +- src/build/ninja_backend.cppm | 35 ++++++- src/build/plan.cppm | 30 ++++++ src/manifest.cppm | 35 +++++++ src/toolchain/fingerprint.cppm | 2 +- tests/e2e/64_shared_soname_runtime_alias.sh | 98 +++++++++++++++++++ .../e2e/65_toolchain_runtime_dirs_for_run.sh | 68 +++++++++++++ tests/unit/test_manifest.cpp | 53 ++++++++++ 10 files changed, 341 insertions(+), 3 deletions(-) create mode 100755 tests/e2e/64_shared_soname_runtime_alias.sh create mode 100755 tests/e2e/65_toolchain_runtime_dirs_for_run.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index db5d03c..f5c34a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ### 修复 diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index a580992..a043cf2 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -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,`,并在输出目录生成 ` -> lib.so` alias, +让下游程序可通过标准 ABI 名称 `DT_NEEDED` 或 `dlopen()` 加载该库。 +该字段只对 `kind = "shared"` 有效,值必须是文件名 basename。 + ### 2.3 `[build]` — 构建配置 ```toml diff --git a/mcpp.toml b/mcpp.toml index 525e207..930ee80 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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"] diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 7934771..374ea9b 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -102,6 +102,17 @@ std::string join_flags(const std::vector& 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); @@ -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. @@ -618,9 +637,17 @@ 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"); @@ -628,6 +655,9 @@ std::string emit_ninja_string(const BuildPlan& plan) { 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"); } @@ -720,6 +750,9 @@ std::expected 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); diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 5d2adde..5d7ba04 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -33,6 +33,8 @@ struct LinkUnit { std::vector implicitInputs; // relative to plan.outputDir std::vector linkFlags; // per-link edge flags std::filesystem::path output; // relative to plan.outputDir + std::string soname; // ABI name for shared libraries + std::vector runtimeAliases; // relative aliases, e.g. bin/libfoo.so.1 std::optional entryMain; // src path of main.cpp for bin }; @@ -133,6 +135,20 @@ std::filesystem::path target_output(const mcpp::manifest::Target& t) { std::format("{}{}", t.name, mcpp::platform::exe_suffix); } +std::vector runtime_aliases_for_target( + const mcpp::manifest::Target& t) { + std::vector 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"; @@ -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). @@ -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)); @@ -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); diff --git a/src/manifest.cppm b/src/manifest.cppm index 85af6d9..720ec67 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -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. @@ -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 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 normalize_cpp_standard(std::string_view raw) { @@ -481,6 +501,16 @@ std::expected 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())` @@ -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(); @@ -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(); } diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index a5b41d9..34be18c 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -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; diff --git a/tests/e2e/64_shared_soname_runtime_alias.sh b/tests/e2e/64_shared_soname_runtime_alias.sh new file mode 100755 index 0000000..5a74538 --- /dev/null +++ b/tests/e2e/64_shared_soname_runtime_alias.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# requires: elf +# Shared libraries can declare an ABI SONAME. Consumers may load that ABI name +# through dlopen(), and mcpp run must provide the runtime alias automatically. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +cd "$TMP" +mkdir -p depShared/src app/src + +cat > depShared/src/dep.c <<'EOF' +int dep_shared_answer(void) { + return 42; +} +EOF + +cat > depShared/mcpp.toml <<'EOF' +[package] +name = "depShared" +version = "0.1.0" + +[build] +sources = ["src/*.c"] + +[targets.depShared] +kind = "shared" +soname = "libdepShared.so.1" +EOF + +cat > app/src/main.cpp <<'EOF' +#include + +using answer_fn = int (*)(); + +int main() { + void* handle = dlopen("libdepShared.so.1", RTLD_NOW); + if (!handle) { + return 10; + } + auto answer = reinterpret_cast(dlsym(handle, "dep_shared_answer")); + if (!answer) { + dlclose(handle); + return 11; + } + int result = answer(); + dlclose(handle); + return result == 42 ? 0 : 12; +} +EOF + +cat > app/mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[build] +sources = ["src/*.cpp"] +ldflags = ["-ldl"] + +[targets.app] +kind = "bin" +main = "src/main.cpp" + +[dependencies.depShared] +path = "../depShared" +EOF + +cd app +"$MCPP" build > build.log 2>&1 || { + cat build.log + echo "build failed" + exit 1 +} + +so="$(find target -name 'libdepShared.so' | head -1)" +alias="$(find target -name 'libdepShared.so.1' | head -1)" +[[ -n "$so" && -n "$alias" ]] || { + cat build.log + find target -path '*/bin/*' -maxdepth 4 -type f -o -type l 2>/dev/null || true + echo "expected shared library and ABI soname alias were not produced" + exit 1 +} + +readelf -d "$so" | grep -q 'Library soname: \[libdepShared.so.1\]' || { + readelf -d "$so" || true + echo "shared library missing requested SONAME" + exit 1 +} + +"$MCPP" run > run.log 2>&1 || { + cat run.log + echo "run failed" + exit 1 +} + +echo "OK" diff --git a/tests/e2e/65_toolchain_runtime_dirs_for_run.sh b/tests/e2e/65_toolchain_runtime_dirs_for_run.sh new file mode 100755 index 0000000..fdc6bfa --- /dev/null +++ b/tests/e2e/65_toolchain_runtime_dirs_for_run.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# requires: llvm linux +# dlopen() providers such as GLX drivers do not use the main executable's +# RUNPATH for their own DT_NEEDED closure. mcpp run must therefore expose the +# toolchain runtime directories in LD_LIBRARY_PATH as well. +set -e + +OS="$(uname -s)" +if [[ "$OS" != "Linux" ]]; then + echo "SKIP: LD_LIBRARY_PATH runtime-dir check is Linux-specific" + exit 0 +fi + +LLVM_ROOT="${HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7" +if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then + echo "SKIP: xlings llvm@20.1.7 is not installed" + exit 0 +fi + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" + +mkdir -p "$TMP/proj/src" +cd "$TMP/proj" + +cat > mcpp.toml <<'EOF' +[package] +name = "toolchain_runtime_env" +version = "0.1.0" + +[toolchain] +linux = "llvm@20.1.7" + +[targets.toolchain_runtime_env] +kind = "bin" +main = "src/main.cpp" +EOF + +cat > src/main.cpp <<'EOF' +#include +#include + +int main() { + const char* value = std::getenv("LD_LIBRARY_PATH"); + if (value == nullptr) return 10; + + std::string path(value); + if (path.find("xim-x-llvm/20.1.7/lib") == std::string::npos) return 11; + if (path.find("xim-x-glibc/2.39/lib64") == std::string::npos) return 12; + return 0; +} +EOF + +"$MCPP" build > "$TMP/build.log" 2>&1 || { + cat "$TMP/build.log" + echo "FAIL: build failed" + exit 1 +} + +"$MCPP" run > "$TMP/run.log" 2>&1 || { + cat "$TMP/run.log" + echo "FAIL: mcpp run did not expose toolchain runtime dirs" + exit 1 +} + +echo "OK" diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 4025fb7..45416d5 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -28,6 +28,40 @@ main = "src/main.cpp" EXPECT_EQ(m->targets[0].kind, mcpp::manifest::Target::Binary); } +TEST(Manifest, SharedTargetSoname) { + constexpr auto src = R"( +[package] +name = "dep" +version = "0.1.0" +[build] +sources = ["src/*.c"] +[targets.dep] +kind = "shared" +soname = "libdep.so.1" +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + ASSERT_EQ(m->targets.size(), 1u); + EXPECT_EQ(m->targets[0].kind, mcpp::manifest::Target::SharedLibrary); + EXPECT_EQ(m->targets[0].soname, "libdep.so.1"); +} + +TEST(Manifest, RejectsSonameOnNonSharedTarget) { + constexpr auto src = R"( +[package] +name = "app" +version = "0.1.0" +[targets.app] +kind = "bin" +main = "src/main.cpp" +soname = "libapp.so.1" +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_FALSE(m.has_value()); + EXPECT_NE(m.error().message.find("soname is only valid for shared targets"), + std::string::npos); +} + TEST(Manifest, PackageStandardCpp26AcceptedAndMirrored) { constexpr auto src = R"( [package] @@ -314,6 +348,25 @@ package = { EXPECT_EQ(m->modules.sources[0], "*/src/*.c"); } +TEST(SynthesizeFromXpkgLua, SharedTargetSoname) { + constexpr auto src = R"( +package = { + spec = "1", + name = "tinyshared", + xpm = { linux = { ["1.0.0"] = { url = "u", sha256 = "h" } } }, + mcpp = { + sources = { "*/src/*.c" }, + targets = { ["tinyshared"] = { kind = "shared", soname = "libtinyshared.so.1" } }, + }, +} +)"; + auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "tinyshared", "1.0.0"); + ASSERT_TRUE(m.has_value()) << m.error().format(); + ASSERT_EQ(m->targets.size(), 1u); + EXPECT_EQ(m->targets[0].kind, mcpp::manifest::Target::SharedLibrary); + EXPECT_EQ(m->targets[0].soname, "libtinyshared.so.1"); +} + TEST(SynthesizeFromXpkgLua, RuntimeConfig) { constexpr auto src = R"( package = { From ef5be0d67b7506c5fcd10338606660bbecc2a101 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 3 Jun 2026 07:22:20 +0800 Subject: [PATCH 2/2] fix: isolate host tools from target runtime env --- src/platform/env.cppm | 11 +++++++++ src/platform/linux.cppm | 37 ++++++++++++++++++++++++---- src/platform/process.cppm | 14 +++++++++++ src/pm/publisher.cppm | 2 +- src/toolchain/probe.cppm | 4 +-- tests/unit/test_toolchain_detect.cpp | 25 +++++++++++++++++++ tests/unit/test_xpkg_emit.cpp | 26 +++++++++++++++++++ 7 files changed, 111 insertions(+), 8 deletions(-) diff --git a/src/platform/env.cppm b/src/platform/env.cppm index d6964d4..ad9069b 100644 --- a/src/platform/env.cppm +++ b/src/platform/env.cppm @@ -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 dirs); @@ -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 dirs) { if (key.empty() || dirs.empty()) return ""; diff --git a/src/platform/linux.cppm b/src/platform/linux.cppm index 5dfd05c..465ff04 100644 --- a/src/platform/linux.cppm +++ b/src/platform/linux.cppm @@ -19,6 +19,12 @@ export namespace mcpp::platform::linux_ { std::string build_ld_library_path_prefix( const std::vector& 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& dirs); + // Return Linux toolchain runtime library directories. std::vector runtime_lib_dirs(const std::filesystem::path& toolchain_root); @@ -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& dirs) -{ -#if defined(__linux__) - if (dirs.empty()) return ""; +namespace { + +std::string join_dirs(const std::vector& 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& 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 @@ -47,6 +61,19 @@ std::string build_ld_library_path_prefix( #endif } +std::string build_clean_ld_library_path_prefix( + const std::vector& 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 runtime_lib_dirs(const std::filesystem::path& toolchain_root) { std::vector dirs; diff --git a/src/platform/process.cppm b/src/platform/process.cppm index e258f36..d3ee9c6 100644 --- a/src/platform/process.cppm +++ b/src/platform/process.cppm @@ -30,6 +30,7 @@ module; export module mcpp.platform.process; import std; +import mcpp.platform.env; export namespace mcpp::platform::process { @@ -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). @@ -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 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>& env) diff --git a/src/pm/publisher.cppm b/src/pm/publisher.cppm index 6eee15d..139a18f 100644 --- a/src/pm/publisher.cppm +++ b/src/pm/publisher.cppm @@ -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> \n" auto sp = r.output.find(' '); diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index 3108f32..6f2d483 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -88,13 +88,13 @@ std::string join_colon_paths(const std::vector& dirs) { } std::string env_prefix_for_dirs(const std::vector& dirs) { - return mcpp::platform::linux_::build_ld_library_path_prefix(dirs); + return mcpp::platform::linux_::build_clean_ld_library_path_prefix(dirs); } } // namespace std::expected run_capture(const std::string& cmd) { - auto r = mcpp::platform::process::capture(cmd); + auto r = mcpp::platform::process::capture_host_tool(cmd); if (r.exit_code != 0 && r.output.empty()) { return std::unexpected(DetectError{std::format("failed to execute: {}", cmd)}); } diff --git a/tests/unit/test_toolchain_detect.cpp b/tests/unit/test_toolchain_detect.cpp index 1722281..eac7407 100644 --- a/tests/unit/test_toolchain_detect.cpp +++ b/tests/unit/test_toolchain_detect.cpp @@ -1,6 +1,7 @@ #include import std; +import mcpp.platform.env; import mcpp.toolchain.detect; import mcpp.toolchain.probe; @@ -52,6 +53,14 @@ esac return clang; } +std::filesystem::path make_hostile_ld_dir() { + auto dir = std::filesystem::temp_directory_path() + / std::format("mcpp_hostile_ld_{}", std::random_device{}()); + std::filesystem::create_directories(dir); + std::ofstream(dir / "libc.so.6").close(); + return dir; +} + } // namespace #if !defined(_WIN32) @@ -70,6 +79,22 @@ TEST(ToolchainDetect, ClangVersionOutputIsNotMisclassifiedByGccPaths) { } #endif // !defined(_WIN32) +#if defined(__linux__) +TEST(ToolchainDetect, IgnoresTargetRuntimeLibraryPathDuringProbe) { + auto clang = make_fake_clang(); + TempDirGuard cleanup_clang{clang.parent_path()}; + auto hostile = make_hostile_ld_dir(); + TempDirGuard cleanup_ld{hostile}; + + mcpp::platform::env::ScopedEnv ld("LD_LIBRARY_PATH", hostile.string()); + + auto tc = detect(clang); + ASSERT_TRUE(tc.has_value()) << tc.error().message; + EXPECT_EQ(tc->compiler, CompilerId::Clang); + EXPECT_EQ(tc->targetTriple, "x86_64-unknown-linux-gnu"); +} +#endif // defined(__linux__) + // ─── normalize_driver_output: path-free semantic identity ───────────── // // Background: the toolchain fingerprint used to hash the compiler binary diff --git a/tests/unit/test_xpkg_emit.cpp b/tests/unit/test_xpkg_emit.cpp index bf5b09f..4be1af6 100644 --- a/tests/unit/test_xpkg_emit.cpp +++ b/tests/unit/test_xpkg_emit.cpp @@ -3,6 +3,7 @@ import std; import mcpp.manifest; import mcpp.modgraph.graph; +import mcpp.platform.env; import mcpp.publish.xpkg_emit; using namespace mcpp::publish; @@ -120,6 +121,31 @@ TEST(XpkgEmit, Sha256OfFile) { } #endif // !defined(_WIN32) +#if defined(__linux__) +TEST(XpkgEmit, Sha256OfFileIgnoresTargetRuntimeLibraryPath) { + using namespace mcpp::publish; + + auto hostile = std::filesystem::temp_directory_path() + / std::format("mcpp_hostile_ld_{}", std::random_device{}()); + std::filesystem::create_directories(hostile); + { std::ofstream(hostile / "libc.so.6").close(); } + + auto p = std::filesystem::temp_directory_path() + / std::format("mcpp_unit_sha_hostile_{}", std::random_device{}()); + { std::ofstream(p).close(); } + + { + mcpp::platform::env::ScopedEnv ld("LD_LIBRARY_PATH", hostile.string()); + EXPECT_EQ(sha256_of_file(p), + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + } + + std::error_code ec; + std::filesystem::remove(p, ec); + std::filesystem::remove_all(hostile, ec); +} +#endif // defined(__linux__) + TEST(XpkgEmit, LongBracketSequenceInValueIsHarmless) { // We emit `"..."` strings, not `[[...]]`, so a literal `]=]` in // user data can't terminate the literal early. Just make sure it