From 4cf87fa84a6938e262fd6122e654e5a483a78abe Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Fri, 11 Dec 2020 07:20:18 +0200 Subject: Add support for module interface-only libraries Also suppress generation of the object file in cases where we don't need it. --- libbuild2/bin/init.cxx | 21 +++-- libbuild2/bin/utility.cxx | 16 ++-- libbuild2/bin/utility.hxx | 2 +- libbuild2/c/init.cxx | 2 + libbuild2/cc/common.cxx | 4 +- libbuild2/cc/common.hxx | 12 +++ libbuild2/cc/compile-rule.cxx | 97 +++++++++++++++++------ libbuild2/cc/compile-rule.hxx | 2 +- libbuild2/cc/link-rule.cxx | 176 ++++++++++++++++++++++++++++++++++-------- libbuild2/cc/link-rule.hxx | 8 ++ libbuild2/cc/pkgconfig.cxx | 2 +- libbuild2/cxx/init.cxx | 2 + 12 files changed, 269 insertions(+), 75 deletions(-) diff --git a/libbuild2/bin/init.cxx b/libbuild2/bin/init.cxx index 9c16432..49ba518 100644 --- a/libbuild2/bin/init.cxx +++ b/libbuild2/bin/init.cxx @@ -116,12 +116,23 @@ namespace build2 // // If unspecified, defaults to false for liba{} and to true for libu*{}. // - vp.insert ("bin.whole", variable_visibility::target); + vp.insert ("bin.whole", variable_visibility::target); - vp.insert ("bin.exe.prefix"); - vp.insert ("bin.exe.suffix"); - vp.insert ("bin.lib.prefix"); - vp.insert ("bin.lib.suffix"); + // Mark library as binless. + // + // For example, the user can mark a C++ library consisting of only + // module interfaces as binless so it becomes a modules equivalent to + // header-only library (which we will call a module interface-only + // library). + // + vp.insert ("bin.binless", variable_visibility::target); + + // Executable and library name prefixes and suffixes. + // + vp.insert ("bin.exe.prefix"); + vp.insert ("bin.exe.suffix"); + vp.insert ("bin.lib.prefix"); + vp.insert ("bin.lib.suffix"); // The optional custom clean patterns should be just the pattern stem, // without the library prefix/name or extension. For example, `-[A-Z]` diff --git a/libbuild2/bin/utility.cxx b/libbuild2/bin/utility.cxx index 6b0c4de..11230cd 100644 --- a/libbuild2/bin/utility.cxx +++ b/libbuild2/bin/utility.cxx @@ -47,9 +47,11 @@ namespace build2 return lmembers {a, s}; } - const target* + const file* link_member (const libx& x, action a, linfo li, bool exist) { + const target* r; + if (x.is_a ()) { // For libul{} that is linked to an executable the member choice @@ -72,7 +74,7 @@ namespace build2 // Called by the compile rule during execute. // - return x.ctx.phase == run_phase::match && !exist + r = x.ctx.phase == run_phase::match && !exist ? &search (x, tt, x.dir, x.out, x.name) : search_existing (x.ctx, tt, x.dir, x.out, x.name); } @@ -87,15 +89,17 @@ namespace build2 group_view gv (resolve_members (a, l)); assert (gv.members != nullptr); - pair r ( + pair p ( link_member (lmembers {l.a != nullptr, l.s != nullptr}, li.order)); - if (!r.second) - fail << (r.first == otype::s ? "shared" : "static") + if (!p.second) + fail << (p.first == otype::s ? "shared" : "static") << " variant of " << l << " is not available"; - return r.first == otype::s ? static_cast (l.s) : l.a; + r = p.first == otype::s ? static_cast (l.s) : l.a; } + + return static_cast (r); } pattern_paths diff --git a/libbuild2/bin/utility.hxx b/libbuild2/bin/utility.hxx index 5d7eed4..e12bb5f 100644 --- a/libbuild2/bin/utility.hxx +++ b/libbuild2/bin/utility.hxx @@ -59,7 +59,7 @@ namespace build2 // If existing is true, then only return the member target if it exists // (currently only used and supported for utility libraries). // - LIBBUILD2_BIN_SYMEXPORT const target* + LIBBUILD2_BIN_SYMEXPORT const file* link_member (const libx&, action, linfo, bool existing = false); // As above but return otype::a or otype::s as well as an indication if diff --git a/libbuild2/c/init.cxx b/libbuild2/c/init.cxx index 923fbcb..227afb2 100644 --- a/libbuild2/c/init.cxx +++ b/libbuild2/c/init.cxx @@ -161,6 +161,8 @@ namespace build2 hinters, + vp["bin.binless"], + // NOTE: remember to update documentation if changing anything here. // vp.insert ("config.c"), diff --git a/libbuild2/cc/common.cxx b/libbuild2/cc/common.cxx index 62f0ee0..9cac1b0 100644 --- a/libbuild2/cc/common.cxx +++ b/libbuild2/cc/common.cxx @@ -721,7 +721,7 @@ namespace build2 } // Look for binary-less libraries via pkg-config .pc files. Note that - // it is possible we have already found one of them as binfull but the + // it is possible we have already found one of them as binful but the // other is binless. // { @@ -731,7 +731,7 @@ namespace build2 if (na || ns) { // Only consider the common .pc file if we can be sure there - // is no binfull variant. + // is no binful variant. // pair r ( pkgconfig_search (d, p.proj, name, na && ns /* common */)); diff --git a/libbuild2/cc/common.hxx b/libbuild2/cc/common.hxx index bf971ab..f072591 100644 --- a/libbuild2/cc/common.hxx +++ b/libbuild2/cc/common.hxx @@ -42,6 +42,18 @@ namespace build2 // const char* const* x_hinters; + // We set this variable on the bmi*{} target to indicate whether it + // belongs to a binless library. More specifically, it controls both + // the production and consumption (linking) of the object file with + // the following possible states: + // + // produce consume + // true y y (binless normal or sidebuild) + // false n n (binful sidebuild) + // absent y n (binful normal) + // + const variable& b_binless; // bin.binless + const variable& config_x; const variable& config_x_id; // [-] const variable& config_x_version; diff --git a/libbuild2/cc/compile-rule.cxx b/libbuild2/cc/compile-rule.cxx index 7cea9d6..a02b3c4 100644 --- a/libbuild2/cc/compile-rule.cxx +++ b/libbuild2/cc/compile-rule.cxx @@ -704,12 +704,16 @@ namespace build2 } } - // If we are compiling a BMI-producing module TU, then the obj*{} is - // an ad hoc member of bmi*{}. For now neither GCC nor Clang produce - // an object file for a header unit (but something tells me this is - // going to change). + // If we are compiling a BMI-producing module TU, then add obj*{} an + // ad hoc member of bmi*{} unless we only need the BMI (see + // config_data::b_binless for details). // - if (ut == unit_type::module_intf) // Note: still unrefined. + // For now neither GCC nor Clang produce an object file for a header + // unit (but something tells me this might change). + // + // Note: ut is still unrefined. + // + if (ut == unit_type::module_intf && cast_true (t[b_binless])) { // The module interface unit can be the same as an implementation // (e.g., foo.mxx and foo.cxx) which means obj*{} targets could @@ -5404,8 +5408,8 @@ namespace build2 // implicit import, if you will). Do you see where it's going? Nowever // good, that's right. This shallow reference means that the compiler // should be able to find BMIs for all the re-exported modules, - // recursive. The good news is we are actually in a pretty good shape to - // handle this: after match all our prerequisite BMIs will have their + // recursively. The good news is we are actually in a pretty good shape + // to handle this: after match all our prerequisite BMIs will have their // prerequisite BMIs known, recursively. The only bit that is missing is // the re-export flag of some sorts. As well as deciding where to handle // it: here or in append_module_options(). After some meditation it @@ -5423,6 +5427,8 @@ namespace build2 // bmi{}s have been matched by the same rule. But let's not kid // ourselves, there will be no other rule that matches bmi{}s. // + // @@ I think now we could use prerequisite_targets::data for this? + // // 2. Once we have matched all the bmi{}s we are importing directly // (with all the re-exported by us at the back), we will go over them // and copy all of their re-exported bmi{}s (using the position we @@ -5553,12 +5559,12 @@ namespace build2 if (pt != nullptr) { - const target* lt (nullptr); + const file* lt (nullptr); if (const libx* l = pt->is_a ()) lt = link_member (*l, a, li); else if (pt->is_a () || pt->is_a () || pt->is_a ()) - lt = pt; + lt = &pt->as (); // If this is a library, check its bmi{}s and mxx{}s. // @@ -5951,7 +5957,7 @@ namespace build2 const file& compile_rule:: make_module_sidebuild (action a, const scope& bs, - const target& lt, + const file& lt, const target& mt, const string& mn) const { @@ -5965,17 +5971,16 @@ namespace build2 // not to conflict with other modules. If we assume that within an // amalgamation there is only one "version" of each module, then the // module name itself seems like a good fit. We just replace '.' with - // '-'. + // '-' and ':' with '+'. // string mf; transform (mn.begin (), mn.end (), back_inserter (mf), - [] (char c) {return c == '.' ? '-' : c;}); + [] (char c) {return c == '.' ? '-' : c == ':' ? '+' : c;}); // It seems natural to build a BMI type that corresponds to the library // type. After all, this is where the object file part of the BMI is - // going to come from (though things will probably be different for - // module-only libraries). + // going to come from (unless it's a module interface-only library). // const target_type& tt (compile_types (link_type (lt).type).bmi); @@ -6012,6 +6017,10 @@ namespace build2 // @@ TODO: will probably need revision if using sidebuild for // non-installed libraries (e.g., direct BMI dependencies // will probably have to be translated to mxx{} or some such). + // Hm, don't think we want it this way: we want BMIs of binless + // library to be built in the library rather than on the side + // (so they can be potentially re-used by multiple independent + // importers). // if (p.is_a () || p.is_a () || p.is_a () || p.is_a ()) @@ -6034,8 +6043,15 @@ namespace build2 // while we were preparing the prerequisite list. // if (p.second.owns_lock ()) + { bt.prerequisites (move (ps)); + // Unless this is a binless library, we don't need the object file + // (see config_data::b_binless for details). + // + bt.vars.assign (b_binless) = (lt.mtime () == timestamp_unreal); + } + return bt; } @@ -6410,17 +6426,28 @@ namespace build2 cstrings args {cpath.recall_string ()}; // If we are building a module interface or partition, then the target - // is bmi*{} and its ad hoc member is obj*{}. For header units there is - // no obj*{}. + // is bmi*{} and it may have an ad hoc obj*{} member. For header units + // there is no obj*{} (see the corresponding add_adhoc_member() call in + // apply()). // path relm; - path relo (ut == unit_type::module_header - ? path () - : relative (ut == unit_type::module_intf || - ut == unit_type::module_intf_part || - ut == unit_type::module_impl_part - ? find_adhoc_member (t, tts.obj)->path () - : tp)); + path relo; + switch (ut) + { + case unit_type::module_header: + break; + case unit_type::module_intf: + case unit_type::module_intf_part: + case unit_type::module_impl_part: + { + if (const file* o = find_adhoc_member (t, tts.obj)) + relo = relative (o->path ()); + + break; + } + default: + relo = relative (tp); + } // Build the command line. // @@ -6526,6 +6553,8 @@ namespace build2 // Note also that what we are doing here appears to be incompatible // with PCH (/Y* options) and /Gm (minimal rebuild). // + // @@ MOD: TODO deal with absent relo. + // if (find_options ({"/Zi", "/ZI"}, args)) { if (fc) @@ -6699,16 +6728,34 @@ namespace build2 // Output module file is specified in the mapping file, the // same as input. // - if (ut != unit_type::module_header) // No object file. + if (ut == unit_type::module_header) // No obj, -c implied. + break; + + if (!relo.empty ()) { args.push_back ("-o"); args.push_back (relo.string ().c_str ()); - args.push_back ("-c"); } + else if (ut != unit_type::module_header) + { + // Should this be specified in append_lang_options() like + // -fmodule-header (which, BTW, implies -fmodule-only)? + // While it's plausible that -fmodule-header has some + // semantic differences that should be in effect during + // preprocessing, -fmodule-only seems to only mean "don't + // write the object file" so for now we specify it only + // here. + // + args.push_back ("-fmodule-only"); + } + + args.push_back ("-c"); break; } case compiler_type::clang: { + // @@ MOD TODO: deal with absent relo. + relm = relative (tp); args.push_back ("-o"); diff --git a/libbuild2/cc/compile-rule.hxx b/libbuild2/cc/compile-rule.hxx index 2d80d18..a716b4c 100644 --- a/libbuild2/cc/compile-rule.hxx +++ b/libbuild2/cc/compile-rule.hxx @@ -166,7 +166,7 @@ namespace build2 find_modules_sidebuild (const scope&) const; const file& - make_module_sidebuild (action, const scope&, const target&, + make_module_sidebuild (action, const scope&, const file&, const target&, const string&) const; const file& diff --git a/libbuild2/cc/link-rule.cxx b/libbuild2/cc/link-rule.cxx index dea5879..3f6824e 100644 --- a/libbuild2/cc/link-rule.cxx +++ b/libbuild2/cc/link-rule.cxx @@ -583,37 +583,37 @@ namespace build2 move (cp_l), move (cp_v)}; } - // Look for binary-full utility library recursively until we hit a - // non-utility "barier". + // Look for binful utility library recursively until we hit a non-utility + // "barier". // - static bool - find_binfull (action a, const target& t, linfo li) + static const libux* + find_binful (action a, const target& t, linfo li) { for (const target* pt: t.prerequisite_targets[a]) { if (pt == nullptr || unmark (pt) != 0) // Called after pass 1 below. continue; - const file* pf; + const libux* ux; // If this is the libu*{} group, then pick the appropriate member. // if (const libul* ul = pt->is_a ()) { - pf = &link_member (*ul, a, li)->as (); + ux = &link_member (*ul, a, li)->as (); } - else if ((pf = pt->is_a ()) || - (pf = pt->is_a ()) || - (pf = pt->is_a ())) + else if ((ux = pt->is_a ()) || + (ux = pt->is_a ()) || + (ux = pt->is_a ())) ; else continue; - if (!pf->path ().empty () || find_binfull (a, *pf, li)) - return true; + if (!ux->path ().empty () || (ux = find_binful (a, *ux, li))) + return ux; } - return false; + return nullptr; }; recipe link_rule:: @@ -642,6 +642,7 @@ namespace build2 t.state[a].assign (c_type) = string (x); bool binless (lt.library ()); // Binary-less until proven otherwise. + bool user_binless (lt.library () && cast_false (t[b_binless])); // Inject dependency on the output directory. Note that we do it even // for binless libraries since there could be other output (e.g., .pc @@ -655,7 +656,7 @@ namespace build2 // // 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 - // binless since it can depend on a binfull utility library. This we + // binless since it can depend on a binful utility library. This we // check below, after matching the libraries. // // We do libraries first in order to indicate that we will execute these @@ -680,9 +681,9 @@ namespace build2 optional usr_lib_dirs; // Extract lazily. compile_target_types tts (compile_types (ot)); - auto skip = [&a, &rs] (const target* pt) -> bool + auto skip = [&a, &rs] (const target& pt) -> bool { - return a.operation () == clean_id && !pt->dir.sub (rs.out_path ()); + return a.operation () == clean_id && !pt.dir.sub (rs.out_path ()); }; auto& pts (t.prerequisite_targets[a]); @@ -713,15 +714,13 @@ namespace build2 if (mod || p.is_a (x_src) || p.is_a ()) { - binless = binless && false; + binless = binless && (mod ? user_binless : false); // Rule chaining, part 1. // - // Which scope shall we use to resolve the root? Unlikely, but // possible, the prerequisite is from a different project // altogether. So we are going to use the target's project. - // // If the source came from the lib{} group, then create the obj{} // group and add the source as a prerequisite of the obj{} group, @@ -763,19 +762,44 @@ 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. // - pt = &search (t, rtt, d, dir_path (), *cp.tk.name, nullptr, cp.scope); + pair r ( + search_locked ( + t, 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 (pt)) + if (skip (r.first)) { pt = nullptr; continue; } + // Either set of verify the bin.binless value on this bmi*{} target + // (see config_data::b_binless for semantics). + // + if (mod) + { + if (r.second.owns_lock ()) + { + if (user_binless) + r.first.assign (b_binless) = true; + } + else + { + lookup l (r.first[b_binless]); + + if (user_binless ? !cast_false (l) : l.defined ()) + fail << "synthesized dependency for prerequisite " << p + << " would be incompatible with existing target " + << r.first << + info << "incompatible bin.binless value"; + } + } + + pt = &r.first; m = mod ? 2 : 1; } else if (p.is_a () || @@ -797,7 +821,7 @@ namespace build2 if (pt == nullptr) pt = &p.search (t); - if (skip (pt)) + if (skip (*pt)) m = 3; // Mark so it is not matched. // If this is the lib{}/libu{} group, then pick the appropriate @@ -844,19 +868,28 @@ namespace build2 pt = &p.search (t); } - if (skip (pt)) + if (skip (*pt)) { pt = nullptr; continue; } - // @@ MODHDR: hbmix{} has no objx{} + // Header BMIs have no object file. Module BMI must be explicitly + // marked with bin.binless by the user to be usable in a binless + // library. // - binless = binless && !(pt->is_a () || pt->is_a ()); + binless = binless && !( + pt->is_a () || + (pt->is_a () && + !pt->is_a () && + cast_false ((*pt)[b_binless]))); m = 3; } + if (user_binless && !binless) + fail << t << " cannot be binless due to " << p << " prerequisite"; + mark (pt, m); } @@ -864,9 +897,19 @@ namespace build2 // match_members (a, t, pts, start); - // Check if we have any binfull utility libraries. + // Check if we have any binful utility libraries. // - binless = binless && !find_binfull (a, t, li); + if (binless) + { + if (const libux* l = find_binful (a, t, li)) + { + binless = false; + + if (user_binless) + fail << t << " cannot be binless due to binful " << *l + << " prerequisite"; + } + } // Now that we know for sure whether we are binless, derive file name(s) // and add ad hoc group members. Note that for binless we still need the @@ -1176,10 +1219,11 @@ namespace build2 ? (group ? bmi::static_type : tts.bmi) : (group ? obj::static_type : tts.obj)); - // If this obj*{} already has prerequisites, then verify they are - // "compatible" with what we are doing here. Otherwise, synthesize - // the dependency. Note that we may also end up synthesizing with - // someone beating us to it. In this case also verify. + // If this obj*/bmi*{} already has prerequisites, then verify they + // are "compatible" with what we are doing here. Otherwise, + // synthesize the dependency. Note that we may also end up + // synthesizing with someone beating us to it. In this case also + // verify. // bool verify (true); @@ -1397,7 +1441,7 @@ namespace build2 // If this is a library not to be cleaned, we can finally blank it // out. // - if (skip (pt)) + if (skip (*pt)) { pt = nullptr; continue; @@ -2003,6 +2047,50 @@ namespace build2 } } + // Append object files of bmi{} prerequisites that belong to binless + // libraries. + // + void link_rule:: + append_binless_modules (strings& args, + const scope& bs, action a, const file& t) const + { + // Note that here we don't need to hoist anything on duplicate detection + // since the order in which we link object files is not important. + // + for (const target* pt: t.prerequisite_targets[a]) + { + if (pt != nullptr && + pt->is_a () && + cast_false ((*pt)[b_binless])) + { + const objx& o (*find_adhoc_member (*pt)); // Must be there. + string p (relative (o.path ()).string ()); + if (find (args.begin (), args.end (), p) == args.end ()) + { + args.push_back (move (p)); + append_binless_modules (args, bs, a, o); + } + } + } + } + + void link_rule:: + append_binless_modules (sha256& cs, + const scope& bs, action a, const file& t) const + { + for (const target* pt: t.prerequisite_targets[a]) + { + if (pt != nullptr && + pt->is_a () && + cast_false ((*pt)[b_binless])) + { + const objx& o (*find_adhoc_member (*pt)); + hash_path (cs, o.path (), bs.root_scope ()->out_path ()); + append_binless_modules (cs, bs, a, o); + } + } + } + // Filter link.exe noise (msvc.cxx). // void @@ -2635,8 +2723,13 @@ namespace build2 // if (modules) { - if (pt->is_a ()) // @@ MODHDR: hbmix{} has no objx{} + if (pt->is_a ()) + { pt = find_adhoc_member (*pt, tts.obj); + + if (pt == nullptr) // Header BMIs have no object file. + continue; + } } const file* f; @@ -2669,7 +2762,12 @@ namespace build2 f = nullptr; // Timestamp checked by hash_libraries(). } else + { hash_path (cs, f->path (), rs.out_path ()); + + if (modules) + append_binless_modules (cs, rs, a, *f); + } } else if ((f = pt->is_a ())) { @@ -2963,8 +3061,13 @@ namespace build2 if (modules) { - if (pt->is_a ()) // @@ MODHDR: hbmix{} has no objx{} + if (pt->is_a ()) + { pt = find_adhoc_member (*pt, tts.obj); + + if (pt == nullptr) // Header BMIs have no object file. + continue; + } } const file* f; @@ -2985,7 +3088,12 @@ namespace build2 // files might satisfy symbols in the preceding libraries. // als.clear (); - sargs.push_back (relative (f->path ()).string ()); // string()&& + + sargs.push_back (relative (f->path ()).string ()); + + if (modules) + append_binless_modules (sargs, bs, a, *f); + seen_obj = true; } } diff --git a/libbuild2/cc/link-rule.hxx b/libbuild2/cc/link-rule.hxx index 6d0649c..baccf8d 100644 --- a/libbuild2/cc/link-rule.hxx +++ b/libbuild2/cc/link-rule.hxx @@ -131,6 +131,14 @@ namespace build2 const scope&, action, const target&, linfo, bool) const; + void + append_binless_modules (strings&, + const scope&, action, const file&) const; + + void + append_binless_modules (sha256&, + const scope&, action, const file&) const; + protected: static void functions (function_family&, const char*); // functions.cxx diff --git a/libbuild2/cc/pkgconfig.cxx b/libbuild2/cc/pkgconfig.cxx index f0a5e0c..6c0f1fc 100644 --- a/libbuild2/cc/pkgconfig.cxx +++ b/libbuild2/cc/pkgconfig.cxx @@ -1524,7 +1524,7 @@ namespace build2 os << "Libs:"; // While we don't need it for a binless library itselt, it may be - // necessary to resolve its binfull dependencies. + // necessary to resolve its binful dependencies. // os << " -L" << escape (ldir.string ()); diff --git a/libbuild2/cxx/init.cxx b/libbuild2/cxx/init.cxx index 21acedc..ad0dd05 100644 --- a/libbuild2/cxx/init.cxx +++ b/libbuild2/cxx/init.cxx @@ -414,6 +414,8 @@ namespace build2 hinters, + vp["bin.binless"], + // NOTE: remember to update documentation if changing anything here. // vp.insert ("config.cxx"), -- cgit v1.1