From 930ae81f85e5ee5ac37311f5e9c89000fbcb59a6 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Thu, 1 Aug 2019 16:17:09 +0300 Subject: Move bash build system module to separate library --- bootstrap.sh | 2 - build2/b.cxx | 13 +- build2/bash/init.cxx | 89 --------- build2/bash/init.hxx | 28 --- build2/bash/rule.cxx | 442 --------------------------------------------- build2/bash/rule.hxx | 89 --------- build2/bash/target.cxx | 30 --- build2/bash/target.hxx | 32 ---- build2/bash/utility.hxx | 27 --- build2/buildfile | 2 +- libbuild2/bash/buildfile | 65 +++++++ libbuild2/bash/export.hxx | 34 ++++ libbuild2/bash/init.cxx | 104 +++++++++++ libbuild2/bash/init.hxx | 28 +++ libbuild2/bash/rule.cxx | 442 +++++++++++++++++++++++++++++++++++++++++++++ libbuild2/bash/rule.hxx | 91 ++++++++++ libbuild2/bash/target.cxx | 30 +++ libbuild2/bash/target.hxx | 34 ++++ libbuild2/bash/utility.hxx | 27 +++ libbuild2/buildfile | 2 +- tests/libbuild2/buildfile | 7 +- tests/libbuild2/driver.cxx | 4 +- 22 files changed, 871 insertions(+), 751 deletions(-) delete mode 100644 build2/bash/init.cxx delete mode 100644 build2/bash/init.hxx delete mode 100644 build2/bash/rule.cxx delete mode 100644 build2/bash/rule.hxx delete mode 100644 build2/bash/target.cxx delete mode 100644 build2/bash/target.hxx delete mode 100644 build2/bash/utility.hxx create mode 100644 libbuild2/bash/buildfile create mode 100644 libbuild2/bash/export.hxx create mode 100644 libbuild2/bash/init.cxx create mode 100644 libbuild2/bash/init.hxx create mode 100644 libbuild2/bash/rule.cxx create mode 100644 libbuild2/bash/rule.hxx create mode 100644 libbuild2/bash/target.cxx create mode 100644 libbuild2/bash/target.hxx create mode 100644 libbuild2/bash/utility.hxx diff --git a/bootstrap.sh b/bootstrap.sh index 8de5e3c..6eb570f 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -121,8 +121,6 @@ src="$src build2/bin/*.cxx" src="$src build2/c/*.cxx" src="$src build2/cc/*.cxx" src="$src build2/cxx/*.cxx" -src="$src build2/cli/*.cxx" -src="$src build2/bash/*.cxx" src="$src libbuild2/*.cxx" src="$src libbuild2/config/*.cxx" diff --git a/build2/b.cxx b/build2/b.cxx index a016278..2bb5920 100644 --- a/build2/b.cxx +++ b/build2/b.cxx @@ -54,16 +54,17 @@ #include #include +#ifndef BUILD2_BOOTSTRAP +# include + +# include +#endif + #include #include #include #include -#ifndef BUILD2_BOOTSTRAP -# include -# include -#endif - using namespace butl; using namespace std; @@ -481,7 +482,7 @@ main (int argc, char* argv[]) bm["cli.config"] = mf {"cli.config", nullptr, &cli::config_init}; bm["cli"] = mf {"cli", nullptr, &cli::init}; - bm["bash"] = mf {"bash", nullptr, &bash::init}; + reg (&bash::build2_bash_load); #endif } diff --git a/build2/bash/init.cxx b/build2/bash/init.cxx deleted file mode 100644 index 146e680..0000000 --- a/build2/bash/init.cxx +++ /dev/null @@ -1,89 +0,0 @@ -// file : build2/bash/init.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 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;}); - - // Load in.base (in.* variables, 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 project_name& 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 deleted file mode 100644 index 2a7e95c..0000000 --- a/build2/bash/init.hxx +++ /dev/null @@ -1,28 +0,0 @@ -// file : build2/bash/init.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 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 deleted file mode 100644 index a612d23..0000000 --- a/build2/bash/rule.cxx +++ /dev/null @@ -1,442 +0,0 @@ -// file : build2/bash/rule.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 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"); - - // 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 ()); // 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_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 ()); - - // 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_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 ()); - 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 ()) - { - 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 ()); - - 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 ()) - { - const target& pt (search (t, p)); - return pt.in (t.weak_scope ()) ? &pt : nullptr; - } - else - return file_rule::filter (a, t, p); - } - } -} diff --git a/build2/bash/rule.hxx b/build2/bash/rule.hxx deleted file mode 100644 index c160bb7..0000000 --- a/build2/bash/rule.hxx +++ /dev/null @@ -1,89 +0,0 @@ -// file : build2/bash/rule.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 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{}). Here - // we do: - // - // 1. Signal to in_rule that this is update for install. - // - // 2. Custom filtering of prerequisites. - // - 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; - - virtual const target* - filter (action, const target&, const prerequisite&) const override; - - private: - const in_rule& in_; - }; - } -} - -#endif // BUILD2_BASH_RULE_HXX diff --git a/build2/bash/target.cxx b/build2/bash/target.cxx deleted file mode 100644 index e843d53..0000000 --- a/build2/bash/target.cxx +++ /dev/null @@ -1,30 +0,0 @@ -// file : build2/bash/target.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 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 deleted file mode 100644 index 6be83c4..0000000 --- a/build2/bash/target.hxx +++ /dev/null @@ -1,32 +0,0 @@ -// file : build2/bash/target.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 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 deleted file mode 100644 index 31a6b99..0000000 --- a/build2/bash/utility.hxx +++ /dev/null @@ -1,27 +0,0 @@ -// file : build2/bash/utility.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 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. - // - // Note that the result may not be a valid project name. - // - inline string - project_base (const project_name& pn) - { - return pn.base ("bash"); - } - } -} - -#endif // BUILD2_BASH_UTILITY_HXX diff --git a/build2/buildfile b/build2/buildfile index ea39e87..57c2561 100644 --- a/build2/buildfile +++ b/build2/buildfile @@ -8,7 +8,7 @@ import libs += libpkgconf%lib{pkgconf} include ../libbuild2/ libs += ../libbuild2/lib{build2} -for m: version in +for m: bash in version { include ../libbuild2/$m/ libs += ../libbuild2/$m/lib{build2-$m} diff --git a/libbuild2/bash/buildfile b/libbuild2/bash/buildfile new file mode 100644 index 0000000..e1a9f53 --- /dev/null +++ b/libbuild2/bash/buildfile @@ -0,0 +1,65 @@ +# file : libbuild2/bash/buildfile +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import int_libs = libbutl%lib{butl} + +include ../ +int_libs += ../lib{build2} + +include ../in/ +int_libs += ../in/lib{build2-in} + +./: lib{build2-bash}: libul{build2-bash}: {hxx ixx txx cxx}{** -**.test...} \ + $int_libs + +# Unit tests. +# +exe{*.test}: +{ + test = true + install = false +} + +for t: cxx{**.test...} +{ + d = $directory($t) + n = $name($t)... + + ./: $d/exe{$n}: $t $d/{hxx ixx txx}{+$n} $d/testscript{+$n} + $d/exe{$n}: libul{build2-bash}: bin.whole = false +} + +# Build options. +# +obja{*}: cxx.poptions += -DLIBBUILD2_BASH_STATIC_BUILD +objs{*}: cxx.poptions += -DLIBBUILD2_BASH_SHARED_BUILD + +# Export options. +# +lib{build2-bash}: +{ + cxx.export.poptions = "-I$out_root" "-I$src_root" + cxx.export.libs = $int_libs +} + +liba{build2-bash}: cxx.export.poptions += -DLIBBUILD2_BASH_STATIC +libs{build2-bash}: cxx.export.poptions += -DLIBBUILD2_BASH_SHARED + +# For pre-releases use the complete version to make sure they cannot be used +# in place of another pre-release or the final version. See the version module +# for details on the version.* variable values. +# +if $version.pre_release + lib{build2-bash}: bin.lib.version = @"-$version.project_id" +else + lib{build2-bash}: bin.lib.version = @"-$version.major.$version.minor" + +# Install into the libbuild2/bash/ subdirectory of, say, /usr/include/ +# recreating subdirectories. +# +{hxx ixx txx}{*}: +{ + install = include/libbuild2/bash/ + install.subdirs = true +} diff --git a/libbuild2/bash/export.hxx b/libbuild2/bash/export.hxx new file mode 100644 index 0000000..d87e677 --- /dev/null +++ b/libbuild2/bash/export.hxx @@ -0,0 +1,34 @@ +#pragma once + +// Normally we don't export class templates (but do complete specializations), +// inline functions, and classes with only inline member functions. Exporting +// classes that inherit from non-exported/imported bases (e.g., std::string) +// will end up badly. The only known workarounds are to not inherit or to not +// export. Also, MinGW GCC doesn't like seeing non-exported functions being +// used before their inline definition. The workaround is to reorder code. In +// the end it's all trial and error. + +#if defined(LIBBUILD2_BASH_STATIC) // Using static. +# define LIBBUILD2_BASH_SYMEXPORT +#elif defined(LIBBUILD2_BASH_STATIC_BUILD) // Building static. +# define LIBBUILD2_BASH_SYMEXPORT +#elif defined(LIBBUILD2_BASH_SHARED) // Using shared. +# ifdef _WIN32 +# define LIBBUILD2_BASH_SYMEXPORT __declspec(dllimport) +# else +# define LIBBUILD2_BASH_SYMEXPORT +# endif +#elif defined(LIBBUILD2_BASH_SHARED_BUILD) // Building shared. +# ifdef _WIN32 +# define LIBBUILD2_BASH_SYMEXPORT __declspec(dllexport) +# else +# define LIBBUILD2_BASH_SYMEXPORT +# endif +#else +// If none of the above macros are defined, then we assume we are being used +// by some third-party build system that cannot/doesn't signal the library +// type. Note that this fallback works for both static and shared but in case +// of shared will be sub-optimal compared to having dllimport. +// +# define LIBBUILD2_BASH_SYMEXPORT // Using static or shared. +#endif diff --git a/libbuild2/bash/init.cxx b/libbuild2/bash/init.cxx new file mode 100644 index 0000000..17c9ddd --- /dev/null +++ b/libbuild2/bash/init.cxx @@ -0,0 +1,104 @@ +// file : libbuild2/bash/init.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 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;}); + + // Load in.base (in.* variables, 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 project_name& 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; + } + + static const module_functions mod_functions[] = + { + // NOTE: don't forget to also update the documentation in init.hxx if + // changing anything here. + + {"bash", nullptr, init}, + {nullptr, nullptr, nullptr} + }; + + const module_functions* + build2_bash_load () + { + return mod_functions; + } + } +} diff --git a/libbuild2/bash/init.hxx b/libbuild2/bash/init.hxx new file mode 100644 index 0000000..4d05f2d --- /dev/null +++ b/libbuild2/bash/init.hxx @@ -0,0 +1,28 @@ +// file : libbuild2/bash/init.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_BASH_INIT_HXX +#define LIBBUILD2_BASH_INIT_HXX + +#include +#include + +#include + +#include + +namespace build2 +{ + namespace bash + { + // Module `bash` does not require bootstrapping. + // + // No submodules. + // + extern "C" LIBBUILD2_BASH_SYMEXPORT const module_functions* + build2_bash_load (); + } +} + +#endif // LIBBUILD2_BASH_INIT_HXX 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 + +#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"); + + // 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 ()); // 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_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 ()); + + // 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_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 ()); + 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 ()) + { + 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 ()); + + 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 ()) + { + const target& pt (search (t, p)); + return pt.in (t.weak_scope ()) ? &pt : nullptr; + } + else + return file_rule::filter (a, t, p); + } + } +} diff --git a/libbuild2/bash/rule.hxx b/libbuild2/bash/rule.hxx new file mode 100644 index 0000000..665a6c2 --- /dev/null +++ b/libbuild2/bash/rule.hxx @@ -0,0 +1,91 @@ +// file : libbuild2/bash/rule.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_BASH_RULE_HXX +#define LIBBUILD2_BASH_RULE_HXX + +#include +#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 LIBBUILD2_BASH_SYMEXPORT 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{}). Here + // we do: + // + // 1. Signal to in_rule that this is update for install. + // + // 2. Custom filtering of prerequisites. + // + class LIBBUILD2_BASH_SYMEXPORT 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; + + virtual const target* + filter (action, const target&, const prerequisite&) const override; + + protected: + const in_rule& in_; + }; + } +} + +#endif // LIBBUILD2_BASH_RULE_HXX diff --git a/libbuild2/bash/target.cxx b/libbuild2/bash/target.cxx new file mode 100644 index 0000000..7313316 --- /dev/null +++ b/libbuild2/bash/target.cxx @@ -0,0 +1,30 @@ +// file : libbuild2/bash/target.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 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/libbuild2/bash/target.hxx b/libbuild2/bash/target.hxx new file mode 100644 index 0000000..af8b32c --- /dev/null +++ b/libbuild2/bash/target.hxx @@ -0,0 +1,34 @@ +// file : libbuild2/bash/target.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_BASH_TARGET_HXX +#define LIBBUILD2_BASH_TARGET_HXX + +#include +#include + +#include + +#include + +namespace build2 +{ + namespace bash + { + // Bash module file to be sourced by a script. The default/standard + // extension is .bash. + // + class LIBBUILD2_BASH_SYMEXPORT 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 // LIBBUILD2_BASH_TARGET_HXX diff --git a/libbuild2/bash/utility.hxx b/libbuild2/bash/utility.hxx new file mode 100644 index 0000000..1f981c2 --- /dev/null +++ b/libbuild2/bash/utility.hxx @@ -0,0 +1,27 @@ +// file : libbuild2/bash/utility.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_BASH_UTILITY_HXX +#define LIBBUILD2_BASH_UTILITY_HXX + +#include +#include + +namespace build2 +{ + namespace bash + { + // Strip the .bash extension from the project name. + // + // Note that the result may not be a valid project name. + // + inline string + project_base (const project_name& pn) + { + return pn.base ("bash"); + } + } +} + +#endif // LIBBUILD2_BASH_UTILITY_HXX diff --git a/libbuild2/buildfile b/libbuild2/buildfile index ab95098..377ea30 100644 --- a/libbuild2/buildfile +++ b/libbuild2/buildfile @@ -2,7 +2,7 @@ # copyright : Copyright (c) 2014-2019 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -./: lib{build2} version/ in/ +./: lib{build2} bash/ in/ version/ import int_libs = libbutl%lib{butl} diff --git a/tests/libbuild2/buildfile b/tests/libbuild2/buildfile index aa6c17f..bd22b4c 100644 --- a/tests/libbuild2/buildfile +++ b/tests/libbuild2/buildfile @@ -2,9 +2,10 @@ # copyright : Copyright (c) 2014-2019 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -import libs = build2%lib{build2} -import libs += build2%lib{build2-in} -import libs += build2%lib{build2-version} +import libs = build2%lib{build2} + +for m: bash in version + import libs += build2%lib{build2-$m} exe{driver}: {hxx cxx}{*} $libs testscript diff --git a/tests/libbuild2/driver.cxx b/tests/libbuild2/driver.cxx index 544d43e..20c0ec2 100644 --- a/tests/libbuild2/driver.cxx +++ b/tests/libbuild2/driver.cxx @@ -9,6 +9,7 @@ #include #include +#include #include using namespace build2; @@ -21,8 +22,9 @@ main (int, char* argv[]) init_diag (1); init (argv[0]); - version::build2_version_load (); + bash::build2_bash_load (); in::build2_in_load (); + version::build2_version_load (); sched.startup (1); // Serial execution. reset (strings ()); // No command line variables. -- cgit v1.1