@@ -77,6 +77,11 @@ struct BuildPlan {
7777 std::vector<CapabilityProvider> runtimeProviders;
7878};
7979
80+ // True if a source file defines a top-level `int main(`/`auto main(` entry,
81+ // ignoring comments and string/raw-string literals. Drives the archive-vs-inline
82+ // choice for kind="lib" dependencies (see plan.cppm).
83+ bool source_defines_main (const std::filesystem::path& src);
84+
8085// Build a BuildPlan from already-validated inputs.
8186BuildPlan make_plan (const mcpp::manifest::Manifest& manifest,
8287 const mcpp::toolchain::Toolchain& tc,
@@ -212,6 +217,70 @@ void append_unique_path(std::vector<std::filesystem::path>& out,
212217
213218} // namespace
214219
220+ // True if `src` defines a top-level `int main(` / `auto main(` entry point.
221+ // Comments and string/char/raw-string literals are stripped first, so test
222+ // fixtures that embed `"int main() {...}"` or R"(int main(){})" don't
223+ // false-positive (that misfire chose archive linking for a no-main test →
224+ // gtest_main.o not pulled by MSVC lld-link → LNK1561). Heuristic but robust;
225+ // worst case is a sub-optimal archive-vs-inline choice, never a miscompile.
226+ bool source_defines_main (const std::filesystem::path& src) {
227+ std::ifstream is (src);
228+ if (!is) return false ;
229+ std::string raw ((std::istreambuf_iterator<char >(is)),
230+ std::istreambuf_iterator<char >());
231+ std::string code;
232+ code.reserve (raw.size ());
233+ enum State { Normal, Line, Block, Str, Chr, RawStr } st = Normal;
234+ std::string rawEnd; // ")delim\"" terminator for the active raw string
235+ for (std::size_t i = 0 ; i < raw.size (); ++i) {
236+ char c = raw[i];
237+ char n = (i + 1 < raw.size ()) ? raw[i + 1 ] : ' \0 ' ;
238+ switch (st) {
239+ case Normal:
240+ if (c == ' R' && n == ' "' ) {
241+ std::size_t j = i + 2 ;
242+ std::string delim;
243+ while (j < raw.size () && raw[j] != ' (' ) delim.push_back (raw[j++]);
244+ rawEnd = " )" + delim + " \" " ;
245+ st = RawStr;
246+ i = j; // sit on '(' ; loop ++ moves past
247+ } else if (c == ' /' && n == ' /' ) { st = Line; ++i; }
248+ else if (c == ' /' && n == ' *' ) { st = Block; ++i; }
249+ else if (c == ' "' ) { st = Str; }
250+ else if (c == ' \' ' ) { st = Chr; }
251+ else { code.push_back (c); }
252+ break ;
253+ case Line: if (c == ' \n ' ) { st = Normal; code.push_back (c); } break ;
254+ case Block: if (c == ' *' && n == ' /' ) { st = Normal; ++i; } break ;
255+ case Str: if (c == ' \\ ' ) ++i; else if (c == ' "' ) st = Normal; break ;
256+ case Chr: if (c == ' \\ ' ) ++i; else if (c == ' \' ' ) st = Normal; break ;
257+ case RawStr:
258+ if (raw.compare (i, rawEnd.size (), rawEnd) == 0 ) {
259+ st = Normal;
260+ i += rawEnd.size () - 1 ;
261+ }
262+ break ;
263+ }
264+ }
265+ auto isws = [](char c) {
266+ return c == ' ' || c == ' \t ' || c == ' \n ' || c == ' \r ' || c == ' \f ' || c == ' \v ' ;
267+ };
268+ for (std::size_t i = 0 ; i + 4 <= code.size (); ++i) {
269+ if (code.compare (i, 4 , " main" ) != 0 ) continue ;
270+ std::size_t p = i;
271+ bool sawWs = false ;
272+ while (p > 0 && isws (code[p - 1 ])) { --p; sawWs = true ; }
273+ bool prevOk = sawWs && (
274+ (p >= 3 && code.compare (p - 3 , 3 , " int" ) == 0 ) ||
275+ (p >= 4 && code.compare (p - 4 , 4 , " auto" ) == 0 ));
276+ std::size_t q = i + 4 ;
277+ while (q < code.size () && isws (code[q])) ++q;
278+ bool nextOk = q < code.size () && code[q] == ' (' ;
279+ if (prevOk && nextOk) return true ;
280+ }
281+ return false ;
282+ }
283+
215284BuildPlan make_plan (const mcpp::manifest::Manifest& manifest,
216285 const mcpp::toolchain::Toolchain& tc,
217286 const mcpp::toolchain::Fingerprint& fp,
@@ -546,9 +615,9 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
546615 // Whether this consumer's own entry source defines `main`. Decides how
547616 // kind="lib" dependencies are linked (archive vs inline) so the
548617 // gtest_main-style optional entry works on EVERY linker — see the
549- // dependency-linking block further below. Default false → if we can't
550- // tell, fall back to inlining (the pre-archive behavior).
551- bool entryDefinesMain = false ;
618+ // dependency-linking block further below. Can't tell (no entry) →
619+ // false → inline (the pre-archive behavior, always provides the entry ).
620+ bool entryDefinesMain = lu. entryMain && source_defines_main (*lu. entryMain ) ;
552621
553622 if ((lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary) && lu.entryMain ) {
554623 // Add main.cpp -> obj/main.o
@@ -587,18 +656,6 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
587656 }
588657 if (!name.empty ()) main_cu.imports .push_back (name);
589658 }
590- // Detect a top-level `int main(`/`auto main(` definition
591- // (space-insensitive; skip comment lines). Heuristic, but the
592- // worst case is a wrong archive-vs-inline choice, not breakage.
593- if (!entryDefinesMain && !line.starts_with (" //" ) && !line.starts_with (" *" )) {
594- std::string nospace;
595- for (char c : line)
596- if (!std::isspace (static_cast <unsigned char >(c))) nospace.push_back (c);
597- if (nospace.find (" intmain(" ) != std::string::npos
598- || nospace.find (" automain(" ) != std::string::npos) {
599- entryDefinesMain = true ;
600- }
601- }
602659 }
603660
604661 // Avoid duplicate insert if main was already scanned
0 commit comments