aboutsummaryrefslogtreecommitdiff
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
parentb4286df8c9bafdab1934cef99ccc0506ec4555e2 (diff)
Implement bash module
-rw-r--r--bootstrap-mingw.bat1
-rw-r--r--bootstrap-msvc.bat1
-rw-r--r--bootstrap.gmake3
-rwxr-xr-xbootstrap.sh1
-rw-r--r--build2/algorithm.cxx33
-rw-r--r--build2/algorithm.hxx40
-rw-r--r--build2/algorithm.ixx57
-rw-r--r--build2/b.cxx4
-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
-rw-r--r--build2/in/init.cxx2
-rw-r--r--build2/in/rule.cxx155
-rw-r--r--build2/in/rule.hxx23
-rw-r--r--build2/in/target.cxx2
-rw-r--r--build2/version/rule.cxx17
-rw-r--r--build2/version/rule.hxx7
-rw-r--r--tests/bash/buildfile5
-rw-r--r--tests/bash/testscript223
-rw-r--r--tests/common.test6
24 files changed, 1138 insertions, 128 deletions
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 <typename R>
+ template <typename R, typename S>
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 <typename T>
@@ -1715,7 +1724,7 @@ namespace build2
pair<optional<target_state>, 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<bool (const target&, size_t pos)>;
+ using execute_filter = function<bool (const target&, size_t pos)>;
optional<target_state>
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<optional<target_state>, const T&>
execute_prerequisites (action, const target&,
const timestamp&,
- const prerequisite_filter& = nullptr,
+ const execute_filter& = nullptr,
size_t count = 0);
pair<optional<target_state>, 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 <typename T>
@@ -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<optional<target_state>, const target*>
execute_prerequisites (const target_type*,
action, const target&,
- const timestamp&, const prerequisite_filter&,
+ const timestamp&, const execute_filter&,
size_t);
inline optional<target_state>
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 <typename T>
inline pair<optional<target_state>, 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<optional<target_state>, const T&> (
p.first, static_cast<const T&> (p.second));
}
@@ -682,10 +699,10 @@ namespace build2
inline pair<optional<target_state>, 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<optional<target_state>, const target&> (p.first, *p.second);
}
@@ -693,10 +710,10 @@ namespace build2
inline pair<optional<target_state>, 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<optional<target_state>, const T&> (
p.first, static_cast<const T&> (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 <build2/cli/init.hxx>
+#include <build2/bash/init.hxx>
+
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 <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
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<string> (
- v.type == nullptr
- ? move (v)
- : functions.call (&t.base_scope (),
- "string",
- vector_view<value> (&v, 1),
- l));
- }
- catch (const invalid_argument& e)
- {
- fail (l) << e <<
- info << "while substituting '" << n << "'" << endf;
- }
- }
- else
- fail (l) << "undefined variable '" << n << "'" << endf;
- }
-
- optional<string> 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 $<name>; 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<string> 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<string> val = substitute (l, t, name, strict))
+ if (optional<string> 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<string> (
+ v.type == nullptr
+ ? move (v)
+ : functions.call (&t.base_scope (),
+ "string",
+ vector_view<value> (&v, 1),
+ l));
+ }
+ catch (const invalid_argument& e)
+ {
+ fail (l) << e <<
+ info << "while substituting '" << n << "'" << endf;
+ }
+ }
+ else
+ fail (l) << "undefined variable '" << n << "'" << endf;
+ }
+
+ optional<string> 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 $<name>; 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<string>
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 <build2/version/rule.hxx>
-#include <build2/depdb.hxx>
#include <build2/scope.hxx>
#include <build2/target.hxx>
-#include <build2/context.hxx>
-#include <build2/function.hxx>
-#include <build2/algorithm.hxx>
-#include <build2/filesystem.hxx>
#include <build2/diagnostics.hxx>
#include <build2/in/target.hxx>
@@ -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 <<EOI >+build/bootstrap.build
+ using test
+ using install
+ EOI
+
+ +cat <<EOI >=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 <<EOI >=sub.bash/build/bootstrap.build
+ project = sub.bash
+ using install
+ EOI
+ +cat <<EOI >=sub.bash/build/root.build
+ using bash
+ EOI
+ +cat <<EOI >=sub.bash/build/export.build
+ $out_root/
+ {
+ include sub/
+ }
+ export $src_root/sub/bash{foo}
+ EOI
+ +cat <<EOI >=sub.bash/buildfile
+ ./: dir{*/ -build/}
+ EOI
+ +mkdir sub.bash/sub
+ +cat <<EOI >=sub.bash/sub/foo.bash
+ echo sub
+ EOI
+ +cat <<EOI >=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 <<EOI >=hello.bash;
+ function hello () { echo "Hello, $@!"; }
+ EOI
+
+ cat <<EOI >=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 <<EOI >=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 <<EOI >=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 <<EOI >=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 <<EOI >=test.bash.in;
+ @import sub/foo@
+ EOI
+
+ cat <<EOI >=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 <<EOI >=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 <<EOI >=driver.in;
+ #!/usr/bin/env bash
+ @import sub/foo@
+ EOI
+
+ cat <<EOI >=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.
#