@@ -21,6 +21,7 @@ import mcpp.toolchain.fingerprint; // hash_file / hash_string (FNV-1a, 16 hex)
2121import mcpp.toolchain.linkmodel; // shared C-library / clang-cfg-bypass model
2222import mcpp.toolchain.model; // Toolchain, PayloadPaths, is_clang/is_musl_target/is_mingw_target
2323import mcpp.toolchain.registry; // archive_tool
24+ import mcpp.toolchain.stdmod; // ensure_built — the SAME std BMI the main build uses
2425import mcpp.toolchain.triple; // host_triple (MCPP_HOST contract value)
2526import mcpp.ui;
2627
@@ -243,10 +244,13 @@ std::vector<std::string> host_base_flags(const mcpp::toolchain::Toolchain& tc) {
243244 return f;
244245}
245246
246- // The bundled `mcpp` build module — a typed API over the stdout wire protocol so
247- // build.mcpp can `import mcpp;` (no `#include`, no `import std;`). I/O uses
248- // C-level primitives in the global module fragment, so the module needs no std
249- // module BMI. The functions mirror the directive set 1:1; they just print the
247+ // The bundled `mcpp` build module — a typed API over the stdout wire protocol
248+ // so build.mcpp can `import mcpp;` instead of `#include`. Its own I/O uses
249+ // C-level primitives in the global module fragment, so the module itself
250+ // needs no std BMI and stays buildable before one exists. (That was once also
251+ // a limit on build.mcpp; it no longer is — a build.mcpp may `import std;` and
252+ // the engine stages the same std module the main build uses.)
253+ // The functions mirror the directive set 1:1; they just print the
250254// `mcpp:` lines the engine already parses. Embedded in the binary (not shipped as
251255// a file) so it always matches this mcpp's protocol.
252256// NOTE: the module declaration line uses a `@MODULE@` placeholder (substituted
@@ -311,6 +315,34 @@ inline const char* dep_dir(const char* name) {
311315// GCC : -fmodules → gcm.cache/mcpp.gcm + mcpp.o; build.mcpp compiles from
312316// `bdir` (cwd) so GCC finds gcm.cache/mcpp.gcm.
313317// Clang : --precompile → mcpp.pcm, then -c → mcpp.o; pass -fmodule-file=mcpp=<pcm>.
318+ // Does the source contain `import <name>;`?
319+ //
320+ // A plain substring search is not enough here: "import std" is a prefix of
321+ // "import std.compat", so the naive test reports both for a program that
322+ // only imports the latter, and mcpp would build a std BMI nobody asked for.
323+ // Match the whole module name and require the terminating `;`, tolerating
324+ // the whitespace the grammar allows. Occurrences inside comments or string
325+ // literals still match — over-detection costs one cached BMI lookup, never
326+ // a wrong build, and that is the same trade the `import mcpp` check has
327+ // always made.
328+ bool imports_module (std::string_view src, std::string_view name) {
329+ constexpr std::string_view kImport = " import" ;
330+ std::size_t pos = 0 ;
331+ while ((pos = src.find (kImport , pos)) != std::string_view::npos) {
332+ std::size_t i = pos + kImport .size ();
333+ // `importfoo` is not an import.
334+ if (i >= src.size () || (src[i] != ' ' && src[i] != ' \t ' )) { ++pos; continue ; }
335+ while (i < src.size () && (src[i] == ' ' || src[i] == ' \t ' )) ++i;
336+ if (src.compare (i, name.size (), name) == 0 ) {
337+ std::size_t j = i + name.size ();
338+ while (j < src.size () && (src[j] == ' ' || src[j] == ' \t ' )) ++j;
339+ if (j < src.size () && src[j] == ' ;' ) return true ;
340+ }
341+ ++pos;
342+ }
343+ return false ;
344+ }
345+
314346std::expected<std::vector<std::string>, std::string>
315347build_mcpp_module (const fs::path& bdir, const fs::path& compiler,
316348 const std::vector<std::string>& base, const std::string& stdFlag,
@@ -663,7 +695,9 @@ std::expected<void, std::string> run_build_program(
663695 // finds gcm.cache/mcpp.gcm.
664696 std::string srcText;
665697 { std::ifstream is (src); std::ostringstream ss; ss << is.rdbuf (); srcText = ss.str (); }
666- bool usesModule = srcText.find (" import mcpp" ) != std::string::npos;
698+ bool usesModule = srcText.find (" import mcpp" ) != std::string::npos;
699+ bool usesStdCompat = imports_module (srcText, " std.compat" );
700+ bool usesStd = usesStdCompat || imports_module (srcText, " std" );
667701
668702 std::vector<std::string> moduleFlags;
669703 if (usesModule) {
@@ -673,18 +707,104 @@ std::expected<void, std::string> run_build_program(
673707 moduleFlags = std::move (*mf);
674708 }
675709
710+ // ── `import std;` in build.mcpp ─────────────────────────────────────────
711+ //
712+ // mcpp asks projects to `import std;` everywhere and then made their build
713+ // script fall back to `#include` — the bundled `mcpp` module even says so
714+ // in its own header comment. The std module the main build already uses is
715+ // reusable verbatim: stdmod::ensure_built caches on
716+ // (toolchain × standard × dialect), so for a native build this is a cache
717+ // HIT on the very artifact the project's own TUs import. Only a cross
718+ // build pays for a second one, which is unavoidable — see below.
719+ //
720+ // `tc` here is the HOST toolchain: prepare.cppm's
721+ // host_tc_for_build_program() resolves the spec WITHOUT the --target axis
722+ // and hands it in. That is load-bearing. build.mcpp is compiled AND run on
723+ // the machine doing the build, so a std BMI built for the target would
724+ // produce a helper that cannot execute — the same host≠target mistake the
725+ // mingw-cross work had to fix in four separate places.
726+ std::vector<std::string> stdFlags;
727+ std::vector<std::string> stdObjects;
728+ // GCC finds staged BMIs by cwd; Clang/MSVC get an explicit path flag.
729+ bool stdStagedInBdir = false ;
730+ if (usesStd) {
731+ if (!tc.hasImportStd ) {
732+ return std::unexpected (std::format (
733+ " build.mcpp uses `import std;` but the host toolchain ({}) "
734+ " ships no std module.\n "
735+ " Use #include in build.mcpp, or switch to a toolchain "
736+ " that provides one." , tc.label ()));
737+ }
738+ auto sm = mcpp::toolchain::ensure_built (
739+ tc, cppStandard.canonical , std_flag,
740+ mcpp::platform::macos::deployment_target (
741+ m.buildConfig .macosDeploymentTarget ));
742+ if (!sm) {
743+ return std::unexpected (std::format (
744+ " build.mcpp uses `import std;` but the std module could not be "
745+ " built for the host toolchain: {}" , sm.error ().message ));
746+ }
747+
748+ auto traits = mcpp::toolchain::bmi_traits (tc);
749+ if (traits.stdBmiUsePrefix .empty ()) {
750+ // GCC: BMIs are found implicitly under <cwd>/gcm.cache, so stage
751+ // the cached ones where the compile will look. Copy rather than
752+ // symlink — this mirrors the main build's staging edge, and a
753+ // stale copy is caught by ensure_built's own cache key.
754+ std::error_code ec;
755+ fs::path gcmDir = bdir / traits.bmiDir ;
756+ fs::create_directories (gcmDir, ec);
757+ auto stage = [&](const fs::path& from, std::string_view name)
758+ -> std::expected<void , std::string> {
759+ if (from.empty () || !fs::exists (from)) return {};
760+ fs::path to = gcmDir / std::format (" {}{}" , name, traits.bmiExt );
761+ fs::copy_file (from, to, fs::copy_options::overwrite_existing, ec);
762+ if (ec) return std::unexpected (std::format (
763+ " staging {} for build.mcpp failed: {}" , name, ec.message ()));
764+ return {};
765+ };
766+ if (auto r = stage (sm->bmiPath , " std" ); !r)
767+ return std::unexpected (r.error ());
768+ if (usesStdCompat) {
769+ if (auto r = stage (sm->compatBmiPath , " std.compat" ); !r)
770+ return std::unexpected (r.error ());
771+ }
772+ // -fmodules may already be present from the `mcpp` module path;
773+ // GCC tolerates the repeat, but keep the argv honest.
774+ if (!usesModule) stdFlags.push_back (" -fmodules" );
775+ stdStagedInBdir = true ;
776+ } else {
777+ stdFlags.push_back (std::string (traits.stdBmiUsePrefix )
778+ + sm->bmiPath .string ());
779+ if (usesStdCompat && !sm->compatBmiPath .empty ())
780+ stdFlags.push_back (std::string (traits.stdCompatBmiUsePrefix )
781+ + sm->compatBmiPath .string ());
782+ // The prefixes carry a leading space for the ninja string channel;
783+ // an argv element must not.
784+ for (auto & f : stdFlags)
785+ if (!f.empty () && f.front () == ' ' ) f.erase (0 , 1 );
786+ }
787+ if (!sm->objectPath .empty () && fs::exists (sm->objectPath ))
788+ stdObjects.push_back (sm->objectPath .string ());
789+ if (usesStdCompat && !sm->compatObjectPath .empty ()
790+ && fs::exists (sm->compatObjectPath ))
791+ stdObjects.push_back (sm->compatObjectPath .string ());
792+ }
793+
676794 // `-x c++` is required: the `.mcpp` extension is unknown to the compiler, so
677795 // without it the driver hands build.mcpp to the linker as a linker script.
678796 std::vector<std::string> compileArgv = { hostCompiler.string (), std_flag, " -O0" };
679797 for (auto & bf : base) compileArgv.push_back (bf);
680798 for (auto & mf : moduleFlags) compileArgv.push_back (mf);
799+ for (auto & sf : stdFlags) compileArgv.push_back (sf);
681800 compileArgv.push_back (" -x" ); compileArgv.push_back (" c++" );
682801 compileArgv.push_back (src.string ());
683- if (usesModule) {
684- // Link the module object (reset the input language first so the .o isn't
685- // treated as C++ source).
802+ if (usesModule || !stdObjects. empty () ) {
803+ // Link the module objects (reset the input language first so the .o
804+ // isn't treated as C++ source).
686805 compileArgv.push_back (" -x" ); compileArgv.push_back (" none" );
687- compileArgv.push_back ((bdir / " mcpp.o" ).string ());
806+ if (usesModule) compileArgv.push_back ((bdir / " mcpp.o" ).string ());
807+ for (auto & so : stdObjects) compileArgv.push_back (so);
688808 }
689809 // Self-contained helper link — see the staticHostHelper doctrine above.
690810 // Deliberately NOT in `base`: that also feeds the bundled module's
@@ -693,9 +813,13 @@ std::expected<void, std::string> run_build_program(
693813 if (staticHostHelper) compileArgv.push_back (" -static" );
694814 compileArgv.push_back (" -o" ); compileArgv.push_back (bin.string ());
695815 mcpp::ui::info (" build.mcpp" , " compiling" );
696- // GCC resolves `import mcpp;` via gcm.cache/ relative to the compile cwd, so
697- // run the module-using compile from bdir; otherwise the project root is fine.
698- std::string compileCwd = usesModule ? bdir.string () : root.string ();
816+ // GCC resolves imported BMIs via gcm.cache/ relative to the compile cwd, so
817+ // any compile that imports a module — `mcpp`, `std`, or both — has to run
818+ // from bdir, where they were staged. One condition, not two: a build.mcpp
819+ // that imports only std needs exactly the same cwd as one that imports
820+ // only mcpp. Otherwise the project root is fine.
821+ const bool needsBmiCwd = usesModule || stdStagedInBdir;
822+ std::string compileCwd = needsBmiCwd ? bdir.string () : root.string ();
699823 auto cres = mcpp::platform::process::capture_exec (compileArgv, {}, compileCwd);
700824 if (cres.exit_code != 0 ) {
701825 return std::unexpected (std::format (
0 commit comments