From 5e51d523e71231cb190e9ed981962df527f4ee7e Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Sun, 4 Apr 2021 13:11:56 +0200 Subject: Add base functionality for hermetic build configurations --- libbuild2/config/functions.cxx | 7 ++ libbuild2/config/init.cxx | 206 ++++++++++++++++++++++++++--------------- libbuild2/config/module.hxx | 70 +++++++++++++- libbuild2/config/operation.cxx | 140 ++++++++++++++++++++++------ libbuild2/config/operation.hxx | 7 ++ libbuild2/variable.cxx | 8 ++ libbuild2/variable.hxx | 19 ++-- 7 files changed, 345 insertions(+), 112 deletions(-) diff --git a/libbuild2/config/functions.cxx b/libbuild2/config/functions.cxx index 5e85238..398512c 100644 --- a/libbuild2/config/functions.cxx +++ b/libbuild2/config/functions.cxx @@ -7,6 +7,7 @@ #include #include +#include #include using namespace std; @@ -39,6 +40,11 @@ namespace build2 if (s == nullptr) fail << "config.save() called out of project" << endf; + module* mod (s->find_module (module::name)); + + if (mod == nullptr) + fail << "config.save() called without config module"; + ostringstream os; // Empty project set should is ok as long as inherit is false. @@ -47,6 +53,7 @@ namespace build2 save_config (*s, os, path_name ("config.save()"), false /* inherit */, + *mod, ps); return os.str (); diff --git a/libbuild2/config/init.cxx b/libbuild2/config/init.cxx index 1e6b36b..950579a 100644 --- a/libbuild2/config/init.cxx +++ b/libbuild2/config/init.cxx @@ -5,7 +5,6 @@ #include #include // getenv() -#include // strncmp() #include #include @@ -30,24 +29,6 @@ namespace build2 void functions (function_map&); // functions.cxx - // Compare environment variable names. Note that on Windows they are - // case-insensitive. - // - static inline bool - compare_env (const string& x, size_t xn, const string& y, size_t yn) - { - if (xn == string::npos) xn = x.size (); - if (yn == string::npos) yn = y.size (); - - return xn == yn && -#ifdef _WIN32 - icasecmp (x.c_str (), y.c_str (), xn) == 0 -#else - strncmp (x.c_str (), y.c_str (), xn) == 0 -#endif - ; - } - // Custom save function for config.config.environment. // // It tries to optimize the storage in subprojects by appending the @@ -82,7 +63,8 @@ namespace build2 if (find_if (ds.rbegin (), i, [&v, p] (const string& v1) { - return compare_env (v, p, v1, v1.find ('=')); + return saved_environment::compare ( + v, v1, p, v1.find ('=')); }) != i) continue; @@ -91,7 +73,8 @@ namespace build2 auto j (find_if (bs.rbegin (), bs.rend (), [&v, p] (const string& v1) { - return compare_env (v, p, v1, v1.find ('=')); + return saved_environment::compare ( + v, v1, p, v1.find ('=')); })); if (j == bs.rend () || *j != v) @@ -130,7 +113,7 @@ namespace build2 // as a command line override. // // Note: must be entered during bootstrap since we need it in - // configure_execute(). + // configure_execute() even for the forward case. // vp.insert ("config.config.save", true /* ovr */); @@ -166,39 +149,11 @@ namespace build2 // // Use the NULL value to clear. // - auto& c_p (vp.insert>> ( - "config.config.persist", true /* ovr */, v_p)); - - // Configuration environment variables. - // - // Environment variables used by tools (e.g., compilers), buildfiles - // (e.g., $getenv()), and the build system itself (e.g., to locate - // tools) in ways that affect the build result are in essence part of - // the project configuration. - // - // This variable allows storing environment variable overrides that - // should be applied to the environment when executing tools, etc., as - // part of a project build. Specifically, it contains a list of - // environment variable "sets" (=) and "unsets" (). - // If multiple entries are specified for the same environment variable, - // the last entry has effect. For example: - // - // config.config.environment="LC_ALL=C LANG" - // - // Note that a subproject inherits overrides from its amalgamation (this - // semantics is the result of the way we optimize the storage of this - // variable in subproject's config.build; the thinking is that if a - // variable is not overridden by the subproject then it doesn't affect - // the build result and therefore it's irrelevant whether it has a value - // that came from the original environment of from the amalgamation - // override). - // - // Use the NULL or empty value to clear. - // - // @@ We could use = as a "pass-through" instruction (e.g., if - // we need to use original value in subproject). + // Note: must be entered during bootstrap since we need it in create's + // save_config(). // - auto& c_e (vp.insert ("config.config.environment", true /* ovr */)); + vp.insert>> ( + "config.config.persist", true /* ovr */, v_p); // Only create the module if we are configuring, creating, or // disfiguring or if it was requested with config.config.module (useful @@ -229,11 +184,6 @@ namespace build2 // m.save_module ("config", INT32_MIN); m.save_module ("import", INT32_MIN); - - m.save_variable (c_p, save_null_omitted); - m.save_variable (c_e, - save_null_omitted | save_empty_omitted | save_base, - &save_environment); } } @@ -277,16 +227,106 @@ namespace build2 return true; } + context& ctx (rs.ctx); + l5 ([&]{trace << "for " << rs;}); + // If we have the module, then we are configuring (or the project wishes + // to call $config.save(); we don't get here on disfigure). + // + module* m (extra.module != nullptr + ? &extra.module_as () + : nullptr); + auto& vp (rs.var_pool ()); // Note: config.* is pattern-typed to global visibility. // const auto v_p (variable_visibility::project); - auto& c_l (vp.insert ("config.config.load", true /* ovr */)); auto& c_v (vp.insert ("config.version", false /*ovr*/, v_p)); + auto& c_l (vp.insert ("config.config.load", true /* ovr */)); + + // Hermetic configurations. + // + // A hermetic configuration stores environment variables that affect the + // project in config.config.environment. + // + // Note that this is essentially a tri-state value: true means keep + // hermetizing (save the environment in config.config.environment), + // false means keep de-hermetizing (clear config.config.environment) and + // undefined/NULL means don't touch config.config.environment. + // + // During reconfiguration things stay hermetic unless re-hermetization + // is explicitly requested with config.config.hermetic.reload=true (or + // de-hermetization is requested with config.config.hermetic=false). + // + // Use the NULL value to clear. + // + auto& c_h (vp.insert ("config.config.hermetic", true /* ovr */)); + + if (m != nullptr) + m->save_variable (c_h, save_null_omitted); + + // Request hermetic configuration re-hermetization. + // + // Note that this variable is not saved in config.build and is expected + // to always be specified as a command line override. + // + auto& c_h_r ( + vp.insert ("config.config.hermetic.reload", true /* ovr */)); + + // Hermetic configuration environment variables inclusion/exclusion. + // + // This configuration variable can be used to include additional or + // exclude existing environment variables into/from the list that should + // be saved in order to make the configuration hermetic. For example: + // + // config.config.hermetic.environment="LANG PATH@false" + // + // Use the NULL or empty value to clear. + // + auto& c_h_e ( + vp.insert ("config.config.hermetic.environment")); + + if (m != nullptr) + m->save_variable (c_h_e, save_null_omitted | save_empty_omitted); + + // Configuration environment variables. + // + // Environment variables used by tools (e.g., compilers), buildfiles + // (e.g., $getenv()), and the build system itself (e.g., to locate + // tools) in ways that affect the build result are in essence part of + // the project configuration. + // + // This variable allows storing environment variable overrides that + // should be applied to the environment when executing tools, etc., as + // part of a project build. Specifically, it contains a list of + // environment variable "sets" (=) and "unsets" (). + // If multiple entries are specified for the same environment variable, + // the last entry has effect. For example: + // + // config.config.environment="LC_ALL=C LANG" + // + // Note that a subproject inherits overrides from its amalgamation (this + // semantics is the result of the way we optimize the storage of this + // variable in subproject's config.build; the thinking is that if a + // variable is not overridden by the subproject then it doesn't affect + // the build result and therefore it's irrelevant whether it has a value + // that came from the original environment of from the amalgamation + // override). + // + // Use the NULL or empty value to clear. + // + // @@ We could use = as a "pass-through" instruction (e.g., if + // we need to use original value in subproject). + // + auto& c_e (vp.insert ("config.config.environment", true /* ovr */)); + + if (m != nullptr) + m->save_variable (c_e, + save_null_omitted | save_empty_omitted | save_base, + &save_environment); // Load config.build if one exists followed by extra files specified in // config.config.load (we don't need to worry about disfigure since we @@ -358,7 +398,7 @@ namespace build2 // b ./config.config.load=.../config.build # this project // b !config.config.load=.../config.build # every project // - if (l.belongs (rs) || l.belongs (rs.ctx.global_scope)) + if (l.belongs (rs) || l.belongs (ctx.global_scope)) { for (const path& f: cast (l)) { @@ -384,31 +424,50 @@ namespace build2 } } - // Cache the config.config.persist value, if any. + // Save and cache the config.config.persist value, if any. // - if (extra.module != nullptr) + if (m != nullptr) { - auto& m (extra.module_as ()); + auto& c_p (*vp.find ("config.config.persist")); + m->save_variable (c_p, save_null_omitted); + m->persist = cast_null>> (rs[c_p]); + } - m.persist = - cast_null>> ( - rs["config.config.persist"]); + // If we are configuring, handle config.config.hermetic. + // + // The overall plan is to either clear config.config.environment (if + // c.c.h=false) or populate it with the values that affect this project + // (if c.c.h=true). We have to do it half here (because c.c.e is used as + // a source for the project environment and we would naturally want the + // semantics to be equivalent to what will be saved in config.build) and + // half in configure_execute() (because that's where we have the final + // list of all the environment variables we need to save). + // + // So here we must deal with the cases where the current c.c.e value + // will be changed: either cleared (c.c.h=false) or set to new values + // from the "outer" environment (c.c.h.reload=true). Note also that even + // then a c.c.e value from an amalgamation, if any, should be in effect. + // + if (ctx.current_mif->id == configure_id && + (!cast_true (rs[c_h]) || // c.c.h=false + cast_false (rs[c_h_r]))) // c.c.h.r=true + { + rs.vars.erase (c_e); // Undefine. } // Copy config.config.environment to scope::root_extra::environment. // // Note that we store shallow copies that point to the c.c.environment - // value which means it shall not change. + // value which means it should not change. // - if (const strings* src = cast_null ( - rs["config.config.environment"])) + if (const strings* src = cast_null (rs[c_e])) { vector& dst (rs.root_extra->environment); // The idea is to only copy entries that are effective, that is those // that actually override something in the environment. This should be - // both more efficient and less noisy (e.g., if we decide to print - // this in diagnostics). + // both more efficient and less noisy (e.g., if we need to print this + // in diagnostics). // // Note that config.config.environment may contain duplicates and the // last entry should have effect. @@ -429,7 +488,8 @@ namespace build2 if (find_if (src->rbegin (), i, [&v, p] (const string& v1) { - return compare_env (v, p, v1, v1.find ('=')); + return saved_environment::compare ( + v, v1, p, v1.find ('=')); }) != i) continue; diff --git a/libbuild2/config/module.hxx b/libbuild2/config/module.hxx index b8b5d07..92fed32 100644 --- a/libbuild2/config/module.hxx +++ b/libbuild2/config/module.hxx @@ -4,6 +4,8 @@ #ifndef LIBBUILD2_CONFIG_MODULE_HXX #define LIBBUILD2_CONFIG_MODULE_HXX +#include // strncmp() + #include #include @@ -50,7 +52,7 @@ namespace build2 const_iterator find (const variable& var) const { - return std::find_if ( + return find_if ( begin (), end (), [&var] (const saved_variable& v) {return var == v.var;}); @@ -76,8 +78,69 @@ namespace build2 } }; - struct module: build2::module + // List of environment variable names that effect this project. + // + // Note that on Windows environment variable names are case-insensitive. + // + struct saved_environment: vector { + // Compare environment variable names. + // + static inline bool + compare (const string& x, + const string& y, + size_t xn = string::npos, + size_t yn = string::npos) + { + if (xn == string::npos) xn = x.size (); + if (yn == string::npos) yn = y.size (); + + return xn == yn && +#ifdef _WIN32 + icasecmp (x.c_str (), y.c_str (), xn) == 0 +#else + strncmp (x.c_str (), y.c_str (), xn) == 0 +#endif + ; + } + + iterator + find (const string& v) + { + return find_if ( + begin (), + end (), + [&v] (const string& v1) {return compare (v, v1);}); + } + + const_iterator + find (const string& v) const + { + return find_if ( + begin (), + end (), + [&v] (const string& v1) {return compare (v, v1);}); + } + + void + insert (string v) + { + if (find (v) == end ()) + push_back (move (v)); + } + + void + erase (const string& v) + { + auto i (find (v)); + if (i != end ()) + vector::erase (i); + } + }; + + class module: public build2::module + { + public: config::saved_modules saved_modules; // Return true if variable/module were newly inserted. @@ -110,6 +173,9 @@ namespace build2 return nullptr; } + config::saved_environment saved_environment; + strings old_environment; + // Configure/disfigure hooks. // static bool diff --git a/libbuild2/config/operation.cxx b/libbuild2/config/operation.cxx index c5e8004..760f998 100644 --- a/libbuild2/config/operation.cxx +++ b/libbuild2/config/operation.cxx @@ -157,24 +157,19 @@ namespace build2 // If inherit is false, then don't rely on inheritance from outer scopes. // + // @@ We are modifying the module (marking additional variables as saved) + // and this function can be called from a buildfile (probably only + // during serial execution but still). + // void save_config (const scope& rs, ostream& os, const path_name& on, bool inherit, + module& mod, const project_set& projects) { context& ctx (rs.ctx); - // @@ We are modifying the module (marking additional variables as - // saved) and this function can be called from a buildfile (probably - // only during serial execution but still). - // - module* mod (rs.find_module (module::name)); - - if (mod == nullptr) - fail (on) << "no configuration information available during this " - << "meta-operation"; - names storage; auto info_value = [&storage] (diag_record& dr, const value& v) mutable @@ -234,18 +229,18 @@ namespace build2 var->name.compare (0, 14, "config.config.") == 0) continue; - if (mod->find_variable (*var)) // Saved or unsaved. + if (mod.find_variable (*var)) // Saved or unsaved. continue; const value& v (p.first->second); pair r (save_config_variable (*var, - mod->persist, + mod.persist, false /* inherited */, true /* unused */)); if (r.first) // save { - mod->save_variable (*var, 0); + mod.save_variable (*var, 0); if (r.second) // warn { @@ -281,7 +276,7 @@ namespace build2 // Save config variables. // - for (auto p: mod->saved_modules.order) + for (auto p: mod.saved_modules.order) { const string& sname (p.second->first); const saved_variables& svars (p.second->second); @@ -542,6 +537,7 @@ namespace build2 save_config (const scope& rs, const path& f, bool inherit, + module& mod, const project_set& projects) { path_name fn (f); @@ -555,7 +551,8 @@ namespace build2 try { ofdstream ofs; - save_config (rs, open_file_or_stdout (fn, ofs), fn, inherit, projects); + save_config ( + rs, open_file_or_stdout (fn, ofs), fn, inherit, mod, projects); ofs.close (); } catch (const io_error& e) @@ -564,10 +561,81 @@ namespace build2 } } + // Update config.config.environment value for a hermetic configuration. + // + static void + save_environment (scope& rs, module& mod) + { + // Here we have two parts: (1) get the list of environment variables we + // need to save and (2) save their values in config.config.environment. + // + // The saved_environment list should by now contain all the project- + // specific environment variables. To that we add builtin defaults and + // then filter the result against config.config.hermetic.environment + // inclusions/exclusions. + // + auto& vars (mod.saved_environment); + + vars.insert ("PATH"); + +#if defined(_WIN32) +#elif defined(__APPLE__) + vars.insert ("DYLD_LIBRARY_PATH"); +#else // Linux, FreeBSD, NetBSD, OpenBSD + vars.insert ("LD_LIBRARY_PATH"); +#endif + + for (const pair>& p: + cast_empty ( + rs["config.config.hermetic.environment"])) + { + if (!p.second || *p.second) + vars.insert (p.first); + else + vars.erase (p.first); + } + + // Get the values. + // + strings vals; + { + // Set the project environment before querying the values. Note that + // the logic in init() makes sure that this is all we need to do to + // handle reload (in which case we should still be using + // config.config.environment from amalgamation, if any). + // + auto_project_env penv (rs); + + for (const string& var: vars) + { + if (optional val = getenv (var)) + { + vals.push_back (var + '=' + *val); + } + else + vals.push_back (var); // Unset. + } + } + + value& v (rs.assign (*rs.ctx.var_pool.find ("config.config.environment"))); + + // Note that setting new config.config.environment value invalidates the + // project's environment (scope::root_extra::environment) which could be + // queried in the post-configuration hook. We could re-initialize it but + // the c.c.e value from amalgamation could be referenced by subprojects. + // So instead it seems easier to just save the old value in the module. + // + if (v) + mod.old_environment = move (v.as ()); + + v = move (vals); + } + static void configure_project (action a, - const scope& rs, + scope& rs, const variable* c_s, // config.config.save + module& mod, project_set& projects) { tracer trace ("configure_project"); @@ -597,6 +665,12 @@ namespace build2 { l5 ([&]{trace << "completely configuring " << out_root;}); + // Save the environment if this configuration is hermetic (see init() + // for the other half of this logic). + // + if (cast_false (rs["config.config.hermetic"])) + save_environment (rs, mod); + // Save src-root.build unless out_root is the same as src. // if (c_s == nullptr && out_root != src_root) @@ -621,7 +695,7 @@ namespace build2 // may end up leaving it in partially configured state. // if (c_s == nullptr) - save_config (rs, config_file (rs), true /* inherit */, projects); + save_config (rs, config_file (rs), true /* inherit */, mod, projects); else { lookup l (rs[*c_s]); @@ -634,7 +708,8 @@ namespace build2 // still want to support this mode somehow in the future (it seems // like an override of config.config.persist should do the trick). // - save_config (rs, cast (l), false /* inherit */, projects); + save_config ( + rs, cast (l), false /* inherit */, mod, projects); } } } @@ -645,11 +720,8 @@ namespace build2 if (c_s == nullptr) { - if (module* m = rs.find_module (module::name)) - { - for (auto hook: m->configure_post_) - hook (a, rs); - } + for (auto hook: mod.configure_post_) + hook (a, rs); } // Configure subprojects that have been loaded. @@ -660,15 +732,19 @@ namespace build2 { const dir_path& pd (p.second); dir_path out_nroot (out_root / pd); - const scope& nrs (ctx.scopes.find_out (out_nroot)); + scope& nrs (ctx.scopes.find_out (out_nroot).rw ()); - // @@ Strictly speaking we need to check whether the config module - // was loaded for this subproject. + // Skip this subproject if it is not loaded or doesn't use the + // config module. // - if (nrs.out_path () != out_nroot) // This subproject not loaded. - continue; + if (nrs.out_path () == out_nroot) + { + if (module* m = rs.find_module (module::name)) + { + configure_project (a, nrs, c_s, *m, projects); + } + } - configure_project (a, nrs, c_s, projects); } } } @@ -874,7 +950,11 @@ namespace build2 } } - configure_project (a, *rs, c_s, projects); + configure_project (a, + rs->rw (), + c_s, + *rs->find_module (module::name), + projects); } } } diff --git a/libbuild2/config/operation.hxx b/libbuild2/config/operation.hxx index a887eb7..ded3c8b 100644 --- a/libbuild2/config/operation.hxx +++ b/libbuild2/config/operation.hxx @@ -15,6 +15,8 @@ namespace build2 { namespace config { + class module; + extern const meta_operation_info mo_configure; extern const meta_operation_info mo_disfigure; @@ -37,7 +39,12 @@ namespace build2 save_config (const scope& rs, ostream&, const path_name&, bool inherit, + module&, const project_set&); + + // See config.config.hermetic.environment. + // + using hermetic_environment = vector>>; } } diff --git a/libbuild2/variable.cxx b/libbuild2/variable.cxx index 917b9e7..efda63d 100644 --- a/libbuild2/variable.cxx +++ b/libbuild2/variable.cxx @@ -1759,6 +1759,14 @@ namespace build2 return pair (r, p.second); } + bool variable_map:: + erase (const variable& var) + { + assert (!global_ || ctx->phase == run_phase::load); + + return m_.erase (var) != 0; + } + // variable_type_map // lookup variable_type_map:: diff --git a/libbuild2/variable.hxx b/libbuild2/variable.hxx index 898648c..d59a891 100644 --- a/libbuild2/variable.hxx +++ b/libbuild2/variable.hxx @@ -1562,6 +1562,14 @@ namespace build2 pair lookup_to_modify (const variable&, bool typed = true); + pair + lookup_namespace (const variable& ns) const + { + auto r (m_.find_sub (ns)); + return make_pair (const_iterator (r.first, *this), + const_iterator (r.second, *this)); + } + // Convert a lookup pointing to a value belonging to this variable map // to its non-const version. Note that this is only safe on the original // values (see find_original()). @@ -1599,13 +1607,10 @@ namespace build2 pair insert (const variable&, bool typed = true); - pair - lookup_namespace (const variable& ns) const - { - auto r (m_.find_sub (ns)); - return make_pair (const_iterator (r.first, *this), - const_iterator (r.second, *this)); - } + // Note: does not deal with aliases. + // + bool + erase (const variable&); const_iterator begin () const {return const_iterator (m_.begin (), *this);} -- cgit v1.1