aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2020-03-20 09:51:01 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2020-03-20 09:51:01 +0200
commitda89d944bbc3cca9fd36a4a360f94023134a9a8c (patch)
tree1c31738b128b0628ff2e82817e43df9f9461beb0
parentce29e3d72ded432b9ac9354ac92c588142de9b89 (diff)
Initial implementation of config directive for project-specific configuration
-rw-r--r--libbuild2/parser.cxx136
-rw-r--r--libbuild2/parser.hxx3
-rw-r--r--libbuild2/scope.hxx12
-rw-r--r--tests/common.testscript2
-rw-r--r--tests/directive/config.testscript164
5 files changed, 310 insertions, 7 deletions
diff --git a/libbuild2/parser.cxx b/libbuild2/parser.cxx
index ec5e161..f20a4c4 100644
--- a/libbuild2/parser.cxx
+++ b/libbuild2/parser.cxx
@@ -20,6 +20,8 @@
#include <libbuild2/diagnostics.hxx>
#include <libbuild2/prerequisite.hxx>
+#include <libbuild2/config/utility.hxx> // lookup_config
+
using namespace std;
using namespace butl;
@@ -411,6 +413,10 @@ namespace build2
{
f = &parser::parse_for;
}
+ else if (n == "config")
+ {
+ f = &parser::parse_config;
+ }
if (f != nullptr)
{
@@ -1640,6 +1646,134 @@ namespace build2
}
void parser::
+ parse_config (token& t, type& tt)
+ {
+ tracer trace ("parser::parse_config", &path_);
+
+ // General config format:
+ //
+ // config [<var-attrs>] <var>[?=[<val-attrs>]<default-val>]
+ //
+
+ // @@ TODO: enforce appears in root.build
+ //
+ if (root_ != scope_)
+ fail (t) << "configuration variable in non-root scope";
+
+ // We enforce the config.<project> prefix.
+ //
+ // Note that this could be a subproject and it could be unnamed (e.g., the
+ // tests subproject). The current thinking is to use hierarchical names
+ // like config.<project>.tests.remote for subprojects, similar to how we
+ // do the same for submodules (e.g., cxx.config). Of course, the
+ // subproject could also be some named third-party top-level project that
+ // we just happened to amalgamate. So what we are going to do is enforce
+ // the config[.**].<project>.** pattern where <project> is the innermost
+ // named project.
+ //
+ string proj;
+ for (auto r (root_), a (root_->strong_scope ());
+ ;
+ r = r->parent_scope ()->root_scope ())
+ {
+ const project_name& n (project (*r));
+ if (!n.empty ())
+ {
+ proj = n.variable ();
+ break;
+ }
+
+ if (r == a)
+ break;
+ }
+
+ if (proj.empty ())
+ fail (t) << "configuration variable in unnamed project";
+
+ // We are now in the normal lexing mode. Since we always have <var> we
+ // don't have to resort to manual parsing (as in import) and can just let
+ // the lexer handle `?=`.
+ //
+ next_with_attributes (t, tt);
+
+ // Get variable attributes, if any.
+ //
+ attributes_push (t, tt);
+
+ if (tt != type::word)
+ fail (t) << "expected configuration variable name instead of " << t;
+
+ string name (move (t.value));
+
+ // Enforce the variable name pattern. The simplest is to check for the
+ // config prefix and the project substring.
+ //
+ {
+ diag_record dr;
+
+ if (name.compare (0, 7, "config.") != 0)
+ dr << fail (t) << "configuration variable '" << name
+ << "' does not start with 'config.'";
+
+ if (name.find ('.' + proj + '.') == string::npos)
+ dr << fail (t) << "configuration variable '" << name
+ << "' does not include project name";
+
+ if (!dr.empty ())
+ dr << info << "expected variable name in the 'config[.**]." << proj
+ << ".**' form";
+ }
+
+ const variable& var (
+ scope_->var_pool ().insert (move (name), true /* overridable */));
+
+ apply_variable_attributes (var);
+
+ // Note that even though we are relying on the config.** variable pattern
+ // to set global visibility, let's make sure as a sanity check.
+ //
+ if (var.visibility != variable_visibility::normal)
+ {
+ fail (t) << "configuration variable " << var << " has " << var.visibility
+ << " visibility";
+ }
+
+ // We have to lookup the value whether we have the default part or not in
+ // order to mark it as saved (we would also have to do this to get the new
+ // value status if we need it).
+ //
+ using config::lookup_config;
+
+ auto l (lookup_config (*root_, var));
+
+ // See if we have the default value part.
+ //
+ next (t, tt);
+
+ if (tt != type::newline && tt != type::eos)
+ {
+ if (tt != type::default_assign)
+ fail (t) << "expected '?=' instead of " << t << " after configuration "
+ << "variable name";
+
+ // The rest is the default value which we should parse in the value
+ // mode. But before switching check whether we need to evaluate it at
+ // all.
+ //
+ if (l.defined ())
+ skip_line (t, tt);
+ else
+ {
+ value lhs, rhs (parse_variable_value (t, tt));
+ apply_value_attributes (&var, lhs, move (rhs), type::assign);
+ lookup_config (*root_, var, move (lhs));
+ }
+ }
+
+ next_after_newline (t, tt);
+ }
+
+ void parser::
parse_import (token& t, type& tt)
{
tracer trace ("parser::parse_import", &path_);
@@ -1653,7 +1787,7 @@ namespace build2
//
type atype; // Assignment type.
value* val (nullptr);
- const build2::variable* var (nullptr);
+ const variable* var (nullptr);
// We are now in the normal lexing mode and here is the problem: we need
// to switch to the value mode so that we don't treat certain characters
diff --git a/libbuild2/parser.hxx b/libbuild2/parser.hxx
index f1cd976..dd5cbda 100644
--- a/libbuild2/parser.hxx
+++ b/libbuild2/parser.hxx
@@ -128,6 +128,9 @@ namespace build2
parse_run (token&, token_type&);
void
+ parse_config (token&, token_type&);
+
+ void
parse_import (token&, token_type&);
void
diff --git a/libbuild2/scope.hxx b/libbuild2/scope.hxx
index c49778f..fdc660d 100644
--- a/libbuild2/scope.hxx
+++ b/libbuild2/scope.hxx
@@ -58,11 +58,11 @@ namespace build2
scope* root_scope () {return root_;}
const scope* root_scope () const {return root_;}
- // Root scope of a strong amalgamation of this scope or NULL if
- // this scope is not (yet) in any (known) project. If there is
- // no strong amalgamation, then this function returns the root
- // scope of the project (in other words, in this case a project
- // is treated as its own strong amalgamation).
+ // Root scope of the outermost "strong" (source-based) amalgamation of
+ // this scope or NULL if this scope is not (yet) in any (known) project.
+ // If there is no strong amalgamation, then this function returns the root
+ // scope of the project (in other words, in this case a project is treated
+ // as its own strong amalgamation).
//
scope* strong_scope ();
const scope* strong_scope () const;
@@ -491,7 +491,7 @@ namespace build2
out_src (const dir_path& src,
const dir_path& out_root, const dir_path& src_root);
- // Return the project name or empty string if unnamed.
+ // Return the project name or empty if unnamed.
//
const project_name&
project (const scope& root);
diff --git a/tests/common.testscript b/tests/common.testscript
index b3f0393..9a8ecd8 100644
--- a/tests/common.testscript
+++ b/tests/common.testscript
@@ -34,6 +34,8 @@ EOI
test.options += --no-default-options --serial-stop --quiet
+# By default read stdin for the buildfile.
+#
if ($null($buildfile) || !$buildfile)
test.options += --buildfile -
end
diff --git a/tests/directive/config.testscript b/tests/directive/config.testscript
new file mode 100644
index 0000000..d10f45d
--- /dev/null
+++ b/tests/directive/config.testscript
@@ -0,0 +1,164 @@
+# file : tests/directive/config.testscript
+# license : MIT; see accompanying LICENSE file
+
+buildfile = true
+test.arguments =
+
+: default-value
+:
+{
+ .include ../common.testscript
+
+ +cat <<EOI >+build/bootstrap.build
+ using config
+ EOI
+
+ +cat <<EOI >=build/root.build
+ config [bool] config.test.fancy ?= false
+ print ($defined(config.test.fancy) ? $config.test.fancy : undefined)
+ EOI
+
+ # This must be a single, serial test since we are sharing config.build.
+ #
+ : test
+ :
+ cat <<EOI >=buildfile;
+ ./:
+ EOI
+
+ # Unconfigured.
+ #
+ $* noop >'false' ;
+ $* noop config.test.fancy=true >'true' ;
+
+ # Configured as default.
+ #
+ $* configure >'false' ;
+ cat ../build/config.build >>~/EOO/ ;
+ /.*
+ config.test.fancy = false
+ /.*
+ EOO
+ $* disfigure ;
+
+ # Configured as specified.
+ #
+ $* configure config.test.fancy=true >'true' ;
+ $* noop >'true' ;
+ $* noop config.test.fancy=false >'false' ;
+ $* configure config.test.fancy=false >'false' ;
+ $* noop >'false' ;
+ $* configure config.test.fancy=true >'true' ;
+ $* disfigure ;
+ $* noop >'false' ;
+
+ $* noop config.test.fancy=[null] >'[null]';
+
+ $* noop config.test.fancy=junk 2>>EOE != 0
+ error: invalid bool value 'junk' in variable config.test.fancy
+ EOE
+}
+
+: default-null
+:
+{
+ .include ../common.testscript
+
+ +cat <<EOI >+build/bootstrap.build
+ using config
+ EOI
+
+ +cat <<EOI >=build/root.build
+ config [bool] config.test.fancy ?= [null]
+ print ($defined(config.test.fancy) ? $config.test.fancy : undefined)
+ EOI
+
+ # This must be a single, serial test since we are sharing config.build.
+ #
+ : test
+ :
+ cat <<EOI >=buildfile;
+ ./:
+ EOI
+
+ # Unconfigured.
+ #
+ $* noop >'[null]';
+ $* noop config.test.fancy=true >'true' ;
+
+ # Configured as default.
+ #
+ $* configure >'[null]';
+ cat ../build/config.build >>~/EOO/ ;
+ /.*
+ config.test.fancy = [null]
+ /.*
+ EOO
+ $* disfigure ;
+
+ # Configured as specified.
+ #
+ $* configure config.test.fancy=true >'true' ;
+ $* noop >'true' ;
+ $* noop config.test.fancy=false >'false' ;
+ $* noop config.test.fancy=[null] >'[null]';
+ $* configure config.test.fancy=false >'false' ;
+ $* noop >'false' ;
+ $* disfigure ;
+ $* noop >'[null]';
+
+ $* noop config.test.fancy=junk 2>>EOE != 0
+ error: invalid bool value 'junk' in variable config.test.fancy
+ EOE
+}
+
+
+: default-none
+:
+{
+ .include ../common.testscript
+
+ +cat <<EOI >+build/bootstrap.build
+ using config
+ EOI
+
+ +cat <<EOI >=build/root.build
+ config [bool] config.test.fancy
+ print ($defined(config.test.fancy) ? $config.test.fancy : undefined)
+ EOI
+
+ # This must be a single, serial test since we are sharing config.build.
+ #
+ : test
+ :
+ cat <<EOI >=buildfile;
+ ./:
+ EOI
+
+ # Unconfigured.
+ #
+ $* noop >'undefined' ;
+ $* noop config.test.fancy=true >'true' ;
+
+ # Configured as default.
+ #
+ $* configure >'undefined' ;
+ sed -n -e 's/(config.test.fancy)/\1/p' ../build/config.build ;
+ $* disfigure ;
+
+ # Configured as specified.
+ #
+ $* configure config.test.fancy=true >'true' ;
+ $* noop >'true' ;
+ $* noop config.test.fancy=false >'false' ;
+ $* configure config.test.fancy=false >'false' ;
+ $* noop >'false' ;
+ $* disfigure ;
+ $* noop >'undefined' ;
+
+ $* noop config.test.fancy=[null] >'[null]';
+
+ $* noop config.test.fancy=junk 2>>EOE != 0
+ error: invalid bool value 'junk' in variable config.test.fancy
+ EOE
+}