aboutsummaryrefslogtreecommitdiff
path: root/mod/build-config-module.cxx
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2018-12-07 23:12:05 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2018-12-11 14:05:48 +0300
commit294c558d577cd4acb2ee8e94e0dfd6acdb946c6c (patch)
tree02742141dad6cf91040eb48eca54b718dee8ad55 /mod/build-config-module.cxx
parent7dabb6e931740b2777be5dca53c3cec0b984f0fb (diff)
Add support for build configuration class inheritance
Diffstat (limited to 'mod/build-config-module.cxx')
-rw-r--r--mod/build-config-module.cxx384
1 files changed, 384 insertions, 0 deletions
diff --git a/mod/build-config-module.cxx b/mod/build-config-module.cxx
new file mode 100644
index 0000000..57c035f
--- /dev/null
+++ b/mod/build-config-module.cxx
@@ -0,0 +1,384 @@
+// file : mod/build-config-module.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/build-config-module.hxx>
+
+#include <errno.h> // EIO
+
+#include <map>
+#include <sstream>
+
+#include <libbutl/sha256.mxx>
+#include <libbutl/utility.mxx> // throw_generic_error(), alpha(), etc.
+#include <libbutl/openssl.mxx>
+#include <libbutl/filesystem.mxx>
+
+namespace brep
+{
+ using namespace std;
+ using namespace butl;
+ using namespace bpkg;
+ using namespace bbot;
+
+ // Return pointer to the shared build configurations instance, creating one
+ // on the first call. Throw tab_parsing on parsing error, io_error on the
+ // underlying OS error. Note: not thread-safe.
+ //
+ static shared_ptr<const build_configs>
+ shared_build_config (const path& p)
+ {
+ static map<path, weak_ptr<build_configs>> configs;
+
+ auto i (configs.find (p));
+ if (i != configs.end ())
+ {
+ if (shared_ptr<build_configs> c = i->second.lock ())
+ return c;
+ }
+
+ shared_ptr<build_configs> c (
+ make_shared<build_configs> (parse_buildtab (p)));
+
+ configs[p] = c;
+ return c;
+ }
+
+ // Return pointer to the shared build bot agent public keys map, creating
+ // one on the first call. Throw system_error on the underlying openssl or OS
+ // error. Note: not thread-safe.
+ //
+ using bot_agent_key_map = map<string, path>;
+
+ static shared_ptr<const bot_agent_key_map>
+ shared_bot_agent_keys (const options::openssl_options& o, const dir_path& d)
+ {
+ static map<dir_path, weak_ptr<bot_agent_key_map>> keys;
+
+ auto i (keys.find (d));
+ if (i != keys.end ())
+ {
+ if (shared_ptr<bot_agent_key_map> k = i->second.lock ())
+ return k;
+ }
+
+ shared_ptr<bot_agent_key_map> ak (make_shared<bot_agent_key_map> ());
+
+ // Intercept exception handling to make error descriptions more
+ // informative.
+ //
+ // Path of the key being converted. Used for diagnostics.
+ //
+ path p;
+
+ try
+ {
+ for (const dir_entry& de: dir_iterator (d, false /* ignore_dangling */))
+ {
+ if (de.path ().extension () == "pem" &&
+ de.type () == entry_type::regular)
+ {
+ p = d / de.path ();
+
+ openssl os (p, path ("-"), 2,
+ process_env (o.openssl (), o.openssl_envvar ()),
+ "pkey",
+ o.openssl_option (), "-pubin", "-outform", "DER");
+
+ string fp (sha256 (os.in).string ());
+ os.in.close ();
+
+ if (!os.wait ())
+ throw io_error ("");
+
+ ak->emplace (move (fp), move (p));
+ }
+ }
+ }
+ catch (const io_error&)
+ {
+ ostringstream os;
+ os << "unable to convert bbot agent pubkey " << p;
+ throw_generic_error (EIO, os.str ().c_str ());
+ }
+ catch (const process_error& e)
+ {
+ ostringstream os;
+ os << "unable to convert bbot agent pubkey " << p;
+ throw_generic_error (e.code ().value (), os.str ().c_str ());
+ }
+ catch (const system_error& e)
+ {
+ ostringstream os;
+ os<< "unable to iterate over agents keys directory '" << d << "'";
+ throw_generic_error (e.code ().value (), os.str ().c_str ());
+ }
+
+ keys[d] = ak;
+ return ak;
+ }
+
+ void build_config_module::
+ init (const options::build& bo)
+ {
+ try
+ {
+ build_conf_ = shared_build_config (bo.build_config ());
+ }
+ catch (const io_error& e)
+ {
+ ostringstream os;
+ os << "unable to read build configuration '" << bo.build_config ()
+ << "': " << e;
+
+ throw_generic_error (EIO, os.str ().c_str ());
+ }
+
+ if (bo.build_bot_agent_keys_specified ())
+ bot_agent_key_map_ =
+ shared_bot_agent_keys (bo, bo.build_bot_agent_keys ());
+
+ cstrings conf_names;
+
+ using conf_map_type = map<const char*,
+ const build_config*,
+ compare_c_string>;
+
+ conf_map_type conf_map;
+
+ for (const auto& c: *build_conf_)
+ {
+ const char* cn (c.name.c_str ());
+ conf_map[cn] = &c;
+ conf_names.push_back (cn);
+ }
+
+ build_conf_names_ = make_shared<cstrings> (move (conf_names));
+ build_conf_map_ = make_shared<conf_map_type> (move (conf_map));
+ }
+
+ // The default underlying class set expression (see below).
+ //
+ static const build_class_expr default_ucs_expr (
+ {"default"}, '+', "Default.");
+
+ bool build_config_module::
+ exclude (const vector<build_class_expr>& exprs,
+ const vector<build_constraint>& constrs,
+ const build_config& cfg,
+ string* reason) const
+ {
+ // Save the first sentence of the reason, lower-case the first letter if
+ // the beginning looks like a word (all subsequent characters until a
+ // whitespace are lower-case letters).
+ //
+ auto sanitize = [] (const string& reason)
+ {
+ string r (reason.substr (0, reason.find ('.')));
+
+ char c (r[0]); // Can be '\0'.
+ if (alpha (c) && c == ucase (c))
+ {
+ bool word (true);
+
+ for (size_t i (1);
+ i != r.size () && (c = r[i]) != ' ' && c != '\t' && c != '\n';
+ ++i)
+ {
+ // Is not a word if contains a non-letter or an upper-case letter.
+ //
+ if (!alpha (c) || c == ucase (c))
+ {
+ word = false;
+ break;
+ }
+ }
+
+ if (word)
+ r[0] = lcase (r[0]);
+ }
+
+ return r;
+ };
+
+ // First, match the configuration against the package underlying build
+ // class set and expressions.
+ //
+ bool m (false);
+
+ // Match the configuration against an expression, updating the match
+ // result.
+ //
+ // We will use a comment of the first encountered excluding expression
+ // (changing the result from true to false) or non-including one (leaving
+ // the false result) as an exclusion reason.
+ //
+ auto match = [&cfg, &m, reason, &sanitize, this]
+ (const build_class_expr& e)
+ {
+ bool pm (m);
+ e.match (cfg.classes, build_conf_->class_inheritance_map, m);
+
+ if (reason != nullptr)
+ {
+ // Reset the reason which, if saved, makes no sense anymore.
+ //
+ if (m)
+ {
+ reason->clear ();
+ }
+ else if (reason->empty () &&
+ //
+ // Exclusion.
+ //
+ (pm ||
+ //
+ // Non-inclusion. Make sure that the build class expression
+ // is empty or starts with an addition (+...).
+ //
+ e.expr.empty () ||
+ e.expr.front ().operation == '+'))
+ {
+ *reason = sanitize (e.comment);
+ }
+ }
+ };
+
+ // Determine the underlying class set. Note that in the future we can
+ // potentially extend the underlying set with special classes.
+ //
+ const build_class_expr* ucs (
+ !exprs.empty () && !exprs.front ().underlying_classes.empty ()
+ ? &exprs.front ()
+ : nullptr);
+
+ // Note that the combined package build configuration class expression can
+ // be represented as the underlying class set used as a starting set for
+ // the original expressions and a restricting set, simultaneously. For
+ // example, for the expression:
+ //
+ // default legacy : -msvc
+ //
+ // the resulting expression will be:
+ //
+ // +( +default +legacy ) -msvc &( +default +legacy )
+ //
+ // Let's, however, optimize it a bit based on the following facts:
+ //
+ // - If the underlying class set expression (+default +legacy in the above
+ // example) evaluates to false, then the resulting expression also
+ // evaluates to false due to the trailing '&' operation. Thus, we don't
+ // need to evaluate further if that's the case.
+ //
+ // - On the other hand, if the underlying class set expression evaluates
+ // to true, then we don't need to apply the trailing '&' operation as it
+ // cannot affect the result.
+ //
+ const build_class_expr& ucs_expr (
+ ucs != nullptr
+ ? build_class_expr (ucs->underlying_classes, '+', ucs->comment)
+ : default_ucs_expr);
+
+ match (ucs_expr);
+
+ if (m)
+ {
+ for (const build_class_expr& e: exprs)
+ match (e);
+ }
+
+ // Exclude the configuration if it doesn't match the compound expression.
+ //
+ if (!m)
+ return true;
+
+ // Now check if the configuration is excluded/included via the patterns.
+ //
+ // To implement matching of absent name components with wildcard-only
+ // pattern components we are going to convert names to paths (see
+ // dash_components_to_path() for details).
+ //
+ // And if any of the build-{include,exclude} values (which is legal) or
+ // the build configuration name/target (illegal) are invalid paths, then
+ // we assume no match.
+ //
+ if (!constrs.empty ())
+ try
+ {
+ path cn (dash_components_to_path (cfg.name));
+ path tg (dash_components_to_path (cfg.target.string ()));
+
+ for (const build_constraint& c: constrs)
+ {
+ if (path_match (dash_components_to_path (c.config),
+ cn,
+ dir_path () /* start */,
+ path_match_flags::match_absent) &&
+ (!c.target ||
+ path_match (dash_components_to_path (*c.target),
+ tg,
+ dir_path () /* start */,
+ path_match_flags::match_absent)))
+ {
+ if (!c.exclusion)
+ return false;
+
+ if (reason != nullptr)
+ *reason = sanitize (c.comment);
+
+ return true;
+ }
+ }
+ }
+ catch (const invalid_path&) {}
+
+ return false;
+ }
+
+ path build_config_module::
+ dash_components_to_path (const string& s)
+ {
+ string r;
+ for (size_t i (0); i != s.size (); ++i)
+ {
+ char c (s[i]);
+
+ switch (c)
+ {
+ case '-':
+ {
+ r += '/';
+ break;
+ }
+ case '*':
+ {
+ if (s[i + 1] == '*') // Can be '\0'.
+ {
+ r += "*/**/*";
+ ++i;
+ break;
+ }
+ }
+ // Fall through.
+ default:
+ {
+ r += c;
+ break;
+ }
+ }
+ }
+
+ // Append the trailing slash to match the resulting paths as directories.
+ // This is required for the trailing /* we could append to match absent
+ // directory path components (see path_match_flags::match_absent for
+ // details).
+ //
+ // Note that valid dash components may not contain a trailing dash.
+ // Anyway, any extra trailing slashes will be ignored by the path
+ // constructor.
+ //
+ r += '/';
+
+ return path (move (r));
+ }
+}