Skip to content
Open
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
80 changes: 75 additions & 5 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ std::unique_ptr<Backend> make_ninja_backend();
std::string emit_ninja_string(const BuildPlan& plan);
std::string filter_ninja_output(std::string_view output,
std::span<const std::string> commandPrefixes);
std::string compiler_launcher_contents(const std::filesystem::path& loader,
const std::filesystem::path& compiler,
const std::vector<std::filesystem::path>& dirs);

// Advice appended to a failed build whose linker output names a replaceable
// function nothing in the graph defines. Empty when there is nothing to add.
Expand All @@ -81,6 +84,21 @@ std::string link_failure_advice(std::string_view output);

namespace mcpp::build {

std::string compiler_launcher_contents(const std::filesystem::path& loader,
const std::filesystem::path& compiler,
const std::vector<std::filesystem::path>& dirs) {
std::string libraryPath;
for (auto const& dir : dirs) {
if (!libraryPath.empty()) libraryPath += ':';
libraryPath += dir.string();
}
return std::format(
"#!/bin/sh\nexec {} --library-path {} {} \"$@\"\n",
mcpp::platform::shell::quote(loader.string()),
mcpp::platform::shell::quote(libraryPath),
mcpp::platform::shell::quote(compiler.string()));
}

namespace {

std::string escape_ninja_path(const std::filesystem::path& p) {
Expand Down Expand Up @@ -281,6 +299,33 @@ void write_file(const std::filesystem::path& p, std::string_view content) {
os << content;
}

const std::vector<std::filesystem::path>& compiler_invocation_dirs(const BuildPlan& plan) {
return plan.toolchain.compilerInvocationRuntimeDirs.empty()
? plan.toolchain.compilerRuntimeDirs
: plan.toolchain.compilerInvocationRuntimeDirs;
}

bool needs_compiler_launcher(const BuildPlan& plan) {
return mcpp::platform::is_linux
&& plan.toolchain.compiler == mcpp::toolchain::CompilerId::GCC
&& !plan.toolchain.compilerInvocationLoader.empty();
}

std::filesystem::path compiler_launcher_path(const BuildPlan& plan, bool cxx) {
return plan.outputDir / (cxx ? "mcpp-cxx" : "mcpp-cc");
}

void write_compiler_launcher(const std::filesystem::path& path,
const std::filesystem::path& loader,
const std::filesystem::path& compiler,
const std::vector<std::filesystem::path>& dirs) {
write_file(path, compiler_launcher_contents(loader, compiler, dirs));

std::error_code ec;
std::filesystem::permissions(path, std::filesystem::perms::owner_exec,
std::filesystem::perm_options::add, ec);
}

bool run(const std::string& cmd, std::string& output_capture, bool capture_output = true) {
output_capture.clear();
if (capture_output) {
Expand Down Expand Up @@ -384,6 +429,10 @@ std::vector<std::string> command_prefixes(const CompileFlags& flags,
};
add(flags.cxxBinary);
add(flags.ccBinary);
if (needs_compiler_launcher(plan)) {
add(compiler_launcher_path(plan, /*cxx=*/true));
add(compiler_launcher_path(plan, /*cxx=*/false));
}
add(flags.arBinary);
add(plan.scanDepsPath);
// mcpp itself drives the dyndep and stage_file rules; its echoed command
Expand Down Expand Up @@ -589,10 +638,19 @@ std::string emit_ninja_string(const BuildPlan& plan) {
// The macOS initializer-ordering shim (#336) is a C translation unit, so
// it needs the C driver bindings even in a project with no .c sources.
const bool need_ios_init_shim = flags.needsStreamInitShim;
append(std::format("cxx = {}\n", escape_ninja_path(flags.cxxBinary)));
auto compiler_command = [&](const std::filesystem::path& binary, bool cxx) {
if (needs_compiler_launcher(plan))
return shell_quote_arg(escape_ninja_chars(
compiler_launcher_path(plan, cxx).string()));
return escape_ninja_path(binary);
};
append(std::format("cxx = {}\n", compiler_command(flags.cxxBinary, /*cxx=*/true)));
// clang-scan-deps receives the driver after `--` as argv, not as a shell
// command. Keep a raw path for that interface; `$cxx` may be a launcher.
append(std::format("cxx_driver = {}\n", escape_ninja_path(flags.cxxBinary)));
append(std::format("cxxflags = {}\n", flags.cxx));
if (need_c_rule || need_asm_rule || need_ios_init_shim) { // asm_object drives the C compiler too
append(std::format("cc = {}\n", escape_ninja_path(flags.ccBinary)));
append(std::format("cc = {}\n", compiler_command(flags.ccBinary, /*cxx=*/false)));
}
if (need_c_rule || need_ios_init_shim) {
append(std::format("cflags = {}\n", flags.cc));
Expand Down Expand Up @@ -1267,7 +1325,7 @@ std::string emit_ninja_string(const BuildPlan& plan) {
// overruns (#261: 48 -I entries at a deep consumer path).
append(std::format(
" command = $scan_deps -format=p1689 -o $out -- "
"$cxx{} $cxxflags $unit_cxxflags $unit_lang -c $in "
"$cxx_driver{} $cxxflags $unit_cxxflags $unit_lang -c $in "
"-o $compile_target\n",
rsp_ref(scanPayload)));
}
Expand Down Expand Up @@ -2271,10 +2329,24 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
plan.outputDir.string(), ec.message()),
plan.outputDir});

auto flags = compute_flags(plan);
stage("compute-flags");

auto ninja_path = plan.outputDir / "build.ninja";
auto manifest = emit_ninja_string(plan);
stage("emit-ninja");

if (needs_compiler_launcher(plan)) {
const auto& dirs = compiler_invocation_dirs(plan);
write_compiler_launcher(compiler_launcher_path(plan, /*cxx=*/true),
plan.toolchain.compilerInvocationLoader,
flags.cxxBinary, dirs);
write_compiler_launcher(compiler_launcher_path(plan, /*cxx=*/false),
plan.toolchain.compilerInvocationLoader,
flags.ccBinary, dirs);
}
stage("write-compiler-launcher");

// Command-length backstop (see
// .agents/docs/2026-08-06-command-length-architecture.md). The structural
// defence is that every unbounded payload goes through a response file;
Expand All @@ -2289,8 +2361,6 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
stage("write-ninja");

// compile_commands.json — via the dedicated module.
auto flags = compute_flags(plan);
stage("compute-flags");
auto cdb = write_compile_commands(plan, flags);
stage("compile-commands");
if (!cdb) {
Expand Down
6 changes: 6 additions & 0 deletions src/toolchain/detect.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ detect(const std::filesystem::path& explicit_compiler,
}

tc.compilerRuntimeDirs = discover_compiler_runtime_dirs(tc.binaryPath);
tc.compilerInvocationRuntimeDirs =
discover_compiler_invocation_runtime_dirs(tc.binaryPath, runtimeBinding);
auto loader = discover_compiler_invocation_loader(tc.binaryPath, runtimeBinding);
if (!loader) return std::unexpected(loader.error());
if (*loader)
tc.compilerInvocationLoader = std::move(**loader);
auto envPrefix = compiler_env_prefix(tc);

auto ver_r = run_capture(std::format("{}{} --version 2>&1",
Expand Down
20 changes: 16 additions & 4 deletions src/toolchain/model.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,24 @@ struct Toolchain {
// zero-libc tier and on hosted targets.
std::string targetSysrootPkg;
std::filesystem::path targetSysrootLib;
std::vector<std::filesystem::path> compilerRuntimeDirs; // LD_LIBRARY_PATH for private tools
// Runtime directories inherited by Ninja and its whole process tree. Keep
// private libc out of this list: Ninja launches each edge through /bin/sh.
std::vector<std::filesystem::path> compilerRuntimeDirs;
// Directories required when starting a compiler driver. For native managed
// GCC on Linux, the private loader below consumes these after Ninja's
// shell has already started.
std::vector<std::filesystem::path> compilerInvocationRuntimeDirs;
// The private ELF loader for a native managed GCC and its bound glibc.
// Empty for every other toolchain: they retain their ordinary driver
// invocation. Keeping this separate from the runtime directories matters
// because an LD_LIBRARY_PATH export reaches GCC's host as/ld children.
std::filesystem::path compilerInvocationLoader;
std::vector<std::filesystem::path> linkRuntimeDirs; // -L/-rpath dirs for produced binaries
// Environment the toolchain's tools need when invoked (set on the ninja
// process, inherited by compiler/linker children). Empty for GCC/Clang
// (their LD_LIBRARY_PATH need goes through compilerRuntimeDirs); the
// MSVC backend fills INCLUDE/LIB/PATH here (design §5.1).
// process, inherited by compiler/linker children). Empty for GCC/Clang;
// native managed GCC uses compilerInvocationLoader instead of exporting
// its bound glibc. The MSVC backend fills INCLUDE/LIB/PATH here (design
// §5.1).
// (Own struct, not std::pair — GCC 16 modules choke on a std::pair
// member added to this exported class: "failed to load pendings".)
std::vector<EnvVar> envOverrides;
Expand Down
107 changes: 106 additions & 1 deletion src/toolchain/probe.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ std::string normalize_driver_output(std::string_view s);
std::vector<std::filesystem::path>
discover_compiler_runtime_dirs(const std::filesystem::path& compilerBin);

std::vector<std::filesystem::path>
discover_compiler_invocation_runtime_dirs(const std::filesystem::path& compilerBin,
std::string_view runtimeBinding);

std::expected<std::optional<std::filesystem::path>, DetectError>
discover_compiler_invocation_loader(const std::filesystem::path& compilerBin,
std::string_view runtimeBinding);

std::vector<std::filesystem::path>
discover_link_runtime_dirs(const std::filesystem::path& compilerBin,
std::string_view targetTriple);
Expand Down Expand Up @@ -95,8 +103,24 @@ std::string env_prefix_for_dirs(const std::vector<std::filesystem::path>& dirs)
return mcpp::platform::linux_::build_clean_ld_library_path_prefix(dirs);
}

bool is_native_managed_gcc(const std::filesystem::path& compilerBin) {
// Managed compiler paths are <xpkgs>/xim-x-gcc/<version>/bin/g++.
// Cross compilers also accept a glibc runtime binding for their target,
// but their host-side driver must not load that private libc.
const auto packageVersion = compilerBin.parent_path().parent_path();
return packageVersion.parent_path().filename() == "xim-x-gcc";
}

bool is_elf_loader_name(std::string_view name) {
return name.starts_with("ld-linux-") && name.find(".so") != std::string_view::npos;
}

} // namespace

std::optional<std::filesystem::path>
payload_root_for_binding(const std::filesystem::path& compilerBin,
std::string_view binding);

std::expected<std::string, DetectError> run_capture(const std::string& cmd) {
auto r = mcpp::platform::process::capture_host_tool(cmd);
if (r.exit_code != 0 && r.output.empty()) {
Expand Down Expand Up @@ -205,9 +229,84 @@ discover_compiler_runtime_dirs(const std::filesystem::path& compilerBin) {
append_existing_unique(dirs, *rt / "lib64");
append_existing_unique(dirs, *rt / "lib");
}

return dirs;
}

std::vector<std::filesystem::path>
discover_compiler_invocation_runtime_dirs(const std::filesystem::path& compilerBin,
std::string_view runtimeBinding) {
auto dirs = discover_compiler_runtime_dirs(compilerBin);

// A managed compiler's DT_RUNPATH reaches its bound private libc for its
// own direct dependencies only. A host /etc/ld.so.preload library can
// require libdl.so.2 itself, where that non-transitive RUNPATH cannot help.
// Use the resolved binding rather than scanning installed glibc versions:
// the selected payload is an ABI decision, not a directory-order choice.
if constexpr (mcpp::platform::is_linux) {
if (runtimeBinding.starts_with("glibc@")
&& is_native_managed_gcc(compilerBin)) {
if (auto glibc = payload_root_for_binding(compilerBin, runtimeBinding)) {
append_existing_unique(dirs, *glibc / "lib64");
append_existing_unique(dirs, *glibc / "lib");
}
}
}
return dirs;
}

std::expected<std::optional<std::filesystem::path>, DetectError>
discover_compiler_invocation_loader(const std::filesystem::path& compilerBin,
std::string_view runtimeBinding) {
if constexpr (!mcpp::platform::is_linux) {
return std::optional<std::filesystem::path>{};
}

if (!runtimeBinding.starts_with("glibc@") || !is_native_managed_gcc(compilerBin))
return std::optional<std::filesystem::path>{};

auto glibc = payload_root_for_binding(compilerBin, runtimeBinding);
if (!glibc) {
return std::unexpected(DetectError{std::format(
"native managed GCC '{}' requires bound {} but its payload is unavailable",
compilerBin.string(), std::string(runtimeBinding))});
}

std::vector<std::filesystem::path> candidates;
std::error_code ec;
for (auto const& dir : {*glibc / "lib64", *glibc / "lib"}) {
if (!std::filesystem::is_directory(dir, ec)) {
ec.clear();
continue;
}
for (std::filesystem::directory_iterator it(dir, ec), end; !ec && it != end;
it.increment(ec)) {
const auto& candidate = it->path();
if (!is_elf_loader_name(candidate.filename().string())
|| !std::filesystem::is_regular_file(candidate, ec)) {
ec.clear();
continue;
}
auto resolved = std::filesystem::weakly_canonical(candidate, ec);
if (ec) {
ec.clear();
resolved = std::filesystem::absolute(candidate, ec);
if (ec) resolved = candidate;
}
if (std::find(candidates.begin(), candidates.end(), resolved) == candidates.end())
candidates.push_back(std::move(resolved));
}
ec.clear();
}

if (candidates.size() != 1) {
return std::unexpected(DetectError{std::format(
"native managed GCC '{}' requires exactly one ld-linux-*.so* in bound {} payload; found {}",
compilerBin.string(), std::string(runtimeBinding), candidates.size())});
}
return std::optional<std::filesystem::path>{candidates.front()};
}

std::vector<std::filesystem::path>
discover_link_runtime_dirs(const std::filesystem::path& compilerBin,
std::string_view targetTriple) {
Expand All @@ -231,7 +330,13 @@ discover_link_runtime_dirs(const std::filesystem::path& compilerBin,
}

std::string compiler_env_prefix(const Toolchain& tc) {
return env_prefix_for_dirs(tc.compilerRuntimeDirs);
if (!tc.compilerInvocationLoader.empty()) {
return std::format("{} --library-path {} ",
mcpp::xlings::shq(tc.compilerInvocationLoader.string()),
mcpp::xlings::shq(join_colon_paths(tc.compilerInvocationRuntimeDirs)));
}
return env_prefix_for_dirs(tc.compilerInvocationRuntimeDirs.empty()
? tc.compilerRuntimeDirs : tc.compilerInvocationRuntimeDirs);
}

std::expected<std::filesystem::path, DetectError>
Expand Down
7 changes: 5 additions & 2 deletions tests/e2e/188_build_actions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,11 @@ EOF
printf 'int main() { return 0; }\n' > src/main.cpp
"$MCPP" build > o0.log 2>&1 || { cat o0.log; echo "FAIL: probe build failed"; exit 1; }
OBJ_NINJA=$(find target -name build.ninja | head -1)
OBJ_CXX=$(sed -n 's/^cxx *= *//p' "$OBJ_NINJA" | head -1)
[ -n "$OBJ_CXX" ] || { cat "$OBJ_NINJA"; echo "FAIL: could not read the compiler out of build.ninja"; exit 1; }
# `cxx` may be a fingerprint-scoped launcher. This action is deliberately
# retained after that build directory is removed below, so use the stable raw
# driver exported for clang-scan-deps instead.
OBJ_CXX=$(sed -n 's/^cxx_driver *= *//p' "$OBJ_NINJA" | head -1)
[ -n "$OBJ_CXX" ] || { cat "$OBJ_NINJA"; echo "FAIL: could not read the compiler driver out of build.ninja"; exit 1; }

cat > src/main.cpp <<'EOF'
#include <cstdio>
Expand Down
Loading
Loading