aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2020-05-13 07:14:50 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2020-05-27 08:35:29 +0200
commit741cce26c1caeacc0e578a8bef1efefa993adcc1 (patch)
treebe3dcd6db8c1fea92ada21a6fa8f88fd53047aec
parent9a0f07035b34a356ce9b5601d71d388595762184 (diff)
Initial support for ad hoc C++ recipes
-rw-r--r--build2/b.cxx2
-rw-r--r--libbuild2/cc/compile-rule.cxx14
-rw-r--r--libbuild2/config/operation.cxx4
-rw-r--r--libbuild2/file.cxx65
-rw-r--r--libbuild2/file.hxx10
-rw-r--r--libbuild2/module.cxx341
-rw-r--r--libbuild2/parser.cxx49
-rw-r--r--libbuild2/rule.cxx279
-rw-r--r--libbuild2/rule.hxx71
-rw-r--r--libbuild2/target.cxx4
-rw-r--r--libbuild2/types.hxx6
-rw-r--r--libbuild2/types.ixx21
-rw-r--r--tests/dependency/recipe/testscript27
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 <libbuild2/lexer.hxx>
#include <libbuild2/parser.hxx>
-#include <libbuild2/config/utility.hxx> // lookup_config()
+#include <libbuild2/config/module.hxx> // config::module::version
+#include <libbuild2/config/utility.hxx> // 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<bool> 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<string>& config,
+ const optional<string>& config_mod,
+ const optional<string>& 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<string>& config_module, // Config module to load.
+ const optional<string>& 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<target> ();
+ }
+
+ // Note: also used by ad hoc recipes thus not static.
+ //
+ pair<void* /* handle */, void* /* symbol */>
+ 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<void*> (m);
+ s = function_cast<void*> (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<target> ());
+ 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<void*, void*> 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<module_load_function*> (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<module_load_function*> (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<module_load_function*> (
- 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.
//
// % [<attrs>]
- // {{
+ // {{ [<lang>]
// ...
// }}
//
// 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<string> 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 <lang>.
+ }
+ 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<adhoc_rule> ar (
- new adhoc_script_rule (move (t.value),
- move (diag),
- get_location (st),
- st.value.size ()));
+ shared_ptr<adhoc_rule> 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 <libbuild2/rule.hxx>
+#include <libbuild2/file.hxx>
#include <libbuild2/depdb.hxx>
#include <libbuild2/scope.hxx>
#include <libbuild2/target.hxx>
@@ -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<void*, void*>
+ 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<bool> 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 <libbuild2/types.hxx>" << '\n'
+ << "#include <libbuild2/forward.hxx>" << '\n'
+ << "#include <libbuild2/utility.hxx>" << '\n'
+ << '\n'
+ << "#include <libbuild2/file.hxx>" << '\n'
+ << "#include <libbuild2/rule.hxx>" << '\n'
+ << "#include <libbuild2/depdb.hxx>" << '\n'
+ << "#include <libbuild2/scope.hxx>" << '\n'
+ << "#include <libbuild2/target.hxx>" << '\n'
+ << "#include <libbuild2/context.hxx>" << '\n'
+ << "#include <libbuild2/variable.hxx>" << '\n'
+ << "#include <libbuild2/algorithm.hxx>" << '\n'
+ << "#include <libbuild2/filesystem.hxx>" << '\n'
+ << "#include <libbuild2/diagnostics.hxx>" << '\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<file> ().path ());
+
+ string err;
+ pair<void*, void*> 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<cxx_rule* (*) (const location*)> (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<string> 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<string> 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<string> diag; // Command name for low-verbosity diagnostics.
+ string code;
+ mutable unique_ptr<cxx_rule> 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 <file>:<line>:<column> 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
+:
+$* <<EOI 2>>/~%EOE%
+alias{x}:
+{{ c++
+ void f ();
+}}
+dump alias{x}
+EOI
+<stdin>:5:1: dump:
+% .+/alias\{x\}:%
+ {{ c++
+ void f ();
+ }}
+EOE
+
: with-vars
:
$* <<EOI 2>>/~%EOE%
@@ -242,6 +258,17 @@ EOI
<stdin>:2:1: info: recipe block starts here
EOE
+: expected-lang
+:
+$* <<EOI 2>>EOE != 0
+alias{x}:
+{{ $lang
+ cmd
+}}
+EOI
+<stdin>:2:4: error: expected recipe language instead of '$'
+EOE
+
: header-attribute
:
$* <<EOI 2>>/~!EOE!