From e4a9ccadf751b88f5508ce9f890484bae33d1aaf Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Fri, 5 Jun 2020 09:02:05 +0200 Subject: Add ability to split ad hoc C++ recipe into global and local fragments Specifically, now we can write: {{ c++ 1 -- #include -- recipe apply (action, target&) const override { ... } }} --- libbuild2/parser.cxx | 40 +++++++++++++++--- libbuild2/rule.cxx | 116 ++++++++++++++++++++++++++++++++++++++++++++------- libbuild2/rule.hxx | 5 ++- 3 files changed, 139 insertions(+), 22 deletions(-) diff --git a/libbuild2/parser.cxx b/libbuild2/parser.cxx index 6fb20fe..4b958e8 100644 --- a/libbuild2/parser.cxx +++ b/libbuild2/parser.cxx @@ -1103,6 +1103,10 @@ namespace build2 optional lang; location lloc; + + // Use value mode to minimize the number of special characters. + // + mode (lexer_mode::value, '@'); if (next (t, tt) == type::newline) ; else if (tt == type::word) @@ -1132,12 +1136,12 @@ namespace build2 // ar.reset (new adhoc_script_rule (loc, st.value.size ())); } - else if (*lang == "c++") + else if (icasecmp (*lang, "c++") == 0) { // C++ // - // Parse recipe version. + // Parse recipe version and optional fragment separator. // if (tt == type::newline || tt == type::eos) fail (t) << "expected c++ recipe version instead of " << t; @@ -1145,18 +1149,42 @@ namespace build2 location nloc (get_location (t)); names ns (parse_names (t, tt, pattern_mode::ignore)); - uint64_t v; + uint64_t ver; try { - v = convert (move (ns)); + if (ns.empty ()) + throw invalid_argument ("empty"); + + if (ns[0].pair) + throw invalid_argument ("pair in value"); + + ver = convert (move (ns[0])); + } + catch (const invalid_argument& e) + { + fail (nloc) << "invalid c++ recipe version: " << e << endf; + } + + optional sep; + if (ns.size () != 1) + try + { + if (ns.size () != 2) + throw invalid_argument ("multiple names"); + + sep = convert (move (ns[1])); + + if (sep->empty ()) + throw invalid_argument ("empty"); } catch (const invalid_argument& e) { - fail (nloc) << "invalid c++ recipe version value: " << e + fail (nloc) << "invalid c++ recipe fragment separator: " << e << endf; } - ar.reset (new adhoc_cxx_rule (loc, st.value.size (), v)); + ar.reset ( + new adhoc_cxx_rule (loc, st.value.size (), ver, move (sep))); } else fail (lloc) << "unknown recipe language '" << *lang << "'"; diff --git a/libbuild2/rule.cxx b/libbuild2/rule.cxx index 0b9f066..d9c07cb 100644 --- a/libbuild2/rule.cxx +++ b/libbuild2/rule.cxx @@ -786,8 +786,8 @@ namespace build2 // adhoc_cxx_rule // adhoc_cxx_rule:: - adhoc_cxx_rule (const location& l, size_t b, uint64_t v) - : adhoc_rule (l, b), version (v), impl (nullptr) + adhoc_cxx_rule (const location& l, size_t b, uint64_t v, optional s) + : adhoc_rule (l, b), version (v), separator (move (s)), impl (nullptr) { if (v != 1) fail (l) << "unsupported c++ recipe version " << v; @@ -905,6 +905,7 @@ namespace build2 { sha256 cs; cs.append ("c++"); + cs.append (separator ? *separator : ""); cs.append (code); id = cs.abbreviated_string (12); } @@ -948,6 +949,72 @@ namespace build2 } }; + // Calculate (and cache) the global/local fragments split. + // + struct fragments + { + size_t global_p; // Start position. + size_t global_n; // Length (0 if no global fragment). + location global_l; // Position. + + size_t local_p; + size_t local_n; + location local_l; + }; + + auto split = [this, f = optional ()] () mutable -> + const fragments& + { + if (f) + return *f; + + // Note that the code starts from the next line thus +1. + // + location gl (loc.file, loc.line + 1, 1); + + if (!separator) + { + f = fragments {0, 0, location (), 0, code.size (), gl}; + return *f; + } + + // Iterate over lines (keeping track of the current line) looking + // for the separator. + // + uint64_t l (gl.line); + for (size_t b (0), e (b), n (code.size ()); b < n; b = e + 1, l++) + { + if ((e = code.find ('\n', b)) == string::npos) + e = n; + + // Trim the line. + // + size_t tb (b), te (e); + auto ws = [] (char c) {return c == ' ' || c == '\t' || c == '\r';}; + for (; tb != te && ws (code[tb ]); ++tb) ; + for (; te != tb && ws (code[te - 1]); --te) ; + + // text << "'" << string (code, tb, te - tb) << "'"; + + if (code.compare (tb, te - tb, *separator) == 0) + { + // End the global fragment at the previous newline and start the + // local fragment at the beginning of the next line. + // + location ll (loc.file, l + 1, 1); + + if (++e >= n) + fail (ll) << "empty c++ recipe local fragment"; + + f = fragments {0, b, gl, e, n - e, ll}; + return *f; + } + } + + fail (loc) << "c++ recipe fragment separator '" << *separator + << "' not found" << endf; + }; + bool nested (ctx.module_context == &ctx); // Create the build context if necessary. @@ -993,6 +1060,8 @@ namespace build2 if (create) try { + const fragments& frag (split ()); + // Write ad hoc config.build that loads the ~build2 configuration. // This way the configuration will be always in sync with ~build2 // and we can update the recipe manually (e.g., for debugging). @@ -1043,6 +1112,21 @@ namespace build2 << "#include " << '\n' << '\n'; + // Write the global fragment, if any. Note that it always includes the + // trailing newline. + // + if (frag.global_n != 0) + { + // Use the #line directive to point diagnostics to the code in the + // buildfile. Note that there is no easy way to restore things to + // point back to the source file (other than another #line with a + // line and a file). Let's not bother for now. + // + ofs << "#line RECIPE_GLOBAL_LINE RECIPE_FILE" << '\n'; + ofs.write (code.c_str () + frag.global_p, frag.global_n); + ofs << '\n'; + } + // Normally the recipe code will have one level of indentation so // let's not indent the namespace level to match. // @@ -1090,17 +1174,14 @@ namespace build2 << '\n'; // Use the #line directive to point diagnostics to the code in the - // buildfile. Note that there is no easy way to restore things to - // point back to the source file (other than another #line with a line - // and a file). Seeing that we don't have much after, let's not bother - // for now. + // buildfile similar to the global fragment above. // - ofs << "#line RECIPE_LINE RECIPE_FILE" << '\n'; + ofs << "#line RECIPE_LOCAL_LINE RECIPE_FILE" << '\n'; - // Note that the code always includes trailing newline. + // Note that the local fragment always includes the trailing newline. // - ofs << code - << "};" << '\n' + ofs.write (code.c_str () + frag.local_p, frag.local_n); + ofs << "};" << '\n' << '\n'; // Add an alias that we can use unambiguously in the load function. @@ -1193,6 +1274,8 @@ namespace build2 if (!check_sig (of, lsig)) try { + const fragments& frag (split ()); + entry_time et (file_time (of)); if (verb >= verbosity) @@ -1200,12 +1283,15 @@ namespace build2 ofs.open (of); - // Recipe file and line for the #line directive above. Note that the - // code starts from the next line thus +1. We also need to escape - // backslashes (Windows paths). + // Recipe file and line for the #line directive above. We also need + // to escape backslashes (Windows paths). // - ofs << "#define RECIPE_FILE \"" << sanitize_strlit (lf) << '"'<< '\n' - << "#define RECIPE_LINE " << loc.line + 1 << '\n' + ofs << "#define RECIPE_FILE \"" << sanitize_strlit (lf) << '"'<< '\n'; + + if (frag.global_n != 0) + ofs << "#define RECIPE_GLOBAL_LINE " << frag.global_l.line << '\n'; + + ofs << "#define RECIPE_LOCAL_LINE " << frag.local_l.line << '\n' << '\n' << lsig << '\n'; diff --git a/libbuild2/rule.hxx b/libbuild2/rule.hxx index c98c29e..aa2f003 100644 --- a/libbuild2/rule.hxx +++ b/libbuild2/rule.hxx @@ -253,7 +253,9 @@ namespace build2 virtual recipe apply (action, target&) const override; - adhoc_cxx_rule (const location&, size_t, uint64_t version); + adhoc_cxx_rule (const location&, size_t, + uint64_t ver, + optional sep); virtual bool recipe_text (context&, const target&, string&& t, attributes&) override; @@ -269,6 +271,7 @@ namespace build2 // targets which could all be matched in parallel. // uint64_t version; + optional separator; string code; mutable atomic impl; }; -- cgit v1.1