aboutsummaryrefslogtreecommitdiff
path: root/build2/bash
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2018-07-20 15:31:13 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2018-07-20 16:10:26 +0200
commitc1dc06dfd1d329f8c6499dbe2166725ab9c35e17 (patch)
tree97449ae7263490473a833944cfeb3d889a2f580d /build2/bash
parentb4286df8c9bafdab1934cef99ccc0506ec4555e2 (diff)
Implement bash module
Diffstat (limited to 'build2/bash')
-rw-r--r--build2/bash/init.cxx89
-rw-r--r--build2/bash/init.hxx28
-rw-r--r--build2/bash/rule.cxx398
-rw-r--r--build2/bash/rule.hxx81
-rw-r--r--build2/bash/target.cxx30
-rw-r--r--build2/bash/target.hxx32
-rw-r--r--build2/bash/utility.hxx28
7 files changed, 686 insertions, 0 deletions
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 <build2/bash/init.hxx>
+
+#include <build2/scope.hxx>
+#include <build2/context.hxx>
+#include <build2/variable.hxx>
+#include <build2/diagnostics.hxx>
+
+#include <build2/install/utility.hxx>
+
+#include <build2/bash/rule.hxx>
+#include <build2/bash/target.hxx>
+#include <build2/bash/utility.hxx>
+
+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<module_base>&,
+ 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<bool> (rs["in.base.loaded"]))
+ load_module (rs, rs, "in.base", l);
+
+ bool install_loaded (cast_false<bool> (rs["install.loaded"]));
+
+ // Register target types and configure default installability.
+ //
+ bs.target_types.insert<bash> ();
+
+ if (install_loaded)
+ {
+ using namespace install;
+
+ // Install into bin/<project>/ by default stripping the .bash
+ // extension from <project> if present.
+ //
+ const string& p (cast<string> (rs.vars[var_project]));
+
+ install_path<bash> (bs, dir_path ("bin") /= project_base (p));
+ install_mode<bash> (bs, "644");
+ }
+
+ // Register rules.
+ //
+ {
+ auto& r (bs.rules);
+
+ r.insert<exe> (perform_update_id, "bash.in", in_rule_);
+ r.insert<exe> (perform_clean_id, "bash.in", in_rule_);
+ r.insert<exe> (configure_update_id, "bash.in", in_rule_);
+
+ r.insert<bash> (perform_update_id, "bash.in", in_rule_);
+ r.insert<bash> (perform_clean_id, "bash.in", in_rule_);
+ r.insert<bash> (configure_update_id, "bash.in", in_rule_);
+
+ if (install_loaded)
+ {
+ r.insert<exe> (perform_install_id, "bash.install", install_rule_);
+ r.insert<exe> (perform_uninstall_id, "bash.uninstall", install_rule_);
+
+ r.insert<bash> (perform_install_id, "bash.install", install_rule_);
+ r.insert<bash> (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 <build2/types.hxx>
+#include <build2/utility.hxx>
+
+#include <build2/module.hxx>
+
+namespace build2
+{
+ namespace bash
+ {
+ bool
+ init (scope&,
+ scope&,
+ const location&,
+ unique_ptr<module_base>&,
+ 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 <build2/bash/rule.hxx>
+
+#include <cstring> // strlen(), strchr()
+
+#include <build2/scope.hxx>
+#include <build2/target.hxx>
+#include <build2/algorithm.hxx>
+#include <build2/diagnostics.hxx>
+
+#include <build2/in/target.hxx>
+
+#include <build2/bash/target.hxx>
+#include <build2/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");
+
+ 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<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::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::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.
+ //
+ // 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<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;
+ }
+ }
+}
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 <build2/types.hxx>
+#include <build2/utility.hxx>
+
+#include <build2/in/rule.hxx>
+#include <build2/install/rule.hxx>
+
+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<string>
+ 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 <build2/bash/target.hxx>
+
+#include <build2/context.hxx>
+
+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<bash>,
+ nullptr, /* fixed_extension */
+ &target_extension_var<var_extension, bash_ext_def>,
+ &target_pattern_var<var_extension, bash_ext_def>,
+ 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 <build2/types.hxx>
+#include <build2/utility.hxx>
+
+#include <build2/target.hxx>
+
+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 <build2/types.hxx>
+#include <build2/utility.hxx>
+
+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