diff options
Diffstat (limited to 'libbuild2/bash/rule.cxx')
-rw-r--r-- | libbuild2/bash/rule.cxx | 442 |
1 files changed, 442 insertions, 0 deletions
diff --git a/libbuild2/bash/rule.cxx b/libbuild2/bash/rule.cxx new file mode 100644 index 0000000..d9bf857 --- /dev/null +++ b/libbuild2/bash/rule.cxx @@ -0,0 +1,442 @@ +// file : libbuild2/bash/rule.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/bash/rule.hxx> + +#include <cstring> // strlen(), strchr() + +#include <libbuild2/scope.hxx> +#include <libbuild2/target.hxx> +#include <libbuild2/algorithm.hxx> +#include <libbuild2/diagnostics.hxx> + +#include <libbuild2/in/target.hxx> + +#include <libbuild2/bash/target.hxx> +#include <libbuild2/bash/utility.hxx> + +using namespace std; +using namespace butl; + +namespace build2 +{ + namespace bash + { + using in::in; + + struct match_data + { + // The "for install" condition is signalled to us by install_rule when + // it is matched for the update operation. It also verifies that if we + // have already been executed, then it was for install. + // + // See cc::link_rule for a discussion of some subtleties in this logic. + // + optional<bool> for_install; + }; + + static_assert (sizeof (match_data) <= target::data_size, + "insufficient space"); + + // in_rule + // + bool in_rule:: + match (action a, target& t, const string&) const + { + tracer trace ("bash::in_rule::match"); + + // Note that for bash{} we match even if the target does not depend on + // any modules (while it could have been handled by the in module, that + // would require loading it). + // + bool fi (false); // Found in. + bool fm (t.is_a<bash> ()); // Found module. + for (prerequisite_member p: group_prerequisite_members (a, t)) + { + if (include (a, t, p) != include_type::normal) // Excluded/ad hoc. + continue; + + fi = fi || p.is_a<in> (); + fm = fm || p.is_a<bash> (); + } + + if (!fi) + l4 ([&]{trace << "no in file prerequisite for target " << t;}); + + if (!fm) + l4 ([&]{trace << "no bash module prerequisite for target " << t;}); + + return (fi && fm); + } + + recipe in_rule:: + apply (action a, target& t) const + { + // Note that for-install is signalled by install_rule and therefore + // can only be relied upon during execute. + // + t.data (match_data ()); + + return rule::apply (a, t); + } + + target_state in_rule:: + perform_update (action a, const target& t) const + { + // Unless the outer install rule signalled that this is update for + // install, signal back that we've performed plain update. + // + match_data& md (t.data<match_data> ()); + + if (!md.for_install) + md.for_install = false; + + return rule::perform_update (a, t); + } + + prerequisite_target in_rule:: + search (action a, + const target& t, + const prerequisite_member& pm, + include_type i) const + { + tracer trace ("bash::in_rule::search"); + + // Handle import of installed bash{} modules. + // + if (i == include_type::normal && pm.proj () && pm.is_a<bash> ()) + { + // We only need this during update. + // + if (a != perform_update_id) + return nullptr; + + const prerequisite& p (pm.prerequisite); + + // Form the import path. + // + // Note that unless specified, we use the standard .bash extension + // instead of going through the bash{} target type since this path is + // not in our project (and thus no project-specific customization + // apply). + // + string ext (p.ext ? *p.ext : "bash"); + path ip (dir_path (project_base (*p.proj)) / p.dir / p.name); + + if (!ext.empty ()) + { + ip += '.'; + ip += ext; + } + + // Search in PATH, similar to butl::path_search(). + // + if (optional<string> s = getenv ("PATH")) + { + for (const char* b (s->c_str ()), *e; + b != nullptr; + b = (e != nullptr ? e + 1 : e)) + { + e = strchr (b, path::traits_type::path_separator); + + // Empty path (i.e., a double colon or a colon at the beginning or + // end of PATH) means search in the current dirrectory. We aren't + // going to do that. Also silently skip invalid paths, stat() + // errors, etc. + // + if (size_t n = (e != nullptr ? e - b : strlen (b))) + { + try + { + path ap (b, n); + ap /= ip; + ap.normalize (); + + timestamp mt (file_mtime (ap)); + + if (mt != timestamp_nonexistent) + { + auto rp (targets.insert_locked (bash::static_type, + ap.directory (), + dir_path () /* out */, + p.name, + ext, + true /* implied */, + trace)); + + bash& pt (rp.first.as<bash> ()); + + // Only set mtime/path on first insertion. + // + if (rp.second.owns_lock ()) + { + pt.mtime (mt); + pt.path (move (ap)); + } + + // Save the length of the import path in auxuliary data. We + // use it in substitute_import() to infer the installation + // directory. + // + return prerequisite_target (&pt, i, ip.size ()); + } + } + catch (const invalid_path&) {} + catch (const system_error&) {} + } + } + } + + // Let standard search() handle it. + } + + return rule::search (a, t, pm, i); + } + + optional<string> in_rule:: + substitute (const location& l, + action a, + const target& t, + const string& n, + bool strict) const + { + return n.compare (0, 6, "import") == 0 && (n[6] == ' ' || n[6] == '\t') + ? substitute_import (l, a, t, trim (string (n, 7))) + : rule::substitute (l, a, t, n, strict); + } + + string in_rule:: + substitute_import (const location& l, + action a, + const target& t, + const string& n) const + { + // Derive (relative) import path from the import name. + // + path ip; + + try + { + ip = path (n); + + if (ip.empty () || ip.absolute ()) + throw invalid_path (n); + + if (ip.extension_cstring () == nullptr) + ip += ".bash"; + + ip.normalize (); + } + catch (const invalid_path&) + { + fail (l) << "invalid import path '" << n << "'"; + } + + // Look for a matching prerequisite. + // + const path* ap (nullptr); + for (const prerequisite_target& pt: t.prerequisite_targets[a]) + { + if (pt.target == nullptr || pt.adhoc) + continue; + + if (const bash* b = pt.target->is_a<bash> ()) + { + const path& pp (b->path ()); + assert (!pp.empty ()); // Should have been assigned by update. + + // The simple "tail match" can be ambigous. Consider, for example, + // the foo/bar.bash import path and /.../foo/bar.bash as well as + // /.../x/foo/bar.bash prerequisites: they would both match. + // + // So the rule is the match must be from the project root directory + // or from the installation directory for the import-installed + // prerequisites. + // + // But we still do a simple match first since it can quickly weed + // out candidates that cannot possibly match. + // + if (!pp.sup (ip)) + continue; + + // See if this is import-installed target (refer to search() for + // details). + // + if (size_t n = pt.data) + { + // Both are normalized so we can compare the "tails". + // + const string& ps (pp.string ()); + const string& is (ip.string ()); + + if (path::traits_type::compare ( + ps.c_str () + ps.size () - n, n, + is.c_str (), is.size ()) == 0) + { + ap = &pp; + break; + } + else + continue; + } + + if (const scope* rs = scopes.find (b->dir).root_scope ()) + { + const dir_path& d (pp.sub (rs->src_path ()) + ? rs->src_path () + : rs->out_path ()); + + if (pp.leaf (d) == ip) + { + ap = &pp; + break; + } + else + continue; + } + + fail (l) << "target " << *b << " is out of project nor imported"; + } + } + + if (ap == nullptr) + fail (l) << "unable to resolve import path " << ip; + + match_data& md (t.data<match_data> ()); + assert (md.for_install); + + if (*md.for_install) + { + // For the installed case we assume the script and all its modules are + // installed into the same location (i.e., the same bin/ directory) + // and so we use the path relative to the script. + // + // BTW, the semantics of the source builtin in bash is to search in + // PATH if it's a simple path (that is, does not contain directory + // components) and then in the current working directory. + // + // So we have to determine the scripts's directory ourselves for which + // we use the BASH_SOURCE array. Without going into the gory details, + // the last element in this array is the script's path regardless of + // whether we are in the script or (sourced) module (but it turned out + // not to be what we need; see below). + // + // We also want to get the script's "real" directory even if it was + // itself symlinked somewhere else. And this is where things get + // hairy: we could use either realpath or readlink -f but neither is + // available on Mac OS (there is readlink but it doesn't support the + // -f option). + // + // One can get GNU readlink from Homebrew but it will be called + // greadlink. Note also that for any serious development one will + // probably be also getting newer bash from Homebrew since the system + // one is stuck in the GPLv2 version 3.2.X era. So a bit of a mess. + // + // For now let's use readlink -f and see how it goes. If someone wants + // to use/support their scripts on Mac OS, they have several options: + // + // 1. Install greadlink (coreutils) and symlink it as readlink. + // + // 2. Add the readlink function to their script that does nothing; + // symlinking scripts won't be supported but the rest should work + // fine. + // + // 3. Add the readlink function to their script that calls greadlink. + // + // 4. Add the readlink function to their script that implements the + // -f mode (or at least the part of it that we need). See the bash + // module tests for some examples. + // + // In the future we could automatically inject an implementation along + // the (4) lines at the beginning of the script. + // + // Note also that we really, really want to keep the substitution a + // one-liner since the import can be in an (indented) if-block, etc., + // and we still want the resulting scripts to be human-readable. + // + if (t.is_a<exe> ()) + { + return + "source \"$(dirname" + " \"$(readlink -f" + " \"${BASH_SOURCE[0]}\")\")/" + + ip.string () + "\""; + } + else + { + // Things turned out to be trickier for the installed modules: we + // cannot juts use the script's path since it itself might not be + // installed (import installed). So we have to use the importer's + // path and calculate its "offset" to the installation directory. + // + dir_path d (t.dir.leaf (t.root_scope ().out_path ())); + + string o; + for (auto i (d.begin ()), e (d.end ()); i != e; ++i) + o += "../"; + + // Here we don't use readlink since we assume nobody will symlink + // the modules (or they will all be symlinked together). + // + return + "source \"$(dirname" + " \"${BASH_SOURCE[0]}\")/" + + o + ip.string () + "\""; + } + } + else + return "source " + ap->string (); + } + + // install_rule + // + bool install_rule:: + match (action a, target& t, const string& hint) const + { + // We only want to handle installation if we are also the ones building + // this target. So first run in's match(). + // + return in_.match (a, t, hint) && file_rule::match (a, t, ""); + } + + recipe install_rule:: + apply (action a, target& t) const + { + recipe r (file_rule::apply (a, t)); + + if (a.operation () == update_id) + { + // Signal to the in rule that this is update for install. And if the + // update has already been executed, verify it was done for install. + // + auto& md (t.data<match_data> ()); + + if (md.for_install) + { + if (!*md.for_install) + fail << "target " << t << " already updated but not for install"; + } + else + md.for_install = true; + } + + return r; + } + + const target* install_rule:: + filter (action a, const target& t, const prerequisite& p) const + { + // If this is a module prerequisite, install it as long as it is in the + // same amalgamation as we are. + // + if (p.is_a<bash> ()) + { + const target& pt (search (t, p)); + return pt.in (t.weak_scope ()) ? &pt : nullptr; + } + else + return file_rule::filter (a, t, p); + } + } +} |