From da89d944bbc3cca9fd36a4a360f94023134a9a8c Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Fri, 20 Mar 2020 09:51:01 +0200 Subject: Initial implementation of config directive for project-specific configuration --- libbuild2/parser.cxx | 136 ++++++++++++++++++++++++++++++- libbuild2/parser.hxx | 3 + libbuild2/scope.hxx | 12 +-- tests/common.testscript | 2 + tests/directive/config.testscript | 164 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 310 insertions(+), 7 deletions(-) create mode 100644 tests/directive/config.testscript 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 #include +#include // 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 [] [?=[]] + // + + // @@ TODO: enforce appears in root.build + // + if (root_ != scope_) + fail (t) << "configuration variable in non-root scope"; + + // We enforce the config. 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..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[.**]..** pattern where 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 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 <+build/bootstrap.build + using config + EOI + + +cat <=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 <=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 <+build/bootstrap.build + using config + EOI + + +cat <=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 <=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 <+build/bootstrap.build + using config + EOI + + +cat <=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 <=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 +} -- cgit v1.1