diff options
author | Boris Kolpackov <boris@codesynthesis.com> | 2020-04-27 09:49:45 +0200 |
---|---|---|
committer | Boris Kolpackov <boris@codesynthesis.com> | 2020-04-27 10:03:50 +0200 |
commit | 9e5750ae2e3f837f80860aaab6b01e4d556213ed (patch) | |
tree | d3b2e551e444c47b6ce0289969e78360161b6685 | |
parent | 028e10ba787a7dbb46e3fcba6f88f496b76cebc5 (diff) |
Rework tool importation along with cli module
Specifically, now config.<tool> (like config.cli) is handled by the import
machinery (it is like a shorter alias for config.import.<tool>.<tool>.exe
that we already had). And the cli module now uses that instead of custom
logic.
This also adds support for uniform tool metadata extraction that is handled by
the import machinery. As a result, a tool that follows the "build2 way" can be
imported with metadata by the buildfile and/or corresponding module without
any tool-specific code or brittleness associated with parsing --version or
similar outputs. See the cli tool/module for details.
Finally, two new flavors of the import directive are now supported: import!
triggers immediate importation skipping any rule-specific logic while import?
is optional import (analogous to using?). Note that optional import is always
immediate. There is also the import-specific metadata attribute which can be
specified for these two import flavors in order to trigger metadata
importation. For example:
import? [metadata] cli = cli%exe{cli}
if ($cli != [null])
info "cli version $($cli:cli.version)"
38 files changed, 1624 insertions, 595 deletions
diff --git a/build2/cli/init.cxx b/build2/cli/init.cxx index c68a62f..8f9df4a 100644 --- a/build2/cli/init.cxx +++ b/build2/cli/init.cxx @@ -3,6 +3,7 @@ #include <build2/cli/init.hxx> +#include <libbuild2/file.hxx> #include <libbuild2/scope.hxx> #include <libbuild2/target.hxx> #include <libbuild2/variable.hxx> @@ -12,337 +13,246 @@ #include <libbuild2/cxx/target.hxx> -#include <build2/cli/target.hxx> #include <build2/cli/rule.hxx> - -using namespace std; -using namespace butl; +#include <build2/cli/module.hxx> +#include <build2/cli/target.hxx> namespace build2 { namespace cli { - static const compile_rule compile_rule_; + // Remaining issues/semantics change: + // + // @@ Unconfigured caching. + // + // @@ Default-found cli used to result in config.cli=cli and now it's just + // omitted (and default-not-found -- in config.cli.configured=false). + // + // - Writing any default will take precedence over config.import.cli. + // In fact, this duality is a bigger problem: if we have a config + // that uses config.cli there is no way to reconfigure it to use + // config.import.cli. + // + // - We could have saved it commented. + // + // - We could do this at the module level only since we also have + // config.cli.options? + // + // - Note that in the CLI compiler itself we now rely on default cli + // being NULL/undefined. So if faving, should probably be commented + // out. BUT: it will still be defined, so will need to be defined + // NULL. Note also that long term the CLI compiler will not use the + // module relying on an ad hoc recipe instead. + // + // ! Maybe reserving NULL (instead of making it the same as NULL) for + // this "configured to default" state and saving commented is not a + // bad idea. Feels right to have some marker in config.build that + // things are in effect. And I believe if config.import.cli is + // specified, it will just be dropped. bool - config_init (scope& rs, - scope& bs, - const location& l, - bool first, - bool optional, - module_init_extra&) + guess_init (scope& rs, + scope& bs, + const location& loc, + bool, + bool optional, + module_init_extra& extra) { - tracer trace ("cli::config_init"); - l5 ([&]{trace << "for " << bs;}); + tracer trace ("cli::guess_init"); + l5 ([&]{trace << "for " << rs;}); - // Enter variables. + // We only support root loading (which means there can only be one). // - if (first) - { - auto& vp (rs.var_pool ()); - - // The special config.cli=false value is recognized as an explicit - // request to leave the module unconfigured. - // - vp.insert<path> ("config.cli"); - vp.insert<strings> ("config.cli.options"); + if (rs != bs) + fail (loc) << "cli.guess module must be loaded in project root"; - //@@ TODO: split version into componets (it is stdver). - // - vp.insert<process_path> ("cli.path"); - vp.insert<string> ("cli.version"); - vp.insert<string> ("cli.checksum"); - vp.insert<strings> ("cli.options"); - } - - // Configuration. + // Adjust module config.build save priority (code generator). // - // The plan is as follows: try to configure the module. If this fails, - // we are using default values, and the module is optional, leave it - // unconfigured. + config::save_module (rs, "cli", 150); + + // Enter metadata variables. // - using config::lookup_config; - using config::specified_config; + auto& vp (rs.var_pool ()); + auto& v_ver (vp.insert<string> ("cli.version")); + auto& v_sum (vp.insert<string> ("cli.checksum")); - // First take care of the explicit request by the user to leave the + // Import the CLI compiler target. + // + // Note that the special config.cli=false value (recognized by the + // import machinery) is treated as an explicit request to leave the // module unconfigured. // - bool conf (true); + bool new_cfg (false); + pair<const exe*, import_kind> ir ( + import_direct<exe> ( + new_cfg, + rs, + name ("cli", dir_path (), "exe", "cli"), // cli%exe{cli} + true /* phase2 */, + optional, + true /* metadata */, + loc, + "module load")); + + const exe* tgt (ir.first); + + // Extract metadata. + // + auto* ver (tgt != nullptr ? &cast<string> (tgt->vars[v_ver]) : nullptr); + auto* sum (tgt != nullptr ? &cast<string> (tgt->vars[v_sum]) : nullptr); - if (const path* p = cast_null<path> (rs["config.cli"])) + // Print the report. + // + // If this is a configuration with new values, then print the report + // at verbosity level 2 and up (-v). + // + if (verb >= (new_cfg ? 2 : 3)) { - conf = p->string () != "false"; + diag_record dr (text); + dr << "cli " << project (rs) << '@' << rs << '\n'; - if (!conf && !optional) - fail (l) << "non-optional module requested to be left unconfigured"; + if (tgt != nullptr) + dr << " cli " << ir << '\n' + << " version " << *ver << '\n' + << " checksum " << *sum; + else + dr << " cli " << "not found, leaving unconfigured"; } - if (conf) - { - // Otherwise we will only honor optional if the user didn't specify - // any cli configuration explicitly. - // - optional = optional && !specified_config (rs, "cli"); + if (tgt == nullptr) + return false; - // If the configuration says we are unconfigured, then we should't - // re-run tests, etc. But we may still need to print the config - // report. - // - conf = !optional || !config::unconfigured (rs, "cli"); - } + // The cli variable (untyped) is an imported compiler target name. + // + rs.assign ("cli") = tgt->as_name (); + rs.assign (v_sum) = *sum; + rs.assign (v_ver) = *ver; - if (first) { - // config.cli - // - process_path pp; + standard_version v (*ver); - // Return version or empty string if the cli executable is not found - // or is not the command line interface compiler. - // - // @@ This needs some more thinking/cleanup. Specifically, what does - // it mean "cli not found"? Is it just not found in PATH? That plus - // was not able to execute (e.g., some shared libraries missing)? - // That plus cli that we found is something else? - // - auto test = [optional, &pp] (const path& cli) -> string - { - const char* args[] = {cli.string ().c_str (), "--version", nullptr}; - - // @@ TODO: redo using run_start()/run_finish() or even - // run<string>(). We have the ability to ignore exit code and - // redirect STDERR to STDOUT. - - try - { - // Only search in PATH (specifically, omitting the current - // executable's directory on Windows). - // - pp = process::path_search (cli, - true /* init */, - dir_path () /* fallback */, - true /* path_only */); - args[0] = pp.recall_string (); - - if (verb >= 3) - print_process (args); - - process pr (pp, args, 0, -1); // Open pipe to stdout. - - try - { - ifdstream is (move (pr.in_ofd), fdstream_mode::skip); - - // The version should be the last word on the first line. But - // also check the prefix since there are other things called - // 'cli', for example, "Mono JIT compiler". - // - string v; - getline (is, v); - - if (v.compare (0, 37, - "CLI (command line interface compiler)") == 0) - { - size_t p (v.rfind (' ')); - - if (p == string::npos) - fail << "unexpected output from " << cli; - - v.erase (0, p + 1); - } - else - { - if (!optional) - fail << cli << " is not command line interface compiler" << - info << "use config.cli to override"; - - v.clear (); - } - - is.close (); // Don't block the other end. - - if (pr.wait ()) - return v; - - // Presumably issued diagnostics. Fall through. - } - catch (const io_error&) - { - pr.wait (); - - // Fall through. - } - - // Fall through. - } - catch (const process_error& e) - { - // In some cases this is not enough (e.g., the runtime linker - // will print scary errors if some shared libraries are not - // found). So it would be good to redirect child's STDERR. - // - if (!optional) - error << "unable to execute " << args[0] << ": " << e << - info << "use config.cli to override"; - - if (e.child) - exit (1); - - // Fall through. - } - - return string (); // Not found. - }; + rs.assign<uint64_t> ("cli.version.number") = v.version; + rs.assign<uint64_t> ("cli.version.major") = v.major (); + rs.assign<uint64_t> ("cli.version.minor") = v.minor (); + rs.assign<uint64_t> ("cli.version.patch") = v.patch (); + } - // Adjust module priority (code generator). - // - config::save_module (rs, "cli", 150); + // Cache some values in the module for easier access in the rule. + // + extra.set_module (new module (data {*tgt, *sum})); - string ver; // Empty means unconfigured. - path cli ("cli"); // Default value. - bool new_cfg (false); // New configuration. + return true; + } - if (optional) - { - // Test the default value before setting any config.cli.* values - // so that if we fail to configure, nothing will be written to - // config.build. - // - if (conf) - { - ver = test (cli); - - if (ver.empty ()) - { - conf = false; - new_cfg = true; - } - else - { - auto l (lookup_config (new_cfg, rs, "config.cli", cli)); - assert (new_cfg && cast<path> (l) == cli); - } - } - } - else - { - cli = cast<path> (lookup_config (new_cfg, rs, "config.cli", cli)); - ver = test (cli); + bool + config_init (scope& rs, + scope& bs, + const location& loc, + bool, + bool optional, + module_init_extra& extra) + { + tracer trace ("cli::config_init"); + l5 ([&]{trace << "for " << rs;}); - if (ver.empty ()) - throw failed (); // Diagnostics already issued. - } + // We only support root loading (which means there can only be one). + // + if (rs != bs) + fail (loc) << "cli.config module must be loaded in project root"; - string checksum; - if (conf) - { - // Hash the compiler path and version. - // - sha256 cs; - cs.append (pp.effect_string ()); - cs.append (ver); - checksum = cs.string (); - } - else - { - // Note that we are unconfigured so that we don't keep re-testing - // this on each run. - // - new_cfg = config::unconfigured (rs, "cli", true) || new_cfg; - } - - // If this is a configuration with new values, then print the report - // at verbosity level 2 and up (-v). - // - if (verb >= (new_cfg ? 2 : 3)) - { - diag_record dr (text); - dr << "cli " << project (rs) << '@' << rs << '\n'; - - if (conf) - dr << " cli " << pp << '\n' - << " version " << ver << '\n' - << " checksum " << checksum; - else - dr << " cli " << "not found, leaving unconfigured"; - } - - if (conf) - { - rs.assign ("cli.path") = move (pp); - rs.assign ("cli.version") = move (ver); - rs.assign ("cli.checksum") = move (checksum); - } + // Load cli.guess and share its module instance as ours. + // + if (const shared_ptr<build2::module>* r = load_module ( + rs, rs, "cli.guess", loc, optional, extra.hints)) + { + extra.module = *r; } - - if (conf) + else { - // config.cli.options + // This can happen if someone already optionally loaded cli.guess + // and it has failed to configure. // - // This one is optional. We also merge it into the corresponding cli.* - // variables. See the cc module for more information on this merging - // semantics and some of its tricky aspects. - // - bs.assign ("cli.options") += cast_null<strings> ( - lookup_config (rs, "config.cli.options", nullptr)); + if (!optional) + fail (loc) << "cli could not be configured" << + info << "re-run with -V for more information"; + + return false; } - return conf; + // Configuration. + // + using config::append_config; + + // config.cli.options + // + // Note that we merge it into the corresponding cli.* variable. + // + append_config<strings> (rs, rs, "cli.options", nullptr); + + return true; } bool init (scope& rs, scope& bs, - const location& l, - bool first, + const location& loc, + bool, bool optional, module_init_extra& extra) { tracer trace ("cli::init"); - l5 ([&]{trace << "for " << bs;}); + l5 ([&]{trace << "for " << rs;}); + + // We only support root loading (which means there can only be one). + // + if (rs != bs) + fail (loc) << "cli module must be loaded in project root"; // Make sure the cxx module has been loaded since we need its targets // types (?xx{}). Note that we don't try to load it ourselves because of // the non-trivial variable merging semantics. So it is better to let - // the user load cxx explicitly. + // the user load cxx explicitly. @@ Not sure the reason still holds + // though it might still make sense to expect the user to load cxx. // - if (!cast_false<bool> (bs["cxx.loaded"])) - fail (l) << "cxx module must be loaded before cli"; + if (!cast_false<bool> (rs["cxx.loaded"])) + fail (loc) << "cxx module must be loaded before cli"; - // Load cli.config. + // Load cli.config and get its module instance. // - if (!cast_false<bool> (bs["cli.config.loaded"])) + if (const shared_ptr<build2::module>* r = load_module ( + rs, rs, "cli.config", loc, optional, extra.hints)) { - if (!init_module (rs, bs, "cli.config", l, optional, extra.hints)) - return false; + extra.module = *r; } - else if (!cast_false<bool> (bs["cli.config.configured"])) + else { + // This can happen if someone already optionally loaded cli.config + // and it has failed to configure. + // if (!optional) - fail (l) << "cli module could not be configured" << + fail (loc) << "cli could not be configured" << info << "re-run with -V for more information"; return false; } + auto& m (extra.module_as<module> ()); + // Register target types. // - if (first) - { - rs.insert_target_type<cli> (); - rs.insert_target_type<cli_cxx> (); - } + rs.insert_target_type<cli> (); + rs.insert_target_type<cli_cxx> (); // Register our rules. // { - auto reg = [&bs] (meta_operation_id mid, operation_id oid) + auto reg = [&rs, &m] (meta_operation_id mid, operation_id oid) { - bs.insert_rule<cli_cxx> (mid, oid, "cli.compile", compile_rule_); - bs.insert_rule<cxx::hxx> (mid, oid, "cli.compile", compile_rule_); - bs.insert_rule<cxx::cxx> (mid, oid, "cli.compile", compile_rule_); - bs.insert_rule<cxx::ixx> (mid, oid, "cli.compile", compile_rule_); + rs.insert_rule<cli_cxx> (mid, oid, "cli.compile", m); + rs.insert_rule<cxx::hxx> (mid, oid, "cli.compile", m); + rs.insert_rule<cxx::cxx> (mid, oid, "cli.compile", m); + rs.insert_rule<cxx::ixx> (mid, oid, "cli.compile", m); }; reg (perform_id, update_id); @@ -366,6 +276,7 @@ namespace build2 // NOTE: don't forget to also update the documentation in init.hxx if // changing anything here. + {"cli.guess", nullptr, guess_init}, {"cli.config", nullptr, config_init}, {"cli", nullptr, init}, {nullptr, nullptr, nullptr} diff --git a/build2/cli/init.hxx b/build2/cli/init.hxx index d5998f5..1c54316 100644 --- a/build2/cli/init.hxx +++ b/build2/cli/init.hxx @@ -17,8 +17,9 @@ namespace build2 // // Submodules: // - // `cli.config` -- registers variables. - // `cli` -- loads cli.config and registers target types and rules. + // `cli.guess` -- set variables describing the compiler. + // `cli.config` -- load `cli.guess` and set the rest of the variables. + // `cli` -- load `cli.config` and register targets and rules. // extern "C" const module_functions* build2_cli_load (); diff --git a/build2/cli/module.hxx b/build2/cli/module.hxx new file mode 100644 index 0000000..70f6ba8 --- /dev/null +++ b/build2/cli/module.hxx @@ -0,0 +1,30 @@ +// file : build2/cli/module.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_CLI_MODULE_HXX +#define BUILD2_CLI_MODULE_HXX + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/module.hxx> + +#include <build2/cli/rule.hxx> + +namespace build2 +{ + namespace cli + { + class module: public build2::module, + public virtual data, + public compile_rule + { + public: + explicit + module (data&& d) + : data (move (d)), compile_rule (move (d)) {} + }; + } +} + +#endif // BUILD2_CLI_MODULE_HXX diff --git a/build2/cli/rule.cxx b/build2/cli/rule.cxx index 9917f1a..3109689 100644 --- a/build2/cli/rule.cxx +++ b/build2/cli/rule.cxx @@ -13,9 +13,6 @@ #include <build2/cli/target.hxx> -using namespace std; -using namespace butl; - namespace build2 { namespace cli @@ -166,11 +163,17 @@ namespace build2 // match_prerequisite_members (a, t); - //@@ TODO: inject dependency on exe{cli}. + // For update inject dependency on the CLI compiler target. + // + if (a == perform_update_id) + inject (a, t, ctgt); switch (a) { - case perform_update_id: return &perform_update; + case perform_update_id: return [this] (action a, const target& t) + { + return perform_update (a, t); + }; case perform_clean_id: return &perform_clean_group_depdb; default: return noop_recipe; // Configure/dist update. } @@ -206,7 +209,7 @@ namespace build2 } target_state compile_rule:: - perform_update (action a, const target& xt) + perform_update (action a, const target& xt) const { tracer trace ("cli::compile_rule::perform_update"); @@ -215,7 +218,6 @@ namespace build2 // timestamp, depdb, etc. // const cli_cxx& t (xt.as<cli_cxx> ()); - const scope& rs (t.root_scope ()); const path& tp (t.h->path ()); // Update prerequisites and determine if any relevant ones render us @@ -242,7 +244,7 @@ namespace build2 // Then the compiler checksum. // - if (dd.expect (cast<string> (rs["cli.checksum"])) != nullptr) + if (dd.expect (csum) != nullptr) l4 ([&]{trace << "compiler mismatch forcing update of " << t;}); // Then the options checksum. @@ -277,9 +279,8 @@ namespace build2 path relo (relative (t.dir)); path rels (relative (s.path ())); - const process_path& cli (cast<process_path> (rs["cli.path"])); - - cstrings args {cli.recall_string ()}; + const process_path& pp (ctgt.process_path ()); + cstrings args {pp.recall_string ()}; // See if we need to pass --output-{prefix,suffix} // @@ -323,7 +324,7 @@ namespace build2 if (!t.ctx.dry_run) { - run (cli, args); + run (pp, args); dd.check_mtime (tp); } diff --git a/build2/cli/rule.hxx b/build2/cli/rule.hxx index aa3b8fa..64f1614 100644 --- a/build2/cli/rule.hxx +++ b/build2/cli/rule.hxx @@ -13,12 +13,20 @@ namespace build2 { namespace cli { + // Cached data shared between rules and the module. + // + struct data + { + const exe& ctgt; // CLI compiler target. + const string& csum; // CLI compiler checksum. + }; + // @@ Redo as two separate rules? // - class compile_rule: public rule + class compile_rule: public rule, virtual data { public: - compile_rule () {} + compile_rule (data&& d): data (move (d)) {} virtual bool match (action, target&, const string&) const override; @@ -26,8 +34,8 @@ namespace build2 virtual recipe apply (action, target&) const override; - static target_state - perform_update (action, const target&); + target_state + perform_update (action, const target&) const; }; } } diff --git a/build2/cli/target.cxx b/build2/cli/target.cxx index 09f3e10..ca16044 100644 --- a/build2/cli/target.cxx +++ b/build2/cli/target.cxx @@ -5,9 +5,6 @@ #include <libbuild2/context.hxx> -using namespace std; -using namespace butl; - namespace build2 { namespace cli diff --git a/doc/manual.cli b/doc/manual.cli index ca57199..04bed52 100644 --- a/doc/manual.cli +++ b/doc/manual.cli @@ -2666,17 +2666,17 @@ library to be able to use it. They also need to know where to find its headers, which other libraries to link, etc. This information is carried in a set of target-specific \c{cxx.export.*} variables that parallel the \c{cxx.*} set and that together with the library's prerequisites constitute the -\i{library meta-information protocol}. Every time a source file that depends -on a library is compiled or a binary is linked, this information is -automatically extracted by the compile and link rules from the library -dependency chain, recursively. And when the library is installed, this -information is carried over to its \c{pkg-config(1)} file. +\i{library metadata protocol}. Every time a source file that depends on a +library is compiled or a binary is linked, this information is automatically +extracted by the compile and link rules from the library dependency chain, +recursively. And when the library is installed, this information is carried +over to its \c{pkg-config(1)} file. \N|Similar to the \c{c.*} and \c{cc.*} sets discussed earlier, there are also \c{c.export.*} and \c{cc.export.*} sets.| -Here are the parts relevant to the library meta-information protocol in the -above \c{buildfile}: +Here are the parts relevant to the library metadata protocol in the above +\c{buildfile}: \ int_libs = # Interface dependencies. @@ -2773,7 +2773,7 @@ Note also that this only applies to shared libraries. In case of static libraries, both interface and implementation dependencies are always linked, recursively.| -The remaining lines in the library meta-information fragment are: +The remaining lines in the library metadata fragment are: \ lib{hello}: @@ -4365,7 +4365,7 @@ cross-compilation (specifically, inability to run tests). As a result, we recommend using \i{expectation-based} configuration where your project assumes a feature to be available if certain conditions are -met. Examples of such conditions at the source code level include C++ feature +met. Examples of such conditions at the source code level include feature test macros, platform macros, runtime library macros, compiler macros, etc., with the build system modules exposing some of the same information via variables to allow making similar decisions in \c{buildfiles}. Another @@ -4476,7 +4476,10 @@ subprojects, similar to build system submodules. If a build system module for a tool (such as a source code generator) and the tool itself share a name, then they may need to coordinate their configuration -variable names in order to avoid clashes. +variable names in order to avoid clashes. Note also that when importing an +executable target in the \c{<project>%exe{<project>\}} form, the +\c{config.<project>} variable is treated as an alias for +\c{config.import.<project>.<project>.exe}. The build system core reserves \c{build} and \c{import} as the second component in configuration variables as well as \c{configured} as the third diff --git a/libbuild2/bin/init.cxx b/libbuild2/bin/init.cxx index ab4b686..34bfcd7 100644 --- a/libbuild2/bin/init.cxx +++ b/libbuild2/bin/init.cxx @@ -150,7 +150,7 @@ namespace build2 // We only support root loading (which means there can only be one). // - if (&rs != &bs) + if (rs != bs) fail (loc) << "bin.config module must be loaded in project root"; // Load bin.vars. diff --git a/libbuild2/c/init.cxx b/libbuild2/c/init.cxx index e6e28aa..5eea8de 100644 --- a/libbuild2/c/init.cxx +++ b/libbuild2/c/init.cxx @@ -142,7 +142,7 @@ namespace build2 // We only support root loading (which means there can only be one). // - if (&rs != &bs) + if (rs != bs) fail (loc) << "c.guess module must be loaded in project root"; // Load cc.core.vars so that we can cache all the cc.* variables. @@ -276,7 +276,7 @@ namespace build2 // We only support root loading (which means there can only be one). // - if (&rs != &bs) + if (rs != bs) fail (loc) << "c.config module must be loaded in project root"; // Load c.guess and share its module instance as ours. @@ -313,7 +313,7 @@ namespace build2 // We only support root loading (which means there can only be one). // - if (&rs != &bs) + if (rs != bs) fail (loc) << "c module must be loaded in project root"; // Load c.config. diff --git a/libbuild2/cc/common.cxx b/libbuild2/cc/common.cxx index 99de66e..cf6d546 100644 --- a/libbuild2/cc/common.cxx +++ b/libbuild2/cc/common.cxx @@ -465,20 +465,8 @@ namespace build2 { // This is import. // - name n (cn); - auto rp (s.find_target_type (n, location ())); // Note: changes name. - const target_type* tt (rp.first); - optional<string>& ext (rp.second); - - if (tt == nullptr) - fail << "unknown target type '" << n.type << "' in library " << n; - - // @@ OUT: for now we assume out is undetermined, just like in - // search (name, scope). - // - dir_path out; - - prerequisite_key pk {n.proj, {tt, &n.dir, &out, &n.value, ext}, &s}; + name n (cn), o; // Note: find_prerequisite_key() changes name. + prerequisite_key pk (s.find_prerequisite_key (n, o, location ())); xt = search_library_existing (a, sysd, usrd, pk); if (xt == nullptr) diff --git a/libbuild2/cc/compile-rule.cxx b/libbuild2/cc/compile-rule.cxx index a8916cf..8b082cc 100644 --- a/libbuild2/cc/compile-rule.cxx +++ b/libbuild2/cc/compile-rule.cxx @@ -696,8 +696,8 @@ namespace build2 continue; // A dependency on a library is there so that we can get its - // *.export.poptions, modules, etc. This is the library - // meta-information protocol. See also append_lib_options(). + // *.export.poptions, modules, etc. This is the library metadata + // protocol. See also append_lib_options(). // if (pi == include_type::normal && (p.is_a<libx> () || @@ -4871,7 +4871,7 @@ namespace build2 // // For (direct) library prerequisites, check their prerequisite bmi{}s // (which should be searched and matched with module names discovered; - // see the library meta-information protocol for details). + // see the library metadata protocol for details). // // For our own bmi{} prerequisites, checking if each (better) matches // any of the imports. diff --git a/libbuild2/cc/init.cxx b/libbuild2/cc/init.cxx index 8d66376..2a0dbd2 100644 --- a/libbuild2/cc/init.cxx +++ b/libbuild2/cc/init.cxx @@ -399,7 +399,7 @@ namespace build2 // We only support root loading (which means there can only be one). // - if (&rs != &bs) + if (rs != bs) fail (loc) << m << " module must be loaded in project root"; // We want to order the loading to match what user specified on the diff --git a/libbuild2/cc/utility.hxx b/libbuild2/cc/utility.hxx index fa9f165..017765b 100644 --- a/libbuild2/cc/utility.hxx +++ b/libbuild2/cc/utility.hxx @@ -41,10 +41,10 @@ namespace build2 // // The reason we pass scope and not the target is because this function is // called not only for exe/lib but also for obj as part of the library - // meta-information protocol implementation. Normally the bin.*.lib values - // will be project-wide. With this scheme they can be customized on the - // per-directory basis but not per-target which means all exe/lib in the - // same directory have to have the same link order. + // metadata protocol implementation. Normally the bin.*.lib values will be + // project-wide. With this scheme they can be customized on the per- + // directory basis but not per-target which means all exe/lib in the same + // directory have to have the same link order. // lorder link_order (const scope& base, otype); diff --git a/libbuild2/config/utility.hxx b/libbuild2/config/utility.hxx index 614791f..fb3023b 100644 --- a/libbuild2/config/utility.hxx +++ b/libbuild2/config/utility.hxx @@ -311,6 +311,20 @@ namespace build2 // be used to "remember" that the module is left unconfigured in order to // avoid re-running the tests, etc. // + // @@ This functionality is WIP/unused and still has a number of issues: + // + // - This seems to be a subset of a bigger problem of caching discovered + // configuration results. In fact, what we do in the configured case, + // for example in the cc module (multiple path extraction runs, etc), is + // a lot more expensive. + // + // - The current semantics does not work well for the case where, say, the + // missing tool has appeared in PATH and can now be used via the default + // configuration. In fact, even reconfiguring will not help without a + // "nudge" (e.g., config.<tool>=<tool>). So maybe this value should be + // ignored during configuration? See the "Tool importation: unconfigured + // state" page for more notes. + // LIBBUILD2_SYMEXPORT bool unconfigured (scope& rs, const string& var); diff --git a/libbuild2/context.cxx b/libbuild2/context.cxx index 06cdc6d..0be0046 100644 --- a/libbuild2/context.cxx +++ b/libbuild2/context.cxx @@ -544,7 +544,10 @@ namespace build2 var_project_url = &vp.insert<string> ("project.url"); var_project_summary = &vp.insert<string> ("project.summary"); - var_import_target = &vp.insert<name> ("import.target"); + var_import_target = &vp.insert<name> ("import.target"); + var_import_metadata = &vp.insert<uint64_t> ("import.metadata"); + + var_export_metadata = &vp.insert<uint64_t> ("export.metadata", v_t); var_extension = &vp.insert<string> ("extension", v_t); var_clean = &vp.insert<bool> ("clean", v_t); diff --git a/libbuild2/context.hxx b/libbuild2/context.hxx index ea2c017..573b8d1 100644 --- a/libbuild2/context.hxx +++ b/libbuild2/context.hxx @@ -352,6 +352,11 @@ namespace build2 // const variable* var_import_build2; const variable* var_import_target; + const variable* var_import_metadata; + + // export.* + // + const variable* var_export_metadata; // [string] target visibility // diff --git a/libbuild2/cxx/init.cxx b/libbuild2/cxx/init.cxx index 1d8421c..265bbc0 100644 --- a/libbuild2/cxx/init.cxx +++ b/libbuild2/cxx/init.cxx @@ -382,7 +382,7 @@ namespace build2 // We only support root loading (which means there can only be one). // - if (&rs != &bs) + if (rs != bs) fail (loc) << "cxx.guess module must be loaded in project root"; // Load cc.core.vars so that we can cache all the cc.* variables. @@ -540,7 +540,7 @@ namespace build2 // We only support root loading (which means there can only be one). // - if (&rs != &bs) + if (rs != bs) fail (loc) << "cxx.config module must be loaded in project root"; // Load cxx.guess and share its module instance as ours. @@ -585,7 +585,7 @@ namespace build2 // We only support root loading (which means there can only be one). // - if (&rs != &bs) + if (rs != bs) fail (loc) << "cxx module must be loaded in project root"; // Load cxx.config. diff --git a/libbuild2/file.cxx b/libbuild2/file.cxx index 0bf3fd4..fb24160 100644 --- a/libbuild2/file.cxx +++ b/libbuild2/file.cxx @@ -4,19 +4,20 @@ #include <libbuild2/file.hxx> #include <iomanip> // left, setw() +#include <sstream> #include <libbuild2/scope.hxx> #include <libbuild2/target.hxx> #include <libbuild2/context.hxx> #include <libbuild2/filesystem.hxx> -#include <libbuild2/prerequisite.hxx> #include <libbuild2/diagnostics.hxx> +#include <libbuild2/prerequisite-key.hxx> #include <libbuild2/token.hxx> #include <libbuild2/lexer.hxx> #include <libbuild2/parser.hxx> -#include <libbuild2/config/utility.hxx> // save_variable() +#include <libbuild2/config/utility.hxx> // lookup_config() using namespace std; using namespace butl; @@ -25,6 +26,9 @@ namespace build2 { // Standard and alternative build file/directory naming schemes. // + + // build: + const dir_path std_build_dir ("build"); const dir_path std_root_dir (dir_path (std_build_dir) /= "root"); const dir_path std_bootstrap_dir (dir_path (std_build_dir) /= "bootstrap"); @@ -39,7 +43,7 @@ namespace build2 const path std_buildfile_file ("buildfile"); const path std_buildignore_file (".buildignore"); - // + // build2: const dir_path alt_build_dir ("build2"); const dir_path alt_root_dir (dir_path (alt_build_dir) /= "root"); @@ -171,7 +175,7 @@ namespace build2 try { l5 ([&]{trace << "sourcing " << fn;}); - p.parse_buildfile (l, root, base); + p.parse_buildfile (l, &root, base); } catch (const io_error& e) { @@ -1417,8 +1421,221 @@ namespace build2 return rs; } - pair<name, dir_path> - import_search (scope& ibase, name target, const location& loc, bool subp) + // Find or insert a target based on the file path. + // + static const target* + find_target (tracer& trace, context& ctx, + const target_type& tt, const path& p) + { + const target* t ( + ctx.targets.find (tt, + p.directory (), + dir_path (), + p.leaf ().base ().string (), + p.extension (), + trace)); + + if (t != nullptr) + { + if (const file* f = t->is_a<file> ()) + assert (f->path () == p); + } + + return t; + } + + static pair<target&, ulock> + insert_target (tracer& trace, context& ctx, + const target_type& tt, path p) + { + auto r ( + ctx.targets.insert_locked (tt, + p.directory (), + dir_path (), // No out (not in project). + p.leaf ().base ().string (), + p.extension (), // Always specified. + true /* implied */, + trace)); + + if (const file* f = r.first.is_a<file> ()) + f->path (move (p)); + + return r; + } + + // Extract metadata for an executable target by executing it with the + // --build2-metadata option. In case of an error, issue diagnostics and fail + // if opt is false and return nullopt if it's true. + // + // Note that loading of the metadata is split into two steps, extraction and + // parsing, because extraction also serves as validation that the executable + // is runnable, what we expected, etc. In other words, we sometimes do the + // extraction without parsing. In this light it would have been more + // efficient for extract to return the running process with a pipe rather + // than the extracted data. But this would complicate the code quite a bit + // plus we don't expect the data to be large, typically. + // + // Also note that we do not check the export.metadata here leaving it to + // the caller to do for both this case and export stub. + // + static optional<string> + extract_metadata (const process_path& pp, + const string& key, + bool opt, + const location& loc) + { + // Note: to ease handling (think patching third-party code) we will always + // specify the --build2-metadata option in this single-argument form. + // + const char* args[] {pp.recall_string (), "--build2-metadata=1", nullptr}; + + // @@ TODO This needs some more thinking/clarification. Specifically, what + // does it mean "x not found/not ours"? Is it just not found in PATH? + // That plus was not able to execute (e.g., some shared libraries + // missing)? That plus abnormal termination? That plus x that we found + // is something else? + // + // Specifically, at least on Linux, when a shared library is not found, + // it appears exec() issues the diagnostics and calls exit(127) (that + // is, exec() does not return). So this is a normal termination with a + // peculiar exit code. + // + // Overall, it feels like we should only silently ignore the "not + // found" and "not ours" cases since for all others the result is + // ambigous: it could be "ours" but just broken and the user expects + // us to use it but we silently ignored it. But then the same can be + // said about the "not ours" case: the user expected us to find "ours" + // but we didn't and silently ignored it. + // + try + { + // Note: not using run_*() functions since need to be able to suppress + // all errors, including inability to exec. + // + if (verb >= 3) + print_process (args); + + process pr (pp, + args, + -2 /* stdin to /dev/null */, + -1 /* stdout to pipe */, + opt ? -2 : 2 /* stderr to /dev/null or pass-through */); + + try + { + ifdstream is (move (pr.in_ofd), fdstream_mode::skip); + string r; + getline (is, r, '\0'); // Will fail if there is no data. + is.close (); // Detect errors. + + if (pr.wait ()) + { + // Check the signature line. It should be in the following form: + // + // # build2 buildfile <key> + // + // This makes sure we don't treat bogus output as metadata and also + // will allow us to support other formats (say, JSON) in the future. + // Note that we won't be able to add more options since trying them + // will be expensive. + // + if (r.compare (0, + 20 + key.size (), + "# build2 buildfile " + key + '\n') == 0) + return r; + + if (!opt) + error (loc) << "invalid metadata signature in " << args[0] + << " output"; + + goto fail; + } + + // Process error, fall through. + } + catch (const io_error& e) + { + // IO error (or process error), fall through. + } + + // Deal with process or IO error. + // + if (pr.wait ()) + { + if (!opt) + error (loc) << "unable to read metadata from " << args[0]; + } + else + { + // The child process presumably issued diagnostics but if it didn't, + // the result will be very confusing. So let's issue something + // generic for good measure. + // + if (!opt) + error (loc) << "unable to extract metadata from " << args[0]; + } + + goto fail; + } + catch (const process_error& e) + { + if (!opt) + error (loc) << "unable to execute " << args[0] << ": " << e; + + if (e.child) + exit (1); + + goto fail; + } + + fail: + + if (opt) + return nullopt; + else + throw failed (); + } + + static void + parse_metadata (target& t, const string& md, const location& loc) + { + istringstream is (md); + path_name in ("<metadata>"); + + auto df = make_diag_frame ( + [&t, &loc] (const diag_record& dr) + { + dr << info (loc) << "while loading metadata for " << t; + }); + + parser p (t.ctx); + p.parse_buildfile (is, in, + nullptr /* root */, + t.base_scope ().rw (), // Load phase. + &t); + } + + // Return the processed target name as well as the project directory, if + // any. + // + // Absent project directory means nothing importable for this target was + // found (and the returned target name is the same as the original). Empty + // project directory means the target was found in an ad hoc manner, outside + // of any project (in which case it may still be qualified; see + // config.import.<proj>.<name>[.<type>]). + // + // Return empty name if an ad hoc import resulted in a NULL target (only + // allowed if optional is true). + // + pair<name, optional<dir_path>> + import_search (bool& new_value, + scope& ibase, + name tgt, + bool opt, + const optional<string>& meta, + bool subp, + const location& loc, + const char* what) { tracer trace ("import_search"); @@ -1426,24 +1643,24 @@ namespace build2 // short and sweet: we simply return it as empty-project-qualified and // let someone else (e.g., a rule) take a stab at it. // - if (target.unqualified ()) + if (tgt.unqualified ()) { - target.proj = project_name (); - return make_pair (move (target), dir_path ()); + tgt.proj = project_name (); + return make_pair (move (tgt), optional<dir_path> ()); } context& ctx (ibase.ctx); // Otherwise, get the project name and convert the target to unqualified. // - project_name proj (move (*target.proj)); - target.proj = nullopt; + project_name proj (move (*tgt.proj)); + tgt.proj = nullopt; scope& iroot (*ibase.root_scope ()); // Figure out the imported project's out_root. // - dir_path out_root; + optional<dir_path> out_root; // First try the config.import.* mechanism. The idea is that if the user // explicitly told us the project's location, then we should prefer that @@ -1452,95 +1669,208 @@ namespace build2 // auto& vp (iroot.var_pool ()); + using config::lookup_config; + for (;;) // Break-out loop. { - string n ("config.import." + proj.variable ()); + string projv (proj.variable ()); + string n ("config.import." + projv); - auto skip = [&target, &proj, &trace] () + // Skip import phase 1. + // + auto skip = [&tgt, &proj, &trace] () { - target.proj = move (proj); - l5 ([&]{trace << "skipping " << target;}); - return make_pair (move (target), dir_path ()); + tgt.proj = move (proj); + l5 ([&]{trace << "skipping " << tgt;}); + return make_pair (move (tgt), optional<dir_path> ()); }; - // config.import.<proj> + // Add hoc import. // - { - // Note: pattern-typed in context ctor as an overridable variable of - // type abs_dir_path (path auto-completion). - // - const variable& var (vp.insert (n)); - - if (auto l = iroot[var]) - { - out_root = cast<dir_path> (l); // Normalized and actualized. - - // Mark as part of config. - // - config::save_variable (iroot, var); - - // Empty config.import.* value means don't look in subprojects or - // amalgamations and go straight to the rule-specific import (e.g., - // to use system-installed). - // - if (out_root.empty ()) - return skip (); - - break; - } - } - // config.import.<proj>.<name>.<type> // config.import.<proj>.<name> // // For example: config.import.build2.b.exe=/opt/build2/bin/b // - if (!target.value.empty ()) + // If <type> is exe and <proj> and <name> are the same, then we also + // recognize the special config.<proj> (tool importation; we could + // also handle the case where <proj> is not the same as <name> via + // the config.<proj>.<name> variable). For backwards-compatibility + // reasons, it takes precedence over config.import. + // + // Note: see import phase 2 diagnostics if changing anything here. + // + // @@ How will this work for snake-case targets, say libs{build2-foo}? + // As well as for dot-separated target types, say, cli.cxx{}? + // + // @@ This duality has a nasty side-effect: if we have config.<proj> + // configured, then specifying config.<proj>.import has no effect + // (see also a note below on priority just among these options). + // + // Some ideas on how to resolve this include: using lookup depth, + // using override info, and using the "new value" status. All of + // these undoubtfully will complicate this logic (i.e., we will have + // to lookup all of them and then decide which one "wins"). + // + if (!tgt.value.empty ()) { - auto lookup = [&iroot, &vp, &loc] (string name) -> path + // Return NULL if not found and empty path if NULL. For executable + // targets (exe is true), also treat the special `false` value as + // NULL. + // + auto lookup = [&new_value, &iroot, opt, &loc, what] ( + const variable& var, bool exe) -> const path* { - // Note: pattern-typed in context ctor as an overridable variable of - // type path. - // - const variable& var (vp.insert (move (name))); + auto l (lookup_config (new_value, iroot, var)); - path r; - if (auto l = iroot[var]) + if (l.defined ()) { - r = cast<path> (l); + const path* p (cast_null<path> (l)); + + if (p != nullptr) + { + if (p->empty ()) + fail (loc) << "empty path in " << var; - if (r.empty ()) - fail (loc) << "empty path in " << var.name; + if (!exe || p->to_directory () || p->string () != "false") + return p; + } + + if (!opt) + fail (loc) << (p == nullptr ? "null" : "false") << " in " + << var << " for non-optional " << what; - config::save_variable (iroot, var); + return &empty_path; } - return r; + return nullptr; }; - // First try .<name>.<type>, then just .<name>. + // First try config.<proj>, then import.<name>.<type>, and finally + // just import.<name>. // - path p; - if (target.typed ()) - p = lookup (n + '.' + target.value + '.' + target.type); - - if (p.empty ()) - p = lookup (n + '.' + target.value); + // @@ What should we do if several of them are specified? For example, + // one is inherited from amalgamation while the other is specified + // on the project's root? We could pick the one with the least + // lookup depth. On the other hand, we expect people to stick with + // the config.<proj> notation for tools (since it's a lot easier to + // type) so let's not complicate things for the time being. + // + // Another alternative would be to see which one is new. + // + const path* p (nullptr); - if (!p.empty ()) + if (tgt.typed ()) { - // If the path is relative, then keep it project-qualified assuming - // import phase 2 knows what to do with it. Think: - // - // config.import.build2.b=b-boot + bool e (tgt.type == "exe"); + + // The config.import.* vars are pattern-typed in context ctor as an + // overridable variable of type path. The config.<proj> we have to + // type manually. // - if (p.relative ()) - target.proj = move (proj); + if (e && (projv == tgt.value || proj == tgt.value)) + p = lookup (vp.insert<path> ("config." + projv), e); + + if (p == nullptr) + p = lookup (vp.insert (n + '.' + tgt.value + '.' + tgt.type), e); + } + + if (p == nullptr) + p = lookup (vp.insert (n + '.' + tgt.value), false); + + if (p != nullptr) + { + if (p->empty ()) + tgt = name (); // NULL + else + { + tgt.dir = p->directory (); + tgt.value = p->leaf ().string (); + + // If the path is relative, then keep it project-qualified + // assuming import phase 2 knows what to do with it. Think: + // + // config.import.build2.b=b-boot + // + // @@ Maybe we should still complete it if it's not simple? After + // all, this is a path, do we want interpretations other than + // relative to CWD? Maybe we do, who knows. Doesn't seem to + // harm anything at the moment. + // + // Why not call import phase 2 directly here? Well, one good + // reason would be to allow for rule-specific import resolution. + // + if (p->relative ()) + tgt.proj = move (proj); + else + { + // Enter the target and assign its path (this will most commonly + // be some out of project file). + // + // @@ Should we check that the file actually exists (and cache + // the extracted timestamp)? Or just let things take their + // natural course? + // + name n (tgt); + auto r (ibase.find_target_type (n, loc)); - target.dir = p.directory (); - target.value = p.leaf ().string (); + if (r.first == nullptr) + fail (loc) << "unknown target type " << n.type << " in " << n; + + // Note: not using the extension extracted by find_target_type() + // to be consistent with import phase 2. + // + target& t (insert_target (trace, ctx, *r.first, *p).first); - return make_pair (move (target), dir_path ()); + // Load the metadata, similar to import phase 2. + // + if (meta) + { + if (exe* e = t.is_a<exe> ()) + { + if (!e->vars[ctx.var_export_metadata].defined ()) + { + parse_metadata (*e, + *extract_metadata (e->process_path (), + *meta, + false /* optional */, + loc), + loc); + } + } + } + } + } + + return make_pair (move (tgt), optional<dir_path> (dir_path ())); + } + } + + // Normal import. + // + // config.import.<proj> + // + // Note: see import phase 2 diagnostics if changing anything here. + // + { + // Note: pattern-typed in context ctor as an overridable variable of + // type abs_dir_path (path auto-completion). + // + auto l (lookup_config (new_value, iroot, vp.insert (n))); + + if (l.defined ()) + { + const dir_path* d (cast_null<dir_path> (l)); + + // Empty/NULL config.import.* value means don't look in subprojects + // or amalgamations and go straight to the rule-specific import + // (e.g., to use system-installed). + // + if (d == nullptr || d->empty ()) + return skip (); + + out_root = *d; // Normalized and actualized. + break; } } @@ -1560,7 +1890,7 @@ namespace build2 { out_root = cast<dir_path> (l); - if (out_root.empty ()) + if (out_root->empty ()) return skip (); break; @@ -1609,22 +1939,27 @@ namespace build2 // Add the qualification back to the target (import_load() will remove it // again). // - target.proj = move (proj); + tgt.proj = move (proj); - return make_pair (move (target), move (out_root)); + return make_pair (move (tgt), move (out_root)); } pair<names, const scope&> - import_load (context& ctx, pair<name, dir_path> x, const location& loc) + import_load (context& ctx, + pair<name, optional<dir_path>> x, + bool meta, + const location& loc) { tracer trace ("import_load"); - name target (move (x.first)); - dir_path out_root (move (x.second)); + assert (x.second); - assert (target.proj); - project_name proj (move (*target.proj)); - target.proj = nullopt; + name tgt (move (x.first)); + dir_path out_root (move (*x.second)); + + assert (tgt.proj); + project_name proj (move (*tgt.proj)); + tgt.proj = nullopt; // Bootstrap the imported root scope. This is pretty similar to what we do // in main() except that here we don't try to guess src_root. @@ -1746,15 +2081,27 @@ namespace build2 ts.assign (ctx.var_out_root) = move (out_root); ts.assign (ctx.var_src_root) = move (src_root); - // Also pass the target being imported in the import.target variable. + // Pass the target being imported in import.target. // { value& v (ts.assign (ctx.var_import_target)); - if (!target.empty ()) // Otherwise leave NULL. - v = target; // Can't move (need for diagnostics below). + if (!tgt.empty ()) // Otherwise leave NULL. + v = tgt; // Can't move (need for diagnostics below). } + // Pass the metadata compatibility version in import.metadata. + // + // This serves both as an indication that the metadata is required (can be + // useful, for example, in cases where it is expensive to calculate) as + // well as the maximum version we recognize. The exporter may return it in + // any version up to and including this maximum. And it may return it even + // if not requested (but only in version 1). The exporter should also set + // the returned version as the target-specific export.metadata variable. + // + if (meta) + ts.assign (ctx.var_import_metadata) = uint64_t (1); + // Load the export stub. Note that it is loaded in the context // of the importing project, not the imported one. The export // stub will normally switch to the imported root scope at some @@ -1778,8 +2125,8 @@ namespace build2 // If there were no export directive executed in an export stub, assume // the target is not exported. // - if (v.empty () && !target.empty ()) - fail (loc) << "target " << target << " is not exported by project " + if (v.empty () && !tgt.empty ()) + fail (loc) << "target " << tgt << " is not exported by project " << proj; return pair<names, const scope&> (move (v), *root); @@ -1790,32 +2137,104 @@ namespace build2 } } - names - import (scope& base, name target, const location& loc) + pair<names, import_kind> + import (scope& base, + name tgt, + bool ph2, + bool opt, + bool metadata, + const location& loc) { tracer trace ("import"); - l5 ([&]{trace << target << " from " << base;}); + l5 ([&]{trace << tgt << " from " << base;}); + + assert ((!opt || ph2) && (!metadata || ph2)); + + context& ctx (base.ctx); + assert (ctx.phase == run_phase::load); + + // If metadata is requested, delegate to import_direct() which will lookup + // the target and verify the metadata was loaded. + // + if (metadata) + { + pair<const target*, import_kind> r ( + import_direct (base, move (tgt), ph2, opt, metadata, loc)); + + return make_pair (r.first != nullptr ? r.first->as_name () : names {}, + r.second); + } + + // Save the original target name as metadata key. + // + auto meta (metadata ? optional<string> (tgt.value) : nullopt); - pair<name, dir_path> r (import_search (base, move (target), loc)); + pair<name, optional<dir_path>> r ( + import_search (base, move (tgt), opt, meta, true /* subpproj */, loc)); - // If we couldn't find the project, return to let someone else (e.g., a - // rule) take a stab at it. + // If there is no project, we are either done or go straight to phase 2. // - if (r.second.empty ()) + if (!r.second || r.second->empty ()) { - l5 ([&]{trace << "postponing " << r.first;}); - return names {move (r.first)}; + names ns; + + if (r.first.empty ()) + { + assert (opt); // NULL + } + else + { + ns.push_back (move (r.first)); + + // If the target is still qualified, it is either phase 2 now or we + // return it as is to let someone else (e.g., a rule, import phase 2) + // take a stab at it later. + // + if (ns.back ().qualified ()) + { + if (ph2) + { + // This is tricky: we only want the optional semantics for the + // fallback case. + // + if (const target* t = import (ctx, + base.find_prerequisite_key (ns, loc), + opt && !r.second /* optional */, + meta, + false /* existing */, + loc)) + ns = t->as_name (); + else + ns.clear (); // NULL + } + else + l5 ([&]{trace << "postponing " << r.first;}); + } + } + + return make_pair ( + move (ns), + r.second.has_value () ? import_kind::adhoc : import_kind::fallback); } - return import_load (base.ctx, move (r), loc).first; + return make_pair ( + import_load (base.ctx, move (r), metadata, loc).first, + import_kind::normal); } const target* - import (context& ctx, const prerequisite_key& pk, bool existing) + import (context& ctx, + const prerequisite_key& pk, + bool opt, + const optional<string>& meta, + bool exist, + const location& loc) { tracer trace ("import"); + assert (!meta || !exist); + assert (pk.proj); const project_name& proj (*pk.proj); @@ -1826,7 +2245,7 @@ namespace build2 // Try to find the executable in PATH (or CWD if relative). // - if (tt.is_a<exe> ()) + for (; tt.is_a<exe> (); ) // Breakout loop. { path n (*tk.dir); n /= *tk.name; @@ -1836,63 +2255,218 @@ namespace build2 n += *tk.ext; } - // Only search in PATH (or CWD). + // Only search in PATH (or CWD if not simple). // - process_path pp (process::try_path_search (n, true, dir_path (), true)); + process_path pp ( + process::try_path_search (n, + false /* init */, + dir_path () /* fallback */, + true /* path_only */)); + if (pp.empty ()) + break; - if (!pp.empty ()) - { - path& p (pp.effect); - assert (!p.empty ()); // We searched for a simple name. - - const exe* t ( - !existing - ? &ctx.targets.insert<exe> (tt, - p.directory (), - dir_path (), // No out (not in project). - p.leaf ().base ().string (), - p.extension (), // Always specified. - trace) - : ctx.targets.find<exe> (tt, - p.directory (), - dir_path (), - p.leaf ().base ().string (), - p.extension (), - trace)); - - if (t != nullptr) - { - if (!existing) - t->path (move (p)); - else - assert (t->path () == p); + const path& p (pp.effect); + assert (!p.empty ()); // We searched for a relative path. + if (exist) // Note: then meta is false. + { + if (const target* t = find_target (trace, ctx, tt, p)) return t; + + break; + } + + // Try hard to avoid re-extracting the metadata (think of a tool that is + // used by multiple projects in an amalgamation). + // + optional<string> md; + optional<const target*> t; + if (meta) + { + t = find_target (trace, ctx, tt, p); + + if (*t != nullptr && (*t)->vars[ctx.var_export_metadata].defined ()) + return *t; // We've got all we need. + + if (!(md = extract_metadata (pp, *meta, opt, loc))) + break; + } + + if (!t || *t == nullptr) + { + pair<target&, ulock> r (insert_target (trace, ctx, tt, p)); + t = &r.first; + + // Cache the process path if we've created the target (it's possible + // that the same target will be imported via different paths, e.g., as + // a simple name via PATH search and as an absolute path in which case + // the first import will determine the path). + // + if (r.second.owns_lock ()) + { + r.first.as<exe> ().process_path (move (pp)); + r.second.unlock (); } } + + // Save the metadata. Note that this happens during the load phase and + // so MT-safe. + // + if (meta) + parse_metadata ((*t)->rw (), *md, loc); + + return *t; } - if (existing) + if (opt || exist) return nullptr; - // @@ We no longer have location. This is especially bad for the - // empty case, i.e., where do I need to specify the project - // name)? Looks like the only way to do this is to keep location - // in name and then in prerequisite. Perhaps one day... - // diag_record dr; - dr << fail << "unable to import target " << pk; + dr << fail (loc) << "unable to import target " << pk; if (proj.empty ()) dr << info << "consider adding its installation location" << info << "or explicitly specify its project name"; else - dr << info << "use config.import." << proj.variable () - << " command line variable to specify its project out_root"; + { + string projv (proj.variable ()); + + // Suggest normal import. + // + dr << info << "use config.import." << projv << " configuration variable " + << "to specify its project out_root"; + + // Suggest ad hoc import. + // + string v (tt.is_a<exe> () && (projv == *tk.name || proj == *tk.name) + ? "config." + projv + : "config.import." + projv + '.' + *tk.name + '.' + tt.name); + + dr << info << "or use " << v << " configuration variable to specify its " + << "path"; + } dr << endf; } + pair<const target*, import_kind> + import_direct (bool& new_value, + scope& base, + name tgt, + bool ph2, + bool opt, + bool metadata, + const location& loc, + const char* what) + { + // This is like normal import() except we return the target rather than + // its name. + // + tracer trace ("import_direct"); + + l5 ([&]{trace << tgt << " from " << base << " for " << what;}); + + assert ((!opt || ph2) && (!metadata || ph2)); + + context& ctx (base.ctx); + assert (ctx.phase == run_phase::load); + + auto meta (metadata ? optional<string> (tgt.value) : nullopt); + + names ns; + import_kind k; + const target* t (nullptr); + + pair<name, optional<dir_path>> r ( + import_search (new_value, + base, + move (tgt), + opt, + meta, + true /* subpproj */, + loc, + what)); + + // If there is no project, we are either done or go straight to phase 2. + // + if (!r.second || r.second->empty ()) + { + k = r.second.has_value () ? import_kind::adhoc : import_kind::fallback; + + if (r.first.empty ()) + { + assert (opt); + return make_pair (t, k); // NULL + } + else if (r.first.qualified ()) + { + if (ph2) + { + names ns {move (r.first)}; + + // This is tricky: we only want the optional semantics for the + // fallback case. + // + t = import (ctx, + base.find_prerequisite_key (ns, loc), + opt && !r.second, + meta, + false /* existing */, + loc); + } + + if (t == nullptr) + return make_pair (t, k); // NULL + + // Otherwise fall through. + } + else + ns.push_back (move (r.first)); // And fall through. + } + else + { + k = import_kind::normal; + ns = import_load (base.ctx, move (r), metadata, loc).first; + } + + if (t == nullptr) + { + // Similar logic to perform's search(). + // + target_key tk (base.find_target_key (ns, loc)); + t = ctx.targets.find (tk, trace); + if (t == nullptr) + fail (loc) << "unknown imported target " << tk; + } + + if (meta) + { + if (auto* v = cast_null<uint64_t> (t->vars[ctx.var_export_metadata])) + { + if (*v != 1) + fail (loc) << "unexpected metadata version " << *v + << " in imported target " << *t; + } + else + fail (loc) << "no metadata for imported target " << *t; + } + + return make_pair (t, k); + } + + ostream& + operator<< (ostream& o, const pair<const exe*, import_kind>& p) + { + assert (p.first != nullptr); + + if (p.second == import_kind::normal) + o << *p.first; + else + o << p.first->process_path (); + + return o; + } + void create_project (const dir_path& d, const optional<dir_path>& amal, diff --git a/libbuild2/file.hxx b/libbuild2/file.hxx index b44efb6..231ea73 100644 --- a/libbuild2/file.hxx +++ b/libbuild2/file.hxx @@ -11,6 +11,7 @@ #include <libbuild2/utility.hxx> #include <libbuild2/scope.hxx> +#include <libbuild2/target.hxx> #include <libbuild2/variable.hxx> // list_value #include <libbuild2/export.hxx> @@ -241,37 +242,190 @@ namespace build2 // that (or failed to find anything usable), it calls the standard // prerequisite search() function which sees this is a project-qualified // prerequisite and goes straight to the second phase of import. Here, - // currently, we simply fail but in the future this will be the place where - // we can call custom "last resort" import hooks. For example, we can hook a - // package manager that will say, "Hey, dude, I see you are trying to import - // foo and I see there is a package foo available in repository bar. Wanna, - // like, download and use it or something?" + // currently, we only have special handling of exe{} targets (search in + // PATH) simply failing for the rest. But in the future this coud be the + // place where we could call custom "last resort" import hooks. For example, + // we can hook a package manager that will say, "Hey, dude, I see you are + // trying to import foo and I see there is a package foo available in + // repository bar. Wanna, like, download and use it or something?" Though + // the latest thoughts indicate this is probably a bad idea (implicitness, + // complexity, etc). + // + // More specifically, we have the following kinds of import (tried in this + // order): + // + // ad hoc + // + // The target is imported by specifying its path directly with + // config.import.<proj>.<name>[.<type>]. For example, this can be + // used to import an installed target. + // + // + // normal + // + // The target is imported from a project that was either specified with + // config.import.<proj> or was found via the subproject search. This also + // loads the target's dependency information. + // + // + // rule-specific + // + // The target was imported in a rule-specific manner (e.g., a library was + // found in the compiler's search paths). + // + // + // fallback/default + // + // The target was found by the second phase of import (e.g., an executable + // was found in PATH). + + // Import phase 1. Return the imported target(s) as well as the kind of + // import that was performed with `fallback` indicating it was not found. + // + // If second is `fallback`, then first contains the original, project- + // qualified target. If second is `adhoc`, first may still contain a + // project-qualified target (which may or may not be the same as the + // original; see the config.import.<proj>.<name>[.<type>] logic for details) + // in which case it should still be passed to import phase 2. + // + // If phase2 is true then the phase 2 is performed right away (we call it + // immediate import). Note that if optional is true, phase2 must be true as + // well (and thus there is no rule-specific logic for optional imports). In + // case of optional, empty names value is retuned if nothing was found. + // + // If metadata is true, then load the target metadata. In this case phase2 + // must be true as well. // // Note also that we return names rather than a single name: while normally // it will be a single target name, it can be an out-qualified pair (if // someone wants to return a source target) but it can also be a non-target // since we don't restrict what users can import/export. // - LIBBUILD2_SYMEXPORT names - import (scope& base, name, const location&); - - LIBBUILD2_SYMEXPORT pair<name, dir_path> - import_search (scope& base, name, const location&, bool subproj = true); + // Finally, note that import is (and should be kept) idempotent or, more + // precisely, "accumulatively idempotent" in that additional steps may be + // performed (phase 2, loading of the metadata) unless already done. + // + enum class import_kind {adhoc, normal, fallback}; - LIBBUILD2_SYMEXPORT pair<names, const scope&> - import_load (context&, pair<name, dir_path>, const location&); + LIBBUILD2_SYMEXPORT pair<names, import_kind> + import (scope& base, + name, + bool phase2, + bool optional, + bool metadata, + const location&); + // Import phase 2. + // const target& import (context&, const prerequisite_key&); - // As above but only imports as an already existing target. Unlike the above - // version, this one can be called during the execute phase. + // As above but import the target "here and now" without waiting for phase 2 + // (and thus omitting any rule-specific logic). This version of import is, + // for example, used by build system modules to perform an implicit import + // of the corresponding tool. + // + // If phase2 is false, then the second phase's fallback/default logic is + // only invoked if the import was ad hoc (i.e., a relative path was + // specified via config.import.<proj>.<name>[.<type>]) with NULL returned + // otherwise. + // + // If phase2 is true and optional is true, then NULL is returned instead of + // failing if phase 2 could not find anything. + // + // If metadata is true, then load the target metadata. In this case phase2 + // must be true as well. + // + // The what argument specifies what triggered the import (for example, + // "module load") and is used in diagnostics. + // + // This function also returns the kind of import that was performed. + // + pair<const target*, import_kind> + import_direct (scope& base, + name, + bool phase2, + bool optional, + bool metadata, + const location&, + const char* what = "import"); + + // As above but also return (in new_value) an indication of whether this + // import is based on a new config.* value. See config::lookup_config() for + // details. Note that a phase 2 fallback/default logic is not considered new + // (though this can be easily adjusted based on import kind). + // + LIBBUILD2_SYMEXPORT pair<const target*, import_kind> + import_direct (bool& new_value, + scope& base, + name, + bool phase2, + bool optional, + bool metadata, + const location&, + const char* what = "import"); + + + template <typename T> + pair<const T*, import_kind> + import_direct (scope&, + name, bool, bool, bool, + const location&, const char* = "import"); + + template <typename T> + pair<const T*, import_kind> + import_direct (bool&, + scope&, + name, + bool, bool, bool, + const location&, const char* = "import"); + + // Print import_direct<exe>() result either as a target for a normal import + // or as a process path for ad hoc and fallback imports. Normally used in + // build system modules to print the configuration report. + // + LIBBUILD2_SYMEXPORT ostream& + operator<< (ostream&, const pair<const exe*, import_kind>&); + + // As import phase 2 but only imports as an already existing target. But + // unlike it, this function can be called during the execute phase. // // Note: similar to search_existing(). // const target* import_existing (context&, const prerequisite_key&); + // Lower-level components of phase 1 (see implementation for details). + // + pair<name, optional<dir_path>> + import_search (scope& base, + name, + bool optional_, + const optional<string>& metadata, // False or metadata key. + bool subprojects, + const location&, + const char* what = "import"); + + // As above but also return (in new_value) an indication of whether this + // import is based on a new config.* value. See config::lookup_config() + // for details. + // + LIBBUILD2_SYMEXPORT pair<name, optional<dir_path>> + import_search (bool& new_value, + scope& base, + name, + bool optional_, + const optional<string>& metadata, + bool subprojects, + const location&, + const char* what = "import"); + + LIBBUILD2_SYMEXPORT pair<names, const scope&> + import_load (context&, + pair<name, optional<dir_path>>, + bool metadata, + const location&); + // Create a build system project in the specified directory. // LIBBUILD2_SYMEXPORT void diff --git a/libbuild2/file.ixx b/libbuild2/file.ixx index e940eb3..7c09d3d 100644 --- a/libbuild2/file.ixx +++ b/libbuild2/file.ixx @@ -11,20 +11,76 @@ namespace build2 return source_once (root, base, bf, base); } + inline pair<name, optional<dir_path>> + import_search (scope& base, + name tgt, + bool opt, const optional<string>& md, bool sp, + const location& loc, const char* w) + { + bool dummy (false); + return import_search (dummy, base, move (tgt), opt, md, sp, loc, w); + } + LIBBUILD2_SYMEXPORT const target* - import (context&, const prerequisite_key&, bool existing); + import (context&, + const prerequisite_key&, + bool optional_, + const optional<string>& metadata, // False or metadata key. + bool existing, + const location&); inline const target& import (context& ctx, const prerequisite_key& pk) { assert (ctx.phase == run_phase::match); - return *import (ctx, pk, false); + + // @@ We no longer have location. This is especially bad for the empty + // project case (i.e., where do I need to specify the project name)? + // Looks like the only way to do this is to keep location in name and + // then in prerequisite. Perhaps one day... + // + return *import (ctx, pk, false, nullopt, false, location ()); + } + + inline pair<const target*, import_kind> + import_direct (scope& base, + name tgt, + bool ph2, bool opt, bool md, + const location& loc, const char* w) + { + bool dummy (false); + return import_direct (dummy, base, move (tgt), ph2, opt, md, loc, w); + } + + template <typename T> + inline pair<const T*, import_kind> + import_direct (scope& base, + name tgt, + bool ph2, bool opt, bool md, + const location& loc, const char* w) + { + auto r (import_direct (base, move (tgt), ph2, opt, md, loc, w)); + return make_pair (r.first != nullptr ? &r.first->as<const T> () : nullptr, + r.second); + } + + template <typename T> + inline pair<const T*, import_kind> + import_direct (bool& nv, + scope& base, + name tgt, + bool ph2, bool opt, bool md, + const location& loc, const char* w) + { + auto r (import_direct (nv, base, move (tgt), ph2, opt, md, loc, w)); + return make_pair (r.first != nullptr ? &r.first->as<const T> () : nullptr, + r.second); } inline const target* import_existing (context& ctx, const prerequisite_key& pk) { assert (ctx.phase == run_phase::match || ctx.phase == run_phase::execute); - return import (ctx, pk, true); + return import (ctx, pk, false, nullopt, true, location ()); } } diff --git a/libbuild2/forward.hxx b/libbuild2/forward.hxx index b96aa31..1679b4a 100644 --- a/libbuild2/forward.hxx +++ b/libbuild2/forward.hxx @@ -55,10 +55,13 @@ namespace build2 class include_type; struct prerequisite_member; + // <libbuild2/prerequisite-key.hxx> + // + class prerequisite_key; + // <libbuild2/prerequisite.hxx> // class prerequisite; - class prerequisite_key; // <libbuild2/rule.hxx> // diff --git a/libbuild2/function.test.cxx b/libbuild2/function.test.cxx index e059619..514ac1e 100644 --- a/libbuild2/function.test.cxx +++ b/libbuild2/function.test.cxx @@ -124,7 +124,7 @@ namespace build2 scope& s (ctx.global_scope.rw ()); parser p (ctx); - p.parse_buildfile (cin, path_name ("buildfile"), s, s); + p.parse_buildfile (cin, path_name ("buildfile"), &s, s); } catch (const failed&) { diff --git a/libbuild2/module.cxx b/libbuild2/module.cxx index 28d2468..3abb102 100644 --- a/libbuild2/module.cxx +++ b/libbuild2/module.cxx @@ -147,18 +147,32 @@ namespace build2 // (remember, if it's top-level, then it must be in an isolated // configuration). // - pair<name, dir_path> ir ( + pair<name, optional<dir_path>> ir ( import_search (bs, name (proj, dir_path (), "libs", "build2-" + mod), - loc, - nested /* subprojects */)); + opt, + nullopt /* metadata */, + nested /* subprojects */, + loc)); - if (!ir.second.empty ()) + if (ir.first.empty ()) { + assert (opt); + return nullptr; + } + + if (ir.second) + { + // What if a module is specified with config.import.<mod>.<lib>.libs? + // Note that this could still be a project-qualified target. + // + if (ir.second->empty ()) + fail (loc) << "direct module target importation not yet supported"; + // We found the module as a target in a project. Now we need to update // the target (which will also give us the shared library path). // - l5 ([&]{trace << "found " << ir.first << " in " << ir.second;}); + l5 ([&]{trace << "found " << ir.first << " in " << *ir.second;}); // Create the build context if necessary. // @@ -215,7 +229,8 @@ namespace build2 // Load the imported project in the module context. // - pair<names, const scope&> lr (import_load (ctx, move (ir), loc)); + pair<names, const scope&> lr ( + import_load (ctx, move (ir), false /* metadata */, loc)); l5 ([&]{trace << "loaded " << lr.first;}); @@ -638,7 +653,7 @@ namespace build2 // Note that it would have been nice to keep these inline but we need the // definition of scope for the variable lookup. // - bool + const shared_ptr<module>* load_module (scope& rs, scope& bs, const string& name, @@ -646,8 +661,18 @@ namespace build2 bool opt, const variable_map& hints) { - return cast_false<bool> (bs[name + ".loaded"]) || - init_module (rs, bs, name, loc, opt, hints); + if (cast_false<bool> (bs[name + ".loaded"])) + { + if (cast_false<bool> (bs[name + ".configured"])) + return &rs.root_extra->modules.find (name)->second.module; + } + else + { + if (module_state* ms = init_module (rs, bs, name, loc, opt, hints)) + return &ms->module; + } + + return nullptr; } const shared_ptr<module>& @@ -657,6 +682,9 @@ namespace build2 const location& loc, const variable_map& hints) { + //@@ TODO: shouldn't we also check for configured? What if the previous + // attempt to load it was optional? + return cast_false<bool> (bs[name + ".loaded"]) ? rs.root_extra->modules.find (name)->second.module : init_module (rs, bs, name, loc, false /* optional */, hints)->module; diff --git a/libbuild2/module.hxx b/libbuild2/module.hxx index f5c726e..b2b42e4 100644 --- a/libbuild2/module.hxx +++ b/libbuild2/module.hxx @@ -170,9 +170,18 @@ namespace build2 // A wrapper over init_module() to use from other modules that incorporates // the <name>.loaded variable check (use init_module() directly to sidestep - // this check). + // this check). Return a pointer to the pointer to the module instance if it + // was both successfully loaded and configured and NULL otherwise (so can be + // used as bool). // - LIBBUILD2_SYMEXPORT bool + // Note also that NULL can be returned even of optional is false which + // happens if the requested module has already been loaded but failed to + // configure. The function could have issued diagnostics but the caller can + // normally provide additional information. + // + // Note: for non-optional modules use the versions below. + // + LIBBUILD2_SYMEXPORT const shared_ptr<module>* load_module (scope& root, scope& base, const string& name, diff --git a/libbuild2/name.hxx b/libbuild2/name.hxx index f623110..d0e8d85 100644 --- a/libbuild2/name.hxx +++ b/libbuild2/name.hxx @@ -50,6 +50,10 @@ namespace build2 name (dir_path d, string t, string v) : dir (move (d)), type (move (t)), value (move (v)) {} + name (string p, dir_path d, string t, string v) + : proj (project_name (move (p))), dir (move (d)), type (move (t)), + value (move (v)) {} + name (optional<project_name> p, dir_path d, string t, string v) : proj (move (p)), dir (move (d)), type (move (t)), value (move (v)) {} diff --git a/libbuild2/parser.cxx b/libbuild2/parser.cxx index cd6fe5e..a5c34d9 100644 --- a/libbuild2/parser.cxx +++ b/libbuild2/parser.cxx @@ -222,37 +222,53 @@ namespace build2 } void parser:: - parse_buildfile (istream& is, const path_name& in, scope& root, scope& base) + parse_buildfile (istream& is, + const path_name& in, + scope* root, + scope& base, + target* tgt, + prerequisite* prq) { lexer l (is, in); - parse_buildfile (l, root, base); + parse_buildfile (l, root, base, tgt, prq); } void parser:: - parse_buildfile (lexer& l, scope& root, scope& base) + parse_buildfile (lexer& l, + scope* root, + scope& base, + target* tgt, + prerequisite* prq) { path_ = &l.name (); lexer_ = &l; - root_ = &root; + root_ = root; scope_ = &base; - target_ = nullptr; - prerequisite_ = nullptr; + target_ = tgt; + prerequisite_ = prq; pbase_ = scope_->src_path_; - enter_buildfile (*path_); // Needs scope_. + if (path_->path != nullptr) + enter_buildfile (*path_->path); // Note: needs scope_. token t; type tt; next (t, tt); - parse_clause (t, tt); + if (target_ != nullptr || prerequisite_ != nullptr) + { + parse_variable_block (t, tt); + } + else + { + parse_clause (t, tt); + process_default_target (t); + } if (tt != type::eos) fail (t) << "unexpected " << t; - - process_default_target (t); } token parser:: @@ -385,7 +401,9 @@ namespace build2 { f = &parser::parse_run; } - else if (n == "import") + else if (n == "import" || + n == "import?" || + n == "import!") { f = &parser::parse_import; } @@ -892,7 +910,7 @@ namespace build2 // NULL, then this is a target type/pattern-specific block. // // enter: first token of first line in the block (normal lexer mode) - // leave: rcbrace + // leave: rcbrace or eos // // This is a more restricted variant of parse_clause() that only allows // variable assignments. @@ -1098,6 +1116,8 @@ namespace build2 // name n (tt != type::colon ? move (pn) : pn); + // See also scope::find_prerequisite_key(). + // auto rp (scope_->find_target_type (n, ploc)); const target_type* tt (rp.first); optional<string>& e (rp.second); @@ -1210,7 +1230,7 @@ namespace build2 { next (t, tt); // First token inside the block. - parse_variable_block (t, tt, nullptr, string ()); + parse_variable_block (t, tt); if (tt != type::rcbrace) fail (t) << "expected '}' instead of " << t; @@ -1318,7 +1338,7 @@ namespace build2 { next (t, tt); // First token inside the block. - parse_variable_block (t, tt, nullptr, string ()); + parse_variable_block (t, tt); if (tt != type::rcbrace) fail (t) << "expected '}' instead of " << t; @@ -1333,18 +1353,14 @@ namespace build2 } void parser:: - source (istream& is, - const path_name& in, - const location& loc, - bool enter, - bool deft) + source (istream& is, const path_name& in, const location& loc, bool deft) { tracer trace ("parser::source", &path_); l5 ([&]{trace (loc) << "entering " << in;}); - if (enter) - enter_buildfile (in); + if (in.path != nullptr) + enter_buildfile (*in.path); const path_name* op (path_); path_ = ∈ @@ -1417,7 +1433,6 @@ namespace build2 source (ifs, path_name (p), get_location (t), - true /* enter */, false /* default_target */); } catch (const io_error& e) @@ -1558,8 +1573,7 @@ namespace build2 source (ifs, path_name (p), get_location (t), - true /* enter */, - true /* default_target */); + true /* default_target */); } catch (const io_error& e) { @@ -1642,7 +1656,6 @@ namespace build2 source (is, path_name ("<stdout>"), l, - false /* enter */, false /* default_target */); } @@ -1689,13 +1702,10 @@ namespace build2 // the config[.**].<project>.** pattern where <project> is the innermost // named project. // - // Note that we currently don't allow just the config.<project> name even - // though this is used quite liberally in build system modules. Allowing - // this will complicate the logic (and documentation) a bit and there are - // no obvious use-cases. On the other hand, for tools that could be used - // during the build (say yacc), such a variable would most likely be used - // to specify its location (say config.yacc) . So let's "reserve" it for - // now. + // Note that we also allow just the config.<project> name which can be + // used by tools (such as source code generators) that use themselves in + // their own build. This is a bit of an advanced/experimental setup so + // we leave this undocumented for now. // // What should we do if there is no named project? We used to fail but // there are valid cases where this can happen, for example, a standalone @@ -1788,7 +1798,7 @@ namespace build2 name.compare (0, 7, "config.") != 0) { if (!as.empty ()) - fail (t) << "unexpected attributes for report-only variable"; + fail (as.loc) << "unexpected attributes for report-only variable"; attributes_pop (); @@ -1822,9 +1832,15 @@ namespace build2 if (!proj.empty ()) { - if (name.find ('.' + proj + '.') == string::npos) + size_t p (name.find ('.' + proj)); + + if (p == string::npos || + ((p += proj.size () + 1) != name.size () && // config.<proj> + name[p] != '.')) // config.<proj>. + { dr << fail (t) << "configuration variable '" << name << "' does not include project name"; + } } if (!dr.empty ()) @@ -1937,8 +1953,11 @@ namespace build2 // General import format: // - // import [<var>=](<project>|<project>/<target>])+ + // import[?!] [<attrs>] [<var>=](<project>|<project>%<target>])+ // + bool opt (t.value.back () == '?'); + bool ph2 (opt || t.value.back () == '!'); + type atype; // Assignment type. value* val (nullptr); const variable* var (nullptr); @@ -1950,13 +1969,38 @@ namespace build2 // switch to the value mode, get the first token, and then re-parse it // manually looking for =/=+/+=. // + // Note that if we ever wanted to support value attributes, that would be + // non-trivial. + // mode (lexer_mode::value, '@'); next_with_attributes (t, tt); - // Get variable attributes, if any (note that here we will go into a - // nested value mode with a different pair character). + // 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. // - auto at (attributes_push (t, tt)); + attributes_push (t, tt); + attributes& as (attributes_top ()); + + bool meta (false); + for (auto i (as.begin ()); i != as.end (); ) + { + if (i->name == "metadata") + { + if (!ph2) + fail (as.loc) << "loading metadata requires immediate import" << + info << "consider using the import! directive instead"; + + meta = true; + } + else + { + ++i; + continue; + } + + i = as.erase (i); + } const location vloc (get_location (t)); @@ -1964,8 +2008,9 @@ namespace build2 { // Split the token into the variable name and value at position (p) of // '=', taking into account leading/trailing '+'. The variable name is - // returned while the token is set to value. If the resulting token - // value is empty, get the next token. Also set assignment type (at). + // returned while the token is set to the value part. If the resulting + // token value is empty, get the next token. Also set assignment type + // (at). // auto split = [&atype, &t, &tt, this] (size_t p) -> string { @@ -2042,10 +2087,10 @@ namespace build2 } else { - if (at.first) - fail (at.second) << "attributes without variable"; - else - attributes_pop (); + if (!as.empty ()) + fail (as.loc) << "attributes without variable"; + + attributes_pop (); } // The rest should be a list of projects and/or targets. Parse them as @@ -2064,19 +2109,27 @@ namespace build2 // import() will check the name, if required. // - names r (import (*scope_, move (n), l)); + names r (import (*scope_, move (n), ph2, opt, meta, l).first); if (val != nullptr) { - if (atype == type::assign) + if (r.empty ()) // Optional not found. { - val->assign (move (r), var); - atype = type::append; // Append subsequent values. + if (atype == type::assign) + *val = nullptr; } - else if (atype == type::prepend) - val->prepend (move (r), var); else - val->append (move (r), var); + { + if (atype == type::assign) + val->assign (move (r), var); + else if (atype == type::prepend) + val->prepend (move (r), var); + else + val->append (move (r), var); + } + + if (atype == type::assign) + atype = type::append; // Append subsequent values. } } @@ -6170,11 +6223,10 @@ namespace build2 } void parser:: - enter_buildfile (const path_name& pn) + enter_buildfile (const path& p) { tracer trace ("parser::enter_buildfile", &path_); - const path& p (pn.path != nullptr ? *pn.path : path ()); dir_path d (p.directory ()); // Empty for a path name with the NULL path. // Figure out if we need out. @@ -6190,8 +6242,8 @@ namespace build2 ctx.targets.insert<buildfile> ( move (d), move (out), - pn.name ? *pn.name : p.leaf ().base ().string (), - p.extension (), // Always specified. + p.leaf ().base ().string (), + p.extension (), // Always specified. trace); } diff --git a/libbuild2/parser.hxx b/libbuild2/parser.hxx index 6552114..c55e14f 100644 --- a/libbuild2/parser.hxx +++ b/libbuild2/parser.hxx @@ -33,11 +33,17 @@ namespace build2 void parse_buildfile (istream&, const path_name&, - scope& root, - scope& base); + scope* root, + scope& base, + target* = nullptr, + prerequisite* = nullptr); void - parse_buildfile (lexer&, scope& root, scope& base); + parse_buildfile (lexer&, + scope* root, + scope& base, + target* = nullptr, + prerequisite* = nullptr); buildspec parse_buildspec (istream&, const path_name&); @@ -51,7 +57,7 @@ namespace build2 names parse_export_stub (istream& is, const path_name& name, scope& r, scope& b) { - parse_buildfile (is, name, r, b); + parse_buildfile (is, name, &r, b); return move (export_value); } @@ -99,7 +105,9 @@ namespace build2 parse_clause (token&, token_type&, bool one = false); void - parse_variable_block (token&, token_type&, const target_type*, string); + parse_variable_block (token&, token_type&, + const target_type* = nullptr, + string = string ()); // Ad hoc target names inside < ... >. // @@ -270,14 +278,14 @@ namespace build2 attributes& attributes_top () {return attributes_.back ();} - // Source a stream optionnaly entering it as a buildfile and performing - // the default target processing. + // Source a stream optionnaly performing the default target processing. + // If the specified path name has a real path, then also enter it as a + // buildfile. // void source (istream&, const path_name&, const location&, - bool enter, bool default_target); // The what argument is used in diagnostics (e.g., "expected <what> @@ -494,7 +502,7 @@ namespace build2 // Enter buildfile as a target. // void - enter_buildfile (const path_name&); + enter_buildfile (const path&); // Lexer. // diff --git a/libbuild2/prerequisite-key.hxx b/libbuild2/prerequisite-key.hxx new file mode 100644 index 0000000..12b94a8 --- /dev/null +++ b/libbuild2/prerequisite-key.hxx @@ -0,0 +1,44 @@ +// file : libbuild2/prerequisite-key.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_PREREQUISITE_KEY_HXX +#define LIBBUILD2_PREREQUISITE_KEY_HXX + +#include <libbuild2/types.hxx> +#include <libbuild2/forward.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/target-key.hxx> +#include <libbuild2/target-type.hxx> + +#include <libbuild2/export.hxx> + +namespace build2 +{ + // Light-weight (by being shallow-pointing) prerequisite key, similar + // to (and based on) target key. + // + // Note that unlike prerequisite, the key is not (necessarily) owned by a + // target. So for the key we instead have the base scope of the target that + // (would) own it. Note that we assume keys to be ephemeral enough for the + // base scope to remain unchanged. + // + class prerequisite_key + { + public: + using scope_type = build2::scope; + + const optional<project_name>& proj; + target_key tk; // The .dir and .out members can be relative. + const scope_type* scope; // Can be NULL if tk.dir is absolute. + + template <typename T> + bool is_a () const {return tk.is_a<T> ();} + bool is_a (const target_type& tt) const {return tk.is_a (tt);} + }; + + LIBBUILD2_SYMEXPORT ostream& + operator<< (ostream&, const prerequisite_key&); +} + +#endif // LIBBUILD2_PREREQUISITE_KEY_HXX diff --git a/libbuild2/prerequisite.hxx b/libbuild2/prerequisite.hxx index 9448fbc..476ed9d 100644 --- a/libbuild2/prerequisite.hxx +++ b/libbuild2/prerequisite.hxx @@ -13,36 +13,12 @@ #include <libbuild2/variable.hxx> #include <libbuild2/target-key.hxx> #include <libbuild2/diagnostics.hxx> +#include <libbuild2/prerequisite-key.hxx> #include <libbuild2/export.hxx> namespace build2 { - // Light-weight (by being shallow-pointing) prerequisite key, similar - // to (and based on) target key. - // - // Note that unlike prerequisite, the key is not (necessarily) owned by a - // target. So for the key we instead have the base scope of the target that - // (would) own it. Note that we assume keys to be ephemeral enough for the - // base scope to remain unchanged. - // - class prerequisite_key - { - public: - using scope_type = build2::scope; - - const optional<project_name>& proj; - target_key tk; // The .dir and .out members can be relative. - const scope_type* scope; // Can be NULL if tk.dir is absolute. - - template <typename T> - bool is_a () const {return tk.is_a<T> ();} - bool is_a (const target_type& tt) const {return tk.is_a (tt);} - }; - - LIBBUILD2_SYMEXPORT ostream& - operator<< (ostream&, const prerequisite_key&); - // Note that every data member except for the target is immutable (const). // class LIBBUILD2_SYMEXPORT prerequisite diff --git a/libbuild2/scope.cxx b/libbuild2/scope.cxx index 842b839..92fc12c 100644 --- a/libbuild2/scope.cxx +++ b/libbuild2/scope.cxx @@ -738,7 +738,7 @@ namespace build2 auto r (find_target_type (n, loc)); if (r.first == nullptr) - fail (loc) << "unknown target type " << n.type; + fail (loc) << "unknown target type " << n.type << " in " << n; bool src (n.pair); // If out-qualified, then it is from src. if (src) @@ -784,20 +784,54 @@ namespace build2 if (n == (ns[0].pair ? 2 : 1)) { name dummy; - target_key r (find_target_key (ns[0], n == 1 ? dummy : ns[1], loc)); - - return target_key { - r.type, - r.dir, - n == 1 ? &empty_dir_path : r.out, - r.name, - move (r.ext)}; + return find_target_key (ns[0], n == 1 ? dummy : ns[1], loc); } } fail (loc) << "invalid target name: " << ns << endf; } + pair<const target_type&, optional<string>> scope:: + find_prerequisite_type (name& n, name& o, const location& loc) const + { + auto r (find_target_type (n, loc)); + + if (r.first == nullptr) + fail (loc) << "unknown target type " << n.type << " in " << n; + + if (n.pair) // If out-qualified, then it is from src. + { + assert (n.pair == '@'); + + if (!o.directory ()) + fail (loc) << "expected directory after '@'"; + } + + if (!n.dir.empty ()) + n.dir.normalize (false, true); // Current dir collapses to an empty one. + + if (!o.dir.empty ()) + o.dir.normalize (false, true); // Ditto. + + return pair<const target_type&, optional<string>> ( + *r.first, move (r.second)); + } + + prerequisite_key scope:: + find_prerequisite_key (names& ns, const location& loc) const + { + if (size_t n = ns.size ()) + { + if (n == (ns[0].pair ? 2 : 1)) + { + name dummy; + return find_prerequisite_key (ns[0], n == 1 ? dummy : ns[1], loc); + } + } + + fail (loc) << "invalid prerequisite name: " << ns << endf; + } + static target* derived_tt_factory (context& c, const target_type& t, dir_path d, dir_path o, string n) diff --git a/libbuild2/scope.hxx b/libbuild2/scope.hxx index e1cdf78..e7c2db7 100644 --- a/libbuild2/scope.hxx +++ b/libbuild2/scope.hxx @@ -19,6 +19,7 @@ #include <libbuild2/target-key.hxx> #include <libbuild2/target-type.hxx> #include <libbuild2/target-state.hxx> +#include <libbuild2/prerequisite-key.hxx> #include <libbuild2/export.hxx> @@ -330,6 +331,20 @@ namespace build2 target_key find_target_key (names&, const location&) const; + // Similar to the find_target_type() but does not complete relative + // directories. + // + pair<const target_type&, optional<string>> + find_prerequisite_type (name&, name&, const location&) const; + + // As above, but return a prerequisite key. + // + prerequisite_key + find_prerequisite_key (name&, name&, const location&) const; + + prerequisite_key + find_prerequisite_key (names&, const location&) const; + // Dynamically derive a new target type from an existing one. Return the // reference to the target type and an indicator of whether it was // actually created. @@ -497,6 +512,12 @@ namespace build2 // NULL means no strong amalgamtion. }; + inline bool + operator== (const scope& x, const scope& y) { return &x == &y; } + + inline bool + operator!= (const scope& x, const scope& y) { return !(x == y); } + inline ostream& operator<< (ostream& os, const scope& s) { diff --git a/libbuild2/scope.ixx b/libbuild2/scope.ixx index f13aa70..9aecd48 100644 --- a/libbuild2/scope.ixx +++ b/libbuild2/scope.ixx @@ -55,7 +55,28 @@ namespace build2 find_target_key (name& n, name& o, const location& loc) const { auto p (find_target_type (n, o, loc)); - return target_key {&p.first, &n.dir, &o.dir, &n.value, move (p.second)}; + return target_key { + &p.first, + &n.dir, + o.dir.empty () ? &empty_dir_path : &o.dir, + &n.value, + move (p.second)}; + } + + inline prerequisite_key scope:: + find_prerequisite_key (name& n, name& o, const location& loc) const + { + auto p (find_prerequisite_type (n, o, loc)); + return prerequisite_key { + n.proj, + { + &p.first, + &n.dir, + o.dir.empty () ? &empty_dir_path : &o.dir, + &n.value, + move (p.second) + }, + this}; } inline dir_path diff --git a/libbuild2/search.cxx b/libbuild2/search.cxx index 0ff49ac..5887138 100644 --- a/libbuild2/search.cxx +++ b/libbuild2/search.cxx @@ -6,8 +6,8 @@ #include <libbuild2/scope.hxx> #include <libbuild2/target.hxx> #include <libbuild2/filesystem.hxx> // mtime() -#include <libbuild2/prerequisite.hxx> #include <libbuild2/diagnostics.hxx> +#include <libbuild2/prerequisite-key.hxx> using namespace std; using namespace butl; diff --git a/libbuild2/target-key.hxx b/libbuild2/target-key.hxx index bd9b8c7..0096d46 100644 --- a/libbuild2/target-key.hxx +++ b/libbuild2/target-key.hxx @@ -19,7 +19,7 @@ namespace build2 { // Light-weight (by being shallow-pointing) target key. // - class target_key + class LIBBUILD2_SYMEXPORT target_key { public: const target_type* const type; @@ -31,6 +31,11 @@ namespace build2 template <typename T> bool is_a () const {return type->is_a<T> ();} bool is_a (const target_type& tt) const {return type->is_a (tt);} + + // Return the target name or a pair of names if out-qualified. + // + names + as_name () const; }; inline bool diff --git a/libbuild2/target.cxx b/libbuild2/target.cxx index b49071a..d313b05 100644 --- a/libbuild2/target.cxx +++ b/libbuild2/target.cxx @@ -42,6 +42,27 @@ namespace build2 return false; } + // target_key + // + names target_key:: + as_name () const + { + names r; + + string v (*name); + target::combine_name (v, ext, false /* @@ TODO: what to do? */); + + r.push_back (build2::name (*dir, type->name, move (v))); + + if (!out->empty ()) + { + r.front ().pair = '@'; + r.push_back (build2::name (*out)); + } + + return r; + } + // target_state // static const char* const target_state_[] = diff --git a/libbuild2/target.hxx b/libbuild2/target.hxx index ea72925..6edfe5c 100644 --- a/libbuild2/target.hxx +++ b/libbuild2/target.hxx @@ -138,7 +138,7 @@ namespace build2 class LIBBUILD2_SYMEXPORT target { public: - // Context this scope belongs to. + // Context this target belongs to. // context& ctx; @@ -305,6 +305,12 @@ namespace build2 target_key key () const; + names + as_name () const + { + return key ().as_name (); + } + // Scoping. // public: @@ -764,6 +770,15 @@ namespace build2 virtual const target_type& dynamic_type () const = 0; static const target_type static_type; + // RW access. + // + target& + rw () const + { + assert (ctx.phase == run_phase::load); + return const_cast<target&> (*this); + } + public: // Split the name leaf into target name (in place) and extension // (returned). @@ -1529,8 +1544,15 @@ namespace build2 // // An empty path may signify special unknown/undetermined/unreal location // (for example, a binless library or an installed import library -- we - // know the DLL is there, just not exactly where). In this case you would - // also normally set its mtime. + // know the DLL is there, just not exactly where). In this case you could + // also set its mtime to timestamp_unreal (but don't have to, if a real + // timestamp can be derived, for example, the from the import library in + // the DLL case above). + // + // Note, however, that a target with timestamp_unreal does not have to + // have an empty path. One consequence of this arrangement (assigned path + // with unreal timestamp) is that the timestamp of such a target when used + // as a prerequisite won't affect the dependent's target out-of-date-ness. // // We used to return a pointer to properly distinguish between not set and // empty but that proved too tedious to work with. So now we return empty @@ -1705,9 +1727,44 @@ namespace build2 public: using file::file; + using process_path_type = build2::process_path; + + // Return the process path of this executable target. Normally it will be + // the absolute path returned by path() but can also be something custom + // if, for example, the target was found via a PATH search (see import for + // details). The idea is to use this path if we need to execute the target + // in which case, for the above example, we will see a short (recall) path + // instead of the absolute one in diagnostics. + // + process_path_type + process_path () const + { + // It's unfortunate we have to return by value but hopefully the + // compiler will see through it. Note also that returning empty + // process path if path is empty. + // + return process_path_.empty () + ? process_path_type (path ().string ().c_str (), + path_type (), + path_type ()) + : process_path_type (process_path_, false /* init */); + } + + // Note that setting the custom process path is not MT-safe and must be + // done while holding the insertion lock. + // + void + process_path (process_path_type p) + { + process_path_ = move (p); + } + public: static const target_type static_type; virtual const target_type& dynamic_type () const {return static_type;} + + private: + process_path_type process_path_; }; class LIBBUILD2_SYMEXPORT buildfile: public file diff --git a/libbuild2/target.txx b/libbuild2/target.txx index 0e4d9bf..b482d64 100644 --- a/libbuild2/target.txx +++ b/libbuild2/target.txx @@ -5,7 +5,6 @@ #include <libbuild2/scope.hxx> #include <libbuild2/diagnostics.hxx> -#include <libbuild2/prerequisite.hxx> namespace build2 { diff --git a/libbuild2/utility.hxx b/libbuild2/utility.hxx index 3a5e708..9800d6c 100644 --- a/libbuild2/utility.hxx +++ b/libbuild2/utility.hxx @@ -579,7 +579,7 @@ namespace build2 void append_options (sha256&, T&, const char*); - // As above but from the strings value directly. + // As above but from the lookup directly. // LIBBUILD2_SYMEXPORT void append_options (cstrings&, const lookup&, const char* excl = nullptr); @@ -590,6 +590,8 @@ namespace build2 LIBBUILD2_SYMEXPORT void append_options (sha256&, const lookup&); + // As above but from the strings value directly. + // void append_options (cstrings&, const strings&, const char* excl = nullptr); |