diff options
Diffstat (limited to 'libbuild2/file.cxx')
-rw-r--r-- | libbuild2/file.cxx | 1075 |
1 files changed, 855 insertions, 220 deletions
diff --git a/libbuild2/file.cxx b/libbuild2/file.cxx index 7c20152..18147a2 100644 --- a/libbuild2/file.cxx +++ b/libbuild2/file.cxx @@ -4,9 +4,11 @@ #include <libbuild2/file.hxx> #include <cerrno> +#include <cstring> // strlen() #include <iomanip> // left, setw() #include <sstream> +#include <libbuild2/rule.hxx> #include <libbuild2/scope.hxx> #include <libbuild2/target.hxx> #include <libbuild2/context.hxx> @@ -28,6 +30,8 @@ namespace build2 { // Standard and alternative build file/directory naming schemes. // + extern const dir_path std_export_dir; + extern const dir_path alt_export_dir; // build: @@ -35,6 +39,7 @@ namespace build2 const dir_path std_root_dir (dir_path (std_build_dir) /= "root"); const dir_path std_bootstrap_dir (dir_path (std_build_dir) /= "bootstrap"); const dir_path std_build_build_dir (dir_path (std_build_dir) /= "build"); + const dir_path std_export_dir (dir_path (std_build_dir) /= "export"); const path std_root_file (std_build_dir / "root.build"); const path std_bootstrap_file (std_build_dir / "bootstrap.build"); @@ -52,6 +57,7 @@ namespace build2 const dir_path alt_root_dir (dir_path (alt_build_dir) /= "root"); const dir_path alt_bootstrap_dir (dir_path (alt_build_dir) /= "bootstrap"); const dir_path alt_build_build_dir (dir_path (alt_build_dir) /= "build"); + const dir_path alt_export_dir (dir_path (alt_build_dir) /= "export"); const path alt_root_file (alt_build_dir / "root.build2"); const path alt_bootstrap_file (alt_build_dir / "bootstrap.build2"); @@ -218,7 +224,7 @@ namespace build2 // Checking for plausability feels expensive since we have to recursively // traverse the directory tree. Note, however, that if the answer is // positive, then shortly after we will be traversing this tree anyway and - // presumably this time getting the data from the cash (we don't really + // presumably this time getting the data from the cache (we don't really // care about the negative answer since this is a degenerate case). // optional<path> bf; @@ -306,7 +312,7 @@ namespace build2 { tracer trace ("source_once"); - if (!once.buildfiles.insert (bf).second) + if (!once.root_extra->insert_buildfile (bf)) { l5 ([&]{trace << "skipping already sourced " << bf;}); return false; @@ -357,7 +363,7 @@ namespace build2 // try { - for (const dir_entry& de: dir_iterator (d, false /* ignore_dangling */)) + for (const dir_entry& de: dir_iterator (d, dir_iterator::no_follow)) { // If this is a link, then type() will try to stat() it. And if the // link is dangling or points to something inaccessible, it will fail. @@ -522,10 +528,14 @@ namespace build2 pair<scope&, scope*> switch_scope (scope& root, const dir_path& out_base, bool proj) { + context& ctx (root.ctx); + + assert (ctx.phase == run_phase::load); + // First, enter the scope into the map and see if it is in any project. If // it is not, then there is nothing else to do. // - auto i (root.ctx.scopes.rw (root).insert_out (out_base)); + auto i (ctx.scopes.rw (root).insert_out (out_base)); scope& base (*i->second.front ()); scope* rs (nullptr); @@ -546,7 +556,7 @@ namespace build2 // Switch to the new root scope. // - if (rs != &root) + if (rs != &root && !rs->root_extra->loaded) load_root (*rs); // Load new root(s) recursively. // Now we can figure out src_base and finish setting the scope. @@ -581,37 +591,37 @@ namespace build2 fail << "variable out_root expected as first line in " << f << endf; } + scope::root_extra_type:: + root_extra_type (scope& root, bool a) + : altn (a), + loaded (false), + + build_ext (a ? alt_build_ext : std_build_ext), + build_dir (a ? alt_build_dir : std_build_dir), + buildfile_file (a ? alt_buildfile_file : std_buildfile_file), + buildignore_file (a ? alt_buildignore_file : std_buildignore_file), + root_dir (a ? alt_root_dir : std_root_dir), + bootstrap_dir (a ? alt_bootstrap_dir : std_bootstrap_dir), + build_build_dir (a ? alt_build_build_dir : std_build_build_dir), + bootstrap_file (a ? alt_bootstrap_file : std_bootstrap_file), + root_file (a ? alt_root_file : std_root_file), + export_file (a ? alt_export_file : std_export_file), + src_root_file (a ? alt_src_root_file : std_src_root_file), + out_root_file (a ? alt_out_root_file : std_out_root_file), + + var_pool (&root.ctx, &root.ctx.var_pool.rw (root), nullptr) + { + root.var_pool_ = &var_pool; + } + static void setup_root_extra (scope& root, optional<bool>& altn) { assert (altn && root.root_extra == nullptr); - bool a (*altn); - - root.root_extra.reset ( - new scope::root_extra_type { - nullopt /* project */, - nullopt /* amalgamation */, - nullopt /* subprojects */, - a, - a ? alt_build_ext : std_build_ext, - a ? alt_build_dir : std_build_dir, - a ? alt_buildfile_file : std_buildfile_file, - a ? alt_buildignore_file : std_buildignore_file, - a ? alt_root_dir : std_root_dir, - a ? alt_bootstrap_dir : std_bootstrap_dir, - a ? alt_build_build_dir : std_build_build_dir, - a ? alt_bootstrap_file : std_bootstrap_file, - a ? alt_root_file : std_root_file, - a ? alt_export_file : std_export_file, - a ? alt_src_root_file : std_src_root_file, - a ? alt_out_root_file : std_out_root_file, - {}, /* meta_operations */ - {}, /* operations */ - {}, /* modules */ - {}, /* override_cache */ - {}, /* target_types */ - {}, /* environment */ - ""} /* environment_checksum */); + + context& ctx (root.ctx); + + root.root_extra.reset (new scope::root_extra_type (root, *altn)); // Enter built-in meta-operation and operation names. Loading of // modules (via the src bootstrap; see below) can result in @@ -621,9 +631,9 @@ namespace build2 root.insert_meta_operation (perform_id, mo_perform); root.insert_meta_operation (info_id, mo_info); - root.insert_operation (default_id, op_default); - root.insert_operation (update_id, op_update); - root.insert_operation (clean_id, op_clean); + root.insert_operation (default_id, op_default, nullptr); + root.insert_operation (update_id, op_update, ctx.var_update); + root.insert_operation (clean_id, op_clean, ctx.var_clean); } value& @@ -842,12 +852,35 @@ namespace build2 try { - for (const dir_entry& de: dir_iterator (d, true /* ignore_dangling */)) + // It's probably possible that a subproject can be a symlink with the + // link target, for example, being in a git submodule. Considering that, + // it makes sense to warn about dangling symlinks. + // + for (const dir_entry& de: + dir_iterator (d, dir_iterator::detect_dangling)) { + const path& n (de.path ()); + + // Skip hidden entries. + // + if (n.empty () || n.string ().front () == '.') + continue; + if (de.type () != entry_type::directory) + { + if (de.type () == entry_type::unknown) + { + bool sl (de.ltype () == entry_type::symlink); + + warn << "skipping " + << (sl ? "dangling symlink" : "inaccessible entry") << ' ' + << d / n; + } + continue; + } - dir_path sd (d / path_cast<dir_path> (de.path ())); + dir_path sd (d / path_cast<dir_path> (n)); bool src (false); optional<bool> altn; @@ -913,7 +946,9 @@ namespace build2 } void - bootstrap_src (scope& rs, optional<bool>& altn) + bootstrap_src (scope& rs, optional<bool>& altn, + optional<dir_path> aovr, + bool sovr) { tracer trace ("bootstrap_src"); @@ -943,13 +978,24 @@ namespace build2 rs.root_extra->project = nullptr; rs.root_extra->amalgamation = nullptr; rs.root_extra->subprojects = nullptr; + + // See GH issue #322. + // +#if 0 + assert (!aovr || aovr->empty ()); +#else + if (!(!aovr || aovr->empty ())) + fail << "amalgamation directory " << *aovr << " specified for simple " + << "project " << src_root << + info << "see https://github.com/build2/build2/issues/322 for details"; +#endif } // We assume that bootstrap out cannot load this file explicitly. It // feels wrong to allow this since that makes the whole bootstrap // process hard to reason about. But we may try to bootstrap the same // root scope multiple time. // - else if (rs.buildfiles.insert (bf).second) + else if (rs.root_extra->insert_buildfile (bf)) { // Extract the project name and amalgamation variable value so that // we can make them available while loading bootstrap.build. @@ -985,7 +1031,13 @@ namespace build2 const project_name pn (cast<project_name> (move (*pv))); rs.root_extra->project = &pn; - if (av && (av->null || av->empty ())) + // @@ We will still have original values in the variables during + // bootstrap. Not sure what we can do about that. But it seems + // harmless. + // + if (aovr) + rs.root_extra->amalgamation = aovr->empty () ? nullptr : &*aovr; + else if (av && (av->null || av->empty ())) rs.root_extra->amalgamation = nullptr; { @@ -1005,6 +1057,13 @@ namespace build2 fail << "variable " << *ctx.var_amalgamation << " expected as a " << "second line in " << bf; } + + // Replace the value if overridden. + // + // Note that root_extra::amalgamation will be re-pointed below. + // + if (aovr) + rs.vars.assign (ctx.var_amalgamation) = move (*aovr); } else { @@ -1022,6 +1081,11 @@ namespace build2 // which we convert to NULL below). When calculated, the NULL value // indicates that we are not amalgamated. // + // Before we used to assume that if there is an outer root scope, then + // that got to be our amalgamation. But it turns our this is not always + // the case (for example, a private host configuration in bpkg) and there + // could be an unbootstrapped project between us and an outer root scope. + // // Note: the amalgamation variable value is always a relative directory. // if (!simple) @@ -1032,66 +1096,84 @@ namespace build2 if (v && v.empty ()) // Convert empty to NULL. v = nullptr; - if (scope* ars = rs.parent_scope ()->root_scope ()) + scope* ars (rs.parent_scope ()->root_scope ()); + + if (rp.second) { - // We must not be amalgamated by a simple project. + // If the amalgamation variable hasn't been set, then we need to check + // if any of the outer directories is a project's out_root. If so, + // then that's (likely) our amalgamation. // - if (!ars->root_extra->project || *ars->root_extra->project != nullptr) - { - const dir_path& ad (ars->out_path ()); - dir_path rd (ad.relative (out_root)); + optional<bool> altn; + const dir_path& d (find_out_root (out_root.directory (), altn).first); - // If we already have the amalgamation variable set, verify that - // aroot matches its value. + if (!d.empty ()) + { + // Note that the sub() test is important: during configuration we + // may find a project that is outside the outer root scope in which + // case we should use the latter instead. // - if (!rp.second) - { - if (v) - { - const dir_path& vd (cast<dir_path> (v)); - - if (vd != rd) - { - fail << "inconsistent amalgamation of " << out_root << - info << "specified: " << vd << - info << "actual: " << rd << " by " << ad; - } - } - } - else + if (ars == nullptr || + (d != ars->out_path () && d.sub (ars->out_path ()))) { - // Otherwise, use the outer root as our amalgamation. - // + dir_path rd (d.relative (out_root)); l5 ([&]{trace << out_root << " amalgamated as " << rd;}); v = move (rd); + ars = nullptr; // Skip the checks blow. } + // Else fall through. + } + else + { + // Note that here ars may be not NULL. This can happen both when ars + // is a simple project or if out_root is in out directory that has + // no been configured. In this case falling through is what we want. } } - else if (rp.second) + else if (v) { - // If there is no outer root and the amalgamation variable hasn't been - // set, then we need to check if any of the outer directories is a - // project's out_root. If so, then that's our amalgamation. + if (cast<dir_path> (v).absolute ()) + fail << "absolute directory in variable " << *ctx.var_amalgamation + << " value"; + } + + // Do additional checks if the outer root could be our amalgamation. + // + if (ars != nullptr) + { + const dir_path& ad (ars->out_path ()); + + // If we have the amalgamation variable set by the user, verify that + // it's a subdirectory of the outer root scope. // - optional<bool> altn; - const dir_path& ad (find_out_root (out_root.directory (), altn).first); + // Note that in this case we allow amalgamation by a simple project + // (we rely on this, for example, in our modules sidebuild machinery). + // + if (!rp.second) + { + if (v) + { + const dir_path& vd (cast<dir_path> (v)); + dir_path d (out_root / vd); + d.normalize (); - if (!ad.empty ()) + if (!d.sub (ad)) + fail << "incorrect amalgamation " << vd << " of " << out_root; + } + } + // By default we do not get amalgamated by a simple project. + // + else if (!(ars->root_extra->project && + *ars->root_extra->project == nullptr)) { + // Otherwise, use the outer root as our amalgamation. + // dir_path rd (ad.relative (out_root)); + l5 ([&]{trace << out_root << " amalgamated as " << rd;}); v = move (rd); } - //@@ else: the value will be NULL and amalgamation will be disabled. - // We could omit setting it in root_extra... But maybe this is - // correct: we don't want to load half of the project as - // amalgamated and the other half as not, would we now? - } - // @@ else if (v): shouldn't we try to bootstrap a project in the - // user-specified directory? Though this case is not - // used outside of some controlled cases (like module - // sidebuilds). rs.root_extra->amalgamation = cast_null<dir_path> (v); } @@ -1112,6 +1194,14 @@ namespace build2 auto rp (rs.vars.insert (*ctx.var_subprojects)); // Set NULL by default. value& v (rp.first); + if (!sovr) + { + if (rp.second) + rp.second = false; // Keep NULL. + else + v = nullptr; // Make NULL. + } + if (rp.second) { // No subprojects set so we need to figure out if there are any. @@ -1268,9 +1358,9 @@ namespace build2 // Call module's post-boot functions. // - for (size_t i (0); i != root.root_extra->modules.size (); ++i) + for (size_t i (0); i != root.root_extra->loaded_modules.size (); ++i) { - module_state& s (root.root_extra->modules[i]); + module_state& s (root.root_extra->loaded_modules[i]); if (s.boot_post != nullptr) boot_post_module (root, s); @@ -1311,7 +1401,7 @@ namespace build2 } void - create_bootstrap_outer (scope& root) + create_bootstrap_outer (scope& root, bool subp) { context& ctx (root.ctx); @@ -1359,7 +1449,7 @@ namespace build2 setup_root (rs, forwarded (root, out_root, v.as<dir_path> (), altn)); bootstrap_pre (rs, altn); - bootstrap_src (rs, altn); + bootstrap_src (rs, altn, nullopt, subp); // bootstrap_post() delayed until after create_bootstrap_outer(). } else @@ -1370,7 +1460,7 @@ namespace build2 rs.assign (ctx.var_forwarded) = true; // Only upgrade (see main()). } - create_bootstrap_outer (rs); + create_bootstrap_outer (rs, subp); if (!bstrapped) bootstrap_post (rs); @@ -1458,22 +1548,19 @@ namespace build2 } void - load_root (scope& root) + load_root (scope& root, + const function<void (parser&)>& pre, + const function<void (parser&)>& post) { tracer trace ("load_root"); - context& ctx (root.ctx); - - const dir_path& out_root (root.out_path ()); - const dir_path& src_root (root.src_path ()); - - // As an optimization, check if we have already loaded root.build. If - // that's the case, then we have already been called for this project. - // - path f (src_root / root.root_extra->root_file); - - if (root.buildfiles.find (f) != root.buildfiles.end ()) + if (root.root_extra->loaded) + { + assert (pre == nullptr && post == nullptr); return; + } + + context& ctx (root.ctx); if (ctx.no_external_modules) fail << "attempt to load project " << root << " after skipped loading " @@ -1482,18 +1569,19 @@ namespace build2 // First load outer roots, if any. // if (scope* rs = root.parent_scope ()->root_scope ()) - load_root (*rs); + if (!rs->root_extra->loaded) + load_root (*rs); // Finish off initializing bootstrapped modules (before mode). // // Note that init() can load additional modules invalidating iterators. // auto init_modules = - [&root, n = root.root_extra->modules.size ()] (module_boot_init v) + [&root, n = root.root_extra->loaded_modules.size ()] (module_boot_init v) { for (size_t i (0); i != n; ++i) { - module_state& s (root.root_extra->modules[i]); + module_state& s (root.root_extra->loaded_modules[i]); if (s.boot_init && *s.boot_init == v) init_module (root, root, s.name, s.loc); @@ -1513,6 +1601,11 @@ namespace build2 // Load hooks and root.build. // + const dir_path& out_root (root.out_path ()); + const dir_path& src_root (root.src_path ()); + + path f (src_root / root.root_extra->root_file); + // We can load the pre hooks before finishing off loading the bootstrapped // modules (which, in case of config would load config.build) or after and // one can come up with a plausible use-case for either approach. Note, @@ -1528,10 +1621,22 @@ namespace build2 // parser p (ctx, load_stage::root); + if (pre != nullptr) + { + pre (p); + p.reset (); + } + if (he) {source_hooks (p, root, hd, true /* pre */); p.reset ();} if (fe) {source_once (p, root, root, f, root);} if (he) {p.reset (); source_hooks (p, root, hd, false /* pre */);} + if (post != nullptr) + { + p.reset (); + post (p); + } + // Finish off initializing bootstrapped modules (after mode). // { @@ -1539,12 +1644,19 @@ namespace build2 init_modules (module_boot_init::after); } - // Print the project configuration report, similar to how we do it in + // Print the project configuration report(s), similar to how we do it in // build system modules. // - if (!p.config_report.empty () && verb >= (p.config_report_new ? 2 : 3)) + using config_report = parser::config_report; + + const project_name* proj (nullptr); // Resolve lazily. + for (const config_report& cr: p.config_reports) { - const project_name& proj (named_project (root)); // Can be empty. + if (verb < (cr.new_value ? 2 : 3)) + continue; + + if (proj == nullptr) + proj = &named_project (root); // Can be empty. // @@ TODO/MAYBE: // @@ -1562,46 +1674,74 @@ namespace build2 // config @/tmp/tests // libhello.tests.remote true // - string stem (!proj.empty () ? '.' + proj.variable () + '.' : string ()); + // If the module name is not empty then it means the config variables + // are from the imported project and so we use that for <project>. + // + string stem (!cr.module.empty () + ? '.' + cr.module.variable () + '.' + : (!proj->empty () + ? '.' + proj->variable () + '.' + : string ())); - // Calculate max name length. + // Return the variable name for printing. // - size_t pad (10); - for (const pair<lookup, string>& lf: p.config_report) + auto name = [&stem] (const config_report::value& cv) -> const char* { - lookup l (lf.first); + lookup l (cv.val); - size_t n; if (l.value == nullptr) { - n = l.var->name.size (); + if (cv.org.empty ()) + return l.var->name.c_str (); + + // This case may or may not have the prefix. + // + size_t p, n ( + !stem.empty () + ? (p = cv.org.find (stem)) != string::npos ? p + stem.size () : 0 + : cv.org.compare (0, 7, "config.") == 0 ? 7 : 0); + + return cv.org.c_str () + n; } else { + assert (cv.org.empty ()); // Sanity check. + size_t p (!stem.empty () ? l.var->name.find (stem) + stem.size () : 7); // "config." - n = l.var->name.size () - p; + + return l.var->name.c_str () + p; } + }; + + // Calculate max name length. + // + size_t pad (10); + for (const config_report::value& cv: cr.values) + { + size_t n (strlen (name (cv))); if (n > pad) pad = n; } // Use the special `config` module name (which doesn't have its own - // report) for project configuration. + // report) for project's own configuration. // diag_record dr (text); - dr << "config " << proj << '@' << root; + dr << (cr.module.empty () ? "config" : cr.module.string ().c_str ()) + << ' ' << *proj << '@' << root; names storage; - for (const pair<lookup, string>& lf: p.config_report) + for (const config_report::value& cv: cr.values) { - lookup l (lf.first); - const string& f (lf.second); + lookup l (cv.val); + const string& f (cv.fmt); // If the report variable has been overriden, now is the time to - // lookup its value. + // lookup its value. Note: see also the name() lambda above if + // changing anything here. // string n; if (l.value == nullptr) @@ -1617,26 +1757,30 @@ namespace build2 n = string (l.var->name, p); } + const char* pn (name (cv)); // Print name. + dr << "\n "; - if (const value& v = *l) + if (l) { storage.clear (); - auto ns (reverse (v, storage)); + auto ns (reverse (*l, storage, true /* reduce */)); if (f == "multiline") { - dr << n; + dr << pn; for (auto& n: ns) dr << "\n " << n; } else - dr << left << setw (static_cast<int> (pad)) << n << ' ' << ns; + dr << left << setw (static_cast<int> (pad)) << pn << ' ' << ns; } else - dr << left << setw (static_cast<int> (pad)) << n << " [null]"; + dr << left << setw (static_cast<int> (pad)) << pn << " [null]"; } } + + root.root_extra->loaded = true; } scope& @@ -1673,7 +1817,8 @@ namespace build2 if (load) { - load_root (rs); + if (!rs.root_extra->loaded) + load_root (rs); setup_base (i, out_root, src_root); // Setup as base. } @@ -1728,8 +1873,10 @@ namespace build2 } // 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. + // --build2-metadata option. Key is the target name (and not necessarily the + // same as metadata variable prefix in export.metadata; e.g., openbsd-m4 and + // openbsd_m4). 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 @@ -1793,7 +1940,9 @@ namespace build2 try { // Note: not using run_*() functions since need to be able to suppress - // all errors, including inability to exec. + // all errors, including abnormal, inability to exec, etc., in case of + // optional import. Also, no need to buffer diagnostics since in the + // serial load. // if (verb >= 3) print_process (args); @@ -1853,10 +2002,19 @@ namespace build2 return r; if (!opt) - error (loc) << "invalid metadata signature in " << args[0] - << " output" << + { + diag_record dr; + dr << error (loc) << "invalid metadata signature in " << args[0] + << " output" << info << "expected '" << s << "'"; + if (verb >= 1 && verb <= 2) + { + dr << info << "command line: "; + print_process (dr, args); + } + } + goto fail; } @@ -1872,16 +2030,27 @@ namespace build2 if (pr.wait ()) { if (!opt) - error (loc) << "unable to read metadata from " << args[0]; + error (loc) << "io error reading 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. + // the result will be very confusing. So let's issue something generic + // for good measure. But also make it consistent with diagnostics + // issued by run_finish(). // if (!opt) - error (loc) << "unable to extract metadata from " << args[0]; + { + diag_record dr; + dr << error (loc) << "unable to extract metadata from " << args[0] << + info << "process " << args[0] << " " << *pr.exit; + + if (verb >= 1 && verb <= 2) + { + dr << info << "command line: "; + print_process (dr, args); + } + } } goto fail; @@ -1897,8 +2066,7 @@ namespace build2 goto fail; } - fail: - + fail: if (opt) { metadata_cache.insert (pp.effect_string (), true); @@ -1927,15 +2095,13 @@ namespace build2 &t); } - // Suggest appropriate ways to import the specified target (as type and - // name) from the specified project. - // - static void + void import_suggest (const diag_record& dr, const project_name& pn, - const target_type& tt, + const target_type* tt, const string& tn, - const char* qual = nullptr) + bool rule_hint, + const char* qual) { string pv (pn.variable ()); @@ -1947,15 +2113,19 @@ namespace build2 // Suggest ad hoc import but only if it's a path-based target (doing it // for lib{} is very confusing). // - if (tt.is_a<path_target> ()) + if (tt != nullptr && tt->is_a<path_target> ()) { - string v (tt.is_a<exe> () && (pv == tn || pn == tn) + string v (tt->is_a<exe> () && (pv == tn || pn == tn) ? "config." + pv - : "config.import." + pv + '.' + tn + '.' + tt.name); + : "config.import." + pv + '.' + tn + '.' + tt->name); dr << info << "or use " << v << " configuration variable to specify " << "its " << (qual != nullptr ? qual : "") << "path"; } + + if (rule_hint) + dr << info << "or use rule_hint attribute to specify a rule that can " + << "find this target"; } // Return the processed target name as well as the project directory, if @@ -1970,6 +2140,9 @@ namespace build2 // Return empty name if an ad hoc import resulted in a NULL target (only // allowed if optional is true). // + // Note that this function has a side effect of potentially marking some + // config.import.* variables as used. + // pair<name, optional<dir_path>> import_search (bool& new_value, scope& ibase, @@ -2001,6 +2174,9 @@ namespace build2 // // 4. Normal import. // + // @@ PERF: in quite a few places (local, subproject) we could have + // returned the scope and save on bootstrap in import_load(). + // if (tgt.unqualified ()) { if (tgt.directory () && tgt.relative ()) @@ -2008,6 +2184,8 @@ namespace build2 if (tgt.absolute ()) { + // Ad hoc import. + // // Actualize the directory to be analogous to the config.import.<proj> // case (which is of abs_dir_path type). // @@ -2024,7 +2202,7 @@ namespace build2 fail (loc) << "project-local importation of target " << tgt << " from an unnamed project"; - tgt.proj = pn; + tgt.proj = pn; // Reduce to normal import. return make_pair (move (tgt), optional<dir_path> (iroot.out_path ())); } @@ -2056,7 +2234,9 @@ namespace build2 // over anything that we may discover. In particular, we will prefer it // over any bundled subprojects. // - auto& vp (iroot.var_pool ()); + // Note: go straight for the public variable pool. + // + auto& vp (iroot.var_pool (true /* public */)); using config::lookup_config; @@ -2226,7 +2406,8 @@ namespace build2 auto df = make_diag_frame ( [&proj, tt, &on] (const diag_record& dr) { - import_suggest (dr, proj, *tt, on, "alternative "); + import_suggest ( + dr, proj, tt, on, false, "alternative "); }); md = extract_metadata (e->process_path (), @@ -2349,6 +2530,8 @@ namespace build2 { tracer trace ("import_load"); + uint64_t metav (meta ? 1 : 0); // Metadata version. + // We end up here in two cases: Ad hoc import, in which case name is // unqualified and absolute and path is a base, not necessarily root. And // normal import, in which case name must be project-qualified and path is @@ -2411,14 +2594,51 @@ namespace build2 } } + // First check the cache. + // + using import_key = context::import_key; + + auto cache_find = [&ctx, &tgt, metav] (dir_path& out_root) -> + const pair<names, const scope&>* + { + import_key k {move (out_root), move (tgt), metav}; + + auto i (ctx.import_cache.find (k)); + if (i != ctx.import_cache.end ()) + return &i->second; + + out_root = move (k.out_root); + tgt = move (k.target); + + return nullptr; + }; + + if (proj) + { + if (const auto* r = cache_find (out_root)) + return *r; + } + + dir_path cache_out_root; + // Clear current project's environment. // auto_project_env penv (nullptr); + // Note: this loop does at most two iterations. + // for (const scope* proot (nullptr); ; proot = root) { bool top (proot == nullptr); + // Check the cache for the subproject. + // + if (!top && proj) + { + if (const auto* r = cache_find (out_root)) + return *r; + } + root = create_root (ctx, out_root, src_root)->second.front (); bool bstrapped (bootstrapped (*root)); @@ -2497,6 +2717,8 @@ namespace build2 if (i != ps->end ()) { + cache_out_root = move (out_root); + const dir_path& d ((*i).second); altn = nullopt; out_root = root->out_path () / d; @@ -2508,9 +2730,72 @@ namespace build2 fail (loc) << out_root << " is not out_root for " << *proj; } + // Buildfile importation is quite different so handle it separately. + // + // Note that we don't need to load the project in this case. + // + // @@ For now we don't out-qualify the resulting target to be able to + // re-import it ad hoc (there is currently no support for out-qualified + // ad hoc import). Feels like this should be harmless since it's just a + // glorified path to a static file that nobody is actually going to use + // as a target (e.g., to depend upon). + // + if (tgt.type == "buildfile") + { + auto add_ext = [&altn] (string& n) + { + if (path_traits::find_extension (n) == string::npos) + { + if (n != (*altn ? alt_buildfile_file : std_buildfile_file).string ()) + { + n += "."; + n += *altn ? alt_build_ext : std_build_ext; + } + } + }; + + if (proj) + { + name n; + + if (src_root.empty ()) + src_root = root->src_path (); + + n.dir = move (src_root); + n.dir /= *altn ? alt_export_dir : std_export_dir; + if (!tgt.dir.empty ()) + { + n.dir /= tgt.dir; + n.dir.normalize (); + } + + n.type = tgt.type; + n.value = tgt.value; + add_ext (n.value); + + pair<names, const scope&> r (names {move (n)}, *root); + + // Cache. + // + if (cache_out_root.empty ()) + cache_out_root = move (out_root); + + ctx.import_cache.emplace ( + import_key {move (cache_out_root), move (tgt), metav}, r); + + return r; + } + else + { + add_ext (tgt.value); + return pair<names, const scope&> (names {move (tgt)}, *root); + } + } + // Load the imported root scope. // - load_root (*root); + if (!root->root_extra->loaded) + load_root (*root); // If this is a normal import, then we go through the export stub. // @@ -2525,6 +2810,12 @@ namespace build2 // "Pass" the imported project's roots to the stub. // + if (cache_out_root.empty ()) + cache_out_root = out_root; + + if (src_root.empty ()) + src_root = root->src_path (); + ts.assign (ctx.var_out_root) = move (out_root); ts.assign (ctx.var_src_root) = move (src_root); @@ -2540,7 +2831,7 @@ namespace build2 // Pass the metadata compatibility version in import.metadata. // if (meta) - ts.assign (ctx.var_import_metadata) = uint64_t (1); + ts.assign (ctx.var_import_metadata) = metav; // Load the export stub. Note that it is loaded in the context of the // importing project, not the imported one. The export stub will @@ -2555,10 +2846,20 @@ namespace build2 l5 ([&]{trace << "importing " << es;}); // @@ Should we verify these are all unqualified names? Or maybe there - // is a use-case for the export stub to return a qualified name? + // is a use-case for the export stub to return a qualified name? E.g., + // re-export? // - parser p (ctx); - names v (p.parse_export_stub (ifs, path_name (es), gs, ts)); + names v; + { + auto df = make_diag_frame ( + [&tgt, &loc] (const diag_record& dr) + { + dr << info (loc) << "while loading export stub for " << tgt; + }); + + parser p (ctx); + v = p.parse_export_stub (ifs, path_name (es), *root, gs, ts); + } // If there were no export directive executed in an export stub, // assume the target is not exported. @@ -2567,7 +2868,14 @@ namespace build2 fail (loc) << "target " << tgt << " is not exported by project " << *proj; - return pair<names, const scope&> (move (v), *root); + pair<names, const scope&> r (move (v), *root); + + // Cache. + // + ctx.import_cache.emplace ( + import_key {move (cache_out_root), move (tgt), metav}, r); + + return r; } catch (const io_error& e) { @@ -2624,10 +2932,39 @@ namespace build2 } } - pair<names, import_kind> + const target_type& + import_target_type (scope& root, + const scope& iroot, const string& n, + const location& l) + { + // NOTE: see similar code in parser::parse_define(). + + const target_type* tt (iroot.find_target_type (n)); + if (tt == nullptr) + fail (l) << "unknown imported target type " << n << " in project " + << iroot; + + auto p (root.root_extra->target_types.insert (*tt)); + + if (!p.second && &p.first.get () != tt) + fail (l) << "imported target type " << n << " already defined in project " + << root; + + return *tt; + } + + static names + import2_buildfile (context&, names&&, bool, const location&); + + static const target* + import2 (context&, const scope&, names&, + const string&, bool, const optional<string>&, bool, + const location&); + + import_result<scope> import (scope& base, name tgt, - bool ph2, + const optional<string>& ph2, bool opt, bool metadata, const location& loc) @@ -2651,11 +2988,13 @@ namespace build2 // if (metadata) { - pair<const target*, import_kind> r ( + import_result<target> r ( import_direct (base, move (tgt), ph2, opt, metadata, loc)); - return make_pair (r.first != nullptr ? r.first->as_name () : names {}, - r.second); + return import_result<scope> { + r.target != nullptr ? r.target->base_scope ().root_scope () : nullptr, + move (r.name), + r.kind}; } pair<name, optional<dir_path>> r ( @@ -2671,6 +3010,7 @@ namespace build2 if (!r.second || r.second->empty ()) { names ns; + const target* t (nullptr); if (r.first.empty ()) { @@ -2686,18 +3026,32 @@ namespace build2 // if (ns.back ().qualified ()) { - if (ph2) + if (ns.back ().type == "buildfile") + { + assert (ph2); + ns = import2_buildfile (ctx, move (ns), opt && !r.second, loc); + } + else 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 */, - nullopt /* metadata */, - false /* existing */, - loc)) + t = import2 (ctx, + base, ns, + *ph2, + opt && !r.second /* optional */, + nullopt /* metadata */, + false /* existing */, + loc); + + if (t != nullptr) + { + // Note that here r.first was still project-qualified and we + // have no choice but to call as_name(). This shouldn't cause + // any problems since the import() call assigns the extension. + // ns = t->as_name (); + } else ns.clear (); // NULL } @@ -2706,36 +3060,99 @@ namespace build2 } } - return make_pair ( + return import_result<scope> { + t != nullptr ? t->base_scope ().root_scope () : nullptr, move (ns), - r.second.has_value () ? import_kind::adhoc : import_kind::fallback); + r.second.has_value () ? import_kind::adhoc : import_kind::fallback}; } import_kind k (r.first.absolute () ? import_kind::adhoc : import_kind::normal); - return make_pair ( - import_load (base.ctx, move (r), false /* metadata */, loc).first, - k); + pair<names, const scope&> p ( + import_load (base.ctx, move (r), false /* metadata */, loc)); + + return import_result<scope> {&p.second, move (p.first), k}; } const target* - import (context& ctx, - const prerequisite_key& pk, - bool opt, - const optional<string>& meta, - bool exist, - const location& loc) + import2 (context& ctx, + const prerequisite_key& pk, + const string& hint, + bool opt, + const optional<string>& meta, + bool exist, + const location& loc) { - tracer trace ("import"); + tracer trace ("import2"); - assert (!meta || !exist); + // Neither hint nor metadata can be requested for existing. + // + assert (!exist || (!meta && hint.empty ())); assert (pk.proj); const project_name& proj (*pk.proj); - // Target type-specific search. + // Note that if this function returns a target, it should have the + // extension assigned (like the find/insert_target() functions) so that + // as_name() returns a stable name. + + // Rule-specific resolution. + // + if (!hint.empty ()) + { + assert (pk.scope != nullptr); + + // Note: similar to/inspired by match_rule_impl(). + // + // Search scopes outwards, stopping at the project root. + // + for (const scope* s (pk.scope); + s != nullptr; + s = s->root () ? nullptr : s->parent_scope ()) + { + // We only look for rules that are registered for perform(update). + // + if (const operation_rule_map* om = s->rules[perform_id]) + { + if (const target_type_rule_map* ttm = (*om)[update_id]) + { + // Ignore the target type the rules are registered for (this is + // about prerequisite types, not target). + // + // @@ Note that the same rule could be registered for several + // types which means we will keep calling it repeatedly. + // + for (const auto& p: *ttm) + { + const name_rule_map& nm (p.second); + + // Filter against the hint. + // + for (auto p (nm.find_sub (hint)); p.first != p.second; ++p.first) + { + const string& n (p.first->first); + const rule& r (p.first->second); + + auto df = make_diag_frame ( + [&pk, &n](const diag_record& dr) + { + if (verb != 0) + dr << info << "while importing " << pk << " using rule " + << n; + }); + + if (const target* t = r.import (pk, meta, loc)) + return t; + } + } + } + } + } + } + + // Builtin resolution for certain target types. // const target_key& tk (pk.tk); const target_type& tt (*tk.type); @@ -2788,8 +3205,7 @@ namespace build2 auto df = make_diag_frame ( [&proj, &tt, &tk] (const diag_record& dr) { - import_suggest ( - dr, proj, tt, *tk.name, "alternative "); + import_suggest (dr, proj, &tt, *tk.name, false, "alternative "); }); if (!(md = extract_metadata (pp, *meta, opt, loc))) @@ -2798,6 +3214,9 @@ namespace build2 if (!t || *t == nullptr) { + // Note: we need the lock because process_path() call below is not + // MT-safe. + // pair<target&, ulock> r (insert_target (trace, ctx, tt, p)); t = &r.first; @@ -2819,6 +3238,8 @@ namespace build2 return *t; } + // NOTE: see similar code in import2() below if changing anything here. + if (opt || exist) return nullptr; @@ -2829,40 +3250,178 @@ namespace build2 dr << info << "consider adding its installation location" << info << "or explicitly specify its project name"; else - import_suggest (dr, proj, tt, *tk.name); + // Use metadata as proxy for immediate import. + // + import_suggest (dr, proj, &tt, *tk.name, meta && hint.empty ()); + + dr << endf; + } + + // As above but with scope/ns instead of pk. This version deals with the + // unknown target type case. + // + static const target* + import2 (context& ctx, + const scope& base, names& ns, + const string& hint, + bool opt, + const optional<string>& meta, + bool exist, + const location& loc) + { + // If we have a rule hint, then it's natural to expect this target type is + // known to the importing project. Ditto for project-less import. + // + const target_type* tt (nullptr); + if (hint.empty ()) + { + size_t n; + if ((n = ns.size ()) != 0 && n == (ns[0].pair ? 2 : 1)) + { + const name& n (ns.front ()); + + if (n.typed () && !n.proj->empty ()) + { + tt = base.find_target_type (n.type); + + if (tt == nullptr) + { + // A subset of code in the above version of import2(). + // + if (opt || exist) + return nullptr; + + diag_record dr; + dr << fail (loc) << "unable to import target " << ns; + import_suggest (dr, *n.proj, nullptr, string (), meta.has_value ()); + } + } + } + } + + return import2 (ctx, + base.find_prerequisite_key (ns, loc, tt), + hint, + opt, + meta, + exist, + loc); + } + + static names + import2_buildfile (context&, names&& ns, bool opt, const location& loc) + { + tracer trace ("import2_buildfile"); + + assert (ns.size () == 1); + name n (move (ns.front ())); + + // Our approach doesn't work for targets without a project so let's fail + // hard, even if optional. + // + if (!n.proj || n.proj->empty ()) + fail (loc) << "unable to import target " << n << " without project name"; + + while (!build_install_buildfile.empty ()) // Breakout loop. + { + path f (build_install_buildfile / + dir_path (n.proj->string ()) / + n.dir / + n.value); + + // See if we need to try with extensions. + // + bool ext (path_traits::find_extension (n.value) == string::npos && + n.value != std_buildfile_file.string () && + n.value != alt_buildfile_file.string ()); + + if (ext) + { + f += '.'; + f += std_build_ext; + } + + if (!exists (f)) + { + l6 ([&]{trace << "tried " << f;}); + + if (ext) + { + f.make_base (); + f += '.'; + f += alt_build_ext; + + if (!exists (f)) + { + l6 ([&]{trace << "tried " << f;}); + break; + } + } + else + break; + } + + // Split the path into the target. + // + ns = {name (f.directory (), move (n.type), f.leaf ().string ())}; + return move (ns); + } + + if (opt) + return names {}; + + diag_record dr; + dr << fail (loc) << "unable to import target " << n; + + import_suggest (dr, *n.proj, nullptr /* tt */, n.value, false); + + if (build_install_buildfile.empty ()) + dr << info << "no exported buildfile installation location is " + << "configured in build2"; + else + dr << info << "exported buildfile installation location is " + << build_install_buildfile; dr << endf; } - pair<const target*, import_kind> + import_result<target> import_direct (bool& new_value, scope& base, name tgt, - bool ph2, + const optional<string>& ph2, bool opt, bool metadata, const location& loc, const char* what) { - // This is like normal import() except we return the target rather than + // This is like normal import() except we return the target in addition to // its name. // tracer trace ("import_direct"); l5 ([&]{trace << tgt << " from " << base << " for " << what;}); - assert ((!opt || ph2) && (!metadata || ph2)); + assert ((!opt || ph2) && (!metadata || ph2) && tgt.type != "buildfile"); context& ctx (base.ctx); assert (ctx.phase == run_phase::load); + scope& root (*base.root_scope ()); + // Use the original target name as metadata key. // auto meta (metadata ? optional<string> (tgt.value) : nullopt); - names ns; + names ns, rns; import_kind k; const target* pt (nullptr); + const scope* iroot (nullptr); // Imported root scope. + + // Original project/name as imported for diagnostics. + // + string oname (meta ? tgt.value : string ()); + project_name oproj (meta && tgt.proj ? *tgt.proj : project_name ()); pair<name, optional<dir_path>> r ( import_search (new_value, @@ -2883,7 +3442,7 @@ namespace build2 if (r.first.empty ()) { assert (opt); - return make_pair (pt, k); // NULL + return import_result<target> {nullptr, {}, k}; // NULL } else if (r.first.qualified ()) { @@ -2894,38 +3453,72 @@ namespace build2 // This is tricky: we only want the optional semantics for the // fallback case. // - pt = import (ctx, - base.find_prerequisite_key (ns, loc), - opt && !r.second, - meta, - false /* existing */, - loc); + pt = import2 (ctx, + base, ns, + *ph2, + opt && !r.second, + meta, + false /* existing */, + loc); } if (pt == nullptr) - return make_pair (pt, k); // NULL + return import_result<target> {nullptr, {}, k}; // NULL + + // Note that here r.first was still project-qualified and we have no + // choice but to call as_name() (below). This shouldn't cause any + // problems since the import() call assigns the extension. - // Otherwise fall through. + // Fall through. } else + { + // It's a bit fuzzy in which cases we end up here. So for now we keep + // the original if it's absolute and call as_name() otherwise. + // + // @@ TODO: resolve iroot or assume target type should be known? + // + if (r.first.absolute ()) + rns.push_back (r.first); + ns.push_back (move (r.first)); // And fall through. + } } else { k = r.first.absolute () ? import_kind::adhoc : import_kind::normal; - ns = import_load (base.ctx, move (r), metadata, loc).first; + + pair<names, const scope&> p ( + import_load (base.ctx, move (r), metadata, loc)); + + rns = ns = move (p.first); + iroot = &p.second; } if (pt == nullptr) { - // Similar logic to perform's search(). + // Import (more precisely, alias) the target type into this project + // if not known. // - target_key tk (base.find_target_key (ns, loc)); + const target_type* tt (nullptr); + if (iroot != nullptr && !ns.empty ()) + { + const name& n (ns.front ()); + if (n.typed ()) + tt = &import_target_type (root, *iroot, n.type, loc); + } + + // Similar logic to perform's search(). Note: modifies ns. + // + target_key tk (base.find_target_key (ns, loc, tt)); pt = ctx.targets.find (tk, trace); if (pt == nullptr) fail (loc) << "unknown imported target " << tk; } + if (rns.empty ()) + rns = pt->as_name (); + target& t (pt->rw ()); // Load phase. // Note that if metadata is requested via any of the import*() functions, @@ -2934,10 +3527,20 @@ namespace build2 // if (meta) { + auto df = make_diag_frame ( + [&oproj, &oname, &t] (const diag_record& dr) + { + if (!oproj.empty ()) + import_suggest (dr, oproj, &t.type (), oname, false, "alternative "); + }); + // The export.metadata value should start with the version followed by // the metadata variable prefix. // - lookup l (t.vars[ctx.var_export_metadata]); + // Note: lookup on target, not target::vars since it could come from + // the group (think lib{} metadata). + // + lookup l (t[ctx.var_export_metadata]); if (l && !l->empty ()) { const names& ns (cast<names> (l)); @@ -2955,7 +3558,7 @@ namespace build2 catch (const invalid_argument& e) { fail (loc) << "invalid metadata version in imported target " << t - << ": " << e; + << ": " << e << endf; } if (ver != 1) @@ -2970,13 +3573,15 @@ namespace build2 const string& pfx (ns[1].value); - auto& vp (ctx.var_pool.rw ()); // Load phase. - // See if we have the stable program name in the <var-prefix>.name // variable. If its missing, set it to the metadata key (i.e., target // name as imported) by default. // { + // Note: go straight for the public variable pool. + // + auto& vp (ctx.var_pool.rw ()); // Load phase. + value& nv (t.assign (vp.insert (pfx + ".name"))); if (!nv) nv = *meta; @@ -2987,28 +3592,51 @@ namespace build2 // if (const auto* e = cast_null<strings> (t.vars[pfx + ".environment"])) { - scope& rs (*base.root_scope ()); - for (const string& v: *e) - config::save_environment (rs, v); + config::save_environment (root, v); } } else fail (loc) << "no metadata for imported target " << t; } - return make_pair (pt, k); + return import_result<target> {pt, move (rns), k}; + } + + path + import_buildfile (scope& bs, name n, bool opt, const location& loc) + { + names r (import (bs, + move (n), + string () /* phase2 */, + opt, + false /* metadata */, + loc).name); + + path p; + if (!r.empty ()) // Optional not found. + { + // Note: see also parse_import(). + // + assert (r.size () == 1); // See import_load() for details. + name& n (r.front ()); + p = n.dir / n.value; // Should already include extension. + } + else + assert (opt); + + return p; } ostream& - operator<< (ostream& o, const pair<const exe*, import_kind>& p) + operator<< (ostream& o, const import_result<exe>& r) { - assert (p.first != nullptr); + assert (r.target != nullptr); - if (p.second == import_kind::normal) - o << *p.first; + if (r.kind == import_kind::normal) + o << *r.target; else - o << p.first->process_path (); + o << r.target->process_path (); return o; } @@ -3048,13 +3676,23 @@ namespace build2 // mkdir (d / std_build_dir, verbosity); + auto diag = [verbosity] (const path& f) + { + if (verb >= verbosity) + { + if (verb >= 2) + text << "cat >" << f; + else if (verb) + print_diag ("save", f); + } + }; + // Write build/bootstrap.build. // { path f (d / std_bootstrap_file); - if (verb >= verbosity) - text << (verb >= 2 ? "cat >" : "save ") << f; + diag (f); try { @@ -3100,8 +3738,7 @@ namespace build2 { path f (d / std_root_file); - if (verb >= verbosity) - text << (verb >= 2 ? "cat >" : "save ") << f; + diag (f); try { @@ -3149,8 +3786,7 @@ namespace build2 { path f (d / std_build_dir / "config.build"); // std_config_file - if (verb >= verbosity) - text << (verb >= 2 ? "cat >" : "save ") << f; + diag (f); try { @@ -3175,8 +3811,7 @@ namespace build2 { path f (d / std_buildfile_file); - if (verb >= verbosity) - text << (verb >= 2 ? "cat >" : "save ") << f; + diag (f); try { |