From c1dc06dfd1d329f8c6499dbe2166725ab9c35e17 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Fri, 20 Jul 2018 15:31:13 +0200 Subject: Implement bash module --- bootstrap-mingw.bat | 1 + bootstrap-msvc.bat | 1 + bootstrap.gmake | 3 +- bootstrap.sh | 1 + build2/algorithm.cxx | 33 ++-- build2/algorithm.hxx | 40 +++-- build2/algorithm.ixx | 57 ++++--- build2/b.cxx | 4 + build2/bash/init.cxx | 89 +++++++++++ build2/bash/init.hxx | 28 ++++ build2/bash/rule.cxx | 398 ++++++++++++++++++++++++++++++++++++++++++++++++ build2/bash/rule.hxx | 81 ++++++++++ build2/bash/target.cxx | 30 ++++ build2/bash/target.hxx | 32 ++++ build2/bash/utility.hxx | 28 ++++ build2/in/init.cxx | 2 +- build2/in/rule.cxx | 155 +++++++++++-------- build2/in/rule.hxx | 23 ++- build2/in/target.cxx | 2 - build2/version/rule.cxx | 17 +-- build2/version/rule.hxx | 7 +- tests/bash/buildfile | 5 + tests/bash/testscript | 223 +++++++++++++++++++++++++++ tests/common.test | 6 +- 24 files changed, 1138 insertions(+), 128 deletions(-) create mode 100644 build2/bash/init.cxx create mode 100644 build2/bash/init.hxx create mode 100644 build2/bash/rule.cxx create mode 100644 build2/bash/rule.hxx create mode 100644 build2/bash/target.cxx create mode 100644 build2/bash/target.hxx create mode 100644 build2/bash/utility.hxx create mode 100644 tests/bash/buildfile create mode 100644 tests/bash/testscript diff --git a/bootstrap-mingw.bat b/bootstrap-mingw.bat index 68b6b7b..8f5f0e7 100644 --- a/bootstrap-mingw.bat +++ b/bootstrap-mingw.bat @@ -73,6 +73,7 @@ set "src=%src% build2\test\script\*.cxx" set "src=%src% build2\version\*.cxx" set "src=%src% build2\install\*.cxx" set "src=%src% build2\in\*.cxx" +set "src=%src% build2\bash\*.cxx" set "src=%src% %libbutl%\libbutl\*.cxx" rem Get the compile options. diff --git a/bootstrap-msvc.bat b/bootstrap-msvc.bat index ff1360b..ffbbee2 100644 --- a/bootstrap-msvc.bat +++ b/bootstrap-msvc.bat @@ -104,6 +104,7 @@ set "src=%src% build2\test\script" set "src=%src% build2\version" set "src=%src% build2\install" set "src=%src% build2\in" +set "src=%src% build2\bash" set "src=%src% %libbutl%\libbutl" rem Get the compile options. diff --git a/bootstrap.gmake b/bootstrap.gmake index 58ee069..493befd 100644 --- a/bootstrap.gmake +++ b/bootstrap.gmake @@ -141,7 +141,8 @@ test/script \ test \ version \ install \ -in +in \ +bash build2_src := $(wildcard $(src_root)/build2/*.cxx) build2_src += $(foreach d,$(sub_dirs),$(wildcard $(src_root)/build2/$d/*.cxx)) diff --git a/bootstrap.sh b/bootstrap.sh index 3b9ffda..e6088c2 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -129,6 +129,7 @@ src="$src build2/test/script/*.cxx" src="$src build2/version/*.cxx" src="$src build2/install/*.cxx" src="$src build2/in/*.cxx" +src="$src build2/bash/*.cxx" src="$src $libbutl/libbutl/*.cxx" # Note that for as long as we support GCC 4.9 we have to compile in the C++14 diff --git a/build2/algorithm.cxx b/build2/algorithm.cxx index c46e878..550a491 100644 --- a/build2/algorithm.cxx +++ b/build2/algorithm.cxx @@ -654,9 +654,12 @@ namespace build2 match_impl (l, true /* step */, true /* try_match */); } - template + template static void - match_prerequisite_range (action a, target& t, R&& r, const scope* s) + match_prerequisite_range (action a, target& t, + R&& r, + const S& ms, + const scope* s) { auto& pts (t.prerequisite_targets[a]); @@ -675,13 +678,15 @@ namespace build2 if (!pi) continue; - const target& pt (search (t, p)); + prerequisite_target pt (ms + ? ms (a, t, p, pi) + : prerequisite_target (&search (t, p), pi)); - if (s != nullptr && !pt.in (*s)) + if (pt.target == nullptr || (s != nullptr && !pt.target->in (*s))) continue; - match_async (a, pt, target::count_busy (), t[a].task_count); - pts.push_back (prerequisite_target (&pt, pi)); + match_async (a, *pt.target, target::count_busy (), t[a].task_count); + pts.push_back (move (pt)); } wg.wait (); @@ -696,15 +701,19 @@ namespace build2 } void - match_prerequisites (action a, target& t, const scope* s) + match_prerequisites (action a, target& t, + const match_search& ms, + const scope* s) { - match_prerequisite_range (a, t, group_prerequisites (t), s); + match_prerequisite_range (a, t, group_prerequisites (t), ms, s); } void - match_prerequisite_members (action a, target& t, const scope* s) + match_prerequisite_members (action a, target& t, + const match_search_member& msm, + const scope* s) { - match_prerequisite_range (a, t, group_prerequisite_members (a, t), s); + match_prerequisite_range (a, t, group_prerequisite_members (a, t), msm, s); } template @@ -1715,7 +1724,7 @@ namespace build2 pair, const target*> execute_prerequisites (const target_type* tt, action a, const target& t, - const timestamp& mt, const prerequisite_filter& pf, + const timestamp& mt, const execute_filter& ef, size_t n) { assert (current_mode == execution_mode::first); @@ -1770,7 +1779,7 @@ namespace build2 // Should we compare the timestamp to this target's? // - if (!e && (!pf || pf (pt, i))) + if (!e && (!ef || ef (pt, i))) { // If this is an mtime-based target, then compare timestamps. // diff --git a/build2/algorithm.hxx b/build2/algorithm.hxx index 93fe131..22b8471 100644 --- a/build2/algorithm.hxx +++ b/build2/algorithm.hxx @@ -289,18 +289,34 @@ namespace build2 match_inner (action, const target&, unmatch); // The standard prerequisite search and match implementations. They call - // search() and then match() for each prerequisite in a loop omitting out of - // project prerequisites for the clean operation. If this target is a member - // of a group, then they first do this to the group's prerequisites. + // search() (unless a custom is provided) and then match() (unless custom + // returned NULL) for each prerequisite in a loop omitting out of project + // prerequisites for the clean operation. If this target is a member of a + // group, then first do this to the group's prerequisites. // + using match_search =function< + prerequisite_target (action, + const target&, + const prerequisite&, + include_type)>; + void - match_prerequisites (action, target&); + match_prerequisites (action, target&, const match_search& = nullptr); - // If we are cleaning, this function doesn't go into group members, - // as an optimization (the group should clean everything up). + // As above but go into group members. + // + // Note that if we cleaning, this function doesn't go into group members, as + // an optimization (the group should clean everything up). // + using match_search_member = function< + prerequisite_target (action, + const target&, + const prerequisite_member&, + include_type)>; + void - match_prerequisite_members (action, target&); + match_prerequisite_members (action, target&, + const match_search_member& = nullptr); // As above but omit prerequisites that are not in the specified scope. // @@ -495,12 +511,12 @@ namespace build2 // Note that because we use mtime, this function should normally only be // used in the perform_update action (which is straight). // - using prerequisite_filter = function; + using execute_filter = function; optional execute_prerequisites (action, const target&, const timestamp&, - const prerequisite_filter& = nullptr, + const execute_filter& = nullptr, size_t count = 0); // Another version of the above that does two extra things for the caller: @@ -514,14 +530,14 @@ namespace build2 pair, const T&> execute_prerequisites (action, const target&, const timestamp&, - const prerequisite_filter& = nullptr, + const execute_filter& = nullptr, size_t count = 0); pair, const target&> execute_prerequisites (const target_type&, action, const target&, const timestamp&, - const prerequisite_filter& = nullptr, + const execute_filter& = nullptr, size_t count = 0); template @@ -529,7 +545,7 @@ namespace build2 execute_prerequisites (const target_type&, action, const target&, const timestamp&, - const prerequisite_filter& = nullptr, + const execute_filter& = nullptr, size_t count = 0); // Execute members of a group or similar prerequisite-like dependencies. diff --git a/build2/algorithm.ixx b/build2/algorithm.ixx index 16117ab..fb77e9c 100644 --- a/build2/algorithm.ixx +++ b/build2/algorithm.ixx @@ -504,43 +504,60 @@ namespace build2 } void - match_prerequisites (action, target&, const scope*); + match_prerequisites (action, target&, const match_search&, const scope*); void - match_prerequisite_members (action, target&, const scope*); + match_prerequisite_members (action, target&, + const match_search_member&, + const scope*); inline void - match_prerequisites (action a, target& t) + match_prerequisites (action a, target& t, const match_search& ms) { match_prerequisites ( a, t, + ms, (a.operation () != clean_id ? nullptr : &t.root_scope ())); } inline void - match_prerequisite_members (action a, target& t) + match_prerequisite_members (action a, target& t, + const match_search_member& msm) { if (a.operation () != clean_id) - match_prerequisite_members (a, t, nullptr); + match_prerequisite_members (a, t, msm, nullptr); else - // Note that here we don't iterate over members even for see- - // through groups since the group target should clean eveything - // up. A bit of an optimization. + { + // Note that here we don't iterate over members even for see-through + // groups since the group target should clean eveything up. A bit of an + // optimization. // - match_prerequisites (a, t, &t.root_scope ()); + match_search ms ( + msm + ? [&msm] (action a, + const target& t, + const prerequisite& p, + include_type i) + { + return msm (a, t, prerequisite_member {p, nullptr}, i); + } + : match_search ()); + + match_prerequisites (a, t, ms, &t.root_scope ()); + } } inline void match_prerequisites (action a, target& t, const scope& s) { - match_prerequisites (a, t, &s); + match_prerequisites (a, t, nullptr, &s); } inline void match_prerequisite_members (action a, target& t, const scope& s) { - match_prerequisite_members (a, t, &s); + match_prerequisite_members (a, t, nullptr, &s); } target_state @@ -657,24 +674,24 @@ namespace build2 pair, const target*> execute_prerequisites (const target_type*, action, const target&, - const timestamp&, const prerequisite_filter&, + const timestamp&, const execute_filter&, size_t); inline optional execute_prerequisites (action a, const target& t, - const timestamp& mt, const prerequisite_filter& pf, + const timestamp& mt, const execute_filter& ef, size_t n) { - return execute_prerequisites (nullptr, a, t, mt, pf, n).first; + return execute_prerequisites (nullptr, a, t, mt, ef, n).first; } template inline pair, const T&> execute_prerequisites (action a, const target& t, - const timestamp& mt, const prerequisite_filter& pf, + const timestamp& mt, const execute_filter& ef, size_t n) { - auto p (execute_prerequisites (T::static_type, a, t, mt, pf, n)); + auto p (execute_prerequisites (T::static_type, a, t, mt, ef, n)); return pair, const T&> ( p.first, static_cast (p.second)); } @@ -682,10 +699,10 @@ namespace build2 inline pair, const target&> execute_prerequisites (const target_type& tt, action a, const target& t, - const timestamp& mt, const prerequisite_filter& pf, + const timestamp& mt, const execute_filter& ef, size_t n) { - auto p (execute_prerequisites (&tt, a, t, mt, pf, n)); + auto p (execute_prerequisites (&tt, a, t, mt, ef, n)); return pair, const target&> (p.first, *p.second); } @@ -693,10 +710,10 @@ namespace build2 inline pair, const T&> execute_prerequisites (const target_type& tt, action a, const target& t, - const timestamp& mt, const prerequisite_filter& pf, + const timestamp& mt, const execute_filter& ef, size_t n) { - auto p (execute_prerequisites (tt, a, t, mt, pf, n)); + auto p (execute_prerequisites (tt, a, t, mt, ef, n)); return pair, const T&> ( p.first, static_cast (p.second)); } diff --git a/build2/b.cxx b/build2/b.cxx index 977a76e..0f2a928 100644 --- a/build2/b.cxx +++ b/build2/b.cxx @@ -59,6 +59,8 @@ using namespace std; #include +#include + namespace build2 { int @@ -419,6 +421,8 @@ main (int argc, char* argv[]) bm["cli.config"] = mf {nullptr, &cli::config_init}; bm["cli"] = mf {nullptr, &cli::init}; + + bm["bash"] = mf {nullptr, &bash::init}; } keep_going = !ops.serial_stop (); diff --git a/build2/bash/init.cxx b/build2/bash/init.cxx new file mode 100644 index 0000000..ff653c2 --- /dev/null +++ b/build2/bash/init.cxx @@ -0,0 +1,89 @@ +// file : build2/bash/init.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +using namespace std; + +namespace build2 +{ + namespace bash + { + static const in_rule in_rule_; + static const install_rule install_rule_ (in_rule_); + + bool + init (scope& rs, + scope& bs, + const location& l, + unique_ptr&, + bool, + bool, + const variable_map&) + { + tracer trace ("bash::init"); + l5 ([&]{trace << "for " << bs.out_path ();}); + + // Load in.base (in.* varibales, in{} target type). + // + if (!cast_false (rs["in.base.loaded"])) + load_module (rs, rs, "in.base", l); + + bool install_loaded (cast_false (rs["install.loaded"])); + + // Register target types and configure default installability. + // + bs.target_types.insert (); + + if (install_loaded) + { + using namespace install; + + // Install into bin// by default stripping the .bash + // extension from if present. + // + const string& p (cast (rs.vars[var_project])); + + install_path (bs, dir_path ("bin") /= project_base (p)); + install_mode (bs, "644"); + } + + // Register rules. + // + { + auto& r (bs.rules); + + r.insert (perform_update_id, "bash.in", in_rule_); + r.insert (perform_clean_id, "bash.in", in_rule_); + r.insert (configure_update_id, "bash.in", in_rule_); + + r.insert (perform_update_id, "bash.in", in_rule_); + r.insert (perform_clean_id, "bash.in", in_rule_); + r.insert (configure_update_id, "bash.in", in_rule_); + + if (install_loaded) + { + r.insert (perform_install_id, "bash.install", install_rule_); + r.insert (perform_uninstall_id, "bash.uninstall", install_rule_); + + r.insert (perform_install_id, "bash.install", install_rule_); + r.insert (perform_uninstall_id, "bash.uninstall", install_rule_); + } + } + + return true; + } + } +} diff --git a/build2/bash/init.hxx b/build2/bash/init.hxx new file mode 100644 index 0000000..20ae9b7 --- /dev/null +++ b/build2/bash/init.hxx @@ -0,0 +1,28 @@ +// file : build2/bash/init.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_BASH_INIT_HXX +#define BUILD2_BASH_INIT_HXX + +#include +#include + +#include + +namespace build2 +{ + namespace bash + { + bool + init (scope&, + scope&, + const location&, + unique_ptr&, + bool, + bool, + const variable_map&); + } +} + +#endif // BUILD2_BASH_INIT_HXX diff --git a/build2/bash/rule.cxx b/build2/bash/rule.cxx new file mode 100644 index 0000000..4746ffe --- /dev/null +++ b/build2/bash/rule.cxx @@ -0,0 +1,398 @@ +// file : build2/bash/rule.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include // strlen(), strchr() + +#include +#include +#include +#include + +#include + +#include +#include + +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 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"); + + bool fi (false); // Found in. + bool fm (false); // 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 (); + fm = fm || p.is_a (); + } + + 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 ()); + + 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 ()) + { + // 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 s = getenv ("PATH")) + { + for (const char* b (s->c_str ()), *e; + b != nullptr; + b = (e != nullptr ? e + 1 : e)) + { + e = strchr (b, path::traits::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 ()); + + // 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 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 ()) + { + 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::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 ()); + 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. + // + // 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. + // + return + "source \"$(dirname" + " \"$(readlink -f" + " \"${BASH_SOURCE[${#BASH_SOURCE[@]}-1]}\")\")/" + + 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 ()); + + if (md.for_install) + { + if (!*md.for_install) + fail << "target " << t << " already updated but not for install"; + } + else + md.for_install = true; + } + + return r; + } + } +} diff --git a/build2/bash/rule.hxx b/build2/bash/rule.hxx new file mode 100644 index 0000000..9957970 --- /dev/null +++ b/build2/bash/rule.hxx @@ -0,0 +1,81 @@ +// file : build2/bash/rule.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_BASH_RULE_HXX +#define BUILD2_BASH_RULE_HXX + +#include +#include + +#include +#include + +namespace build2 +{ + namespace bash + { + // Preprocess a bash script (exe{}) or module (bash{}) .in file that + // imports one or more bash modules. + // + // Note that the default substitution symbol is '@' and the mode is lax + // (think bash arrays). The idea is that '@' is normally used in ways that + // are highly unlikely to be misinterpreted as substitutions. The user, + // however, is still able to override both of these choices with the + // corresponding in.* variables (e.g., to use '`' and strict mode). + // + class in_rule: public in::rule + { + public: + in_rule (): rule ("bash.in 1", "bash.in", '@', false /* strict */) {} + + virtual bool + match (action, target&, const string&) const override; + + virtual recipe + apply (action, target&) const override; + + virtual target_state + perform_update (action, const target&) const override; + + virtual prerequisite_target + search (action, + const target&, + const prerequisite_member&, + include_type) const override; + + virtual optional + substitute (const location&, + action a, + const target&, + const string&, + bool) const override; + + string + substitute_import (const location&, + action a, + const target&, + const string&) const; + }; + + // Installation rule for bash scripts (exe{}) and modules (bash{}) that + // signals to in_rule that this is update for install. + // + class install_rule: public install::file_rule + { + public: + install_rule (const in_rule& in): in_ (in) {} + + virtual bool + match (action, target&, const string&) const override; + + virtual recipe + apply (action, target&) const override; + + private: + const in_rule& in_; + }; + } +} + +#endif // BUILD2_BASH_RULE_HXX diff --git a/build2/bash/target.cxx b/build2/bash/target.cxx new file mode 100644 index 0000000..660528e --- /dev/null +++ b/build2/bash/target.cxx @@ -0,0 +1,30 @@ +// file : build2/bash/target.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include + +using namespace std; + +namespace build2 +{ + namespace bash + { + extern const char bash_ext_def[] = "bash"; + + const target_type bash::static_type + { + "bash", + &file::static_type, + &target_factory, + nullptr, /* fixed_extension */ + &target_extension_var, + &target_pattern_var, + nullptr, + &file_search, + false + }; + } +} diff --git a/build2/bash/target.hxx b/build2/bash/target.hxx new file mode 100644 index 0000000..b8f4d28 --- /dev/null +++ b/build2/bash/target.hxx @@ -0,0 +1,32 @@ +// file : build2/bash/target.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_BASH_TARGET_HXX +#define BUILD2_BASH_TARGET_HXX + +#include +#include + +#include + +namespace build2 +{ + namespace bash + { + // Bash module file to be sourced by a script. The default/standard + // extension is .bash. + // + class bash: public file + { + public: + using file::file; + + public: + static const target_type static_type; + virtual const target_type& dynamic_type () const {return static_type;} + }; + } +} + +#endif // BUILD2_BASH_TARGET_HXX diff --git a/build2/bash/utility.hxx b/build2/bash/utility.hxx new file mode 100644 index 0000000..d8d9a43 --- /dev/null +++ b/build2/bash/utility.hxx @@ -0,0 +1,28 @@ +// file : build2/bash/utility.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_BASH_UTILITY_HXX +#define BUILD2_BASH_UTILITY_HXX + +#include +#include + +namespace build2 +{ + namespace bash + { + // Strip the .bash extension from the project name. + // + inline string + project_base (const string& n) + { + size_t p (path::traits::find_extension (n)); + return p == string::npos || casecmp (n.c_str () + p, ".bash", 5) != 0 + ? n + : string (n, 0, p); + } + } +} + +#endif // BUILD2_BASH_UTILITY_HXX diff --git a/build2/in/init.cxx b/build2/in/init.cxx index a87f8b3..2583456 100644 --- a/build2/in/init.cxx +++ b/build2/in/init.cxx @@ -91,7 +91,7 @@ namespace build2 // Register rules. // { - auto& r (rs.rules); + auto& r (bs.rules); // There are rules that are "derived" from this generic in rule in // order to provide extended preprocessing functionality (see the diff --git a/build2/in/rule.cxx b/build2/in/rule.cxx index 6be2e71..41f2f71 100644 --- a/build2/in/rule.cxx +++ b/build2/in/rule.cxx @@ -66,7 +66,15 @@ namespace build2 // Match prerequisite members. // - match_prerequisite_members (a, t); + match_prerequisite_members (a, + t, + [this] (action a, + const target& t, + const prerequisite_member& p, + include_type i) + { + return search (a, t, p, i); + }); switch (a) { @@ -79,66 +87,6 @@ namespace build2 } } - string rule:: - lookup (const location& l, const target& t, const string& n) const - { - if (auto x = t[n]) - { - value v (*x); - - // For typed values call string() for conversion. - // - try - { - return convert ( - v.type == nullptr - ? move (v) - : functions.call (&t.base_scope (), - "string", - vector_view (&v, 1), - l)); - } - catch (const invalid_argument& e) - { - fail (l) << e << - info << "while substituting '" << n << "'" << endf; - } - } - else - fail (l) << "undefined variable '" << n << "'" << endf; - } - - optional rule:: - substitute (const location& l, - const target& t, - const string& n, - bool strict) const - { - // In the lax mode scan the fragment to make sure it is a variable name - // (that is, it can be expanded in a buildfile as just $; see - // lexer's variable mode for details). - // - if (!strict) - { - for (size_t i (0), e (n.size ()); i != e; ) - { - bool f (i == 0); // First. - char c (n[i++]); - bool l (i == e); // Last. - - if (c == '_' || (f ? alpha (c) : alnum (c))) - continue; - - if (c == '.' && !l) - continue; - - return nullopt; // Ignore this substitution. - } - } - - return lookup (l, t, n); - } - target_state rule:: perform_update (action a, const target& xt) const { @@ -265,9 +213,18 @@ namespace build2 if (p1 != string::npos && p2 != string::npos && p2 - p1 > 1) { string n (*s, p1 + 1, p2 - p1 - 1); - string v (lookup (location (&ip, ln), t, n)); - if (s->compare (p2 + 1, string::npos, sha256 (v).string ()) == 0) + // Note that we have to call substitute(), not lookup() since it + // can be overriden with custom substitution semantics. + // + optional v ( + substitute (location (&ip, ln), a, t, n, strict)); + + assert (v); // Rule semantics change without version increment? + + if (s->compare (p2 + 1, + string::npos, + sha256 (*v).string ()) == 0) { dd_skip++; continue; @@ -391,7 +348,7 @@ namespace build2 // pointing to the opening symbol and e -- to the closing. // string name (s, b + 1, e - b -1); - if (optional val = substitute (l, t, name, strict)) + if (optional val = substitute (l, a, t, name, strict)) { // Save in depdb. // @@ -444,5 +401,75 @@ namespace build2 t.mtime (system_clock::now ()); return target_state::changed; } + + prerequisite_target rule:: + search (action, + const target& t, + const prerequisite_member& p, + include_type i) const + { + return prerequisite_target (&build2::search (t, p), i); + } + + string rule:: + lookup (const location& l, action, const target& t, const string& n) const + { + if (auto x = t[n]) + { + value v (*x); + + // For typed values call string() for conversion. + // + try + { + return convert ( + v.type == nullptr + ? move (v) + : functions.call (&t.base_scope (), + "string", + vector_view (&v, 1), + l)); + } + catch (const invalid_argument& e) + { + fail (l) << e << + info << "while substituting '" << n << "'" << endf; + } + } + else + fail (l) << "undefined variable '" << n << "'" << endf; + } + + optional rule:: + substitute (const location& l, + action a, + const target& t, + const string& n, + bool strict) const + { + // In the lax mode scan the fragment to make sure it is a variable name + // (that is, it can be expanded in a buildfile as just $; see + // lexer's variable mode for details). + // + if (!strict) + { + for (size_t i (0), e (n.size ()); i != e; ) + { + bool f (i == 0); // First. + char c (n[i++]); + bool l (i == e); // Last. + + if (c == '_' || (f ? alpha (c) : alnum (c))) + continue; + + if (c == '.' && !l) + continue; + + return nullopt; // Ignore this substitution. + } + } + + return lookup (l, a, t, n); + } } } diff --git a/build2/in/rule.hxx b/build2/in/rule.hxx index 17fbaed..9c40e4f 100644 --- a/build2/in/rule.hxx +++ b/build2/in/rule.hxx @@ -41,23 +41,38 @@ namespace build2 virtual recipe apply (action, target&) const override; + virtual target_state + perform_update (action, const target&) const; + + // Customization hooks. + // + + // Perform prerequisite search. + // + virtual prerequisite_target + search (action, + const target&, + const prerequisite_member&, + include_type) const; + // Perform variable lookup. // virtual string - lookup (const location&, const target&, const string& name) const; + lookup (const location&, + action, + const target&, + const string& name) const; // Perform variable substitution. Return nullopt if it should be // ignored. // virtual optional substitute (const location&, + action, const target&, const string& name, bool strict) const; - target_state - perform_update (action, const target&) const; - protected: const string rule_id_; const string program_; diff --git a/build2/in/target.cxx b/build2/in/target.cxx index ecf975f..fec1135 100644 --- a/build2/in/target.cxx +++ b/build2/in/target.cxx @@ -10,8 +10,6 @@ namespace build2 { namespace in { - // in - // static const target* in_search (const target& xt, const prerequisite_key& cpk) { diff --git a/build2/version/rule.cxx b/build2/version/rule.cxx index a0ff1e2..262d623 100644 --- a/build2/version/rule.cxx +++ b/build2/version/rule.cxx @@ -4,13 +4,8 @@ #include -#include #include #include -#include -#include -#include -#include #include #include @@ -88,7 +83,10 @@ namespace build2 } string in_rule:: - lookup (const location& l, const target& t, const string& n) const + lookup (const location& l, + action a, + const target& t, + const string& n) const { // Note that this code will be executed during up-to-date check for each // substitution so let's try not to do anything overly sub-optimal here. @@ -105,9 +103,10 @@ namespace build2 if (p == string::npos || n.compare (0, p, m.project) == 0) { - // Standard lookup. - // - return rule::lookup (l, t, p == string::npos ? n : string (n, p + 1)); + return rule::lookup (l, // Standard lookup. + a, + t, + p == string::npos ? n : string (n, p + 1)); } string pn (n, 0, p); diff --git a/build2/version/rule.hxx b/build2/version/rule.hxx index afed11a..c3b41be 100644 --- a/build2/version/rule.hxx +++ b/build2/version/rule.hxx @@ -20,13 +20,16 @@ namespace build2 class in_rule: public in::rule { public: - in_rule (): rule ("version.in 1", "ver") {} + in_rule (): rule ("version.in 1", "version.in") {} virtual bool match (action, target&, const string&) const override; virtual string - lookup (const location&, const target&, const string&) const override; + lookup (const location&, + action, + const target&, + const string&) const override; }; // Pre-process manifest before installation to patch in the version. diff --git a/tests/bash/buildfile b/tests/bash/buildfile new file mode 100644 index 0000000..92fd280 --- /dev/null +++ b/tests/bash/buildfile @@ -0,0 +1,5 @@ +# file : tests/bash/buildfile +# copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +./: testscript $b diff --git a/tests/bash/testscript b/tests/bash/testscript new file mode 100644 index 0000000..39f247b --- /dev/null +++ b/tests/bash/testscript @@ -0,0 +1,223 @@ +# file : tests/bash/testscript +# copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +# Only native testing on non-Windows platforms +# +: dummy +: +if ($test.target == $build.host && $build.host.class != 'windows') + { + buildfile = true + test.arguments = + + .include ../common.test + + +cat <+build/bootstrap.build + using test + using install + EOI + + +cat <=build/root.build + using bash + test/bash{*}: install.subdirs = true + exe{*}: test = true + EOI + + # Setup a subproject that we can import a module from. + # + # Note: used by multiple tests so should be static. + # + +mkdir -p sub.bash/build + +cat <=sub.bash/build/bootstrap.build + project = sub.bash + using install + EOI + +cat <=sub.bash/build/root.build + using bash + EOI + +cat <=sub.bash/build/export.build + $out_root/ + { + include sub/ + } + export $src_root/sub/bash{foo} + EOI + +cat <=sub.bash/buildfile + ./: dir{*/ -build/} + EOI + +mkdir sub.bash/sub + +cat <=sub.bash/sub/foo.bash + echo sub + EOI + +cat <=sub.bash/sub/buildfile + ./: bash{foo} + EOI + + # This scopes creates the test/ subdirectory corresponding to our project in + # the import path. A bit of a hack but the alternative would be creating a + # project for each test. + # + : test + : + { + : basics + : + { + cat <=hello.bash; + function hello () { echo "Hello, $@!"; } + EOI + + cat <=hello.in; + #!/usr/bin/env bash + + if [[ "$OSTYPE" == darwin* ]]; then + function readlink () + { + if [ "$1" != -f ]; then + command readlink "$@" + else + echo "$2" + fi + } + fi + + @import test/basics/hello@ + + hello "$@" + EOI + + cat <=buildfile; + exe{hello}: in{hello} bash{hello} + exe{hello}: test.arguments = 'World' + EOI + + $* test >'Hello, World!'; + + $* install config.install.root=tmp; + tmp/bin/hello 'John' >'Hello, John!'; + $* uninstall config.install.root=tmp; + + $* clean + } + + : import + : + { + cat <=driver.in; + #!/usr/bin/env bash + + if [[ "$OSTYPE" == darwin* ]]; then + function readlink () + { + if [ "$1" != -f ]; then + command readlink "$@" + else + echo "$2" + fi + } + fi + + @import sub/foo@ + EOI + + cat <=buildfile; + import mods = sub.bash%bash{foo} + exe{driver}: in{driver} $mods + EOI + + $* test >'sub'; + + $* install config.install.root=tmp; + tmp/bin/driver >'sub'; + $* uninstall config.install.root=tmp; + + $* clean + } + + : recursive + : + { + cat <=test.bash.in; + @import sub/foo@ + EOI + + cat <=driver.in; + #!/usr/bin/env bash + + if [[ "$OSTYPE" == darwin* ]]; then + function readlink () + { + if [ "$1" != -f ]; then + command readlink "$@" + else + local r="$2" + if test -L "$r"; then + r="$(command readlink "$r")" + if [[ "$r" != /* ]]; then + r="$(dirname "$2")/$r" + fi + fi + echo "$r" + fi + } + fi + + @import test/recursive/test@ + EOI + + cat <=buildfile; + import mods = sub.bash%bash{foo} + exe{driver}: in{driver} bash{test} + bash{test}: in{test} $mods + EOI + + $* test >'sub'; + + $* install config.install.root=tmp; + + tmp/bin/driver >'sub'; + + # Test execution via symlink. + # + mkdir bin; + ln -s tmp/bin/driver bin/driver; + bin/driver >'sub'; + + # Test execution via PATH. + # + #@@ TODO: add $~/bin to the PATH environment variable. + #driver >'sub'; + + $* uninstall config.install.root=tmp; + $* clean + } + + #\ + : import-installed + : + { + # Note that here we import the project as sub, not sub.bash in order + # to avoid importing as a subproject. + # + cat <=driver.in; + #!/usr/bin/env bash + @import sub/foo@ + EOI + + cat <=buildfile; + import mods = sub%bash{foo} + exe{driver}: in{driver} $mods + EOI + + $* 'install(../../sub.bash/)' config.install.root=tmp; + + #@@ TODO: add $~/tmp/bin to the PATH environment variable. + $* test clean >'sub'; + $* clean; + + $* 'uninstall(../../sub.bash/)' config.install.root=tmp + } + #\ + } +} diff --git a/tests/common.test b/tests/common.test index a1c9316..586e793 100644 --- a/tests/common.test +++ b/tests/common.test @@ -32,7 +32,11 @@ project = test amalgamation = EOI -test.options += --serial-stop --quiet --buildfile - +test.options += --serial-stop --quiet + +if ($null($buildfile) || !$buildfile) + test.options += --buildfile - +end # By default just load the buildfile. # -- cgit v1.1