diff options
Diffstat (limited to 'libbuild2/cc')
-rw-r--r-- | libbuild2/cc/common.cxx | 12 | ||||
-rw-r--r-- | libbuild2/cc/compile-rule.cxx | 84 | ||||
-rw-r--r-- | libbuild2/cc/compiledb.cxx | 1100 | ||||
-rw-r--r-- | libbuild2/cc/compiledb.hxx | 236 | ||||
-rw-r--r-- | libbuild2/cc/guess.cxx | 42 | ||||
-rw-r--r-- | libbuild2/cc/guess.hxx | 3 | ||||
-rw-r--r-- | libbuild2/cc/init.cxx | 617 | ||||
-rw-r--r-- | libbuild2/cc/link-rule.cxx | 447 | ||||
-rw-r--r-- | libbuild2/cc/module.cxx | 9 | ||||
-rw-r--r-- | libbuild2/cc/module.hxx | 61 | ||||
-rw-r--r-- | libbuild2/cc/msvc.cxx | 12 | ||||
-rw-r--r-- | libbuild2/cc/pkgconfig.cxx | 8 | ||||
-rw-r--r-- | libbuild2/cc/windows-manifest.cxx | 5 |
13 files changed, 2514 insertions, 122 deletions
diff --git a/libbuild2/cc/common.cxx b/libbuild2/cc/common.cxx index 9a4a07c..fcc8961 100644 --- a/libbuild2/cc/common.cxx +++ b/libbuild2/cc/common.cxx @@ -185,7 +185,7 @@ namespace build2 // const string* pt ( cast_null<string> ( - l.state[a].lookup_original (c_type, true /* target_only */).first)); + l.state[a].lookup_original (c_type, lookup_limit::target).first)); // cc.type value format is <lang>[,...]. // @@ -242,7 +242,7 @@ namespace build2 // { const variable& v (impl ? c_export_impl_libs : c_export_libs); - c_e_libs = l.lookup_original (v, false, &bs).first; + c_e_libs = l.lookup_original (v, &bs).first; } if (!cc) @@ -251,7 +251,7 @@ namespace build2 same ? (impl ? x_export_impl_libs : x_export_libs) : vp[t + (impl ? ".export.impl_libs" : ".export.libs")]); - x_e_libs = l.lookup_original (v, false, &bs).first; + x_e_libs = l.lookup_original (v, &bs).first; } // Process options first. @@ -669,7 +669,7 @@ namespace build2 // See the link rule for the lookup semantics. // lookup l ( - t->lookup_original (var, true /* target_only */).first); + t->lookup_original (var, lookup_limit::target).first); if (l ? cast<bool> (*l) : u) lf |= lflag_whole; @@ -778,8 +778,8 @@ namespace build2 if (proc_lib) { const variable& v (same ? x_libs : vp[t + ".libs"]); - proc_impl (l.lookup_original (c_libs, false, &bs).first); - proc_impl (l.lookup_original (v, false, &bs).first); + proc_impl (l.lookup_original (c_libs, &bs).first); + proc_impl (l.lookup_original (v, &bs).first); } } } diff --git a/libbuild2/cc/compile-rule.cxx b/libbuild2/cc/compile-rule.cxx index 95ba89f..29a26b5 100644 --- a/libbuild2/cc/compile-rule.cxx +++ b/libbuild2/cc/compile-rule.cxx @@ -25,6 +25,7 @@ #include <libbuild2/cc/target.hxx> // h #include <libbuild2/cc/module.hxx> #include <libbuild2/cc/utility.hxx> +#include <libbuild2/cc/compiledb.hxx> using std::exit; using std::strlen; @@ -1181,6 +1182,11 @@ namespace build2 fsdir_rule::perform_update_direct (a, *dir); } + // Use the subset of the depdb checks to detect changes to the + // compilation database entry. + // + bool compiledb_changed (false); + // Note: the leading '@' is reserved for the module map prefix (see // extract_modules()) and no other line must start with it. // @@ -1198,8 +1204,14 @@ namespace build2 // but only in what it targets, then the checksum will still change. // if (dd.expect (cast<string> (rs[x_checksum])) != nullptr) + { l4 ([&]{trace << "compiler mismatch forcing update of " << t;}); + // The checksum includes the absolute compiler path. + // + compiledb_changed = true; + } + // Then the compiler environment checksum. // if (dd.expect (env_checksum) != nullptr) @@ -1263,7 +1275,17 @@ namespace build2 append_sys_hdr_options (cs); // Extra system header dirs (last). if (dd.expect (cs.string ()) != nullptr) + { l4 ([&]{trace << "options mismatch forcing update of " << t;}); + + // Note that this doesn't include any of the "plumbing" options + // like -x, -c, -o, etc. In the unlikely event that there are + // changes in this area that also affect the semantics of the + // compilation database (options reordering doesn't, for example), + // then we can resort to incrementing the rule version. + // + compiledb_changed = true; + } } // Finally the source file. @@ -1273,7 +1295,10 @@ namespace build2 assert (!p.empty ()); // Sanity check. if (dd.expect (p) != nullptr) + { l4 ([&]{trace << "source file mismatch forcing update of " << t;}); + compiledb_changed = true; + } } // If any of the above checks resulted in a mismatch (different @@ -1296,6 +1321,14 @@ namespace build2 u = dd.mtime > mt; } + // Confirm the entry in the compilation database, if any. + // + if (compiledb::match (bs, t, tp, src, compiledb_changed) && !u) + { + l4 ([&]{trace << "compilation database forcing update of " << t;}); + u = true; + } + // If updating for any of the above reasons, treat it as if doesn't // exist. // @@ -1950,8 +1983,8 @@ namespace build2 // // @@ Should we print the pid we are talking to? It gets hard to // follow once things get nested. But if all our diag will include - // some kind of id (chain, thread?), then this will not be strictly - // necessary. + // some kind of id (dependency chain, thread?), then this will not + // be strictly necessary. // diag_record dr (text); for (size_t i (0); i != batch_n; ++i) @@ -2628,8 +2661,8 @@ namespace build2 // @@ MODHDR: Should we print the pid we are talking to? It gets hard to // follow once things get nested. But if all our diag will - // include some kind of id (chain, thread?), then this will - // not be strictly necessary. + // include some kind of id (dependency chain, thread?), then + // this will not be strictly necessary. // if (verb >= 3) text << " > " << rq; @@ -3148,7 +3181,7 @@ namespace build2 } hk.file = move (fp); - hk.hash = hash<path> () (hk.file); + hk.hash = hash<string> () (hk.file.string ()); slock l (hc.header_map_mutex); auto i (hc.header_map.find (hk)); @@ -3201,7 +3234,7 @@ namespace build2 // path has changed (header has been remapped). // if (!e || r.second) - hk.hash = hash<path> () (hk.file); + hk.hash = hash<string> () (hk.file.string ()); const file* f; { @@ -3214,6 +3247,8 @@ namespace build2 { //cache_cls.fetch_add (1, memory_order_relaxed); + // @@ TMP cleanup. + // #if 0 assert (r.first == f); #else @@ -4167,9 +4202,10 @@ namespace build2 if (l->empty ()) // Done, nothing changed. { // If modules are enabled, then we keep the preprocessed output - // around (see apply() for details). + // around (see apply() for details). Unless reprocessing was + // requested. // - if (modules) + if (modules && !reprocess) { result.first = ctx.fcache->create_existing (t.path () + pext); result.second = true; @@ -7351,8 +7387,11 @@ namespace build2 // apply()). For named modules there may be no obj*{} if this is a // sidebuild (obj*{} is already in the library binary). // - path relm; + const path* abso (nullptr); + const path* absm (nullptr); path relo; + path relm; + switch (ut) { case unit_type::module_header: @@ -7362,12 +7401,18 @@ namespace build2 case unit_type::module_impl_part: { if (const file* o = find_adhoc_member<file> (t, tts.obj)) - relo = relative (o->path ()); + { + abso = &o->path (); + relo = relative (*abso); + } break; } default: - relo = relative (tp); + { + abso = &tp; + relo = relative (tp); + } } // Build the command line. @@ -7397,6 +7442,9 @@ namespace build2 small_vector<string, 2> header_args; // Header unit options storage. small_vector<string, 2> module_args; // Module options storage. + // NOTE: see a note in apply() on the compilation database implications + // if changing anything below. + // switch (cclass) { case compiler_class::msvc: @@ -7531,6 +7579,7 @@ namespace build2 { assert (ut != unit_type::module_header); // @@ MODHDR + absm = &tp; relm = relative (tp); args.push_back ("/ifcOutput"); @@ -7744,6 +7793,9 @@ namespace build2 // Output module file is specified in the mapping file, the // same as input. // + // We set neither relm nor absm since they are not on the + // command line. + // if (ut == unit_type::module_header) // No obj, -c implied. break; @@ -7772,6 +7824,7 @@ namespace build2 { assert (ut != unit_type::module_header); // @@ MODHDR + absm = &tp; relm = relative (tp); // Without this option Clang's .pcm will reference source @@ -7883,6 +7936,15 @@ namespace build2 else if (verb == 2) print_process (args); + // Insert or update the entry in the compilation database, if any. + // + compiledb::execute ( + bs, + t, tp, s, *sp, + cpath, args, + relo, abso != nullptr ? *abso : empty_path, + relm, absm != nullptr ? *absm : empty_path); + // If we have the (partially) preprocessed output, switch to that. // // But we remember the original source/position to restore later. diff --git a/libbuild2/cc/compiledb.cxx b/libbuild2/cc/compiledb.cxx new file mode 100644 index 0000000..ccf08a9 --- /dev/null +++ b/libbuild2/cc/compiledb.cxx @@ -0,0 +1,1100 @@ +// file : libbuild2/cc/compiledb.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/cc/compiledb.hxx> + +#include <cstring> // strlen() +#include <iostream> // cout + +#ifndef BUILD2_BOOTSTRAP +# include <libbutl/json/parser.hxx> +#endif + +#include <libbuild2/filesystem.hxx> +#include <libbuild2/diagnostics.hxx> + +#include <libbuild2/cc/module.hxx> + +#include <libbuild2/cc/target.hxx> +#include <libbuild2/bin/target.hxx> + +using namespace std; + +namespace build2 +{ + namespace cc + { + compiledb_set compiledbs; + + // compiledb + // + compiledb:: + ~compiledb () + { + } + + // Return true if this entry should be written to the database with the + // specified name. + // + static bool + filter (const scope& rs, + const core_module& m, + const string& name, + const file& ot, const file& it) + { + tracer trace ("cc::compiledb_filter"); + + bool r (true); + const char* w (nullptr); // Why r is false. + + // First check if writing to this database is enabled. + // + // No filter means not enabled. + // + if (m.cdb_filter_ == nullptr) + { + r = false; + w = "no database name filter"; + } + else + { + // Iterate in reverse (so that later values override earlier) and take + // the first name match. + // + r = false; + for (const pair<optional<string>, bool>& p: + reverse_iterate (*m.cdb_filter_)) + { + if (!p.first || *p.first == name) + { + r = p.second; + break; + } + } + + if (!r) + w = "no match in database name filter"; + } + + // Verify the name is known in this amalgamation. Note that without + // this check we may end up writing to unrelated databases in other + // amalgamations (think linked configurations). + // + if (r) + { + r = false; + for (const core_module* pm (&m); + pm != nullptr; + pm = pm->outer_module_) + { + const strings& ns (pm->cdb_names_); + + if (find (ns.begin (), ns.end (), name) != ns.end ()) + { + r = true; + break; + } + } + + if (!r) + w = "database name unknown in amalgamation"; + } + + // Filter based on the output target. + // + // If there is no filter specified, then accept all targets. + // + if (r && m.cdb_filter_output_ != nullptr) + { + // If the filter is empty, then there is no match. + // + if (m.cdb_filter_output_->empty ()) + { + r = false; + w = "empty output target type filter"; + } + else + { + const target_type& ott (ot.type ()); + + // Iterate in reverse (so that later values override earlier) and + // take the first name match. + // + r = false; + for (const pair<optional<string>, string>& p: + reverse_iterate (*m.cdb_filter_output_)) + { + if (p.first && *p.first != name) + continue; + + using namespace bin; + + const string& n (p.second); + + if (ott.name == n || n == "target") + { + r = true; + } + // + // Handle obj/bmi/hbmi{} groups ad hoc. + // + else if (n == "obj") + { + r = ott.is_a<obje> () || ott.is_a<objs> () || ott.is_a<obja> (); + } + else if (n == "bmi") + { + r = ott.is_a<bmie> () || ott.is_a<bmis> () || ott.is_a<bmia> (); + } + else if (n == "hbmi") + { + r = ott.is_a<hbmie> () || ott.is_a<hbmis> () || ott.is_a<hbmia> (); + } + else + { + // Handle the commonly-used, well-known targets directly (see + // note in core_config_init() for why we cannot pre-lookup + // them). + // + const target_type* tt ( + n == "obje" ? &obje::static_type : + n == "objs" ? &objs::static_type : + n == "obja" ? &obja::static_type : + n == "bmie" ? &bmie::static_type : + n == "bmis" ? &bmis::static_type : + n == "bmia" ? &bmia::static_type : + n == "hbmie" ? &hbmie::static_type : + n == "hbmis" ? &hbmis::static_type : + n == "hbmia" ? &hbmia::static_type : + rs.find_target_type (n)); + + if (tt == nullptr) + fail << "unknown target type '" << n << "' in " + << "config.cc.compiledb.filter.output value"; + + r = ott.is_a (*tt); + } + + if (r) + break; + } + + if (!r) + w = "no match in output target type filter"; + } + } + + // Filter based on the input target. + // + // If there is no filter specified, then accept all targets. + // + if (r && m.cdb_filter_input_ != nullptr) + { + // If the filter is empty, then there is no match. + // + if (m.cdb_filter_input_->empty ()) + { + r = false; + w = "empty input target type filter"; + } + else + { + const target_type& itt (it.type ()); + + // Iterate in reverse (so that later values override earlier) and + // take the first name match. + // + r = false; + for (const pair<optional<string>, string>& p: + reverse_iterate (*m.cdb_filter_input_)) + { + if (p.first && *p.first != name) + continue; + + const string& n (p.second); + + if (itt.name == n || n == "target") + r = true; + else + { + // The same optimization as above. Note: cxx{}, etc., are in the + // cxx module so we have to look them up. + // + const target_type* tt ( + n == "c" ? &c::static_type : + n == "m" ? &m::static_type : + n == "S" ? &m::static_type : + rs.find_target_type (n)); + + if (tt == nullptr) + fail << "unknown target type '" << n << "' in " + << "config.cc.compiledb.filter.input value"; + + r = itt.is_a (*tt); + } + + if (r) + break; + } + + if (!r) + w = "no match in input target type filter"; + } + } + + l6 ([&] + { + if (r) + trace << "keep " << ot << " in " << name; + else + trace << "omit " << ot << " from " << name << ": " << w; + }); + + return r; + } + + bool compiledb:: + match (const scope& bs, + const file& ot, const path_type& op, + const file& it, + bool changed) + { + if (compiledbs.empty ()) + return false; + + const scope& rs (*bs.root_scope ()); + const auto* m (rs.find_module<core_module> (core_module::name)); + + assert (m != nullptr); + + bool u (false); + + for (const unique_ptr<compiledb>& db: compiledbs) + { + if (filter (rs, *m, db->name, ot, it)) + u = db->match (ot, op, changed) || u; + } + + return u; + } + + void compiledb:: + execute (const scope& bs, + const file& ot, const path_type& op, + const file& it, const path_type& ip, + const process_path& cpath, const cstrings& args, + const path_type& relo, const path_type& abso, + const path_type& relm, const path_type& absm) + { + if (compiledbs.empty ()) + return; + + const scope& rs (*bs.root_scope ()); + const auto* m (rs.find_module<core_module> (core_module::name)); + + assert (m != nullptr); + + assert (relo.empty () == abso.empty () && + relm.empty () == absm.empty ()); + + for (const unique_ptr<compiledb>& db: compiledbs) + { + if (filter (rs, *m, db->name, ot, it)) + db->execute (ot, op, it, ip, cpath, args, relo, abso, relm, absm); + } + } + + void + compiledb_pre (context& ctx, action a, const action_targets&) + { + // Note: won't be registered if compiledbs is empty. + + // Note: may be called directly with empty action_targets. + + assert (a.inner_action () == perform_update_id); + + tracer trace ("cc::compiledb_pre"); + + bool mctx (ctx.module_context == &ctx); + + l6 ([&]{trace << (mctx ? "module" : "normal") << " context " << &ctx;}); + + for (const unique_ptr<compiledb>& db: compiledbs) + db->pre (ctx); + } + + void + compiledb_post (context& ctx, + action a, + const action_targets& ts, + bool failed) + { + // Note: won't be registered if compiledbs is empty. + + assert (a.inner_action () == perform_update_id); + + tracer trace ("cc::compiledb_post"); + + bool mctx (ctx.module_context == &ctx); + + l6 ([&]{trace << (mctx ? "module" : "normal") << " context " << &ctx + << ", failed: " << failed;}); + + for (const unique_ptr<compiledb>& db: compiledbs) + db->post (ctx, ts, failed); + } + +#ifndef BUILD2_BOOTSTRAP + + namespace json = butl::json; + + // compiledb_stdout + // + compiledb_stdout:: + compiledb_stdout (string n) + : compiledb (move (n), path_type ()), + state_ (state::init), + nesting_ (0), + js_ (cout, 0 /* indentation */, "" /* multi_value_separator */) + { + } + + void compiledb_stdout:: + pre (context&) + { + // If the previous operation batch failed, then we shouldn't be here. + // + assert (state_ != state::failed); + + // The module context (used to build build system modules) poses a + // problem: we can receive its callbacks before the main context's or + // nested in the pre/post calls of the main context (or both, in + // fact). Plus there may be multiple pre/post sequences corresponding to + // the module context of both kinds. The three distinct cases are: + // + // 1. Module is loaded as part of the initial buildfile load (e.g., from + // root.build) -- in this case we will observe module pre/post before + // the main context's pre/post. + // + // In fact, to be precise, we will only observe them if cc is loaded + // before such a module. + // + // 2. Module is loaded via the interrupting load (e.g., from a directory + // buildfile that is loaded implicitly during match) -- in this case + // we will observe pre/post calls nested into the main context's + // pre/post. + // + // 3. The module context is used to build an ad hoc C++ recipe -- in + // this case we also get nested calls like in (2) since this happens + // during the recipe's match(). + // + // One thing to keep in mind (and which we rely upon quite a bit below) + // is that the main context's post will always be last (within any given + // operation; there could be another for the subsequent operation in a + // batch). + // + // Handling the nested case is relatively straightforward: we can keep + // track and ignore all the nested calls. + // + // The before case is where things get complicated. We could "take" the + // first module pre call and then wait until the main post, unless we + // see a module post call with failed=true, in which case there will be + // no further pre/post calls. There is, however, a nuance: the module is + // loaded and build for any operation, not just update, which means that + // if the main operation is not update (say, it's clean), we won't see + // any of the main context's pre/post calls. + // + // The way we are going to resolve this problem is different for the + // stdout and file implementations: + // + // For stdout we will just say that it should only be used with the + // update operation. There is really no good reason to use it with + // anything else anyway. See compiledb_stdout::post() for additional + // details. + // + // For file we will rely on its persistence and simply close and reopen + // the database for each pre/post sequence, the same way as if they were + // separate operations in a batch. + // + if (nesting_++ != 0) // Nested pre() call. + return; + + if (state_ == state::init) // First pre() call. + { + state_ = state::empty; + cout << "[\n"; + } + } + + bool compiledb_stdout:: + match (const file&, const path_type&, bool) + { + return true; + } + + static inline const char* + rel_to_abs (const char* a, + const string& rs, const string& as, + string& buf) + { + if (size_t rn = rs.size ()) + { + size_t an (strlen (a)); + + if (an >= rn && rs.compare (0, rn, a, rn) == 0) + { + if (an == rn) + return as.c_str (); + + buf = as; + buf.append (a + rn, an - rn); + + return buf.c_str (); + } + } + + return nullptr; + } + + void compiledb_stdout:: + execute (const file&, const path_type& op, + const file&, const path_type& ip, + const process_path& cpath, const cstrings& args, + const path_type& relo, const path_type& abso, + const path_type& relm, const path_type& absm) + { + const string& ro (relo.string ()); + const string& ao (abso.string ()); + + const string& rm (relm.string ()); + const string& am (absm.string ()); + + mlock l (mutex_); + + switch (state_) + { + case state::full: + { + cout << ",\n"; + break; + } + case state::empty: + { + state_ = state::full; + break; + } + case state::failed: + return; + case state::init: + assert (false); + return; + } + + try + { + // Duplicate what we have in the file implementation (instead of + // factoring it out to something common) in case here we need to + // adjust things (change order, omit some values; for example to + // accommodate broken consumers). We have this freedom here but not + // there. + // + js_.begin_object (); + { + js_.member ("output", op.string ()); + js_.member ("file", ip.string ()); + + js_.member_begin_array ("arguments"); + { + string buf; // Reuse. + for (auto b (args.begin ()), i (b), e (args.end ()); + i != e && *i != nullptr; + ++i) + { + const char* r; + + if (i == b) + r = cpath.effect_string (); + else + { + // Untranslate relative paths back to absolute. + // + const char* a (*i); + + if ((r = rel_to_abs (a, ro, ao, buf)) == nullptr && + (r = rel_to_abs (a, rm, am, buf)) == nullptr) + r = a; + } + + js_.value (r); + } + } + js_.end_array (); + + js_.member ("directory", work.string ()); + } + js_.end_object (); + } + catch (const json::invalid_json_output& e) + { + // There is no way (nor reason; the output will most likely be invalid + // anyway) to reuse the failed json serializer so make sure we ignore + // all the subsequent callbacks. + // + state_ = state::failed; + + l.unlock (); + + fail << "invalid compilation database json output: " << e; + } + } + + void compiledb_stdout:: + post (context& ctx, const action_targets&, bool failed) + { + assert (nesting_ != 0); + if (--nesting_ != 0) // Nested post() call. + return; + + bool mctx (ctx.module_context == &ctx); + + switch (state_) + { + case state::empty: + case state::full: + { + // If this is a module context's post, wait for the main context's + // post (last) unless the module load failed (in which case there + // will be no main pre/post). + // + // Note that there is no easy way to diagnose the case where we + // won't get the main pre/post calls. Instead, we will just produce + // invalid JSON (array won't be closed). In a somewhat hackish way, + // this actually makes the `b [-n] clean update` sequence work: we + // will take the pre() call from clean and the main post() from + // update. + // + if (mctx && !failed) + return; + + if (state_ == state::full) + cout << '\n'; + + cout << "]\n"; + break; + } + case state::failed: + return; + case state::init: + assert (false); + } + + state_ = state::init; + } + + // compiledb_file + // + compiledb_file:: + compiledb_file (string n, path_type p) + : compiledb (move (n), move (p)), + state_ (state::closed), + nesting_ (0) + { + } + + void compiledb_file:: + pre (context&) + { + // If the previous operation batch failed, then we shouldn't be here. + // + assert (state_ != state::failed); + + // See compiledb_stdout::pre() for background on dealing with the module + // context. Here are some file-specific nuances: + // + // We are going to load the database on the first pre call and flush + // (but not close) it on the matching post. Flushing means that we will + // update the file but still keep the in-memory state, in case there is + // another pre/post session coming. This is both a performance + // optimization but also the way we handle prunning no longer present + // entries, which gets tricky across multiple pre/post sessions (see + // post() for details). + // + if (nesting_++ != 0) // Nested pre() call. + return; + + if (state_ == state::closed) // First pre() call. + { + // Load the contents of the file if it exists, marking all the entries + // as (presumed) absent. + // + if (exists (path)) + { + uint64_t line (1); + try + { + ifdstream ifs (path, ifdstream::badbit); + + // Parse the top-level array manually (see post() for the expected + // format). + // + auto throw_invalid_input = [] (const string& d) + { + throw json::invalid_json_input ("", 0, 1, 0, d); + }; + + enum {first, second, next, last, end} s (first); + + for (string l; !eof (getline (ifs, l)); line++) + { + switch (s) + { + case first: + { + if (l != "[") + throw_invalid_input ("beginning of array expected"); + + s = second; + continue; + } + case second: + { + if (l == "]") + { + s = end; + continue; + } + + s = next; + } + // Fall through. + case next: + { + if (!l.empty () && l.back () == ',') + l.pop_back (); + else + s = last; + + break; + } + case last: + { + if (l != "]") + throw_invalid_input ("end of array expected"); + + s = end; + continue; + } + case end: + { + throw_invalid_input ("junk after end of array"); + } + } + + // Parse just the output target path, which must come first. + // + json::parser jp (l, "" /* name */); + + jp.next_expect (json::event::begin_object); + string op (move (jp.next_expect_member_string ("output"))); + + auto r (db_.emplace (move (op), entry {entry_status::absent, l})); + if (!r.second) + throw_invalid_input ( + "duplicate output value '" + r.first->first + '\''); + } + + if (s != end) + throw_invalid_input ("corrupt input text"); + } + catch (const json::invalid_json_input& e) + { + state_ = state::failed; + + location l (path, line, e.column); + fail (l) << "invalid compilation database json input: " << e << + info << "remove this file if it was produced by a different tool"; + } + catch (const io_error& e) + { + state_ = state::failed; + fail << "unable to read " << path << ": " << e; + } + } + + absent_ = db_.size (); + changed_ = false; + + state_ = state::open; + } + } + + bool compiledb_file:: + match (const file&, const path_type& op, bool changed) + { + mlock l (mutex_); + + switch (state_) + { + case state::open: + break; + case state::failed: + return false; + case state::closed: + assert (false); + return false; + } + + // Mark an existing entry as present or changed. And if one does not + // exist, then (for now) as missing. + // + auto i (db_.find (op.string ())); + + if (i != db_.end ()) + { + entry& e (i->second); + + // Note: we can end up with present entries via the module context + // (see post() below). And we can see changed entries in a subsequent + // nested module context. + // + switch (e.status) + { + case entry_status::present: + case entry_status::changed: + assert (!changed); + break; + case entry_status::absent: + { + e.status = changed ? entry_status::changed : entry_status::present; + + absent_--; + changed_ = changed_ || (e.status == entry_status::changed); + break; + } + case entry_status::missing: + assert (false); + } + + return false; + } + else + { + db_.emplace (op.string (), entry {entry_status::missing, string ()}); + + changed_ = true; + + return true; + } + } + + void compiledb_file:: + execute (const file&, const path_type& op, + const file&, const path_type& ip, + const process_path& cpath, const cstrings& args, + const path_type& relo, const path_type& abso, + const path_type& relm, const path_type& absm) + { + const string& ro (relo.string ()); + const string& ao (abso.string ()); + + const string& rm (relm.string ()); + const string& am (absm.string ()); + + mlock l (mutex_); + + switch (state_) + { + case state::open: + break; + case state::failed: + return; + case state::closed: + assert (false); + return; + } + + auto i (db_.find (op.string ())); + + // We should have had the match() call before execute(). + // + assert (i != db_.end () && i->second.status != entry_status::absent); + + entry& e (i->second); + + if (e.status == entry_status::present) // Present and unchanged. + return; + + // The entry is either missing or changed. + // + try + { + e.json.clear (); + json::buffer_serializer js (e.json, 0 /* indentation */); + + js.begin_object (); + { + js.member ("output", op.string ()); // Note: must come first. + js.member ("file", ip.string ()); + + js.member_begin_array ("arguments"); + { + string buf; // Reuse. + for (auto b (args.begin ()), i (b), e (args.end ()); + i != e && *i != nullptr; + ++i) + { + const char* r; + + if (i == b) + r = cpath.effect_string (); + else + { + // Untranslate relative paths back to absolute. + // + const char* a (*i); + + if ((r = rel_to_abs (a, ro, ao, buf)) == nullptr && + (r = rel_to_abs (a, rm, am, buf)) == nullptr) + r = a; + } + + js.value (r); + } + } + js.end_array (); + + js.member ("directory", work.string ()); + } + js.end_object (); + } + catch (const json::invalid_json_output& e) + { + // There is no way (nor reason; the output will most likely be invalid + // anyway) to reuse the failed json serializer so make sure we ignore + // all the subsequent callbacks. + // + state_ = state::failed; + + l.unlock (); + + fail << "invalid compilation database json output: " << e; + } + + e.status = entry_status::changed; + } + + void compiledb_file:: + post (context& ctx, const action_targets& ts, bool failed) + { + assert (nesting_ != 0); + if (--nesting_ != 0) // Nested post() call. + return; + + switch (state_) + { + case state::open: + break; + case state::failed: + return; + case state::closed: + assert (false); + return; + } + + bool mctx (ctx.module_context == &ctx); + + tracer trace ("cc::compiledb_file::post"); + + // See if we need to update the file. + // + if (changed_) + l6 ([&]{trace << "updating due to missing/changed entries: " << path;}); + + // Don't prune the stale entries if the operation failed since we may + // not have gotten to execute some of them. + // + // And if this is a module context's post, then also don't prune the + // stale entries, instead waiting for the main context's post (if there + // will be one; this means we will only prune on update). + // + // Actually, this pruning business is even trickier than that: if we + // are not updating the entire project (say, rather only a subdirectory + // or even a specific target), then we will naturally not get any + // match/execute calls for targets of this project that don't get pulled + // into this build. Which means that we cannot just prune entries that + // we did not match/execute. It feels the correct semantics is to only + // prune the entries if they are in a subdirectory of the dir{} targets + // which we are building. + // + // What do we do about the module context, where we always update a + // specific libs{}? We could use its directory instead but that may lead + // to undesirable results. For example, if there are unit tests in the + // same directory, we will end up dropping their entries. It feels like + // the correct approach is to just ignore module context's entries + // entirely. If someone wants to prune the compilation database of a + // module, they will just need to update it directly (i.e., via the main + // context). Note that we cannot apply the same "simplification" to the + // changed entries since we will only observe the change once. + // + bool absent (false); + + if (!failed && !mctx && absent_ != 0) + { + // Pre-scan the entries and drop the appropriate absent ones. + // + for (auto i (db_.begin ()); i != db_.end (); ) + { + const entry& e (i->second); + + if (e.status == entry_status::absent) + { + // Absent entries should be rare enough during the normal + // development that we don't need to bother with caching the + // directories. + // + bool a (false); + for (const action_target& at: ts) + { + const target& t (at.as<target> ()); + if (t.is_a<dir> ()) + { + const string& p (i->first); + const string& d (t.out_dir ().string ()); + + if (path_traits::sub (p.c_str (), p.size (), + d.c_str (), d.size ())) + { + // Remove this entry from the in-memory state so that it + // matches the file state. + // + i = db_.erase (i); + --absent_; + a = absent = true; + break; + } + } + } + + if (a) + continue; + } + + ++i; + } + } + + if (absent) + l6 ([&]{trace << "updating due to absent entries: " << path;}); + + try + { + auto_rmfile rm; + ofdstream ofs; + + bool u (changed_ || absent); // Update the file. + + if (u) + { + rm = auto_rmfile (path); + ofs.open (path); + + // We parse the top-level array manually (see pre() above) and the + // expected format is as follows: + // + // [ + // {"output":...}, + // ... + // {"output":...} + // ] + // + ofs.write ("[\n", 2); + } + + // Iterate over the entries resetting their status and writing them to + // the file if necessary. + // + bool first (true); + for (auto& p: db_) + { + entry& e (p.second); + + // First sort out the status also skipping appropriate entries. + // + switch (e.status) + { + case entry_status::absent: + { + // This is an absent entry that we should keep (see pre-scan + // above). + // + break; + } + case entry_status::missing: + { + // This should only happen if this operation has failed (see + // also below) or we are in the match-only mode. + // + assert (failed || ctx.match_only); + continue; + } + case entry_status::present: + case entry_status::changed: + { + // This is tricky: if this is a module context, then we don't + // want to mark the entries as absent since they will then get + // dropped by the main operation context. + // + if (mctx) + e.status = entry_status::present; + else + { + // Note: this is necessary for things to work across multiple + // operations in a batch. + // + e.status = entry_status::absent; + absent_++; + } + } + } + + if (u) + { + if (first) + first = false; + else + ofs.write (",\n", 2); + + ofs.write (e.json.c_str (), e.json.size ()); + } + } + + if (u) + { + ofs.write (first ? "]\n" : "\n]\n", first ? 2 : 3); + + ofs.close (); + rm.cancel (); + } + } + catch (const io_error& e) + { + state_ = state::failed; + fail << "unable to write to " << path << ": " << e; + } + + // If this operation has failed, then our state may not be accurate + // (e.g., entries with missing status) but we also don't expect any + // further pre calls. Let's change out state to failed as a sanity + // check. + // + if (failed) + state_ = state::failed; + else + changed_ = false; + + // Note: keep in the open state (see pre() for details). + } + +#endif // BUILD2_BOOTSTRAP + } +} diff --git a/libbuild2/cc/compiledb.hxx b/libbuild2/cc/compiledb.hxx new file mode 100644 index 0000000..8288cf5 --- /dev/null +++ b/libbuild2/cc/compiledb.hxx @@ -0,0 +1,236 @@ +// file : libbuild2/cc/compiledb.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_CC_COMPILEDB_HXX +#define LIBBUILD2_CC_COMPILEDB_HXX + +#include <unordered_map> + +#ifndef BUILD2_BOOTSTRAP +# include <libbutl/json/serializer.hxx> +#endif + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/target.hxx> +#include <libbuild2/action.hxx> +#include <libbuild2/context.hxx> + +namespace build2 +{ + namespace cc + { + using compiledb_name_filter = vector<pair<optional<string>, bool>>; + using compiledb_type_filter = vector<pair<optional<string>, string>>; + + class compiledb + { + public: + // Match callback where we confirm an entry in the database and also + // signal whether it has changes (based on change tracking in depdb). + // Return true to force compilation of this target and thus make sure + // the below execute() is called (unless something before that failed). + // + // Besides noticing changes, this callback is also necessary to notice + // and delete entries that should no longer be in the database (e.g., a + // source file was removed from the project). + // + // Note that output is either obj*{}, bmi*{}, of hbmi*{}. + // + static bool + match (const scope& bs, + const file& output, const path& output_path, + const file& input, + bool changed); + + // Execute callback where we insert or update an entry in the database. + // + // The {relo, abso}, and {relm, absm} pairs are used to "untranslate" + // relative paths to absolute. Specifically, any argument that has rel? + // as a prefix has this prefix replaced with the corresponding abs?. + // Note that this means we won't be able to handle old MSVC and + // clang-cl, which don't support the `/F?: <path>` form, only + // `/F?<path>`. Oh, well. Note also that either relo or relm (but not + // both) could be empty if unused. + // + // Note that we assume the source file is always absolute and is the + // last argument. + // + // Why do we want absolute paths? That's a good question. Our initial + // plan was to compare command lines in order to detect when we need to + // update the database. And if those changed with every change of CWD, + // that would be of little use. But then we realized we could do better + // by using depdb to detect changes. So now we actually don't have a + // need to get rid of the relative paths in the command line. But seeing + // that we already have it, let's keep it for now in case it makes a + // different to some broken/legacy consumers. Note also that C++ module + // name-to-BMI mapping is not untranslated (see append_module_options()). + // + static void + execute (const scope& bs, + const file& output, const path& output_path, + const file& input, const path& input_path, + const process_path& cpath, const cstrings& args, + const path& relo, const path& abso, + const path& relm, const path& absm); + + public: + using path_type = build2::path; + + string name; + path_type path; + + // The path is expected to be absolute and normalized or empty if the + // name is `-` (stdout). + // + compiledb (string n, path_type p) + : name (move (n)), path (move (p)) + { + } + + virtual void + pre (context&) = 0; + + virtual bool + match (const file& output, const path_type& output_path, + bool changed) = 0; + + virtual void + execute (const file& output, const path_type& output_path, + const file& input, const path_type& input_path, + const process_path& cpath, const cstrings& args, + const path_type& relo, const path_type& abso, + const path_type& relm, const path_type& absm) = 0; + + virtual void + post (context&, const action_targets&, bool failed) = 0; + + virtual + ~compiledb (); + }; + + using compiledb_set = vector<unique_ptr<compiledb>>; + + // Populated by core_config_init() during serial load. + // + extern compiledb_set compiledbs; + + // Context operation callbacks. + // + void + compiledb_pre (context&, action, const action_targets&); + + void + compiledb_post (context&, action, const action_targets&, bool failed); + +#ifndef BUILD2_BOOTSTRAP + + // Implementation that writes to stdout. + // + // Note that this implementation forces compilation of all the targets for + // which it is called to make sure their entries are in the database. So + // typically used in the dry run mode. + // + class compiledb_stdout: public compiledb + { + public: + // The path is expected to be empty. + // + explicit + compiledb_stdout (string name); + + virtual void + pre (context&) override; + + virtual bool + match (const file& output, const path_type& output_path, + bool changed) override; + + virtual void + execute (const file& output, const path_type& output_path, + const file& input, const path_type& input_path, + const process_path& cpath, const cstrings& args, + const path_type& relo, const path_type& abso, + const path_type& relm, const path_type& absm) override; + + virtual void + post (context&, const action_targets&, bool failed) override; + + private: + mutex mutex_; + enum class state {init, empty, full, failed} state_; + size_t nesting_; + butl::json::stream_serializer js_; + }; + + // Implementation that maintains a file. + // + class compiledb_file: public compiledb + { + public: + compiledb_file (string name, path_type path); + + virtual void + pre (context&) override; + + virtual bool + match (const file& output, const path_type& output_path, + bool changed) override; + + virtual void + execute (const file& output, const path_type& output_path, + const file& input, const path_type& input_path, + const process_path& cpath, const cstrings& args, + const path_type& relo, const path_type& abso, + const path_type& relm, const path_type& absm) override; + + virtual void + post (context&, const action_targets&, bool failed) override; + + private: + mutex mutex_; + enum class state {closed, open, failed} state_; + size_t nesting_; + + // We want to optimize the performance for the incremental update case + // where only a few files will be recompiled and most of the time there + // will be no change in the command line, which means we won't need to + // rewrite the file. + // + // As a result, our in-memory representation is a hashmap (we could have + // thousands of entries) of absolute and normalized output file paths + // (stored as strings for lookup efficiency) to their serialized JSON + // text lines plus the status: absent, present, changed, or missing + // (entry should be there but is not). This way we don't waste + // (completely) parsing (and re-serializing) each line knowing that we + // won't need to touch most of them. + // + // In fact, we could have gone even further and used a sorted vector + // since insertions will be rare in this case. But we will need to + // lookup every entry on each update, so it's unclear this is a win. + // + enum class entry_status {absent, present, changed, missing}; + + struct entry + { + entry_status status; + string json; + }; + + using map_type = std::unordered_map<string, entry>; + map_type db_; + + // Number/presence of various entries in the database (used to determine + // whether we need to update the file without iterating over all the + // entries). + // + size_t absent_; // Number of absent entries. + bool changed_; // Presence of changed or missing entries. + }; + +#endif // BUILD2_BOOTSTRAP + } +} + +#endif // LIBBUILD2_CC_COMPILEDB_HXX diff --git a/libbuild2/cc/guess.cxx b/libbuild2/cc/guess.cxx index 5ae6fb2..2d079f0 100644 --- a/libbuild2/cc/guess.cxx +++ b/libbuild2/cc/guess.cxx @@ -949,9 +949,15 @@ namespace build2 // there is nothing like -m32/-m64 or /MACHINE). Targeting // 64-bit seems like as good of a default as any. // - fb = ((dir_path (mi->msvc_dir) /= "bin") /= "Hostx64") /= - "x64"; - + fb = dir_path (mi->msvc_dir) /= "bin"; + +#if defined(_M_ARM64) || defined(__aarch64__) + fb /= "HostARM64"; + fb /= "ARM64"; +#else + fb /= "Hostx64"; + fb /= "x64"; +#endif search_info = info_ptr ( new msvc_info (move (*mi)), msvc_info_deleter); } @@ -1629,15 +1635,25 @@ namespace build2 // Seeing that we only do 64-bit on Windows, let's always use 64-bit // MSVC tools (link.exe, etc). In case of the Platform SDK, it's unclear - // what the CPU signifies (host, target, both). + // what the CPU signifies (host, target, both). It appears to be host. // - r = (((dir_path (mi.msvc_dir) /= "bin") /= "Hostx64") /= cpu). - representation (); + r = (((dir_path (mi.msvc_dir) /= "bin") /= +#if defined(_M_ARM64) || defined(__aarch64__) + "HostARM64" +#else + "Hostx64" +#endif + ) /= cpu).representation (); r += path::traits_type::path_separator; - r += (((dir_path (mi.psdk_dir) /= "bin") /= mi.psdk_ver) /= cpu). - representation (); + r += (((dir_path (mi.psdk_dir) /= "bin") /= mi.psdk_ver) /= +#if defined(_M_ARM64) || defined(__aarch64__) + "arm64" +#else + "x64" +#endif + ).representation (); return r; } @@ -1878,7 +1894,7 @@ namespace build2 move (ver), nullopt, move (gr.signature), - "", + "", // Checksum to be calculated from signature. move (t), move (ot), move (cpat), @@ -2128,7 +2144,7 @@ namespace build2 move (ver), nullopt, move (gr.signature), - move (gr.checksum), // Calculated on whole -v output. + "", // Checksum calculated on whole -v output. move (t), move (ot), move (pat), @@ -2898,7 +2914,7 @@ namespace build2 move (ver), move (var_ver), move (gr.signature), - move (gr.checksum), // Calculated on whole -v output. + "", // Checksum calculated on whole -v output. move (t), move (ot), move (cpat), @@ -3216,7 +3232,7 @@ namespace build2 move (ver), nullopt, move (gr.signature), - "", + "", // Checksum to be calculated from signature. move (t), move (ot), move (pat), @@ -3370,6 +3386,8 @@ namespace build2 cs.append (gr.type_signature); } + cs.append (r.path.effect_string ()); + r.checksum = cs.string (); // Derive binutils pattern unless this has already been done by the diff --git a/libbuild2/cc/guess.hxx b/libbuild2/cc/guess.hxx index 7cbbd87..dfa8aa2 100644 --- a/libbuild2/cc/guess.hxx +++ b/libbuild2/cc/guess.hxx @@ -160,6 +160,9 @@ namespace build2 // checksum will still change. This is currently the case for all the // compilers that we support. // + // And we assume that the checksum incorporates the absolute compiler + // path. This is used to detect compilation database changes. + // // The target is the compiler's traget architecture triplet. Note that // unlike all the preceding fields, this one takes into account the // compile options (e.g., -m32). diff --git a/libbuild2/cc/init.cxx b/libbuild2/cc/init.cxx index e124450..d691bc5 100644 --- a/libbuild2/cc/init.cxx +++ b/libbuild2/cc/init.cxx @@ -10,8 +10,10 @@ #include <libbuild2/config/utility.hxx> +#include <libbuild2/cc/module.hxx> #include <libbuild2/cc/target.hxx> #include <libbuild2/cc/utility.hxx> +#include <libbuild2/cc/compiledb.hxx> using namespace std; using namespace butl; @@ -23,7 +25,7 @@ namespace build2 // Scope operation callback that cleans up module sidebuilds. // static target_state - clean_module_sidebuilds (action, const scope& rs, const dir&) + clean_module_sidebuilds (const scope& rs) { context& ctx (rs.ctx); @@ -67,6 +69,131 @@ namespace build2 return target_state::unchanged; } + // Scope operation callback that cleans up compilation databases. + // + static target_state + clean_compiledb (const scope& rs) + { + context& ctx (rs.ctx); + + target_state r (target_state::unchanged); + + for (const unique_ptr<compiledb>& db: compiledbs) + { + const path& p (db->path); + + if (p.empty () || + ctx.scopes.find_out (p.directory ()).root_scope () != &rs) + continue; + + if (rmfile (ctx, p)) + r = target_state::changed; + } + + return r; + } + + // Scope operation callback for cleaning module sidebuilds and compilation + // databases. + // + static target_state + clean_callback (action, const scope& rs, const dir&) + { + target_state r (clean_module_sidebuilds (rs)); + + if (!compiledbs.empty ()) + r |= clean_compiledb (rs); + + return r; + } + + // Detect if just <name> in the <name>[@<path>] form is actually <path>. + // We assume it is <path> and not <name> if it contains a directory + // component or is the special directory name (`.`/`..`) . If that's the + // case, return canonicalized name representing <path>. See the call site + // in core_config_init() below for background. + // + static optional<name> + compiledb_name_to_path (const name& n) + { + if (n.directory ()) + return n; + + if (n.file ()) + { + if (!n.dir.empty () || + path_traits::find_separator (n.value) != string::npos) + { + name r (n); + r.canonicalize (); + return r; + } + else if (n.value == "." || n.value == "..") + { + return name (dir_path (n.value)); + } + } + + return nullopt; + } + + // Custom save function that completes relative paths in the + // config.cc.compiledb and config.cc.compiledb.name values. + // + static pair<names_view, const char*> + save_compiledb_name (const scope&, + const value& v, + const value*, + names& storage) + { + const names& ns (v.as<names> ()); // Value is untyped. + + // Detect and handle the case where just <name> is actually <path>. + // + if (ns.size () == 1) + { + const name& n (ns.back ()); + + if (optional<name> otn = compiledb_name_to_path (n)) + { + name& tn (*otn); + + if (tn.dir.relative ()) + tn.dir.complete (); + + tn.dir.normalize (); + + storage.push_back (move (tn)); + return make_pair (names_view (storage), "="); + } + } + + if (find_if (ns.begin (), ns.end (), + [] (const name& n) {return n.pair;}) == ns.end ()) + { + return make_pair (names_view (ns), "="); + } + + storage = ns; + for (auto i (storage.begin ()); i != storage.end (); ++i) + { + if (i->pair) + { + name& n (*++i); + + if (!n.directory ()) + n.canonicalize (); + + if (n.dir.relative ()) + n.dir.complete (); + + n.dir.normalize (); + } + } + + return make_pair (names_view (storage), "="); + } + bool core_vars_init (scope& rs, scope&, @@ -107,6 +234,22 @@ namespace build2 vp.insert<abs_dir_path> ("config.cc.pkgconfig.sysroot"); + // Compilation database. + // + // See the manual for the semantics. + // + // config.cc.compiledb -- <name>[@<path>]|<path> (untyped) + // config.cc.compiledb.name -- <name>[@<path>]... (untyped) + // config.cc.compiledb.filter -- [<name>@]<bool>... + // config.cc.compiledb.filter.input -- [<name>@]<target-type>... + // config.cc.compiledb.filter.output -- [<name>@]<target-type>... + // + vp.insert ("config.cc.compiledb"); + vp.insert ("config.cc.compiledb.name"); + vp.insert<compiledb_name_filter> ("config.cc.compiledb.filter"); + vp.insert<compiledb_type_filter> ("config.cc.compiledb.filter.input"); + vp.insert<compiledb_type_filter> ("config.cc.compiledb.filter.output"); + vp.insert<strings> ("cc.poptions"); vp.insert<strings> ("cc.coptions"); vp.insert<strings> ("cc.loptions"); @@ -192,16 +335,6 @@ namespace build2 // vp.insert<bool> ("cc.serialize"); - // Register scope operation callback. - // - // It feels natural to clean up sidebuilds as a post operation but that - // prevents the (otherwise-empty) out root directory to be cleaned up - // (via the standard fsdir{} chain). - // - rs.operation_callbacks.emplace ( - perform_clean_id, - scope::operation_callback {&clean_module_sidebuilds, nullptr /*post*/}); - return true; } @@ -292,6 +425,8 @@ namespace build2 assert (first); + context& ctx (rs.ctx); + // Load cc.core.guess. // load_module (rs, rs, "cc.core.guess", loc); @@ -312,7 +447,6 @@ namespace build2 // // @@ Same nonsense as in module. // - // rs.assign ("cc.poptions") += cast_null<strings> ( lookup_config (rs, "config.cc.poptions", nullptr)); @@ -363,21 +497,16 @@ namespace build2 if (!cast_false<bool> (rs["bin.config.loaded"])) { // Prepare configuration hints (pretend it belongs to root scope). - // They are only used on the first load of bin.config so we only - // populate them on our first load. // variable_map h (rs); - if (first) - { - // Note that all these variables have already been registered. - // - h.assign ("config.bin.target") = - cast<target_triplet> (rs["cc.target"]).representation (); + // Note that all these variables have already been registered. + // + h.assign ("config.bin.target") = + cast<target_triplet> (rs["cc.target"]).representation (); - if (auto l = extra.hints["config.bin.pattern"]) - h.assign ("config.bin.pattern") = cast<string> (l); - } + if (auto l = extra.hints["config.bin.pattern"]) + h.assign ("config.bin.pattern") = cast<string> (l); init_module (rs, rs, "bin.config", loc, false /* optional */, h); } @@ -386,7 +515,6 @@ namespace build2 // ourselves since the target can come from the configuration and not // our hint). // - if (first) { const auto& ct (cast<target_triplet> (rs["cc.target"])); const auto& bt (cast<target_triplet> (rs["bin.target"])); @@ -416,6 +544,447 @@ namespace build2 if (tsys == "mingw32") load_module (rs, rs, "bin.rc.config", loc); + // Find the innermost outer core_module, if any. + // + const core_module* om (nullptr); + for (const scope* s (&rs); + (s = s->parent_scope ()->root_scope ()) != nullptr; ) + { + if ((om = s->find_module<core_module> (core_module::name)) != nullptr) + break; + } + + auto& m (extra.set_module (new core_module (om))); + + // config.cc.compiledb.* + // + { + // For config.cc.compiledb and config.cc.compiledb.name we only + // consider a value in this root scope (if it's inherited from the + // outer scope, then that's where it will be handled). One special + // case is when it's specified on a scope that doesn't load the cc + // module (including, ultimately, the global scope for a global + // override). We handle it by assuming the value belongs to the + // outermost amalgamation that loads the cc module. + // + // Note: cache the result. + // + auto find_outermost = + [&rs, o = optional<pair<scope*, core_module*>> ()] () mutable + { + if (!o) + { + o = pair<scope*, core_module*> (&rs, nullptr); + for (scope* s (&rs); + (s = s->parent_scope ()->root_scope ()) != nullptr; ) + { + if (auto* m = s->find_module<core_module> (core_module::name)) + { + o->first = s; + o->second = m; + } + } + } + + return *o; + }; + + auto belongs = [&rs, &find_outermost] (const lookup& l) + { + return l.belongs (rs) || find_outermost ().first == &rs; + }; + + // Add compilation databases specified in ns as <name>[@<path>] pairs, + // appending their names to cdb_names. If <path> is absent, then place + // the database into the base directory. Return the last added name. + // + auto add_cdbs = [&ctx, + &loc, + &trace] (strings& cdb_names, + const names& ns, + const dir_path& base) -> const string& + { + // Check that names and paths match. Return false if this entry + // already exist. + // + // Note that before we also checked that the same paths are not used + // across contexts. But, actually, there doesn't seem to be anything + // wrong with that and this can actually be useful, for example, + // when developing build system modules. + // + auto check = [&loc] (const string& n, const path& p) + { + for (const unique_ptr<compiledb>& db: compiledbs) + { + bool nm (db->name == n); + bool pm (db->path == p); + + if (nm != pm) + fail (loc) << "inconsistent compilation database names/paths" << + info << p << " is called " << n << + info << db->path << " is called " << db->name; + + if (nm) + return false; + } + + return true; + }; + + const string* r (&empty_string); + + bool reg (false); + size_t j (compiledbs.size ()); // First newly added database. + for (auto i (ns.begin ()); i != ns.end (); ++i) + { + // Each element has the <name>[@<path>] form. + // + // The special `-` <name> signifies stdout. + // + // If <path> is absent, then the file is called <name>.json and + // placed into the output directory of the amalgamation or project + // root scope (passed as the base argument). + // + // If <path> is (syntactically) a directory, then the file path is + // <path>/<name>.json. + // + if (!i->simple () || i->empty ()) + fail (loc) << "invalid compilation database name '" << *i << "'"; + + // Don't allow names that have (or are) directory components. + // + if (compiledb_name_to_path (*i)) + fail (loc) << "directory component in compilation database name '" + << *i << "'"; + + string n (i->value); + + path p; + if (i->pair) + { + ++i; + + if (n == "-") + fail (loc) << "compilation database path specified for stdout " + << "name"; + try + { + if (i->directory ()) + p = i->dir / n + ".json"; + else if (i->file ()) + { + if (i->dir.empty ()) + p = path (i->value); + else + p = i->dir / i->value; + } + else + throw invalid_path (""); + + if (p.relative ()) + p.complete (); + + p.normalize (); + } + catch (const invalid_path&) + { + fail (loc) << "invalid compilation database path '" << *i + << "'"; + } + } + else if (n != "-") + { + p = base / n + ".json"; + } + + if (check (n, p)) + { + reg = compiledbs.empty (); // First time. + +#ifdef BUILD2_BOOTSTRAP + fail (loc) << "compilation database requested during bootstrap"; +#else + if (n == "-") + compiledbs.push_back ( + unique_ptr<compiledb> ( + new compiledb_stdout (n))); + else + compiledbs.push_back ( + unique_ptr<compiledb> ( + new compiledb_file (n, move (p)))); +#endif + } + + // We may end up with duplicates via the config.cc.compiledb + // logic. + // + auto k (find (cdb_names.begin (), cdb_names.end (), n)); + + if (k == cdb_names.end ()) + { + cdb_names.push_back (move (n)); + r = &cdb_names.back (); + } + else + r = &*k; + } + + // Register context operation callback for compiledb generation. + // + // We have two complications here: + // + // 1. We could be performing all this from the load phase that + // interrupted the match phase, which means the point where the + // pre callback would have been called is already gone (but the + // post callback will still be called). This will happen if we, + // say, import a project that has a compilation database from a + // project that doesn't. + // + // (Note that if you think that this can be solved by simply + // always registering the callbacks, regardless of whether we + // have any databases or not, consider a slightly different + // scenario where we import a project that loads the cc module + // from a project that does not). + // + // What we are going to do in this case is simply call the pre + // callback manually. + // + // 2. We could again be performing all this from the load phase that + // interrupted the match phase, but this time the pre callback + // has already been called, which means there will be no pre() + // call for the newly added database(s). This will happen if we, + // say, import a project that has a compilation database from a + // project that also has one. + // + // Again, what we are going to do in this case is simply call the + // pre callback for the new database(s) manually. + // + if (reg) + ctx.operation_callbacks.emplace ( + perform_update_id, + context::operation_callback {&compiledb_pre, &compiledb_post}); + + if (ctx.load_generation > 1) + { + action a (ctx.current_action ()); + + if (a.inner_action () == perform_update_id) + { + if (reg) // Case #1. + { + l6 ([&]{trace << "direct compiledb_pre for context " << &ctx;}); + compiledb_pre (ctx, a, action_targets {}); + } + else // Case #2. + { + size_t n (compiledbs.size ()); + + if (j != n) + { + l6 ([&]{trace << "additional compiledb for context " << &ctx;}); + + for (; j != n; ++j) + compiledbs[j]->pre (ctx); + } + } + } + } + + return *r; + }; + + lookup l; + + // config.cc.compiledb + // + // The semantics of this value is as follows: + // + // Location: outermost amalgamation that loads the cc module. + // Name filter: enable from this scope unless specified explicitly. + // Type filter: enable from this scope unless specified explicitly. + // + // Note: save omitted. + // + optional<string> enable_filter; + + l = lookup_config (rs, "config.cc.compiledb", 0, &save_compiledb_name); + if (l && belongs (l)) + { + l6 ([&]{trace << "config.cc.compiledb specified on " << rs;}); + + const names& ns (cast<names> (l)); + + // Make sure it's one name/path. + // + size_t n (ns.size ()); + if (n == 0 || n != (ns.front ().pair ? 2 : 1)) + fail (loc) << "invalid compilation database name '" << ns << "'"; + + // Detect and translate just <name> which is actually <path> to the + // <name>@<path> form: + // + // - The <name> part is the name of the directory where the database + // file will reside (typically project/repository or package + // name). + // + // - If <path> is a directory, then the database name is + // compile_commands.json. + // + names tns; + if (n == 1) + { + const name& n (ns.front ()); + + if (optional<name> otn = compiledb_name_to_path (n)) + { + name& tn (*otn); + + // Note: the add_cdbs() call below completes and normalizes the + // path but we need to do it earlier in order to be able to + // derive the name (the last component can be `.`/`..`). + // + if (tn.dir.relative ()) + tn.dir.complete (); + + tn.dir.normalize (); + + if (!exists (tn.dir)) + fail (loc) << "compilation database directory " << tn.dir + << " does not exist"; + + if (tn.value.empty ()) + tn.value = "compile_commands.json"; + + tns.push_back (name (tn.dir.leaf ().string ())); + tns.back ().pair = '@'; + tns.push_back (move (tn)); + } + } + + // We inject the database directly into the outer amalgamation's + // module, as-if config.cc.compiledb.name was specified in its + // scope. Unless there isn't one, in which case it's us. + // + pair<scope*, core_module*> p (find_outermost ()); + + // Save the name for the name filter below. + // + enable_filter = add_cdbs ( + (p.second != nullptr ? *p.second : m).cdb_names_, + tns.empty () ? ns : tns, + p.first->out_path ()); + } + + // config.cc.compiledb.name + // + // Note: save omitted. + // + l = lookup_config (rs, + "config.cc.compiledb.name", + 0, + &save_compiledb_name); + if (l && belongs (l)) + { + l6 ([&]{trace << "config.cc.compiledb.name specified on " << rs;}); + + add_cdbs (m.cdb_names_, cast<names> (l), rs.out_path ()); + } + + // config.cc.compiledb.filter + // + // Note: save omitted. + // + l = lookup_config (rs, "config.cc.compiledb.filter"); + if (l && belongs (l)) // Custom. + { + m.cdb_filter_ = &cast<compiledb_name_filter> (l); + } + else if (enable_filter) // Override. + { + // Inherit outer filter. + // + if (om != nullptr && om->cdb_filter_ != nullptr) + m.cdb_filter_storage_ = *om->cdb_filter_; + + m.cdb_filter_storage_.emplace_back (*enable_filter, true); + m.cdb_filter_ = &m.cdb_filter_storage_; + } + else if (om != nullptr) // Inherit. + { + m.cdb_filter_ = om->cdb_filter_; + } + + // config.cc.compiledb.filter.input + // config.cc.compiledb.filter.output + // + // Note that filtering happens before we take into account the change + // status, which means for larger projects there would be a lot of + // targets to filter even during the incremental update. So it feels + // it would have been better to pre-lookup the target types. However, + // the targets that would normally be used are registered by other + // modules (bin, c/cxx) and which haven't been loaded yet. So instead + // we try to optimize the lookup for the commonly used targets. + // + // Note: save omitted. + // + l = lookup_config (rs, "config.cc.compiledb.filter.input"); + if (l && belongs (l)) // Custom. + { + m.cdb_filter_input_ = &cast<compiledb_type_filter> (l); + } + else if (enable_filter) // Override. + { + // Inherit outer filter. + // + if (om != nullptr && om->cdb_filter_input_ != nullptr) + { + m.cdb_filter_input_storage_ = *om->cdb_filter_input_; + m.cdb_filter_input_storage_.emplace_back (*enable_filter, "target"); + m.cdb_filter_input_ = &m.cdb_filter_input_storage_; + } + else + m.cdb_filter_input_ = nullptr; // Enable all. + } + else if (om != nullptr) // Inherit. + { + m.cdb_filter_input_ = om->cdb_filter_input_; + } + + l = lookup_config (rs, "config.cc.compiledb.filter.output"); + if (l && belongs (l)) // Custom. + { + m.cdb_filter_output_ = &cast<compiledb_type_filter> (l); + } + else if (enable_filter) // Override. + { + // Inherit outer filter. + // + if (om != nullptr && om->cdb_filter_output_ != nullptr) + { + m.cdb_filter_output_storage_ = *om->cdb_filter_output_; + m.cdb_filter_output_storage_.emplace_back (*enable_filter, "target"); + m.cdb_filter_output_ = &m.cdb_filter_output_storage_; + } + else + m.cdb_filter_output_ = nullptr; // Enable all. + } + else if (om != nullptr) // Inherit. + { + m.cdb_filter_output_ = om->cdb_filter_output_; + } + } + + // Register scope operation callback for cleaning module sidebuilds and + // compilation databases. + // + // It feels natural to clean this stuff up as a post operation but that + // prevents the (otherwise-empty) out root directory to be cleaned up + // (via the standard fsdir{} chain). + // + rs.operation_callbacks.emplace ( + perform_clean_id, + scope::operation_callback {&clean_callback, nullptr /*post*/}); + return true; } diff --git a/libbuild2/cc/link-rule.cxx b/libbuild2/cc/link-rule.cxx index 417cba5..a669f37 100644 --- a/libbuild2/cc/link-rule.cxx +++ b/libbuild2/cc/link-rule.cxx @@ -102,13 +102,13 @@ namespace build2 // const scope& bs (t->base_scope ()); - if (lookup l = t->lookup_original (c_export_libs, false, &bs).first) + if (lookup l = t->lookup_original (c_export_libs, &bs).first) { if (!deduplicate_export_libs (bs, cast<vector<name>> (l), r, seen)) return false; } - if (lookup l = t->lookup_original (x_export_libs, false, &bs).first) + if (lookup l = t->lookup_original (x_export_libs, &bs).first) { if (!deduplicate_export_libs (bs, cast<vector<name>> (l), r, seen)) return false; @@ -503,8 +503,8 @@ namespace build2 return false; } - // We will only chain a C source if there is also an X source or we were - // explicitly told to. + // We will only synthesize a dependency for a C source if there is also + // an X source or we were explicitly told to. // if (r.seen_c && !r.seen_x && hint.empty ()) { @@ -915,8 +915,8 @@ namespace build2 const fsdir* dir (inject_fsdir (a, t)); // Process prerequisites, pass 1: search and match prerequisite - // libraries, search obj/bmi{} targets, and search targets we do rule - // chaining for. + // libraries, search obj/bmi{} targets, and search targets we do + // dependency synthesis for. // // Also clear the binless flag if we see any source or object files. // Note that if we don't see any this still doesn't mean the library is @@ -928,7 +928,7 @@ namespace build2 // compile::apply() to unmatch them and therefore not to hinder // parallelism (or mess up for-install'ness). // - // We also create obj/bmi{} chain targets because we need to add + // We also synthesize obj/bmi{} dependencies because we need to add // (similar to lib{}) all the bmi{} as prerequisites to all the other // obj/bmi{} that we are creating. Note that this doesn't mean that the // compile rule will actually treat them all as prerequisite targets. @@ -955,6 +955,20 @@ namespace build2 auto& pts (t.prerequisite_targets[a]); size_t start (pts.size ()); + // Compile rule options specified on lib/exe{} to propagate to obj/bmi{} + // during dependency synthesis (see below). + // + struct cr_options + { + const strings* c_p; // cc.poptions + const strings* x_p; // x.poptions + const strings* c_c; // cc.coptions + const strings* x_c; // x.coptions + + bool member; // Any value came from member as opposed to group. + }; + optional<cr_options> cr_ops; // Lookup lazily. + for (prerequisite_member p: group_prerequisite_members (a, t)) { // Note that we have to recognize update=match for *(update), not just @@ -1025,7 +1039,14 @@ namespace build2 continue; } - const target*& pt (pto); + const target*& pt (pto.target); + + // Auxiliary data in prerequisite_target: + // + // - for libraries it stores link flags (lflag_whole) + // - for synthesized obj/bmi{} it strores the group flag + // + uintptr_t& pd (pto.data); // Mark (2 bits): // @@ -1045,7 +1066,7 @@ namespace build2 { binless = binless && (mod ? user_binless : false); - // Rule chaining, part 1. + // Dependency synthesis, part 1. // // Which scope shall we use to resolve the root? Unlikely, but // possible, the prerequisite is from a different project @@ -1058,6 +1079,62 @@ namespace build2 // bool group (!p.prerequisite.belongs (t)); // Group's prerequisite. + // Lookup all the relevant binary-specific compile option values if + // this hasn't already been done. + // + if (!cr_ops) + { + bool m (false); + + auto lookup = [&bs, &t, &m] (const variable& var) -> const strings* + { + // We don't want to pick anything beyond target-specific + // values (but including target type/pattern-specific) since + // they will also be in effect for obj/bmi{} and setting them + // to the same values is a waste (and, in fact, it could be + // that when this feature is not used, different obj/bmi{} are + // compiled with different options). + // + // Note that we need to take into account target type/pattern- + // specific append/prepend since it could modify the scope + // value but only be applicable to lib/exe{}. Something like + // this: + // + // cc.poptions += -DFOO + // lib{*}: cc.poptions += -DBAR + // + // Then there is this nuance: if any value came from the member + // and not from the group, then we need to override the above + // group semantics. In particular, this allows us to do: + // + // liba{hello}: cxx.poptions += -DLIBHELLO_STATIC_BUILD + // libs{hello}: cxx.poptions += -DLIBHELLO_SHARED_BUILD + // + // Note also that the variables we are dealing with are not + // overridable. + // + pair<build2::lookup, size_t> p ( + t.lookup_original (var, + lookup_limit::target_type, + &bs)); + + const strings* r (cast_null<strings> (p.first)); + + if (r != nullptr) + m = m || p.second == 1; // Found in the target itself. + + return r; + }; + + cr_ops = cr_options {lookup (c_poptions), lookup (x_poptions), + lookup (c_coptions), lookup (x_coptions), + false}; + cr_ops->member = m; + } + + if (group && cr_ops->member) + group = false; + const target_type& rtt (mod ? (group ? bmi::static_type : tts.bmi) : (group ? obj::static_type : tts.obj)); @@ -1091,44 +1168,278 @@ namespace build2 // obj/bmi{} is always in the out tree. Note that currently it could // be the group -- we will pick a member in part 2 below. // + // Note: d is used below. + // pair<target&, ulock> r ( search_new_locked ( ctx, rtt, d, dir_path (), *cp.tk.name, nullptr, cp.scope)); - // If we shouldn't clean obj{}, then it is fair to assume we - // shouldn't clean the source either (generated source will be in - // the same directory as obj{} and if not, well, go find yourself - // another build system ;-)). - // - if (skip (r.first)) - { - pt = nullptr; - continue; - } + const target& cpt (r.first); + bool locked (r.second.owns_lock ()); - // Either set of verify the bin.binless value on this bmi*{} target + // Either set or verify the bin.binless value on this bmi*{} target // (see config_data::b_binless for semantics). // if (mod) { - if (r.second.owns_lock ()) + if (locked) { if (user_binless) r.first.assign (b_binless) = true; } else { - lookup l (r.first[b_binless]); + lookup l (cpt[b_binless]); if (user_binless ? !cast_false<bool> (l) : l.defined ()) fail << "synthesized dependency for prerequisite " << p << " would be incompatible with existing target " - << r.first << + << cpt << info << "incompatible bin.binless value"; } } - pt = &r.first; + // Binary-specific compile options. + // + // Propagate compile rule options (*.poptions and *.coptions) + // specified on the binary to the obj/bmi{} targets that we + // synthesize. + // + // The semantics we are aiming for is as-if they were set on the + // obj/bmi{} target at the end of the buildfile (to be precise, at + // the end of loading all buildfiles for the scope). + // + // While ideally we would like to prevent sharing such obj/bmi{} + // between multiple binaries or for the user to specify any compile + // options explicitly, this is not easy to do (since once the + // options are set, we don't know who set them: same binary on the + // previous operation batch, another binary, or user). Instead, we + // are going to approximate this by making sure the options (or + // their absence) match. + // + // Note also that we don't touch user-specified obj/bmi{} + // prerequisites (neither set nor verify). In particular, this + // allows customizing compile options for specific translation + // units. + // + // NOTE: keep last since unlocks the lock. + // + { + // If we have any value, then either set them (locked) or make + // sure they all match (unlocked). + // + // Note that the case where one lib/exe{} specifies a value while + // the other doesn't specify any (and they share a synthesized + // dependency) will be racy to verify. Getting rid of this race + // will be difficult because in the verify case we can't say who + // set the value on obj/bmi{}. We could set some sort of a marker + // variable but then it means we would need to look it up for + // every lib/exe{} (whether they use this feature or not, and most + // won't). However, we can handle the common case based on the + // target being newly created (as opposed to being mentioned in + // the buildfile; see target_decl). + // + auto check = [&bs, &p, &d] (const variable& var, + const target& et, + const strings* e, + const target& st) + { + // If the expected value is NULL, then just make sure this + // variable is not set on the target. Otherwise, compare the + // result of the normal lookup and if it matches, then we assume + // it's good regardless of where it comes from (this covers all + // the corner cases which we cannot verify precisely; see above + // for details). + // + const strings* v; + size_t vd (0); + + if (e == nullptr) + { + v = cast_null<strings> (st.vars[var]); + + if (v == nullptr) + return; + } + else + { + // Optimize the lookup for the common case. + // + pair<lookup, size_t> p ( + st.lookup_original ( + var, + d.empty () ? &bs : nullptr)); + + v = cast_null<strings> (p.first); + + if (v != nullptr && *v == *e) + return; + + vd = p.second; + } + + diag_record dr (fail); + + dr << "synthesized dependency for prerequisite " << p + << " would be incompatible with existing target " << st << + info << "variable " << var << " value mismatch"; + + if (e == nullptr) // Expected to be absent. + { + dr << info << st << " value: "; to_stream_quoted (dr.os, *v); + dr << info << et << " value is absent"; + } + else if (v == nullptr) // Expected to be present. + { + dr << info << st << " value is absent"; + dr << info << et << " value: "; to_stream_quoted (dr.os, *e); + } + else // Expected to match. + { + dr << info << st << " value: "; to_stream_quoted (dr.os, *v); + dr << info << et << " value: "; to_stream_quoted (dr.os, *e); + + if (vd > 1) + { + if (const target* g = st.group) + dr << info << st << " value came from group " << *g; + } + } + }; + + auto set = [&r] (const variable& var, const strings& v) + { + // One nuance here is that target type/pattern-specific + // append/prepend/assign specified for obj/bmi{} will not be + // in effect for options specified on lib/exe{}. For example: + // + // obj{*}: cc.poptions = -DFOO + // lib{bar}: cc.poptions += -DBAR + // + // It doesn't seem there is anything sensible we can do about + // it automatically other than documenting this nuance and + // suggesting the user adds lib/exe{} to such a pattern. + // + r.first.assign (var) = v; + }; + + bool absent (false); + if (cr_ops->c_p != nullptr || cr_ops->x_p != nullptr || + cr_ops->c_c != nullptr || cr_ops->x_c != nullptr || + (absent = !operator>= (cpt.decl, target_decl::implied))) // VC14 + { + if (locked) + { + if (!absent) + { + if (cr_ops->c_p != nullptr) set (c_poptions, *cr_ops->c_p); + if (cr_ops->x_p != nullptr) set (x_poptions, *cr_ops->x_p); + if (cr_ops->c_c != nullptr) set (c_coptions, *cr_ops->c_c); + if (cr_ops->x_c != nullptr) set (x_coptions, *cr_ops->x_c); + } + + // @@ PERF: maybe pass the lock to search_new_locked() below? + // + r.second.unlock (); + locked = false; + } + else + { + if (absent && cpt.vars.empty ()) + ; // Optimize for the common case. + else + { + // If the values came from the group, then use the group in + // diagnostics. + // + const target& et (group ? *t.group : t); + + check (c_poptions, et, cr_ops->c_p, cpt); + check (x_poptions, et, cr_ops->x_p, cpt); + check (c_coptions, et, cr_ops->c_c, cpt); + check (x_coptions, et, cr_ops->x_c, cpt); + } + } + + // Verify member/group consistency. + // + // The check above doesn't quite work for libraries where the + // one that specified any options will likely end up + // synthesizing obja/objs{} targets (see the group logic above) + // while the one that didn't -- obj{}. So we also need to check + // obj{} vs obj[as]{} consistency if both are synthesized. + // + // (This is actually even hairier than that: sometimes, the + // obj{} library will race ahead in its matching and manage to + // create the obja/objs{} prerequisite. In which case we will + // end up failing the above check, which will be quite confusing + // and which is the reason we have added the "value came from + // group" info above). + // + // Implementing this verification in this racing environment is + // challanging, to put it mildly. So what we are going to do is, + // in case of a group, also enter the member (which we will be + // doing anyway shortly). If we were the ones who created it, + // then we have "staked out" our view of its variables and any + // subsequent attempts to enter it will trigger the above + // check. If, however, this target was already there, then we + // verify that it's consistent with the values we expect. + // + if (group) + { + const target_type& tt (mod ? tts.bmi : tts.obj); + + // @@ PERF: maybe we should stash the member in pd and avoid + // another search when we pick the member? (Though obj{} + // might also not be synthesized.) + + pair<target&, ulock> r ( + search_new_locked ( + ctx, tt, d, dir_path (), *cp.tk.name, nullptr, cp.scope)); + + if (r.second.owns_lock ()) + r.second.unlock (); + else + { + const target& cmt (r.first); + + if (!absent || + !operator>= (cmt.decl, target_decl::implied)) // VC14 + { + if (absent && cmt.vars.empty ()) + ; // Optimize for the common case. + else + { + check (c_poptions, cpt, cr_ops->c_p, cmt); + check (x_poptions, cpt, cr_ops->x_p, cmt); + check (c_coptions, cpt, cr_ops->c_c, cmt); + check (x_coptions, cpt, cr_ops->x_c, cmt); + } + } + } + } + } + } + + if (locked) + r.second.unlock (); + + // If we shouldn't clean obj{}, then it is fair to assume we + // shouldn't clean the source either (generated source will be in + // the same directory as obj{} and if not, well, go find yourself + // another build system ;-)). + // + // Note: should be done after setting/verifying variables since can + // be an operation batch. + // + if (skip (cpt)) + { + pt = nullptr; + continue; + } + + pt = &cpt; + pd = group ? 1 : 0; mk = mod ? 2 : 1; } else if (p.is_a<libx> () || @@ -1310,7 +1621,7 @@ namespace build2 // if (const string* t = cast_null<string> ( ft->state[a].lookup_original ( - c_type, true /* target_only */).first)) + c_type, lookup_limit::target).first)) { if (recursively_binless (*t)) continue; @@ -1330,7 +1641,7 @@ namespace build2 { auto find = [&t, &bs] (const variable& v) -> lookup { - return t.lookup_original (v, false, &bs).first; + return t.lookup_original (v, &bs).first; }; auto has_simple = [] (lookup l) @@ -1729,9 +2040,9 @@ namespace build2 } } - // Process prerequisites, pass 2: finish rule chaining but don't start - // matching anything yet since that may trigger recursive matching of - // bmi{} targets we haven't completed yet. Hairy, I know. + // Process prerequisites, pass 2: finish dependency synthesis but don't + // start matching anything yet since that may trigger recursive matching + // of bmi{} targets we haven't completed yet. Hairy, I know. // // Parallel prerequisites/prerequisite_targets loop. @@ -1759,14 +2070,14 @@ namespace build2 // Note that if this is a library not to be cleaned, we keep it // marked for completion (see the next phase). } - else if (mk == 1 || mk == 2) // Source/module chain. + else if (mk == 1 || mk == 2) // Synthesized source/module dependency. { bool mod (mk == 2); // p is_a x_mod mk = 1; const target& rt (*pt); - bool group (!p.prerequisite.belongs (t)); // Group's prerequisite. + bool group (pd != 0); // Group's prerequisite (see pass 1). // If we have created a obj/bmi{} target group, pick one of its // members; the rest would be primarily concerned with it. @@ -1812,7 +2123,7 @@ namespace build2 } // Add our lib*{} (see the export.* machinery for details) and - // bmi*{} (both original and chained; see module search logic) + // bmi*{} (both original and synthesized; see module search logic) // prerequisites. // // Note that we don't resolve lib{} to liba{}/libs{} here @@ -1833,7 +2144,8 @@ namespace build2 size_t j (start); for (prerequisite_member p: group_prerequisite_members (a, t)) { - const target* pt (pts[j++]); + const target* pt (pts[j].target); + uintptr_t& pd (pts[j++].data); if (pt == nullptr) // Note: ad hoc is taken care of. continue; @@ -1847,7 +2159,8 @@ namespace build2 { ps.push_back (p.as_prerequisite ()); } - else if (x_mod != nullptr && p.is_a (*x_mod)) // Chained module. + else if (x_mod != nullptr && p.is_a (*x_mod)) // Synthesized + // module dependency. { // Searched during pass 1 but can be NULL or marked. // @@ -1857,7 +2170,7 @@ namespace build2 // was a group, then we would have picked up a member. So // here we may have to "unpick" it. // - bool group (j < i && !p.prerequisite.belongs (t)); + bool group (j < i && pd != 0); unmark (pt); ps.push_back (prerequisite (group ? *pt->group : *pt)); @@ -1975,7 +2288,7 @@ namespace build2 lookup l (p.prerequisite.vars[var]); if (!l.defined ()) - l = pt->lookup_original (var, true /* target_only */).first; + l = pt->lookup_original (var, lookup_limit::target).first; if (!l.defined ()) { @@ -1997,7 +2310,8 @@ namespace build2 mark (pt, mk); } - // Process prerequisites, pass 3: match everything and verify chains. + // Process prerequisites, pass 3: match everything and verify synthesized + // dependencies. // // Wait with unlocked phase to allow phase switching. @@ -2051,7 +2365,9 @@ namespace build2 i = start; for (prerequisite_member p: group_prerequisite_members (a, t)) { - const target*& pt (pts[i++]); + const target*& pt (pts[i].target); + uintptr_t& pd (pts[i++].data); + // Skipped or not marked for completion. // @@ -2088,7 +2404,7 @@ namespace build2 if (&tp != &tp1) { - bool group (!p.prerequisite.belongs (t)); + bool group (pd != 0); // See pass 1. const target_type& rtt (mod ? (group ? bmi::static_type : tts.bmi) @@ -2193,12 +2509,19 @@ namespace build2 // appear after the preceding static library of which this binless // library is a dependency. // + // Note that we omit the duplicate suppression if we are linking the + // whole archive since the previous instance may not necessarily do + // the same (see GH issue #411; we could have complicated things and + // stored the flag in appended_libraries but it doesn't feel + // worthwhile in this case). + // // From the process_libraries() semantics we know that this callback // is always called and always after the options callbacks. // - appended_library* al (l != nullptr - ? &d.ls.append (*l, d.args.size ()) - : d.ls.append (ns, d.args.size ())); + appended_library* al ( + f & lflag_whole ? nullptr : + l != nullptr ? &d.ls.append (*l, d.args.size ()) : + d.ls.append (ns, d.args.size ())); if (al != nullptr && al->end != appended_library::npos) // Closed. { @@ -2397,13 +2720,13 @@ namespace build2 else { d.args.push_back ("-Wl,--whole-archive"); - d.args.push_back (move (p)); + d.args.push_back (move (p)); p.clear (); d.args.push_back ("-Wl,--no-whole-archive"); - goto done; } } - d.args.push_back (move (p)); + if (!p.empty ()) + d.args.push_back (move (p)); } if (d.cs != nullptr) @@ -3199,10 +3522,6 @@ namespace build2 // args.push_back ("/NOLOGO"); - // Add /MACHINE. - // - args.push_back (msvc_machine (cast<string> (rs[x_target_cpu]))); - // For utility libraries use thin archives if possible. // // LLVM's lib replacement had the /LLVMLIBTHIN option at least from @@ -3269,7 +3588,16 @@ namespace build2 { // Are we using the compiler or the linker (e.g., link.exe) directly? // - bool ldc (tsys != "win32-msvc"); + bool ldc; + + if (tsys == "win32-msvc") + { + args.push_back ("/NOLOGO"); + ldc = false; + } + else + ldc = true; + if (ldc) { @@ -3679,6 +4007,20 @@ namespace build2 if (lt.shared_library () && (tsys == "win32-msvc" || tsys == "mingw32")) reli = relative (find_adhoc_member<libi> (t)->path ()); + if (tsys == "win32-msvc") + { + // Add /MACHINE unless there is a custom value (/MACHINE:ARM64EC). + // + // Note that we don't bother hashing it since to change its value one + // would have to use a different MSVC toolchain (which means things + // would be rebuilt from scratch anyway). + // + if (!find_option_prefix ("/MACHINE:", args, true)) + { + args.push_back (msvc_machine (cast<string> (rs[x_target_cpu]))); + } + } + const process_path* ld (nullptr); if (lt.static_library ()) { @@ -3702,15 +4044,10 @@ namespace build2 // Using link.exe directly. // ld = &cast<process_path> (rs["bin.ld.path"]); - args.push_back ("/NOLOGO"); if (ot == otype::s) args.push_back ("/DLL"); - // Add /MACHINE. - // - args.push_back (msvc_machine (cast<string> (rs[x_target_cpu]))); - // Unless explicitly enabled with /INCREMENTAL, disable incremental // linking (it is implicitly enabled if /DEBUG is specified). The // reason is the .ilk file: its name cannot be changed and if we diff --git a/libbuild2/cc/module.cxx b/libbuild2/cc/module.cxx index cf6c6e4..a3c64d9 100644 --- a/libbuild2/cc/module.cxx +++ b/libbuild2/cc/module.cxx @@ -22,6 +22,12 @@ namespace build2 { namespace cc { + // cc.core_module + // + const string core_module::name ("cc.core.config"); + + // x.config_module + // void config_module:: guess (scope& rs, const location& loc, const variable_map&) { @@ -891,6 +897,9 @@ namespace build2 config::save_environment (rs, xi.platform_environment); } + // x module + // + // Global cache of ad hoc importable headers. // // The key is a hash of the system header search directories diff --git a/libbuild2/cc/module.hxx b/libbuild2/cc/module.hxx index 4213516..d4e9a67 100644 --- a/libbuild2/cc/module.hxx +++ b/libbuild2/cc/module.hxx @@ -14,6 +14,8 @@ #include <libbuild2/cc/common.hxx> +#include <libbuild2/cc/compiledb.hxx> + #include <libbuild2/cc/compile-rule.hxx> #include <libbuild2/cc/link-rule.hxx> #include <libbuild2/cc/install-rule.hxx> @@ -27,6 +29,35 @@ namespace build2 { struct compiler_info; + // cc.core module + // + class core_module: public build2::module + { + public: + static const string name; + + explicit + core_module (const core_module* om) + : outer_module_ (om) + { + } + + public: + const core_module* outer_module_; + + strings cdb_names_; + + const compiledb_name_filter* cdb_filter_ = nullptr; + const compiledb_type_filter* cdb_filter_input_ = nullptr; + const compiledb_type_filter* cdb_filter_output_ = nullptr; + + compiledb_name_filter cdb_filter_storage_; + compiledb_type_filter cdb_filter_input_storage_; + compiledb_type_filter cdb_filter_output_storage_; + }; + + // x.config module + // class LIBBUILD2_CC_SYMEXPORT config_module: public build2::module, public config_data { @@ -92,13 +123,36 @@ namespace build2 // struct header_key { - path file; + // We used to use path comparison/hash which are case-insensitive on + // Windows. While this sounds right on the surface, the catch is that + // our target names are always case-sensitive, even on Windows. So + // what can happen is that the same header spelled in different case + // ends up with distinct targets but trying to occupy the same cache + // entry. See GH issue #390 for details. + // + // The conceptually correct way to fix this would be to actualize the + // header name. But that would be prohibitively expensive, especially + // on Windows. Plus the header may be generated and thus not yet + // exist. + // + // On the other hand, we can already end up with different targets + // mapped to the same filesystem entry in other situations (h{} vs + // hxx{} is the canonical example). And we are ok with that provided + // they have noop recipes. So it feels like the simplest solution here + // is to go along by having case-sensitive cache entries. This means + // that auto-generated headers will have to be included using the same + // spelling, but that seems like a sensible restriction (doing + // otherwise won't be portable). + // + path file; size_t hash; friend bool operator== (const header_key& x, const header_key& y) { - return x.file == y.file; // Note: hash was already compared. + // Note: hash was already compared. + // + return x.file.string () == y.file.string (); } }; @@ -130,6 +184,8 @@ namespace build2 msvc_library_search_dirs (const compiler_info&, scope&) const; }; + // x module + // class LIBBUILD2_CC_SYMEXPORT module: public build2::module, public virtual common, public link_rule, @@ -139,7 +195,6 @@ namespace build2 public predefs_rule { public: - explicit module (data&& d, const scope& rs) : common (move (d)), link_rule (move (d)), diff --git a/libbuild2/cc/msvc.cxx b/libbuild2/cc/msvc.cxx index d21969c..416df36 100644 --- a/libbuild2/cc/msvc.cxx +++ b/libbuild2/cc/msvc.cxx @@ -33,10 +33,10 @@ namespace build2 const char* msvc_cpu (const string& cpu) { - const char* m (cpu == "i386" || cpu == "i686" ? "x86" : - cpu == "x86_64" ? "x64" : + const char* m (cpu == "x86_64" ? "x64" : + cpu == "i386" || cpu == "i686" ? "x86" : + cpu == "aarch64" ? "arm64" : cpu == "arm" ? "arm" : - cpu == "arm64" ? "arm64" : nullptr); if (m == nullptr) @@ -51,10 +51,10 @@ namespace build2 const char* msvc_machine (const string& cpu) { - const char* m (cpu == "i386" || cpu == "i686" ? "/MACHINE:x86" : - cpu == "x86_64" ? "/MACHINE:x64" : + const char* m (cpu == "x86_64" ? "/MACHINE:x64" : + cpu == "i386" || cpu == "i686" ? "/MACHINE:x86" : + cpu == "aarch64" ? "/MACHINE:ARM64" : cpu == "arm" ? "/MACHINE:ARM" : - cpu == "arm64" ? "/MACHINE:ARM64" : nullptr); if (m == nullptr) diff --git a/libbuild2/cc/pkgconfig.cxx b/libbuild2/cc/pkgconfig.cxx index 046fbc8..79a38ea 100644 --- a/libbuild2/cc/pkgconfig.cxx +++ b/libbuild2/cc/pkgconfig.cxx @@ -1,6 +1,8 @@ // file : libbuild2/cc/pkgconfig.cxx -*- C++ -*- // license : MIT; see accompanying LICENSE file +#include <libbuild2/cc/pkgconfig.hxx> + #include <libbuild2/scope.hxx> #include <libbuild2/target.hxx> #include <libbuild2/context.hxx> @@ -18,7 +20,6 @@ #include <libbuild2/cc/utility.hxx> #include <libbuild2/cc/common.hxx> -#include <libbuild2/cc/pkgconfig.hxx> #include <libbuild2/cc/compile-rule.hxx> #include <libbuild2/cc/link-rule.hxx> @@ -707,6 +708,7 @@ namespace build2 cmp ("user32") || cmp ("userenv") || cmp ("uuid") || + cmp ("uxtheme") || cmp ("version") || cmp ("windowscodecs") || cmp ("winhttp") || @@ -2256,7 +2258,7 @@ namespace build2 const string& t ( cast<string> ( l.state[a].lookup_original ( - c_type, true /* target_only */).first)); + c_type, lookup_limit::target).first)); // If common, then only save the language (the rest could be // static/shared-specific; strictly speaking even the language could @@ -2276,7 +2278,7 @@ namespace build2 // if (cast_false<bool> (l.lookup_original ( ctx.var_pool["bin.whole"], - true /* target_only */).first)) + lookup_limit::target).first)) { os << endl << "bin.whole = true" << endl; diff --git a/libbuild2/cc/windows-manifest.cxx b/libbuild2/cc/windows-manifest.cxx index 14f4a53..ff77327 100644 --- a/libbuild2/cc/windows-manifest.cxx +++ b/libbuild2/cc/windows-manifest.cxx @@ -23,8 +23,9 @@ namespace build2 const char* windows_manifest_arch (const string& tcpu) { - const char* pa (tcpu == "i386" || tcpu == "i686" ? "x86" : - tcpu == "x86_64" ? "amd64" : + const char* pa (tcpu == "x86_64" ? "amd64" : + tcpu == "i386" || tcpu == "i686" ? "x86" : + tcpu == "aarch64" ? "arm64" : nullptr); if (pa == nullptr) |