@@ -281,6 +281,38 @@ void write_file(const std::filesystem::path& p, std::string_view content) {
281281 os << content;
282282}
283283
284+ const std::vector<std::filesystem::path>& compiler_invocation_dirs (const BuildPlan& plan) {
285+ return plan.toolchain .compilerInvocationRuntimeDirs .empty ()
286+ ? plan.toolchain .compilerRuntimeDirs
287+ : plan.toolchain .compilerInvocationRuntimeDirs ;
288+ }
289+
290+ bool needs_compiler_launcher (const BuildPlan& plan) {
291+ return mcpp::platform::is_linux && !compiler_invocation_dirs (plan).empty ();
292+ }
293+
294+ std::filesystem::path compiler_launcher_path (const BuildPlan& plan, bool cxx) {
295+ return plan.outputDir / (cxx ? " mcpp-cxx" : " mcpp-cc" );
296+ }
297+
298+ void write_compiler_launcher (const std::filesystem::path& path,
299+ const std::filesystem::path& compiler,
300+ const std::vector<std::filesystem::path>& dirs) {
301+ std::string libraryPath;
302+ for (auto const & dir : dirs) {
303+ if (!libraryPath.empty ()) libraryPath += ' :' ;
304+ libraryPath += dir.string ();
305+ }
306+ write_file (path, std::format (
307+ " #!/bin/sh\n LD_LIBRARY_PATH={}\n export LD_LIBRARY_PATH\n exec {} \" $@\"\n " ,
308+ mcpp::platform::shell::quote (libraryPath),
309+ mcpp::platform::shell::quote (compiler.string ())));
310+
311+ std::error_code ec;
312+ std::filesystem::permissions (path, std::filesystem::perms::owner_exec,
313+ std::filesystem::perm_options::add, ec);
314+ }
315+
284316bool run (const std::string& cmd, std::string& output_capture, bool capture_output = true ) {
285317 output_capture.clear ();
286318 if (capture_output) {
@@ -384,6 +416,10 @@ std::vector<std::string> command_prefixes(const CompileFlags& flags,
384416 };
385417 add (flags.cxxBinary );
386418 add (flags.ccBinary );
419+ if (needs_compiler_launcher (plan)) {
420+ add (compiler_launcher_path (plan, /* cxx=*/ true ));
421+ add (compiler_launcher_path (plan, /* cxx=*/ false ));
422+ }
387423 add (flags.arBinary );
388424 add (plan.scanDepsPath );
389425 // mcpp itself drives the dyndep and stage_file rules; its echoed command
@@ -589,20 +625,18 @@ std::string emit_ninja_string(const BuildPlan& plan) {
589625 // The macOS initializer-ordering shim (#336) is a C translation unit, so
590626 // it needs the C driver bindings even in a project with no .c sources.
591627 const bool need_ios_init_shim = flags.needsStreamInitShim ;
592- auto compiler_command = [&](const std::filesystem::path& binary) {
593- const auto & dirs = plan.toolchain .compilerInvocationRuntimeDirs .empty ()
594- ? plan.toolchain .compilerRuntimeDirs
595- : plan.toolchain .compilerInvocationRuntimeDirs ;
596- return mcpp::platform::linux_::build_clean_ld_library_path_prefix (dirs)
597- + escape_ninja_path (binary);
628+ auto compiler_command = [&](const std::filesystem::path& binary, bool cxx) {
629+ if (needs_compiler_launcher (plan))
630+ return escape_ninja_path (compiler_launcher_path (plan, cxx));
631+ return escape_ninja_path (binary);
598632 };
599- append (std::format (" cxx = {}\n " , compiler_command (flags.cxxBinary )));
633+ append (std::format (" cxx = {}\n " , compiler_command (flags.cxxBinary , /* cxx= */ true )));
600634 // clang-scan-deps receives the driver after `--` as argv, not as a shell
601- // command. Keep a raw path for that interface; `$cxx` may start with env .
635+ // command. Keep a raw path for that interface; `$cxx` may be a launcher .
602636 append (std::format (" cxx_driver = {}\n " , escape_ninja_path (flags.cxxBinary )));
603637 append (std::format (" cxxflags = {}\n " , flags.cxx ));
604638 if (need_c_rule || need_asm_rule || need_ios_init_shim) { // asm_object drives the C compiler too
605- append (std::format (" cc = {}\n " , compiler_command (flags.ccBinary )));
639+ append (std::format (" cc = {}\n " , compiler_command (flags.ccBinary , /* cxx= */ false )));
606640 }
607641 if (need_c_rule || need_ios_init_shim) {
608642 append (std::format (" cflags = {}\n " , flags.cc ));
@@ -2281,10 +2315,22 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
22812315 plan.outputDir .string (), ec.message ()),
22822316 plan.outputDir });
22832317
2318+ auto flags = compute_flags (plan);
2319+ stage (" compute-flags" );
2320+
22842321 auto ninja_path = plan.outputDir / " build.ninja" ;
22852322 auto manifest = emit_ninja_string (plan);
22862323 stage (" emit-ninja" );
22872324
2325+ if (needs_compiler_launcher (plan)) {
2326+ const auto & dirs = compiler_invocation_dirs (plan);
2327+ write_compiler_launcher (compiler_launcher_path (plan, /* cxx=*/ true ),
2328+ flags.cxxBinary , dirs);
2329+ write_compiler_launcher (compiler_launcher_path (plan, /* cxx=*/ false ),
2330+ flags.ccBinary , dirs);
2331+ }
2332+ stage (" write-compiler-launcher" );
2333+
22882334 // Command-length backstop (see
22892335 // .agents/docs/2026-08-06-command-length-architecture.md). The structural
22902336 // defence is that every unbounded payload goes through a response file;
@@ -2299,8 +2345,6 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
22992345 stage (" write-ninja" );
23002346
23012347 // compile_commands.json — via the dedicated module.
2302- auto flags = compute_flags (plan);
2303- stage (" compute-flags" );
23042348 auto cdb = write_compile_commands (plan, flags);
23052349 stage (" compile-commands" );
23062350 if (!cdb) {
0 commit comments