From 741cce26c1caeacc0e578a8bef1efefa993adcc1 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Wed, 13 May 2020 07:14:50 +0200 Subject: Initial support for ad hoc C++ recipes --- build2/b.cxx | 2 +- libbuild2/cc/compile-rule.cxx | 14 +- libbuild2/config/operation.cxx | 4 +- libbuild2/file.cxx | 65 +++++-- libbuild2/file.hxx | 10 +- libbuild2/module.cxx | 341 +++++++++++++++++++++---------------- libbuild2/parser.cxx | 49 ++++-- libbuild2/rule.cxx | 279 +++++++++++++++++++++++++++++- libbuild2/rule.hxx | 71 ++++++-- libbuild2/target.cxx | 4 +- libbuild2/types.hxx | 6 + libbuild2/types.ixx | 21 +++ tests/dependency/recipe/testscript | 27 +++ 13 files changed, 673 insertions(+), 220 deletions(-) diff --git a/build2/b.cxx b/build2/b.cxx index cdcfd59..fdd1b1c 100644 --- a/build2/b.cxx +++ b/build2/b.cxx @@ -1015,7 +1015,7 @@ main (int argc, char* argv[]) // use to the bootstrap files (other than src-root.build, which, // BTW, doesn't need to exist if src_root == out_root). // - scope& rs (create_root (gs, out_root, src_root)->second); + scope& rs (create_root (*ctx, out_root, src_root)->second); bool bstrapped (bootstrapped (rs)); diff --git a/libbuild2/cc/compile-rule.cxx b/libbuild2/cc/compile-rule.cxx index 8b082cc..6b9104f 100644 --- a/libbuild2/cc/compile-rule.cxx +++ b/libbuild2/cc/compile-rule.cxx @@ -5247,6 +5247,8 @@ namespace build2 dir_path compile_rule:: find_modules_sidebuild (const scope& rs) const { + context& ctx (rs.ctx); + // First figure out where we are going to build. We want to avoid // multiple sidebuilds so the outermost scope that has loaded the // cc.config module and that is within our amalgmantion seems like a @@ -5284,18 +5286,18 @@ namespace build2 modules_sidebuild_dir /= x); - const scope* ps (&rs.ctx.scopes.find (pd)); + const scope* ps (&ctx.scopes.find (pd)); if (ps->out_path () != pd) { // Switch the phase to load then create and load the subproject. // - phase_switch phs (rs.ctx, run_phase::load); + phase_switch phs (ctx, run_phase::load); // Re-test again now that we are in exclusive phase (another thread // could have already created and loaded the subproject). // - ps = &rs.ctx.scopes.find (pd); + ps = &ctx.scopes.find (pd); if (ps->out_path () != pd) { @@ -5322,15 +5324,13 @@ namespace build2 {string (x) + '.'}, /* root_modules */ "", /* root_post */ nullopt, /* config_module */ + nullopt, /* config_file */ false, /* buildfile */ "the cc module", 2); /* verbosity */ } - ps = &load_project (as->rw () /* lock */, - pd, - pd, - false /* forwarded */); + ps = &load_project (ctx, pd, pd, false /* forwarded */); } } diff --git a/libbuild2/config/operation.cxx b/libbuild2/config/operation.cxx index 17eb99a..41d982b 100644 --- a/libbuild2/config/operation.cxx +++ b/libbuild2/config/operation.cxx @@ -1103,8 +1103,7 @@ namespace build2 // this information is stored). So what we are going to do is bootstrap // the newly created project, similar to the way main() does it. // - scope& gs (ctx.global_scope.rw ()); - scope& rs (load_project (gs, d, d, false /* fwd */, false /* load */)); + scope& rs (load_project (ctx, d, d, false /* fwd */, false /* load */)); // Add the default config.config.persist value unless there is a custom // one (specified as a command line override). @@ -1223,6 +1222,7 @@ namespace build2 rmod, "", /* root_post */ string ("config"), /* config_module */ + nullopt, /* config_file */ true, /* buildfile */ "the create meta-operation"); diff --git a/libbuild2/file.cxx b/libbuild2/file.cxx index 0bcb198..571980e 100644 --- a/libbuild2/file.cxx +++ b/libbuild2/file.cxx @@ -17,7 +17,8 @@ #include #include -#include // lookup_config() +#include // config::module::version +#include // config::lookup_config() using namespace std; using namespace butl; @@ -310,13 +311,13 @@ namespace build2 } scope_map::iterator - create_root (scope& s, const dir_path& out_root, const dir_path& src_root) + create_root (context& ctx, + const dir_path& out_root, + const dir_path& src_root) { - auto i (s.ctx.scopes.rw (s).insert (out_root, true /* root */)); + auto i (ctx.scopes.rw ().insert (out_root, true /* root */)); scope& rs (i->second); - context& ctx (rs.ctx); - // Set out_path. Note that src_path is set in setup_root() below. // if (rs.out_path_ != &i->first) @@ -1208,7 +1209,7 @@ namespace build2 // probably be tried first since that src_root was explicitly configured // by the user. After that, #2 followed by #1 seems reasonable. // - scope& rs (create_root (root, out_root, dir_path ())->second); + scope& rs (create_root (ctx, out_root, dir_path ())->second); bool bstrapped (bootstrapped (rs)); @@ -1275,7 +1276,7 @@ namespace build2 // The same logic to src_root as in create_bootstrap_outer(). // - scope& rs (create_root (root, out_root, dir_path ())->second); + scope& rs (create_root (ctx, out_root, dir_path ())->second); optional altn; if (!bootstrapped (rs)) @@ -1466,17 +1467,16 @@ namespace build2 } scope& - load_project (scope& s, + load_project (context& ctx, const dir_path& out_root, const dir_path& src_root, bool forwarded, bool load) { + assert (ctx.phase == run_phase::load); assert (!forwarded || out_root != src_root); - context& ctx (s.ctx); - - auto i (create_root (s, out_root, src_root)); + auto i (create_root (ctx, out_root, src_root)); scope& rs (i->second); if (!bootstrapped (rs)) @@ -2065,13 +2065,11 @@ namespace build2 fwd = (src_root != out_root); } - scope& gs (ctx.global_scope.rw ()); - for (const scope* proot (nullptr); ; proot = root) { bool top (proot == nullptr); - root = &create_root (gs, out_root, src_root)->second; + root = &create_root (ctx, out_root, src_root)->second; bool bstrapped (bootstrapped (*root)); @@ -2153,6 +2151,8 @@ namespace build2 // load_root (*root); + scope& gs (ctx.global_scope.rw ()); + // Use a temporary scope so that the export stub doesn't mess anything up. // temp_scope ts (gs); @@ -2555,11 +2555,14 @@ namespace build2 const string& rpre, const strings& rmod, const string& rpos, - const optional& config, + const optional& config_mod, + const optional& config_file, bool buildfile, const char* who, uint16_t verbosity) { + assert (!config_file || (config_mod && *config_mod == "config")); + string hdr ("# Generated by " + string (who) + ". Edit if you know" " what you are doing.\n" "#"); @@ -2610,12 +2613,12 @@ namespace build2 ofs << endl; - if (config) - ofs << "using " << *config << endl; + if (config_mod) + ofs << "using " << *config_mod << endl; for (const string& m: bmod) { - if (!config || m != *config) + if (!config_mod || m != *config_mod) ofs << "using " << m << endl; } @@ -2675,6 +2678,32 @@ namespace build2 } } + // Write build/config.build. + // + if (config_file) + { + path f (d / std_build_dir / "config.build"); // std_config_file + + if (verb >= verbosity) + text << (verb >= 2 ? "cat >" : "save ") << f; + + try + { + ofdstream ofs (f); + + ofs << hdr << endl + << "config.version = " << config::module::version << endl + << endl + << *config_file << endl; + + ofs.close (); + } + catch (const io_error& e) + { + fail << "unable to write to " << f << ": " << e; + } + } + // Write root buildfile. // if (buildfile) diff --git a/libbuild2/file.hxx b/libbuild2/file.hxx index 0123591..78be600 100644 --- a/libbuild2/file.hxx +++ b/libbuild2/file.hxx @@ -106,11 +106,10 @@ namespace build2 source_once (scope& root, scope& base, const path&, scope& once); // Create project's root scope. Only set the src_root variable if the passed - // src_root value is not empty. The scope argument is only used for context - // and as a proof of lock. + // src_root value is not empty. // LIBBUILD2_SYMEXPORT scope_map::iterator - create_root (scope&, const dir_path& out_root, const dir_path& src_root); + create_root (context&, const dir_path& out_root, const dir_path& src_root); // Setup root scope. Note that it assumes the src_root variable has already // been set. @@ -142,10 +141,8 @@ namespace build2 // loaded and currently we do not add the newly loaded subproject to the // outer project's subprojects map. // - // The scope argument is only used as proof of lock. - // LIBBUILD2_SYMEXPORT scope& - load_project (scope&, + load_project (context&, const dir_path& out_root, const dir_path& src_root, bool forwarded, @@ -441,6 +438,7 @@ namespace build2 const strings& root_modules, // Root modules. const string& root_post, // Extra root.build text. const optional& config_module, // Config module to load. + const optional& config_file, // Ad hoc config.build contents. bool buildfile, // Create root buildfile. const char* who, // Who is creating it. uint16_t verbosity = 1); // Diagnostic verbosity. diff --git a/libbuild2/module.cxx b/libbuild2/module.cxx index 3abb102..4972d7b 100644 --- a/libbuild2/module.cxx +++ b/libbuild2/module.cxx @@ -63,6 +63,177 @@ namespace build2 mod); } + // Note: also used by ad hoc recipes thus not static. + // + void + create_module_context (context& ctx, const location& loc, const char* what) + { + assert (ctx.module_context == nullptr); + + if (!ctx.module_context_storage) + fail (loc) << "unable to update " << what << + info << "updating of " << what << "s is disabled"; + + assert (*ctx.module_context_storage == nullptr); + + // Since we are using the same scheduler, it makes sense to reuse the + // same global mutexes. Also disable nested module context for good + // measure. + // + ctx.module_context_storage->reset ( + new context (ctx.sched, + ctx.mutexes, + false, /* match_only */ + false, /* dry_run */ + ctx.keep_going, + ctx.global_var_overrides, /* cmd_vars */ + nullopt)); /* module_context */ + + // We use the same context for building any nested modules that might be + // required while building modules. + // + ctx.module_context = ctx.module_context_storage->get (); + ctx.module_context->module_context = ctx.module_context; + + // Setup the context to perform update. In a sense we have a long-running + // perform meta-operation batch (indefinite, in fact, since we never call + // the meta-operation's *_post() callbacks) in which we periodically + // execute the update operation. + // + if (mo_perform.meta_operation_pre != nullptr) + mo_perform.meta_operation_pre ({} /* parameters */, loc); + + ctx.module_context->current_meta_operation (mo_perform); + + if (mo_perform.operation_pre != nullptr) + mo_perform.operation_pre ({} /* parameters */, update_id); + + ctx.module_context->current_operation (op_update); + } + + // Note: also used by ad hoc recipes thus not static. + // + const target& + update_in_module_context (context& ctx, const scope& rs, names tgt, + const location& loc, const path& bf, + const char* what, const char* name) + { + action_targets tgs; + { + action a (perform_id, update_id); + + // Cutoff the existing diagnostics stack and push our own entry. + // + diag_frame::stack_guard diag_cutoff (nullptr); + + auto df = make_diag_frame ( + [&loc, what, name] (const diag_record& dr) + { + dr << info (loc) << "while " << what; + + if (name != nullptr) + dr << ' ' << name; + }); + + // Un-tune the scheduler. + // + // Note that we can only do this if we are running serially because + // otherwise we cannot guarantee the scheduler is idle (we could have + // waiting threads from the outer context). This is fine for now since + // the only two tuning level we use are serial and full concurrency + // (turns out currently we don't really need this: we will always be + // called during load or match phases and we always do parallel match; + // but let's keep it in case things change). + // + auto sched_tune (ctx.sched.serial () + ? scheduler::tune_guard (ctx.sched, 0) + : scheduler::tune_guard ()); + + // Remap verbosity level 0 to 1 unless we were requested to be silent. + // Failed that, we may have long periods of seemingly nothing happening + // while we quietly update the module, which may look like things have + // hung up. + // + // @@ CTX: modifying global verbosity level won't work if we have + // multiple top-level contexts running in parallel. + // + auto verbg = make_guard ( + [z = !silent && verb == 0 ? (verb = 1, true) : false] () + { + if (z) + verb = 0; + }); + + // Note that for now we suppress progress since it would clash with + // the progress of what we are already doing (maybe in the future we + // can do save/restore but then we would need some sort of + // diagnostics that we have switched to another task). + // + mo_perform.search ({}, /* parameters */ + rs, /* root scope */ + rs, /* base scope */ + bf, /* buildfile */ + rs.find_target_key (tgt, loc), + loc, + tgs); + + mo_perform.match ({}, /* parameters */ + a, + tgs, + 1, /* diag (failures only) */ + false /* progress */); + + mo_perform.execute ({}, /* parameters */ + a, + tgs, + 1, /* diag (failures only) */ + false /* progress */); + } + + assert (tgs.size () == 1); + return tgs[0].as (); + } + + // Note: also used by ad hoc recipes thus not static. + // + pair + load_module_library (const path& lib, const string& sym, string& err) + { + // Note that we don't unload our modules since it's not clear what would + // the benefit be. + // + void* h (nullptr); + void* s (nullptr); + +#ifndef _WIN32 + // Use RTLD_NOW instead of RTLD_LAZY to both speed things up (we are going + // to use this module now) and to detect any symbol mismatches. + // + if ((h = dlopen (lib.string ().c_str (), RTLD_NOW | RTLD_GLOBAL))) + { + s = dlsym (h, sym.c_str ()); + + if (s == nullptr) + err = dlerror (); + } + else + err = dlerror (); +#else + if (HMODULE m = LoadLibrary (lib.string ().c_str ())) + { + h = static_cast (m); + s = function_cast (GetProcAddress (m, sym.c_str ())); + + if (s == nullptr) + err = win32::last_error_msg (); + } + else + err = win32::last_error_msg (); +#endif + + return make_pair (h, s); + } + static module_load_function* import_module (scope& bs, const string& mod, @@ -177,47 +348,7 @@ namespace build2 // Create the build context if necessary. // if (ctx.module_context == nullptr) - { - if (!ctx.module_context_storage) - fail (loc) << "unable to update build system module " << mod << - info << "updating of build system modules is disabled"; - - assert (*ctx.module_context_storage == nullptr); - - // Since we are using the same scheduler, it makes sense to reuse the - // same global mutexes. Also disable nested module context for good - // measure. - // - ctx.module_context_storage->reset ( - new context (ctx.sched, - ctx.mutexes, - false, /* match_only */ - false, /* dry_run */ - ctx.keep_going, - ctx.global_var_overrides, /* cmd_vars */ - nullopt)); /* module_context */ - - // We use the same context for building any nested modules that - // might be required while building modules. - // - ctx.module_context = ctx.module_context_storage->get (); - ctx.module_context->module_context = ctx.module_context; - - // Setup the context to perform update. In a sense we have a long- - // running perform meta-operation batch (indefinite, in fact, since we - // never call the meta-operation's *_post() callbacks) in which we - // periodically execute the update operation. - // - if (mo_perform.meta_operation_pre != nullptr) - mo_perform.meta_operation_pre ({} /* parameters */, loc); - - ctx.module_context->current_meta_operation (mo_perform); - - if (mo_perform.operation_pre != nullptr) - mo_perform.operation_pre ({} /* parameters */, update_id); - - ctx.module_context->current_operation (op_update); - } + create_module_context (ctx, loc, "build system module"); // Inherit loaded_modules lock from the outer context. // @@ -234,7 +365,7 @@ namespace build2 l5 ([&]{trace << "loaded " << lr.first;}); - // When happens next depends on whether this is a top-level or nested + // What happens next depends on whether this is a top-level or nested // module update. // if (nested) @@ -249,77 +380,10 @@ namespace build2 { const scope& rs (lr.second); - action_targets tgs; - action a (perform_id, update_id); - - { - // Cutoff the existing diagnostics stack and push our own entry. - // - diag_frame::stack_guard diag_cutoff (nullptr); - - auto df = make_diag_frame ( - [&loc, &mod] (const diag_record& dr) - { - dr << info (loc) << "while loading build system module " << mod; - }); - - // Un-tune the scheduler. - // - // Note that we can only do this if we are running serially because - // otherwise we cannot guarantee the scheduler is idle (we could - // have waiting threads from the outer context). This is fine for - // now since the only two tuning level we use are serial and full - // concurrency (turns out currently we don't really need this: we - // will always be called during load or match phases and we always - // do parallel match; but let's keep it in case things change). - // - auto sched_tune (ctx.sched.serial () - ? scheduler::tune_guard (ctx.sched, 0) - : scheduler::tune_guard ()); - - // Remap verbosity level 0 to 1 unless we were requested to be - // silent. Failed that, we may have long periods of seemingly - // nothing happening while we quietly update the module, which - // may look like things have hung up. - // - // @@ CTX: modifying global verbosity level won't work if we have - // multiple top-level contexts running in parallel. - // - auto verbg = make_guard ( - [z = !silent && verb == 0 ? (verb = 1, true) : false] () - { - if (z) - verb = 0; - }); - - // Note that for now we suppress progress since it would clash with - // the progress of what we are already doing (maybe in the future we - // can do save/restore but then we would need some sort of - // diagnostics that we have switched to another task). - // - mo_perform.search ({}, /* parameters */ - rs, /* root scope */ - rs, /* base scope */ - path (), /* buildfile */ - rs.find_target_key (lr.first, loc), - loc, - tgs); - - mo_perform.match ({}, /* parameters */ - a, - tgs, - 1, /* diag (failures only) */ - false /* progress */); - - mo_perform.execute ({}, /* parameters */ - a, - tgs, - 1, /* diag (failures only) */ - false /* progress */); - } - - assert (tgs.size () == 1); - const target& l (tgs[0].as ()); + const target& l ( + update_in_module_context ( + ctx, rs, move (lr.first), + loc, path (), "loading build system module", mod.c_str ())); if (!l.is_a ("libs")) fail (loc) << "wrong export from build system module " << mod; @@ -364,53 +428,30 @@ namespace build2 // string sym (sanitize_identifier ("build2_" + mod + "_load")); - // Note that we don't unload our modules since it's not clear what would - // the benefit be. - // - diag_record dr; + string err; + pair hs (load_module_library (lib, sym, err)); -#ifndef _WIN32 - // Use RTLD_NOW instead of RTLD_LAZY to both speed things up (we are going - // to use this module now) and to detect any symbol mismatches. - // - if (void* h = dlopen (lib.string ().c_str (), RTLD_NOW | RTLD_GLOBAL)) + if (hs.first != nullptr) { - r = function_cast (dlsym (h, sym.c_str ())); - // I don't think we should ignore this even if the module is optional. // - if (r == nullptr) + if (hs.second == nullptr) fail (loc) << "unable to lookup " << sym << " in build system module " - << mod << " (" << lib << "): " << dlerror (); + << mod << " (" << lib << "): " << err; + + r = function_cast (hs.second); } else if (!opt) - dr << fail (loc) << "unable to load build system module " << mod - << " (" << lib << "): " << dlerror (); - else - l5 ([&]{trace << "unable to load " << lib << ": " << dlerror ();}); -#else - if (HMODULE h = LoadLibrary (lib.string ().c_str ())) { - r = function_cast ( - GetProcAddress (h, sym.c_str ())); - - if (r == nullptr) - fail (loc) << "unable to lookup " << sym << " in build system module " - << mod << " (" << lib << "): " << win32::last_error_msg (); + // Add import suggestion similar to import phase 2. + // + fail (loc) << "unable to load build system module " << mod << " (" + << lib << "): " << err << + info << "use config.import." << proj.variable () << " command " + << "line variable to specify its project out_root"; } - else if (!opt) - dr << fail (loc) << "unable to load build system module " << mod - << " (" << lib << "): " << win32::last_error_msg (); else - l5 ([&]{trace << "unable to load " << lib << ": " - << win32::last_error_msg ();}); -#endif - - // Add a suggestion similar to import phase 2. - // - if (!dr.empty ()) - dr << info << "use config.import." << proj.variable () << " command " - << "line variable to specify its project out_root" << endf; + l5 ([&]{trace << "unable to load " << lib << ": " << err;}); #endif // BUILD2_BOOTSTRAP diff --git a/libbuild2/parser.cxx b/libbuild2/parser.cxx index 54c2065..db95297 100644 --- a/libbuild2/parser.cxx +++ b/libbuild2/parser.cxx @@ -1017,13 +1017,12 @@ namespace build2 // Parse a recipe chain. // // % [] - // {{ + // {{ [] // ... // }} // // enter: percent or openining multi-curly-brace // leave: token past newline after last closing multi-curly-brace - // // If we have a recipe, the target is not implied. // @@ -1045,15 +1044,17 @@ namespace build2 next_with_attributes (t, tt); attributes_push (t, tt, true /* standalone */); - // Get variable (or value) attributes, if any, and deal with the special - // metadata attribute. Since currently it can only appear in the import - // directive, we handle it in an ad hoc manner. + // Get variable (or value) attributes, if any, and deal with the + // special metadata attribute. Since currently it can only appear in + // the import directive, we handle it in an ad hoc manner. // attributes& as (attributes_top ()); for (attribute& a: as) { const string& n (a.name); + // @@ TODO: diag is script-specific, pass as attributes to rule? + // if (n == "diag") { try @@ -1079,7 +1080,19 @@ namespace build2 st = t; // And fall through. } - next (t, tt); // Newline after {{. + optional lang; + location lloc; + if (next (t, tt) == type::newline) + ; + else if (tt == type::word) + { + lang = t.value; + lloc = get_location (t); + next (t, tt); // Newline after . + } + else + fail (t) << "expected recipe language instead of " << t; + mode (lexer_mode::foreign, '\0', st.value.size ()); next_after_newline (t, tt, st); // Should be on its own line. @@ -1090,14 +1103,26 @@ namespace build2 // @@ TODO: we need to reuse the same rules for all the targets! Kill // me now. // - shared_ptr ar ( - new adhoc_script_rule (move (t.value), - move (diag), - get_location (st), - st.value.size ())); + shared_ptr ar; - action a (perform_id, update_id); + // Note that this is always the location of the opening multi-curly- + // brace, whether we have the header or not. This is relied upon by the + // rule implementations (e.g., to calculate the first line of the recipe + // code). + // + location loc (get_location (st)); + + if (!lang) + ar.reset (new adhoc_script_rule (move (t.value), + move (diag), + loc, + st.value.size ())); + else if (*lang == "c++") + ar.reset (new adhoc_cxx_rule (move (t.value), loc, st.value.size ())); + else + fail (lloc) << "unknown recipe language '" << *lang << "'"; + action a (perform_id, update_id); target_->adhoc_recipes.push_back (adhoc_recipe {a, move (ar)}); next (t, tt); diff --git a/libbuild2/rule.cxx b/libbuild2/rule.cxx index d645ec3..98b4458 100644 --- a/libbuild2/rule.cxx +++ b/libbuild2/rule.cxx @@ -3,6 +3,7 @@ #include +#include #include #include #include @@ -342,7 +343,7 @@ namespace build2 } os << ind << string (braces, '{') << endl - << ind << script + << ind << code << ind << string (braces, '}'); } @@ -466,7 +467,7 @@ namespace build2 // It feels like we need a special execute mode that instead // of executing hashes the commands. // - if (dd.expect (sha256 (script).string ()) != nullptr) + if (dd.expect (sha256 (code).string ()) != nullptr) l4 ([&]{trace << "recipe change forcing update of " << t;}); } @@ -488,7 +489,7 @@ namespace build2 //print_process (args); - text << trim (string (script)); + text << trim (string (code)); } else if (verb) { @@ -532,7 +533,7 @@ namespace build2 //print_process (args); - text << trim (string (script)); + text << trim (string (code)); } else if (verb) { @@ -549,4 +550,274 @@ namespace build2 return target_state::changed; } + + // cxx_rule + // + bool cxx_rule:: + match (action, target&, const string&) const + { + return true; + } + + // adhoc_cxx_rule + // + void adhoc_cxx_rule:: + dump (ostream& os, const string& ind) const + { + // @@ TODO: indentation is multi-line recipes is off (would need to insert + // indentation after every newline). + // + os << ind << string (braces, '{') << " c++" << endl + << ind << code + << ind << string (braces, '}'); + } + + static const dir_path recipes_build_dir ("recipes.out"); + + // From module.cxx. + // + void + create_module_context (context&, const location&, const char* what); + + const target& + update_in_module_context (context&, const scope&, names tgt, + const location&, const path& bf, + const char* what, const char* name); + + pair + load_module_library (const path& lib, const string& sym, string& err); + + bool adhoc_cxx_rule:: + match (action a, target& t, const string& hint) const + { + tracer trace ("adhoc_cxx_rule::match"); + + context& ctx (t.ctx); + const scope& rs (t.root_scope ()); + + // The plan is to reduce this to the build system module case as much as + // possible. Specifically, we switch to the load phase, create a module- + // like library with the recipe text as a rule implementation, then build + // and load it. + // + // Since the recipe can be shared among multiple targets, several threads + // can all be trying to do this in parallel. + + // The only way to guarantee that the name of our module matches its + // implementation is to based the name on the implementation hash. + // + // Unfortunately, this means we will be creating a new project (and + // leaving behind the old one as garbage) for every change to the + // recipe. On the other hand, if the recipe is moved around unchanged, we + // will reuse the same project. In fact, two different recipes (e.g., in + // different buildfiles) with the same text will share the project. + // + // @@ Shouldn't we also include buildfile path and line seeing that we + // add them as #line? Or can we do something clever for this case + // (i.e., if update is successful, then this information is no longer + // necessary, unless update is caused by something external, like + // change of compiler). Also location in comment. Why not just + // overwrite the source file every time we compile it, to KISS? + // + string id (sha256 (code).abbreviated_string (12)); + + // @@ TODO: locking. + // @@ Need to unlock phase while waiting. + if (impl == nullptr) + { + dir_path pd (rs.out_path () / + rs.root_extra->build_dir / + recipes_build_dir /= id); + + string sym ("load_" + id); + + // Switch the phase to load. + // + phase_switch ps (ctx, run_phase::load); + + optional altn (false); // Standard naming scheme. + if (!is_src_root (pd, altn)) + { + const uint16_t verbosity (3); + + // 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). + // + create_project ( + pd, + dir_path (), /* amalgamation */ + {}, /* boot_modules */ + "cxx.std = latest", /* root_pre */ + {"cxx."}, /* root_modules */ + "", /* root_post */ + string ("config"), /* config_module */ + string ("config.config.load = ~build2"), /* config_file */ + false, /* buildfile */ + "build2 core", /* who */ + verbosity); /* verbosity */ + + path f; + + try + { + ofdstream ofs; + + // Write source file. + // + f = path (pd / "rule.cxx"); + + if (verb >= verbosity) + text << (verb >= 2 ? "cat >" : "save ") << f; + + ofs.open (f); + + ofs << "// " << loc << endl + << endl; + + // Include every header that can plausibly be needed by a rule. + // + ofs << "#include " << '\n' + << "#include " << '\n' + << "#include " << '\n' + << '\n' + << "#include " << '\n' + << "#include " << '\n' + << "#include " << '\n' + << "#include " << '\n' + << "#include " << '\n' + << "#include " << '\n' + << "#include " << '\n' + << "#include " << '\n' + << "#include " << '\n' + << "#include " << '\n' + << '\n'; + + // Normally the recipe code will have one level of indentation so + // let's not indent the namespace level to match. + // + ofs << "namespace build2" << '\n' + << "{" << '\n' + << "class rule_" << id << ": public cxx_rule" << '\n' + << "{" << '\n' + << "public:" << '\n'; + + // Inherit base constructor. This way the user may provide their + // own but don't have to. + // + ofs << " using cxx_rule::cxx_rule;" << '\n' + << '\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. Note that the code start from the next line thus + // +1. + // + // @@ TODO: need to escape backslashes in path. + // + if (!loc.file.path.empty ()) + ofs << "#line " << loc.line + 1 << " \"" << + loc.file.path.string () << '"' << '\n'; + + // Note that the code always includes trailing newline. + // + ofs << code + << "};" << '\n' + << "}" << '\n' + << '\n'; + + ofs << "extern \"C\"" << '\n' + << "#ifdef _WIN32" << '\n' + << "__declspec(dllexport)" << '\n' + << "#endif" << '\n' + << "build2::cxx_rule*" << '\n' + << sym << " (const build2::location* l)" << '\n' + << "{" << '\n' + << "return new build2::rule_" << id << " (*l);" << '\n' + << "}" << '\n'; + + ofs.close (); + + // Write buildfile. + // + f = path (pd / std_buildfile_file); + + if (verb >= verbosity) + text << (verb >= 2 ? "cat >" : "save ") << f; + + ofs.open (f); + + ofs << "import imp_libs += build2%lib{build2}" << '\n' + << "libs{" << id << "}: cxx{rule} $imp_libs" << '\n'; + + ofs.close (); + } + catch (const io_error& e) + { + fail << "unable to write to " << f << ": " << e; + } + } + + const target* l; + { + bool nested (ctx.module_context == &ctx); + + // Create the build context if necessary. + // + if (ctx.module_context == nullptr) + create_module_context (ctx, loc, "ad hoc recipe"); + + // "Switch" to the module context. + // + context& ctx (*t.ctx.module_context); + + // Load the project in the module context. + // + path bf (pd / std_buildfile_file); + scope& rs (load_project (ctx, pd, pd, false /* forwarded */)); + source (rs, rs, bf); + + if (nested) + { + // @@ TODO: we probably want to make this work. + + fail (loc) << "nested ad hoc recipe updates not yet supported" << endf; + } + else + { + l = &update_in_module_context ( + ctx, rs, names {name (pd, "libs", id)}, + loc, bf, "updating ad hoc recipe", nullptr); + } + } + + const path& lib (l->as ().path ()); + + string err; + pair hs (load_module_library (lib, sym, err)); + + if (hs.first == nullptr) + fail (loc) << "unable to load recipe library " << lib << ": " << err; + + if (hs.second == nullptr) + fail (loc) << "unable to lookup " << sym << " in recipe library " + << lib << ": " << err; + + // @@ TODO: this function cannot throw (extern C). + // + auto f (function_cast (hs.second)); + + impl.reset (f (&loc)); + } + + return impl->match (a, t, hint); + } + + recipe adhoc_cxx_rule:: + apply (action a, target& t) const + { + return impl->apply (a, t); + } } diff --git a/libbuild2/rule.hxx b/libbuild2/rule.hxx index 39f89fa..69be2cc 100644 --- a/libbuild2/rule.hxx +++ b/libbuild2/rule.hxx @@ -114,22 +114,14 @@ namespace build2 class LIBBUILD2_SYMEXPORT adhoc_rule: rule { public: - using location_type = build2::location; + location_value loc; // Buildfile location of the recipe. + size_t braces; // Number of braces in multi-brace tokens. - // Diagnostics-related information. - // - path_name_value buildfile; // Buildfile of recipe. - location_type location; // Buildfile location of recipe. - size_t braces; // Number of braces in multi-brace tokens. - - build2::rule_match rule_match; - - adhoc_rule (const location_type& l, size_t b) - : buildfile (l.file), location (buildfile, l.line, l.column), braces (b), - rule_match ("adhoc", *this) {} + adhoc_rule (const location& l, size_t b) + : loc (l), braces (b), rule_match ("adhoc", *this) {} public: - // Some of the operations come in compensating pairs, sush as update and + // Some of the operations come in compensating pairs, such as update and // clean, install and uninstall. An ad hoc rule implementation may choose // to provide a fallback implementation of a compensating operation if it // is providing the other half (passed in the fallback argument). @@ -147,6 +139,11 @@ namespace build2 virtual void dump (ostream&, const string& indentation) const = 0; + + // Implementation details. + // + public: + build2::rule_match rule_match; }; // Ad hoc script rule. @@ -171,14 +168,52 @@ namespace build2 virtual void dump (ostream&, const string&) const override; - adhoc_script_rule (string s, + adhoc_script_rule (string c, optional d, - const location_type& l, size_t b) - : adhoc_rule (l, b), script (move (s)), diag (move (d)) {} + const location& l, size_t b) + : adhoc_rule (l, b), code (move (c)), diag (move (d)) {} + + public: + string code; + optional diag; // Command name for low-verbosity diagnostics. + }; + + // Ad hoc C++ rule. + // + // Note: should not be used directly (i.e., registered). + // + class LIBBUILD2_SYMEXPORT cxx_rule: public rule + { + public: + const location loc; // Buildfile location of the recipe. + + explicit + cxx_rule (const location& l): loc (l) {} + + // Return true by default. + // + virtual bool + match (action, target&, const string&) const override; + }; + + class LIBBUILD2_SYMEXPORT adhoc_cxx_rule: public adhoc_rule + { + public: + virtual bool + match (action, target&, const string&) const override; + + virtual recipe + apply (action, target&) const override; + + virtual void + dump (ostream&, const string&) const override; + + adhoc_cxx_rule (string c, const location& l, size_t b) + : adhoc_rule (l, b), code (move (c)) {} public: - string script; - optional diag; // Command name for low-verbosity diagnostics. + string code; + mutable unique_ptr impl; }; } diff --git a/libbuild2/target.cxx b/libbuild2/target.cxx index 83ed4a5..dfd0075 100644 --- a/libbuild2/target.cxx +++ b/libbuild2/target.cxx @@ -964,8 +964,8 @@ namespace build2 phase_switch ps (t.ctx, run_phase::load); // This is subtle: while we were fussing around another thread may - // have loaded the buildfile. So re-test now that we are in exclusive - // phase. + // have loaded the buildfile. So re-test now that we are in an + // exclusive phase. // if (e == nullptr) e = search_existing_target (t.ctx, pk); diff --git a/libbuild2/types.hxx b/libbuild2/types.hxx index d20fa22..d9f222b 100644 --- a/libbuild2/types.hxx +++ b/libbuild2/types.hxx @@ -347,6 +347,12 @@ namespace build2 location (uint64_t l, uint64_t c): line (l), column (c) {} }; + // Print in the :: form with 0 lines/columns not + // printed. Nothing is printed for an empty location. + // + ostream& + operator<< (ostream&, const location&); + // Similar (and implicit-convertible) to the above but stores a copy of the // path. // diff --git a/libbuild2/types.ixx b/libbuild2/types.ixx index c770842..750c8c7 100644 --- a/libbuild2/types.ixx +++ b/libbuild2/types.ixx @@ -3,6 +3,27 @@ namespace build2 { + // location + // + inline ostream& + operator<< (ostream& o, const location& l) + { + if (!l.empty ()) + { + o << l.file; + + if (l.line != 0) + { + o << ':' << l.line; + + if (l.column != 0) + o << ':' << l.column; + } + } + + return o; + } + // Note that in the constructors we cannot pass the file data member to the // base class constructor as it is not initialized yet (and so its base // path/name pointers are not initialized). Thus, we initialize the path diff --git a/tests/dependency/recipe/testscript b/tests/dependency/recipe/testscript index 6cb4711..503ad7e 100644 --- a/tests/dependency/recipe/testscript +++ b/tests/dependency/recipe/testscript @@ -69,6 +69,22 @@ EOI }} EOE +: basics-lang +: +$* <>/~%EOE% +alias{x}: +{{ c++ + void f (); +}} +dump alias{x} +EOI +:5:1: dump: +% .+/alias\{x\}:% + {{ c++ + void f (); + }} +EOE + : with-vars : $* <>/~%EOE% @@ -242,6 +258,17 @@ EOI :2:1: info: recipe block starts here EOE +: expected-lang +: +$* <>EOE != 0 +alias{x}: +{{ $lang + cmd +}} +EOI +:2:4: error: expected recipe language instead of '$' +EOE + : header-attribute : $* <>/~!EOE! -- cgit v1.1