From 50f9844b8a97aa06edf09b6d8a538721a0cd24ea Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Thu, 26 Oct 2023 11:15:18 +0200 Subject: Use match options for runtime/buildtime distinction when installing libraries Specifically, now, if a library is installed solely as a prerequisite of an executable (potentially recursively), then only its runtime files are installed omitting everything buildtime-related (static/import libraries, non-versioned symlinks for shared libraries, pkg-config files, headers, etc). If you are familiar with the runtime and -dev/-devel package splits for libraries in Debian/Fedora, this is an analogous semantics. --- libbuild2/bash/rule.cxx | 4 +- libbuild2/bash/rule.hxx | 2 +- libbuild2/bin/target.hxx | 15 ++ libbuild2/cc/install-rule.cxx | 418 ++++++++++++++++++++++++++++++++++++------ libbuild2/cc/install-rule.hxx | 39 ++-- libbuild2/cc/module.cxx | 3 + libbuild2/install/rule.cxx | 415 +++++++++++++++++++++++++++++++---------- libbuild2/install/rule.hxx | 142 ++++++++++---- 8 files changed, 837 insertions(+), 201 deletions(-) diff --git a/libbuild2/bash/rule.cxx b/libbuild2/bash/rule.cxx index 502a206..6e96b34 100644 --- a/libbuild2/bash/rule.cxx +++ b/libbuild2/bash/rule.cxx @@ -455,9 +455,9 @@ namespace build2 } recipe install_rule:: - apply (action a, target& t) const + apply (action a, target& t, match_extra& me) const { - recipe r (file_rule::apply_impl (a, t)); + recipe r (file_rule::apply_impl (a, t, me)); if (r == nullptr) return noop_recipe; diff --git a/libbuild2/bash/rule.hxx b/libbuild2/bash/rule.hxx index 444d176..3f9618f 100644 --- a/libbuild2/bash/rule.hxx +++ b/libbuild2/bash/rule.hxx @@ -74,7 +74,7 @@ namespace build2 match (action, target&) const override; virtual recipe - apply (action, target&) const override; + apply (action, target&, match_extra&) const override; protected: const in_rule& in_; diff --git a/libbuild2/bin/target.hxx b/libbuild2/bin/target.hxx index 9685e39..8f2a92e 100644 --- a/libbuild2/bin/target.hxx +++ b/libbuild2/bin/target.hxx @@ -412,6 +412,21 @@ namespace build2 virtual group_view group_members (action) const override; + // Match options for the install operation on the liba{}/libs{} and + // libua{}/libus{} target types (note: not lib{}/libul{} nor libue{}). + // + // If only install_runtime option is specified, then only install the + // runtime files omitting everything buildtime (headers, pkg-config + // files, shared library version-related symlinks, etc). + // + // Note that it's either runtime-only or runtime and buildtime (i.e., + // everything), so match with install_all instead of install_buildtime + // (the latter is only useful in the rule implementations). + // + static constexpr uint64_t option_install_runtime = 0x01; + static constexpr uint64_t option_install_buildtime = 0x02; + static constexpr uint64_t option_install_all = match_extra::all_options; + public: static const target_type static_type; }; diff --git a/libbuild2/cc/install-rule.cxx b/libbuild2/cc/install-rule.cxx index 3eaaa94..7327711 100644 --- a/libbuild2/cc/install-rule.cxx +++ b/libbuild2/cc/install-rule.cxx @@ -24,14 +24,58 @@ namespace build2 install_rule (data&& d, const link_rule& l) : common (move (d)), link_ (l) {} - const target* install_rule:: + // Wrap the file_rule's recipe into a data-carrying recipe. + // + struct install_match_data + { + build2::recipe recipe; + uint64_t options; // Match options. + link_rule::libs_paths libs_paths; + + target_state + operator() (action a, const target& t) + { + return recipe (a, t); + } + }; + + bool install_rule:: + filter (action a, const target& t, const target& m) const + { + if (!t.is_a ()) + { + // If runtime-only, filter out all known buildtime target types. + // + const auto& md (t.data (a)); + + if ((md.options & lib::option_install_buildtime) == 0) + { + if (m.is_a () || // Staic library. + m.is_a () || // pkg-config file. + m.is_a ()) // Import library. + return false; + } + } + + return true; + } + + pair install_rule:: filter (const scope* is, - action a, const target& t, prerequisite_iterator& i) const + action a, const target& t, prerequisite_iterator& i, + match_extra& me) const { // NOTE: see libux_install_rule::filter() if changing anything here. const prerequisite& p (i->prerequisite); + uint64_t options (match_extra::all_options); + + otype ot (link_type (t).type); + + // @@ TMP: drop eventually. + // +#if 0 // If this is a shared library prerequisite, install it as long as it is // in the installation scope. // @@ -43,7 +87,6 @@ namespace build2 // // Note: we install ad hoc prerequisites by default. // - otype ot (link_type (t).type); // Note: at least one must be true since we only register this rule for // exe{}, and lib[as]{} (this makes sure the following if-condition will @@ -64,26 +107,115 @@ namespace build2 if (const libx* l = pt->is_a ()) pt = link_member (*l, a, link_info (t.base_scope (), ot)); - // Note: not redundant since we are returning a member. + // Note: not redundant since we could be returning a member. // if ((st && pt->is_a ()) || (at && pt->is_a ())) - return is == nullptr || pt->in (*is) ? pt : nullptr; + { + // Adjust match options. + // + if (a.operation () != update_id) + { + if (t.is_a ()) + options = lib::option_install_runtime; + else + { + // This is a library prerequisite of a library target and + // runtime-only begets runtime-only. + // + if (me.cur_options == lib::option_install_runtime) + options = lib::option_install_runtime; + } + } + + return make_pair (is == nullptr || pt->in (*is) ? pt : nullptr, + options); + } // See through to libu*{} members. Note that we are always in the same // project (and thus amalgamation). // if (pt->is_a ()) - return pt; + { + // Adjust match options (similar to above). + // + if (a.operation () != update_id && !pt->is_a ()) + { + if (t.is_a ()) + options = lib::option_install_runtime; + else + { + if (me.cur_options == lib::option_install_runtime) + options = lib::option_install_runtime; + } + } + + return make_pair (pt, options); + } } +#else + // Note that at first it may seem like we don't need to install static + // library prerequisites of executables. But such libraries may still + // have prerequisites that are needed at runtime (say, some data files). + // So we install all libraries as long as they are in the installation + // scope and deal with runtime vs buildtime distiction using match + // options. + // + // Note: for now we assume these prerequisites never come from see- + // through groups. + // + // Note: we install ad hoc prerequisites by default. + // + if (p.is_a () || p.is_a () || p.is_a ()) + { + const target* pt (&search (t, p)); + + // If this is the lib{}/libu*{} group, pick a member which we would + // link. For libu*{} we want the "see through" logic. + // + if (const libx* l = pt->is_a ()) + pt = link_member (*l, a, link_info (t.base_scope (), ot)); + + // Adjust match options. + // + if (a.operation () != update_id) + { + if (t.is_a ()) + options = lib::option_install_runtime; + else + { + // This is a library prerequisite of a library target and + // runtime-only begets runtime-only. + // + if (me.cur_options == lib::option_install_runtime) + options = lib::option_install_runtime; + } + } + + // Note: not redundant since we could be returning a member. + // + if (pt->is_a () || pt->is_a ()) + { + return make_pair (is == nullptr || pt->in (*is) ? pt : nullptr, + options); + } + else // libua{} or libus{} + { + // See through to libu*{} members. Note that we are always in the + // same project (and thus amalgamation). + // + return make_pair (pt, options); + } + } +#endif // The rest of the tests only succeed if the base filter() succeeds. // - const target* pt (file_rule::filter (is, a, t, p)); + const target* pt (file_rule::filter (is, a, t, p, me).first); if (pt == nullptr) - return pt; + return make_pair (pt, options); - // Don't install executable's prerequisite headers and module - // interfaces. + // Don't install executable's or runtime-only library's prerequisite + // headers and module interfaces. // // Note that if they come from a group, then we assume the entire // group is not to be installed. @@ -100,7 +232,9 @@ namespace build2 (x_obj != nullptr && p.is_a (*x_obj))); }; - if (t.is_a ()) + if (t.is_a () || + (a.operation () != update_id && + me.cur_options == lib::option_install_runtime)) { if (header_source (p)) pt = nullptr; @@ -115,7 +249,7 @@ namespace build2 } if (pt == nullptr) - return pt; + return make_pair (pt, options); } // Here is a problem: if the user spells the obj*/bmi*{} targets @@ -145,16 +279,16 @@ namespace build2 { pt = t.is_a () ? nullptr - : file_rule::filter (is, a, *pt, pm.prerequisite); + : file_rule::filter (is, a, *pt, pm.prerequisite, me).first; break; } } if (pt == nullptr) - return pt; + return make_pair (pt, options); } - return pt; + return make_pair (pt, options); } bool install_rule:: @@ -167,27 +301,34 @@ namespace build2 file_rule::match (a, t); } - // Wrap the file_rule's recipe into a data-carrying recipe. - // - struct install_match_data + recipe install_rule:: + apply (action a, target& t, match_extra& me) const { - build2::recipe recipe; - link_rule::libs_paths libs_paths; - - target_state - operator() (action a, const target& t) + // Handle match options. + // + // Do it before calling apply_impl() since we need this information + // in the filter() callbacks. + // + if (a.operation () != update_id) { - return recipe (a, t); + if (!t.is_a ()) + { + if (me.new_options == 0) + me.new_options = lib::option_install_runtime; // Minimum we can do. + + me.cur_options = me.new_options; + } } - }; - recipe install_rule:: - apply (action a, target& t) const - { - recipe r (file_rule::apply_impl (a, t)); + recipe r (file_rule::apply_impl ( + a, t, me, + me.cur_options != match_extra::all_options /* reapply */)); if (r == nullptr) + { + me.cur_options = match_extra::all_options; // Noop for all options. return noop_recipe; + } if (a.operation () == update_id) { @@ -209,29 +350,72 @@ namespace build2 } else // install or uninstall { - // Derive shared library paths and cache them in the target's aux - // storage if we are un/installing (used in the *_extra() functions - // below). - // - if (file* f = t.is_a ()) + file* ls; + if ((ls = t.is_a ()) || t.is_a ()) { - if (!f->path ().empty ()) // Not binless. + // Derive shared library paths and cache them in the target's aux + // storage if we are un/installing (used in the *_extra() functions + // below). + // + link_rule::libs_paths lsp; + if (ls != nullptr && !ls->path ().empty ()) // Not binless. { const string* p (cast_null (t["bin.lib.prefix"])); const string* s (cast_null (t["bin.lib.suffix"])); - return install_match_data { - move (r), - link_.derive_libs_paths (*f, - p != nullptr ? p->c_str (): nullptr, - s != nullptr ? s->c_str (): nullptr)}; + lsp = link_.derive_libs_paths (*ls, + p != nullptr ? p->c_str (): nullptr, + s != nullptr ? s->c_str (): nullptr); } + + return install_match_data {move (r), me.cur_options, move (lsp)}; } } return r; } + void install_rule:: + reapply (action a, target& t, match_extra& me) const + { + tracer trace ("cc::install_rule::reapply"); + + assert (a.operation () != update_id && !t.is_a ()); + + l6 ([&]{trace << "rematching " << t + << ", current options " << me.cur_options + << ", new options " << me.new_options;}); + + me.cur_options |= me.new_options; + + // We also need to update options in install_match_data. + // + t.data (a).options = me.cur_options; + + if ((me.new_options & lib::option_install_buildtime) != 0) + { + // If we are rematched with the buildtime option, propagate it to our + // prerequisite libraries. + // + for (const target* pt: t.prerequisite_targets[a]) + { + if (pt != nullptr && (pt->is_a () || pt->is_a () || + pt->is_a () || pt->is_a ())) + { + // Go for all options instead of just install_buildtime to avoid + // any further relocking/reapply (we only support runtime-only or + // everything). + // + rematch_sync (a, *pt, match_extra::all_options); + } + } + + // Also match any additional prerequisites (e.g., headers). + // + file_rule::reapply_impl (a, t, me); + } + } + bool install_rule:: install_extra (const file& t, const install_dir& id) const { @@ -239,10 +423,15 @@ namespace build2 if (t.is_a ()) { + const auto& md (t.data (perform_install_id)); + // Here we may have a bunch of symlinks that we need to install. // + // Note that for runtime-only install we only omit the name that is + // used for linking (e.g., libfoo.so). + // const scope& rs (t.root_scope ()); - auto& lp (t.data (perform_install_id).libs_paths); + const link_rule::libs_paths& lp (md.libs_paths); auto ln = [&t, &rs, &id] (const path& f, const path& l) { @@ -260,7 +449,10 @@ namespace build2 if (!in.empty ()) {r = ln (*f, in) || r; f = ∈} if (!so.empty ()) {r = ln (*f, so) || r; f = &so;} if (!ld.empty ()) {r = ln (*f, ld) || r; f = &ld;} - if (!lk.empty ()) {r = ln (*f, lk) || r; } + if ((md.options & lib::option_install_buildtime) != 0) + { + if (!lk.empty ()) {r = ln (*f, lk) || r;} + } } return r; @@ -273,10 +465,12 @@ namespace build2 if (t.is_a ()) { + const auto& md (t.data (perform_uninstall_id)); + // Here we may have a bunch of symlinks that we need to uninstall. // const scope& rs (t.root_scope ()); - auto& lp (t.data (perform_uninstall_id).libs_paths); + const link_rule::libs_paths& lp (md.libs_paths); auto rm = [&rs, &id] (const path& f, const path& l) { @@ -293,7 +487,10 @@ namespace build2 if (!in.empty ()) {r = rm (*f, in) || r; f = ∈} if (!so.empty ()) {r = rm (*f, so) || r; f = &so;} if (!ld.empty ()) {r = rm (*f, ld) || r; f = &ld;} - if (!lk.empty ()) {r = rm (*f, lk) || r; } + if ((md.options & lib::option_install_buildtime) != 0) + { + if (!lk.empty ()) {r = rm (*f, lk) || r;} + } } return r; @@ -305,20 +502,27 @@ namespace build2 libux_install_rule (data&& d, const link_rule& l) : common (move (d)), link_ (l) {} - const target* libux_install_rule:: + pair libux_install_rule:: filter (const scope* is, - action a, const target& t, prerequisite_iterator& i) const + action a, const target& t, prerequisite_iterator& i, + match_extra& me) const { using file_rule = install::file_rule; const prerequisite& p (i->prerequisite); + uint64_t options (match_extra::all_options); + + otype ot (link_type (t).type); + // The "see through" semantics that should be parallel to install_rule // above. In particular, here we use libue/libua/libus{} as proxies for // exe/liba/libs{} there. // - otype ot (link_type (t).type); + // @@ TMP: drop eventually. + // +#if 0 bool st (t.is_a () || t.is_a ()); // Target needs shared. bool at (t.is_a () || t.is_a ()); // Target needs static. assert (st || at); @@ -332,15 +536,70 @@ namespace build2 pt = link_member (*l, a, link_info (t.base_scope (), ot)); if ((st && pt->is_a ()) || (at && pt->is_a ())) - return is == nullptr || pt->in (*is) ? pt : nullptr; + { + if (a.operation () != update_id) + { + if (t.is_a ()) + options = lib::option_install_runtime; + else + { + if (me.cur_options == lib::option_install_runtime) + options = lib::option_install_runtime; + } + } + + return make_pair (is == nullptr || pt->in (*is) ? pt : nullptr, + options); + } if (pt->is_a ()) - return pt; + { + if (a.operation () != update_id && !pt->is_a ()) + { + if (t.is_a ()) + options = lib::option_install_runtime; + else + { + if (me.cur_options == lib::option_install_runtime) + options = lib::option_install_runtime; + } + } + + return make_pair (pt, options); + } + } +#else + if (p.is_a () || p.is_a () || p.is_a ()) + { + const target* pt (&search (t, p)); + + if (const libx* l = pt->is_a ()) + pt = link_member (*l, a, link_info (t.base_scope (), ot)); + + if (a.operation () != update_id) + { + if (t.is_a ()) + options = lib::option_install_runtime; + else + { + if (me.cur_options == lib::option_install_runtime) + options = lib::option_install_runtime; + } + } + + if (pt->is_a () || pt->is_a ()) + { + return make_pair (is == nullptr || pt->in (*is) ? pt : nullptr, + options); + } + else + return make_pair (pt, options); } +#endif - const target* pt (file_rule::instance.filter (is, a, t, p)); + const target* pt (file_rule::instance.filter (is, a, t, p, me).first); if (pt == nullptr) - return pt; + return make_pair (pt, options); auto header_source = [this] (const auto& p) { @@ -351,7 +610,9 @@ namespace build2 (x_obj != nullptr && p.is_a (*x_obj))); }; - if (t.is_a ()) + if (t.is_a () || + (a.operation () != update_id && + me.cur_options == lib::option_install_runtime)) { if (header_source (p)) pt = nullptr; @@ -366,7 +627,7 @@ namespace build2 } if (pt == nullptr) - return pt; + return make_pair (pt, options); } bool g (false); @@ -382,16 +643,17 @@ namespace build2 { pt = t.is_a () ? nullptr - : file_rule::instance.filter (is, a, *pt, pm.prerequisite); + : file_rule::instance.filter ( + is, a, *pt, pm.prerequisite, me).first; break; } } if (pt == nullptr) - return pt; + return make_pair (pt, options); } - return pt; + return make_pair (pt, options); } bool libux_install_rule:: @@ -403,5 +665,49 @@ namespace build2 return link_.sub_match (x_link, update_id, a, t, me) && alias_rule::match (a, t); } + + recipe libux_install_rule:: + apply (action a, target& t, match_extra& me) const + { + if (a.operation () != update_id) + { + if (!t.is_a ()) + { + if (me.new_options == 0) + me.new_options = lib::option_install_runtime; + + me.cur_options = me.new_options; + } + } + + return alias_rule::apply_impl ( + a, t, me, me.cur_options != match_extra::all_options /* reapply */); + } + + void libux_install_rule:: + reapply (action a, target& t, match_extra& me) const + { + tracer trace ("cc::linux_install_rule::reapply"); + + assert (a.operation () != update_id && !t.is_a ()); + + l6 ([&]{trace << "rematching " << t + << ", current options " << me.cur_options + << ", new options " << me.new_options;}); + + me.cur_options |= me.new_options; + + if ((me.new_options & lib::option_install_buildtime) != 0) + { + for (const target* pt: t.prerequisite_targets[a]) + { + if (pt != nullptr && (pt->is_a () || pt->is_a () || + pt->is_a () || pt->is_a ())) + rematch_sync (a, *pt, match_extra::all_options); + } + + alias_rule::reapply_impl (a, t, me); + } + } } } diff --git a/libbuild2/cc/install-rule.hxx b/libbuild2/cc/install-rule.hxx index 6998d63..9d9211b 100644 --- a/libbuild2/cc/install-rule.hxx +++ b/libbuild2/cc/install-rule.hxx @@ -20,7 +20,7 @@ namespace build2 { class link_rule; - // Installation rule for exe{} and lib*{}. Here we do: + // Installation rule for exe{} and lib[as]{}. Here we do: // // 1. Signal to the link rule that this is update for install. // @@ -28,17 +28,23 @@ namespace build2 // // 3. Extra un/installation (e.g., libs{} symlinks). // + // 4. Handling runtime/buildtime match options for lib[as]{}. + // class LIBBUILD2_CC_SYMEXPORT install_rule: public install::file_rule, virtual common { public: install_rule (data&&, const link_rule&); - virtual const target* + virtual bool + filter (action, const target&, const target&) const override; + + virtual pair filter (const scope*, - action, const target&, prerequisite_iterator&) const override; + action, const target&, prerequisite_iterator&, + match_extra&) const override; - // Note: rule::match() override. + // Note: rule::match() override (with hint and match_extra). // virtual bool match (action, target&, const string&, match_extra&) const override; @@ -46,7 +52,10 @@ namespace build2 using file_rule::match; // Make Clang happy. virtual recipe - apply (action, target&) const override; + apply (action, target&, match_extra&) const override; + + virtual void + reapply (action, target&, match_extra&) const override; virtual bool install_extra (const file&, const install_dir&) const override; @@ -58,22 +67,24 @@ namespace build2 const link_rule& link_; }; - // Installation rule for libu*{}. + // Installation rule for libu[eas]{}. // // While libu*{} members themselves are not installable, we need to see // through them in case they depend on stuff that we need to install // (e.g., headers). Note that we use the alias_rule as a base. // - class LIBBUILD2_CC_SYMEXPORT libux_install_rule: - public install::alias_rule, - virtual common + class LIBBUILD2_CC_SYMEXPORT libux_install_rule: public install::alias_rule, + virtual common { public: libux_install_rule (data&&, const link_rule&); - virtual const target* + // Note: utility libraries currently have no ad hoc members. + + virtual pair filter (const scope*, - action, const target&, prerequisite_iterator&) const override; + action, const target&, prerequisite_iterator&, + match_extra&) const override; // Note: rule::match() override. // @@ -82,6 +93,12 @@ namespace build2 using alias_rule::match; // Make Clang happy. + virtual recipe + apply (action, target&, match_extra&) const override; + + virtual void + reapply (action, target&, match_extra&) const override; + private: const link_rule& link_; }; diff --git a/libbuild2/cc/module.cxx b/libbuild2/cc/module.cxx index 40857d7..eed7db1 100644 --- a/libbuild2/cc/module.cxx +++ b/libbuild2/cc/module.cxx @@ -1125,6 +1125,9 @@ namespace build2 // if (install_loaded) { + // Note: we rely quite heavily in these rule implementations that + // these are the only target types they are registered for. + const install_rule& ir (*this); r.insert (perform_install_id, x_install, ir); diff --git a/libbuild2/install/rule.cxx b/libbuild2/install/rule.cxx index 4dd11e8..29888ae 100644 --- a/libbuild2/install/rule.cxx +++ b/libbuild2/install/rule.cxx @@ -71,27 +71,45 @@ namespace build2 return true; } - const target* alias_rule:: + pair alias_rule:: filter (const scope* is, - action a, const target& t, prerequisite_iterator& i) const + action a, const target& t, prerequisite_iterator& i, + match_extra& me) const { assert (i->member == nullptr); - return filter (is, a, t, i->prerequisite); + return filter (is, a, t, i->prerequisite, me); } - const target* alias_rule:: + pair alias_rule:: filter (const scope* is, - action, const target& t, const prerequisite& p) const + action, const target& t, const prerequisite& p, + match_extra&) const { const target& pt (search (t, p)); - return is == nullptr || pt.in (*is) ? &pt : nullptr; + return make_pair (is == nullptr || pt.in (*is) ? &pt : nullptr, + match_extra::all_options); } recipe alias_rule:: - apply (action a, target& t) const + apply (action a, target& t, match_extra& me) const + { + return apply_impl (a, t, me); + } + + recipe alias_rule:: + apply (action, target&) const + { + assert (false); // Never called. + return nullptr; + } + + recipe alias_rule:: + apply_impl (action a, target& t, match_extra& me, bool reapply) const { tracer trace ("install::alias_rule::apply"); + assert (!reapply || a.operation () != update_id); + // Pass-through to our installable prerequisites. // // @@ Shouldn't we do match in parallel (here and below)? @@ -102,6 +120,8 @@ namespace build2 auto pms (group_prerequisite_members (a, t, members_mode::never)); for (auto i (pms.begin ()), e (pms.end ()); i != e; ++i) { + // NOTE: see essentially the same logic in reapply_impl() below. + // const prerequisite& p (i->prerequisite); // Ignore excluded. @@ -125,13 +145,17 @@ namespace build2 if (!is) is = a.operation () != update_id ? install_scope (t) : nullptr; - const target* pt (filter (*is, a, t, i)); + pair fr (filter (*is, a, t, i, me)); + + const target* pt (fr.first); + uint64_t options (fr.second); + + lookup l; + if (pt == nullptr) { l5 ([&]{trace << "ignoring " << p << " (filtered out)";}); - continue; } - // Check if this prerequisite is explicitly "not installable", that // is, there is the 'install' variable and its value is false. // @@ -143,64 +167,108 @@ namespace build2 // // Note: not the same as lookup_install() above. // - auto l ((*pt)[var_install (*p.scope.root_scope ())]); - if (l && cast (l).string () == "false") + else if ((l = (*pt)[var_install (*p.scope.root_scope ())]) && + cast (l).string () == "false") { l5 ([&]{trace << "ignoring " << *pt << " (not installable)";}); - continue; + pt = nullptr; } - // If this is not a file-based target (e.g., a target group such as // libu{}) then ignore it if there is no rule to install. // - if (pt->is_a ()) - match_sync (a, *pt); - else if (!try_match_sync (a, *pt).first) + else if (pt->is_a ()) + { + match_sync (a, *pt, options); + } + else if (!try_match_sync (a, *pt, options).first) { l5 ([&]{trace << "ignoring " << *pt << " (no rule)";}); pt = nullptr; } - if (pt != nullptr) - pts.push_back (prerequisite_target (pt, pi)); + if (pt != nullptr || reapply) + { + // Use auxiliary data for a NULL entry to distinguish between + // filtered out (1) and ignored for other reasons (0). + // + pts.push_back ( + prerequisite_target (pt, pi, fr.first == nullptr ? 1 : 0)); + } } return default_recipe; } - // fsdir_rule - // - const fsdir_rule fsdir_rule::instance; - - bool fsdir_rule:: - match (action, target&) const + void alias_rule:: + reapply_impl (action a, target& t, match_extra& me) const { - // We always match. - // - // Note that we are called both as the outer part during the update-for- - // un/install pre-operation and as the inner part during the un/install - // operation itself. - // - return true; - } + tracer trace ("install::alias_rule::reapply"); - recipe fsdir_rule:: - apply (action a, target& t) const - { - // If this is outer part of the update-for-un/install, delegate to the - // default fsdir rule. Otherwise, this is a noop (we don't install - // fsdir{}). - // - // For now we also assume we don't need to do anything for prerequisites - // (the only sensible prerequisite of fsdir{} is another fsdir{}). + assert (a.operation () != update_id); + + optional is; + + // Iterate over prerequisites and prerequisite targets in parallel. // - if (a.operation () == update_id) + auto& pts (t.prerequisite_targets[a]); + size_t j (0), n (pts.size ()), en (0); + + auto pms (group_prerequisite_members (a, t, members_mode::never)); + for (auto i (pms.begin ()), e (pms.end ()); + i != e && j != n; + ++i, ++j, ++en) { - match_inner (a, t); - return inner_recipe; + // The same logic as in apply() above except that we skip + // prerequisites that were not filtered out. + // + const prerequisite& p (i->prerequisite); + + include_type pi (include (a, t, p)); + if (!pi) + continue; + + if (p.proj) + continue; + + prerequisite_target& pto (pts[j]); + + if (pto.target != nullptr || pto.data == 0) + continue; + + if (!is) + is = a.operation () != update_id ? install_scope (t) : nullptr; + + pair fr (filter (*is, a, t, i, me)); + + const target* pt (fr.first); + uint64_t options (fr.second); + + lookup l; + + if (pt == nullptr) + { + l5 ([&]{trace << "ignoring " << p << " (filtered out)";}); + } + else if ((l = (*pt)[var_install (*p.scope.root_scope ())]) && + cast (l).string () == "false") + { + l5 ([&]{trace << "ignoring " << *pt << " (not installable)";}); + pt = nullptr; + } + else if (pt->is_a ()) + { + match_sync (a, *pt, options); + } + else if (!try_match_sync (a, *pt, options).first) + { + l5 ([&]{trace << "ignoring " << *pt << " (no rule)";}); + pt = nullptr; + } + + pto = prerequisite_target (pt, pi, fr.first == nullptr ? 1 : 0); } - else - return noop_recipe; + + assert (en == n); // Did not call apply() with true for reapply? } // group_rule @@ -214,16 +282,19 @@ namespace build2 alias_rule::match (a, t); } - const target* group_rule:: - filter (action, const target&, const target& m) const + bool group_rule:: + filter (action, const target&, const target&) const { - return &m; + return true; } - const target* group_rule:: + pair group_rule:: filter (const scope* is, - action, const target& t, const prerequisite& p) const + action, const target& t, const prerequisite& p, + match_extra&) const { + pair r (nullptr, match_extra::all_options); + // The same logic as in file_rule::filter() below. // if (p.is_a ()) @@ -232,15 +303,18 @@ namespace build2 if (p.vars.empty () || cast_empty (p.vars[var_install (rs)]).string () != "true") - return nullptr; + return r; } const target& pt (search (t, p)); - return is == nullptr || pt.in (*is) ? &pt : nullptr; + if (is == nullptr || pt.in (*is)) + r.first = &pt; + + return r; } recipe group_rule:: - apply (action a, target& t) const + apply (action a, target& t, match_extra& me) const { tracer trace ("install::group_rule::apply"); @@ -265,17 +339,16 @@ namespace build2 auto& pts (t.prerequisite_targets[a]); for (size_t i (0); i != gv.count; ++i) { - const target* m (gv.members[i]); + const target* mt (gv.members[i]); - if (m == nullptr) + if (mt == nullptr) continue; // Let a customized rule have its say. // - const target* mt (filter (a, t, *m)); - if (mt == nullptr) + if (!filter (a, t, *mt)) { - l5 ([&]{trace << "ignoring " << *m << " (filtered out)";}); + l5 ([&]{trace << "ignoring " << *mt << " (filtered out)";}); continue; } @@ -298,7 +371,7 @@ namespace build2 // Delegate to the base rule. // - return alias_rule::apply (a, t); + return alias_rule::apply (a, t, me); } @@ -315,18 +388,28 @@ namespace build2 return true; } - const target* file_rule:: + bool file_rule:: + filter (action, const target&, const target&) const + { + return true; + } + + pair file_rule:: filter (const scope* is, - action a, const target& t, prerequisite_iterator& i) const + action a, const target& t, prerequisite_iterator& i, + match_extra& me) const { assert (i->member == nullptr); - return filter (is, a, t, i->prerequisite); + return filter (is, a, t, i->prerequisite, me); } - const target* file_rule:: + pair file_rule:: filter (const scope* is, - action, const target& t, const prerequisite& p) const + action, const target& t, const prerequisite& p, + match_extra&) const { + pair r (nullptr, match_extra::all_options); + // See also group_rule::filter() with identical semantics. // if (p.is_a ()) @@ -340,25 +423,37 @@ namespace build2 // if (p.vars.empty () || cast_empty (p.vars[var_install (rs)]).string () != "true") - return nullptr; + return r; } const target& pt (search (t, p)); - return is == nullptr || pt.in (*is) ? &pt : nullptr; + if (is == nullptr || pt.in (*is)) + r.first = &pt; + + return r; } recipe file_rule:: - apply (action a, target& t) const + apply (action a, target& t, match_extra& me) const { - recipe r (apply_impl (a, t)); + recipe r (apply_impl (a, t, me)); return r != nullptr ? move (r) : noop_recipe; } recipe file_rule:: - apply_impl (action a, target& t) const + apply (action, target&) const + { + assert (false); // Never called. + return nullptr; + } + + recipe file_rule:: + apply_impl (action a, target& t, match_extra& me, bool reapply) const { tracer trace ("install::file_rule::apply"); + assert (!reapply || a.operation () != update_id); + // Note that we are called both as the outer part during the update-for- // un/install pre-operation and as the inner part during the un/install // operation itself. @@ -415,6 +510,8 @@ namespace build2 auto pms (group_prerequisite_members (a, t, members_mode::never)); for (auto i (pms.begin ()), e (pms.end ()); i != e; ++i) { + // NOTE: see essentially the same logic in reapply_impl() below. + // const prerequisite& p (i->prerequisite); // Ignore excluded. @@ -438,27 +535,30 @@ namespace build2 if (!is) is = a.operation () != update_id ? install_scope (t) : nullptr; - const target* pt (filter (*is, a, t, i)); + pair fr (filter (*is, a, t, i, me)); + + const target* pt (fr.first); + uint64_t options (fr.second); + + lookup l; if (pt == nullptr) { l5 ([&]{trace << "ignoring " << p << " (filtered out)";}); - continue; } - + // // See if we were explicitly instructed not to touch this target (the // same semantics as in alias_rule). // // Note: not the same as lookup_install() above. // - auto l ((*pt)[var_install (*p.scope.root_scope ())]); - if (l && cast (l).string () == "false") + else if ((l = (*pt)[var_install (*p.scope.root_scope ())]) && + cast (l).string () == "false") { l5 ([&]{trace << "ignoring " << *pt << " (not installable)";}); - continue; + pt = nullptr; } - - if (pt->is_a ()) + else if (pt->is_a ()) { // If the matched rule returned noop_recipe, then the target state // is set to unchanged as an optimization. Use this knowledge to @@ -466,17 +566,28 @@ namespace build2 // when updating static installable content (headers, documentation, // etc). // - if (match_sync (a, *pt, unmatch::unchanged).first) + // Regarding options, the expectation here is that they are not used + // for the update operation. And for install/uninstall, if they are + // used, then they don't effect whether the target is unchanged. All + // feels reasonable. + // + if (match_sync (a, *pt, unmatch::unchanged, options).first) pt = nullptr; } - else if (!try_match_sync (a, *pt).first) + else if (!try_match_sync (a, *pt, options).first) { l5 ([&]{trace << "ignoring " << *pt << " (no rule)";}); pt = nullptr; } - if (pt != nullptr) - pts.push_back (prerequisite_target (pt, pi)); + if (pt != nullptr || reapply) + { + // Use auxiliary data for a NULL entry to distinguish between + // filtered out (1) and ignored for other reasons (0). + // + pts.push_back ( + prerequisite_target (pt, pi, fr.first == nullptr ? 1 : 0)); + } } #if 1 @@ -502,6 +613,79 @@ namespace build2 } } + void file_rule:: + reapply_impl (action a, target& t, match_extra& me) const + { + tracer trace ("install::file_rule::reapply"); + + assert (a.operation () != update_id); + + optional is; + + // Iterate over prerequisites and prerequisite targets in parallel. + // + auto& pts (t.prerequisite_targets[a]); + size_t j (0), n (pts.size ()), en (0); + + auto pms (group_prerequisite_members (a, t, members_mode::never)); + for (auto i (pms.begin ()), e (pms.end ()); + i != e && j != n; + ++i, ++j, ++en) + { + // The same logic as in apply() above except that we skip + // prerequisites that were not filtered out. + // + const prerequisite& p (i->prerequisite); + + include_type pi (include (a, t, p)); + if (!pi) + continue; + + if (p.proj) + continue; + + prerequisite_target& pto (pts[j]); + + if (pto.target != nullptr || pto.data == 0) + continue; + + if (!is) + is = a.operation () != update_id ? install_scope (t) : nullptr; + + pair fr (filter (*is, a, t, i, me)); + + const target* pt (fr.first); + uint64_t options (fr.second); + + lookup l; + + if (pt == nullptr) + { + l5 ([&]{trace << "ignoring " << p << " (filtered out)";}); + } + else if ((l = (*pt)[var_install (*p.scope.root_scope ())]) && + cast (l).string () == "false") + { + l5 ([&]{trace << "ignoring " << *pt << " (not installable)";}); + pt = nullptr; + } + else if (pt->is_a ()) + { + if (match_sync (a, *pt, unmatch::unchanged, options).first) + pt = nullptr; + } + else if (!try_match_sync (a, *pt, options).first) + { + l5 ([&]{trace << "ignoring " << *pt << " (no rule)";}); + pt = nullptr; + } + + pto = prerequisite_target (pt, pi, fr.first == nullptr ? 1 : 0); + } + + assert (en == n); // Did not call apply() with true for reapply? + } + target_state file_rule:: perform_update (action a, const target& t) { @@ -1160,6 +1344,8 @@ namespace build2 // target_state r (straight_execute_prerequisites (a, t)); + bool fr (filter (a, t, t)); + // Then installable ad hoc group members, if any. // for (const target* m (t.adhoc_member); @@ -1170,10 +1356,13 @@ namespace build2 { if (!mf->path ().empty () && mf->mtime () != timestamp_nonexistent) { - if (const path* p = lookup_install (*mf, "install")) + if (filter (a, t, *mf)) { - install_target (*mf, *p, tp.empty () ? 1 : 2); - r |= target_state::changed; + if (const path* p = lookup_install (*mf, "install")) + { + install_target (*mf, *p, !fr || tp.empty () ? 1 : 2); + r |= target_state::changed; + } } } } @@ -1182,7 +1371,7 @@ namespace build2 // Finally install the target itself (since we got here we know the // install variable is there). // - if (!tp.empty ()) + if (fr && !tp.empty ()) { install_target (t, cast (t[var_install (rs)]), 1); r |= target_state::changed; @@ -1534,7 +1723,9 @@ namespace build2 // target_state r (target_state::unchanged); - if (!tp.empty ()) + bool fr (filter (a, t, t)); + + if (fr && !tp.empty ()) r |= uninstall_target (t, cast (t[var_install (rs)]), 1); // Then installable ad hoc group members, if any. To be anally precise, @@ -1549,12 +1740,15 @@ namespace build2 { if (!mf->path ().empty () && mf->mtime () != timestamp_nonexistent) { - if (const path* p = lookup_install (*m, "install")) + if (filter (a, t, *mf)) { - r |= uninstall_target ( - *mf, - *p, - tp.empty () || r != target_state::changed ? 1 : 2); + if (const path* p = lookup_install (*m, "install")) + { + r |= uninstall_target ( + *mf, + *p, + !fr || tp.empty () || r != target_state::changed ? 1 : 2); + } } } } @@ -1566,5 +1760,40 @@ namespace build2 return r; } + + // fsdir_rule + // + const fsdir_rule fsdir_rule::instance; + + bool fsdir_rule:: + match (action, target&) const + { + // We always match. + // + // Note that we are called both as the outer part during the update-for- + // un/install pre-operation and as the inner part during the un/install + // operation itself. + // + return true; + } + + recipe fsdir_rule:: + apply (action a, target& t) const + { + // If this is outer part of the update-for-un/install, delegate to the + // default fsdir rule. Otherwise, this is a noop (we don't install + // fsdir{}). + // + // For now we also assume we don't need to do anything for prerequisites + // (the only sensible prerequisite of fsdir{} is another fsdir{}). + // + if (a.operation () == update_id) + { + match_inner (a, t); + return inner_recipe; + } + else + return noop_recipe; + } } } diff --git a/libbuild2/install/rule.hxx b/libbuild2/install/rule.hxx index b319071..b023af5 100644 --- a/libbuild2/install/rule.hxx +++ b/libbuild2/install/rule.hxx @@ -25,42 +25,60 @@ namespace build2 match (action, target&) const override; // Return NULL if this prerequisite should be ignored and pointer to its - // target otherwise. + // target otherwise. In the latter case, return the match options that + // should be used for this prerequisite (use match_extra::all_options + // and not 0 if no match options are needed). // // The default implementation ignores prerequsites that are outside of // the installation scope (see install_scope() for details). // + // The default implementation always returns match_extra::all_options. + // The match_extra argument is not used by the default implementation. + // // The prerequisite is passed as an iterator allowing the filter to // "see" inside groups. // using prerequisite_iterator = prerequisite_members_range::iterator; - virtual const target* + virtual pair filter (const scope*, - action, const target&, prerequisite_iterator&) const; + action, const target&, prerequisite_iterator&, + match_extra&) const; - virtual const target* - filter (const scope*, action, const target&, const prerequisite&) const; + virtual pair + filter (const scope*, + action, const target&, const prerequisite&, + match_extra&) const; + // Note: rule::apply() override (with match_extra). + // virtual recipe - apply (action, target&) const override; + apply (action, target&, match_extra&) const override; + + // Implementation of apply(). + // + // If the implementation may call reapply_impl(), then the reapply + // argument to apply_impl() must be true. Note that in this case, the + // *_impl() functions use the prerequisite_target::data member for own + // housekeeping. + // + recipe + apply_impl (action, target&, match_extra&, bool reapply = false) const; + + // Implementation of reapply() that re-tries prerequisites that have + // been filtered out during the reapply() call. Note that currently not + // supported for update, only for install/uninstall. + // + void + reapply_impl (action, target&, match_extra&) const; alias_rule () {} static const alias_rule instance; - }; - - class fsdir_rule: public simple_rule - { - public: - virtual bool - match (action, target&) const override; + private: virtual recipe - apply (action, target&) const override; - - fsdir_rule () {} - static const fsdir_rule instance; + apply (action, target&) const override; // Dummy simple_rule override. }; // In addition to the alias rule's semantics, this rule sees through to @@ -80,28 +98,26 @@ namespace build2 virtual bool match (action, target&) const override; - // Return NULL if this group member should be ignored and pointer to its - // target otherwise. + // Return false if this group member should be ignored and true + // otherwise. Note that this filter is called during apply(). // // The default implementation accepts all members. // - virtual const target* + virtual bool filter (action, const target&, const target& group_member) const; // Return NULL if this prerequisite should be ignored and pointer to its - // target otherwise. - // - // The same semantics as in file_rule below. + // target otherwise. The same semantics as in file_rule below. // - using alias_rule::filter; // "Unhide" to make Clang happy. - - virtual const target* + virtual pair filter (const scope*, - action, const target&, - const prerequisite&) const override; + action, const target&, const prerequisite&, + match_extra&) const override; + + using alias_rule::filter; // "Unhide" to make Clang happy. virtual recipe - apply (action, target&) const override; + apply (action, target&, match_extra&) const override; group_rule (bool sto): see_through_only (sto) {} static const group_rule instance; @@ -117,8 +133,21 @@ namespace build2 virtual bool match (action, target&) const override; + // Return false if this ad hoc group member should be ignored and true + // otherwise. Note that this filter is called during execute and only + // for install/uninstall (and not update). For generality, it is also + // (first) called on the target itself (can be detected by comparing + // the second and third arguments). + // + // The default implementation accepts all members. + // + virtual bool + filter (action, const target&, const target& adhoc_group_member) const; + // Return NULL if this prerequisite should be ignored and pointer to its - // target otherwise. + // target otherwise. In the latter case, return the match options that + // should be used for this prerequisite (use match_extra::all_options + // and not 0 if no match options are needed). // // The default implementation ignores prerequsites that are outside of // the installation scope (see install_scope() for details). It also @@ -130,27 +159,47 @@ namespace build2 // // exe{foo}: exe{bar}: install = true # foo runs bar // + // The default implementation always returns match_extra::all_options. + // The match_extra argument is not used by the default implementation. + // // The prerequisite is passed as an iterator allowing the filter to // "see" inside groups. // using prerequisite_iterator = prerequisite_members_range::iterator; - virtual const target* + virtual pair filter (const scope*, - action, const target&, prerequisite_iterator&) const; + action, const target&, prerequisite_iterator&, + match_extra&) const; - virtual const target* - filter (const scope*, action, const target&, const prerequisite&) const; + virtual pair + filter (const scope*, + action, const target&, const prerequisite&, + match_extra&) const; + // Note: rule::apply() override (with match_extra). + // virtual recipe - apply (action, target&) const override; + apply (action, target&, match_extra&) const override; - // Implementation of apply() that returns empty_recipe if the target is - // not installable. + // Implementation of apply() that returns empty_recipe (i.e., NULL) if + // the target is not installable. + // + // If the implementation may call reapply_impl(), then the reapply + // argument to apply_impl() must be true. Note that in this case, the + // *_impl() functions use the prerequisite_target::data member for own + // housekeeping. // recipe - apply_impl (action, target&) const; + apply_impl (action, target&, match_extra&, bool reapply = false) const; + + // Implementation of reapply() that re-tries prerequisites that have + // been filtered out during the reapply() call. Note that currently not + // supported for update, only for install/uninstall. + // + void + reapply_impl (action, target&, match_extra&) const; static target_state perform_update (action, const target&); @@ -288,6 +337,23 @@ namespace build2 static const file_rule instance; file_rule () {} + + private: + virtual recipe + apply (action, target&) const override; // Dummy simple_rule override. + }; + + class fsdir_rule: public simple_rule + { + public: + virtual bool + match (action, target&) const override; + + virtual recipe + apply (action, target&) const override; + + fsdir_rule () {} + static const fsdir_rule instance; }; } } -- cgit v1.1