From 920ed11a433b0e292a18adb8c68829a00e8c70cc Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Tue, 26 May 2020 21:35:59 +0300 Subject: Allow process path values and targets as buildscript program names Also deduce the recipe name. --- libbuild2/build/script/parser+line.test.testscript | 4 +- libbuild2/build/script/parser.cxx | 423 ++++++++++++++++++++- libbuild2/build/script/parser.hxx | 47 ++- libbuild2/build/script/parser.test.cxx | 9 +- libbuild2/build/script/runner.cxx | 10 +- libbuild2/build/script/script.hxx | 1 - libbuild2/functions-process.cxx | 7 +- libbuild2/parser.cxx | 4 +- libbuild2/rule.cxx | 21 +- libbuild2/rule.hxx | 6 +- libbuild2/script/parser.cxx | 83 +++- libbuild2/script/parser.hxx | 32 +- libbuild2/script/run.cxx | 56 ++- libbuild2/script/script.cxx | 2 +- libbuild2/script/script.hxx | 7 +- libbuild2/target.hxx | 6 + libbuild2/target.txx | 18 + libbuild2/test/script/parser.test.cxx | 2 +- libbuild2/variable.cxx | 2 + tests/dependency/recipe/testscript | 336 ++++++++++++++-- 20 files changed, 953 insertions(+), 123 deletions(-) diff --git a/libbuild2/build/script/parser+line.test.testscript b/libbuild2/build/script/parser+line.test.testscript index 6401d91..45b07b7 100644 --- a/libbuild2/build/script/parser+line.test.testscript +++ b/libbuild2/build/script/parser+line.test.testscript @@ -38,7 +38,7 @@ $* <>EOF $* <>EOO foo 'bar' "baz" '' "" "$foo" - "foo$" + "foo$bar" "fo"o "foo"\" "foo\\" @@ -55,7 +55,7 @@ $* <>EOO EOI foo 'bar' "baz" '' "" "$foo" - "foo$" + "foo$bar" "foo" "foo\"" "foo\\" diff --git a/libbuild2/build/script/parser.cxx b/libbuild2/build/script/parser.cxx index 81e78f3..2a0555e 100644 --- a/libbuild2/build/script/parser.cxx +++ b/libbuild2/build/script/parser.cxx @@ -3,10 +3,15 @@ #include +#include + +#include + #include #include using namespace std; +using namespace butl; namespace build2 { @@ -21,8 +26,9 @@ namespace build2 // script parser:: - pre_parse (istream& is, const path_name& pn, uint64_t line, - optional diag) + pre_parse (const target& tg, + istream& is, const path_name& pn, uint64_t line, + optional diag_name, const location& diag_loc) { path_ = &pn; @@ -31,13 +37,25 @@ namespace build2 lexer l (is, *path_, line, lexer_mode::command_line); set_lexer (&l); - script s; - s.diag = move (diag); + // The script shouldn't be able to modify the target/scopes. + // + target_ = const_cast (&tg); + scope_ = const_cast (&tg.base_scope ()); + root_ = scope_->root_scope (); + + pbase_ = scope_->src_path_; + script s; script_ = &s; runner_ = nullptr; environment_ = nullptr; + if (diag_name) + { + diag = make_pair (move (*diag_name), diag_loc); + diag_weight = 4; + } + s.start_loc = location (*path_, line, 1); token t (pre_parse_script ()); @@ -46,6 +64,36 @@ namespace build2 s.end_loc = get_location (t); + // Diagnose absent/ambigous script name. + // + { + diag_record dr; + + if (!diag) + { + dr << fail (s.start_loc) + << "unable to deduce low-verbosity script diagnostics name"; + } + else if (diag2) + { + dr << fail (s.start_loc) + << "low-verbosity script diagnostics name is ambiguous" << + info (diag->second) << "could be '" << diag->first << "'" << + info (diag2->second) << "could be '" << diag2->first << "'"; + } + + if (!dr.empty ()) + { + dr << info << "consider specifying it explicitly with the 'diag' " + << "recipe attribute"; + + dr << info << "or provide custom low-verbosity diagnostics with " + << "the 'diag' builtin"; + } + } + + s.diag = move (diag->first); + return s; } @@ -150,16 +198,26 @@ namespace build2 assert (tt == type::newline); - ln.type = lt; - ln.tokens = replay_data (); - script_->lines.push_back (move (ln)); + //@@ TODO: we need to make sure special builtin is the first command. - if (lt == line_type::cmd_if || lt == line_type::cmd_ifn) + // Save the script line, unless this is a special builtin (indicated + // by the replay::stop mode). + // + if (replay_ == replay::save) { - tt = peek (lexer_mode::first_token); + ln.type = lt; + ln.tokens = replay_data (); + script_->lines.push_back (move (ln)); + + if (lt == line_type::cmd_if || lt == line_type::cmd_ifn) + { + tt = peek (lexer_mode::first_token); - pre_parse_if_else (t, tt); + pre_parse_if_else (t, tt); + } } + else + assert (replay_ == replay::stop && lt == line_type::cmd); } void parser:: @@ -244,6 +302,333 @@ namespace build2 // Execute. // + optional parser:: + parse_program (token& t, build2::script::token_type& tt, names& ns) + { + const location l (get_location (t)); + + // Set the current script name if it is not set or its weight is less + // than the new name weight, skipping names with the zero weight. If + // the weight is the same but the name is different then record this + // ambiguity, unless one is already recorded. This ambiguity will be + // reported at the end of the script pre-parsing, unless discarded by + // the name with a greater weight. + // + auto set_diag = [&l, this] (string d, uint8_t w) + { + if (diag_weight < w) + { + diag = make_pair (move (d), l); + diag_weight = w; + diag2 = nullopt; + } + else if (w != 0 && w == diag_weight && d != diag->first && !diag2) + diag2 = make_pair (move (d), l); + }; + + // Handle special builtins. + // + if (pre_parse_) + { + if (tt == type::word && t.value == "diag") + { + // @@ Redo the diag directive handling (save line separately and + // execute later, before script execution). + // + if (diag_weight == 4) + { + fail (script_->start_loc) + << "low-verbosity script diagnostics name is ambiguous" << + info (diag->second) << "could be '" << diag->first << "'" << + info (l) << "could be ''"; + } + + set_diag ("", 4); + + build2::script::parser::parse_program (t, tt, ns); + replay_stop (); + + return nullopt; + } + } + + auto suggest_diag = [this] (const diag_record& dr) + { + dr << info << "consider specifying it explicitly with " + << "the 'diag' recipe attribute"; + + dr << info << "or provide custom low-verbosity diagnostics " + << " with the 'diag' builtin"; + }; + + parse_names_result pr; + { + // During pre-parse, if the script name is not set manually, we + // suspend pre-parse, parse the command names for real and try to + // deduce the script name from the result. Otherwise, we continue to + // pre-parse and bail out after parsing the names. + // + // Note that the later is not just an optimization since expansion + // that wouldn't fail during execution may fail in this special + // mode, for example: + // + // ... + // {{ + // x = true + // ba($x ? r : z) + // }} + // + // v = a b + // ... + // {{ + // v = o + // fo$v + // }} + // + // This is also the reason why we add a diag frame. + // + if (pre_parse_ && diag_weight != 4) + { + pre_parse_ = false; // Make parse_names() perform expansions. + pre_parse_suspended_ = true; + } + + auto df = make_diag_frame ( + [&l, &suggest_diag, this] (const diag_record& dr) + { + if (pre_parse_suspended_) + { + dr << info (l) << "while deducing low-verbosity script " + << "diagnostics name"; + + suggest_diag (dr); + } + }); + + pr = parse_names (t, tt, + ns, + pattern_mode::ignore, + true /* chunk */, + "command line", + nullptr); + + if (pre_parse_suspended_) + { + pre_parse_suspended_ = false; + pre_parse_ = true; + } + + if (pre_parse_ && diag_weight == 4) + return nullopt; + } + + // Try to translate names into a process path, unless there is nothing + // to translate. + // + // We only end up here in the pre-parse mode if we are still searching + // for the script name. + // + if (!pr.not_null || ns.empty ()) + { + if (pre_parse_) + { + diag_record dr (fail (l)); + dr << "unable to deduce low-verbosity script diagnostics name"; + suggest_diag (dr); + } + + return nullopt; + } + + // We have to handle process_path[_ex] and executable target. The + // process_path[_ex] we may have to recognize syntactically because + // of the loss of type, for example: + // + // c = $cxx.path --version + // + // {{ + // $c ... + // }} + // + // This is further complicated by the fact that the first name in + // process_path[_ex] may or may not be a pair (it's not a pair if + // recall and effective paths are the same). If it's not a pair and we + // are dealing with process_path, then we don't need to do anything + // extra -- it will just be treated as normal program path. However, + // if it's process_path_ex, then we may end up with something along + // these lines: + // + // /usr/bin/g++ name@c++ checksum@deadbeef --version + // + // Which is a bit harder to recognize syntactically. So what we are + // going to do is have a separate first pass which reduces the + // syntactic cases to the typed ones. + // + names pp_ns; + if (pr.type == &value_traits::value_type || + pr.type == &value_traits::value_type) + { + pp_ns = move (ns); + ns.clear (); + } + else if (ns[0].file ()) + { + // Find the end of the value. + // + auto b (ns.begin ()), i (b), e (ns.end ()); + for (i += i->pair ? 2 : 1; i != e && i->pair; i += 2) + { + if (!i->simple () || + (i->value != "name" && i->value != "checksum")) + break; + } + + if (b->pair || i != b + 1) // First is a pair or pairs after. + { + pp_ns.insert (pp_ns.end (), + make_move_iterator (b), make_move_iterator (i)); + + ns.erase (b, i); + + pr.type = i != b + 1 + ? &value_traits::value_type + : &value_traits::value_type; + } + } + + // Handle process_path[_ex], for example: + // + // {{ + // $cxx.path ... + // }} + // + if (pr.type == &value_traits::value_type) + { + auto pp (convert (move (pp_ns))); + + if (pre_parse_) + { + diag_record dr (fail (l)); + dr << "unable to deduce low-verbosity script diagnostics name " + << "from process path " << pp; + suggest_diag (dr); + } + else + return optional (move (pp)); + } + else if (pr.type == &value_traits::value_type) + { + auto pp (convert (move (pp_ns))); + + if (pre_parse_) + { + if (pp.name) + { + set_diag (move (*pp.name), 3); + return nullopt; + } + + diag_record dr (fail (l)); + dr << "unable to deduce low-verbosity script diagnostics name " + << "from process path " << pp; + suggest_diag (dr); + } + else + return optional (move (pp)); + } + // + // Handle the executable target, for example: + // + // import! [metadata] cli = cli%exe{cli} + // ... + // {{ + // $cli ... + // }} + // + else if (!ns[0].simple ()) + { + if (const target* t = search_existing ( + ns[0], *scope_, ns[0].pair ? ns[1].dir : empty_dir_path)) + { + if (const auto* et = t->is_a ()) + { + if (pre_parse_) + { + if (auto* n = et->lookup_metadata ("name")) + { + set_diag (*n, 3); + return nullopt; + } + // Fall through. + } + else + { + process_path pp (et->process_path ()); + + if (pp.empty ()) + fail (l) << "target " << *et << " is out of date" << + info << "consider specifying it as a prerequisite of " + << environment_->target; + + ns.erase (ns.begin (), ns.begin () + (ns[0].pair ? 2 : 1)); + return optional (move (pp)); + } + } + + if (pre_parse_) + { + diag_record dr (fail (l)); + dr << "unable to deduce low-verbosity script diagnostics name " + << "from target " << *t; + suggest_diag (dr); + } + } + + if (pre_parse_) + { + diag_record dr (fail (l)); + dr << "unable to deduce low-verbosity script diagnostics name " + << "from " << ns; + suggest_diag (dr); + } + else + return nullopt; + } + else if (pre_parse_) + { + // If we are here, the name is simple and is not part of a pair. + // + string& v (ns[0].value); + + // Try to interpret the name as a builtin. + // + const builtin_info* bi (builtins.find (v)); + + if (bi != nullptr) + { + set_diag (move (v), bi->weight); + return nullopt; + } + // + // Try to interpret the name as a pseudo-builtin. + // + // Note that both of them has the zero weight and cannot be picked + // up as a script name. + // + else if (v == "set" || v == "exit") + { + return nullopt; + } + + diag_record dr (fail (l)); + dr << "unable to deduce low-verbosity script diagnostics name " + << "for program " << ns[0]; + suggest_diag (dr); + } + + return nullopt; + } + void parser:: execute (const scope& rs, const scope& bs, environment& e, const script& s, runner& r) @@ -256,6 +641,10 @@ namespace build2 // The script shouldn't be able to modify the scopes. // + // Note that for now we don't set target_ since it's not clear what + // it could be used for (we need scope_ for calling functions such as + // $target.path()). + // root_ = const_cast (&rs); scope_ = const_cast (&bs); pbase_ = scope_->src_path_; @@ -350,8 +739,10 @@ namespace build2 // In the pre-parse mode collect the referenced variable names for the // script semantics change tracking. // - if (pre_parse_) + if (pre_parse_ || pre_parse_suspended_) { + lookup r; + // Add the variable name skipping special variables and suppressing // duplicates. While at it, check if the script temporary directory // is referenced and set the flag, if that's the case. @@ -363,13 +754,21 @@ namespace build2 } else if (!name.empty ()) { + if (pre_parse_suspended_) + { + const variable* pvar (scope_->ctx.var_pool.find (name)); + + if (pvar != nullptr) + r = (*target_)[*pvar]; + } + auto& vars (script_->vars); if (find (vars.begin (), vars.end (), name) == vars.end ()) vars.push_back (move (name)); } - return lookup (); + return r; } if (!qual.empty ()) diff --git a/libbuild2/build/script/parser.hxx b/libbuild2/build/script/parser.hxx index 5f08209..15d4ede 100644 --- a/libbuild2/build/script/parser.hxx +++ b/libbuild2/build/script/parser.hxx @@ -34,8 +34,9 @@ namespace build2 // name. // script - pre_parse (istream&, const path_name&, uint64_t line, - optional diag); + pre_parse (const target&, + istream&, const path_name&, uint64_t line, + optional diag, const location& diag_loc); // Recursive descent parser. // @@ -82,9 +83,51 @@ namespace build2 virtual lookup lookup_variable (name&&, string&&, const location&) override; + // During execution translate the process path and executable targets + // leaving the rest for the base parser to handle. + // + // During pre-parsing try to deduce the low-verbosity script + // diagnostics name. + // + virtual optional + parse_program (token&, build2::script::token_type&, names&) override; + + void + parse_program_diag (token&, build2::script::token_type&, names&); + protected: script* script_; + // Current low-verbosity script diagnostics name and weight. + // + // During pre-parsing each command leading names are translated into a + // potential script name, unless it is set manually (with the diag + // directive or via the constructor). The potential script name has a + // weight associated with it, so script names with greater weights + // override names with lesser weights. The possible weights are: + // + // 0 - builtins that do not add to the script semantics (exit, + // true, etc) and are never picked up as a script name + // + // [1 2] - other builtins + // + // 3 - process path or executable target + // + // 4 - manually set names + // + // If two potential script names with the same weights are encountered + // then this ambiguity is reported unless a higher-weighted name is + // encountered later. + // + optional> diag; + optional> diag2; + uint8_t diag_weight = 0; + + // True during pre-parsing when the pre-parse mode is temporarily + // suspended to perform expansion. + // + bool pre_parse_suspended_ = false; + // Execute state. // runner* runner_; diff --git a/libbuild2/build/script/parser.test.cxx b/libbuild2/build/script/parser.test.cxx index 10e1701..de3e839 100644 --- a/libbuild2/build/script/parser.test.cxx +++ b/libbuild2/build/script/parser.test.cxx @@ -58,7 +58,7 @@ namespace build2 cout << endl; - return e.back ().pipe.back ().program.string () == "true"; + return e.back ().pipe.back ().program.recall.string () == "true"; } virtual void @@ -160,7 +160,12 @@ namespace build2 // parser p (ctx); path_name nm ("buildfile"); - script s (p.pre_parse (cin, nm, 11 /* line */, nullopt)); + + script s (p.pre_parse (tt, + cin, nm, + 11 /* line */, + string ("test"), + location (nm, 10))); switch (m) { diff --git a/libbuild2/build/script/runner.cxx b/libbuild2/build/script/runner.cxx index 315a248..2a59505 100644 --- a/libbuild2/build/script/runner.cxx +++ b/libbuild2/build/script/runner.cxx @@ -61,14 +61,14 @@ namespace build2 try { - // Note that the temporary directory must be empty to date. + // Note that the temporary directory must be empty. // rmdir_status r (try_rmdir (td)); if (r != rmdir_status::success) { // While there can be no fault of the script being currently - // executed let's add the location anyway to ease the + // executed let's add the location anyway to help with // troubleshooting. And let's stick to that principle down the // road. // @@ -110,8 +110,10 @@ namespace build2 find_if (expr.begin (), expr.end (), [] (const expr_term& et) { - const string& p (et.pipe.back ().program.string ()); - return p == "set" || p == "exit"; + const process_path& p (et.pipe.back ().program); + return p.initial == nullptr && + (p.recall.string () == "set" || + p.recall.string () == "exit"); }) != expr.end ()) build2::script::run (env, expr, li, ll); else if (verb >= 2) diff --git a/libbuild2/build/script/script.hxx b/libbuild2/build/script/script.hxx index 08cdbd2..de759de 100644 --- a/libbuild2/build/script/script.hxx +++ b/libbuild2/build/script/script.hxx @@ -37,7 +37,6 @@ namespace build2 class script { public: - // Note that the variables are not pre-entered into a pool during the // parsing phase, so the line variable pointers are NULL. // diff --git a/libbuild2/functions-process.cxx b/libbuild2/functions-process.cxx index 3fa7819..e6c0582 100644 --- a/libbuild2/functions-process.cxx +++ b/libbuild2/functions-process.cxx @@ -93,8 +93,8 @@ namespace build2 return value (move (r)); } - // Return the builtin function pointer if this is a call to a builtin and - // NULL otherwise. + // Return the builtin function pointer if this is a call to an internal + // builtin and NULL otherwise. // static builtin_function* builtin (const names& args) @@ -106,7 +106,8 @@ namespace build2 if (!nm.simple () || nm.pair) return nullptr; - return builtins.find (nm.value); + const builtin_info* r (builtins.find (nm.value)); + return r != nullptr ? r->function : nullptr; } // Return the builtin name and its arguments. The builtin function is only diff --git a/libbuild2/parser.cxx b/libbuild2/parser.cxx index dada34d..756a45b 100644 --- a/libbuild2/parser.cxx +++ b/libbuild2/parser.cxx @@ -1187,7 +1187,7 @@ namespace build2 { if (d.first) { - if (ar->recipe_text (ctx, move (t.value), d.as)) + if (ar->recipe_text (ctx, *target_, move (t.value), d.as)) d.clean = true; // Verify we have no unhandled attributes. @@ -5862,7 +5862,7 @@ namespace build2 { fail (l) << "invalid value subscript: " << e << info (bl) << "use the '\\[' escape sequence if this is a " - << "wildcard pattern"; + << "wildcard pattern" << endf; } // Similar to expanding an undefined variable, we return NULL if diff --git a/libbuild2/rule.cxx b/libbuild2/rule.cxx index 9e2ef95..a5d51ca 100644 --- a/libbuild2/rule.cxx +++ b/libbuild2/rule.cxx @@ -365,7 +365,7 @@ namespace build2 // adhoc_script_rule // bool adhoc_script_rule:: - recipe_text (context& ctx, string&& t, attributes& as) + recipe_text (context& ctx, const target& tg, string&& t, attributes& as) { // Handle and erase recipe-specific attributes. // @@ -397,7 +397,10 @@ namespace build2 istringstream is (move (t)); build::script::parser p (ctx); - script = p.pre_parse (is, loc.file, loc.line + 1, move (diag)); + + script = p.pre_parse (tg, + is, loc.file, loc.line + 1, + move (diag), as.loc); return false; } @@ -545,17 +548,9 @@ namespace build2 { if (auto* e = pt->is_a ()) { - if (auto* ns = cast_null (e->vars[ctx.var_export_metadata])) + if (auto* c = e->lookup_metadata ("checksum")) { - // Metadata variable prefix is in the second name. - // - assert (ns->size () == 2 && (*ns)[1].simple ()); - - if (auto* c = cast_null ( - e->vars[(*ns)[1].value + ".checksum"])) - { - prog_cs.append (*c); - } + prog_cs.append (*c); } } } @@ -778,7 +773,7 @@ namespace build2 } bool adhoc_cxx_rule:: - recipe_text (context&, string&& t, attributes&) + recipe_text (context&, const target&, string&& t, attributes&) { code = move (t); return true; diff --git a/libbuild2/rule.hxx b/libbuild2/rule.hxx index 76ab306..a79eeed 100644 --- a/libbuild2/rule.hxx +++ b/libbuild2/rule.hxx @@ -134,7 +134,7 @@ namespace build2 // therefore requires cleanup. // virtual bool - recipe_text (context&, string&&, attributes&) = 0; + recipe_text (context&, const target&, string&&, attributes&) = 0; public: // Some of the operations come in compensating pairs, such as update and @@ -194,7 +194,7 @@ namespace build2 adhoc_script_rule (const location& l, size_t b): adhoc_rule (l, b) {} virtual bool - recipe_text (context&, string&&, attributes&) override; + recipe_text (context&, const target&, string&&, attributes&) override; public: using script_type = build::script::script; @@ -251,7 +251,7 @@ namespace build2 adhoc_cxx_rule (const location&, size_t, uint64_t version); virtual bool - recipe_text (context&, string&& t, attributes&) override; + recipe_text (context&, const target&, string&& t, attributes&) override; virtual ~adhoc_cxx_rule () override; diff --git a/libbuild2/script/parser.cxx b/libbuild2/script/parser.cxx index aa60111..33dc273 100644 --- a/libbuild2/script/parser.cxx +++ b/libbuild2/script/parser.cxx @@ -96,6 +96,19 @@ namespace build2 return regex_parts (string (s, 1, rn), s[0], string (s, fp, p - fp)); } + optional parser:: + parse_program (token& t, type& tt, names& ns) + { + parse_names (t, tt, + ns, + pattern_mode::ignore, + true /* chunk */, + "command line", + nullptr); + + return nullopt; + } + pair parser:: parse_command_expr (token& t, type& tt, const redirect_aliases& ra) @@ -279,7 +292,9 @@ namespace build2 { case pending::none: c.arguments.push_back (move (w)); break; case pending::program: - c.program = parse_path (move (w), "program path"); + c.program = process_path (nullptr /* initial */, + parse_path (move (w), "program path"), + path () /* effect */); break; case pending::out_merge: add_merge (c.out, w, 2); break; @@ -726,12 +741,20 @@ namespace build2 { if (pre_parse_) { - // The only things we need to handle here are the here-document - // and here-document regex end markers since we need to know - // how many of them to pre-parse after the command. + // The only things we need to handle here are the tokens that + // introduce the next command, since we handle the command + // leading name chunks specially, and the here-document and + // here-document regex end markers, since we need to know how + // many of them to pre-parse after the command. // switch (tt) { + case type::pipe: + case type::log_or: + case type::log_and: + p = pending::program; + break; + case type::in_doc: case type::out_doc: mod = move (t.value); @@ -994,23 +1017,53 @@ namespace build2 } } - // Parse the next chunk as simple names to get expansion, etc. - // Note that we do it in the chunking mode to detect whether - // anything in each chunk is quoted. + // Parse the next chunk as names to get expansion, etc. Note that + // we do it in the chunking mode to detect whether anything in + // each chunk is quoted. If we are waiting for the command + // program, then delegate the parsing to the derived parser, so it + // can translate complex program names (targets, process_paths) + // during execution and perform some static analysis during + // pre-parsing. // // @@ PAT: should we support pattern expansion? This is even // fuzzier than the variable case above. Though this is the // shell semantics. Think what happens when we do rm *.txt? // reset_quoted (t); - parse_names (t, tt, - ns, - pattern_mode::ignore, - true, - "command line", - nullptr); - - if (pre_parse_) // Nothing else to do if we are pre-parsing. + + if (p == pending::program) + { + optional pp (parse_program (t, tt, ns)); + + // During pre-parsing we are not interested in the + // parse_program() call result, so just discard the potentially + // unhandled program chunk names. + // + if (!pre_parse_) + { + if (pp) + { + c.program = move (*pp); + p = pending::none; + } + } + else + { + ns.clear (); + p = pending::none; + } + } + else + parse_names (t, tt, + ns, + pattern_mode::ignore, + true /* chunk */, + "command line", + nullptr); + + // Nothing else to do if we are pre-parsing. + // + if (pre_parse_) break; // Process what we got. Determine whether anything inside was diff --git a/libbuild2/script/parser.hxx b/libbuild2/script/parser.hxx index a63ecde..b15f632 100644 --- a/libbuild2/script/parser.hxx +++ b/libbuild2/script/parser.hxx @@ -162,14 +162,42 @@ namespace build2 size_t& li, variable_pool* = nullptr); + // Customization hooks. + // + protected: + // Parse the command's leading name chunk. + // + // During the execution phase try to parse and translate the leading + // names into the process path and return nullopt if choose not to do + // so, leaving it to the parser to handle. Also return in the last + // argument uninterpreted names, if any. + // + // The default implementation always returns nullopt. The derived parser + // can provide an override that can, for example, handle process path + // values, executable targets, etc. + // + // Note that normally it makes sense to leave simple unpaired names for + // the parser to handle, unless there is a good reason not to (e.g., + // it's a special builtin or some such). Such names may contain + // something that requires re-lexing, for example `foo|bar`, which won't + // be easy to translate but which are handled by the parser. + // + // During the pre-parsing phase the returned process path and names + // (that must still be parsed) are discarded. The main purpose of the + // call is to allow implementations to perform static script analysis, + // recognize and execute certain directives, or some such. + // + virtual optional + parse_program (token&, token_type&, names&); + // Set lexer pointers for both the current and the base classes. // protected: void set_lexer (lexer*); - // Number of quoted tokens since last reset. Note that this includes - // the peeked token, if any. + // Number of quoted tokens since last reset. Note that this includes the + // peeked token, if any. // protected: size_t diff --git a/libbuild2/script/run.cxx b/libbuild2/script/run.cxx index 38436b9..5629a15 100644 --- a/libbuild2/script/run.cxx +++ b/libbuild2/script/run.cxx @@ -102,7 +102,7 @@ namespace build2 catch (const io_error& e) { // While there can be no fault of the script command being currently - // executed let's add the location anyway to ease the + // executed let's add the location anyway to help with // troubleshooting. And let's stick to that principle down the road. // fail (ll) << "unable to read " << p << ": " << e << endf; @@ -949,7 +949,14 @@ namespace build2 command_pipe::const_iterator nc (bc + 1); bool last (nc == ec); - const string& program (c.program.string ()); + // True if the process path is not pre-searched and the program path + // still needs to be resolved. + // + bool resolve (c.program.initial == nullptr); + + // Program name that may require resolution. + // + const string& program (c.program.recall.string ()); const redirect& in ((c.in ? *c.in : env.in).effective ()); @@ -961,7 +968,7 @@ namespace build2 auto process_args = [&c] () -> cstrings { - cstrings args {c.program.string ().c_str ()}; + cstrings args {c.program.recall_string ()}; for (const auto& a: c.arguments) args.push_back (a.c_str ()); @@ -982,7 +989,7 @@ namespace build2 // specify any redirects or exit code check sounds like a right thing // to do. // - if (program == "exit") + if (resolve && program == "exit") { // In case the builtin is erroneously pipelined from the other // command, we will close stdin gracefully (reading out the stream @@ -1150,7 +1157,7 @@ namespace build2 // that. Checking that the user didn't specify any meaningless // redirects or exit code check sounds as a right thing to do. // - if (program == "set") + if (resolve && program == "set") { if (!last) fail (ll) << "set builtin must be the last pipe command"; @@ -1322,11 +1329,13 @@ namespace build2 assert (ofd.out.get () != -1 && efd.get () != -1); optional exit; - builtin_function* bf (builtins.find (program)); + const builtin_info* bi (resolve + ? builtins.find (program) + : nullptr); bool success; - if (bf != nullptr) + if (bi != nullptr && bi->function != nullptr) { // Execute the builtin. // @@ -1544,11 +1553,11 @@ namespace build2 try { uint8_t r; // Storage. - builtin b (bf (r, - c.arguments, - move (ifd), move (ofd.out), move (efd), - *env.work_dir.path, - bcs)); + builtin b (bi->function (r, + c.arguments, + move (ifd), move (ofd.out), move (efd), + *env.work_dir.path, + bcs)); success = run_pipe (env, nc, @@ -1570,14 +1579,15 @@ namespace build2 // cstrings args (process_args ()); - // Resolve the relative not simple program path against the script's - // working directory. The simple one will be left for the process - // path search machinery. Also strip the potential leading `^`, - // indicating that this is an external program rather than a - // builtin. + // If the process path is not pre-searched then resolve the relative + // non-simple program path against the script's working directory. The + // simple one will be left for the process path search machinery. Also + // strip the potential leading `^` (indicates that this is an external + // program rather than a builtin). // path p; + if (resolve) try { p = path (args[0]); @@ -1610,7 +1620,9 @@ namespace build2 try { - process_path pp (process::path_search (args[0])); + process_path pp (resolve + ? process::path_search (args[0]) + : process_path ()); // Note: the builtin-escaping character '^' is not printed. // @@ -1618,7 +1630,7 @@ namespace build2 print_process (args); process pr ( - pp, + resolve ? pp : c.program, args.data (), {ifd.get (), -1}, process::pipe (ofd), {-1, efd.get ()}, env.work_dir.path->string ().c_str ()); @@ -1656,7 +1668,11 @@ namespace build2 if (!success) return false; - const path& pr (c.program); + // Use the program path for diagnostics (print relative, etc). + // + const path& pr (resolve + ? c.program.recall + : path (c.program.recall_string ())); // Can't throw. // If there is no valid exit code available by whatever reason then we // print the proper diagnostics, dump stderr (if cached and not too diff --git a/libbuild2/script/script.cxx b/libbuild2/script/script.cxx index c85bfd3..7722b47 100644 --- a/libbuild2/script/script.cxx +++ b/libbuild2/script/script.cxx @@ -350,7 +350,7 @@ namespace build2 { // Program. // - to_stream_q (o, c.program.string ()); + to_stream_q (o, c.program.recall_string ()); // Arguments. // diff --git a/libbuild2/script/script.hxx b/libbuild2/script/script.hxx index f4998b7..891b2f6 100644 --- a/libbuild2/script/script.hxx +++ b/libbuild2/script/script.hxx @@ -294,7 +294,12 @@ namespace build2 // struct command { - path program; + // We use NULL initial as an indication that the path stored in recall + // is a program name that still needs to be resolved into the builtin + // function or the process path. + // + process_path program; + strings arguments; optional in; diff --git a/libbuild2/target.hxx b/libbuild2/target.hxx index eb958c8..667ef79 100644 --- a/libbuild2/target.hxx +++ b/libbuild2/target.hxx @@ -1711,6 +1711,12 @@ namespace build2 void process_path (process_path_type); + // Lookup metadata variable (see {import,export}.metadata). + // + template + const T* + lookup_metadata (const char* var) const; + public: static const target_type static_type; virtual const target_type& dynamic_type () const {return static_type;} diff --git a/libbuild2/target.txx b/libbuild2/target.txx index b482d64..ef14cf5 100644 --- a/libbuild2/target.txx +++ b/libbuild2/target.txx @@ -179,4 +179,22 @@ namespace build2 t.prerequisites (move (ps)); return &t; } + + // exe + // + template + const T* exe:: + lookup_metadata (const char* var) const + { + if (auto* ns = cast_null (vars[ctx.var_export_metadata])) + { + // Metadata variable prefix is in the second name. + // + assert (ns->size () == 2 && (*ns)[1].simple ()); + + return cast_null (vars[(*ns)[1].value + '.' + var]); + } + + return nullptr; + } } diff --git a/libbuild2/test/script/parser.test.cxx b/libbuild2/test/script/parser.test.cxx index a12fda4..69583ae 100644 --- a/libbuild2/test/script/parser.test.cxx +++ b/libbuild2/test/script/parser.test.cxx @@ -123,7 +123,7 @@ namespace build2 cout << endl; - return e.back ().pipe.back ().program.string () == "true"; + return e.back ().pipe.back ().program.recall.string () == "true"; } virtual void diff --git a/libbuild2/variable.cxx b/libbuild2/variable.cxx index ed710af..e0502ef 100644 --- a/libbuild2/variable.cxx +++ b/libbuild2/variable.cxx @@ -1083,6 +1083,8 @@ namespace build2 const string& k ((i++)->value); + // NOTE: see also build::script::parser::parse_program(). + // if (k == "name") { if (!i->simple ()) diff --git a/tests/dependency/recipe/testscript b/tests/dependency/recipe/testscript index c5e4a8c..9843151 100644 --- a/tests/dependency/recipe/testscript +++ b/tests/dependency/recipe/testscript @@ -11,14 +11,15 @@ $* <>/~%EOE% alias{x}: alias{z} {{ - cmd + echo }} dump alias{x} EOI :5:1: dump: % .+/alias\{x\}: .+/:alias\{z\}% + % [diag=echo] {{ - cmd + echo }} EOE @@ -27,14 +28,15 @@ EOE $* <>/~%EOE% alias{x y}: alias{z} {{ - cmd + echo }} dump alias{y} EOI :5:1: dump: % .+/alias\{y\}: .+/:alias\{z\}% + % [diag=echo] {{ - cmd + echo }} EOE @@ -44,14 +46,15 @@ $* <>/~%EOE% alias{x}: % {{ - cmd + echo }} dump alias{x} EOI :6:1: dump: % .+/alias\{x\}:% + % [diag=echo] {{ - cmd + echo }} EOE @@ -61,14 +64,15 @@ $* <>/~%EOE% alias{x y}: % {{ - cmd + echo }} dump alias{y} EOI :6:1: dump: % .+/alias\{y\}:% + % [diag=echo] {{ - cmd + echo }} EOE @@ -96,7 +100,7 @@ alias{x}: var = x } {{ - cmd + echo }} dump alias{x} EOI @@ -105,8 +109,9 @@ EOI { var = x } + % [diag=echo] {{ - cmd + echo }} EOE @@ -118,7 +123,7 @@ alias{x y}: alias{z} var = x } {{ - cmd + echo }} dump alias{y} EOI @@ -127,8 +132,9 @@ EOI { var = x } + % [diag=echo] {{ - cmd + echo }} EOE @@ -141,7 +147,7 @@ alias{x}: alias{z} } % {{ - cmd + echo }} dump alias{x} EOI @@ -150,8 +156,9 @@ EOI { var = x } + % [diag=echo] {{ - cmd + echo }} EOE @@ -164,7 +171,7 @@ alias{x y}: } % {{ - cmd + echo }} dump alias{y} EOI @@ -173,8 +180,9 @@ EOI { var = x } + % [diag=echo] {{ - cmd + echo }} EOE @@ -183,20 +191,22 @@ EOE $* <>/~%EOE% alias{x}: {{ - cmd1 + echo }} {{{ - cmd2 + cat }}} dump alias{x} EOI :8:1: dump: % .+/alias\{x\}:% + % [diag=echo] {{ - cmd1 + echo }} + % [diag=cat] {{{ - cmd2 + cat }}} EOE @@ -205,20 +215,22 @@ EOE $* <>/~%EOE% alias{x y}: alias{z} {{ - cmd1 + echo }} {{{ - cmd2 + cat }}} dump alias{y} EOI :8:1: dump: % .+/alias\{y\}: .+/:alias\{z\}% + % [diag=echo] {{ - cmd1 + echo }} + % [diag=cat] {{{ - cmd2 + cat }}} EOE @@ -228,22 +240,24 @@ $* <>/~%EOE% alias{x}: alias{z} {{ - cmd1 + echo }} % {{{ - cmd2 + cat }}} dump alias{x} EOI :11:1: dump: % .+/alias\{x\}: .+/:alias\{z\}% + % [diag=echo] {{ - cmd1 + echo }} + % [diag=cat] {{{ - cmd2 + cat }}} EOE @@ -253,22 +267,24 @@ $* <>/~%EOE% alias{x y}: {{ - cmd1 + echo }} % {{{ - cmd2 + cat }}} dump alias{y} EOI :11:1: dump: % .+/alias\{y\}:% + % [diag=echo] {{ - cmd1 + echo }} + % [diag=cat] {{{ - cmd2 + cat }}} EOE @@ -277,7 +293,7 @@ EOE $* <>EOE != 0 alias{x}: {{{ - cmd + echo }} EOI :5:1: error: unterminated recipe block @@ -289,7 +305,7 @@ EOE $* <>EOE != 0 alias{x}: {{ $lang - cmd + echo }} EOI :2:4: error: expected recipe language instead of '$' @@ -301,7 +317,7 @@ $* <>/~!EOE! alias{x}: % [diag=gen] {{ - cmd + echo }} dump alias{x} EOI @@ -309,7 +325,7 @@ EOI ! .+/alias\{x\}:! % [diag=gen] {{ - cmd + echo }} EOE @@ -319,7 +335,7 @@ $* <>/~!EOE! alias{x y}: % [diag=gen] {{ - cmd + echo }} dump alias{y} EOI @@ -327,7 +343,7 @@ EOI ! .+/alias\{y\}:! % [diag=gen] {{ - cmd + echo }} EOE @@ -337,8 +353,250 @@ $* <>EOE != 0 alias{x}: % { - cmd + echo } EOI :3:1: error: expected recipe block instead of '{' EOE + +: diag +: +{ + : builtins + : + { + : weight-0 + : + $* <>EOE != 0 + alias{x}: + {{ + + exit + }} + dump alias{x} + EOI + :3:1: error: unable to deduce low-verbosity script diagnostics name + info: consider specifying it explicitly with the 'diag' recipe attribute + info: or provide custom low-verbosity diagnostics with the 'diag' builtin + EOE + + : weight-1 + : + $* <>~%EOE% + alias{x}: + {{ + true + rm b + }} + dump alias{x} + EOI + %.{2} + % [diag=rm] + %.{4} + EOE + + : weight-2 + : + $* <>~%EOE% + alias{x}: + {{ + rm a + echo a + }} + dump alias{x} + EOI + %.{2} + % [diag=echo] + %.{4} + EOE + + : ambiguity + : + $* <>EOE != 0 + alias{x}: + {{ + echo a + cat b + }} + dump alias{x} + EOI + :3:1: error: low-verbosity script diagnostics name is ambiguous + :3:3: info: could be 'echo' + :4:3: info: could be 'cat' + info: consider specifying it explicitly with the 'diag' recipe attribute + info: or provide custom low-verbosity diagnostics with the 'diag' builtin + EOE + } + + : process-path-ex + : + { + config_cxx = config.cxx=$quote($recall($cxx.path) $cxx.mode, true) + + mkdir build; + cat <=build/bootstrap.build; + project = test + amalgamation = + subprojects = + + using config + EOI + + cat <=build/root.build; + using cxx + EOI + + $* $config_cxx <>~%EOE% + c = $cxx.path --version + alias{x}: + {{ + $c + }} + dump alias{x} + EOI + %.{2} + % [diag=c++] + %.{3} + EOE + } + + : unrecognized + : + { + : expansion-failure + : + $* <>EOE != 0 + alias{x}: + {{ + x = true + ech($x ? o : a) + }} + dump alias{x} + EOI + :4:8: error: invalid bool value: null + :4:11: info: use the '\?' escape sequence if this is a wildcard pattern + :4:4: info: while deducing low-verbosity script diagnostics name + info: consider specifying it explicitly with the 'diag' recipe attribute + info: or provide custom low-verbosity diagnostics with the 'diag' builtin + EOE + + : empty + : + $* <>EOE != 0 + alias{x}: + {{ + foo = bar + $foo + }} + dump alias{x} + EOI + :4:3: error: unable to deduce low-verbosity script diagnostics name + info: consider specifying it explicitly with the 'diag' recipe attribute + info: or provide custom low-verbosity diagnostics with the 'diag' builtin + EOE + + : process-path-typed + : + $* <>~%EOE% != 0 + alias{x}: + {{ + $build.path --version + }} + dump alias{x} + EOI + %:3:4: error: unable to deduce low-verbosity script diagnostics name from process path .+% + info: consider specifying it explicitly with the 'diag' recipe attribute + info: or provide custom low-verbosity diagnostics with the 'diag' builtin + EOE + + : process-path-syntactic + : + $* <>~%EOE% != 0 + b = $build.path + alias{x}: + {{ + $b --version + }} + dump alias{x} + EOI + %:4:4: error: unable to deduce low-verbosity script diagnostics name from process path .+% + info: consider specifying it explicitly with the 'diag' recipe attribute + info: or provide custom low-verbosity diagnostics with the 'diag' builtin + EOE + + : target-no-name + : + : Disable when cross-testing for the sake of simplicity. + : + if ($test.target == $build.host) + { + $* <>/~%EOE% != 0 + import! b = build2%exe{b} + + alias{x}: $b + {{ + $b --version + }} + dump alias{x} + EOI + %:5:3: error: unable to deduce low-verbosity script diagnostics name from target .+/exe\{b\}% + info: consider specifying it explicitly with the 'diag' recipe attribute + info: or provide custom low-verbosity diagnostics with the 'diag' builtin + EOE + } + + : program + : + $* <>~%EOE% != 0 + alias{x}: + {{ + echo a + foo + }} + dump alias{x} + EOI + :4:3: error: unable to deduce low-verbosity script diagnostics name for program foo + info: consider specifying it explicitly with the 'diag' recipe attribute + info: or provide custom low-verbosity diagnostics with the 'diag' builtin + EOE + } + + : manual + : + { + : attribute + : + $* <>~%EOE% + alias{x}: + % [diag=foo] + {{ + rm a + echo b | set c + bar + }} + dump alias{x} + EOI + %.{2} + % [diag=foo] + %.{5} + EOE + + : diag + : + $* <>~%EOE% + v = a b + alias{x}: + {{ + rm a + echo b | set c + diag bar + fo$v + }} + dump alias{x} + EOI + %.{2} + % [diag=] + %.{5} + EOE + } +} -- cgit v1.1