diff options
Diffstat (limited to 'build2')
91 files changed, 29 insertions, 22661 deletions
diff --git a/build2/b.cxx b/build2/b.cxx index 4a446ac..a76a8e4 100644 --- a/build2/b.cxx +++ b/build2/b.cxx @@ -42,19 +42,17 @@ #include <libbuild2/parser.hxx> -#include <build2/b-options.hxx> +#include <libbuild2/dist/init.hxx> +#include <libbuild2/test/init.hxx> +#include <libbuild2/config/init.hxx> +#include <libbuild2/install/init.hxx> -#include <build2/config/utility.hxx> // config::save_variable() -#include <build2/config/operation.hxx> // config::preprocess_create() +#include <build2/b-options.hxx> using namespace butl; using namespace std; -#include <build2/config/init.hxx> #include <build2/version/init.hxx> -#include <build2/test/init.hxx> -#include <build2/dist/init.hxx> -#include <build2/install/init.hxx> #include <build2/in/init.hxx> @@ -439,14 +437,11 @@ main (int argc, char* argv[]) using mf = module_functions; auto& bm (builtin_modules); - bm["config"] = mf {&config::boot, &config::init}; - - config_save_variable = &config::save_variable; - config_preprocess_create = &config::preprocess_create; - - bm["dist"] = mf {&dist::boot, &dist::init}; + bm["config"] = config::build2_config_load (); + bm["dist"] = dist::build2_dist_load (); bm["test"] = test::build2_test_load (); - bm["install"] = mf {&install::boot, &install::init}; + bm["install"] = install::build2_install_load (); + bm["version"] = mf {&version::boot, &version::init}; bm["in.base"] = mf {nullptr, &in::base_init}; diff --git a/build2/bash/init.cxx b/build2/bash/init.cxx index 83bfdb9..a23bc61 100644 --- a/build2/bash/init.cxx +++ b/build2/bash/init.cxx @@ -9,7 +9,7 @@ #include <libbuild2/variable.hxx> #include <libbuild2/diagnostics.hxx> -#include <build2/install/utility.hxx> +#include <libbuild2/install/utility.hxx> #include <build2/bash/rule.hxx> #include <build2/bash/target.hxx> diff --git a/build2/bash/rule.hxx b/build2/bash/rule.hxx index 6430947..a6ec235 100644 --- a/build2/bash/rule.hxx +++ b/build2/bash/rule.hxx @@ -8,8 +8,9 @@ #include <libbuild2/types.hxx> #include <libbuild2/utility.hxx> +#include <libbuild2/install/rule.hxx> + #include <build2/in/rule.hxx> -#include <build2/install/rule.hxx> namespace build2 { diff --git a/build2/bin/init.cxx b/build2/bin/init.cxx index 5932b29..2df0572 100644 --- a/build2/bin/init.cxx +++ b/build2/bin/init.cxx @@ -10,12 +10,12 @@ #include <libbuild2/variable.hxx> #include <libbuild2/diagnostics.hxx> -#include <build2/config/utility.hxx> +#include <libbuild2/config/utility.hxx> -#include <build2/test/module.hxx> +#include <libbuild2/test/module.hxx> -#include <build2/install/rule.hxx> -#include <build2/install/utility.hxx> +#include <libbuild2/install/rule.hxx> +#include <libbuild2/install/utility.hxx> #include <build2/bin/rule.hxx> #include <build2/bin/guess.hxx> diff --git a/build2/cc/compile-rule.cxx b/build2/cc/compile-rule.cxx index c0c7eb3..7490517 100644 --- a/build2/cc/compile-rule.cxx +++ b/build2/cc/compile-rule.cxx @@ -16,8 +16,9 @@ #include <libbuild2/filesystem.hxx> // mtime() #include <libbuild2/diagnostics.hxx> +#include <libbuild2/config/utility.hxx> // create_project() + #include <build2/bin/target.hxx> -#include <build2/config/utility.hxx> // create_project() #include <build2/cc/parser.hxx> #include <build2/cc/target.hxx> // h diff --git a/build2/cc/init.cxx b/build2/cc/init.cxx index 18fba20..e095b08 100644 --- a/build2/cc/init.cxx +++ b/build2/cc/init.cxx @@ -10,7 +10,7 @@ #include <libbuild2/filesystem.hxx> #include <libbuild2/diagnostics.hxx> -#include <build2/config/utility.hxx> +#include <libbuild2/config/utility.hxx> #include <build2/cc/target.hxx> #include <build2/cc/utility.hxx> diff --git a/build2/cc/install-rule.hxx b/build2/cc/install-rule.hxx index ea966b8..55f6d2f 100644 --- a/build2/cc/install-rule.hxx +++ b/build2/cc/install-rule.hxx @@ -8,7 +8,7 @@ #include <libbuild2/types.hxx> #include <libbuild2/utility.hxx> -#include <build2/install/rule.hxx> +#include <libbuild2/install/rule.hxx> #include <build2/cc/types.hxx> #include <build2/cc/common.hxx> diff --git a/build2/cc/module.cxx b/build2/cc/module.cxx index ec35444..0fe9459 100644 --- a/build2/cc/module.cxx +++ b/build2/cc/module.cxx @@ -14,8 +14,8 @@ #include <build2/cc/target.hxx> // pc* -#include <build2/config/utility.hxx> -#include <build2/install/utility.hxx> +#include <libbuild2/config/utility.hxx> +#include <libbuild2/install/utility.hxx> #include <build2/cc/guess.hxx> diff --git a/build2/cc/pkgconfig.cxx b/build2/cc/pkgconfig.cxx index 99715be..c23b746 100644 --- a/build2/cc/pkgconfig.cxx +++ b/build2/cc/pkgconfig.cxx @@ -17,7 +17,7 @@ #include <libbuild2/filesystem.hxx> #include <libbuild2/diagnostics.hxx> -#include <build2/install/utility.hxx> +#include <libbuild2/install/utility.hxx> #include <build2/bin/target.hxx> diff --git a/build2/cli/init.cxx b/build2/cli/init.cxx index 24266ca..6d20aa0 100644 --- a/build2/cli/init.cxx +++ b/build2/cli/init.cxx @@ -9,9 +9,9 @@ #include <libbuild2/variable.hxx> #include <libbuild2/diagnostics.hxx> -#include <build2/cxx/target.hxx> +#include <libbuild2/config/utility.hxx> -#include <build2/config/utility.hxx> +#include <build2/cxx/target.hxx> #include <build2/cli/target.hxx> #include <build2/cli/rule.hxx> diff --git a/build2/config/init.cxx b/build2/config/init.cxx deleted file mode 100644 index bd2d573..0000000 --- a/build2/config/init.cxx +++ /dev/null @@ -1,148 +0,0 @@ -// file : build2/config/init.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/config/init.hxx> - -#include <libbuild2/file.hxx> -#include <libbuild2/rule.hxx> -#include <libbuild2/scope.hxx> -#include <libbuild2/context.hxx> -#include <libbuild2/filesystem.hxx> // exists() -#include <libbuild2/diagnostics.hxx> - -#include <build2/config/module.hxx> -#include <build2/config/utility.hxx> -#include <build2/config/operation.hxx> - -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace config - { - bool - boot (scope& rs, const location&, unique_ptr<module_base>& mod) - { - tracer trace ("config::boot"); - - l5 ([&]{trace << "for " << rs;}); - - const string& mname (current_mname); - const string& oname (current_oname); - - // Only create the module if we are configuring or creating. This is a - // bit tricky since the build2 core may not yet know if this is the - // case. But we know. - // - if (( mname == "configure" || mname == "create") || - (mname.empty () && (oname == "configure" || oname == "create"))) - { - unique_ptr<module> m (new module); - - // Adjust priority for the import pseudo-module so that - // config.import.* values come first in config.build. - // - m->save_module ("import", INT32_MIN); - - mod = move (m); - } - - // Register meta-operations. Note that we don't register create_id - // since it will be pre-processed into configure. - // - rs.insert_meta_operation (configure_id, mo_configure); - rs.insert_meta_operation (disfigure_id, mo_disfigure); - - return true; // Initialize first (load config.build). - } - - bool - init (scope& rs, - scope&, - const location& l, - unique_ptr<module_base>&, - bool first, - bool, - const variable_map& config_hints) - { - tracer trace ("config::init"); - - if (!first) - { - warn (l) << "multiple config module initializations"; - return true; - } - - const dir_path& out_root (rs.out_path ()); - l5 ([&]{trace << "for " << out_root;}); - - assert (config_hints.empty ()); // We don't known any hints. - - auto& vp (var_pool.rw (rs)); - - // Load config.build if one exists (we don't need to worry about - // disfigure since we will never be init'ed). - // - const variable& c_v (vp.insert<uint64_t> ("config.version", false)); - - { - path f (config_file (rs)); - - if (exists (f)) - { - // Check the config version. We assume that old versions cannot - // understand new configs and new versions are incompatible with old - // configs. - // - // We extract the value manually instead of loading and then - // checking in order to be able to fixup/migrate the file which we - // may want to do in the future. - // - { - // Assume missing version is 0. - // - auto p (extract_variable (f, c_v)); - uint64_t v (p.second ? cast<uint64_t> (p.first) : 0); - - if (v != module::version) - fail (l) << "incompatible config file " << f << - info << "config file version " << v - << (p.second ? "" : " (missing)") << - info << "config module version " << module::version << - info << "consider reconfiguring " << project (rs) << '@' - << out_root; - } - - source (rs, rs, f); - } - } - - // Register alias and fallback rule for the configure meta-operation. - // - // We need this rule for out-of-any-project dependencies (e.g., - // libraries imported from /usr/lib). We are registring it on the - // global scope similar to builtin rules. - // - { - auto& r (rs.global ().rules); - r.insert<mtime_target> ( - configure_id, 0, "config.file", file_rule::instance); - } - { - auto& r (rs.rules); - - //@@ outer - r.insert<alias> (configure_id, 0, "config.alias", alias_rule::instance); - - // This allows a custom configure rule while doing nothing by default. - // - r.insert<target> (configure_id, 0, "config", noop_rule::instance); - r.insert<file> (configure_id, 0, "config.file", noop_rule::instance); - } - - return true; - } - } -} diff --git a/build2/config/init.hxx b/build2/config/init.hxx deleted file mode 100644 index 5a9b66d..0000000 --- a/build2/config/init.hxx +++ /dev/null @@ -1,31 +0,0 @@ -// file : build2/config/init.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_CONFIG_INIT_HXX -#define BUILD2_CONFIG_INIT_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/module.hxx> - -namespace build2 -{ - namespace config - { - bool - boot (scope&, const location&, unique_ptr<module_base>&); - - bool - init (scope&, - scope&, - const location&, - unique_ptr<module_base>&, - bool, - bool, - const variable_map&); - } -} - -#endif // BUILD2_CONFIG_INIT_HXX diff --git a/build2/config/module.cxx b/build2/config/module.cxx deleted file mode 100644 index 7c3aae4..0000000 --- a/build2/config/module.cxx +++ /dev/null @@ -1,54 +0,0 @@ -// file : build2/config/module.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/config/module.hxx> - -using namespace std; - -namespace build2 -{ - namespace config - { - void module:: - save_variable (const variable& var, uint64_t flags) - { - const string& n (var.name); - - // First try to find the module with the name that is the longest - // prefix of this variable name. - // - auto& sm (saved_modules); - auto i (sm.find_sup (n)); - - // If no module matched, then create one based on the variable name. - // - if (i == sm.end ()) - { - // @@ For now with 'config.' prefix. - // - i = sm.insert (string (n, 0, n.find ('.', 7))); - } - - // Don't insert duplicates. The config.import vars are particularly - // susceptible to duplication. - // - saved_variables& sv (i->second); - auto j (sv.find (var)); - - if (j == sv.end ()) - sv.push_back (saved_variable {var, flags}); - else - assert (j->flags == flags); - } - - void module:: - save_module (const char* name, int prio) - { - saved_modules.insert (string ("config.") += name, prio); - } - - const string module::name ("config"); - const uint64_t module::version (1); - } -} diff --git a/build2/config/module.hxx b/build2/config/module.hxx deleted file mode 100644 index 0c78b18..0000000 --- a/build2/config/module.hxx +++ /dev/null @@ -1,93 +0,0 @@ -// file : build2/config/module.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_CONFIG_MODULE_HXX -#define BUILD2_CONFIG_MODULE_HXX - -#include <map> - -#include <libbutl/prefix-map.mxx> - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/module.hxx> -#include <libbuild2/variable.hxx> - -namespace build2 -{ - namespace config - { - // An ordered list of modules each with an ordered list of list of - // config.* variables and their "save flags" (see save_variable()) that - // are used (as opposed to just being specified) in this configuration. - // Populated by the config utility functions (required(), optional()) - // and saved in the order populated. - // - struct saved_variable - { - reference_wrapper<const variable> var; - uint64_t flags; - }; - - struct saved_variables: vector<saved_variable> - { - // Normally each module only have a handful of config variables and we - // only do this during configuration so for now we do linear search - // instead of adding a map. - // - const_iterator - find (const variable& var) const - { - return std::find_if ( - begin (), - end (), - [&var] (const saved_variable& v) {return var == v.var;}); - } - }; - - struct saved_modules: butl::prefix_map<string, saved_variables, '.'> - { - // Priority order with INT32_MIN being the highest. Modules with the - // same priority are saved in the order inserted. - // - // Generally, the idea is that we want higher-level modules at the top - // of the file since that's the configuration that we usualy want to - // change. So we have the following priority bands/defaults: - // - // 101-200/150 - code generators (e.g., yacc, bison) - // 201-300/250 - compilers (e.g., C, C++), - // 301-400/350 - binutils (ar, ld) - // - std::multimap<std::int32_t, const_iterator> order; - - iterator - insert (string name, int prio = 0) - { - auto p (emplace (move (name), saved_variables ())); - - if (p.second) - order.emplace (prio, p.first); - - return p.first; - } - }; - - struct module: module_base - { - config::saved_modules saved_modules; - - void - save_variable (const variable&, uint64_t flags = 0); - - void - save_module (const char* name, int prio = 0); - - static const string name; - static const uint64_t version; - }; - } -} - -#endif // BUILD2_CONFIG_MODULE_HXX diff --git a/build2/config/operation.cxx b/build2/config/operation.cxx deleted file mode 100644 index ff5b44a..0000000 --- a/build2/config/operation.cxx +++ /dev/null @@ -1,997 +0,0 @@ -// file : build2/config/operation.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/config/operation.hxx> - -#include <set> - -#include <libbuild2/file.hxx> -#include <libbuild2/spec.hxx> -#include <libbuild2/scope.hxx> -#include <libbuild2/target.hxx> -#include <libbuild2/context.hxx> -#include <libbuild2/algorithm.hxx> -#include <libbuild2/filesystem.hxx> -#include <libbuild2/diagnostics.hxx> - -#include <build2/config/module.hxx> -#include <build2/config/utility.hxx> - -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace config - { - // configure - // - static void - save_src_root (const scope& root) - { - const dir_path& out_root (root.out_path ()); - const dir_path& src_root (root.src_path ()); - - path f (out_root / root.root_extra->src_root_file); - - if (verb >= 2) - text << "cat >" << f; - - try - { - ofdstream ofs (f); - - ofs << "# Created automatically by the config module." << endl - << "#" << endl - << "src_root = "; - to_stream (ofs, name (src_root), true, '@'); // Quote. - ofs << endl; - - ofs.close (); - } - catch (const io_error& e) - { - fail << "unable to write " << f << ": " << e; - } - } - - static void - save_out_root (const scope& root) - { - const dir_path& out_root (root.out_path ()); - const dir_path& src_root (root.src_path ()); - - path f (src_root / root.root_extra->out_root_file); - - if (verb) - text << (verb >= 2 ? "cat >" : "save ") << f; - - try - { - ofdstream ofs (f); - - ofs << "# Created automatically by the config module." << endl - << "#" << endl - << "out_root = "; - to_stream (ofs, name (out_root), true, '@'); // Quote. - ofs << endl; - - ofs.close (); - } - catch (const io_error& e) - { - fail << "unable to write " << f << ": " << e; - } - } - - using project_set = set<const scope*>; // Use pointers to get comparison. - - static void - save_config (const scope& root, const project_set& projects) - { - path f (config_file (root)); - - if (verb) - text << (verb >= 2 ? "cat >" : "save ") << f; - - const module& mod (*root.lookup_module<const module> (module::name)); - - try - { - ofdstream ofs (f); - - ofs << "# Created automatically by the config module, but feel " << - "free to edit." << endl - << "#" << endl; - - ofs << "config.version = " << module::version << endl; - - if (auto l = root.vars[var_amalgamation]) - { - const dir_path& d (cast<dir_path> (l)); - - ofs << endl - << "# Base configuration inherited from " << d << endl - << "#" << endl; - } - - // Save config variables. - // - names storage; - - for (auto p: mod.saved_modules.order) - { - const string& sname (p.second->first); - const saved_variables& svars (p.second->second); - - bool first (true); // Separate modules with a blank line. - for (const saved_variable& sv: svars) - { - const variable& var (sv.var); - - pair<lookup, size_t> org (root.find_original (var)); - pair<lookup, size_t> ovr (var.overrides == nullptr - ? org - : root.find_override (var, org)); - const lookup& l (ovr.first); - - // We definitely write values that are set on our root scope or - // are global overrides. Anything in-between is presumably - // inherited. We might also not have any value at all (see - // unconfigured()). - // - if (!l.defined ()) - continue; - - if (!(l.belongs (root) || l.belongs (*global_scope))) - { - // This is presumably an inherited value. But it could also be - // some left-over garbage. For example, an amalgamation could - // have used a module but then dropped it while its config - // values are still lingering in config.build. They are probably - // still valid and we should probably continue using them but we - // definitely want to move them to our config.build since they - // will be dropped from the amalgamation's config.build. Let's - // also warn the user just in case. - // - // There is also another case that falls under this now that - // overrides are by default amalgamation-wide rather than just - // "project and subprojects": we may be (re-)configuring a - // subproject but the override is now set on the outer project's - // root. - // - bool found (false); - const scope* r (&root); - while ((r = r->parent_scope ()->root_scope ()) != nullptr) - { - if (l.belongs (*r)) - { - // Find the config module. - // - if (auto* m = r->lookup_module<const module> (module::name)) - { - // Find the corresponding saved module. - // - auto i (m->saved_modules.find (sname)); - - if (i != m->saved_modules.end ()) - { - // Find the variable. - // - const saved_variables& sv (i->second); - found = sv.find (var) != sv.end (); - - // Handle that other case: if this is an override but - // the outer project itself is not being configured, - // then we need to save this override. - // - // One problem with using the already configured project - // set is that the outer project may be configured only - // after us in which case both projects will save the - // value. But perhaps this is a feature, not a bug since - // this is how project-local (%) override behaves. - // - if (found && - org.first != ovr.first && - projects.find (r) == projects.end ()) - found = false; - } - } - - break; - } - } - - if (found) // Inherited. - continue; - - location loc (&f); - - // If this value is not defined in a project's root scope, then - // something is broken. - // - if (r == nullptr) - fail (loc) << "inherited variable " << var << " value " - << "is not from a root scope"; - - // If none of the outer project's configurations use this value, - // then we warn and save as our own. One special case where we - // don't want to warn the user is if the variable is overriden. - // - if (org.first == ovr.first) - { - diag_record dr; - dr << warn (loc) << "saving previously inherited variable " - << var; - - dr << info (loc) << "because project " << *r - << " no longer uses it in its configuration"; - - if (verb >= 2) - { - dr << info (loc) << "variable value: "; - - if (*l) - { - storage.clear (); - dr << "'" << reverse (*l, storage) << "'"; - } - else - dr << "[null]"; - } - } - } - - const string& n (var.name); - const value& v (*l); - - // We will only write config.*.configured if it is false (true is - // implied by its absence). We will also ignore false values if - // there is any other value for this module (see unconfigured()). - // - if (n.size () > 11 && - n.compare (n.size () - 11, 11, ".configured") == 0) - { - if (cast<bool> (v) || svars.size () != 1) - continue; - } - - // If we got here then we are saving this variable. Handle the - // blank line. - // - if (first) - { - ofs << endl; - first = false; - } - - // Handle the save_commented flag. - // - if ((org.first.defined () && org.first->extra) && // Default value. - org.first == ovr.first && // Not overriden. - (sv.flags & save_commented) == save_commented) - { - ofs << '#' << n << " =" << endl; - continue; - } - - if (v) - { - storage.clear (); - names_view ns (reverse (v, storage)); - - ofs << n; - - if (ns.empty ()) - ofs << " ="; - else - { - ofs << " = "; - to_stream (ofs, ns, true, '@'); // Quote. - } - - ofs << endl; - } - else - ofs << n << " = [null]" << endl; - } - } - - ofs.close (); - } - catch (const io_error& e) - { - fail << "unable to write " << f << ": " << e; - } - } - - static void - configure_project (action a, const scope& root, project_set& projects) - { - tracer trace ("configure_project"); - - const dir_path& out_root (root.out_path ()); - const dir_path& src_root (root.src_path ()); - - if (!projects.insert (&root).second) - { - l5 ([&]{trace << "skipping already configured " << out_root;}); - return; - } - - // Make sure the directories exist. - // - if (out_root != src_root) - { - mkdir_p (out_root / root.root_extra->build_dir); - mkdir (out_root / root.root_extra->bootstrap_dir, 2); - } - - // We distinguish between a complete configure and operation- - // specific. - // - if (a.operation () == default_id) - { - l5 ([&]{trace << "completely configuring " << out_root;}); - - // Save src-root.build unless out_root is the same as src. - // - if (out_root != src_root) - save_src_root (root); - - // Save config.build. - // - save_config (root, projects); - } - else - { - } - - // Configure subprojects that have been loaded. - // - if (auto l = root.vars[var_subprojects]) - { - for (auto p: cast<subprojects> (l)) - { - const dir_path& pd (p.second); - dir_path out_nroot (out_root / pd); - const scope& nroot (scopes.find (out_nroot)); - - // @@ Strictly speaking we need to check whether the config - // module was loaded for this subproject. - // - if (nroot.out_path () != out_nroot) // This subproject not loaded. - continue; - - configure_project (a, nroot, projects); - } - } - } - - static void - configure_forward (const scope& root, project_set& projects) - { - tracer trace ("configure_forward"); - - const dir_path& out_root (root.out_path ()); - const dir_path& src_root (root.src_path ()); - - if (!projects.insert (&root).second) - { - l5 ([&]{trace << "skipping already configured " << src_root;}); - return; - } - - mkdir (src_root / root.root_extra->bootstrap_dir, 2); // Make sure exists. - save_out_root (root); - - // Configure subprojects. Since we don't load buildfiles if configuring - // a forward, we do it for all known subprojects. - // - if (auto l = root.vars[var_subprojects]) - { - for (auto p: cast<subprojects> (l)) - { - dir_path out_nroot (out_root / p.second); - const scope& nroot (scopes.find (out_nroot)); - assert (nroot.out_path () == out_nroot); - - configure_forward (nroot, projects); - } - } - } - - operation_id (*pre) (const values&, meta_operation_id, const location&); - - static operation_id - configure_operation_pre (const values&, operation_id o) - { - // Don't translate default to update. In our case unspecified - // means configure everything. - // - return o; - } - - // The (vague) idea is that in the future we may turn this into to some - // sort of key-value sequence (similar to the config initializer idea), - // for example: - // - // configure(out/@src/, forward foo bar@123) - // - // Though using commas instead spaces and '=' instead of '@' would have - // been nicer. - // - static bool - forward (const values& params, - const char* mo = nullptr, - const location& l = location ()) - { - if (params.size () == 1) - { - const names& ns (cast<names> (params[0])); - - if (ns.size () == 1 && ns[0].simple () && ns[0].value == "forward") - return true; - else if (!ns.empty ()) - fail (l) << "unexpected parameter '" << ns << "' for " - << "meta-operation " << mo; - } - else if (!params.empty ()) - fail (l) << "unexpected parameters for meta-operation " << mo; - - return false; - } - - static void - configure_pre (const values& params, const location& l) - { - forward (params, "configure", l); // Validate. - } - - static void - configure_load (const values& params, - scope& root, - const path& buildfile, - const dir_path& out_base, - const dir_path& src_base, - const location& l) - { - if (forward (params)) - { - // We don't need to load the buildfiles in order to configure - // forwarding but in order to configure subprojects we have to - // bootstrap them (similar to disfigure). - // - create_bootstrap_inner (root); - - if (root.out_path () == root.src_path ()) - fail (l) << "forwarding to source directory " << root.src_path (); - } - else - load (params, root, buildfile, out_base, src_base, l); // Normal load. - } - - static void - configure_search (const values& params, - const scope& root, - const scope& base, - const path& bf, - const target_key& tk, - const location& l, - action_targets& ts) - { - if (forward (params)) - { - // For forwarding we only collect the projects (again, similar to - // disfigure). - // - ts.push_back (&root); - } - else - search (params, root, base, bf, tk, l, ts); // Normal search. - } - - static void - configure_match (const values&, action, action_targets&, uint16_t, bool) - { - // Don't match anything -- see execute (). - } - - static void - configure_execute (const values& params, - action a, - action_targets& ts, - uint16_t, - bool) - { - bool fwd (forward (params)); - - project_set projects; - - for (const action_target& at: ts) - { - if (fwd) - { - // Forward configuration. - // - const scope& root (*static_cast<const scope*> (at.target)); - configure_forward (root, projects); - continue; - } - - // Normal configuration. - // - // Match rules to configure every operation supported by each project. - // Note that we are not calling operation_pre/post() callbacks here - // since the meta operation is configure and we know what we are - // doing. - // - // Note that we cannot do this in parallel. We cannot parallelize the - // outer loop because we should match for a single action at a time. - // And we cannot swap the loops because the list of operations is - // target-specific. However, inside match(), things can proceed in - // parallel. - // - const target& t (at.as_target ()); - const scope* rs (t.base_scope ().root_scope ()); - - if (rs == nullptr) - fail << "out of project target " << t; - - const operations& ops (rs->root_extra->operations); - - for (operation_id id (default_id + 1); // Skip default_id. - id < ops.size (); - ++id) - { - if (const operation_info* oif = ops[id]) - { - // Skip aliases (e.g., update-for-install). - // - if (oif->id != id) - continue; - - set_current_oif (*oif); - - phase_lock pl (run_phase::match); - match (action (configure_id, id), t); - } - } - - configure_project (a, *rs, projects); - } - } - - const meta_operation_info mo_configure { - configure_id, - "configure", - "configure", - "configuring", - "configured", - "is configured", - true, // bootstrap_outer - &configure_pre, // meta-operation pre - &configure_operation_pre, - &configure_load, // normal load unless configuring forward - &configure_search, // normal search unless configuring forward - &configure_match, - &configure_execute, - nullptr, // operation post - nullptr, // meta-operation post - nullptr // include - }; - - // disfigure - // - - static bool - disfigure_project (action a, const scope& root, project_set& projects) - { - tracer trace ("disfigure_project"); - - const dir_path& out_root (root.out_path ()); - const dir_path& src_root (root.src_path ()); - - if (!projects.insert (&root).second) - { - l5 ([&]{trace << "skipping already disfigured " << out_root;}); - return false; - } - - bool r (false); // Keep track of whether we actually did anything. - - // Disfigure subprojects. Since we don't load buildfiles during - // disfigure, we do it for all known subprojects. - // - if (auto l = root.vars[var_subprojects]) - { - for (auto p: cast<subprojects> (l)) - { - const dir_path& pd (p.second); - dir_path out_nroot (out_root / pd); - const scope& nroot (scopes.find (out_nroot)); - assert (nroot.out_path () == out_nroot); // See disfigure_load(). - - r = disfigure_project (a, nroot, projects) || r; - - // We use mkdir_p() to create the out_root of a subproject - // which means there could be empty parent directories left - // behind. Clean them up. - // - if (!pd.simple () && out_root != src_root) - { - for (dir_path d (pd.directory ()); - !d.empty (); - d = d.directory ()) - { - rmdir_status s (rmdir (out_root / d, 2)); - - if (s == rmdir_status::not_empty) - break; // No use trying do remove parent ones. - - r = (s == rmdir_status::success) || r; - } - } - } - } - - // We distinguish between a complete disfigure and operation- - // specific. - // - if (a.operation () == default_id) - { - l5 ([&]{trace << "completely disfiguring " << out_root;}); - - r = rmfile (config_file (root)) || r; - - if (out_root != src_root) - { - r = rmfile (out_root / root.root_extra->src_root_file, 2) || r; - - // Clean up the directories. - // - // Note: try to remove the root/ hooks directory if it is empty. - // - r = rmdir (out_root / root.root_extra->root_dir, 2) || r; - r = rmdir (out_root / root.root_extra->bootstrap_dir, 2) || r; - r = rmdir (out_root / root.root_extra->build_dir, 2) || r; - - switch (rmdir (out_root)) - { - case rmdir_status::not_empty: - { - // We used to issue a warning but it is actually a valid usecase - // to leave the build output around in case, for example, of a - // reconfigure. - // - if (verb) - info << "directory " << out_root << " is " - << (out_root == work - ? "current working directory" - : "not empty") << ", not removing"; - break; - } - case rmdir_status::success: - r = true; - default: - break; - } - } - } - else - { - } - - return r; - } - - static bool - disfigure_forward (const scope& root, project_set& projects) - { - // Pretty similar logic to disfigure_project(). - // - tracer trace ("disfigure_forward"); - - const dir_path& out_root (root.out_path ()); - const dir_path& src_root (root.src_path ()); - - if (!projects.insert (&root).second) - { - l5 ([&]{trace << "skipping already disfigured " << src_root;}); - return false; - } - - bool r (false); - - if (auto l = root.vars[var_subprojects]) - { - for (auto p: cast<subprojects> (l)) - { - dir_path out_nroot (out_root / p.second); - const scope& nroot (scopes.find (out_nroot)); - assert (nroot.out_path () == out_nroot); - - r = disfigure_forward (nroot, projects) || r; - } - } - - // Remove the out-root.build file and try to remove the bootstrap/ - // directory if it is empty. - // - r = rmfile (src_root / root.root_extra->out_root_file) || r; - r = rmdir (src_root / root.root_extra->bootstrap_dir, 2) || r; - - return r; - } - - static void - disfigure_pre (const values& params, const location& l) - { - forward (params, "disfigure", l); // Validate. - } - - static operation_id - disfigure_operation_pre (const values&, operation_id o) - { - // Don't translate default to update. In our case unspecified - // means disfigure everything. - // - return o; - } - - static void - disfigure_load (const values&, - scope& root, - const path&, - const dir_path&, - const dir_path&, - const location&) - { - // Since we don't load buildfiles during disfigure but still want to - // disfigure all the subprojects (see disfigure_project() below), we - // bootstrap all the known subprojects. - // - create_bootstrap_inner (root); - } - - static void - disfigure_search (const values&, - const scope& root, - const scope&, - const path&, - const target_key&, - const location&, - action_targets& ts) - { - ts.push_back (&root); - } - - static void - disfigure_match (const values&, action, action_targets&, uint16_t, bool) - { - } - - static void - disfigure_execute (const values& params, - action a, - action_targets& ts, - uint16_t diag, - bool) - { - tracer trace ("disfigure_execute"); - - bool fwd (forward (params)); - - project_set projects; - - // Note: doing everything in the load phase (disfigure_project () does - // modify the build state). - // - for (const action_target& at: ts) - { - const scope& root (*static_cast<const scope*> (at.target)); - - if (!(fwd - ? disfigure_forward ( root, projects) - : disfigure_project (a, root, projects))) - { - // Create a dir{$out_root/} target to signify the project's root in - // diagnostics. Not very clean but seems harmless. - // - target& t ( - targets.insert (dir::static_type, - fwd ? root.src_path () : root.out_path (), - dir_path (), // Out tree. - "", - nullopt, - true, // Implied. - trace).first); - - if (verb != 0 && diag >= 2) - info << diag_done (a, t); - } - } - } - - const meta_operation_info mo_disfigure { - disfigure_id, - "disfigure", - "disfigure", - "disfiguring", - "disfigured", - "is disfigured", - false, // bootstrap_outer - disfigure_pre, // meta-operation pre - &disfigure_operation_pre, - &disfigure_load, - &disfigure_search, - &disfigure_match, - &disfigure_execute, - nullptr, // operation post - nullptr, // meta-operation post - nullptr // include - }; - - // create - // - static void - save_config (const dir_path& d, const variable_overrides& var_ovs) - { - // Since there aren't any sub-projects yet, any config.import.* values - // that the user may want to specify won't be saved in config.build. So - // let's go ahead and mark them all to be saved. To do this, however, we - // need the config module (which is where this information is stored). - // And the module is created by init() during bootstrap. So what we are - // going to do is bootstrap the newly created project, similar to the - // way main() does it. - // - scope& gs (*scope::global_); - scope& rs (load_project (gs, d, d, false /* fwd */, false /* load */)); - module& m (*rs.lookup_module<module> (module::name)); - - // Save all the global config.import.* variables. - // - variable_pool& vp (var_pool.rw (rs)); - for (auto p (gs.vars.find_namespace (vp.insert ("config.import"))); - p.first != p.second; - ++p.first) - { - const variable& var (p.first->first); - - // Annoyingly, this can be (always is?) one of the overrides - // (__override, __prefix, etc). - // - size_t n (var.override ()); - m.save_variable (n != 0 ? *vp.find (string (var.name, 0, n)) : var); - } - - // Now project-specific. For now we just save all of them and let - // save_config() above weed out the ones that don't apply. - // - for (const variable_override& vo: var_ovs) - { - const variable& var (vo.var); - - if (var.name.compare (0, 14, "config.import.") == 0) - m.save_variable (var); - } - } - - const string& - preprocess_create (const variable_overrides& var_ovs, - values& params, - vector_view<opspec>& spec, - bool lifted, - const location& l) - { - tracer trace ("preprocess_create"); - - // The overall plan is to create the project(s), update the buildspec, - // clear the parameters, and then continue as if we were the configure - // meta-operation. - - // Start with process parameters. The first parameter, if any, is a list - // of root.build modules. The second parameter, if any, is a list of - // bootstrap.build modules. If the second is not specified, then the - // default is test, dist, and install (config is mandatory). - // - strings bmod {"test", "dist", "install"}; - strings rmod; - try - { - size_t n (params.size ()); - - if (n > 0) - rmod = convert<strings> (move (params[0])); - - if (n > 1) - bmod = convert<strings> (move (params[1])); - - if (n > 2) - fail (l) << "unexpected parameters for meta-operation create"; - } - catch (const invalid_argument& e) - { - fail (l) << "invalid module name: " << e.what (); - } - - current_oname = empty_string; // Make sure valid. - - // Now handle each target in each operation spec. - // - for (const opspec& os: spec) - { - // First do some sanity checks: there should be no explicit operation - // and our targets should all be directories. - // - if (!lifted && !os.name.empty ()) - fail (l) << "explicit operation specified for meta-operation create"; - - for (const targetspec& ts: os) - { - const name& tn (ts.name); - - // Figure out the project directory. This logic must be consistent - // with find_target_type() and other places (grep for ".."). - // - dir_path d; - - if (tn.simple () && - (tn.empty () || tn.value == "." || tn.value == "..")) - d = dir_path (tn.value); - else if (tn.directory ()) - d = tn.dir; - else if (tn.typed () && tn.type == "dir") - d = tn.dir / dir_path (tn.value); - else - fail(l) << "non-directory target '" << ts << "' in " - << "meta-operation create"; - - if (d.relative ()) - d = work / d; - - d.normalize (true); - - // If src_base was explicitly specified, make sure it is the same as - // the project directory. - // - if (!ts.src_base.empty ()) - { - dir_path s (ts.src_base); - - if (s.relative ()) - s = work / s; - - s.normalize (true); - - if (s != d) - fail(l) << "different src/out directories for target '" << ts - << "' in meta-operation create"; - } - - l5 ([&]{trace << "creating project in " << d;}); - - // For now we disable amalgamating this project. Sooner or later - // someone will probably want to do this, though (i.e., nested - // configurations). - // - create_project (d, - dir_path (), /* amalgamation */ - bmod, - "", /* root_pre */ - rmod, - "", /* root_post */ - true, /* config */ - true, /* buildfile */ - "the create meta-operation"); - - save_config (d, var_ovs); - } - } - - params.clear (); - return mo_configure.name; - } - } -} diff --git a/build2/config/operation.hxx b/build2/config/operation.hxx deleted file mode 100644 index 9f426ca..0000000 --- a/build2/config/operation.hxx +++ /dev/null @@ -1,29 +0,0 @@ -// file : build2/config/operation.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_CONFIG_OPERATION_HXX -#define BUILD2_CONFIG_OPERATION_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/operation.hxx> - -namespace build2 -{ - namespace config - { - extern const meta_operation_info mo_configure; - extern const meta_operation_info mo_disfigure; - - const string& - preprocess_create (const variable_overrides&, - values&, - vector_view<opspec>&, - bool, - const location&); - } -} - -#endif // BUILD2_CONFIG_OPERATION_HXX diff --git a/build2/config/utility.cxx b/build2/config/utility.cxx deleted file mode 100644 index 1ce07f7..0000000 --- a/build2/config/utility.cxx +++ /dev/null @@ -1,307 +0,0 @@ -// file : build2/config/utility.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/config/utility.hxx> - -#include <libbuild2/file.hxx> -#include <libbuild2/context.hxx> -#include <libbuild2/filesystem.hxx> -#include <libbuild2/diagnostics.hxx> - -#include <build2/config/module.hxx> - -using namespace std; - -namespace build2 -{ - namespace config - { - pair<lookup, bool> - omitted (scope& r, const variable& var) - { - // This is a stripped-down version of the required() twisted - // implementation. - - pair<lookup, size_t> org (r.find_original (var)); - - bool n (false); // New flag. - lookup l (org.first); - - // Treat an inherited value that was set to default as new. - // - if (l.defined () && l->extra) - n = true; - - if (var.overrides != nullptr) - { - pair<lookup, size_t> ovr (r.find_override (var, move (org))); - - if (l != ovr.first) // Overriden? - { - // Override is always treated as new. - // - n = true; - l = move (ovr.first); - } - } - - if (l.defined () && current_mif->id == configure_id) - save_variable (r, var); - - return pair<lookup, bool> (l, n); - } - - lookup - optional (scope& r, const variable& var) - { - if (current_mif->id == configure_id) - save_variable (r, var); - - auto l (r[var]); - return l.defined () - ? l - : lookup (r.assign (var), var, r); // NULL. - } - - bool - specified (scope& r, const string& n) - { - // Search all outer scopes for any value in this namespace. - // - // What about "pure" overrides, i.e., those without any original values? - // Well, they will also be found since their names have the original - // variable as a prefix. But do they apply? Yes, since we haven't found - // any original values, they will be "visible"; see find_override() for - // details. - // - const variable& vns (var_pool.rw (r).insert ("config." + n)); - for (scope* s (&r); s != nullptr; s = s->parent_scope ()) - { - for (auto p (s->vars.find_namespace (vns)); - p.first != p.second; - ++p.first) - { - const variable& var (p.first->first); - - // Ignore config.*.configured. - // - if (var.name.size () < 11 || - var.name.compare (var.name.size () - 11, 11, ".configured") != 0) - return true; - } - } - - return false; - } - - bool - unconfigured (scope& rs, const string& n) - { - // Pattern-typed in boot() as bool. - // - const variable& var ( - var_pool.rw (rs).insert ("config." + n + ".configured")); - - if (current_mif->id == configure_id) - save_variable (rs, var); - - auto l (rs[var]); // Include inherited values. - return l && !cast<bool> (l); - } - - bool - unconfigured (scope& rs, const string& n, bool v) - { - // Pattern-typed in boot() as bool. - // - const variable& var ( - var_pool.rw (rs).insert ("config." + n + ".configured")); - - if (current_mif->id == configure_id) - save_variable (rs, var); - - value& x (rs.assign (var)); - - if (x.null || cast<bool> (x) != !v) - { - x = !v; - return true; - } - else - return false; - } - - void - save_variable (scope& r, const variable& var, uint64_t flags) - { - if (current_mif->id != configure_id) - return; - - // The project might not be using the config module. But then how - // could we be configuring it? Good question. - // - if (module* m = r.lookup_module<module> (module::name)) - m->save_variable (var, flags); - } - - void - save_module (scope& r, const char* name, int prio) - { - if (current_mif->id != configure_id) - return; - - if (module* m = r.lookup_module<module> (module::name)) - m->save_module (name, prio); - } - - void - create_project (const dir_path& d, - const build2::optional<dir_path>& amal, - const strings& bmod, - const string& rpre, - const strings& rmod, - const string& rpos, - bool config, - bool buildfile, - const char* who, - uint16_t verbosity) - { - string hdr ("# Generated by " + string (who) + ". Edit if you know" - " what you are doing.\n" - "#"); - - // If the directory exists, verify it's empty. Otherwise, create it. - // - if (exists (d)) - { - if (!empty (d)) - fail << "directory " << d << " exists and is not empty"; - } - else - mkdir_p (d, verbosity); - - // Create the build/ subdirectory. - // - // Note that for now we use the standard build file/directory scheme. - // - mkdir (d / std_build_dir, verbosity); - - // Write build/bootstrap.build. - // - { - path f (d / std_bootstrap_file); - - if (verb >= verbosity) - text << (verb >= 2 ? "cat >" : "save ") << f; - - try - { - ofdstream ofs (f); - - ofs << hdr << endl - << "project =" << endl; - - if (amal) - { - ofs << "amalgamation ="; - - if (!amal->empty ()) - ofs << ' ' << amal->representation (); - - ofs << endl; - } - - ofs << endl; - - if (config) - ofs << "using config" << endl; - - for (const string& m: bmod) - { - if (!config || m != "config") - ofs << "using " << m << endl; - } - - ofs.close (); - } - catch (const io_error& e) - { - fail << "unable to write " << f << ": " << e; - } - } - - // Write build/root.build. - // - { - path f (d / std_root_file); - - if (verb >= verbosity) - text << (verb >= 2 ? "cat >" : "save ") << f; - - try - { - ofdstream ofs (f); - - ofs << hdr << endl; - - if (!rpre.empty ()) - ofs << rpre << endl - << endl; - - for (const string& cm: rmod) - { - // If the module name start with '?', then use optional load. - // - bool opt (cm.front () == '?'); - string m (cm, opt ? 1 : 0); - - // Append .config unless the module name ends with '.', in which - // case strip it. - // - if (m.back () == '.') - m.pop_back (); - else - m += ".config"; - - ofs << "using" << (opt ? "?" : "") << " " << m << endl; - } - - if (!rpos.empty ()) - ofs << endl - << rpre << endl; - - ofs.close (); - } - catch (const io_error& e) - { - fail << "unable to write " << f << ": " << e; - } - } - - // Write root buildfile. - // - if (buildfile) - { - path f (d / std_buildfile_file); - - if (verb >= verbosity) - text << (verb >= 2 ? "cat >" : "save ") << f; - - try - { - ofdstream ofs (f); - - ofs << hdr << endl - << "./: {*/ -build/}" << endl; - - ofs.close (); - } - catch (const io_error& e) - { - fail << "unable to write " << f << ": " << e; - } - } - } - } -} diff --git a/build2/config/utility.hxx b/build2/config/utility.hxx deleted file mode 100644 index 5e4eac2..0000000 --- a/build2/config/utility.hxx +++ /dev/null @@ -1,177 +0,0 @@ -// file : build2/config/utility.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_CONFIG_UTILITY_HXX -#define BUILD2_CONFIG_UTILITY_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/scope.hxx> -#include <libbuild2/variable.hxx> -#include <libbuild2/diagnostics.hxx> - -namespace build2 -{ - class scope; - - namespace config - { - // Set, if necessary, a required config.* variable. - // - // If override is true and the variable doesn't come from this root scope - // or from the command line (i.e., it is inherited from the amalgamtion), - // then its value is "overridden" to the default value on this root scope. - // See save_variable() for more information on save_flags. - // - // Return the reference to the value as well as the indication of whether - // the value is "new", that is, it was set to the default value (inherited - // or not, including overrides). We also treat command line overrides - // (inherited or not) as new. This flag is usually used to test that the - // new value is valid, print report, etc. We return the value as lookup - // (always defined) to pass alone its location (could be used to detect - // inheritance, etc). - // - // Note also that if save_flags has save_commented, then a default value - // is never considered "new" since for such variables absence of a value - // means the default value. - // - template <typename T> - pair<lookup, bool> - required (scope& root, - const variable&, - const T& default_value, - bool override = false, - uint64_t save_flags = 0); - - // Note that the variable is expected to have already been registered. - // - template <typename T> - inline pair<lookup, bool> - required (scope& root, - const string& name, - const T& default_value, - bool override = false, - uint64_t save_flags = 0) - { - return required ( - root, var_pool[name], default_value, override, save_flags); - } - - inline pair<lookup, bool> - required (scope& root, - const string& name, - const char* default_value, - bool override = false, - uint64_t save_flags = 0) - { - return required ( - root, name, string (default_value), override, save_flags); - } - - // As above, but leave the unspecified value as undefined rather than - // setting it to the default value. - // - // This can be useful when we don't have a default value but may figure - // out some fallback. See config.bin.target for an example. - // - pair<lookup, bool> - omitted (scope& root, const variable&); - - // Note that the variable is expected to have already been registered. - // - inline pair<lookup, bool> - omitted (scope& root, const string& name) - { - return omitted (root, var_pool[name]); - } - - // Set, if necessary, an optional config.* variable. In particular, an - // unspecified variable is set to NULL which is used to distinguish - // between the "configured as unspecified" and "not yet configured" cases. - // - // Return the value (as always defined lookup), which can be NULL. - // - // @@ Rename since clashes with the optional class template. - // - lookup - optional (scope& root, const variable&); - - // Note that the variable is expected to have already been registered. - // - inline lookup - optional (scope& root, const string& name) - { - return optional (root, var_pool[name]); - } - - // Check whether there are any variables specified from the config - // namespace. The idea is that we can check if there are any, say, - // config.install.* values. If there are none, then we can assume - // this functionality is not (yet) used and omit writing a whole - // bunch of NULL config.install.* values to the config.build file. - // We call it omitted/delayed configuration. - // - // Note that this function detects and ignores the special - // config.*.configured variable which may be used by a module to - // "remember" that it is unconfigured (e.g., in order to avoid re- - // running the tests, etc). - // - bool - specified (scope& root, const string& name); - - // Check if there is a false config.*.configured value. This mechanism can - // be used to "remember" that the module is left unconfigured in order to - // avoid re-running the tests, etc. - // - bool - unconfigured (scope& root, const string& name); - - // Set the config.*.configured value. Note that you only need to set it to - // false. It will be automatically ignored if there are any other config.* - // values for this module. Return true if this sets a new value. - // - bool - unconfigured (scope& root, const string& name, bool); - - // Enter the variable so that it is saved during configuration. See - // config::module for details. - // - const uint64_t save_commented = 0x01; // Save default value as commented. - - void - save_variable (scope& root, const variable&, uint64_t flags = 0); - - // Establish module order/priority. See config::module for details. - // - void - save_module (scope& root, const char* name, int prio = 0); - - // Create a project in the specified directory. - // - void - create_project (const dir_path& d, - const build2::optional<dir_path>& amalgamation, - const strings& boot_modules, // Bootstrap modules. - const string& root_pre, // Extra root.build text. - const strings& root_modules, // Root modules. - const string& root_post, // Extra root.build text. - bool config, // Load config module. - bool buildfile, // Create root buildfile. - const char* who, // Who is creating it. - uint16_t verbosity = 1); // Diagnostic verbosity. - - inline path - config_file (const scope& root) - { - return (root.out_path () / - root.root_extra->build_dir / - "config." + root.root_extra->build_ext); - } - } -} - -#include <build2/config/utility.txx> - -#endif // BUILD2_CONFIG_UTILITY_HXX diff --git a/build2/config/utility.txx b/build2/config/utility.txx deleted file mode 100644 index 84650d9..0000000 --- a/build2/config/utility.txx +++ /dev/null @@ -1,66 +0,0 @@ -// file : build2/config/utility.txx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <libbuild2/scope.hxx> -#include <libbuild2/context.hxx> - -namespace build2 -{ - namespace config - { - template <typename T> - pair<lookup, bool> - required (scope& root, - const variable& var, - const T& def_val, - bool def_ovr, - uint64_t save_flags) - { - // Note: see also omitted() if changing anything here. - - if (current_mif->id == configure_id) - save_variable (root, var, save_flags); - - pair<lookup, size_t> org (root.find_original (var)); - - bool n (false); // New flag. - lookup l (org.first); - - // The interaction with command line overrides can get tricky. For - // example, the override to defaul value could make (non-recursive) - // command line override in the outer scope no longer apply. So what we - // are going to do is first ignore overrides and perform the normal - // logic on the original. Then we apply the overrides on the result. - // - if (!l.defined () || (def_ovr && !l.belongs (root))) - { - value& v (root.assign (var) = def_val); - v.extra = true; // Default value flag. - - n = (save_flags & save_commented) == 0; // Absence means default. - l = lookup (v, var, root); - org = make_pair (l, 1); // Lookup depth is 1 since it's in root.vars. - } - // Treat an inherited value that was set to default as new. - // - else if (l->extra) - n = (save_flags & save_commented) == 0; // Absence means default. - - if (var.overrides != nullptr) - { - pair<lookup, size_t> ovr (root.find_override (var, move (org))); - - if (l != ovr.first) // Overriden? - { - // Override is always treated as new. - // - n = true; - l = move (ovr.first); - } - } - - return pair<lookup, bool> (l, n); - } - } -} diff --git a/build2/dist/init.cxx b/build2/dist/init.cxx deleted file mode 100644 index 8edbccb..0000000 --- a/build2/dist/init.cxx +++ /dev/null @@ -1,186 +0,0 @@ -// file : build2/dist/init.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/dist/init.hxx> - -#include <libbuild2/scope.hxx> -#include <libbuild2/file.hxx> -#include <libbuild2/diagnostics.hxx> - -#include <build2/config/utility.hxx> - -#include <build2/dist/rule.hxx> -#include <build2/dist/module.hxx> -#include <build2/dist/operation.hxx> - -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace dist - { - static const rule rule_; - - bool - boot (scope& rs, const location&, unique_ptr<module_base>& mod) - { - tracer trace ("dist::boot"); - - l5 ([&]{trace << "for " << rs;}); - - // Register the meta-operation. - // - rs.insert_meta_operation (dist_id, mo_dist); - - // Enter module variables. Do it during boot in case they get assigned - // in bootstrap.build (which is customary for, e.g., dist.package). - // - auto& vp (var_pool.rw (rs)); - - // Note: some overridable, some not. - // - // config.dist.archives is a list of archive extensions (e.g., zip, - // tar.gz) that can be optionally prefixed with a directory. If it is - // relative, then it is prefixed with config.dist.root. Otherwise, the - // archive is written to the absolute location. - // - // config.dist.checksums is a list of archive checksum extensions (e.g., - // sha1, sha256) that can also be optionally prefixed with a directory - // with the same semantics as config.dist.archives. If the directory is - // absent, then the checksum file is written into the same directory as - // the corresponding archive. - // - vp.insert<abs_dir_path> ("config.dist.root", true); - vp.insert<paths> ("config.dist.archives", true); - vp.insert<paths> ("config.dist.checksums", true); - vp.insert<path> ("config.dist.cmd", true); - - // Allow distribution of uncommitted projects. This is enforced by the - // version module. - // - vp.insert<bool> ("config.dist.uncommitted", true); - - vp.insert<dir_path> ("dist.root"); - vp.insert<process_path> ("dist.cmd"); - vp.insert<paths> ("dist.archives"); - vp.insert<paths> ("dist.checksums"); - vp.insert<paths> ("dist.uncommitted"); - - vp.insert<bool> ("dist", variable_visibility::target); // Flag. - - // Project's package name. - // - auto& v_d_p ( - vp.insert<string> ("dist.package", variable_visibility::project)); - - // Create the module. - // - mod.reset (new module (v_d_p)); - - return false; - } - - bool - init (scope& rs, - scope&, - const location& l, - unique_ptr<module_base>&, - bool first, - bool, - const variable_map& config_hints) - { - tracer trace ("dist::init"); - - if (!first) - { - warn (l) << "multiple dist module initializations"; - return true; - } - - const dir_path& out_root (rs.out_path ()); - l5 ([&]{trace << "for " << out_root;}); - - assert (config_hints.empty ()); // We don't known any hints. - - // Register our wildcard rule. Do it explicitly for the alias to prevent - // something like insert<target>(dist_id, test_id) taking precedence. - // - rs.rules.insert<target> (dist_id, 0, "dist", rule_); - rs.rules.insert<alias> (dist_id, 0, "dist.alias", rule_); //@@ outer? - - // Configuration. - // - // Note that we don't use any defaults for root -- the location - // must be explicitly specified or we will complain if and when - // we try to dist. - // - bool s (config::specified (rs, "dist")); - - // Adjust module priority so that the config.dist.* values are saved at - // the end of config.build. - // - if (s) - config::save_module (rs, "dist", INT32_MAX); - - // dist.root - // - { - value& v (rs.assign ("dist.root")); - - if (s) - { - if (lookup l = config::optional (rs, "config.dist.root")) - v = cast<dir_path> (l); // Strip abs_dir_path. - } - } - - // dist.cmd - // - { - value& v (rs.assign<process_path> ("dist.cmd")); - - if (s) - { - if (lookup l = config::required (rs, - "config.dist.cmd", - path ("install")).first) - v = run_search (cast<path> (l), true); - } - } - - // dist.archives - // dist.checksums - // - { - value& a (rs.assign ("dist.archives")); - value& c (rs.assign ("dist.checksums")); - - if (s) - { - if (lookup l = config::optional (rs, "config.dist.archives")) - a = *l; - - if (lookup l = config::optional (rs, "config.dist.checksums")) - { - c = *l; - - if (!c.empty () && (!a || a.empty ())) - fail << "config.dist.checksums specified without " - << "config.dist.archives"; - - } - } - } - - // dist.uncommitted - // - // Omit it from the configuration unless specified. - // - config::omitted (rs, "config.dist.uncommitted"); - - return true; - } - } -} diff --git a/build2/dist/init.hxx b/build2/dist/init.hxx deleted file mode 100644 index 0449a99..0000000 --- a/build2/dist/init.hxx +++ /dev/null @@ -1,31 +0,0 @@ -// file : build2/dist/init.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_DIST_INIT_HXX -#define BUILD2_DIST_INIT_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/module.hxx> - -namespace build2 -{ - namespace dist - { - bool - boot (scope&, const location&, unique_ptr<module_base>&); - - bool - init (scope&, - scope&, - const location&, - unique_ptr<module_base>&, - bool, - bool, - const variable_map&); - } -} - -#endif // BUILD2_DIST_INIT_HXX diff --git a/build2/dist/module.cxx b/build2/dist/module.cxx deleted file mode 100644 index 2d619f4..0000000 --- a/build2/dist/module.cxx +++ /dev/null @@ -1,15 +0,0 @@ -// file : build2/dist/module.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/dist/module.hxx> - -using namespace std; - -namespace build2 -{ - namespace dist - { - const string module::name ("dist"); - } -} diff --git a/build2/dist/module.hxx b/build2/dist/module.hxx deleted file mode 100644 index cd0d9a3..0000000 --- a/build2/dist/module.hxx +++ /dev/null @@ -1,69 +0,0 @@ -// file : build2/dist/module.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_DIST_MODULE_HXX -#define BUILD2_DIST_MODULE_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/module.hxx> -#include <libbuild2/variable.hxx> - -namespace build2 -{ - namespace dist - { - struct module: module_base - { - static const string name; - - const variable& var_dist_package; - - // Distribution post-processing callbacks. - // - // The last component in the pattern may contain shell wildcards. If the - // path contains a directory, then it is matched from the distribution - // root only. Otherwise, it is matched against all the files being - // distributed. For example: - // - // buildfile - every buildfile - // ./buildfile - root buildfile only - // tests/buildfile - tests/buildfile only - // - // The callback is called with the absolute path of the matching file - // after it has been copied to the distribution directory. The project's - // root scope and callback-specific data are passed along. - // - // Note that if registered, the callbacks are also called (recursively) - // in subprojects. - // - using callback_func = void (const path&, const scope&, void*); - - void - register_callback (path pattern, callback_func* f, void* data) - { - callbacks_.push_back (callback {move (pattern), f, data}); - } - - // Implementation details. - // - module (const variable& v_d_p) - : var_dist_package (v_d_p) {} - - public: - struct callback - { - const path pattern; - callback_func* function; - void* data; - }; - using callbacks = vector<callback>; - - callbacks callbacks_; - }; - } -} - -#endif // BUILD2_DIST_MODULE_HXX diff --git a/build2/dist/operation.cxx b/build2/dist/operation.cxx deleted file mode 100644 index ca90b50..0000000 --- a/build2/dist/operation.cxx +++ /dev/null @@ -1,868 +0,0 @@ -// file : build2/dist/operation.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/dist/operation.hxx> - -#include <libbutl/sha1.mxx> -#include <libbutl/sha256.mxx> - -#include <libbutl/filesystem.mxx> // path_match() - -#include <libbuild2/file.hxx> -#include <libbuild2/dump.hxx> -#include <libbuild2/scope.hxx> -#include <libbuild2/target.hxx> -#include <libbuild2/context.hxx> -#include <libbuild2/algorithm.hxx> -#include <libbuild2/filesystem.hxx> -#include <libbuild2/diagnostics.hxx> - -#include <build2/dist/module.hxx> - -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace dist - { - // install -d <dir> - // - static void - install (const process_path& cmd, const dir_path&); - - // install <file> <dir> - // - // Return the destination file path. - // - static path - install (const process_path& cmd, const file&, const dir_path&); - - // tar|zip ... <dir>/<pkg>.<ext> <pkg> - // - // Return the archive file path. - // - static path - archive (const dir_path& root, - const string& pkg, - const dir_path& dir, - const string& ext); - - // <ext>sum <arc> > <dir>/<arc>.<ext> - // - // Return the checksum file path. - // - static path - checksum (const path& arc, const dir_path& dir, const string& ext); - - static operation_id - dist_operation_pre (const values&, operation_id o) - { - if (o != default_id) - fail << "explicit operation specified for meta-operation dist"; - - return o; - } - - static void - dist_execute (const values&, action, action_targets& ts, - uint16_t, bool prog) - { - tracer trace ("dist_execute"); - - // For now we assume all the targets are from the same project. - // - const target& t (ts[0].as_target ()); - const scope* rs (t.base_scope ().root_scope ()); - - if (rs == nullptr) - fail << "out of project target " << t; - - const dir_path& out_root (rs->out_path ()); - const dir_path& src_root (rs->src_path ()); - - if (out_root == src_root) - fail << "in-tree distribution of target " << t << - info << "distribution requires out-of-tree build"; - - // Make sure we have the necessary configuration before we get down to - // business. - // - auto l (rs->vars["dist.root"]); - - if (!l || l->empty ()) - fail << "unknown root distribution directory" << - info << "did you forget to specify config.dist.root?"; - - // We used to complain if dist.root does not exist but then, similar - // to install, got tired of user's complaints. So now we just let - // install -d for the package directory create it if necessary. - // - const dir_path& dist_root (cast<dir_path> (l)); - - l = rs->vars["dist.package"]; - - if (!l || l->empty ()) - fail << "unknown distribution package name" << - info << "did you forget to set dist.package?"; - - const string& dist_package (cast<string> (l)); - const process_path& dist_cmd (cast<process_path> (rs->vars["dist.cmd"])); - - // Verify all the targets are from the same project. - // - for (const action_target& at: ts) - { - const target& t (at.as_target ()); - - if (rs != t.base_scope ().root_scope ()) - fail << "target " << t << " is from a different project" << - info << "one dist meta-operation can handle one project" << - info << "consider using several dist meta-operations"; - } - - // We used to print 'dist <target>' at verbosity level 1 but that has - // proven to be just noise. Though we still want to print something - // since otherwise, once the progress line is cleared, we may end up - // with nothing printed at all. - // - // Note that because of this we can also suppress diagnostics noise - // (e.g., output directory creation) in all the operations below. - // - if (verb == 1) - text << "dist " << dist_package; - - // Match a rule for every operation supported by this project. Skip - // default_id. - // - // Note that we are not calling operation_pre/post() callbacks here - // since the meta operation is dist and we know what we are doing. - // - values params; - const path locf ("<dist>"); - const location loc (&locf); // Dummy location. - - const operations& ops (rs->root_extra->operations); - - for (operations::size_type id (default_id + 1); // Skip default_id. - id < ops.size (); - ++id) - { - if (const operation_info* oif = ops[id]) - { - // Skip aliases (e.g., update-for-install). In fact, one can argue - // the default update should be sufficient since it is assumed to - // update all prerequisites and we no longer support ad hoc stuff - // like test.input. Though here we are using the dist meta-operation, - // not perform. - // - if (oif->id != id) - continue; - - // Use standard (perform) match. - // - if (oif->pre != nullptr) - { - if (operation_id pid = oif->pre (params, dist_id, loc)) - { - const operation_info* poif (ops[pid]); - set_current_oif (*poif, oif, false /* diag_noise */); - action a (dist_id, poif->id, oif->id); - match (params, a, ts, - 1 /* diag (failures only) */, - false /* progress */); - } - } - - set_current_oif (*oif, nullptr, false /* diag_noise */); - action a (dist_id, oif->id); - match (params, a, ts, - 1 /* diag (failures only) */, - false /* progress */); - - if (oif->post != nullptr) - { - if (operation_id pid = oif->post (params, dist_id)) - { - const operation_info* poif (ops[pid]); - set_current_oif (*poif, oif, false /* diag_noise */); - action a (dist_id, poif->id, oif->id); - match (params, a, ts, - 1 /* diag (failures only) */, - false /* progress */); - } - } - } - } - - // Add buildfiles that are not normally loaded as part of the project, - // for example, the export stub. They will still be ignored on the next - // step if the user explicitly marked them dist=false. - // - auto add_adhoc = [&trace] (const scope& rs, const path& f) - { - path p (rs.src_path () / f); - if (exists (p)) - { - dir_path d (p.directory ()); - - // Figure out if we need out. - // - dir_path out (rs.src_path () != rs.out_path () - ? out_src (d, rs) - : dir_path ()); - - targets.insert<buildfile> ( - move (d), - move (out), - p.leaf ().base ().string (), - p.extension (), // Specified. - trace); - } - }; - - add_adhoc (*rs, rs->root_extra->export_file); - - // The same for subprojects that have been loaded. - // - if (auto l = rs->vars[var_subprojects]) - { - for (auto p: cast<subprojects> (l)) - { - const dir_path& pd (p.second); - dir_path out_nroot (out_root / pd); - const scope& nrs (scopes.find (out_nroot)); - - if (nrs.out_path () != out_nroot) // This subproject not loaded. - continue; - - if (!nrs.src_path ().sub (src_root)) // Not a strong amalgamation. - continue; - - add_adhoc (nrs, nrs.root_extra->export_file); - } - } - - // Collect the files. We want to take the snapshot of targets since - // updating some of them may result in more targets being entered. - // - // Note that we are not showing progress here (e.g., "N targets to - // distribute") since it will be useless (too fast). - // - action_targets files; - const variable& dist_var (var_pool["dist"]); - - for (const auto& pt: targets) - { - file* ft (pt->is_a<file> ()); - - if (ft == nullptr) // Not a file. - continue; - - if (ft->dir.sub (src_root)) - { - // Include unless explicitly excluded. - // - auto l ((*ft)[dist_var]); - - if (l && !cast<bool> (l)) - l5 ([&]{trace << "excluding " << *ft;}); - else - files.push_back (ft); - - continue; - } - - if (ft->dir.sub (out_root)) - { - // Exclude unless explicitly included. - // - auto l ((*ft)[dist_var]); - - if (l && cast<bool> (l)) - { - l5 ([&]{trace << "including " << *ft;}); - files.push_back (ft); - } - - continue; - } - } - - // Make sure what we need to distribute is up to date. - // - { - if (mo_perform.meta_operation_pre != nullptr) - mo_perform.meta_operation_pre (params, loc); - - // This is a hack since according to the rules we need to completely - // reset the state. We could have done that (i.e., saved target names - // and then re-searched them in the new tree) but that would just slow - // things down while this little cheat seems harmless (i.e., assume - // the dist mete-opreation is "compatible" with perform). - // - // Note also that we don't do any structured result printing. - // - size_t on (current_on); - set_current_mif (mo_perform); - current_on = on + 1; - - if (mo_perform.operation_pre != nullptr) - mo_perform.operation_pre (params, update_id); - - set_current_oif (op_update, nullptr, false /* diag_noise */); - - action a (perform_id, update_id); - - mo_perform.match (params, a, files, - 1 /* diag (failures only) */, - prog /* progress */); - - mo_perform.execute (params, a, files, - 1 /* diag (failures only) */, - prog /* progress */); - - if (mo_perform.operation_post != nullptr) - mo_perform.operation_post (params, update_id); - - if (mo_perform.meta_operation_post != nullptr) - mo_perform.meta_operation_post (params); - } - - dir_path td (dist_root / dir_path (dist_package)); - - // Clean up the target directory. - // - if (build2::rmdir_r (td, true, 2) == rmdir_status::not_empty) - fail << "unable to clean target directory " << td; - - auto_rmdir rm_td (td); // Clean it up if things go bad. - install (dist_cmd, td); - - // Copy over all the files. Apply post-processing callbacks. - // - module& mod (*rs->lookup_module<module> (module::name)); - - prog = prog && show_progress (1 /* max_verb */); - size_t prog_percent (0); - - for (size_t i (0), n (files.size ()); i != n; ++i) - { - const file& t (*files[i].as_target ().is_a<file> ()); - - // Figure out where this file is inside the target directory. - // - bool src (t.dir.sub (src_root)); - dir_path dl (src ? t.dir.leaf (src_root) : t.dir.leaf (out_root)); - - dir_path d (td / dl); - if (!exists (d)) - install (dist_cmd, d); - - path r (install (dist_cmd, t, d)); - - // See if this file is in a subproject. - // - const scope* srs (rs); - const module::callbacks* cbs (&mod.callbacks_); - - if (auto l = rs->vars[var_subprojects]) - { - for (auto p: cast<subprojects> (l)) - { - const dir_path& pd (p.second); - if (dl.sub (pd)) - { - srs = &scopes.find (out_root / pd); - - if (auto* m = srs->lookup_module<module> (module::name)) - cbs = &m->callbacks_; - else - fail << "dist module not loaded in subproject " << pd; - - break; - } - } - } - - for (module::callback cb: *cbs) - { - const path& pat (cb.pattern); - - // If we have a directory, then it should be relative to the project - // root. - // - if (!pat.simple ()) - { - assert (pat.relative ()); - - dir_path d ((src ? srs->src_path () : srs->out_path ()) / - pat.directory ()); - d.normalize (); - - if (d != t.dir) - continue; - } - - if (path_match (pat.leaf ().string (), t.path ().leaf ().string ())) - cb.function (r, *srs, cb.data); - } - - if (prog) - { - // Note that this is not merely an optimization since if stderr is - // not a terminal, we print real lines for progress. - // - size_t p ((i * 100) / n); - - if (prog_percent != p) - { - prog_percent = p; - - diag_progress_lock pl; - diag_progress = ' '; - diag_progress += to_string (prog_percent); - diag_progress += "% of targets distributed"; - } - } - } - - // Clear the progress if shown. - // - if (prog) - { - diag_progress_lock pl; - diag_progress.clear (); - } - - rm_td.cancel (); - - // Archive and checksum if requested. - // - if (lookup as = rs->vars["dist.archives"]) - { - lookup cs (rs->vars["dist.checksums"]); - - // Split the dist.{archives,checksums} value into a directory and - // extension. - // - auto split = [] (const path& p, const dir_path& r, const char* what) - { - dir_path d (p.relative () ? r : dir_path ()); - d /= p.directory (); - - const string& s (p.string ()); - size_t i (path::traits_type::find_leaf (s)); - - if (i == string::npos) - fail << "invalid extension '" << s << "' in " << what; - - if (s[i] == '.') // Skip the dot if specified. - ++i; - - return pair<dir_path, string> (move (d), string (s, i)); - }; - - for (const path& p: cast<paths> (as)) - { - auto ap (split (p, dist_root, "dist.archives")); - path a (archive (dist_root, dist_package, ap.first, ap.second)); - - if (cs) - { - for (const path& p: cast<paths> (cs)) - { - auto cp (split (p, ap.first, "dist.checksums")); - checksum (a, cp.first, cp.second); - } - } - } - } - } - - // install -d <dir> - // - static void - install (const process_path& cmd, const dir_path& d) - { - path reld (relative (d)); - - cstrings args {cmd.recall_string (), "-d"}; - - args.push_back ("-m"); - args.push_back ("755"); - args.push_back (reld.string ().c_str ()); - args.push_back (nullptr); - - if (verb >= 2) - print_process (args); - - run (cmd, args); - } - - // install <file> <dir> - // - static path - install (const process_path& cmd, const file& t, const dir_path& d) - { - dir_path reld (relative (d)); - path relf (relative (t.path ())); - - cstrings args {cmd.recall_string ()}; - - // Preserve timestamps. This could becomes important if, for - // example, we have pre-generated sources. Note that the - // install-sh script doesn't support this option, while both - // Linux and BSD install's do. - // - args.push_back ("-p"); - - // Assume the file is executable if the owner has execute - // permission, in which case we make it executable for - // everyone. - // - args.push_back ("-m"); - args.push_back ( - (path_permissions (t.path ()) & permissions::xu) == permissions::xu - ? "755" - : "644"); - - args.push_back (relf.string ().c_str ()); - args.push_back (reld.string ().c_str ()); - args.push_back (nullptr); - - if (verb >= 2) - print_process (args); - - run (cmd, args); - - return d / relf.leaf (); - } - - static path - archive (const dir_path& root, - const string& pkg, - const dir_path& dir, - const string& e) - { - path an (pkg + '.' + e); - - // Delete old archive for good measure. - // - path ap (dir / an); - if (exists (ap, false)) - rmfile (ap); - - // Use zip for .zip archives. Also recognize and handle a few well-known - // tar.xx cases (in case tar doesn't support -a or has other issues like - // MSYS). Everything else goes to tar in the auto-compress mode (-a). - // - cstrings args; - - // Separate compressor (gzip, xz, etc) state. - // - size_t i (0); // Command line start or 0 if not used. - auto_rmfile out_rm; // Output file cleanup (must come first). - auto_fd out_fd; // Output file. - - if (e == "zip") - { - // On Windows we use libarchive's bsdtar (zip is an MSYS executabales). - // - // While not explicitly stated, the compression-level option works - // for zip archives. - // -#ifdef _WIN32 - args = {"bsdtar", - "-a", // -a with the .zip extension seems to be the only way. - "--options=compression-level=9", - "-cf", ap.string ().c_str (), - pkg.c_str (), - nullptr}; -#else - args = {"zip", - "-9", - "-rq", ap.string ().c_str (), - pkg.c_str (), - nullptr}; -#endif - } - else - { - // On Windows we use libarchive's bsdtar with auto-compression (tar - // itself and quite a few compressors are MSYS executables). - // - const char* l (nullptr); // Compression level (option). - -#ifdef _WIN32 - const char* tar = "bsdtar"; - - if (e == "tar.gz") - l = "--options=compression-level=9"; -#else - const char* tar = "tar"; - - // For gzip it's a good idea to use -9 by default. For bzip2, -9 is - // the default. And for xz, -9 is not recommended as the default due - // memory requirements. - // - // Note also that the compression level can be altered via the GZIP - // (GZIP_OPT also seems to work), BZIP2, and XZ_OPT environment - // variables, respectively. - // - const char* c (nullptr); - - if (e == "tar.gz") { c = "gzip"; l = "-9"; } - else if (e == "tar.xz") { c = "xz"; } - else if (e == "tar.bz2") { c = "bzip2"; } - - if (c != nullptr) - { - args = {tar, - "--format", "ustar", - "-cf", "-", - pkg.c_str (), - nullptr}; - - i = args.size (); - args.push_back (c); - if (l != nullptr) - args.push_back (l); - args.push_back (nullptr); - args.push_back (nullptr); // Pipe end. - - try - { - out_fd = fdopen (ap, - fdopen_mode::out | fdopen_mode::binary | - fdopen_mode::truncate | fdopen_mode::create); - out_rm = auto_rmfile (ap); - } - catch (const io_error& e) - { - fail << "unable to open " << ap << ": " << e; - } - } - else -#endif - if (e == "tar") - args = {tar, - "--format", "ustar", - "-cf", ap.string ().c_str (), - pkg.c_str (), - nullptr}; - else - { - args = {tar, - "--format", "ustar", - "-a"}; - - if (l != nullptr) - args.push_back (l); - - args.push_back ("-cf"); - args.push_back (ap.string ().c_str ()); - args.push_back (pkg.c_str ()); - args.push_back (nullptr); - } - } - - process_path app; // Archiver path. - process_path cpp; // Compressor path. - - app = run_search (args[0]); - - if (i != 0) - cpp = run_search (args[i]); - - if (verb >= 2) - print_process (args); - else if (verb) - text << args[0] << ' ' << ap; - - process apr; - process cpr; - - // Change the archiver's working directory to dist_root. - // - apr = run_start (app, - args.data (), - 0 /* stdin */, - (i != 0 ? -1 : 1) /* stdout */, - true /* error */, - root); - - // Start the compressor if required. - // - if (i != 0) - { - cpr = run_start (cpp, - args.data () + i, - apr.in_ofd.get () /* stdin */, - out_fd.get () /* stdout */); - - cpr.in_ofd.reset (); // Close the archiver's stdout on our side. - run_finish (args.data () + i, cpr); - } - - run_finish (args.data (), apr); - - out_rm.cancel (); - return ap; - } - - static path - checksum (const path& ap, const dir_path& dir, const string& e) - { - path an (ap.leaf ()); - dir_path ad (ap.directory ()); - - path cn (an + '.' + e); - - // Delete old checksum for good measure. - // - path cp (dir / cn); - if (exists (cp, false)) - rmfile (cp); - - auto_rmfile c_rm; // Note: must come first. - auto_fd c_fd; - try - { - c_fd = fdopen (cp, - fdopen_mode::out | - fdopen_mode::create | - fdopen_mode::truncate); - c_rm = auto_rmfile (cp); - } - catch (const io_error& e) - { - fail << "unable to open " << cp << ": " << e; - } - - // The plan is as follows: look for the <ext>sum program (e.g., sha1sum, - // md5sum, etc). If found, then use that, otherwise, fall back to our - // built-in checksum calculation support. - // - // There are two benefits to first trying the external program: it may - // supports more checksum algorithms and could be faster than our - // built-in code. - // - string pn (e + "sum"); - process_path pp (process::try_path_search (pn, true /* init */)); - - if (!pp.empty ()) - { - const char* args[] { - pp.recall_string (), - "-b" /* binary */, - an.string ().c_str (), - nullptr}; - - if (verb >= 2) - print_process (args); - else if (verb) - text << args[0] << ' ' << cp; - - // Note that to only get the archive name (without the directory) in - // the output we have to run from the archive's directory. - // - process pr (run_start (pp, - args, - 0 /* stdin */, - c_fd.get () /* stdout */, - true /* error */, - ad /* cwd */)); - run_finish (args, pr); - } - else - { - string (*f) (ifdstream&); - - // Note: remember to update info: below if adding another algorithm. - // - if (e == "sha1") - f = [] (ifdstream& i) -> string {return sha1 (i).string ();}; - else if (e == "sha256") - f = [] (ifdstream& i) -> string {return sha256 (i).string ();}; - else - fail << "no built-in support for checksum algorithm " << e - << " nor " << e << "sum program found" << - info << "built-in support is available for sha1, sha256" << endf; - - if (verb >= 2) - text << "cat >" << cp; - else if (verb) - text << e << "sum " << cp; - - string c; - try - { - ifdstream is (ap, fdopen_mode::in | fdopen_mode::binary); - c = f (is); - is.close (); - } - catch (const io_error& e) - { - fail << "unable to read " << ap << ": " << e; - } - - try - { - ofdstream os (move (c_fd)); - os << c << " *" << an << endl; - os.close (); - } - catch (const io_error& e) - { - fail << "unable to write " << cp << ": " << e; - } - } - - c_rm.cancel (); - return cp; - } - - static include_type - dist_include (action, - const target&, - const prerequisite_member& p, - include_type i) - { - tracer trace ("dist_include"); - - // Override excluded to adhoc so that every source is included into the - // distribution. Note that this should be harmless to a custom rule - // given the prescribed semantics of adhoc (match/execute but otherwise - // ignore) is followed. - // - if (i == include_type::excluded) - { - l5 ([&]{trace << "overriding exclusion of " << p;}); - i = include_type::adhoc; - } - - return i; - } - - const meta_operation_info mo_dist { - dist_id, - "dist", - "distribute", - "distributing", - "distributed", - "has nothing to distribute", // We cannot "be distributed". - true, // bootstrap_outer - nullptr, // meta-operation pre - &dist_operation_pre, - &load, // normal load - &search, // normal search - nullptr, // no match (see dist_execute()). - &dist_execute, - nullptr, // operation post - nullptr, // meta-operation post - &dist_include - }; - } -} diff --git a/build2/dist/operation.hxx b/build2/dist/operation.hxx deleted file mode 100644 index 00d8664..0000000 --- a/build2/dist/operation.hxx +++ /dev/null @@ -1,21 +0,0 @@ -// file : build2/dist/operation.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_DIST_OPERATION_HXX -#define BUILD2_DIST_OPERATION_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/operation.hxx> - -namespace build2 -{ - namespace dist - { - extern const meta_operation_info mo_dist; - } -} - -#endif // BUILD2_DIST_OPERATION_HXX diff --git a/build2/dist/rule.cxx b/build2/dist/rule.cxx deleted file mode 100644 index c877abc..0000000 --- a/build2/dist/rule.cxx +++ /dev/null @@ -1,88 +0,0 @@ -// file : build2/dist/rule.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/dist/rule.hxx> - -#include <libbuild2/scope.hxx> -#include <libbuild2/target.hxx> -#include <libbuild2/algorithm.hxx> -#include <libbuild2/diagnostics.hxx> - -using namespace std; - -namespace build2 -{ - namespace dist - { - bool rule:: - match (action, target&, const string&) const - { - return true; // We always match. - } - - recipe rule:: - apply (action a, target& t) const - { - const dir_path& out_root (t.root_scope ().out_path ()); - - // If we can, go inside see-through groups. - // - for (prerequisite_member p: - group_prerequisite_members (a, t, members_mode::maybe)) - { - // Note: no exclusion tests, we want all of them (and see also the - // dist_include() override). - - // Skip prerequisites imported from other projects. - // - if (p.proj ()) - continue; - - // We used to always search and match but that resulted in the - // undesirable behavior in case one of the "source" files is - // missing. In this case we would enter a target as "output", this - // rule would match it, and then dist_execute() would ignore it by - // default. - // - // So now if this is a file target (we still want to always "see - // through" other targets like aliases), we will only match it if (1) - // it exists in src or (2) it exists as a target. It feels like we - // don't need the stronger "... and not implied" condition since if it - // is mentioned as a target, then it is in out (we don't do the same - // target in both src/out). - // - // @@ Note that this is still an issue in a custom dist rule. - // - const target* pt (nullptr); - if (p.is_a<file> ()) - { - pt = p.load (); - - if (pt == nullptr) - { - // Search for an existing target or existing file in src. - // - const prerequisite_key& k (p.key ()); - pt = k.tk.type->search (t, k); - - if (pt == nullptr) - fail << "prerequisite " << k << " is not existing source file " - << "nor known output target" << endf; - - search_custom (p.prerequisite, *pt); // Cache. - } - } - else - pt = &p.search (t); - - // Don't match targets that are outside of our project. - // - if (pt->dir.sub (out_root)) - build2::match (a, *pt); - } - - return noop_recipe; // We will never be executed. - } - } -} diff --git a/build2/dist/rule.hxx b/build2/dist/rule.hxx deleted file mode 100644 index accce4c..0000000 --- a/build2/dist/rule.hxx +++ /dev/null @@ -1,39 +0,0 @@ -// file : build2/dist/rule.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_DIST_RULE_HXX -#define BUILD2_DIST_RULE_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/rule.hxx> -#include <libbuild2/action.hxx> -#include <libbuild2/target.hxx> - -namespace build2 -{ - namespace dist - { - // This is the default rule that simply matches all the prerequisites. - // - // A custom rule (usually the same as perform_update) may be necessary to - // establish group links (so that we see the dist variable set on a - // group). - // - class rule: public build2::rule - { - public: - rule () {} - - virtual bool - match (action, target&, const string&) const override; - - virtual recipe - apply (action, target&) const override; - }; - } -} - -#endif // BUILD2_DIST_RULE_HXX diff --git a/build2/install/functions.cxx b/build2/install/functions.cxx deleted file mode 100644 index 5780fd8..0000000 --- a/build2/install/functions.cxx +++ /dev/null @@ -1,33 +0,0 @@ -// file : build2/install/functions.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <libbuild2/function.hxx> -#include <libbuild2/variable.hxx> - -#include <build2/install/utility.hxx> - -using namespace std; - -namespace build2 -{ - namespace install - { - void - functions () - { - function_family f ("install"); - - // Resolve potentially relative install.* value to an absolute directory - // based on (other) install.* values visible from the calling scope. - // - f[".resolve"] = [] (const scope* s, dir_path d) - { - if (s == nullptr) - fail << "install.resolve() called out of scope" << endf; - - return resolve_dir (*s, move (d)); - }; - } - } -} diff --git a/build2/install/init.cxx b/build2/install/init.cxx deleted file mode 100644 index 055b8b1..0000000 --- a/build2/install/init.cxx +++ /dev/null @@ -1,303 +0,0 @@ -// file : build2/install/init.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/install/init.hxx> - -#include <libbuild2/scope.hxx> -#include <libbuild2/target.hxx> -#include <libbuild2/rule.hxx> -#include <libbuild2/function.hxx> -#include <libbuild2/operation.hxx> -#include <libbuild2/diagnostics.hxx> - -#include <build2/config/utility.hxx> - -#include <build2/install/rule.hxx> -#include <build2/install/utility.hxx> -#include <build2/install/operation.hxx> - -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace install - { - // Set install.<name>.* values based on config.install.<name>.* ones - // or the defaults. If none of config.install.* values were specified, - // then we do omitted/delayed configuration. Note that we still need - // to set all the install.* values to defaults, as if we had the - // default configuration. - // - // If override is true, then override values that came from outer - // configurations. We have to do this for paths that contain the - // package name. - // - // For global values we only set config.install.* variables. Non-global - // values with NULL defaults are omitted. - // - template <typename T, typename CT> - static void - set_var (bool spec, - scope& r, - const char* name, - const char* var, - const CT* dv, - bool override = false) - { - string vn; - lookup l; - - bool global (*name == '\0'); - - if (spec) - { - // Note: overridable. - // - vn = "config.install"; - if (!global) - { - vn += '.'; - vn += name; - } - vn += var; - const variable& vr (var_pool.rw (r).insert<CT> (move (vn), true)); - - l = dv != nullptr - ? config::required (r, vr, *dv, override).first - : (global - ? config::optional (r, vr) - : config::omitted (r, vr).first); - } - - if (global) - return; - - // Note: not overridable. - // - vn = "install."; - vn += name; - vn += var; - const variable& vr (var_pool.rw (r).insert<T> (move (vn))); - - value& v (r.assign (vr)); - - if (spec) - { - if (l) - v = cast<T> (l); // Strip CT to T. - } - else - { - if (dv != nullptr) - v = *dv; - } - } - - template <typename T> - static void - set_dir (bool s, // specified - scope& r, // root scope - const char* n, // var name - const T& p, // path - bool o = false, // override - const string& fm = string (), // file mode - const string& dm = string (), // dir mode - const build2::path& c = build2::path ()) // command - { - using build2::path; - - bool global (*n == '\0'); - - if (!global) - set_var<dir_path> (s, r, n, "", p.empty () ? nullptr : &p, o); - - set_var<path> (s, r, n, ".cmd", c.empty () ? nullptr : &c); - set_var<strings> (s, r, n, ".options", (strings*) (nullptr)); - set_var<string> (s, r, n, ".mode", fm.empty () ? nullptr : &fm); - set_var<string> (s, r, n, ".dir_mode", dm.empty () ? nullptr : &dm); - set_var<string> (s, r, n, ".sudo", (string*) (nullptr)); - - // This one doesn't have config.* value (only set in a buildfile). - // - if (!global) - var_pool.rw (r).insert<bool> (string ("install.") + n + ".subdirs"); - } - - void - functions (); // functions.cxx - - bool - boot (scope& rs, const location&, unique_ptr<module_base>&) - { - tracer trace ("install::boot"); - l5 ([&]{trace << "for " << rs;}); - - // Register install function family if this is the first instance of the - // install modules. - // - if (!function_family::defined ("install")) - functions (); - - // Register our operations. - // - rs.insert_operation (install_id, op_install); - rs.insert_operation (uninstall_id, op_uninstall); - rs.insert_operation (update_for_install_id, op_update_for_install); - - return false; - } - - static const path cmd ("install"); - - static const dir_path dir_root ("root"); - - static const dir_path dir_sbin (dir_path ("exec_root") /= "sbin"); - static const dir_path dir_bin (dir_path ("exec_root") /= "bin"); - static const dir_path dir_lib (dir_path ("exec_root") /= "lib"); - static const dir_path dir_libexec (dir_path ("exec_root") /= "libexec"); - static const dir_path dir_pkgconfig (dir_path ("lib") /= "pkgconfig"); - - static const dir_path dir_data (dir_path ("data_root") /= "share"); - static const dir_path dir_include (dir_path ("data_root") /= "include"); - - static const dir_path dir_doc (dir_path (dir_data) /= "doc"); - static const dir_path dir_man (dir_path (dir_data) /= "man"); - static const dir_path dir_man1 (dir_path ("man") /= "man1"); - - static const group_rule group_rule_ (true /* see_through_only */); - - bool - init (scope& rs, - scope& bs, - const location& l, - unique_ptr<module_base>&, - bool first, - bool, - const variable_map& config_hints) - { - tracer trace ("install::init"); - - if (!first) - { - warn (l) << "multiple install module initializations"; - return true; - } - - const dir_path& out_root (rs.out_path ()); - l5 ([&]{trace << "for " << out_root;}); - - assert (config_hints.empty ()); // We don't known any hints. - - // Enter module variables. - // - auto& vp (var_pool.rw (rs)); - - // Note that the set_dir() calls below enter some more. - // - { - // Note: not overridable. - // - // The install variable is a path, not dir_path, since it can be used - // to both specify the target directory (to install with the same file - // name) or target file (to install with a different name). And the - // way we distinguish between the two is via the presence/absence of - // the trailing directory separator. - // - vp.insert<path> ("install", variable_visibility::target); - vp.insert<string> ("install.mode", variable_visibility::project); - vp.insert<bool> ("install.subdirs", variable_visibility::project); - } - - // Register our rules. - // - { - auto& r (bs.rules); - - const auto& ar (alias_rule::instance); - const auto& dr (fsdir_rule::instance); - const auto& fr (file_rule::instance); - const auto& gr (group_rule_); - - r.insert<alias> (perform_install_id, "install.alias", ar); - r.insert<alias> (perform_uninstall_id, "uninstall.alias", ar); - - r.insert<fsdir> (perform_install_id, "install.fsdir", dr); - r.insert<fsdir> (perform_uninstall_id, "install.fsdir", dr); - - r.insert<file> (perform_install_id, "install.file", fr); - r.insert<file> (perform_uninstall_id, "uninstall.file", fr); - - r.insert<target> (perform_install_id, "install.file", gr); - r.insert<target> (perform_uninstall_id, "uninstall.file", gr); - } - - // Configuration. - // - // Note that we don't use any defaults for root -- the location - // must be explicitly specified or the installer will complain - // if and when we try to install. - // - { - using build2::path; - - bool s (config::specified (rs, "install")); - - // Adjust module priority so that the (numerous) config.install.* - // values are saved at the end of config.build. - // - if (s) - config::save_module (rs, "install", INT32_MAX); - - const string& n (project (rs).string ()); - - // Global config.install.* values. - // - set_dir (s, rs, "", abs_dir_path (), false, "644", "755", cmd); - - set_dir (s, rs, "root", abs_dir_path ()); - - set_dir (s, rs, "data_root", dir_root); - set_dir (s, rs, "exec_root", dir_root, false, "755"); - - set_dir (s, rs, "sbin", dir_sbin); - set_dir (s, rs, "bin", dir_bin); - set_dir (s, rs, "lib", dir_lib); - set_dir (s, rs, "libexec", dir_path (dir_libexec) /= n, true); - set_dir (s, rs, "pkgconfig", dir_pkgconfig, false, "644"); - - set_dir (s, rs, "data", dir_path (dir_data) /= n, true); - set_dir (s, rs, "include", dir_include); - - set_dir (s, rs, "doc", dir_path (dir_doc) /= n, true); - set_dir (s, rs, "man", dir_man); - set_dir (s, rs, "man1", dir_man1); - - // Support for chroot'ed install (aka DESTDIR). - // - { - auto& var (vp.insert<dir_path> ( "install.chroot")); - auto& cvar (vp.insert<abs_dir_path> ("config.install.chroot", true)); - - value& v (rs.assign (var)); - - if (s) - { - if (lookup l = config::optional (rs, cvar)) - v = cast<dir_path> (l); // Strip abs_dir_path. - } - } - } - - // Configure "installability" for built-in target types. - // - install_path<exe> (bs, dir_path ("bin")); // Install into install.bin. - install_path<doc> (bs, dir_path ("doc")); // Install into install.doc. - install_path<man> (bs, dir_path ("man")); // Install into install.man. - install_path<man1> (bs, dir_path ("man1")); // Install into install.man1. - - return true; - } - } -} diff --git a/build2/install/init.hxx b/build2/install/init.hxx deleted file mode 100644 index 579c03e..0000000 --- a/build2/install/init.hxx +++ /dev/null @@ -1,31 +0,0 @@ -// file : build2/install/init.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_INSTALL_INIT_HXX -#define BUILD2_INSTALL_INIT_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/module.hxx> - -namespace build2 -{ - namespace install - { - bool - boot (scope&, const location&, unique_ptr<module_base>&); - - bool - init (scope&, - scope&, - const location&, - unique_ptr<module_base>&, - bool, - bool, - const variable_map&); - } -} - -#endif // BUILD2_INSTALL_INIT_HXX diff --git a/build2/install/operation.cxx b/build2/install/operation.cxx deleted file mode 100644 index 6ad1899..0000000 --- a/build2/install/operation.cxx +++ /dev/null @@ -1,84 +0,0 @@ -// file : build2/install/operation.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/install/operation.hxx> - -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace install - { - static operation_id - install_pre (const values& params, meta_operation_id mo, const location& l) - { - if (!params.empty ()) - fail (l) << "unexpected parameters for operation install"; - - // Run update as a pre-operation, unless we are disfiguring. - // - return mo != disfigure_id ? update_id : 0; - } - - // Note that we run both install and uninstall serially. The reason for - // this is all the fuzzy things we are trying to do like removing empty - // outer directories if they are empty. If we do this in parallel, then - // those things get racy. Also, since all we do here is creating/removing - // files, there is not going to be much speedup from doing it in parallel. - - const operation_info op_install { - install_id, - 0, - "install", - "install", - "installing", - "installed", - "has nothing to install", // We cannot "be installed". - execution_mode::first, - 0, - &install_pre, - nullptr - }; - - // Note that we run update as a pre-operation, just like install. Which - // may seem bizarre at first. We do it to obtain the exact same dependency - // graph as install so that we uninstall exactly the same set of files as - // install would install. Note that just matching the rules without - // executing them may not be enough: for example, a presence of an ad hoc - // group member may only be discovered after executing the rule (e.g., VC - // link.exe only creates a DLL's import library if there are any exported - // symbols). - // - const operation_info op_uninstall { - uninstall_id, - 0, - "uninstall", - "uninstall", - "uninstalling", - "uninstalled", - "is not installed", - execution_mode::last, - 0, - &install_pre, - nullptr - }; - - // Also the explicit update-for-install operation alias. - // - const operation_info op_update_for_install { - update_id, // Note: not update_for_install_id. - install_id, - op_update.name, - op_update.name_do, - op_update.name_doing, - op_update.name_did, - op_update.name_done, - op_update.mode, - op_update.concurrency, - op_update.pre, - op_update.post - }; - } -} diff --git a/build2/install/operation.hxx b/build2/install/operation.hxx deleted file mode 100644 index 7de0225..0000000 --- a/build2/install/operation.hxx +++ /dev/null @@ -1,23 +0,0 @@ -// file : build2/install/operation.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_INSTALL_OPERATION_HXX -#define BUILD2_INSTALL_OPERATION_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/operation.hxx> - -namespace build2 -{ - namespace install - { - extern const operation_info op_install; - extern const operation_info op_uninstall; - extern const operation_info op_update_for_install; - } -} - -#endif // BUILD2_INSTALL_OPERATION_HXX diff --git a/build2/install/rule.cxx b/build2/install/rule.cxx deleted file mode 100644 index faa7c3f..0000000 --- a/build2/install/rule.cxx +++ /dev/null @@ -1,1222 +0,0 @@ -// file : build2/install/rule.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/install/rule.hxx> - -#include <libbutl/filesystem.mxx> // dir_exists(), file_exists() - -#include <libbuild2/scope.hxx> -#include <libbuild2/target.hxx> -#include <libbuild2/algorithm.hxx> -#include <libbuild2/filesystem.hxx> -#include <libbuild2/diagnostics.hxx> - -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace install - { - // Lookup the install or install.* variable. Return NULL if not found or - // if the value is the special 'false' name (which means do not install; - // so the result can be used as bool). T is either scope or target. - // - template <typename P, typename T> - static const P* - lookup_install (T& t, const string& var) - { - auto l (t[var]); - - if (!l) - return nullptr; - - const P& r (cast<P> (l)); - return r.simple () && r.string () == "false" ? nullptr : &r; - } - - // alias_rule - // - const alias_rule alias_rule::instance; - - bool alias_rule:: - match (action, target&, const string&) const - { - // We always match. - // - // Note that we are called both as the outer part during the update-for- - // un/install pre-operation and as the inner part during the un/install - // operation itself. - // - return true; - } - - const target* alias_rule:: - filter (action a, const target& t, prerequisite_iterator& i) const - { - assert (i->member == nullptr); - return filter (a, t, i->prerequisite); - } - - const target* alias_rule:: - filter (action, const target& t, const prerequisite& p) const - { - const target& pt (search (t, p)); - return pt.in (t.weak_scope ()) ? &pt : nullptr; - } - - recipe alias_rule:: - apply (action a, target& t) const - { - tracer trace ("install::alias_rule::apply"); - - // Pass-through to our installable prerequisites. - // - // @@ Shouldn't we do match in parallel (here and below)? - // - auto& pts (t.prerequisite_targets[a]); - - auto pms (group_prerequisite_members (a, t, members_mode::never)); - for (auto i (pms.begin ()), e (pms.end ()); i != e; ++i) - { - const prerequisite& p (i->prerequisite); - - // Ignore excluded. - // - include_type pi (include (a, t, p)); - - if (!pi) - continue; - - // Ignore unresolved targets that are imported from other projects. - // We are definitely not installing those. - // - if (p.proj) - continue; - - // Let a customized rule have its say. - // - // Note: we assume that if the filter enters the group, then it - // iterates over all its members. - // - const target* pt (filter (a, t, i)); - if (pt == nullptr) - { - l5 ([&]{trace << "ignoring " << p << " (filtered out)";}); - continue; - } - - // Check if this prerequisite is explicitly "not installable", that - // is, there is the 'install' variable and its value is false. - // - // At first, this might seem redundand since we could have let the - // file_rule below take care of it. The nuance is this: this - // prerequsite can be in a different subproject that hasn't loaded the - // install module (and therefore has no file_rule registered). The - // typical example would be the 'tests' subproject. - // - // Note: not the same as lookup_install() above. - // - auto l ((*pt)["install"]); - if (l && cast<path> (l).string () == "false") - { - l5 ([&]{trace << "ignoring " << *pt << " (not installable)";}); - continue; - } - - // If this is not a file-based target (e.g., a target group such as - // libu{}) then ignore it if there is no rule to install. - // - if (pt->is_a<file> ()) - build2::match (a, *pt); - else if (!try_match (a, *pt).first) - { - l5 ([&]{trace << "ignoring " << *pt << " (no rule)";}); - pt = nullptr; - } - - if (pt != nullptr) - pts.push_back (prerequisite_target (pt, pi)); - } - - return default_recipe; - } - - // fsdir_rule - // - const fsdir_rule fsdir_rule::instance; - - bool fsdir_rule:: - match (action, target&, const string&) const - { - // We always match. - // - // Note that we are called both as the outer part during the update-for- - // un/install pre-operation and as the inner part during the un/install - // operation itself. - // - return true; - } - - recipe fsdir_rule:: - apply (action a, target& t) const - { - // If this is outer part of the update-for-un/install, delegate to the - // default fsdir rule. Otherwise, this is a noop (we don't install - // fsdir{}). - // - // For now we also assume we don't need to do anything for prerequisites - // (the only sensible prerequisite of fsdir{} is another fsdir{}). - // - if (a.operation () == update_id) - { - match_inner (a, t); - return &execute_inner; - } - else - return noop_recipe; - } - - // group_rule - // - const group_rule group_rule::instance (false /* see_through_only */); - - bool group_rule:: - match (action a, target& t, const string& h) const - { - return (!see_through || t.type ().see_through) && - alias_rule::match (a, t, h); - } - - const target* group_rule:: - filter (action, const target&, const target& m) const - { - return &m; - } - - recipe group_rule:: - apply (action a, target& t) const - { - tracer trace ("install::group_rule::apply"); - - // Resolve group members. - // - // Remember that we are called twice: first during update for install - // (pre-operation) and then during install. During the former, we rely - // on the normall update rule to resolve the group members. During the - // latter, there will be no rule to do this but the group will already - // have been resolved by the pre-operation. - // - // If the rule could not resolve the group, then we ignore it. - // - group_view gv (a.outer () - ? resolve_members (a, t) - : t.group_members (a)); - - if (gv.members != nullptr) - { - auto& pts (t.prerequisite_targets[a]); - - for (size_t i (0); i != gv.count; ++i) - { - const target* m (gv.members[i]); - - if (m == nullptr) - continue; - - // Let a customized rule have its say. - // - const target* mt (filter (a, t, *m)); - if (mt == nullptr) - { - l5 ([&]{trace << "ignoring " << *m << " (filtered out)";}); - continue; - } - - // See if we were explicitly instructed not to touch this target - // (the same semantics as in the prerequisites match). - // - // Note: not the same as lookup_install() above. - // - auto l ((*mt)["install"]); - if (l && cast<path> (l).string () == "false") - { - l5 ([&]{trace << "ignoring " << *mt << " (not installable)";}); - continue; - } - - build2::match (a, *mt); - pts.push_back (mt); // Never ad hoc. - } - } - - // Delegate to the base rule. - // - return alias_rule::apply (a, t); - } - - - // file_rule - // - const file_rule file_rule::instance; - - bool file_rule:: - match (action, target&, const string&) const - { - // We always match, even if this target is not installable (so that we - // can ignore it; see apply()). - // - return true; - } - - const target* file_rule:: - filter (action a, const target& t, prerequisite_iterator& i) const - { - assert (i->member == nullptr); - return filter (a, t, i->prerequisite); - } - - const target* file_rule:: - filter (action, const target& t, const prerequisite& p) const - { - const target& pt (search (t, p)); - return pt.in (t.root_scope ()) ? &pt : nullptr; - } - - recipe file_rule:: - apply (action a, target& t) const - { - tracer trace ("install::file_rule::apply"); - - // Note that we are called both as the outer part during the update-for- - // un/install pre-operation and as the inner part during the un/install - // operation itself. - // - // In both cases we first determine if the target is installable and - // return noop if it's not. Otherwise, in the first case (update-for- - // un/install) we delegate to the normal update and in the second - // (un/install) -- perform the test. - // - if (!lookup_install<path> (t, "install")) - return noop_recipe; - - // In both cases, the next step is to search, match, and collect all the - // installable prerequisites. - // - // But first, in case of the update pre-operation, match the inner rule - // (actual update). We used to do this after matching the prerequisites - // but the inner rule may provide some rule-specific information (like - // the target extension for exe{}) that may be required during the - // prerequisite search (like the base name for in{}). - // - optional<bool> unchanged; - if (a.operation () == update_id) - unchanged = match_inner (a, t, unmatch::unchanged); - - auto& pts (t.prerequisite_targets[a]); - - auto pms (group_prerequisite_members (a, t, members_mode::never)); - for (auto i (pms.begin ()), e (pms.end ()); i != e; ++i) - { - const prerequisite& p (i->prerequisite); - - // Ignore excluded. - // - include_type pi (include (a, t, p)); - - if (!pi) - continue; - - // Ignore unresolved targets that are imported from other projects. - // We are definitely not installing those. - // - if (p.proj) - continue; - - // Let a customized rule have its say. - // - // Note: we assume that if the filter enters the group, then it - // iterates over all its members. - // - const target* pt (filter (a, t, i)); - if (pt == nullptr) - { - l5 ([&]{trace << "ignoring " << p << " (filtered out)";}); - continue; - } - - // See if we were explicitly instructed not to touch this target (the - // same semantics as in alias_rule). - // - // Note: not the same as lookup_install() above. - // - auto l ((*pt)["install"]); - if (l && cast<path> (l).string () == "false") - { - l5 ([&]{trace << "ignoring " << *pt << " (not installable)";}); - continue; - } - - if (pt->is_a<file> ()) - { - // If the matched rule returned noop_recipe, then the target state - // is set to unchanged as an optimization. Use this knowledge to - // optimize things on our side as well since this will help a lot - // when updating static installable content (headers, documentation, - // etc). - // - if (build2::match (a, *pt, unmatch::unchanged)) - pt = nullptr; - } - else if (!try_match (a, *pt).first) - { - l5 ([&]{trace << "ignoring " << *pt << " (no rule)";}); - pt = nullptr; - } - - if (pt != nullptr) - pts.push_back (prerequisite_target (pt, pi)); - } - - if (a.operation () == update_id) - { - return *unchanged - ? (pts.empty () ? noop_recipe : default_recipe) - : &perform_update; - } - else - { - return [this] (action a, const target& t) - { - return a.operation () == install_id - ? perform_install (a, t) - : perform_uninstall (a, t); - }; - } - } - - target_state file_rule:: - perform_update (action a, const target& t) - { - // First execute the inner recipe then prerequisites. - // - target_state ts (execute_inner (a, t)); - - if (t.prerequisite_targets[a].size () != 0) - ts |= straight_execute_prerequisites (a, t); - - return ts; - } - - bool file_rule:: - install_extra (const file&, const install_dir&) const - { - return false; - } - - bool file_rule:: - uninstall_extra (const file&, const install_dir&) const - { - return false; - } - - auto_rmfile file_rule:: - install_pre (const file& t, const install_dir&) const - { - return auto_rmfile (t.path (), false /* active */); - } - - bool file_rule:: - install_post (const file& t, const install_dir& id, auto_rmfile&&) const - { - return install_extra (t, id); - } - - struct install_dir - { - dir_path dir; - - // If not NULL, then point to the corresponding install.* value. - // - const string* sudo = nullptr; - const path* cmd = nullptr; - const strings* options = nullptr; - const string* mode = nullptr; - const string* dir_mode = nullptr; - - explicit - install_dir (dir_path d = dir_path ()): dir (move (d)) {} - - install_dir (dir_path d, const install_dir& b) - : dir (move (d)), - sudo (b.sudo), - cmd (b.cmd), - options (b.options), - mode (b.mode), - dir_mode (b.dir_mode) {} - }; - - using install_dirs = vector<install_dir>; - - // Calculate a subdirectory based on l's location (*.subdirs) and if not - // empty add it to install_dirs. Return the new last element. - // - static install_dir& - resolve_subdir (install_dirs& rs, - const target& t, - const scope& s, - const lookup& l) - { - // Find the scope from which this value came and use as a base - // to calculate the subdirectory. - // - for (const scope* p (&s); p != nullptr; p = p->parent_scope ()) - { - if (l.belongs (*p, true)) // Include target type/pattern-specific. - { - // The target can be in out or src. - // - const dir_path& d (t.out_dir ().leaf (p->out_path ())); - - // Add it as another leading directory rather than modifying - // the last one directly; somehow, it feels right. - // - if (!d.empty ()) - rs.emplace_back (rs.back ().dir / d, rs.back ()); - break; - } - } - - return rs.back (); - } - - // Resolve installation directory name to absolute directory path. Return - // all the super-directories leading up to the destination (last). - // - // If target is not NULL, then also handle the subdirs logic. - // - static install_dirs - resolve (const scope& s, - const target* t, - dir_path d, - bool fail_unknown = true, - const string* var = nullptr) - { - install_dirs rs; - - if (d.absolute ()) - rs.emplace_back (move (d.normalize ())); - else - { - // If it is relative, then the first component is treated as the - // installation directory name, e.g., bin, sbin, lib, etc. Look it - // up and recurse. - // - if (d.empty ()) - fail << "empty installation directory name"; - - const string& sn (*d.begin ()); - const string var ("install." + sn); - if (const dir_path* dn = lookup_install<dir_path> (s, var)) - { - if (dn->empty ()) - fail << "empty installation directory for name " << sn << - info << "did you specified empty config." << var << "?"; - - rs = resolve (s, t, *dn, fail_unknown, &var); - - if (rs.empty ()) - { - assert (!fail_unknown); - return rs; // Empty. - } - - d = rs.back ().dir / dir_path (++d.begin (), d.end ()); - rs.emplace_back (move (d.normalize ()), rs.back ()); - } - else - { - if (fail_unknown) - fail << "unknown installation directory name '" << sn << "'" << - info << "did you forget to specify config." << var << "?"; - - return rs; // Empty. - } - } - - install_dir* r (&rs.back ()); - - // Override components in install_dir if we have our own. - // - if (var != nullptr) - { - if (auto l = s[*var + ".sudo"]) r->sudo = &cast<string> (l); - if (auto l = s[*var + ".cmd"]) r->cmd = &cast<path> (l); - if (auto l = s[*var + ".mode"]) r->mode = &cast<string> (l); - if (auto l = s[*var + ".dir_mode"]) r->dir_mode = &cast<string> (l); - if (auto l = s[*var + ".options"]) r->options = &cast<strings> (l); - - if (t != nullptr) - { - if (auto l = s[*var + ".subdirs"]) - { - if (cast<bool> (l)) - r = &resolve_subdir (rs, *t, s, l); - } - } - } - - // Set globals for unspecified components. - // - if (r->sudo == nullptr) - r->sudo = cast_null<string> (s["config.install.sudo"]); - - if (r->cmd == nullptr) - r->cmd = &cast<path> (s["config.install.cmd"]); - - if (r->options == nullptr) - r->options = cast_null<strings> (s["config.install.options"]); - - if (r->mode == nullptr) - r->mode = &cast<string> (s["config.install.mode"]); - - if (r->dir_mode == nullptr) - r->dir_mode = &cast<string> (s["config.install.dir_mode"]); - - return rs; - } - - static inline install_dirs - resolve (const target& t, dir_path d, bool fail_unknown = true) - { - return resolve (t.base_scope (), &t, d, fail_unknown); - } - - dir_path - resolve_dir (const target& t, dir_path d, bool fail_unknown) - { - install_dirs r (resolve (t, move (d), fail_unknown)); - return r.empty () ? dir_path () : move (r.back ().dir); - } - - dir_path - resolve_dir (const scope& s, dir_path d, bool fail_unknown) - { - install_dirs r (resolve (s, nullptr, move (d), fail_unknown)); - return r.empty () ? dir_path () : move (r.back ().dir); - } - - path - resolve_file (const file& f) - { - // Note: similar logic to perform_install(). - // - const path* p (lookup_install<path> (f, "install")); - - if (p == nullptr) // Not installable. - return path (); - - bool n (!p->to_directory ()); - dir_path d (n ? p->directory () : path_cast<dir_path> (*p)); - - install_dirs ids (resolve (f, d)); - - if (!n) - { - if (auto l = f["install.subdirs"]) - { - if (cast<bool> (l)) - resolve_subdir (ids, f, f.base_scope (), l); - } - } - - return ids.back ().dir / (n ? p->leaf () : f.path ().leaf ()); - } - - // On Windows we use MSYS2 install.exe and MSYS2 by default ignores - // filesystem permissions (noacl mount option). And this means, for - // example, that .exe that we install won't be runnable by Windows (MSYS2 - // itself will still run them since it recognizes the file extension). - // - // NOTE: this is no longer the case and we now use noacl (and acl causes - // other problems; see baseutils fstab for details). - // - // The way we work around this (at least in our distribution of the MSYS2 - // tools) is by changing the mount option for cygdrives (/c, /d, etc) to - // acl. But that's not all: we also have to install via a path that "hits" - // one of those mount points, c:\foo won't work, we have to use /c/foo. - // So this function translates an absolute Windows path to its MSYS - // representation. - // - // Note that we return the result as a string, not dir_path since path - // starting with / are illegal on Windows. Also note that the result - // doesn't have the trailing slash. - // - static string - msys_path (const dir_path& d) - { - assert (d.absolute ()); - string s (d.representation ()); - - // First replace ':' with the drive letter (so the path is no longer - // absolute) but postpone setting the first character to / until we are - // a string. - // - s[1] = lcase (s[0]); - s = dir_path (move (s)).posix_string (); - s[0] = '/'; - - return s; - } - - // Given an abolute path return its chroot'ed version, if any, accoring to - // install.chroot. - // - template <typename P> - static inline P - chroot_path (const scope& rs, const P& p) - { - if (const dir_path* d = cast_null<dir_path> (rs["install.chroot"])) - { - dir_path r (p.root_directory ()); - assert (!r.empty ()); // Must be absolute. - - return *d / p.leaf (r); - } - - return p; - } - - // install -d <dir> - // - static void - install_d (const scope& rs, - const install_dir& base, - const dir_path& d, - bool verbose = true) - { - // Here is the problem: if this is a dry-run, then we will keep showing - // the same directory creation commands over and over again (because we - // don't actually create them). There are two alternative ways to solve - // this: actually create the directories or simply don't show anything. - // While we use the former approach during update (see mkdir() in - // filesystem), here it feels like we really shouldn't be touching the - // destination filesystem. Plus, not showing anything will be symmetric - // with uninstall since the directories won't be empty (because we don't - // actually uninstall any files). - // - if (dry_run) - return; - - dir_path chd (chroot_path (rs, d)); - - try - { - if (dir_exists (chd)) // May throw (e.g., EACCES). - return; - } - catch (const system_error& e) - { - fail << "invalid installation directory " << chd << ": " << e; - } - - // While install -d will create all the intermediate components between - // base and dir, we do it explicitly, one at a time. This way the output - // is symmetrical to uninstall() below. - // - // Note that if the chroot directory does not exist, then install -d - // will create it and we don't bother removing it. - // - if (d != base.dir) - { - dir_path pd (d.directory ()); - - if (pd != base.dir) - install_d (rs, base, pd, verbose); - } - - cstrings args; - - string reld ( - cast<string> ((*global_scope)["build.host.class"]) == "windows" - ? msys_path (chd) - : relative (chd).string ()); - - if (base.sudo != nullptr) - args.push_back (base.sudo->c_str ()); - - args.push_back (base.cmd->string ().c_str ()); - args.push_back ("-d"); - - if (base.options != nullptr) - append_options (args, *base.options); - - args.push_back ("-m"); - args.push_back (base.dir_mode->c_str ()); - args.push_back (reld.c_str ()); - args.push_back (nullptr); - - process_path pp (run_search (args[0])); - - if (verb >= 2) - print_process (args); - else if (verb && verbose) - text << "install " << chd; - - run (pp, args); - } - - // install <file> <dir>/ - // install <file> <file> - // - static void - install_f (const scope& rs, - const install_dir& base, - const path& name, - const file& t, - const path& f, - bool verbose) - { - path relf (relative (f)); - - dir_path chd (chroot_path (rs, base.dir)); - - string reld ( - cast<string> ((*global_scope)["build.host.class"]) == "windows" - ? msys_path (chd) - : relative (chd).string ()); - - if (!name.empty ()) - { - reld += path::traits_type::directory_separator; - reld += name.string (); - } - - cstrings args; - - if (base.sudo != nullptr) - args.push_back (base.sudo->c_str ()); - - args.push_back (base.cmd->string ().c_str ()); - - if (base.options != nullptr) - append_options (args, *base.options); - - args.push_back ("-m"); - args.push_back (base.mode->c_str ()); - args.push_back (relf.string ().c_str ()); - args.push_back (reld.c_str ()); - args.push_back (nullptr); - - process_path pp (run_search (args[0])); - - if (verb >= 2) - print_process (args); - else if (verb && verbose) - text << "install " << t; - - if (!dry_run) - run (pp, args); - } - - void file_rule:: - install_l (const scope& rs, - const install_dir& base, - const path& target, - const path& link, - bool verbose) - { - path rell (relative (chroot_path (rs, base.dir))); - rell /= link; - - // We can create a symlink directly without calling ln. This, however, - // won't work if we have sudo. Also, we would have to deal with existing - // destinations (ln's -f takes care of that). So we are just going to - // always use ln. - // - const char* args_a[] = { - base.sudo != nullptr ? base.sudo->c_str () : nullptr, - "ln", - "-sf", - target.string ().c_str (), - rell.string ().c_str (), - nullptr}; - - const char** args (&args_a[base.sudo == nullptr ? 1 : 0]); - - process_path pp (run_search (args[0])); - - if (verb >= 2) - print_process (args); - else if (verb && verbose) - text << "install " << rell << " -> " << target; - - if (!dry_run) - run (pp, args); - } - - target_state file_rule:: - perform_install (action a, const target& xt) const - { - const file& t (xt.as<file> ()); - const path& tp (t.path ()); - - // Path should have been assigned by update unless it is unreal. - // - assert (!tp.empty () || t.mtime () == timestamp_unreal); - - const scope& rs (t.root_scope ()); - - auto install_target = [&rs, this] (const file& t, - const path& p, - bool verbose) - { - // Note: similar logic to resolve_file(). - // - bool n (!p.to_directory ()); - dir_path d (n ? p.directory () : path_cast<dir_path> (p)); - - // Resolve target directory. - // - install_dirs ids (resolve (t, d)); - - // Handle install.subdirs if one was specified. Unless the target path - // includes the file name in which case we assume it's a "final" path. - // - if (!n) - { - if (auto l = t["install.subdirs"]) - { - if (cast<bool> (l)) - resolve_subdir (ids, t, t.base_scope (), l); - } - } - - // Create leading directories. Note that we are using the leading - // directory (if there is one) for the creation information (mode, - // sudo, etc). - // - for (auto i (ids.begin ()), j (i); i != ids.end (); j = i++) - install_d (rs, *j, i->dir, verbose); // install -d - - install_dir& id (ids.back ()); - - // Override mode if one was specified. - // - if (auto l = t["install.mode"]) - id.mode = &cast<string> (l); - - // Install the target. - // - auto_rmfile f (install_pre (t, id)); - - // If install_pre() returned a different file name, make sure we - // install it as the original. - // - const path& tp (t.path ()); - const path& fp (f.path); - - install_f ( - rs, - id, - n ? p.leaf () : fp.leaf () != tp.leaf () ? tp.leaf () : path (), - t, - f.path, - verbose); - - install_post (t, id, move (f)); - }; - - // First handle installable prerequisites. - // - target_state r (straight_execute_prerequisites (a, t)); - - // Then installable ad hoc group members, if any. - // - for (const target* m (t.member); m != nullptr; m = m->member) - { - if (const path* p = lookup_install<path> (*m, "install")) - { - install_target (m->as<file> (), *p, tp.empty () /* verbose */); - r |= target_state::changed; - } - } - - // Finally install the target itself (since we got here we know the - // install variable is there). - // - if (!tp.empty ()) - { - install_target (t, cast<path> (t["install"]), true /* verbose */); - r |= target_state::changed; - } - - return r; - } - - // uninstall -d <dir> - // - // We try to remove all the directories between base and dir but not base - // itself unless base == dir. Return false if nothing has been removed - // (i.e., the directories do not exist or are not empty). - // - static bool - uninstall_d (const scope& rs, - const install_dir& base, - const dir_path& d, - bool verbose) - { - // See install_d() for the rationale. - // - if (dry_run) - return false; - - dir_path chd (chroot_path (rs, d)); - - // Figure out if we should try to remove this directory. Note that if - // it doesn't exist, then we may still need to remove outer ones. - // - bool r (false); - try - { - if ((r = dir_exists (chd))) // May throw (e.g., EACCES). - { - if (!dir_empty (chd)) // May also throw. - return false; // Won't be able to remove any outer directories. - } - } - catch (const system_error& e) - { - fail << "invalid installation directory " << chd << ": " << e; - } - - if (r) - { - dir_path reld (relative (chd)); - - // Normally when we need to remove a file or directory we do it - // directly without calling rm/rmdir. This however, won't work if we - // have sudo. So we are going to do it both ways. - // - // While there is no sudo on Windows, deleting things that are being - // used can get complicated. So we will always use rm/rmdir there. - // -#ifndef _WIN32 - if (base.sudo == nullptr) - { - if (verb >= 2) - text << "rmdir " << reld; - else if (verb && verbose) - text << "uninstall " << reld; - - try - { - try_rmdir (chd); - } - catch (const system_error& e) - { - fail << "unable to remove directory " << chd << ": " << e; - } - } - else -#endif - { - const char* args_a[] = { - base.sudo != nullptr ? base.sudo->c_str () : nullptr, - "rmdir", - reld.string ().c_str (), - nullptr}; - - const char** args (&args_a[base.sudo == nullptr ? 1 : 0]); - - process_path pp (run_search (args[0])); - - if (verb >= 2) - print_process (args); - else if (verb && verbose) - text << "uninstall " << reld; - - run (pp, args); - } - } - - // If we have more empty directories between base and dir, then try - // to clean them up as well. - // - if (d != base.dir) - { - dir_path pd (d.directory ()); - - if (pd != base.dir) - r = uninstall_d (rs, base, pd, verbose) || r; - } - - return r; - } - - bool file_rule:: - uninstall_f (const scope& rs, - const install_dir& base, - const file* t, - const path& name, - bool verbose) - { - assert (t != nullptr || !name.empty ()); - path f (chroot_path (rs, base.dir) / - (name.empty () ? t->path ().leaf () : name)); - - try - { - // Note: don't follow symlinks so if the target is a dangling symlinks - // we will proceed to removing it. - // - if (!file_exists (f, false)) // May throw (e.g., EACCES). - return false; - } - catch (const system_error& e) - { - fail << "invalid installation path " << f << ": " << e; - } - - path relf (relative (f)); - - if (verb == 1 && verbose) - { - if (t != nullptr) - text << "uninstall " << *t; - else - text << "uninstall " << relf; - } - - // The same story as with uninstall -d. - // -#ifndef _WIN32 - if (base.sudo == nullptr) - { - if (verb >= 2) - text << "rm " << relf; - - if (!dry_run) - { - try - { - try_rmfile (f); - } - catch (const system_error& e) - { - fail << "unable to remove file " << f << ": " << e; - } - } - } - else -#endif - { - const char* args_a[] = { - base.sudo != nullptr ? base.sudo->c_str () : nullptr, - "rm", - "-f", - relf.string ().c_str (), - nullptr}; - - const char** args (&args_a[base.sudo == nullptr ? 1 : 0]); - - process_path pp (run_search (args[0])); - - if (verb >= 2) - print_process (args); - - if (!dry_run) - run (pp, args); - } - - return true; - } - - target_state file_rule:: - perform_uninstall (action a, const target& xt) const - { - const file& t (xt.as<file> ()); - const path& tp (t.path ()); - - // Path should have been assigned by update unless it is unreal. - // - assert (!tp.empty () || t.mtime () == timestamp_unreal); - - const scope& rs (t.root_scope ()); - - auto uninstall_target = [&rs, this] (const file& t, - const path& p, - bool verbose) -> target_state - { - bool n (!p.to_directory ()); - dir_path d (n ? p.directory () : path_cast<dir_path> (p)); - - // Resolve target directory. - // - install_dirs ids (resolve (t, d)); - - // Handle install.subdirs if one was specified. - // - if (!n) - { - if (auto l = t["install.subdirs"]) - { - if (cast<bool> (l)) - resolve_subdir (ids, t, t.base_scope (), l); - } - } - - // Remove extras and the target itself. - // - const install_dir& id (ids.back ()); - - target_state r (uninstall_extra (t, id) - ? target_state::changed - : target_state::unchanged); - - if (uninstall_f (rs, id, &t, n ? p.leaf () : path (), verbose)) - r |= target_state::changed; - - // Clean up empty leading directories (in reverse). - // - // Note that we are using the leading directory (if there is one) - // for the clean up information (sudo, etc). - // - for (auto i (ids.rbegin ()), j (i), e (ids.rend ()); i != e; j = ++i) - { - if (install::uninstall_d (rs, ++j != e ? *j : *i, i->dir, verbose)) - r |= target_state::changed; - } - - return r; - }; - - // Reverse order of installation: first the target itself (since we got - // here we know the install variable is there). - // - target_state r (target_state::unchanged); - - if (!tp.empty ()) - r |= uninstall_target (t, cast<path> (t["install"]), true); - - // Then installable ad hoc group members, if any. To be anally precise - // we would have to do it in reverse, but that's not easy (it's a - // single-linked list). - // - for (const target* m (t.member); m != nullptr; m = m->member) - { - if (const path* p = lookup_install<path> (*m, "install")) - r |= uninstall_target (m->as<file> (), - *p, - tp.empty () || r != target_state::changed); - } - - // Finally handle installable prerequisites. - // - r |= reverse_execute_prerequisites (a, t); - - return r; - } - } -} diff --git a/build2/install/rule.hxx b/build2/install/rule.hxx deleted file mode 100644 index 09dd1b5..0000000 --- a/build2/install/rule.hxx +++ /dev/null @@ -1,195 +0,0 @@ -// file : build2/install/rule.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_INSTALL_RULE_HXX -#define BUILD2_INSTALL_RULE_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/rule.hxx> -#include <libbuild2/action.hxx> -#include <libbuild2/target.hxx> -#include <libbuild2/filesystem.hxx> - -namespace build2 -{ - namespace install - { - class alias_rule: public rule - { - public: - virtual bool - match (action, target&, const string&) const override; - - // Return NULL if this prerequisite should be ignored and pointer to its - // target otherwise. The default implementation accepts all prerequsites - // from the target's (weak) amalgamation. - // - // The prerequisite is passed as an iterator allowing the filter to - // "see" inside groups. - // - using prerequisite_iterator = - prerequisite_members_range<group_prerequisites>::iterator; - - virtual const target* - filter (action, const target&, prerequisite_iterator&) const; - - virtual const target* - filter (action, const target&, const prerequisite&) const; - - virtual recipe - apply (action, target&) const override; - - alias_rule () {} - static const alias_rule instance; - }; - - class fsdir_rule: public rule - { - public: - virtual bool - match (action, target&, const string&) const override; - - virtual recipe - apply (action, target&) const override; - - fsdir_rule () {} - static const fsdir_rule instance; - }; - - // In addition to the alias rule's semantics, this rule sees through to - // the group's members. - // - // The default group_rule::instance matches any target for which it was - // registered. It is to be used for non-see-through groups that should - // exhibit the see-through behavior for install (see lib{} in the bin - // module for an example). - // - // We also register (for all targets) another instance of this rule that - // only matches see-through groups. - // - class group_rule: public alias_rule - { - public: - virtual bool - match (action, target&, const string&) const override; - - // Return NULL if this group member should be ignored and pointer to its - // target otherwise. The default implementation accepts all members. - // - virtual const target* - filter (action, const target&, const target& group_member) const; - - using alias_rule::filter; // "Unhide" to make Clang happy. - - virtual recipe - apply (action, target&) const override; - - group_rule (bool see_through_only): see_through (see_through_only) {} - static const group_rule instance; - - bool see_through; - }; - - struct install_dir; - - class file_rule: public rule - { - public: - virtual bool - match (action, target&, const string&) const override; - - // Return NULL if this prerequisite should be ignored and pointer to its - // target otherwise. The default implementation ignores prerequsites - // that are outside of this target's project. - // - // @@ I wonder why we do weak amalgamation for alias but project for - // file? And then override this for prerequisite libraries/modules - // in cc::install_rule and bash::install_rule... - // - // The prerequisite is passed as an iterator allowing the filter to - // "see" inside groups. - // - using prerequisite_iterator = - prerequisite_members_range<group_prerequisites>::iterator; - - virtual const target* - filter (action, const target&, prerequisite_iterator&) const; - - virtual const target* - filter (action, const target&, const prerequisite&) const; - - virtual recipe - apply (action, target&) const override; - - static target_state - perform_update (action, const target&); - - // Extra un/installation hooks. Return true if anything was actually - // un/installed. - // - using install_dir = install::install_dir; // For derived rules. - - virtual bool - install_extra (const file&, const install_dir&) const; - - virtual bool - uninstall_extra (const file&, const install_dir&) const; - - // Lower-level pre/post installation hooks that can be used to override - // the source file path being installed (for example, to implement - // post-processing, etc). - // - // Note that one cannot generally perform post-processing in-place - // because of permissions. - // - virtual auto_rmfile - install_pre (const file&, const install_dir&) const; - - virtual bool - install_post (const file&, const install_dir&, auto_rmfile&&) const; - - // Installation/uninstallation "commands". - // - // If verbose is false, then only print the command at verbosity level 2 - // or higher. Note that these functions respect the dry_run flag. - - // Install a symlink: base/link -> target. - // - static void - install_l (const scope& rs, - const install_dir& base, - const path& target, - const path& link, - bool verbose); - - // Uninstall a file or symlink: - // - // uninstall <target> <base>/ rm <base>/<target>.leaf (); name empty - // uninstall <target> <name> rm <base>/<name>; target can be NULL - // - // Return false if nothing has been removed (i.e., the file does not - // exist). - // - static bool - uninstall_f (const scope& rs, - const install_dir& base, - const file* target, - const path& name, - bool verbose); - - target_state - perform_install (action, const target&) const; - - target_state - perform_uninstall (action, const target&) const; - - static const file_rule instance; - file_rule () {} - }; - } -} - -#endif // BUILD2_INSTALL_RULE_HXX diff --git a/build2/install/utility.hxx b/build2/install/utility.hxx deleted file mode 100644 index 29c6db0..0000000 --- a/build2/install/utility.hxx +++ /dev/null @@ -1,76 +0,0 @@ -// file : build2/install/utility.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_INSTALL_UTILITY_HXX -#define BUILD2_INSTALL_UTILITY_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/scope.hxx> -#include <libbuild2/target.hxx> - -namespace build2 -{ - namespace install - { - // Set install path, mode for a target type. - // - inline void - install_path (scope& s, const target_type& tt, dir_path d) - { - auto r ( - s.target_vars[tt]["*"].insert ( - var_pool.rw (s).insert ("install"))); - - if (r.second) // Already set by the user? - r.first.get () = path_cast<path> (move (d)); - } - - template <typename T> - inline void - install_path (scope& s, dir_path d) - { - return install_path (s, T::static_type, move (d)); - } - - inline void - install_mode (scope& s, const target_type& tt, string m) - { - auto r ( - s.target_vars[tt]["*"].insert ( - var_pool.rw (s).insert ("install.mode"))); - - if (r.second) // Already set by the user? - r.first.get () = move (m); - } - - template <typename T> - inline void - install_mode (scope& s, string m) - { - return install_mode (s, T::static_type, move (m)); - } - - // Resolve relative installation directory path (e.g., include/libfoo) to - // its absolute directory path (e.g., /usr/include/libfoo). If the - // resolution encountered an unknown directory, issue diagnostics and fail - // unless fail_unknown is false, in which case return empty directory. - // - // Note: implemented in rule.cxx. - // - dir_path - resolve_dir (const target&, dir_path, bool fail_unknown = true); - - dir_path - resolve_dir (const scope&, dir_path, bool fail_unknown = true); - - // Resolve file installation path returning empty path if not installable. - // - path - resolve_file (const file&); // rule.cxx - } -} - -#endif // BUILD2_INSTALL_UTILITY_HXX diff --git a/build2/test/common.cxx b/build2/test/common.cxx deleted file mode 100644 index bbfd489..0000000 --- a/build2/test/common.cxx +++ /dev/null @@ -1,220 +0,0 @@ -// file : build2/test/common.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/test/common.hxx> - -#include <libbuild2/target.hxx> -#include <libbuild2/algorithm.hxx> - -using namespace std; - -namespace build2 -{ - namespace test - { - // Determine if we have the target (first), id path (second), or both (in - // which case we also advance the iterator). - // - static pair<const name*, const name*> - sense (names::const_iterator& i) - { - const name* tn (nullptr); - const name* pn (nullptr); - - if (i->pair) - { - tn = &*i++; - pn = &*i; - } - else - { - // If it has a type (exe{hello}) or a directory (basics/), then - // we assume it is a target. - // - (i->typed () || !i->dir.empty () ? tn : pn) = &*i; - } - - // Validate the target. - // - if (tn != nullptr) - { - if (tn->qualified ()) - fail << "project-qualified target '" << *tn << " in config.test"; - } - - // Validate the id path. - // - if (pn != nullptr) - { - if (!pn->simple () || pn->empty ()) - fail << "invalid id path '" << *pn << " in config.test"; - } - - return make_pair (tn, pn); - } - - bool common:: - pass (const target& a) const - { - if (test_ == nullptr) - return true; - - // We need to "enable" aliases that "lead up" to the targets we are - // interested in. So see if any target is in a subdirectory of this - // alias. - // - // If we don't see any targets (e.g., only id paths), then we assume all - // targets match and therefore we always pass. - // - bool r (true); - - // Directory part from root to this alias (the same in src and out). - // - const dir_path d (a.out_dir ().leaf (root_->out_path ())); - - for (auto i (test_->begin ()); i != test_->end (); ++i) - { - if (const name* n = sense (i).first) - { - // Reset result to false if no match (but we have seen a target). - // - r = n->dir.sub (d); - - // See test() below for details on this special case. - // - if (!r && !n->typed ()) - r = d.sub (n->dir); - - if (r) - break; - } - } - - return r; - } - - bool common:: - test (const target& t) const - { - if (test_ == nullptr) - return true; - - // If we don't see any targets (e.g., only id paths), then we assume - // all of them match. - // - bool r (true); - - // Directory part from root to this alias (the same in src and out). - // - const dir_path d (t.out_dir ().leaf (root_->out_path ())); - const target_type& tt (t.type ()); - - for (auto i (test_->begin ()); i != test_->end (); ++i) - { - if (const name* n = sense (i).first) - { - // Reset result to false if no match (but we have seen a target). - // - - // When specifying a directory, for example, config.tests=tests/, - // one would intuitively expect that all the tests under it will - // run. But that's not what will happen with the below test: while - // the dir{tests/} itself will match, any target underneath won't. - // So we are going to handle this type if a target specially by - // making it match any target in or under it. - // - // Note that we only do this for tests/, not dir{tests/} since it is - // not always the semantics that one wants. Sometimes one may want - // to run tests (scripts) just for the tests/ target but not for any - // of its prerequisites. So dir{tests/} is a way to disable this - // special logic. - // - // Note: the same code as in test() below. - // - if (!n->typed ()) - r = d.sub (n->dir); - else - // First quickly and cheaply weed out names that cannot possibly - // match. Only then search for a target (as if it was a - // prerequisite), which can be expensive. - // - // We cannot specify an src target in config.test since we used - // the pair separator for ids. As a result, we search for both - // out and src targets. - // - r = - t.name == n->value && // Name matches. - tt.name == n->type && // Target type matches. - d == n->dir && // Directory matches. - (search_existing (*n, *root_) == &t || - search_existing (*n, *root_, d) == &t); - - if (r) - break; - } - } - - return r; - } - - bool common:: - test (const target& t, const path& id) const - { - if (test_ == nullptr) - return true; - - // If we don't see any id paths (e.g., only targets), then we assume - // all of them match. - // - bool r (true); - - // Directory part from root to this alias (the same in src and out). - // - const dir_path d (t.out_dir ().leaf (root_->out_path ())); - const target_type& tt (t.type ()); - - for (auto i (test_->begin ()); i != test_->end (); ++i) - { - auto p (sense (i)); - - if (const name* n = p.second) - { - // If there is a target, check that it matches ours. - // - if (const name* n = p.first) - { - // Note: the same code as in test() above. - // - bool r; - - if (!n->typed ()) - r = d.sub (n->dir); - else - r = - t.name == n->value && - tt.name == n->type && - d == n->dir && - (search_existing (*n, *root_) == &t || - search_existing (*n, *root_, d) == &t); - - if (!r) - continue; // Not our target. - } - - // If the id (group) "leads up" to what we want to run or we - // (group) lead up to the id, then match. - // - const path p (n->value); - - // Reset result to false if no match (but we have seen an id path). - // - if ((r = p.sub (id) || id.sub (p))) - break; - } - } - - return r; - } - } -} diff --git a/build2/test/common.hxx b/build2/test/common.hxx deleted file mode 100644 index 7ee72bd..0000000 --- a/build2/test/common.hxx +++ /dev/null @@ -1,72 +0,0 @@ -// file : build2/test/common.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_COMMON_HXX -#define BUILD2_TEST_COMMON_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/target.hxx> - -namespace build2 -{ - namespace test - { - enum class output_before {fail, warn, clean}; - enum class output_after {clean, keep}; - - struct common_data - { - const variable& config_test; - const variable& config_test_output; - - const variable& var_test; - const variable& test_options; - const variable& test_arguments; - - const variable& test_stdin; - const variable& test_stdout; - const variable& test_roundtrip; - const variable& test_input; - - const variable& test_target; - }; - - struct common: common_data - { - // The config.test.output values. - // - output_before before = output_before::warn; - output_after after = output_after::clean; - - // The config.test query interface. - // - const names* test_ = nullptr; // The config.test value if any. - scope* root_ = nullptr; // The root scope for target resolution. - - // Return true if the specified alias target should pass-through to its - // prerequisites. - // - bool - pass (const target& alias_target) const; - - // Return true if the specified target should be tested. - // - bool - test (const target& test_target) const; - - // Return true if the specified target should be tested with the - // specified testscript test (or group). - // - bool - test (const target& test_target, const path& id_path) const; - - explicit - common (common_data&& d): common_data (move (d)) {} - }; - } -} - -#endif // BUILD2_TEST_COMMON_HXX diff --git a/build2/test/init.cxx b/build2/test/init.cxx deleted file mode 100644 index 1f5a3ae..0000000 --- a/build2/test/init.cxx +++ /dev/null @@ -1,231 +0,0 @@ -// file : build2/test/init.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/test/init.hxx> - -#include <libbuild2/scope.hxx> -#include <libbuild2/target.hxx> -#include <libbuild2/rule.hxx> -#include <libbuild2/diagnostics.hxx> - -#include <build2/config/utility.hxx> - -#include <build2/test/module.hxx> -#include <build2/test/target.hxx> -#include <build2/test/operation.hxx> - -#include <build2/test/script/regex.hxx> // script::regex::init() - -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace test - { - bool - boot (scope& rs, const location&, unique_ptr<module_base>& mod) - { - tracer trace ("test::boot"); - - l5 ([&]{trace << "for " << rs;}); - - // Register our operations. - // - rs.insert_operation (test_id, op_test); - rs.insert_operation (update_for_test_id, op_update_for_test); - - // Enter module variables. Do it during boot in case they get assigned - // in bootstrap.build. - // - auto& vp (var_pool.rw (rs)); - - common_data d { - - // Tests to execute. - // - // Specified as <target>@<path-id> pairs with both sides being - // optional. The variable is untyped (we want a list of name-pairs), - // overridable, and inheritable. The target is relative (in essence a - // prerequisite) which is resolved from the (root) scope where the - // config.test value is defined. - // - vp.insert ("config.test", true), - - // Test working directory before/after cleanup (see Testscript spec - // for semantics). - // - vp.insert<name_pair> ("config.test.output", true), - - // The test variable is a name which can be a path (with the - // true/false special values) or a target name. - // - // Note: none are overridable. - // - vp.insert<name> ("test", variable_visibility::target), - vp.insert<strings> ("test.options", variable_visibility::project), - vp.insert<strings> ("test.arguments", variable_visibility::project), - - // Prerequisite-specific. - // - // test.stdin and test.stdout can be used to mark a prerequisite as a - // file to redirect stdin from and to compare stdout to, respectively. - // test.roundtrip is a shortcut to mark a prerequisite as both stdin - // and stdout. - // - // Prerequisites marked with test.input are treated as additional test - // inputs: they are made sure to be up to date and their paths are - // passed as additional command line arguments (after test.options and - // test.arguments). Their primary use is to pass inputs that may have - // varying file names/paths, for example: - // - // exe{parent}: exe{child}: test.input = true - // - // Note that currently this mechanism is only available to simple - // tests though we could also support it for testscript (e.g., by - // appending the input paths to test.arguments or by passing them in a - // separate test.inputs variable). - // - vp.insert<bool> ("test.stdin", variable_visibility::prereq), - vp.insert<bool> ("test.stdout", variable_visibility::prereq), - vp.insert<bool> ("test.roundtrip", variable_visibility::prereq), - vp.insert<bool> ("test.input", variable_visibility::prereq), - - // Test target platform. - // - vp.insert<target_triplet> ("test.target", variable_visibility::project) - }; - - // These are only used in testscript. - // - vp.insert<strings> ("test.redirects", variable_visibility::project); - vp.insert<strings> ("test.cleanups", variable_visibility::project); - - // Unless already set, default test.target to build.host. Note that it - // can still be overriden by the user, e.g., in root.build. - // - { - value& v (rs.assign (d.test_target)); - - if (!v || v.empty ()) - v = cast<target_triplet> ((*global_scope)["build.host"]); - } - - mod.reset (new module (move (d))); - return false; - } - - bool - init (scope& rs, - scope&, - const location& l, - unique_ptr<module_base>& mod, - bool first, - bool, - const variable_map& config_hints) - { - tracer trace ("test::init"); - - if (!first) - { - warn (l) << "multiple test module initializations"; - return true; - } - - const dir_path& out_root (rs.out_path ()); - l5 ([&]{trace << "for " << out_root;}); - - assert (mod != nullptr); - module& m (static_cast<module&> (*mod)); - - // Configure. - // - assert (config_hints.empty ()); // We don't known any hints. - - // Adjust module priority so that the config.test.* values are saved at - // the end of config.build. - // - config::save_module (rs, "test", INT32_MAX); - - // config.test - // - if (lookup l = config::omitted (rs, m.config_test).first) - { - // Figure out which root scope it came from. - // - scope* s (&rs); - for (; - s != nullptr && !l.belongs (*s); - s = s->parent_scope ()->root_scope ()) - assert (s != nullptr); - - m.test_ = &cast<names> (l); - m.root_ = s; - } - - // config.test.output - // - if (lookup l = config::omitted (rs, m.config_test_output).first) - { - const name_pair& p (cast<name_pair> (l)); - - // If second half is empty, then first is the after value. - // - const name& a (p.second.empty () ? p.first : p.second); // after - const name& b (p.second.empty () ? p.second : p.first); // before - - // Parse and validate. - // - if (!b.simple ()) - fail << "invalid config.test.output before value '" << b << "'"; - - if (!a.simple ()) - fail << "invalid config.test.output after value '" << a << "'"; - - if (a.value == "clean") m.after = output_after::clean; - else if (a.value == "keep") m.after = output_after::keep; - else fail << "invalid config.test.output after value '" << a << "'"; - - if (b.value == "fail") m.before = output_before::fail; - else if (b.value == "warn") m.before = output_before::warn; - else if (b.value == "clean") m.before = output_before::clean; - else if (b.value == "") m.before = output_before::clean; - else fail << "invalid config.test.output before value '" << b << "'"; - } - - //@@ TODO: Need ability to specify extra diff options (e.g., - // --strip-trailing-cr, now hardcoded). - // - //@@ TODO: Pring report. - - // Register target types. - // - { - auto& t (rs.target_types); - - auto& tt (t.insert<testscript> ()); - t.insert_file ("testscript", tt); - } - - // Register our test running rule. - // - { - default_rule& dr (m); - - rs.rules.insert<target> (perform_test_id, "test", dr); - rs.rules.insert<alias> (perform_test_id, "test", dr); - } - - return true; - } - - module_functions - build2_test_load () - { - script::regex::init (); - - return module_functions {&boot, &init}; - } - } -} diff --git a/build2/test/init.hxx b/build2/test/init.hxx deleted file mode 100644 index 5272a4d..0000000 --- a/build2/test/init.hxx +++ /dev/null @@ -1,34 +0,0 @@ -// file : build2/test/init.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_INIT_HXX -#define BUILD2_TEST_INIT_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/module.hxx> - -namespace build2 -{ - namespace test - { - bool - boot (scope&, const location&, unique_ptr<module_base>&); - - bool - init (scope&, - scope&, - const location&, - unique_ptr<module_base>&, - bool, - bool, - const variable_map&); - - extern "C" module_functions - build2_test_load (); - } -} - -#endif // BUILD2_TEST_INIT_HXX diff --git a/build2/test/module.hxx b/build2/test/module.hxx deleted file mode 100644 index 0c32fb9..0000000 --- a/build2/test/module.hxx +++ /dev/null @@ -1,37 +0,0 @@ -// file : build2/test/module.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_MODULE_HXX -#define BUILD2_TEST_MODULE_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/module.hxx> - -#include <build2/test/rule.hxx> -#include <build2/test/common.hxx> - -namespace build2 -{ - namespace test - { - struct module: module_base, virtual common, default_rule, group_rule - { - const test::group_rule& - group_rule () const - { - return *this; - } - - explicit - module (common_data&& d) - : common (move (d)), - test::default_rule (move (d)), - test::group_rule (move (d)) {} - }; - } -} - -#endif // BUILD2_TEST_MODULE_HXX diff --git a/build2/test/operation.cxx b/build2/test/operation.cxx deleted file mode 100644 index 8b6c73f..0000000 --- a/build2/test/operation.cxx +++ /dev/null @@ -1,55 +0,0 @@ -// file : build2/test/operation.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/test/operation.hxx> - -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace test - { - static operation_id - test_pre (const values& params, meta_operation_id mo, const location& l) - { - if (!params.empty ()) - fail (l) << "unexpected parameters for operation test"; - - // Run update as a pre-operation, unless we are disfiguring. - // - return mo != disfigure_id ? update_id : 0; - } - - const operation_info op_test { - test_id, - 0, - "test", - "test", - "testing", - "tested", - "has nothing to test", // We cannot "be tested". - execution_mode::first, - 1, - &test_pre, - nullptr - }; - - // Also the explicit update-for-test operation alias. - // - const operation_info op_update_for_test { - update_id, // Note: not update_for_test_id. - test_id, - op_update.name, - op_update.name_do, - op_update.name_doing, - op_update.name_did, - op_update.name_done, - op_update.mode, - op_update.concurrency, - op_update.pre, - op_update.post - }; - } -} diff --git a/build2/test/operation.hxx b/build2/test/operation.hxx deleted file mode 100644 index 09b954e..0000000 --- a/build2/test/operation.hxx +++ /dev/null @@ -1,22 +0,0 @@ -// file : build2/test/operation.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_OPERATION_HXX -#define BUILD2_TEST_OPERATION_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/operation.hxx> - -namespace build2 -{ - namespace test - { - extern const operation_info op_test; - extern const operation_info op_update_for_test; - } -} - -#endif // BUILD2_TEST_OPERATION_HXX diff --git a/build2/test/rule.cxx b/build2/test/rule.cxx deleted file mode 100644 index 7cb830c..0000000 --- a/build2/test/rule.cxx +++ /dev/null @@ -1,882 +0,0 @@ -// file : build2/test/rule.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/test/rule.hxx> - -#include <libbuild2/scope.hxx> -#include <libbuild2/target.hxx> -#include <libbuild2/algorithm.hxx> -#include <libbuild2/filesystem.hxx> -#include <libbuild2/diagnostics.hxx> - -#include <build2/test/target.hxx> - -#include <build2/test/script/parser.hxx> -#include <build2/test/script/runner.hxx> -#include <build2/test/script/script.hxx> - -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace test - { - bool rule:: - match (action, target&, const string&) const - { - // We always match, even if this target is not testable (so that we can - // ignore it; see apply()). - // - return true; - } - - recipe rule:: - apply (action a, target& t) const - { - // Note that we are called both as the outer part during the update-for- - // test pre-operation and as the inner part during the test operation - // itself. - // - // In both cases we first determine if the target is testable and return - // noop if it's not. Otherwise, in the first case (update for test) we - // delegate to the normal update and in the second (test) -- perform the - // test. - // - // And to add a bit more complexity, we want to handle aliases slightly - // differently: we may not want to ignore their prerequisites if the - // alias is not testable since their prerequisites could be. - // - // Here is the state matrix: - // - // test'able | pass'able | neither - // | | - // update-for-test delegate (& pass) | pass | noop - // ---------------------------------------+-------------+--------- - // test test (& pass) | pass | noop - // - auto& pts (t.prerequisite_targets[a]); - - // Resolve group members. - // - if (!see_through || t.type ().see_through) - { - // Remember that we are called twice: first during update for test - // (pre-operation) and then during test. During the former, we rely on - // the normall update rule to resolve the group members. During the - // latter, there will be no rule to do this but the group will already - // have been resolved by the pre-operation. - // - // If the rule could not resolve the group, then we ignore it. - // - group_view gv (a.outer () - ? resolve_members (a, t) - : t.group_members (a)); - - if (gv.members != nullptr) - { - for (size_t i (0); i != gv.count; ++i) - { - if (const target* m = gv.members[i]) - pts.push_back (m); - } - - match_members (a, t, pts); - } - } - - // If we are passing-through, then match our prerequisites. - // - if (t.is_a<alias> () && pass (t)) - { - // For the test operation we have to implement our own search and - // match because we need to ignore prerequisites that are outside of - // our project. They can be from projects that don't use the test - // module (and thus won't have a suitable rule). Or they can be from - // no project at all (e.g., installed). Also, generally, not testing - // stuff that's not ours seems right. - // - match_prerequisites (a, t, t.root_scope ()); - } - - size_t pass_n (pts.size ()); // Number of pass-through prerequisites. - - // See if it's testable and if so, what kind. - // - bool test (false); - bool script (false); - - if (this->test (t)) - { - // We have two very different cases: testscript and simple test (plus - // it may not be a testable target at all). So as the first step - // determine which case this is. - // - // If we have any prerequisites of the testscript{} type, then this is - // the testscript case. - // - // If we can, go inside see-through groups. Normally groups won't be - // resolvable for this action but then normally they won't contain any - // testscripts either. In other words, if there is a group that - // contains testscripts as members then it will need to arrange for - // the members to be resolvable (e.g., by registering an appropriate - // rule for the test operation). - // - for (prerequisite_member p: - group_prerequisite_members (a, t, members_mode::maybe)) - { - if (include (a, t, p) != include_type::normal) // Excluded/ad hoc. - continue; - - if (p.is_a<testscript> ()) - { - if (!script) - { - script = true; - - // We treat this target as testable unless the test variable is - // explicitly set to false. - // - const name* n (cast_null<name> (t[var_test])); - test = (n == nullptr || !n->simple () || n->value != "false"); - - if (!test) - break; - } - - // Collect testscripts after the pass-through prerequisites. - // - const target& pt (p.search (t)); - - // Note that for the test operation itself we don't match nor - // execute them relying on update to assign their paths. - // - // Causing update for test inputs/scripts is tricky: we cannot - // match for update-for-install because this same rule will match - // and since the target is not testable, it will return the noop - // recipe. - // - // So what we are going to do is directly match (and also execute; - // see below) a recipe for the inner update (who thought we could - // do that... but it seems we can). While at first it might feel - // iffy, it does make sense: the outer rule we would have matched - // would have simply delegated to the inner so we might as well - // take a shortcut. The only potential drawback of this approach - // is that we won't be able to provide any for-test customizations - // when updating test inputs/scripts. But such a need seems rather - // far fetched. - // - if (a.operation () == update_id) - match_inner (a, pt); - - pts.push_back (&pt); - } - } - - // If this is not a script, then determine if it is a simple test. - // Ignore testscript files themselves at the outset. - // - if (!script && !t.is_a<testscript> ()) - { - // For the simple case whether this is a test is controlled by the - // test variable. Also, it feels redundant to specify, say, "test = - // true" and "test.stdout = test.out" -- the latter already says this - // is a test. - // - const name* n (cast_null<name> (t[var_test])); - - // If the test variable is explicitly set to false then we treat - // it as not testable regardless of what other test.* variables - // or prerequisites we might have. - // - // Note that the test variable can be set to an "override" target - // (which means 'true' for our purposes). - // - if (n != nullptr && n->simple () && n->value == "false") - test = false; - else - { - // Look for test input/stdin/stdout prerequisites. The same group - // reasoning as in the testscript case above. - // - for (prerequisite_member p: - group_prerequisite_members (a, t, members_mode::maybe)) - { - const auto& vars (p.prerequisite.vars); - - if (vars.empty ()) // Common case. - continue; - - if (include (a, t, p) != include_type::normal) // Excluded/ad hoc. - continue; - - bool rt ( cast_false<bool> (vars[test_roundtrip])); - bool si (rt || cast_false<bool> (vars[test_stdin])); - bool so (rt || cast_false<bool> (vars[test_stdout])); - bool in ( cast_false<bool> (vars[test_input])); - - if (si || so || in) - { - // Verify it is file-based. - // - if (!p.is_a<file> ()) - { - fail << "test." << (si ? "stdin" : so ? "stdout" : "input") - << " prerequisite " << p << " of target " << t - << " is not a file"; - } - - if (!test) - { - test = true; - - // First matching prerequisite. Establish the structure in - // pts: the first element (after pass_n) is stdin (can be - // NULL), the second is stdout (can be NULL), and everything - // after that (if any) is inputs. - // - pts.push_back (nullptr); // stdin - pts.push_back (nullptr); // stdout - } - - // Collect them after the pass-through prerequisites. - // - // Note that for the test operation itself we don't match nor - // execute them relying on update to assign their paths. - // - auto match = [a, &p, &t] () -> const target* - { - const target& pt (p.search (t)); - - // The same match_inner() rationale as for the testcript - // prerequisites above. - // - if (a.operation () == update_id) - match_inner (a, pt); - - return &pt; - }; - - if (si) - { - if (pts[pass_n] != nullptr) - fail << "multiple test.stdin prerequisites for target " - << t; - - pts[pass_n] = match (); - } - - if (so) - { - if (pts[pass_n + 1] != nullptr) - fail << "multiple test.stdout prerequisites for target " - << t; - - pts[pass_n + 1] = match (); - } - - if (in) - pts.push_back (match ()); - } - } - - if (!test) - test = (n != nullptr); // We have the test variable. - - if (!test) - test = t[test_options] || t[test_arguments]; - } - } - } - - // Neither testing nor passing-through. - // - if (!test && pass_n == 0) - return noop_recipe; - - // If we are only passing-through, then use the default recipe (which - // will execute all the matched prerequisites). - // - if (!test) - return default_recipe; - - // Being here means we are definitely testing and maybe passing-through. - // - if (a.operation () == update_id) - { - // For the update pre-operation match the inner rule (actual update). - // - match_inner (a, t); - - return [pass_n] (action a, const target& t) - { - return perform_update (a, t, pass_n); - }; - } - else - { - if (script) - { - return [pass_n, this] (action a, const target& t) - { - return perform_script (a, t, pass_n); - }; - } - else - { - return [pass_n, this] (action a, const target& t) - { - return perform_test (a, t, pass_n); - }; - } - } - } - - target_state rule:: - perform_update (action a, const target& t, size_t pass_n) - { - // First execute the inner recipe then execute prerequisites. - // - target_state ts (execute_inner (a, t)); - - if (pass_n != 0) - ts |= straight_execute_prerequisites (a, t, pass_n); - - ts |= straight_execute_prerequisites_inner (a, t, 0, pass_n); - - return ts; - } - - static script::scope_state - perform_script_impl (const target& t, - const testscript& ts, - const dir_path& wd, - const common& c) - { - using namespace script; - - scope_state r; - - try - { - build2::test::script::script s (t, ts, wd); - - { - parser p; - p.pre_parse (s); - - default_runner r (c); - p.execute (s, r); - } - - r = s.state; - } - catch (const failed&) - { - r = scope_state::failed; - } - - return r; - } - - target_state rule:: - perform_script (action a, const target& t, size_t pass_n) const - { - // First pass through. - // - if (pass_n != 0) - straight_execute_prerequisites (a, t, pass_n); - - // Figure out whether the testscript file is called 'testscript', in - // which case it should be the only one. - // - auto& pts (t.prerequisite_targets[a]); - size_t pts_n (pts.size ()); - - bool one; - { - optional<bool> o; - for (size_t i (pass_n); i != pts_n; ++i) - { - const testscript& ts (*pts[i]->is_a<testscript> ()); - - bool r (ts.name == "testscript"); - - if ((r && o) || (!r && o && *o)) - fail << "both 'testscript' and other names specified for " << t; - - o = r; - } - - assert (o); // We should have a testscript or we wouldn't be here. - one = *o; - } - - // Calculate root working directory. It is in the out_base of the target - // and is called just test for dir{} targets and test-<target-name> for - // other targets. - // - dir_path wd (t.out_dir ()); - - if (t.is_a<dir> ()) - wd /= "test"; - else - wd /= "test-" + t.name; - - // Are we backlinking the test working directory to src? (See - // backlink_*() in algorithm.cxx for details.) - // - const scope& bs (t.base_scope ()); - const scope& rs (*bs.root_scope ()); - const path& buildignore_file (rs.root_extra->buildignore_file); - - dir_path bl; - if (cast_false<bool> (rs.vars[var_forwarded])) - { - bl = bs.src_path () / wd.leaf (bs.out_path ()); - clean_backlink (bl, verb_never); - } - - // If this is a (potentially) multi-testscript test, then create (and - // later cleanup) the root directory. If this is just 'testscript', then - // the root directory is used directly as test's working directory and - // it's the runner's responsibility to create and clean it up. - // - // Note that we create the root directory containing the .buildignore - // file to make sure that it is ignored by name patterns (see the - // buildignore description for details). - // - // What should we do if the directory already exists? We used to fail - // which meant the user had to go and clean things up manually every - // time a test failed. This turned out to be really annoying. So now we - // issue a warning and clean it up automatically. The drawbacks of this - // approach are the potential loss of data from the previous failed test - // run and the possibility of deleting user-created files. - // - if (exists (static_cast<const path&> (wd), false)) - fail << "working directory " << wd << " is a file/symlink"; - - if (exists (wd)) - { - if (before != output_before::clean) - { - bool fail (before == output_before::fail); - - (fail ? error : warn) << "working directory " << wd << " exists " - << (empty_buildignore (wd, buildignore_file) - ? "" - : "and is not empty ") - << "at the beginning of the test"; - - if (fail) - throw failed (); - } - - // Remove the directory itself not to confuse the runner which tries - // to detect when tests stomp on each others feet. - // - build2::rmdir_r (wd, true, 2); - } - - // Delay actually creating the directory in case all the tests are - // ignored (via config.test). - // - bool mk (!one); - - // Start asynchronous execution of the testscripts. - // - wait_guard wg; - - if (!dry_run) - wg = wait_guard (target::count_busy (), t[a].task_count); - - // Result vector. - // - using script::scope_state; - - vector<scope_state> res; - res.reserve (pts_n - pass_n); // Make sure there are no reallocations. - - for (size_t i (pass_n); i != pts_n; ++i) - { - const testscript& ts (*pts[i]->is_a<testscript> ()); - - // If this is just the testscript, then its id path is empty (and it - // can only be ignored by ignoring the test target, which makes sense - // since it's the only testscript file). - // - if (one || test (t, path (ts.name))) - { - // Because the creation of the output directory is shared between us - // and the script implementation (plus the fact that we actually - // don't clean the existing one), we are going to ignore it for - // dry-run. - // - if (!dry_run) - { - if (mk) - { - mkdir_buildignore (wd, buildignore_file, 2); - mk = false; - } - } - - if (verb) - { - diag_record dr (text); - dr << "test " << ts; - - if (!t.is_a<alias> ()) - dr << ' ' << t; - } - - res.push_back (dry_run ? scope_state::passed : scope_state::unknown); - - if (!dry_run) - { - scope_state& r (res.back ()); - - if (!sched.async (target::count_busy (), - t[a].task_count, - [this] (const diag_frame* ds, - scope_state& r, - const target& t, - const testscript& ts, - const dir_path& wd) - { - diag_frame::stack_guard dsg (ds); - r = perform_script_impl (t, ts, wd, *this); - }, - diag_frame::stack (), - ref (r), - cref (t), - cref (ts), - cref (wd))) - { - // Executed synchronously. If failed and we were not asked to - // keep going, bail out. - // - if (r == scope_state::failed && !keep_going) - break; - } - } - } - } - - if (!dry_run) - wg.wait (); - - // Re-examine. - // - bool bad (false); - for (scope_state r: res) - { - switch (r) - { - case scope_state::passed: break; - case scope_state::failed: bad = true; break; - case scope_state::unknown: assert (false); - } - - if (bad) - break; - } - - // Cleanup. - // - if (!dry_run) - { - if (!bad && !one && !mk && after == output_after::clean) - { - if (!empty_buildignore (wd, buildignore_file)) - fail << "working directory " << wd << " is not empty at the " - << "end of the test"; - - rmdir_buildignore (wd, buildignore_file, 2); - } - } - - // Backlink if the working directory exists. - // - // If we dry-run then presumably all tests passed and we shouldn't - // have anything left unless we are keeping the output. - // - if (!bl.empty () && (dry_run ? after == output_after::keep : exists (wd))) - update_backlink (wd, bl, true /* changed */); - - if (bad) - throw failed (); - - return target_state::changed; - } - - // The format of args shall be: - // - // name1 arg arg ... nullptr - // name2 arg arg ... nullptr - // ... - // nameN arg arg ... nullptr nullptr - // - static bool - run_test (const target& t, - diag_record& dr, - char const** args, - process* prev = nullptr) - { - // Find the next process, if any. - // - char const** next (args); - for (next++; *next != nullptr; next++) ; - next++; - - // Redirect stdout to a pipe unless we are last. - // - int out (*next != nullptr ? -1 : 1); - bool pr; - process_exit pe; - - try - { - process p (prev == nullptr - ? process (args, 0, out) // First process. - : process (args, *prev, out)); // Next process. - - pr = *next == nullptr || run_test (t, dr, next, &p); - p.wait (); - - assert (p.exit); - pe = *p.exit; - } - catch (const process_error& e) - { - error << "unable to execute " << args[0] << ": " << e; - - if (e.child) - exit (1); - - throw failed (); - } - - bool wr (pe.normal () && pe.code () == 0); - - if (!wr) - { - if (pr) // First failure? - dr << fail << "test " << t << " failed"; // Multi test: test 1. - - dr << error; - print_process (dr, args); - dr << " " << pe; - } - - return pr && wr; - } - - target_state rule:: - perform_test (action a, const target& tt, size_t pass_n) const - { - // First pass through. - // - if (pass_n != 0) - straight_execute_prerequisites (a, tt, pass_n); - - // See if we have the test executable override. - // - path p; - { - // Note that the test variable's visibility is target. - // - lookup l (tt[var_test]); - - // Note that we have similar code for scripted tests. - // - const target* t (nullptr); - - if (l.defined ()) - { - const name* n (cast_null<name> (l)); - - if (n == nullptr) - fail << "invalid test executable override: null value"; - else if (n->empty ()) - fail << "invalid test executable override: empty value"; - else if (n->simple ()) - { - // Ignore the special 'true' value. - // - if (n->value != "true") - p = path (n->value); - else - t = &tt; - } - else if (n->directory ()) - fail << "invalid test executable override: '" << *n << "'"; - else - { - // Must be a target name. - // - // @@ OUT: what if this is a @-qualified pair of names? - // - t = search_existing (*n, tt.base_scope ()); - - if (t == nullptr) - fail << "invalid test executable override: unknown target: '" - << *n << "'"; - } - } - else - // By default we set it to the test target's path. - // - t = &tt; - - if (t != nullptr) - { - if (auto* pt = t->is_a<path_target> ()) - { - // Do some sanity checks: the target better be up-to-date with - // an assigned path. - // - p = pt->path (); - - if (p.empty ()) - fail << "target " << *pt << " specified in the test variable " - << "is out of date" << - info << "consider specifying it as a prerequisite of " << tt; - } - else - fail << "target " << *t << (t != &tt - ? " specified in the test variable " - : " requested to be tested ") - << "is not path-based"; - } - } - - // See apply() for the structure of prerequisite_targets in the presence - // of test.{input,stdin,stdout}. - // - auto& pts (tt.prerequisite_targets[a]); - size_t pts_n (pts.size ()); - - cstrings args; - - // Do we have stdin? - // - // We simulate stdin redirect (<file) with a fake (already terminate) - // cat pipe (cat file |). - // - bool sin (pass_n != pts_n && pts[pass_n] != nullptr); - - process cat; - if (sin) - { - const file& it (pts[pass_n]->as<file> ()); - const path& ip (it.path ()); - assert (!ip.empty ()); // Should have been assigned by update. - - cat = process (process_exit (0)); // Successfully exited. - - if (!dry_run) - { - try - { - cat.in_ofd = fdopen (ip, fdopen_mode::in); - } - catch (const io_error& e) - { - fail << "unable to open " << ip << ": " << e; - } - } - - // Purely for diagnostics. - // - args.push_back ("cat"); - args.push_back (ip.string ().c_str ()); - args.push_back (nullptr); - } - - // If dry-run, the target may not exist. - // - process_path pp (!dry_run - ? run_search (p, true /* init */) - : try_run_search (p, true)); - args.push_back (pp.empty () ? p.string ().c_str () : pp.recall_string ()); - - // Do we have options and/or arguments? - // - if (auto l = tt[test_options]) - append_options (args, cast<strings> (l)); - - if (auto l = tt[test_arguments]) - append_options (args, cast<strings> (l)); - - // Do we have inputs? - // - for (size_t i (pass_n + 2); i < pts_n; ++i) - { - const file& it (pts[i]->as<file> ()); - const path& ip (it.path ()); - assert (!ip.empty ()); // Should have been assigned by update. - args.push_back (ip.string ().c_str ()); - } - - args.push_back (nullptr); - - // Do we have stdout? - // - path dp ("diff"); - process_path dpp; - if (pass_n != pts_n && pts[pass_n + 1] != nullptr) - { - const file& ot (pts[pass_n + 1]->as<file> ()); - const path& op (ot.path ()); - assert (!op.empty ()); // Should have been assigned by update. - - dpp = run_search (dp, true); - - args.push_back (dpp.recall_string ()); - args.push_back ("-u"); - - // Note that MinGW-built diff utility (as of 3.3) fails trying to - // detect if stdin contains text or binary data. We will help it a bit - // to workaround the issue. - // -#ifdef _WIN32 - args.push_back ("--text"); -#endif - - // Ignore Windows newline fluff if that's what we are running on. - // - if (cast<target_triplet> (tt[test_target]).class_ == "windows") - args.push_back ("--strip-trailing-cr"); - - args.push_back (op.string ().c_str ()); - args.push_back ("-"); - args.push_back (nullptr); - } - - args.push_back (nullptr); // Second. - - if (verb >= 2) - print_process (args); - else if (verb) - text << "test " << tt; - - if (!dry_run) - { - diag_record dr; - if (!run_test (tt, - dr, - args.data () + (sin ? 3 : 0), // Skip cat. - sin ? &cat : nullptr)) - { - dr << info << "test command line: "; - print_process (dr, args); - dr << endf; // return - } - } - - return target_state::changed; - } - } -} diff --git a/build2/test/rule.hxx b/build2/test/rule.hxx deleted file mode 100644 index 2f0ef53..0000000 --- a/build2/test/rule.hxx +++ /dev/null @@ -1,67 +0,0 @@ -// file : build2/test/rule.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_RULE_HXX -#define BUILD2_TEST_RULE_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/rule.hxx> -#include <libbuild2/action.hxx> - -#include <build2/test/common.hxx> - -namespace build2 -{ - namespace test - { - class rule: public build2::rule, protected virtual common - { - public: - virtual bool - match (action, target&, const string&) const override; - - virtual recipe - apply (action, target&) const override; - - static target_state - perform_update (action, const target&, size_t); - - target_state - perform_test (action, const target&, size_t) const; - - target_state - perform_script (action, const target&, size_t) const; - - rule (common_data&& d, bool see_through_only) - : common (move (d)), see_through (see_through_only) {} - - bool see_through; - }; - - class default_rule: public rule - { - public: - explicit - default_rule (common_data&& d) - : common (move (d)), - rule (move (d), true /* see_through_only */) {} - }; - - // To be used for non-see-through groups that should exhibit the see- - // through behavior for install (see lib{} in the bin module for an - // example). - // - class group_rule: public rule - { - public: - explicit - group_rule (common_data&& d) - : common (move (d)), rule (move (d), false /* see_through_only */) {} - }; - } -} - -#endif // BUILD2_TEST_RULE_HXX diff --git a/build2/test/script/builtin.cxx b/build2/test/script/builtin.cxx deleted file mode 100644 index 14ea267..0000000 --- a/build2/test/script/builtin.cxx +++ /dev/null @@ -1,1979 +0,0 @@ -// file : build2/test/script/builtin.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/test/script/builtin.hxx> - -#include <chrono> -#include <locale> -#include <ostream> -#include <sstream> -#include <cstdlib> // strtoull() - -#include <libbutl/regex.mxx> -#include <libbutl/path-io.mxx> // use default operator<< implementation -#include <libbutl/fdstream.mxx> // fdopen_mode, fdstream_mode -#include <libbutl/filesystem.mxx> - -#include <libbuild2/context.hxx> // sched - -#include <build2/test/script/script.hxx> - -// Strictly speaking a builtin which reads/writes from/to standard streams -// must be asynchronous so that the caller can communicate with it through -// pipes without being blocked on I/O operations. However, as an optimization, -// we allow builtins that only print diagnostics to STDERR to be synchronous -// assuming that their output will always fit the pipe buffer. Synchronous -// builtins must not read from STDIN and write to STDOUT. Later we may relax -// this rule to allow a "short" output for such builtins. -// -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace test - { - namespace script - { - using builtin_impl = uint8_t (scope&, - const strings& args, - auto_fd in, auto_fd out, auto_fd err); - - // Operation failed, diagnostics has already been issued. - // - struct failed {}; - - // Accumulate an error message, print it atomically in dtor to the - // provided stream and throw failed afterwards if requested. Prefixes - // the message with the builtin name. - // - // Move constructible-only, not assignable (based to diag_record). - // - class error_record - { - public: - template <typename T> - friend const error_record& - operator<< (const error_record& r, const T& x) - { - r.ss_ << x; - return r; - } - - error_record (ostream& o, bool fail, const char* name) - : os_ (o), fail_ (fail), empty_ (false) - { - ss_ << name << ": "; - } - - // Older versions of libstdc++ don't have the ostringstream move - // support. Luckily, GCC doesn't seem to be actually needing move due - // to copy/move elision. - // -#ifdef __GLIBCXX__ - error_record (error_record&&); -#else - error_record (error_record&& r) - : os_ (r.os_), - ss_ (move (r.ss_)), - fail_ (r.fail_), - empty_ (r.empty_) - { - r.empty_ = true; - } -#endif - - ~error_record () noexcept (false) - { - if (!empty_) - { - // The output stream can be in a bad state (for example as a - // result of unsuccessful attempt to report a previous error), so - // we check it. - // - if (os_.good ()) - { - ss_.put ('\n'); - os_ << ss_.str (); - os_.flush (); - } - - if (fail_) - throw failed (); - } - } - - private: - ostream& os_; - mutable ostringstream ss_; - - bool fail_; - bool empty_; - }; - - // Parse and normalize a path. Also, unless it is already absolute, make - // the path absolute using the specified directory. Throw invalid_path - // if the path is empty, and on parsing and normalization failures. - // - static path - parse_path (string s, const dir_path& d) - { - path p (move (s)); - - if (p.empty ()) - throw invalid_path (""); - - if (p.relative ()) - p = d / move (p); - - p.normalize (); - return p; - } - - // Builtin commands functions. - // - - // cat <file>... - // - // Note that POSIX doesn't specify if after I/O operation failure the - // command should proceed with the rest of the arguments. The current - // implementation exits immediatelly in such a case. - // - // @@ Shouldn't we check that we don't print a nonempty regular file to - // itself, as that would merely exhaust the output device? POSIX - // allows (but not requires) such a check and some implementations do - // this. That would require to fstat() file descriptors and complicate - // the code a bit. Was able to reproduce on a big file (should be - // bigger than the stream buffer size) with the test - // 'cat file >+file'. - // - // Note: must be executed asynchronously. - // - static uint8_t - cat (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "cat"); - }; - - try - { - ifdstream cin (move (in), fdstream_mode::binary); - ofdstream cout (move (out), fdstream_mode::binary); - - // Copy input stream to STDOUT. - // - auto copy = [&cout] (istream& is) - { - if (is.peek () != ifdstream::traits_type::eof ()) - cout << is.rdbuf (); - - is.clear (istream::eofbit); // Sets eofbit. - }; - - // Path of a file being printed to STDOUT. An empty path represents - // STDIN. Used in diagnostics. - // - path p; - - try - { - // Print STDIN. - // - if (args.empty ()) - copy (cin); - - // Print files. - // - for (auto i (args.begin ()); i != args.end (); ++i) - { - if (*i == "-") - { - if (!cin.eof ()) - { - p.clear (); - copy (cin); - } - - continue; - } - - p = parse_path (*i, sp.wd_path); - - ifdstream is (p, ifdstream::binary); - copy (is); - is.close (); - } - } - catch (const io_error& e) - { - error_record d (error ()); - d << "unable to print "; - - if (p.empty ()) - d << "stdin"; - else - d << "'" << p << "'"; - - d << ": " << e; - } - - cin.close (); - cout.close (); - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while creating/closing cin, cout or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // Make a copy of a file at the specified path, preserving permissions, - // and registering a cleanup for a newly created file. The file paths - // must be absolute. Fail if an exception is thrown by the underlying - // copy operation. - // - static void - cpfile (scope& sp, - const path& from, const path& to, - bool overwrite, - bool attrs, - bool cleanup, - const function<error_record()>& fail) - { - try - { - bool exists (file_exists (to)); - - cpflags f ( - overwrite - ? cpflags::overwrite_permissions | cpflags::overwrite_content - : cpflags::none); - - if (attrs) - f |= cpflags::overwrite_permissions | cpflags::copy_timestamps; - - cpfile (from, to, f); - - if (!exists && cleanup) - sp.clean ({cleanup_type::always, to}, true); - } - catch (const system_error& e) - { - fail () << "unable to copy file '" << from << "' to '" << to - << "': " << e; - } - } - - // Make a copy of a directory at the specified path, registering a - // cleanup for the created directory. The directory paths must be - // absolute. Fail if the destination directory already exists or - // an exception is thrown by the underlying copy operation. - // - static void - cpdir (scope& sp, - const dir_path& from, const dir_path& to, - bool attrs, - bool cleanup, - const function<error_record()>& fail) - { - try - { - if (try_mkdir (to) == mkdir_status::already_exists) - throw_generic_error (EEXIST); - - if (cleanup) - sp.clean ({cleanup_type::always, to}, true); - - for (const auto& de: dir_iterator (from, - false /* ignore_dangling */)) - { - path f (from / de.path ()); - path t (to / de.path ()); - - if (de.type () == entry_type::directory) - cpdir (sp, - path_cast<dir_path> (move (f)), - path_cast<dir_path> (move (t)), - attrs, - cleanup, - fail); - else - cpfile (sp, f, t, false /* overwrite */, attrs, cleanup, fail); - } - - // Note that it is essential to copy timestamps and permissions after - // the directory content is copied. - // - if (attrs) - { - path_permissions (to, path_permissions (from)); - dir_time (to, dir_time (from)); - } - } - catch (const system_error& e) - { - fail () << "unable to copy directory '" << from << "' to '" << to - << "': " << e; - } - } - - // cp [-p] [--no-cleanup] <src-file> <dst-file> - // cp [-p] [--no-cleanup] -R|-r <src-dir> <dst-dir> - // cp [-p] [--no-cleanup] <src-file>... <dst-dir>/ - // cp [-p] [--no-cleanup] -R|-r <src-path>... <dst-dir>/ - // - // Note: can be executed synchronously. - // - static uint8_t - cp (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "cp"); - }; - - try - { - in.close (); - out.close (); - - auto i (args.begin ()); - auto e (args.end ()); - - // Process options. - // - bool recursive (false); - bool attrs (false); - bool cleanup (true); - for (; i != e; ++i) - { - const string& o (*i); - - if (o == "-R" || o == "-r") - recursive = true; - else if (o == "-p") - attrs = true; - else if (o == "--no-cleanup") - cleanup = false; - else - { - if (o == "--") - ++i; - - break; - } - } - - // Copy files or directories. - // - if (i == e) - error () << "missing arguments"; - - const dir_path& wd (sp.wd_path); - - auto j (args.rbegin ()); - path dst (parse_path (*j++, wd)); - e = j.base (); - - if (i == e) - error () << "missing source path"; - - auto fail = [&error] () {return error (true);}; - - // If destination is not a directory path (no trailing separator) - // then make a copy of the filesystem entry at the specified path - // (the only source path is allowed in such a case). Otherwise copy - // the source filesystem entries into the destination directory. - // - if (!dst.to_directory ()) - { - path src (parse_path (*i++, wd)); - - // If there are multiple sources but no trailing separator for the - // destination, then, most likelly, it is missing. - // - if (i != e) - error () << "multiple source paths without trailing separator " - << "for destination directory"; - - if (!recursive) - // Synopsis 1: make a file copy at the specified path. - // - cpfile (sp, - src, - dst, - true /* overwrite */, - attrs, - cleanup, - fail); - else - // Synopsis 2: make a directory copy at the specified path. - // - cpdir (sp, - path_cast<dir_path> (src), path_cast<dir_path> (dst), - attrs, - cleanup, - fail); - } - else - { - for (; i != e; ++i) - { - path src (parse_path (*i, wd)); - - if (recursive && dir_exists (src)) - // Synopsis 4: copy a filesystem entry into the specified - // directory. Note that we handle only source directories here. - // Source files are handled below. - // - cpdir (sp, - path_cast<dir_path> (src), - path_cast<dir_path> (dst / src.leaf ()), - attrs, - cleanup, - fail); - else - // Synopsis 3: copy a file into the specified directory. Also, - // here we cover synopsis 4 for the source path being a file. - // - cpfile (sp, - src, - dst / src.leaf (), - true /* overwrite */, - attrs, - cleanup, - fail); - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // echo <string>... - // - // Note: must be executed asynchronously. - // - static uint8_t - echo (scope&, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - try - { - in.close (); - ofdstream cout (move (out)); - - for (auto b (args.begin ()), i (b), e (args.end ()); i != e; ++i) - cout << (i != b ? " " : "") << *i; - - cout << '\n'; - cout.close (); - r = 0; - } - catch (const std::exception& e) - { - cerr << "echo: " << e << endl; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // false - // - // Failure to close the file descriptors is silently ignored. - // - // Note: can be executed synchronously. - // - static builtin - false_ (scope&, uint8_t& r, const strings&, auto_fd, auto_fd, auto_fd) - { - return builtin (r = 1); - } - - // true - // - // Failure to close the file descriptors is silently ignored. - // - // Note: can be executed synchronously. - // - static builtin - true_ (scope&, uint8_t& r, const strings&, auto_fd, auto_fd, auto_fd) - { - return builtin (r = 0); - } - - // Create a symlink to a file or directory at the specified path. The - // paths must be absolute. Fall back to creating a hardlink, if symlink - // creation is not supported for the link path. If hardlink creation is - // not supported either, then fall back to copies. If requested, created - // filesystem entries are registered for cleanup. Fail if the target - // filesystem entry doesn't exist or an exception is thrown by the - // underlying filesystem operation (specifically for an already existing - // filesystem entry at the link path). - // - // Note that supporting optional removal of an existing filesystem entry - // at the link path (the -f option) tends to get hairy. As soon as an - // existing and the resulting filesystem entries could be of different - // types, we would end up with canceling an old cleanup and registering - // the new one. Also removing non-empty directories doesn't look very - // natural, but would be required if we want the behavior on POSIX and - // Windows to be consistent. - // - static void - mksymlink (scope& sp, - const path& target, const path& link, - bool cleanup, - const function<error_record()>& fail) - { - // Determine the target type, fail if the target doesn't exist. - // - bool dir (false); - - try - { - pair<bool, entry_stat> pe (path_entry (target)); - - if (!pe.first) - fail () << "unable to create symlink to '" << target << "': " - << "no such file or directory"; - - dir = pe.second.type == entry_type::directory; - } - catch (const system_error& e) - { - fail () << "unable to stat '" << target << "': " << e; - } - - // First we try to create a symlink. If that fails (e.g., "Windows - // happens"), then we resort to hard links. If that doesn't work out - // either (e.g., not on the same filesystem), then we fall back to - // copies. So things are going to get a bit nested. - // - try - { - mksymlink (target, link, dir); - - if (cleanup) - sp.clean ({cleanup_type::always, link}, true); - } - catch (const system_error& e) - { - // Note that we are not guaranteed (here and below) that the - // system_error exception is of the generic category. - // - int c (e.code ().value ()); - if (!(e.code ().category () == generic_category () && - (c == ENOSYS || // Not implemented. - c == EPERM))) // Not supported by the filesystem(s). - fail () << "unable to create symlink '" << link << "' to '" - << target << "': " << e; - - try - { - mkhardlink (target, link, dir); - - if (cleanup) - sp.clean ({cleanup_type::always, link}, true); - } - catch (const system_error& e) - { - c = e.code ().value (); - if (!(e.code ().category () == generic_category () && - (c == ENOSYS || // Not implemented. - c == EPERM || // Not supported by the filesystem(s). - c == EXDEV))) // On different filesystems. - fail () << "unable to create hardlink '" << link << "' to '" - << target << "': " << e; - - if (dir) - cpdir (sp, - path_cast<dir_path> (target), path_cast<dir_path> (link), - false, - cleanup, - fail); - else - cpfile (sp, - target, - link, - false /* overwrite */, - true /* attrs */, - cleanup, - fail); - } - } - } - - // ln [--no-cleanup] -s <target-path> <link-path> - // ln [--no-cleanup] -s <target-path>... <link-dir>/ - // - // Note: can be executed synchronously. - // - static uint8_t - ln (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "ln"); - }; - - try - { - in.close (); - out.close (); - - auto i (args.begin ()); - auto e (args.end ()); - - // Process options. - // - bool cleanup (true); - bool symlink (false); - - for (; i != e; ++i) - { - const string& o (*i); - - if (o == "--no-cleanup") - cleanup = false; - else if (o == "-s") - symlink = true; - else - { - if (o == "--") - ++i; - - break; - } - } - - if (!symlink) - error () << "missing -s option"; - - // Create file or directory symlinks. - // - if (i == e) - error () << "missing arguments"; - - const dir_path& wd (sp.wd_path); - - auto j (args.rbegin ()); - path link (parse_path (*j++, wd)); - e = j.base (); - - if (i == e) - error () << "missing target path"; - - auto fail = [&error] () {return error (true);}; - - // If link is not a directory path (no trailing separator), then - // create a symlink to the target path at the specified link path - // (the only target path is allowed in such a case). Otherwise create - // links to the target paths inside the specified directory. - // - if (!link.to_directory ()) - { - path target (parse_path (*i++, wd)); - - // If there are multiple targets but no trailing separator for the - // link, then, most likelly, it is missing. - // - if (i != e) - error () << "multiple target paths with non-directory link path"; - - // Synopsis 1: create a target path symlink at the specified path. - // - mksymlink (sp, target, link, cleanup, fail); - } - else - { - for (; i != e; ++i) - { - path target (parse_path (*i, wd)); - - // Synopsis 2: create a target path symlink in the specified - // directory. - // - mksymlink (sp, target, link / target.leaf (), cleanup, fail); - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // Create a directory if not exist and its parent directories if - // necessary. Throw system_error on failure. Register created - // directories for cleanup. The directory path must be absolute. - // - static void - mkdir_p (scope& sp, const dir_path& p, bool cleanup) - { - if (!dir_exists (p)) - { - if (!p.root ()) - mkdir_p (sp, p.directory (), cleanup); - - try_mkdir (p); // Returns success or throws. - - if (cleanup) - sp.clean ({cleanup_type::always, p}, true); - } - } - - // mkdir [--no-cleanup] [-p] <dir>... - // - // Note that POSIX doesn't specify if after a directory creation failure - // the command should proceed with the rest of the arguments. The current - // implementation exits immediatelly in such a case. - // - // Note: can be executed synchronously. - // - static uint8_t - mkdir (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "mkdir"); - }; - - try - { - in.close (); - out.close (); - - auto i (args.begin ()); - auto e (args.end ()); - - // Process options. - // - bool parent (false); - bool cleanup (true); - for (; i != e; ++i) - { - const string& o (*i); - - if (o == "-p") - parent = true; - else if (o == "--no-cleanup") - cleanup = false; - else - { - if (*i == "--") - ++i; - - break; - } - } - - // Create directories. - // - if (i == e) - error () << "missing directory"; - - for (; i != e; ++i) - { - dir_path p (path_cast<dir_path> (parse_path (*i, sp.wd_path))); - - try - { - if (parent) - mkdir_p (sp, p, cleanup); - else if (try_mkdir (p) == mkdir_status::success) - { - if (cleanup) - sp.clean ({cleanup_type::always, p}, true); - } - else // == mkdir_status::already_exists - throw_generic_error (EEXIST); - } - catch (const system_error& e) - { - error () << "unable to create directory '" << p << "': " << e; - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // mv [--no-cleanup] [-f] <src-path> <dst-path> - // mv [--no-cleanup] [-f] <src-path>... <dst-dir>/ - // - // Note: can be executed synchronously. - // - static uint8_t - mv (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "mv"); - }; - - try - { - in.close (); - out.close (); - - auto i (args.begin ()); - auto e (args.end ()); - - // Process options. - // - bool no_cleanup (false); - bool force (false); - for (; i != e; ++i) - { - const string& o (*i); - - if (o == "--no-cleanup") - no_cleanup = true; - else if (*i == "-f") - force = true; - else - { - if (o == "--") - ++i; - - break; - } - } - - // Move filesystem entries. - // - if (i == e) - error () << "missing arguments"; - - const dir_path& wd (sp.wd_path); - - auto j (args.rbegin ()); - path dst (parse_path (*j++, wd)); - e = j.base (); - - if (i == e) - error () << "missing source path"; - - auto mv = [no_cleanup, force, &wd, &sp, &error] (const path& from, - const path& to) - { - const dir_path& rwd (sp.root->wd_path); - - if (!from.sub (rwd) && !force) - error () << "'" << from << "' is out of working directory '" - << rwd << "'"; - - try - { - auto check_wd = [&wd, &error] (const path& p) - { - if (wd.sub (path_cast<dir_path> (p))) - error () << "'" << p << "' contains test working directory '" - << wd << "'"; - }; - - check_wd (from); - check_wd (to); - - bool exists (butl::entry_exists (to)); - - // Fail if the source and destination paths are the same. - // - // Note that for mventry() function (that is based on the POSIX - // rename() function) this is a noop. - // - if (exists && to == from) - error () << "unable to move entity '" << from << "' to itself"; - - // Rename/move the filesystem entry, replacing an existing one. - // - mventry (from, - to, - cpflags::overwrite_permissions | - cpflags::overwrite_content); - - // Unless suppressed, adjust the cleanups that are sub-paths of - // the source path. - // - if (!no_cleanup) - { - // "Move" the matching cleanup if the destination path doesn't - // exist and is a sub-path of the working directory. Otherwise - // just remove it. - // - // Note that it's not enough to just change the cleanup paths. - // We also need to make sure that these cleanups happen before - // the destination directory (or any of its parents) cleanup, - // that is potentially registered. To achieve that we can just - // relocate these cleanup entries to the end of the list, - // preserving their mutual order. Remember that cleanups in - // the list are executed in the reversed order. - // - bool mv_cleanups (!exists && to.sub (rwd)); - cleanups cs; - - // Remove the source path sub-path cleanups from the list, - // adjusting/caching them if required (see above). - // - for (auto i (sp.cleanups.begin ()); i != sp.cleanups.end (); ) - { - cleanup& c (*i); - path& p (c.path); - - if (p.sub (from)) - { - if (mv_cleanups) - { - // Note that we need to preserve the cleanup path - // trailing separator which indicates the removal - // method. Also note that leaf(), in particular, does - // that. - // - p = p != from - ? to / p.leaf (path_cast<dir_path> (from)) - : p.to_directory () - ? path_cast<dir_path> (to) - : to; - - cs.push_back (move (c)); - } - - i = sp.cleanups.erase (i); - } - else - ++i; - } - - // Re-insert the adjusted cleanups at the end of the list. - // - sp.cleanups.insert (sp.cleanups.end (), - make_move_iterator (cs.begin ()), - make_move_iterator (cs.end ())); - } - } - catch (const system_error& e) - { - error () << "unable to move entity '" << from << "' to '" << to - << "': " << e; - } - }; - - // If destination is not a directory path (no trailing separator) - // then move the filesystem entry to the specified path (the only - // source path is allowed in such a case). Otherwise move the source - // filesystem entries into the destination directory. - // - if (!dst.to_directory ()) - { - path src (parse_path (*i++, wd)); - - // If there are multiple sources but no trailing separator for the - // destination, then, most likelly, it is missing. - // - if (i != e) - error () << "multiple source paths without trailing separator " - << "for destination directory"; - - // Synopsis 1: move an entity to the specified path. - // - mv (src, dst); - } - else - { - // Synopsis 2: move entities into the specified directory. - // - for (; i != e; ++i) - { - path src (parse_path (*i, wd)); - mv (src, dst / src.leaf ()); - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // rm [-r] [-f] <path>... - // - // The implementation deviates from POSIX in a number of ways. It doesn't - // interact with a user and fails immediatelly if unable to process an - // argument. It doesn't check for dots containment in the path, and - // doesn't consider files and directory permissions in any way just - // trying to remove a filesystem entry. Always fails if empty path is - // specified. - // - // Note: can be executed synchronously. - // - static uint8_t - rm (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "rm"); - }; - - try - { - in.close (); - out.close (); - - auto i (args.begin ()); - auto e (args.end ()); - - // Process options. - // - bool dir (false); - bool force (false); - for (; i != e; ++i) - { - if (*i == "-r") - dir = true; - else if (*i == "-f") - force = true; - else - { - if (*i == "--") - ++i; - - break; - } - } - - // Remove entries. - // - if (i == e && !force) - error () << "missing file"; - - const dir_path& wd (sp.wd_path); - const dir_path& rwd (sp.root->wd_path); - - for (; i != e; ++i) - { - path p (parse_path (*i, wd)); - - if (!p.sub (rwd) && !force) - error () << "'" << p << "' is out of working directory '" << rwd - << "'"; - - try - { - dir_path d (path_cast<dir_path> (p)); - - if (dir_exists (d)) - { - if (!dir) - error () << "'" << p << "' is a directory"; - - if (wd.sub (d)) - error () << "'" << p << "' contains test working directory '" - << wd << "'"; - - // The call can result in rmdir_status::not_exist. That's not - // very likelly but there is also nothing bad about it. - // - try_rmdir_r (d); - } - else if (try_rmfile (p) == rmfile_status::not_exist && !force) - throw_generic_error (ENOENT); - } - catch (const system_error& e) - { - error () << "unable to remove '" << p << "': " << e; - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // rmdir [-f] <path>... - // - // Note: can be executed synchronously. - // - static uint8_t - rmdir (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "rmdir"); - }; - - try - { - in.close (); - out.close (); - - auto i (args.begin ()); - auto e (args.end ()); - - // Process options. - // - bool force (false); - for (; i != e; ++i) - { - if (*i == "-f") - force = true; - else - { - if (*i == "--") - ++i; - - break; - } - } - - // Remove directories. - // - if (i == e && !force) - error () << "missing directory"; - - const dir_path& wd (sp.wd_path); - const dir_path& rwd (sp.root->wd_path); - - for (; i != e; ++i) - { - dir_path p (path_cast<dir_path> (parse_path (*i, wd))); - - if (wd.sub (p)) - error () << "'" << p << "' contains test working directory '" - << wd << "'"; - - if (!p.sub (rwd) && !force) - error () << "'" << p << "' is out of working directory '" - << rwd << "'"; - - try - { - rmdir_status s (try_rmdir (p)); - - if (s == rmdir_status::not_empty) - throw_generic_error (ENOTEMPTY); - else if (s == rmdir_status::not_exist && !force) - throw_generic_error (ENOENT); - } - catch (const system_error& e) - { - error () << "unable to remove '" << p << "': " << e; - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // sed [-n] [-i] -e <script> [<file>] - // - // Note: must be executed asynchronously. - // - static uint8_t - sed (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "sed"); - }; - - try - { - // Automatically remove a temporary file (used for in place editing) - // on failure. - // - auto_rmfile rm; - - // Do not throw when failbit is set (getline() failed to extract any - // character). - // - ifdstream cin (move (in), ifdstream::badbit); - ofdstream cout (move (out)); - - auto i (args.begin ()); - auto e (args.end ()); - - // Process options. - // - bool auto_prn (true); - bool in_place (false); - - struct substitute - { - string regex; - string replacement; - bool icase = false; - bool global = false; - bool print = false; - }; - optional<substitute> subst; - - for (; i != e; ++i) - { - const string& o (*i); - - if (o == "-n") - auto_prn = false; - else if (o == "-i") - in_place = true; - else if (o == "-e") - { - // Only a single script is supported. - // - if (subst) - error () << "multiple scripts"; - - // If option has no value then bail out and report. - // - if (++i == e) - break; - - const string& v (*i); - if (v.empty ()) - error () << "empty script"; - - if (v[0] != 's') - error () << "only 's' command supported"; - - // Parse the substitute command. - // - if (v.size () < 2) - error () << "no delimiter for 's' command"; - - char delim (v[1]); - if (delim == '\\' || delim == '\n') - error () << "invalid delimiter for 's' command"; - - size_t p (v.find (delim, 2)); - if (p == string::npos) - error () << "unterminated 's' command regex"; - - subst = substitute (); - subst->regex.assign (v, 2, p - 2); - - // Empty regex matches nothing, so not of much use. - // - if (subst->regex.empty ()) - error () << "empty regex in 's' command"; - - size_t b (p + 1); - p = v.find (delim, b); - if (p == string::npos) - error () << "unterminated 's' command replacement"; - - subst->replacement.assign (v, b, p - b); - - // Parse the substitute command flags. - // - char c; - for (++p; (c = v[p]) != '\0'; ++p) - { - switch (c) - { - case 'i': subst->icase = true; break; - case 'g': subst->global = true; break; - case 'p': subst->print = true; break; - default: - { - error () << "invalid 's' command flag '" << c << "'"; - } - } - } - } - else - { - if (o == "--") - ++i; - - break; - } - } - - if (!subst) - error () << "missing script"; - - // Path of a file to edit. An empty path represents stdin. - // - path p; - if (i != e) - { - if (*i != "-") - p = parse_path (*i, sp.wd_path); - - ++i; - } - - if (i != e) - error () << "unexpected argument '" << *i << "'"; - - // If we edit file in place make sure that the file path is specified - // and obtain a temporary file path. We will be writing to the - // temporary file (rather than to stdout) and will move it to the - // original file path afterwards. - // - path tp; - if (in_place) - { - if (p.empty ()) - error () << "-i option specified while reading from stdin"; - - try - { - tp = path::temp_path ("build2-sed"); - - cout.close (); // Flush and close. - - cout.open ( - fdopen (tp, - fdopen_mode::out | fdopen_mode::truncate | - fdopen_mode::create, - path_permissions (p))); - } - catch (const io_error& e) - { - error_record d (error ()); - d << "unable to open '" << tp << "': " << e; - } - catch (const system_error& e) - { - error_record d (error ()); - d << "unable to obtain temporary file: " << e; - } - - rm = auto_rmfile (tp); - } - - // Note that ECMAScript is implied if no grammar flag is specified. - // - regex re (subst->regex, - subst->icase ? regex::icase : regex::ECMAScript); - - // Edit a file or STDIN. - // - try - { - // Open a file if specified. - // - if (!p.empty ()) - { - cin.close (); // Flush and close. - cin.open (p); - } - - // Read until failbit is set (throw on badbit). - // - string s; - while (getline (cin, s)) - { - auto r (regex_replace_search ( - s, - re, - subst->replacement, - subst->global - ? regex_constants::format_default - : regex_constants::format_first_only)); - - // Add newline regardless whether the source line is newline- - // terminated or not (in accordance with POSIX). - // - if (auto_prn || (r.second && subst->print)) - cout << r.first << '\n'; - } - - cin.close (); - cout.close (); - - if (in_place) - { - mvfile ( - tp, p, - cpflags::overwrite_content | cpflags::overwrite_permissions); - - rm.cancel (); - } - - r = 0; - } - catch (const io_error& e) - { - error_record d (error ()); - d << "unable to edit "; - - if (p.empty ()) - d << "stdin"; - else - d << "'" << p << "'"; - - d << ": " << e; - } - } - catch (const regex_error& e) - { - // Print regex_error description if meaningful (no space). - // - error (false) << "invalid regex" << e; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while creating cin, cout or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const system_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // sleep <seconds> - // - // Note: can be executed synchronously. - // - static uint8_t - sleep (scope&, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "sleep"); - }; - - try - { - in.close (); - out.close (); - - if (args.empty ()) - error () << "missing time interval"; - - if (args.size () > 1) - error () << "unexpected argument '" << args[1] << "'"; - - uint64_t n; - - for (;;) // Breakout loop. - { - const string& a (args[0]); - - // Note: strtoull() allows these. - // - if (!a.empty () && a[0] != '-' && a[0] != '+') - { - char* e (nullptr); - n = strtoull (a.c_str (), &e, 10); // Can't throw. - - if (errno != ERANGE && e == a.c_str () + a.size ()) - break; - } - - error () << "invalid time interval '" << a << "'"; - } - - // If/when required we could probably support the precise sleep mode - // (e.g., via an option). - // - sched.sleep (chrono::seconds (n)); - - r = 0; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // test -f|-d <path> - // - // Note: can be executed synchronously. - // - static uint8_t - test (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (2); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "test"); - }; - - try - { - in.close (); - out.close (); - - if (args.size () < 2) - error () << "missing path"; - - bool file (args[0] == "-f"); - - if (!file && args[0] != "-d") - error () << "invalid option"; - - if (args.size () > 2) - error () << "unexpected argument '" << args[2] << "'"; - - path p (parse_path (args[1], sp.wd_path)); - - try - { - r = (file ? file_exists (p) : dir_exists (p)) ? 0 : 1; - } - catch (const system_error& e) - { - error () << "cannot test '" << p << "': " << e; - } - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 2; - } - - // touch [--no-cleanup] [--after <ref-file>] <file>... - // - // Note that POSIX doesn't specify the behavior for touching an entry - // other than file. - // - // Also note that POSIX doesn't specify if after a file touch failure the - // command should proceed with the rest of the arguments. The current - // implementation exits immediatelly in such a case. - // - // Note: can be executed synchronously. - // - static uint8_t - touch (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "touch"); - }; - - try - { - in.close (); - out.close (); - - auto mtime = [&error] (const path& p) -> timestamp - { - try - { - timestamp t (file_mtime (p)); - - if (t == timestamp_nonexistent) - throw_generic_error (ENOENT); - - return t; - } - catch (const system_error& e) - { - error () << "cannot obtain file '" << p - << "' modification time: " << e; - } - assert (false); // Can't be here. - return timestamp (); - }; - - auto i (args.begin ()); - auto e (args.end ()); - - // Process options. - // - bool cleanup (true); - optional<timestamp> after; - for (; i != e; ++i) - { - const string& o (*i); - - if (o == "--no-cleanup") - cleanup = false; - else if (o == "--after") - { - if (++i == e) - error () << "missing --after option value"; - - after = mtime (parse_path (*i, sp.wd_path)); - } - else - { - if (o == "--") - ++i; - - break; - } - } - - if (i == e) - error () << "missing file"; - - // Create files. - // - for (; i != e; ++i) - { - path p (parse_path (*i, sp.wd_path)); - - try - { - // Note that we don't register (implicit) cleanup for an - // existing path. - // - if (touch_file (p) && cleanup) - sp.clean ({cleanup_type::always, p}, true); - - if (after) - { - while (mtime (p) <= *after) - touch_file (p, false /* create */); - } - } - catch (const system_error& e) - { - error () << "cannot create/update '" << p << "': " << e; - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // Run builtin implementation asynchronously. - // - static builtin - async_impl (builtin_impl* fn, - scope& sp, - uint8_t& r, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) - { - return builtin ( - r, - thread ([fn, &sp, &r, &args, - in = move (in), - out = move (out), - err = move (err)] () mutable noexcept - { - r = fn (sp, args, move (in), move (out), move (err)); - })); - } - - template <builtin_impl fn> - static builtin - async_impl (scope& sp, - uint8_t& r, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) - { - return async_impl (fn, sp, r, args, move (in), move (out), move (err)); - } - - // Run builtin implementation synchronously. - // - template <builtin_impl fn> - static builtin - sync_impl (scope& sp, - uint8_t& r, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) - { - r = fn (sp, args, move (in), move (out), move (err)); - return builtin (r, thread ()); - } - - const builtin_map builtins - { - {"cat", &async_impl<&cat>}, - {"cp", &sync_impl<&cp>}, - {"echo", &async_impl<&echo>}, - {"false", &false_}, - {"ln", &sync_impl<&ln>}, - {"mkdir", &sync_impl<&mkdir>}, - {"mv", &sync_impl<&mv>}, - {"rm", &sync_impl<&rm>}, - {"rmdir", &sync_impl<&rmdir>}, - {"sed", &async_impl<&sed>}, - {"sleep", &sync_impl<&sleep>}, - {"test", &sync_impl<&test>}, - {"touch", &sync_impl<&touch>}, - {"true", &true_} - }; - } - } -} diff --git a/build2/test/script/builtin.hxx b/build2/test/script/builtin.hxx deleted file mode 100644 index af7c809..0000000 --- a/build2/test/script/builtin.hxx +++ /dev/null @@ -1,74 +0,0 @@ -// file : build2/test/script/builtin.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_SCRIPT_BUILTIN_HXX -#define BUILD2_TEST_SCRIPT_BUILTIN_HXX - -#include <map> - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -namespace build2 -{ - namespace test - { - namespace script - { - class scope; - - // A process/thread-like object representing a running builtin. - // - // For now, instead of allocating the result storage dynamically, we - // expect it to be provided by the caller. - // - class builtin - { - public: - uint8_t - wait () {if (t_.joinable ()) t_.join (); return r_;} - - ~builtin () {wait ();} - - public: - builtin (uint8_t& r, thread&& t = thread ()): r_ (r), t_ (move (t)) {} - - builtin (builtin&&) = default; - - private: - uint8_t& r_; - thread t_; - }; - - // Start builtin command. Throw system_error on failure. - // - // Note that unlike argc/argv, our args don't include the program name. - // - using builtin_func = builtin (scope&, - uint8_t& result, - const strings& args, - auto_fd in, auto_fd out, auto_fd err); - - class builtin_map: public std::map<string, builtin_func*> - { - public: - using base = std::map<string, builtin_func*>; - using base::base; - - // Return NULL if not a builtin. - // - builtin_func* - find (const string& n) const - { - auto i (base::find (n)); - return i != end () ? i->second : nullptr; - } - }; - - extern const builtin_map builtins; - } - } -} - -#endif // BUILD2_TEST_SCRIPT_BUILTIN_HXX diff --git a/build2/test/script/lexer+command-expansion.test.testscript b/build2/test/script/lexer+command-expansion.test.testscript deleted file mode 100644 index 03e3366..0000000 --- a/build2/test/script/lexer+command-expansion.test.testscript +++ /dev/null @@ -1,248 +0,0 @@ -# file : build2/test/script/lexer+command-expansion.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -test.arguments = command-expansion - -: pass-redirect -: -{ - : in - : - $* <:"0<|" >>EOO - '0' - <| - EOO - - : arg-in - : - $* <:"0 <|" >>EOO - '0 ' - <| - EOO - - : out - : - $* <:"1>|" >>EOO - '1' - >| - EOO - - : arg-out - : - $* <:"1 >|" >>EOO - '1 ' - >| - EOO -} - -: null-redirect -: -{ - : in - : - $* <:"0<-" >>EOO - '0' - <- - EOO - - : arg-in - : - $* <:"0 <-" >>EOO - '0 ' - <- - EOO - - : out - : - $* <:"1>-" >>EOO - '1' - >- - EOO - - : arg-out - : - $* <:"1 >-" >>EOO - '1 ' - >- - EOO -} - -: trace-redirect -: -{ - : out - : - $* <:"1>!" >>EOO - '1' - >! - EOO - - : arg-out - : - $* <:"1 >!" >>EOO - '1 ' - >! - EOO -} - -: merge-redirect -: -{ - : out - : - $* <:"1>&2" >>EOO - '1' - >& - '2' - EOO - - : arg-out - : - $* <:"1 >&2" >>EOO - '1 ' - >& - '2' - EOO -} - -: str-redirect -: -{ - : in - : - { - : newline - : - $* <:"0<a b" >>EOO - '0' - < - 'a b' - EOO - - : no-newline - : - $* <:"0<:a b" >>EOO - '0' - <: - 'a b' - EOO - } - - : out - : - { - : newline - : - $* <:"1>a b" >>EOO - '1' - > - 'a b' - EOO - - : no-newline - : - $* <:"1>:a b" >>EOO - '1' - >: - 'a b' - EOO - } -} - -: doc-redirect -: -{ - : in - : - { - : newline - : - $* <:"0<<E O I" >>EOO - '0' - << - 'E O I' - EOO - - : no-newline - : - $* <:"0<<:E O I" >>EOO - '0' - <<: - 'E O I' - EOO - } - - : out - : - { - : newline - : - $* <:"1>>E O O" >>EOO - '1' - >> - 'E O O' - EOO - - : no-newline - : - $* <:"1>>:E O O" >>EOO - '1' - >>: - 'E O O' - EOO - } -} - -: file-redirect -: -{ - : in - : - $* <:"0<<<a b" >>EOO - '0' - <<< - 'a b' - EOO - - : out - : - $* <:"1>=a b" >>EOO - '1' - >= - 'a b' - EOO - - : out-app - : - $* <:"1>+a b" >>EOO - '1' - >+ - 'a b' - EOO -} - -: cleanup -: -{ - : always - : - $* <:"&file" >>EOO - & - 'file' - EOO - - : maybe - : - $* <:"&?file" >>EOO - &? - 'file' - EOO - - : never - : - $* <:"&!file" >>EOO - &! - 'file' - EOO -} diff --git a/build2/test/script/lexer+command-line.test.testscript b/build2/test/script/lexer+command-line.test.testscript deleted file mode 100644 index 65be837..0000000 --- a/build2/test/script/lexer+command-line.test.testscript +++ /dev/null @@ -1,208 +0,0 @@ -# file : build2/test/script/lexer+command-line.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -test.arguments = command-line - -: semi -{ - : immediate - : - $* <"cmd;" >>EOO - 'cmd' - ; - <newline> - EOO - - : separated - : - $* <"cmd ;" >>EOO - 'cmd' - ; - <newline> - EOO - - : only - : - $* <";" >>EOO - ; - <newline> - EOO -} - -: colon -: -{ - : immediate - : - $* <"cmd: dsc" >>EOO - 'cmd' - : - 'dsc' - <newline> - EOO - - : separated - : - $* <"cmd :dsc" >>EOO - 'cmd' - : - 'dsc' - <newline> - EOO - - : only - : - $* <":" >>EOO - : - <newline> - EOO -} - -: redirect -: -{ - : pass - : - $* <"cmd <| 1>|" >>EOO - 'cmd' - <| - '1' - >| - <newline> - EOO - - : null - : - $* <"cmd <- 1>-" >>EOO - 'cmd' - <- - '1' - >- - <newline> - EOO - - : trace - : - $* <"cmd 1>!" >>EOO - 'cmd' - '1' - >! - <newline> - EOO - - : merge - : - $* <"cmd 1>&2" >>EOO - 'cmd' - '1' - >& - '2' - <newline> - EOO - - : str - : - $* <"cmd <a 1>b" >>EOO - 'cmd' - < - 'a' - '1' - > - 'b' - <newline> - EOO - - : str-nn - : - $* <"cmd <:a 1>:b" >>EOO - 'cmd' - <: - 'a' - '1' - >: - 'b' - <newline> - EOO - - : doc - : - $* <"cmd <<EOI 1>>EOO" >>EOO - 'cmd' - << - 'EOI' - '1' - >> - 'EOO' - <newline> - EOO - - : doc-nn - : - $* <"cmd <<:EOI 1>>:EOO" >>EOO - 'cmd' - <<: - 'EOI' - '1' - >>: - 'EOO' - <newline> - EOO - - : file-cmp - : - $* <"cmd <<<in >>>out 2>>>err" >>EOO - 'cmd' - <<< - 'in' - >>> - 'out' - '2' - >>> - 'err' - <newline> - EOO - - : file-write - : - $* <"cmd >=out 2>+err" >>EOO - 'cmd' - >= - 'out' - '2' - >+ - 'err' - <newline> - EOO -} - -: cleanup -: -{ - : always - : - $* <"cmd &file" >>EOO - 'cmd' - & - 'file' - <newline> - EOO - - : maybe - : - $* <"cmd &?file" >>EOO - 'cmd' - &? - 'file' - <newline> - EOO - - : never - : - $* <"cmd &!file" >>EOO - 'cmd' - &! - 'file' - <newline> - EOO -} diff --git a/build2/test/script/lexer+description-line.test.testscript b/build2/test/script/lexer+description-line.test.testscript deleted file mode 100644 index 2d87d24..0000000 --- a/build2/test/script/lexer+description-line.test.testscript +++ /dev/null @@ -1,33 +0,0 @@ -# file : build2/test/script/lexer+description-line.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -test.arguments = description-line - -: full -: -$* <" foo bar " >>EOO -' foo bar ' -<newline> -EOO - -: space -: -$* <" " >>EOO -' ' -<newline> -EOO - -: empty -: -$* <"" >>EOO -<newline> -EOO - -: eof -: -$* <:"foo" >>EOO 2>>EOE != 0 -'foo' -EOO -stdin:1:4: error: expected newline at the end of description line -EOE diff --git a/build2/test/script/lexer+first-token.test.testscript b/build2/test/script/lexer+first-token.test.testscript deleted file mode 100644 index f20f261..0000000 --- a/build2/test/script/lexer+first-token.test.testscript +++ /dev/null @@ -1,97 +0,0 @@ -# file : build2/test/script/lexer+first-token.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -# Note: this mode auto-expires after each token. -# -test.arguments = first-token - -: dot -: -$* <"." >>EOO -. -<newline> -EOO - -: semi -: -$* <";" >>EOO -; -<newline> -EOO - -: colon -: -$* <":" >>EOO -: -<newline> -EOO - -: lcbrace -: -$* <"{" >>EOO -{ -<newline> -EOO - -: rcbrace -: -$* <"}" >>EOO -} -<newline> -EOO - -: setup -: -$* <"+foo" >>EOO -+ -'foo' -<newline> -EOO - -: tdown -: -$* <"- foo" >>EOO -- -'foo' -<newline> -EOO - -: plus-leading -: -$* <"foo+bar" >>EOO -'foo+bar' -<newline> -EOO - -: minus-leading -: -$* <"foo- x" >>EOO -'foo-' -'x' -<newline> -EOO - -: assign -: -$* <"foo=" >>EOO -'foo' -'=' -<newline> -EOO - -: append -: -$* <"foo+=" >>EOO -'foo' -'+=' -<newline> -EOO - -: prepend -: -$* <"foo=+" >>EOO -'foo' -'=+' -<newline> -EOO diff --git a/build2/test/script/lexer+second-token.test.testscript b/build2/test/script/lexer+second-token.test.testscript deleted file mode 100644 index 8fdee23..0000000 --- a/build2/test/script/lexer+second-token.test.testscript +++ /dev/null @@ -1,68 +0,0 @@ -# file : build2/test/script/lexer+second-token.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -# Note: this mode auto-expires after each token. -# -test.arguments = second-token - -: semi -: -$* <";" >>EOO -; -<newline> -EOO - -: colon -: -$* <":" >>EOO -: -<newline> -EOO - -: assign -: -$* <"=foo" >>EOO -= -'foo' -<newline> -EOO - -: append -: -$* <"+= foo" >>EOO -+= -'foo' -<newline> -EOO - -: prepend -: -$* <" =+ foo" >>EOO -=+ -'foo' -<newline> -EOO - -: assign-leading -: -$* <"foo=bar" >>EOO -'foo=bar' -<newline> -EOO - -: append-leading -: -$* <"foo+= bar" >>EOO -'foo+=' -'bar' -<newline> -EOO - -: prepend-leading -: -$* <"foo =+bar" >>EOO -'foo' -'=+bar' -<newline> -EOO diff --git a/build2/test/script/lexer+variable-line.test.testscript b/build2/test/script/lexer+variable-line.test.testscript deleted file mode 100644 index b9c558d..0000000 --- a/build2/test/script/lexer+variable-line.test.testscript +++ /dev/null @@ -1,28 +0,0 @@ -# file : build2/test/script/lexer+variable-line.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -test.arguments = variable-line - -: semi -: -$* <"cmd;" >>EOO -'cmd' -; -<newline> -EOO - -: semi-separated -: -$* <"cmd ;" >>EOO -'cmd' -; -<newline> -EOO - -: semi-only -: -$* <";" >>EOO -; -<newline> -EOO diff --git a/build2/test/script/lexer+variable.test.testscript b/build2/test/script/lexer+variable.test.testscript deleted file mode 100644 index 0ec323b..0000000 --- a/build2/test/script/lexer+variable.test.testscript +++ /dev/null @@ -1,70 +0,0 @@ -# file : build2/test/script/lexer+variable.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -# Test handling custom variable names ($*, $~, $NN). -# -test.arguments = variable - -: command -: -{ - : only - : - $* <"*" >>EOO - '*' - <newline> - EOO - - : followed - : - $* <"*abc" >>EOO - '*' - 'abc' - <newline> - EOO -} - -: working-dir -: -{ - : only - : - $* <"~" >>EOO - '~' - <newline> - EOO - - : followed - : - $* <"~123" >>EOO - '~' - '123' - <newline> - EOO -} - -: arg -: -{ - : only - : - $* <"0" >>EOO - '0' - <newline> - EOO - - : followed - : - $* <"1abc" >>EOO - '1' - 'abc' - <newline> - EOO - - : multi-digit - : - $* <"10" 2>>EOE != 0 - stdin:1:1: error: multi-digit special variable name - EOE -} diff --git a/build2/test/script/lexer.cxx b/build2/test/script/lexer.cxx deleted file mode 100644 index 2470fcc..0000000 --- a/build2/test/script/lexer.cxx +++ /dev/null @@ -1,551 +0,0 @@ -// file : build2/test/script/lexer.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/test/script/lexer.hxx> - -#include <cstring> // strchr() - -using namespace std; - -namespace build2 -{ - namespace test - { - namespace script - { - using type = token_type; - - void lexer:: - mode (base_mode m, char ps, optional<const char*> esc) - { - const char* s1 (nullptr); - const char* s2 (nullptr); - bool s (true); - bool n (true); - bool q (true); - - if (!esc) - { - assert (!state_.empty ()); - esc = state_.top ().escapes; - } - - switch (m) - { - case lexer_mode::command_line: - { - s1 = ":;=!|&<> $(#\t\n"; - s2 = " == "; - break; - } - case lexer_mode::first_token: - { - // First token on the script line. Like command_line but - // recognizes leading '.+-{}' as tokens as well as variable - // assignments as separators. - // - // Note that to recognize only leading '.+-{}' we shouldn't add - // them to the separator strings. - // - s1 = ":;=+!|&<> $(#\t\n"; - s2 = " == "; - break; - } - case lexer_mode::second_token: - { - // Second token on the script line. Like command_line but - // recognizes leading variable assignments. - // - // Note that to recognize only leading assignments we shouldn't - // add them to the separator strings (so this is identical to - // command_line). - // - s1 = ":;=!|&<> $(#\t\n"; - s2 = " == "; - break; - } - case lexer_mode::variable_line: - { - // Like value except we recognize ';' and don't recognize '{'. - // Note that we don't recognize ':' since having a trailing - // variable assignment is illegal. - // - s1 = "; $([]#\t\n"; - s2 = " "; - break; - } - - case lexer_mode::command_expansion: - { - // Note that whitespaces are not word separators in this mode. - // - s1 = "|&<>"; - s2 = " "; - s = false; - break; - } - case lexer_mode::here_line_single: - { - // This one is like a single-quoted string except it treats - // newlines as a separator. We also treat quotes as literals. - // - // Note that it might be tempting to enable line continuation - // escapes. However, we will then have to also enable escaping of - // the backslash, which makes it a lot less tempting. - // - s1 = "\n"; - s2 = " "; - esc = ""; // Disable escape sequences. - s = false; - q = false; - break; - } - case lexer_mode::here_line_double: - { - // This one is like a double-quoted string except it treats - // newlines as a separator. We also treat quotes as literals. - // - s1 = "$(\n"; - s2 = " "; - s = false; - q = false; - break; - } - case lexer_mode::description_line: - { - // This one is like a single-quoted string and has an ad hoc - // implementation. - // - break; - } - default: - { - // Make sure pair separators are only enabled where we expect - // them. - // - // @@ Should we disable pair separators in the eval mode? - // - assert (ps == '\0' || - m == lexer_mode::eval || - m == lexer_mode::attribute); - - base_lexer::mode (m, ps, esc); - return; - } - } - - assert (ps == '\0'); - state_.push (state {m, ps, s, n, q, *esc, s1, s2}); - } - - token lexer:: - next () - { - token r; - - switch (state_.top ().mode) - { - case lexer_mode::command_line: - case lexer_mode::first_token: - case lexer_mode::second_token: - case lexer_mode::variable_line: - case lexer_mode::command_expansion: - case lexer_mode::here_line_single: - case lexer_mode::here_line_double: - r = next_line (); - break; - case lexer_mode::description_line: - r = next_description (); - break; - default: - r = base_lexer::next (); - break; - } - - if (r.qtype != quote_type::unquoted) - ++quoted_; - - return r; - } - - token lexer:: - next_line () - { - bool sep (skip_spaces ()); - - xchar c (get ()); - uint64_t ln (c.line), cn (c.column); - - if (eos (c)) - return token (type::eos, sep, ln, cn, token_printer); - - state st (state_.top ()); // Make copy (see first/second_token). - lexer_mode m (st.mode); - - auto make_token = [&sep, &m, ln, cn] (type t, string v = string ()) - { - bool q (m == lexer_mode::here_line_double); - - return token (t, move (v), sep, - (q ? quote_type::double_ : quote_type::unquoted), q, - ln, cn, - token_printer); - }; - - auto make_token_with_modifiers = - [&make_token, this] (type t, - const char* mods, // To recorgnize. - const char* stop = nullptr) // To stop after. - { - string v; - if (mods != nullptr) - { - for (xchar p (peek ()); - (strchr (mods, p) != nullptr && // Modifier. - strchr (v.c_str (), p) == nullptr); // Not already seen. - p = peek ()) - { - get (); - v += p; - - if (stop != nullptr && strchr (stop, p) != nullptr) - break; - } - } - - return make_token (t, move (v)); - }; - - // Expire certain modes at the end of the token. Do it early in case - // we push any new mode (e.g., double quote). - // - if (m == lexer_mode::first_token || m == lexer_mode::second_token) - state_.pop (); - - // NOTE: remember to update mode() if adding new special characters. - - if (m != lexer_mode::command_expansion) - { - switch (c) - { - case '\n': - { - // Expire variable value mode at the end of the line. - // - if (m == lexer_mode::variable_line) - state_.pop (); - - sep = true; // Treat newline as always separated. - return make_token (type::newline); - } - } - } - - if (m != lexer_mode::here_line_single) - { - switch (c) - { - // Variable expansion, function call, and evaluation context. - // - case '$': return make_token (type::dollar); - case '(': return make_token (type::lparen); - } - } - - - if (m == lexer_mode::variable_line) - { - switch (c) - { - // Attributes. - // - case '[': return make_token (type::lsbrace); - case ']': return make_token (type::rsbrace); - } - } - - // Line separators. - // - if (m == lexer_mode::command_line || - m == lexer_mode::first_token || - m == lexer_mode::second_token || - m == lexer_mode::variable_line) - { - switch (c) - { - case ';': return make_token (type::semi); - } - } - - if (m == lexer_mode::command_line || - m == lexer_mode::first_token || - m == lexer_mode::second_token) - { - switch (c) - { - case ':': return make_token (type::colon); - } - } - - // Command line operator/separators. - // - if (m == lexer_mode::command_line || - m == lexer_mode::first_token || - m == lexer_mode::second_token) - { - switch (c) - { - // Comparison (==, !=). - // - case '=': - case '!': - { - if (peek () == '=') - { - get (); - return make_token (c == '=' ? type::equal : type::not_equal); - } - } - } - } - - // Command operators/separators. - // - if (m == lexer_mode::command_line || - m == lexer_mode::first_token || - m == lexer_mode::second_token || - m == lexer_mode::command_expansion) - { - switch (c) - { - // |, || - // - case '|': - { - if (peek () == '|') - { - get (); - return make_token (type::log_or); - } - else - return make_token (type::pipe); - } - // &, && - // - case '&': - { - xchar p (peek ()); - - if (p == '&') - { - get (); - return make_token (type::log_and); - } - - // These modifiers are mutually exclusive so stop after seeing - // either one. - // - return make_token_with_modifiers (type::clean, "!?", "!?"); - } - // < - // - case '<': - { - type r (type::in_str); - xchar p (peek ()); - - if (p == '|' || p == '-' || p == '<') - { - get (); - - switch (p) - { - case '|': return make_token (type::in_pass); - case '-': return make_token (type::in_null); - case '<': - { - r = type::in_doc; - p = peek (); - - if (p == '<') - { - get (); - r = type::in_file; - } - break; - } - } - } - - // Handle modifiers. - // - const char* mods (nullptr); - switch (r) - { - case type::in_str: - case type::in_doc: mods = ":/"; break; - } - - return make_token_with_modifiers (r, mods); - } - // > - // - case '>': - { - type r (type::out_str); - xchar p (peek ()); - - if (p == '|' || p == '-' || p == '!' || p == '&' || - p == '=' || p == '+' || p == '>') - { - get (); - - switch (p) - { - case '|': return make_token (type::out_pass); - case '-': return make_token (type::out_null); - case '!': return make_token (type::out_trace); - case '&': return make_token (type::out_merge); - case '=': return make_token (type::out_file_ovr); - case '+': return make_token (type::out_file_app); - case '>': - { - r = type::out_doc; - p = peek (); - - if (p == '>') - { - get (); - r = type::out_file_cmp; - } - break; - } - } - } - - // Handle modifiers. - // - const char* mods (nullptr); - const char* stop (nullptr); - switch (r) - { - case type::out_str: - case type::out_doc: mods = ":/~"; stop = "~"; break; - } - - return make_token_with_modifiers (r, mods, stop); - } - } - } - - // Dot, plus/minus, and left/right curly braces. - // - if (m == lexer_mode::first_token) - { - switch (c) - { - case '.': return make_token (type::dot); - case '+': return make_token (type::plus); - case '-': return make_token (type::minus); - case '{': return make_token (type::lcbrace); - case '}': return make_token (type::rcbrace); - } - } - - // Variable assignment (=, +=, =+). - // - if (m == lexer_mode::second_token) - { - switch (c) - { - case '=': - { - if (peek () == '+') - { - get (); - return make_token (type::prepend); - } - else - return make_token (type::assign); - } - case '+': - { - if (peek () == '=') - { - get (); - return make_token (type::append); - } - } - } - } - - // Otherwise it is a word. - // - unget (c); - return word (st, sep); - } - - token lexer:: - next_description () - { - xchar c (peek ()); - - if (eos (c)) - fail (c) << "expected newline at the end of description line"; - - uint64_t ln (c.line), cn (c.column); - - if (c == '\n') - { - get (); - state_.pop (); // Expire the description mode. - return token (type::newline, true, ln, cn, token_printer); - } - - string lexeme; - - // For now no line continutions though we could support them. - // - for (; !eos (c) && c != '\n'; c = peek ()) - { - get (); - lexeme += c; - } - - return token (move (lexeme), - false, - quote_type::unquoted, false, - ln, cn); - } - - token lexer:: - word (state st, bool sep) - { - lexer_mode m (st.mode); - - // Customized implementation that handles special variable names ($*, - // $N, $~, $@). - // - if (m != lexer_mode::variable) - return base_lexer::word (st, sep); - - xchar c (peek ()); - - if (c != '*' && c != '~' && c != '@' && !digit (c)) - return base_lexer::word (st, sep); - - get (); - - if (digit (c) && digit (peek ())) - fail (c) << "multi-digit special variable name"; - - state_.pop (); // Expire the variable mode. - return token (string (1, c), - sep, - quote_type::unquoted, false, - c.line, c.column); - } - } - } -} diff --git a/build2/test/script/lexer.hxx b/build2/test/script/lexer.hxx deleted file mode 100644 index ad1c386..0000000 --- a/build2/test/script/lexer.hxx +++ /dev/null @@ -1,94 +0,0 @@ -// file : build2/test/script/lexer.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_SCRIPT_LEXER_HXX -#define BUILD2_TEST_SCRIPT_LEXER_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/lexer.hxx> - -#include <build2/test/script/token.hxx> - -namespace build2 -{ - namespace test - { - namespace script - { - struct lexer_mode: build2::lexer_mode - { - using base_type = build2::lexer_mode; - - enum - { - command_line = base_type::value_next, - first_token, // Expires at the end of the token. - second_token, // Expires at the end of the token. - variable_line, // Expires at the end of the line. - command_expansion, - here_line_single, - here_line_double, - description_line // Expires at the end of the line. - }; - - lexer_mode () = default; - lexer_mode (value_type v): base_type (v) {} - lexer_mode (base_type v): base_type (v) {} - }; - - class lexer: public build2::lexer - { - public: - using base_lexer = build2::lexer; - using base_mode = build2::lexer_mode; - - lexer (istream& is, - const path& name, - lexer_mode m, - const char* escapes = nullptr) - : base_lexer (is, - name, - 1 /* line */, - nullptr /* escapes */, - false /* set_mode */) - { - mode (m, '\0', escapes); - } - - virtual void - mode (base_mode, - char = '\0', - optional<const char*> = nullopt) override; - - // Number of quoted (double or single) tokens since last reset. - // - size_t - quoted () const {return quoted_;} - - void - reset_quoted (size_t q) {quoted_ = q;} - - virtual token - next () override; - - protected: - token - next_line (); - - token - next_description (); - - virtual token - word (state, bool) override; - - protected: - size_t quoted_; - }; - } - } -} - -#endif // BUILD2_TEST_SCRIPT_LEXER_HXX diff --git a/build2/test/script/lexer.test.cxx b/build2/test/script/lexer.test.cxx deleted file mode 100644 index c9905ec..0000000 --- a/build2/test/script/lexer.test.cxx +++ /dev/null @@ -1,85 +0,0 @@ -// file : build2/test/script/lexer.test.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <cassert> -#include <iostream> - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <build2/test/script/token.hxx> -#include <build2/test/script/lexer.hxx> - -using namespace std; - -namespace build2 -{ - namespace test - { - namespace script - { - // Usage: argv[0] <lexer-mode> - // - int - main (int argc, char* argv[]) - { - lexer_mode m; - { - assert (argc == 2); - string s (argv[1]); - - if (s == "command-line") m = lexer_mode::command_line; - else if (s == "first-token") m = lexer_mode::first_token; - else if (s == "second-token") m = lexer_mode::second_token; - else if (s == "variable-line") m = lexer_mode::variable_line; - else if (s == "command-expansion") m = lexer_mode::command_expansion; - else if (s == "here-line-single") m = lexer_mode::here_line_single; - else if (s == "here-line-double") m = lexer_mode::here_line_double; - else if (s == "description-line") m = lexer_mode::description_line; - else if (s == "variable") m = lexer_mode::variable; - else assert (false); - } - - try - { - cin.exceptions (istream::failbit | istream::badbit); - - // Some modes auto-expire so we need something underneath. - // - bool u (m == lexer_mode::first_token || - m == lexer_mode::second_token || - m == lexer_mode::variable_line || - m == lexer_mode::description_line || - m == lexer_mode::variable); - - lexer l (cin, path ("stdin"), u ? lexer_mode::command_line : m); - if (u) - l.mode (m); - - // No use printing eos since we will either get it or loop forever. - // - for (token t (l.next ()); t.type != token_type::eos; t = l.next ()) - { - // Print each token on a separate line without quoting operators. - // - t.printer (cout, t, false); - cout << endl; - } - } - catch (const failed&) - { - return 1; - } - - return 0; - } - } - } -} - -int -main (int argc, char* argv[]) -{ - return build2::test::script::main (argc, argv); -} diff --git a/build2/test/script/parser+cleanup.test.testscript b/build2/test/script/parser+cleanup.test.testscript deleted file mode 100644 index 2c94afc..0000000 --- a/build2/test/script/parser+cleanup.test.testscript +++ /dev/null @@ -1,58 +0,0 @@ -# file : build2/test/script/parser+cleanup.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: always -: -$* <<EOI >>EOO -cmd &file -EOI -cmd &file -EOO - -: maybe -: -$* <<EOI >>EOO -cmd &?file -EOI -cmd &?file -EOO - -: never -: -$* <<EOI >>EOO -cmd &!file -EOI -cmd &!file -EOO - -: empty -: -$* <<EOI 2>>EOE != 0 -cmd &"" -EOI -testscript:1:6: error: empty cleanup path -EOE - -: missed-before -: -{ - : token - : - : Path missed before command next token - : - $* <<EOI 2>>EOE != 0 - cmd & >file - EOI - testscript:1:7: error: missing cleanup path - EOE - - : end - : Test path missed before end of command - : - $* <<EOI 2>>EOE != 0 - cmd & - EOI - testscript:1:6: error: missing cleanup path - EOE -} diff --git a/build2/test/script/parser+command-if.test.testscript b/build2/test/script/parser+command-if.test.testscript deleted file mode 100644 index ab6e6d5..0000000 --- a/build2/test/script/parser+command-if.test.testscript +++ /dev/null @@ -1,548 +0,0 @@ -# file : build2/test/script/parser+command-if.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: if -: -{ - : true - : - $* <<EOI >>EOO - if true foo - cmd1 - cmd2 - end - EOI - ? true foo - cmd1 - cmd2 - EOO - - : false - : - $* <<EOI >>EOO - if false foo - cmd1 - cmd2 - end - EOI - ? false foo - EOO - - : not-true - : - $* <<EOI >>EOO - if! true foo - cmd1 - cmd2 - end - EOI - ? true foo - EOO - - : not-false - : - $* <<EOI >>EOO - if! false foo - cmd1 - cmd2 - end - EOI - ? false foo - cmd1 - cmd2 - EOO - - : without-command - : - $* <<EOI 2>>EOE != 0 - if - cmd - end - EOI - testscript:1:3: error: missing program - EOE - - : after-semi - : - $* -s <<EOI >>EOO - cmd1; - if true - cmd2 - end - EOI - { - { - cmd1 - ? true - cmd2 - } - } - EOO - - : setup - : - $* -s <<EOI >>EOO - +if true - cmd - end - EOI - { - ? true - +cmd - } - EOO - - : tdown - : - $* -s <<EOI >>EOO - -if true - cmd - end - EOI - { - ? true - -cmd - } - EOO -} - -: elif -: -{ - : true - : - $* <<EOI >>EOO - if false - cmd1 - cmd2 - elif true - cmd3 - cmd4 - end - EOI - ? false - ? true - cmd3 - cmd4 - EOO - - : false - : - $* <<EOI >>EOO - if false - cmd1 - cmd2 - elif false - cmd3 - cmd4 - end - EOI - ? false - ? false - EOO - - : not-true - : - $* <<EOI >>EOO - if false - cmd1 - cmd2 - elif! true - cmd3 - cmd4 - end - EOI - ? false - ? true - EOO - - : not-false - : - $* <<EOI >>EOO - if false - cmd1 - cmd2 - elif! false - cmd3 - cmd4 - end - EOI - ? false - ? false - cmd3 - cmd4 - EOO - - : without-if - : - $* <<EOI 2>>EOE != 0 - cmd - elif true - cmd - end - EOI - testscript:2:1: error: 'elif' without preceding 'if' - EOE - - : not-without-if - : - $* <<EOI 2>>EOE != 0 - cmd - elif! true - cmd - end - EOI - testscript:2:1: error: 'elif!' without preceding 'if' - EOE - - : after-else - : - $* <<EOI 2>>EOE != 0 - if false - cmd - else - cmd - elif true - cmd - end - EOI - testscript:5:1: error: 'elif' after 'else' - EOE -} - -: else -: -{ - : true - : - $* <<EOI >>EOO - if false - cmd1 - cmd2 - else - cmd3 - cmd4 - end - EOI - ? false - cmd3 - cmd4 - EOO - - : false - : - $* <<EOI >>EOO - if true - cmd1 - cmd2 - else - cmd3 - cmd4 - end - EOI - ? true - cmd1 - cmd2 - EOO - - : chain - : - $* <<EOI >>EOO - if false - cmd - cmd - elif false - cmd - cmd - elif false - cmd - cmd - elif true - cmd1 - cmd2 - elif false - cmd - cmd - else - cmd - cmd - end - EOI - ? false - ? false - ? false - ? true - cmd1 - cmd2 - EOO - - : command-after - : - $* <<EOI 2>>EOE != 0 - if true - cmd - else cmd - cmd - end - EOI - testscript:3:6: error: expected newline instead of 'cmd' - EOE - - : without-if - : - $* <<EOI 2>>EOE != 0 - cmd - else - cmd - end - EOI - testscript:2:1: error: 'else' without preceding 'if' - EOE - - : after-else - : - $* <<EOI 2>>EOE != 0 - if false - cmd - else - cmd - else - cmd - end - EOI - testscript:5:1: error: 'else' after 'else' - EOE -} - -: end -{ - : without-if - : - $* <<EOI 2>>EOE != 0 - cmd - end - EOI - testscript:2:1: error: 'end' without preceding 'if' - EOE - - : before - { - : semi - : - $* -s <<EOI >>EOO - if true - cmd1 - end; - cmd2 - EOI - { - { - ? true - cmd1 - cmd2 - } - } - EOO - - : command - : - $* <<EOI 2>>EOE != 0 - if true - cmd - end cmd - EOI - testscript:3:5: error: expected newline instead of 'cmd' - EOE - - : colon - : - $* -s <<EOI >>EOO - if true - cmd1 - cmd2 - end : test - EOI - { - : id:test - { - ? true - cmd1 - cmd2 - } - } - EOO - } -} - -: nested -: -{ - : take - : - $* <<EOI >>EOO - if true - cmd1 - if false - cmd - elif false - if true - cmd - end - else - cmd2 - end - cmd3 - end - EOI - ? true - cmd1 - ? false - ? false - cmd2 - cmd3 - EOO - - : skip - : - $* <<EOI >>EOO - if false - cmd1 - if false - cmd - elif false - if true - cmd - end - else - cmd2 - end - cmd3 - else - cmd - end - EOI - ? false - cmd - EOO -} - -: contained -{ - : semi - : - $* <<EOI 2>>EOE != 0 - if - cmd; - cmd - end - EOI - testscript:2:3: error: ';' inside 'if' - EOE - - : colon-leading - : - $* <<EOI 2>>EOE != 0 - if - : foo - cmd - end - EOI - testscript:2:3: error: description inside 'if' - EOE - - : colon-trailing - : - $* <<EOI 2>>EOE != 0 - if - cmd : foo - end - EOI - testscript:2:3: error: description inside 'if' - EOE - - : eos - : - $* <<EOI 2>>EOE != 0 - if - EOI - testscript:2:1: error: expected closing 'end' - EOE - - : scope - : - $* <<EOI 2>>EOE != 0 - if - cmd - { - } - end - EOI - testscript:3:3: error: expected closing 'end' - EOE - - : setup - : - $* <<EOI 2>>EOE != 0 - if - +cmd - end - EOI - testscript:2:3: error: setup command inside 'if' - EOE - - : tdown - : - $* <<EOI 2>>EOE != 0 - if - -cmd - end - EOI - testscript:2:3: error: teardown command inside 'if' - EOE -} - -: line-index -: -$* -l <<EOI >>EOO -if false - cmd - if true - cmd - end - cmd -elif false - cmd -else - cmd -end -EOI -? false # 1 -? false # 6 -cmd # 8 -EOO - -: var -: -$* <<EOI >>EOO -if true - x = foo -else - x = bar -end; -cmd $x -EOI -? true -cmd foo -EOO - -: leading-and-trailing-description -: -$* <<EOI 2>>EOE != 0 -: foo -if true - cmd -end : bar -EOI -testscript:4:1: error: both leading and trailing descriptions -EOE diff --git a/build2/test/script/parser+command-re-parse.test.testscript b/build2/test/script/parser+command-re-parse.test.testscript deleted file mode 100644 index ef030de..0000000 --- a/build2/test/script/parser+command-re-parse.test.testscript +++ /dev/null @@ -1,12 +0,0 @@ -# file : build2/test/script/parser+command-re-parse.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: double-quote -: -$* <<EOI >>EOO -x = cmd \">-\" "'<-'" -$x -EOI -cmd '>-' '<-' -EOO diff --git a/build2/test/script/parser+description.test.testscript b/build2/test/script/parser+description.test.testscript deleted file mode 100644 index f38c8d2..0000000 --- a/build2/test/script/parser+description.test.testscript +++ /dev/null @@ -1,486 +0,0 @@ -# file : build2/test/script/parser+description.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: id -: -{ - : lead - : - $* <<EOI >>EOO - : foo - cmd - EOI - : id:foo - cmd - EOO - - : trail - : - $* <<EOI >>EOO - cmd : foo - EOI - : id:foo - cmd - EOO - - : dup - : Id uniqueness - : - { - : test - : - { - : test - : - $* <<EOI 2>>EOE != 0 - : foo - cmd - : foo - cmd - EOI - testscript:3:1: error: duplicate id foo - testscript:1:1: info: previously used here - EOE - - : group - : - $* <<EOI 2>>EOE != 0 - : foo - cmd - : foo - { - cmd - cmd - } - EOI - testscript:3:1: error: duplicate id foo - testscript:1:1: info: previously used here - EOE - - : derived - : - $* <<EOI 2>>EOE != 0 - : 3 - cmd - cmd - EOI - testscript:3:1: error: duplicate id 3 - testscript:1:1: info: previously used here - EOE - } - - : group - : - { - : test - : - $* <<EOI 2>>EOE != 0 - : foo - { - cmd - cmd - } - : foo - cmd - EOI - testscript:6:1: error: duplicate id foo - testscript:1:1: info: previously used here - EOE - - : group - : - $* <<EOI 2>>EOE != 0 - : foo - { - cmd - cmd - } - : foo - { - cmd - cmd - } - EOI - testscript:6:1: error: duplicate id foo - testscript:1:1: info: previously used here - EOE - - : derived - : - $* <<EOI 2>>EOE != 0 - : 3 - cmd - { - cmd - cmd - } - EOI - testscript:3:1: error: duplicate id 3 - testscript:1:1: info: previously used here - EOE - } - } -} - -: summary -{ - : lead - : - $* <<EOI >>EOO - : foo bar - cmd - EOI - : sm:foo bar - cmd - EOO - - : trail - : - $* <<EOI >>EOO - cmd: foo bar - EOI - : sm:foo bar - cmd - EOO - - : id - : - $* <<EOI >>EOO - : foo-bar - : foo bar - cmd - EOI - : id:foo-bar - : sm:foo bar - cmd - EOO -} - -: details -{ - : id - : - $* <<EOI >>EOO - : foo-bar - : - : foo bar - : bar baz - cmd - EOI - : id:foo-bar - : - : foo bar - : bar baz - cmd - EOO - - : summary - : - { - : only - : - $* <<EOI >>EOO - : foo bar - : - : foo bar - : bar baz - cmd - EOI - : sm:foo bar - : - : foo bar - : bar baz - cmd - EOO - - : assumed - : - $* <<EOI >>EOO - : foo bar - : bar baz - cmd - EOI - : foo bar - : bar baz - cmd - EOO - - : id - : - $* <<EOI >>EOO - : foo-bar - : foo bar - : - : foo bar - : bar baz - cmd - EOI - : id:foo-bar - : sm:foo bar - : - : foo bar - : bar baz - cmd - EOO - - : id-assumed - : - $* <<EOI >>EOO - : foo-bar - : bar baz - : baz fox - cmd - EOI - : foo-bar - : bar baz - : baz fox - cmd - EOO - } -} - -: legal -: -: Legal places for description. -: -{ - : var - : - $* <<EOI >>EOO - : foo bar - x = y; - cmd $x - EOI - : sm:foo bar - cmd y - EOO -} - -: illegal -: -: Illegal places for description. -: -{ - : eof - : - $* <": foo" 2>>EOE != 0 - testscript:2:1: error: description before <end of file> - EOE - - : rcbrace - : - $* <<EOI 2>>EOE != 0 - { - cmd - : foo - } - EOI - testscript:4:1: error: description before '}' - EOE - - : setup - : - $* <<EOI 2>>EOE != 0 - : foo - +cmd - EOI - testscript:2:1: error: description before setup command - EOE - - : tdown - : - $* <<EOI 2>>EOE != 0 - : foo - -cmd - EOI - testscript:2:1: error: description before teardown command - EOE - - : var - : - $* <<EOI 2>>EOE != 0 - : foo - x = y - EOI - testscript:2:1: error: description before setup/teardown variable - EOE - - : var-if - : - $* <<EOI 2>>EOE != 0 - : foo - if true - x = y - end - EOI - testscript:2:1: error: description before/after setup/teardown variable-if - EOE - - : var-if-after - : - $* <<EOI 2>>EOE != 0 - if true - x = y - end : foo - EOI - testscript:1:1: error: description before/after setup/teardown variable-if - EOE - - : test - : - $* <<EOI 2>>EOE != 0 - cmd1; - : foo - cmd2 - EOI - testscript:2:1: error: description inside test - EOE -} - -: test-scope -: -: Interaction with test scope merging. -: -{ - : both - : - : No merge since both have description. - : - $* -s -i <<EOI >>EOO - : foo - { - : bar - cmd - } - EOI - { - : id:foo - { # foo - : id:bar - { # foo/bar - cmd - } - } - } - EOO - - : test - : - : No merge since test has description. - : - $* -s -i <<EOI >>EOO - { - : foo-bar - : foo bar - cmd - } - EOI - { - { # 1 - : id:foo-bar - : sm:foo bar - { # 1/foo-bar - cmd - } - } - } - EOO - - : group - : - $* -s -i <<EOI >>EOO - : foo-bar - : foo bar - { - cmd - } - EOI - { - : id:foo-bar - : sm:foo bar - { # foo-bar - cmd - } - } - EOO -} - -: blanks -: -$* <<EOI >>EOO -: -: -: foo bar -: bar baz -: -: baz fox -: -: -cmd -EOI -: foo bar -: bar baz -: -: baz fox -cmd -EOO - -: strip -: -$* <<EOI >>EOO -: foo-bar -: bar baz -: -: baz fox -: fox biz -:biz buz -: -cmd -EOI -: id:foo-bar -: sm:bar baz -: -: baz fox -: fox biz -: biz buz -cmd -EOO - -: trail-compound -: -$* <<EOI >>EOO -cmd1; -cmd2: foo -EOI -: id:foo -cmd1 -cmd2 -EOO - -: empty -: -$* <<EOI 2>>EOE != 0 -: -: -cmd -EOI -testscript:1:1: error: empty description -EOE - -: trail-empty -: -$* <<EOI 2>>EOE != 0 -cmd: -EOI -testscript:1:4: error: empty description -EOE - -: both -: -$* <<EOI 2>>EOE != 0 -: foo -cmd : bar -EOI -testscript:2:1: error: both leading and trailing descriptions -EOE diff --git a/build2/test/script/parser+directive.test.testscript b/build2/test/script/parser+directive.test.testscript deleted file mode 100644 index addd874..0000000 --- a/build2/test/script/parser+directive.test.testscript +++ /dev/null @@ -1,74 +0,0 @@ -# file : build2/test/script/parser+directive.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: not-directive -: -$* <<EOI >>EOO -x = x -".include" foo.testscript -\.include foo.testscript -EOI -.include foo.testscript -.include foo.testscript -EOO - -: expected-name -: -$* <<EOI 2>>EOE != 0 -.$ -EOI -testscript:1:2: error: expected directive name instead of '$' -EOE - -: unknown-name -: -$* <<EOI 2>>EOE != 0 -.bogus -EOI -testscript:1:2: error: unknown directive 'bogus' -EOE - -: separated -: -touch foo.testscript; -$* <<EOI -. include foo.testscript -EOI - -: not-separated -: -touch foo.testscript; -$* <<EOI -x = foo.testscript -.include$x -EOI - -: var-expansion -: -cat <<EOI >="foo-$(build.verson.project).testscript"; -cmd -EOI -$* <<EOI >>EOO -.include "foo-$(build.verson.project).testscript" -EOI -cmd -EOO - -: after-semi -: -$* <<EOI 2>>EOE != 0 -cmd; -.include foo.testscript -EOI -testscript:2:1: error: directive after ';' -EOE - -: semi-after -: -$* <<EOI 2>>EOE != 0 -.include foo.testscript; -cmd -EOI -testscript:1:24: error: ';' after directive -EOE diff --git a/build2/test/script/parser+exit.test.testscript b/build2/test/script/parser+exit.test.testscript deleted file mode 100644 index 014afa4..0000000 --- a/build2/test/script/parser+exit.test.testscript +++ /dev/null @@ -1,27 +0,0 @@ -# file : build2/test/script/parser+exit.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: eq -: -$* <<EOI >>EOO -cmd == 1 -EOI -cmd == 1 -EOO - -: ne -: -$* <<EOI >>EOO -cmd!=1 -EOI -cmd != 1 -EOO - -: end -: -$* <<EOI 2>>EOE != 0 -cmd != 1 <"foo" -EOI -testscript:1:10: error: unexpected '<' after command exit status -EOE diff --git a/build2/test/script/parser+expansion.test.testscript b/build2/test/script/parser+expansion.test.testscript deleted file mode 100644 index 71a21b3..0000000 --- a/build2/test/script/parser+expansion.test.testscript +++ /dev/null @@ -1,36 +0,0 @@ -# file : build2/test/script/parser+expansion.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: quote -: -: Make sure everything expanded as strings. -: -$* <<EOI >>EOO -x = dir/ proj% proj%name proj%proj%dir/type{name name {name}} -cmd dir/ proj% proj%name proj%proj%dir/type{name name {name}} -cmd $x -EOI -cmd dir/ proj% proj%name proj%proj%dir/type{name name {name}} -cmd dir/ proj% proj%name proj%proj%dir/type{name name {name}} -EOO - -: unterm-quoted-seq -: -$* <<EOI 2>>EOE != 0 -x = "'a bc" -cmd xy$x -EOI -<string>:1:8: error: unterminated single-quoted sequence - testscript:2:5: info: while parsing string 'xy'a bc' -EOE - -: invalid-redirect -: -$* <<EOI 2>>EOE != 0 -x = "1>&a" -cmd $x -EOI -<string>:1:4: error: stdout merge redirect file descriptor must be 2 - testscript:2:5: info: while parsing string '1>&a' -EOE diff --git a/build2/test/script/parser+here-document.test.testscript b/build2/test/script/parser+here-document.test.testscript deleted file mode 100644 index 9f82a80..0000000 --- a/build2/test/script/parser+here-document.test.testscript +++ /dev/null @@ -1,213 +0,0 @@ -# file : build2/test/script/parser+here-document.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: end-marker -: -{ - : missing-newline - : - $* <'cmd <<' 2>>EOE != 0 - testscript:1:7: error: expected here-document end marker - EOE - - : missing-exit - : - $* <'cmd << != 0' 2>>EOE != 0 - testscript:1:8: error: expected here-document end marker - EOE - - : missing-empty - : - $* <'cmd <<""' 2>>EOE != 0 - testscript:1:7: error: expected here-document end marker - EOE - - : unseparated-expansion - : - $* <'cmd <<FOO$foo' 2>>EOE != 0 - testscript:1:10: error: here-document end marker must be literal - EOE - - : quoted-single-partial - : - $* <"cmd <<F'O'O" 2>>EOE != 0 - testscript:1:7: error: partially-quoted here-document end marker - EOE - - : quoted-double-partial - : - $* <'cmd <<"FO"O' 2>>EOE != 0 - testscript:1:7: error: partially-quoted here-document end marker - EOE - - : quoted-mixed - : - $* <"cmd <<\"FO\"'O'" 2>>EOE != 0 - testscript:1:7: error: partially-quoted here-document end marker - EOE - - : unseparated - : - $* <<EOI >>EOO - cmd <<EOF!=0 - foo - EOF - EOI - cmd <<EOF != 0 - foo - EOF - EOO - - : quoted-single - : - $* <<EOI >>EOO - cmd <<'EOF' - foo - EOF - EOI - cmd <<EOF - foo - EOF - EOO - - : quoted-double - : - $* <<EOI >>EOO - cmd <<"EOF" - foo - EOF - EOI - cmd <<EOF - foo - EOF - EOO -} - -: indent -: -{ - : basic - : - $* <<EOI >>EOO - cmd <<EOF - foo - bar - baz - EOF - EOI - cmd <<EOF - foo - bar - baz - EOF - EOO - - : blank - : - $* <<EOI >>EOO - cmd <<EOF - foo - - - bar - EOF - EOI - cmd <<EOF - foo - - - bar - EOF - EOO - - : non-ws-prefix - : - $* <<EOI >>EOO - cmd <<EOF - x EOF - EOF - EOI - cmd <<EOF - x EOF - EOF - EOO - - : whole-token - : Test the case where the indentation is a whole token - : - $* <<EOI >>EOO - x = foo bar - cmd <<"EOF" - $x - EOF - EOI - cmd <<EOF - foo bar - EOF - EOO - - : long-line - : Test the case where the line contains multiple tokens - : - $* <<EOI >>EOO - x = foo - cmd <<"EOF" - $x bar $x - EOF - EOI - cmd <<EOF - foo bar foo - EOF - EOO - - : unindented - : - $* <<EOI 2>>EOE != 0 - cmd <<EOF - bar - EOF - EOI - testscript:2:1: error: unindented here-document line - EOE -} - -: blank -: -$* <<EOI >>EOO -cmd <<EOF - -foo - -bar - -EOF -EOI -cmd <<EOF - -foo - -bar - -EOF -EOO - -: quote -: -: Note: they are still recognized in eval contexts. -: -$* <<EOI >>EOO -cmd <<"EOF" -'single' -"double" -b'o't"h" -('single' "double") -EOF -EOI -cmd <<EOF -'single' -"double" -b'o't"h" -single double -EOF -EOO diff --git a/build2/test/script/parser+here-string.test.testscript b/build2/test/script/parser+here-string.test.testscript deleted file mode 100644 index 16544df..0000000 --- a/build2/test/script/parser+here-string.test.testscript +++ /dev/null @@ -1,19 +0,0 @@ -# file : build2/test/script/parser+here-string.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: empty -: -$* <<EOI >>EOO -cmd <"" -EOI -cmd <'' -EOO - -: empty-nn -: -$* <<EOI >>EOO -cmd <:"" -EOI -cmd <:'' -EOO diff --git a/build2/test/script/parser+include.test.testscript b/build2/test/script/parser+include.test.testscript deleted file mode 100644 index 65be149..0000000 --- a/build2/test/script/parser+include.test.testscript +++ /dev/null @@ -1,104 +0,0 @@ -# file : build2/test/script/parser+include.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: none -: -$* <<EOI -.include -.include --once -EOI - -: empty -: -touch foo.testscript; -$* <<EOI -.include foo.testscript -.include --once foo.testscript -EOI - -: one -: -cat <"cmd" >=foo.testscript; -$* <<EOI >>EOO -.include foo.testscript -EOI -cmd -EOO - -: multiple -: -cat <"cmd foo" >=foo.testscript; -cat <"cmd bar" >=bar.testscript; -$* <<EOI >>EOO -.include foo.testscript bar.testscript -EOI -cmd foo -cmd bar -EOO - -: once -: -cat <"cmd" >=foo.testscript; -$* <<EOI >>EOO -.include foo.testscript -x -.include --once foo.testscript -.include --once bar/../foo.testscript -y -.include ../once/foo.testscript -EOI -cmd -x -y -cmd -EOO - -: group-id -: -cat <<EOI >=foo.testscript; -{ - x = b -} -EOI -$* -s -i <<EOI >>EOO -x = a -.include foo.testscript -EOI -{ - { # 2-foo-1 - } -} -EOO - -: test-id -: -cat <<EOI >=foo.testscript; -cmd -EOI -$* -s -i <<EOI >>EOO -x = a -.include foo.testscript -EOI -{ - { # 2-foo-1 - cmd - } -} -EOO - -: invalid-path -: -$* <<EOI 2>>EOE != 0 -.include "" -EOI -testscript:1:2: error: invalid testscript include path '' -EOE - -: unable-open -: -$* <<EOI 2>>~/EOE/ != 0 -.include foo.testscript -EOI -/testscript:1:2: error: unable to read testscript foo.testscript: .+/ -EOE diff --git a/build2/test/script/parser+pipe-expr.test.testscript b/build2/test/script/parser+pipe-expr.test.testscript deleted file mode 100644 index 18eb660..0000000 --- a/build2/test/script/parser+pipe-expr.test.testscript +++ /dev/null @@ -1,133 +0,0 @@ -# file : build2/test/script/parser+pipe-expr.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: pipe -: -$* <<EOI >>EOO -cmd1 | cmd2|cmd3 -EOI -cmd1 | cmd2 | cmd3 -EOO - -: log -: -$* <<EOI >>EOO -cmd1 || cmd2&&cmd3 -EOI -cmd1 || cmd2 && cmd3 -EOO - -: pipe-log -: -$* <<EOI >>EOO -cmd1 | cmd2 && cmd3 | cmd4 -EOI -cmd1 | cmd2 && cmd3 | cmd4 -EOO - -: exit -: -$* <<EOI >>EOO -cmd1|cmd2==1&&cmd3!=0|cmd4 -EOI -cmd1 | cmd2 == 1 && cmd3 != 0 | cmd4 -EOO - -: here-doc -: -$* <<EOI >>EOO -cmd1 <<EOI1 | cmd2 >>EOO2 && cmd3 <<EOI3 2>&1 | cmd4 2>>EOE4 >>EOO4 -input -one -EOI1 -ouput -two -EOO2 -input -three -EOI3 -error -four -EOE4 -output -four -EOO4 -EOI -cmd1 <<EOI1 | cmd2 >>EOO2 && cmd3 <<EOI3 2>&1 | cmd4 >>EOO4 2>>EOE4 -input -one -EOI1 -ouput -two -EOO2 -input -three -EOI3 -output -four -EOO4 -error -four -EOE4 -EOO - -: leading -: -$* <<EOI 2>>EOE != 0 -| cmd -EOI -testscript:1:1: error: missing program -EOE - -: trailing -: -$* <<EOI 2>>EOE != 0 -cmd && -EOI -testscript:1:7: error: missing program -EOE - -: redirected -: -{ - : input - : - { - : first - : - $* <<EOI >>EOO - cmd1 <foo | cmd2 - EOI - cmd1 <foo | cmd2 - EOO - - : non-first - : - $* <<EOI 2>>EOE != 0 - cmd1 | cmd2 <foo - EOI - testscript:1:13: error: stdin is both piped and redirected - EOE - } - - : output - : - { - : last - : - $* <<EOI >>EOO - cmd1 | cmd2 >foo - EOI - cmd1 | cmd2 >foo - EOO - - : non-last - : - $* <<EOI 2>>EOE != 0 - cmd1 >foo | cmd2 - EOI - testscript:1:11: error: stdout is both redirected and piped - EOE - } -} diff --git a/build2/test/script/parser+pre-parse.test.testscript b/build2/test/script/parser+pre-parse.test.testscript deleted file mode 100644 index 7d9eb6c..0000000 --- a/build2/test/script/parser+pre-parse.test.testscript +++ /dev/null @@ -1,23 +0,0 @@ -# file : build2/test/script/parser+pre-parse.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: attribute -: -{ - : pair - : - $* <<EOI 2>>EOE != 0 - x = [foo=bar] - EOI - testscript:1:5: error: unknown value attribute foo=bar - EOE - - : pair-empty - : - $* <<EOI 2>>EOE != 0 - x = [foo=] - EOI - testscript:1:5: error: unknown value attribute foo - EOE -} diff --git a/build2/test/script/parser+redirect.test.testscript b/build2/test/script/parser+redirect.test.testscript deleted file mode 100644 index b0b967a..0000000 --- a/build2/test/script/parser+redirect.test.testscript +++ /dev/null @@ -1,356 +0,0 @@ -# file : build2/test/script/parser+redirect.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -# @@ Add tests for redirects other than trace, here-*, file and merge. -# @@ Does it make sense to split into separate files - one per redirect type? -# - -: trace -: -{ - $* <'cmd >!' >'cmd >!' : out - $* <'cmd 2>!' >'cmd 2>!' : err -} - -: str -: -{ - : literal - : - { - : portable-path - : - $* <<EOI >>EOO - cmd </foo >/bar 2>/baz - EOI - cmd </foo >/bar 2>/baz - EOO - } - - : regex - : - { - : portable-path - : - $* <<EOI >>EOO - cmd >/~%foo% 2>/~%bar% - EOI - cmd >/~%foo% 2>/~%bar% - EOO - } -} - -: doc -: -{ - : literal - : - { - : portable-path - : - $* <<EOI >>EOO - cmd <</EOI_ >/EOO_ 2>/EOE_ - foo - EOI_ - bar - EOO_ - baz - EOE_ - EOI - cmd <</EOI_ >/EOO_ 2>/EOE_ - foo - EOI_ - bar - EOO_ - baz - EOE_ - EOO - - : sharing - : - { - : in-out - : - $* <<EOI >>EOO - cmd <<:/EOF >>:/EOF - foo - EOF - EOI - cmd <<:/EOF >>:/EOF - foo - EOF - EOO - - : different - : - { - : modifiers - : - $* <<EOI 2>>EOE != 0 - cmd <<:/EOF >>:EOF - foo - EOF - EOI - testscript:1:16: error: different modifiers for shared here-document 'EOF' - EOE - - : quoting - : - $* <<EOI 2>>EOE != 0 - cmd <<EOF >>"EOF" - foo - EOF - EOI - testscript:1:13: error: different quoting for shared here-document 'EOF' - EOE - } - } - } - - : regex - : - { - : portable-path - : - $* <<EOI >>EOO - cmd >/~%EOF% 2>/~%EOE% - foo - EOF - bar - EOE - EOI - cmd >/~%EOF% 2>/~%EOE% - foo - EOF - bar - EOE - EOO - - : sharing - : - { - : in-out - : - $* <<EOI >>EOO - cmd >>~/EOF/ 2>>~/EOF/ - foo - EOF - EOI - cmd >>~/EOF/ 2>>~/EOF/ - foo - EOF - EOO - - : different - : - { - : introducers - : - $* <<EOI 2>>EOE != 0 - cmd >>~/EOF/ 2>>~%EOF% - foo - EOF - EOI - testscript:1:18: error: different introducers for shared here-document regex 'EOF' - EOE - - : flags - : - $* <<EOI 2>>EOE != 0 - cmd >>~/EOF/ 2>>~/EOF/i - foo - EOF - EOI - testscript:1:18: error: different global flags for shared here-document regex 'EOF' - EOE - } - } - } -} - -: file -: -{ - : cmp - : - $* <<EOI >>EOO - cmd 0<<<a 1>>>b 2>>>c - EOI - cmd <<<a >>>b 2>>>c - EOO - - : write - : - $* <<EOI >>EOO - cmd 1>=b 2>+c - EOI - cmd >=b 2>+c - EOO - - : quote - : - $* <<EOI >>EOO - cmd 0<<<"a f" 1>="b f" 2>+"c f" - EOI - cmd <<<'a f' >='b f' 2>+'c f' - EOO - - : in - : - { - : missed - : - $* <<EOI 2>>EOE !=0 - cmd <<< - EOI - testscript:1:8: error: missing stdin file - EOE - - : empty - : - $* <<EOI 2>>EOE !=0 - cmd <<<"" - EOI - testscript:1:8: error: empty stdin redirect path - EOE - } - - : out - : - { - : missed - : - $* <<EOI 2>>EOE !=0 - cmd >= - EOI - testscript:1:7: error: missing stdout file - EOE - - : empty - : - $* <<EOI 2>>EOE !=0 - cmd >="" - EOI - testscript:1:7: error: empty stdout redirect path - EOE - } - - : err - : - { - : missed - : - $* <<EOI 2>>EOE !=0 - cmd 2>= - EOI - testscript:1:8: error: missing stderr file - EOE - - : empty - : - $* <<EOI 2>>EOE !=0 - cmd 2>="" - EOI - testscript:1:8: error: empty stderr redirect path - EOE - } -} - -: merge -{ - : out - : - { - : err - : - $* <<EOI >>EOO - cmd 1>&2 - EOI - cmd >&2 - EOO - - : no-mutual - : - $* <<EOI >>EOO - cmd 1>&2 2>&1 2>a - EOI - cmd >&2 2>a - EOO - - : not-descriptor - : - $* <<EOI 2>>EOE != 0 - cmd 1>&a - EOI - testscript:1:8: error: stdout merge redirect file descriptor must be 2 - EOE - - : self - : - $* <<EOI 2>>EOE != 0 - cmd 1>&1 - EOI - testscript:1:8: error: stdout merge redirect file descriptor must be 2 - EOE - - : missed - : - $* <<EOI 2>>EOE != 0 - cmd 1>& - EOI - testscript:1:8: error: missing stdout file descriptor - EOE - } - - : err - { - : out - : - $* <<EOI >>EOO - cmd 2>&1 - EOI - cmd 2>&1 - EOO - - : no-mutual - : - $* <<EOI >>EOO - cmd 1>&2 2>&1 >a - EOI - cmd >a 2>&1 - EOO - - : not-descriptor - : - $* <<EOI 2>>EOE != 0 - cmd 2>&a - EOI - testscript:1:8: error: stderr merge redirect file descriptor must be 1 - EOE - - : self - : - $* <<EOI 2>>EOE != 0 - cmd 2>&2 - EOI - testscript:1:8: error: stderr merge redirect file descriptor must be 1 - EOE - - : missed - : - $* <<EOI 2>>EOE != 0 - cmd 2>& - EOI - testscript:1:8: error: missing stderr file descriptor - EOE - } - - : mutual - : - $* <<EOI 2>>EOE != 0 - cmd 1>&2 2>&1 - EOI - testscript:1:14: error: stdout and stderr redirected to each other - EOE -} diff --git a/build2/test/script/parser+regex.test.testscript b/build2/test/script/parser+regex.test.testscript deleted file mode 100644 index 031492e..0000000 --- a/build2/test/script/parser+regex.test.testscript +++ /dev/null @@ -1,223 +0,0 @@ -# file : build2/test/script/parser+regex.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: here-string -: -{ - : stdout - : - { - : missed - : - $* <'cmd >~' 2>>EOE != 0 - testscript:1:7: error: missing stdout here-string regex - EOE - - : no-introducer - : - $* <'cmd >~""' 2>>EOE != 0 - testscript:1:7: error: no introducer character in stdout regex redirect - EOE - - : no-term-introducer - : - $* <'cmd >~/' 2>>EOE != 0 - testscript:1:7: error: no closing introducer character in stdout regex redirect - EOE - - : portable-path-introducer - : - $* <'cmd >/~/foo/' 2>>EOE != 0 - testscript:1:8: error: portable path modifier and '/' introducer in stdout regex redirect - EOE - - : empty - : - $* <'cmd >~//' 2>>EOE != 0 - testscript:1:7: error: stdout regex redirect is empty - EOE - - : no-flags - : - $* <'cmd >~/fo*/' >'cmd >~/fo*/' - - : idot - : - $* <'cmd >~/fo*/d' >'cmd >~/fo*/d' - - : icase - : - $* <'cmd >~/fo*/i' >'cmd >~/fo*/i' - - : invalid-flags1 - : - $* <'cmd >~/foo/z' 2>>EOE != 0 - testscript:1:7: error: junk at the end of stdout regex redirect - EOE - - : invalid-flags2 - : - $* <'cmd >~/foo/iz' 2>>EOE != 0 - testscript:1:7: error: junk at the end of stdout regex redirect - EOE - - : no-newline - : - $* <'cmd >:~/fo*/' >'cmd >:~/fo*/' - } - - : stderr - : - { - : missed - : - $* <'cmd 2>~' 2>>EOE != 0 - testscript:1:8: error: missing stderr here-string regex - EOE - - : no-introducer - : - : Note that there is no need to reproduce all the errors as for stdout. - : All we need is to make sure that the proper description is passed to - : the parse_regex() function. - : - $* <'cmd 2>~""' 2>>EOE != 0 - testscript:1:8: error: no introducer character in stderr regex redirect - EOE - } - - : modifier-last - : - $* <'cmd >~/x' 2>>EOE != 0 - testscript:1:7: error: no closing introducer character in stdout regex redirect - EOE -} - -: here-doc -: -{ - : stdout - : - { - : missed - : - $* <'cmd >>~' 2>>EOE != 0 - testscript:1:8: error: expected here-document regex end marker - EOE - - : portable-path-introducer - : - $* <<EOI 2>>EOE != 0 - cmd >>/~/EOO/ - foo - EOO - EOI - testscript:1:5: error: portable path modifier and '/' introducer in here-document regex end marker - EOE - - : unterminated-line-char - : - $* <<EOI 2>>EOE != 0 - cmd >>~/EOO/ - / - EOO - EOI - testscript:2:1: error: no syntax line characters - EOE - - : empty - : - $* <<EOI 2>>EOE != 0 - cmd >>:~/EOO/ - EOO - EOI - testscript:2:1: error: empty here-document regex - EOE - - : no-flags - : - $* <<EOI >>EOO - cmd 2>>~/EOE/ - foo - /? - /foo/ - /foo/* - /foo/i - /foo/i* - - // - //* - EOE - EOI - cmd 2>>~/EOE/ - foo - /? - /foo/ - /foo/* - /foo/i - /foo/i* - - // - //* - EOE - EOO - - : no-newline - : - $* <'cmd >:~/fo*/' >'cmd >:~/fo*/' - $* <<EOI >>EOO - cmd 2>>:~/EOE/ - foo - EOE - EOI - cmd 2>>:~/EOE/ - foo - EOE - EOO - - : end-marker-restore - : - { - : idot - : - $* <<EOI >>EOO - cmd 2>>~/EOE/d - foo - EOE - EOI - cmd 2>>~/EOE/d - foo - EOE - EOO - - : icase - : - $* <<EOI >>EOO - cmd 2>>~/EOE/i - foo - EOE - EOI - cmd 2>>~/EOE/i - foo - EOE - EOO - } - } - - : stderr - : - { - : missed - : - $* <'cmd 2>>~' 2>>EOE != 0 - testscript:1:9: error: expected here-document regex end marker - EOE - } - - : modifier-last - : - $* <'cmd >>~:/FOO/' 2>>EOE != 0 - testscript:1:8: error: expected here-document regex end marker - EOE -} diff --git a/build2/test/script/parser+scope-if.test.testscript b/build2/test/script/parser+scope-if.test.testscript deleted file mode 100644 index faae297..0000000 --- a/build2/test/script/parser+scope-if.test.testscript +++ /dev/null @@ -1,554 +0,0 @@ -# file : build2/test/script/parser+scope-if.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: if -: -{ - : true - : - $* -s <<EOI >>EOO - if true foo - { - cmd - } - EOI - { - ? true foo - { - cmd - } - } - EOO - - : false - : - $* -s <<EOI >>EOO - if false foo - { - cmd - } - EOI - { - ? false foo - } - EOO - - : not-true - : - $* -s <<EOI >>EOO - if! true - { - cmd - } - EOI - { - ? true - } - EOO - - : not-false - : - $* -s <<EOI >>EOO - if! false - { - cmd - } - EOI - { - ? false - { - cmd - } - } - EOO - - : eos-inside - : - $* <<EOI 2>>EOE != 0 - if - { - EOI - testscript:3:1: error: expected '}' at the end of the scope - EOE - -} - -: elif -: -{ - : true - : - $* -s <<EOI >>EOO - if false - { - cmd - } - elif true - { - cmd1 - } - EOI - { - ? false - ? true - { - cmd1 - } - } - EOO - - : false - : - $* -s <<EOI >>EOO - if false - { - cmd - } - elif false - { - cmd - } - EOI - { - ? false - ? false - } - EOO - - : not-false - : - $* -s <<EOI >>EOO - if false - { - cmd - } - elif! false - { - cmd1 - } - EOI - { - ? false - ? false - { - cmd1 - } - } - EOO - - : not-true - : - $* -s <<EOI >>EOO - if false - { - cmd - } - elif! true - { - cmd - } - EOI - { - ? false - ? true - } - EOO - - : after-else - : - $* <<EOI 2>>EOE != 0 - if false - { - cmd - } - else - { - cmd - } - elif true - { - cmd - } - EOI - testscript:9:1: error: 'elif' after 'else' - EOE -} - -: else -: -{ - : true - : - $* -s <<EOI >>EOO - if false - { - cmd - } - else - { - cmd1 - } - EOI - { - ? false - { - cmd1 - } - } - EOO - - : false - : - $* -s <<EOI >>EOO - if true - { - cmd1 - } - else - { - cmd - } - EOI - { - ? true - { - cmd1 - } - } - EOO - - : chain - : - $* -s <<EOI >>EOO - if false - { - cmd - } - elif false - { - cmd - cmd - } - elif false - { - cmd - } - elif true - { - cmd1 - cmd2 - } - elif false - { - cmd - } - else - { - cmd - cmd - } - EOI - { - ? false - ? false - ? false - ? true - { - { - cmd1 - } - { - cmd2 - } - } - } - EOO - - : scope-expected - : - $* <<EOI 2>>EOE != 0 - if - { - cmd - } - else - cmd - EOI - testscript:5:1: error: expected scope after 'else' - EOE - - : after-else - : - $* <<EOI 2>>EOE != 0 - if false - { - cmd - } - else - { - cmd - } - else - { - cmd - } - EOI - testscript:9:1: error: 'else' after 'else' - EOE -} - -: nested -: -{ - : take - : - $* -s <<EOI >>EOO - if true - { - cmd1 - if false - { - cmd - } - elif false - { - if true - { - cmd - } - } - else - { - cmd2 - } - cmd3 - } - EOI - { - ? true - { - { - cmd1 - } - ? false - ? false - { - { - cmd2 - } - } - { - cmd3 - } - } - } - EOO - - : skip - : - $* -s <<EOI >>EOO - if false - { - cmd1 - if false - { - cmd - } - elif false - { - if true - { - cmd - } - } - else - { - cmd2 - } - cmd3 - } - else - { - cmd - } - EOI - { - ? false - { - { - cmd - } - } - } - EOO -} - -: demote -: -{ - : group - : Chain remains a group - : - $* -s <<EOI >>EOO - if false - { - cmd - } - elif true - { - cmd1 - cmd2 - } - else - { - cmd - } - EOI - { - ? false - ? true - { - { - cmd1 - } - { - cmd2 - } - } - } - EOO - - : test - : Chain demoted to test - : - $* -s <<EOI >>EOO - if false - { - cmd - } - elif true - { - cmd1 - } - else - { - cmd - } - EOI - { - ? false - ? true - { - cmd1 - } - } - EOO -} - -: line-index -: Make sure command line index spans setup/if/teardown -: -$* -s -l <<EOI >>EOO -+setup # 1 - -if false one # 2 -{ - cmd -} -elif false two # 3 -{ - cmd -} -elif true # 4 -{ - cmd1 -} -elif false # 5 -{ - cmd -} -else -{ - cmd -} - -if false one # 6 -{ - cmd -} -elif false two # 7 -{ - cmd -} -else -{ - cmd2 -} - --tdown # 8 -EOI -{ - +setup # 1 - ? false one # 2 - ? false two # 3 - ? true # 4 - { - cmd1 # 0 - } - ? false one # 6 - ? false two # 7 - { - cmd2 # 0 - } - -tdown # 8 -} -EOO - -: scope-comman-if -: -$* -s <<EOI >>EOO -if true -{ - cmd -} -if true - cmd1 - cmd2 -end -EOI -{ - ? true - { - cmd - } - { - ? true - cmd1 - cmd2 - } -} -EOO - -: shared-id-desc -: -$* -s -i <<EOI >>EOO -: test summary -: -if false -{ - cmd -} -else -{ - cmd1 -} -EOI -{ - ? false - : sm:test summary - { # 3 - cmd1 - } -} -EOO diff --git a/build2/test/script/parser+scope.test.testscript b/build2/test/script/parser+scope.test.testscript deleted file mode 100644 index 9147161..0000000 --- a/build2/test/script/parser+scope.test.testscript +++ /dev/null @@ -1,280 +0,0 @@ -# file : build2/test/script/parser+scope.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -$* testscript <'cmd $@' >"cmd 1" : id-testscript -$* foo.testscript <'cmd $@' >"cmd foo/1" : id - -: wd-testscript -: -$* testscript <'cmd "$~"' >~"%cmd '?.+[/\\\\]test-driver[/\\\\]1'?%" - -: wd -: -$* foo.testscript <'cmd "$~"' >~"%cmd '?.+[/\\\\]test-driver[/\\\\]foo[/\\\\]1'?%" - -: group -: -{ - : empty - : - $* -s <<EOI - { - } - EOI - - : empty-empty - : - $* -s <<EOI - { - { - } - } - EOI - - : non-empty - : - $* -s <<EOI >>EOO - { - cmd1 - cmd2 - } - EOI - { - { - { - cmd1 - } - { - cmd2 - } - } - } - EOO -} - -: test -: -{ - : explicit - : - { - : one-level - : - $* -s -i <<EOI >>EOO - { - cmd - } - EOI - { - { # 1 - cmd - } - } - EOO - - : nested - : - $* -s -i <<EOI >>EOO - { - { - cmd - } - } - EOI - { - { # 1 - cmd - } - } - EOO - - : var - : - $* -s -i <<EOI >>EOO - { - x = abc - cmd $x - } - EOI - { - { # 1 - cmd abc - } - } - EOO - - : setup - : - $* -s -i <<EOI >>EOO - { - x = abc - +setup - cmd $x - } - EOI - { - { # 1 - +setup - { # 1/4 - cmd abc - } - } - } - EOO - } - - : implicit - { - : one-cmd - : - $* -s <<EOI >>EOO - cmd1 - EOI - { - { - cmd1 - } - } - EOO - - : two-cmd - : - $* -s <<EOI >>EOO - cmd1; - cmd2 - EOI - { - { - cmd1 - cmd2 - } - } - EOO - - : three-cmd - : - $* -s <<EOI >>EOO - cmd1; - cmd2; - cmd3 - EOI - { - { - cmd1 - cmd2 - cmd3 - } - } - EOO - - : var - : - $* -s <<EOI >>EOO - cmd1; - x = abc; - cmd2 $x - EOI - { - { - cmd1 - cmd2 abc - } - } - EOO - - : var-first - : - $* -s <<EOI >>EOO - x = abc; - cmd $x - EOI - { - { - cmd abc - } - } - EOO - - : var-setup-tdown - : - $* -s <<EOI >>EOO - x = abc - cmd $x - y = 123 - EOI - { - { - cmd abc - } - } - EOO - - : after-tdown - : - $* <<EOI 2>>EOE != 0 - cmd1 - x = abc - cmd2 - EOI - testscript:3:1: error: test after teardown - testscript:2:1: info: last teardown line appears here - EOE - } -} - -: expected -{ - : newline-lcbrace - : - $* <:"{x" 2>>EOE != 0 - testscript:1:2: error: expected newline after '{' - EOE - - : rcbrace - : - $* <"{" 2>>EOE != 0 - testscript:2:1: error: expected '}' at the end of the scope - EOE - - : line-rcbrace - : - $* <<EOI 2>>EOE != 0 - { - cmd; - } - EOI - testscript:3:1: error: expected another line after ';' - EOE - - : newline-rcbrace - : - $* <<:EOI 2>>EOE != 0 - { - } - EOI - testscript:2:2: error: expected newline after '}' - EOE - - : line-eof - : - $* <<EOI 2>>EOE != 0 - cmd; - EOI - testscript:2:1: error: expected another line after ';' - EOE - - : newline-cmd - : - $* <<:EOI 2>>EOE != 0 - cmd; - EOI - testscript:1:5: error: expected newline instead of <end of file> - EOE - - : newline-var - : - $* <:"x = abc;" 2>>EOE != 0 - testscript:1:9: error: expected newline instead of <end of file> - EOE -} diff --git a/build2/test/script/parser+setup-teardown.test.testscript b/build2/test/script/parser+setup-teardown.test.testscript deleted file mode 100644 index 9d67309..0000000 --- a/build2/test/script/parser+setup-teardown.test.testscript +++ /dev/null @@ -1,151 +0,0 @@ -# file : build2/test/script/parser+setup-teardown.test.testscript -# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -# license : MIT; see accompanying LICENSE file - -: setup -: -{ - : followed - : - { - : semi - : - $* <"+cmd;" 2>>EOE != 0 - testscript:1:5: error: ';' after setup command - EOE - - : colon - : - $* <"+cmd:" 2>>EOE != 0 - testscript:1:5: error: ':' after setup command - EOE - } - - : after - : - { - : test - : - $* <<EOI 2>>EOE != 0 - cmd - +cmd - EOI - testscript:2:1: error: setup command after tests - EOE - - : after-tdownt - : - $* <<EOI 2>>EOE != 0 - -cmd - +cmd - EOI - testscript:2:1: error: setup command after teardown - EOE - } - - : in-test - : - $* <<EOI 2>>EOE != 0 - cmd; - +cmd - EOI - testscript:2:1: error: setup command in test - EOE -} - -: tdown -: -{ - : followed - : - { - : semi - : - $* <"-cmd;" 2>>EOE != 0 - testscript:1:5: error: ';' after teardown command - EOE - - : colon - : - $* <"-cmd:" 2>>EOE != 0 - testscript:1:5: error: ':' after teardown command - EOE - } - - : in-test - : - $* <<EOI 2>>EOE != 0 - cmd; - -cmd - EOI - testscript:2:1: error: teardown command in test - EOE -} - -: var -: -{ - : between-tests - : - $* <<EOI 2>>EOE != 0 - cmd - x = y - cmd - EOI - testscript:3:1: error: test after teardown - testscript:2:1: info: last teardown line appears here - EOE - - : between-tests-scope - : - $* <<EOI 2>>EOE != 0 - cmd - x = y - { - cmd - } - EOI - testscript:3:1: error: scope after teardown - testscript:2:1: info: last teardown line appears here - EOE - - : between-tests-command-if - : - $* <<EOI 2>>EOE != 0 - cmd - x = y - if true - cmd - end - EOI - testscript:3:1: error: test after teardown - testscript:2:1: info: last teardown line appears here - EOE - - : between-tests-scope-if - : - $* <<EOI 2>>EOE != 0 - cmd - x = y - if true - { - cmd - } - EOI - testscript:3:1: error: scope after teardown - testscript:2:1: info: last teardown line appears here - EOE - - : between-tests-variable-if - : - $* <<EOI >>EOO - cmd - x = y - if true - y = x - end - EOI - cmd - ? true - EOO -} diff --git a/build2/test/script/parser.cxx b/build2/test/script/parser.cxx deleted file mode 100644 index 59b950f..0000000 --- a/build2/test/script/parser.cxx +++ /dev/null @@ -1,3451 +0,0 @@ -// file : build2/test/script/parser.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/test/script/parser.hxx> - -#include <sstream> - -#include <libbuild2/context.hxx> // sched, keep_going - -#include <build2/test/script/lexer.hxx> -#include <build2/test/script/runner.hxx> - -using namespace std; - -namespace build2 -{ - namespace test - { - namespace script - { - using type = token_type; - - // Return true if the string contains only a single digit characters - // (used to detect the special $N variables). - // - static inline bool - digit (const string& s) - { - return s.size () == 1 && butl::digit (s[0]); - } - - // - // Pre-parse. - // - - void parser:: - pre_parse (script& s) - { - const path& p (s.script_target.path ()); - assert (!p.empty ()); // Should have been assigned. - - try - { - ifdstream ifs (p); - pre_parse (ifs, s); - } - catch (const io_error& e) - { - fail << "unable to read testscript " << p << ": " << e << endf; - } - } - - void parser:: - pre_parse (istream& is, script& s) - { - path_ = &*s.paths_.insert (s.script_target.path ()).first; - - pre_parse_ = true; - - lexer l (is, *path_, lexer_mode::command_line); - set_lexer (&l); - - id_prefix_.clear (); - - id_map idm; - include_set ins; - - script_ = &s; - runner_ = nullptr; - group_ = script_; - id_map_ = &idm; - include_set_ = &ins; - scope_ = nullptr; - - //@@ PAT TODO: set pbase_? - - // Start location of the implied script group is the beginning of - // the file. End location -- end of the file. - // - group_->start_loc_ = location (path_, 1, 1); - - token t (pre_parse_scope_body ()); - - if (t.type != type::eos) - fail (t) << "stray " << t; - - group_->end_loc_ = get_location (t); - } - - bool parser:: - pre_parse_demote_group_scope (unique_ptr<scope>& s) - { - // See if this turned out to be an explicit test scope. An explicit - // test scope contains a single test, only variable assignments in - // setup and nothing in teardown. Plus only the group can have the - // description. Because we apply this recursively, also disqualify - // a test scope that has an if-condition. - // - // If we have a chain, then all the scopes must be demotable. So we - // first check if this scope is demotable and if so then recurse for - // the next in chain. - // - group& g (static_cast<group&> (*s)); - - auto& sc (g.scopes); - auto& su (g.setup_); - auto& td (g.tdown_); - - test* t; - if (sc.size () == 1 && - (t = dynamic_cast<test*> (sc.back ().get ())) != nullptr && - find_if ( - su.begin (), su.end (), - [] (const line& l) { - return l.type != line_type::var; - }) == su.end () && - - td.empty () && - !t->desc && - !t->if_cond_) - { - if (g.if_chain != nullptr && - !pre_parse_demote_group_scope (g.if_chain)) - return false; - - // It would have been nice to reuse the test object and only throw - // away the group. However, the merged scope has to use id_path and - // wd_path of the group. So to keep things simple we are going to - // throw away both and create a new test object. - // - // We always use the group's id since the test cannot have a - // user-provided one. - // - unique_ptr<test> m (new test (g.id_path.leaf ().string (), *group_)); - - // Move the description, if-condition, and if-chain. - // - m->desc = move (g.desc); - m->if_cond_ = move (g.if_cond_); - m->if_chain = move (g.if_chain); - - // Merge the lines of the group and the test. - // - if (su.empty ()) - m->tests_ = move (t->tests_); - else - { - m->tests_ = move (su); // Should come first. - m->tests_.insert (m->tests_.end (), - make_move_iterator (t->tests_.begin ()), - make_move_iterator (t->tests_.end ())); - } - - // Use start/end locations of the outer scope. - // - m->start_loc_ = g.start_loc_; - m->end_loc_ = g.end_loc_; - - s = move (m); - return true; - } - - return false; - } - - token parser:: - pre_parse_scope_body () - { - // enter: next token is first token of scope body - // leave: rcbrace or eos (returned) - - token t; - type tt; - - // Parse lines (including nested scopes) until we see '}' or eos. - // - for (;;) - { - // Start lexing each line recognizing leading '.+-{}'. - // - tt = peek (lexer_mode::first_token); - - // Handle description. - // - optional<description> d; - if (tt == type::colon) - d = pre_parse_leading_description (t, tt); - - // Determine the line type by peeking at the first token. - // - switch (tt) - { - case type::eos: - case type::rcbrace: - { - next (t, tt); - - if (d) - fail (t) << "description before " << t; - - return t; - } - case type::lcbrace: - { - // Nested scope. - // - next (t, tt); // Get '{'. - const location sl (get_location (t)); - - // First check that we don't have any teardown commands yet. - // This will detect things like variable assignments between - // scopes. - // - if (!group_->tdown_.empty ()) - { - location tl ( - group_->tdown_.back ().tokens.front ().location ()); - - fail (sl) << "scope after teardown" << - info (tl) << "last teardown line appears here"; - } - - // If there is no user-supplied id, use the line number - // (prefixed with include id) as the scope id. - // - const string& id ( - d && !d->id.empty () - ? d->id - : insert_id (id_prefix_ + to_string (sl.line), sl)); - - unique_ptr<scope> g (pre_parse_scope_block (t, tt, id)); - g->desc = move (d); - - pre_parse_demote_group_scope (g); - group_->scopes.push_back (move (g)); - continue; - } - default: - { - pre_parse_line (t, tt, d); - assert (tt == type::newline); - break; - } - } - } - } - - unique_ptr<group> parser:: - pre_parse_scope_block (token& t, type& tt, const string& id) - { - // enter: lcbrace - // leave: newline after rcbrace - - const location sl (get_location (t)); - - if (next (t, tt) != type::newline) - fail (t) << "expected newline after '{'"; - - // Push group. - // - id_map idm; - include_set ins; - - unique_ptr<group> g (new group (id, *group_)); - - id_map* om (id_map_); - id_map_ = &idm; - - include_set* os (include_set_); - include_set_ = &ins; - - group* og (group_); - group_ = g.get (); - - // Parse body. - // - group_->start_loc_ = sl; - token e (pre_parse_scope_body ()); - group_->end_loc_ = get_location (e); - - // Pop group. - // - group_ = og; - include_set_ = os; - id_map_ = om; - - if (e.type != type::rcbrace) - fail (e) << "expected '}' at the end of the scope"; - - if (next (t, tt) != type::newline) - fail (t) << "expected newline after '}'"; - - return g; - } - - // Parse a logical line (as well as scope-if since the only way to - // recognize it is to parse the if line). - // - // If one is true then only parse one line returning an indication of - // whether the line ended with a semicolon. - // - bool parser:: - pre_parse_line (token& t, type& tt, - optional<description>& d, - lines* ls, - bool one) - { - // enter: next token is peeked at (type in tt) - // leave: newline - - // Note: token is only peeked at. - // - const location ll (get_location (peeked ())); - - // Determine the line type/start token. - // - line_type lt; - type st (type::eos); - - switch (tt) - { - case type::dot: - { - // Directive. - // - next (t, tt); // Skip dot. - next (t, tt); // Get the directive name. - - if (tt != type::word || t.qtype != quote_type::unquoted) - fail (t) << "expected directive name instead of " << t; - - // Make sure we are not inside a test (i.e., after semi). - // - if (ls != nullptr) - fail (ll) << "directive after ';'"; - - const string& n (t.value); - - if (n == "include") - pre_parse_directive (t, tt); - else - fail (t) << "unknown directive '" << n << "'"; - - assert (tt == type::newline); - return false; - } - case type::plus: - case type::minus: - { - // Setup/teardown command. - // - st = tt; - - next (t, tt); // Start saving tokens from the next one. - replay_save (); - next (t, tt); - - // See if this is a special command. - // - lt = line_type::cmd; // Default. - - if (tt == type::word && t.qtype == quote_type::unquoted) - { - const string& n (t.value); - - if (n == "if") lt = line_type::cmd_if; - else if (n == "if!") lt = line_type::cmd_ifn; - } - - break; - } - default: - { - // Either variable assignment or test command. - // - replay_save (); // Start saving tokens from the current one. - next (t, tt); - - // Decide whether this is a variable assignment or a command. - // - // It is an assignment if the first token is an unquoted name and - // the next token is an assign/append/prepend operator. Assignment - // to a computed variable name must use the set builtin. - // - // Note also thatspecial commands take precedence over variable - // assignments. - // - lt = line_type::cmd; // Default. - - if (tt == type::word && t.qtype == quote_type::unquoted) - { - const string& n (t.value); - - if (n == "if") lt = line_type::cmd_if; - else if (n == "if!") lt = line_type::cmd_ifn; - else if (n == "elif") lt = line_type::cmd_elif; - else if (n == "elif!") lt = line_type::cmd_elifn; - else if (n == "else") lt = line_type::cmd_else; - else if (n == "end") lt = line_type::cmd_end; - else - { - // Switch the recognition of leading variable assignments for - // the next token. This is safe to do because we know we - // cannot be in the quoted mode (since the current token is - // not quoted). - // - type p (peek (lexer_mode::second_token)); - - if (p == type::assign || - p == type::prepend || - p == type::append) - { - lt = line_type::var; - st = p; - } - } - } - - break; - } - } - - // Pre-parse the line keeping track of whether it ends with a semi. - // - bool semi (false); - - line ln; - switch (lt) - { - case line_type::var: - { - // Check if we are trying to modify any of the special aliases - // ($*, $N, $~, $@). - // - string& n (t.value); - - if (n == "*" || n == "~" || n == "@" || digit (n)) - fail (t) << "attempt to set '" << n << "' variable directly"; - - // Pre-enter the variables now while we are executing serially. - // Once parallel, it becomes a lot harder to do. - // - ln.var = &script_->var_pool.insert (move (n)); - - next (t, tt); // Assignment kind. - parse_variable_line (t, tt); - - semi = (tt == type::semi); - - if (tt == type::semi) - next (t, tt); - - if (tt != type::newline) - fail (t) << "expected newline instead of " << t; - - break; - } - case line_type::cmd_if: - case line_type::cmd_ifn: - case line_type::cmd_elif: - case line_type::cmd_elifn: - case line_type::cmd_else: - case line_type::cmd_end: - next (t, tt); // Skip to start of command. - // Fall through. - case line_type::cmd: - { - pair<command_expr, here_docs> p; - - if (lt != line_type::cmd_else && lt != line_type::cmd_end) - p = parse_command_expr (t, tt); - - // Colon and semicolon are only valid in test command lines and - // after 'end' in if-else. Note that we still recognize them - // lexically, they are just not valid tokens per the grammar. - // - if (tt != type::newline) - { - if (lt != line_type::cmd && lt != line_type::cmd_end) - fail (t) << "expected newline instead of " << t; - - switch (st) - { - case type::plus: fail (t) << t << " after setup command" << endf; - case type::minus: fail (t) << t << " after teardown command" << endf; - } - } - - switch (tt) - { - case type::colon: - { - if (d) - fail (ll) << "both leading and trailing descriptions"; - - d = parse_trailing_description (t, tt); - break; - } - case type::semi: - { - semi = true; - next (t, tt); // Get newline. - break; - } - } - - if (tt != type::newline) - fail (t) << "expected newline instead of " << t; - - parse_here_documents (t, tt, p); - break; - } - } - - assert (tt == type::newline); - - // Stop saving and get the tokens. - // - lines ls_data; - - if (ls == nullptr) - ls = &ls_data; - - ln.type = lt; - ln.tokens = replay_data (); - ls->push_back (move (ln)); - - if (lt == line_type::cmd_if || lt == line_type::cmd_ifn) - { - semi = pre_parse_if_else (t, tt, d, *ls); - - // If this turned out to be scope-if, then ls is empty, semi is - // false, and none of the below logic applies. - // - if (ls->empty ()) - return semi; - } - - // Unless we were told where to put it, decide where it actually goes. - // - if (ls == &ls_data) - { - // First pre-check variable and variable-if: by themselves (i.e., - // without a trailing semicolon) they are treated as either setup or - // teardown without plus/minus. Also handle illegal line types. - // - switch (lt) - { - case line_type::cmd_elif: - case line_type::cmd_elifn: - case line_type::cmd_else: - case line_type::cmd_end: - { - fail (ll) << lt << " without preceding 'if'" << endf; - } - case line_type::cmd_if: - case line_type::cmd_ifn: - { - // See if this is a variable-only command-if. - // - if (find_if (ls_data.begin (), ls_data.end (), - [] (const line& l) { - return l.type == line_type::cmd; - }) != ls_data.end ()) - break; - } - // Fall through. - case line_type::var: - { - // If there is a semicolon after the variable then we assume - // it is part of a test (there is no reason to use semicolons - // after variables in the group scope). Otherwise -- setup or - // teardown. - // - if (!semi) - { - if (d) - { - if (lt == line_type::var) - fail (ll) << "description before setup/teardown variable"; - else - fail (ll) << "description before/after setup/teardown " - << "variable-if"; - } - - // If we don't have any nested scopes or teardown commands, - // then we assume this is a setup, otherwise -- teardown. - // - ls = group_->scopes.empty () && group_->tdown_.empty () - ? &group_->setup_ - : &group_->tdown_; - } - break; - } - default: - break; - } - - // If pre-check didn't change the destination, then it's a test. - // - if (ls == &ls_data) - { - switch (st) - { - // Setup. - // - case type::plus: - { - if (d) - fail (ll) << "description before setup command"; - - if (!group_->scopes.empty ()) - fail (ll) << "setup command after tests"; - - if (!group_->tdown_.empty ()) - fail (ll) << "setup command after teardown"; - - ls = &group_->setup_; - break; - } - - // Teardown. - // - case type::minus: - { - if (d) - fail (ll) << "description before teardown command"; - - ls = &group_->tdown_; - break; - } - - // Test command or variable. - // - default: - { - // First check that we don't have any teardown commands yet. - // This will detect things like variable assignments between - // tests. - // - if (!group_->tdown_.empty ()) - { - location tl ( - group_->tdown_.back ().tokens.front ().location ()); - - fail (ll) << "test after teardown" << - info (tl) << "last teardown line appears here"; - } - break; - } - } - } - - // If the destination changed, then move the data over. - // - if (ls != &ls_data) - ls->insert (ls->end (), - make_move_iterator (ls_data.begin ()), - make_move_iterator (ls_data.end ())); - } - - // If this command ended with a semicolon, then the next one should - // go to the same place. - // - if (semi && !one) - { - tt = peek (lexer_mode::first_token); - const location ll (get_location (peeked ())); - - switch (tt) - { - case type::colon: - fail (ll) << "description inside test" << endf; - case type::eos: - case type::rcbrace: - case type::lcbrace: - fail (ll) << "expected another line after ';'" << endf; - case type::plus: - fail (ll) << "setup command in test" << endf; - case type::minus: - fail (ll) << "teardown command in test" << endf; - default: - semi = pre_parse_line (t, tt, d, ls); - assert (tt == type::newline); // End of last test line. - } - } - - // If this is a test then create implicit test scope. - // - if (ls == &ls_data) - { - // If there is no user-supplied id, use the line number (prefixed - // with include id) as the scope id. - // - const string& id ( - d && !d->id.empty () - ? d->id - : insert_id (id_prefix_ + to_string (ll.line), ll)); - - unique_ptr<test> p (new test (id, *group_)); - - p->desc = move (d); - - p->start_loc_ = ll; - p->tests_ = move (ls_data); - p->end_loc_ = get_location (t); - - group_->scopes.push_back (move (p)); - } - - return semi; - } - - bool parser:: - pre_parse_if_else (token& t, type& tt, - optional<description>& d, - lines& ls) - { - // enter: <newline> (previous line) - // leave: <newline> - - tt = peek (lexer_mode::first_token); - - return tt == type::lcbrace - ? pre_parse_if_else_scope (t, tt, d, ls) - : pre_parse_if_else_command (t, tt, d, ls); - } - - bool parser:: - pre_parse_if_else_scope (token& t, type& tt, - optional<description>& d, - lines& ls) - { - // enter: peeked token of next line (lcbrace) - // leave: newline - - assert (ls.size () == 1); // The if/if! line. - - // Use if/if! as the entire scope chain location. - // - const location sl (ls.back ().tokens.front ().location ()); - - // First check that we don't have any teardown commands yet. This - // will detect things like variable assignments between scopes. - // - if (!group_->tdown_.empty ()) - { - location tl ( - group_->tdown_.back ().tokens.front ().location ()); - - fail (sl) << "scope after teardown" << - info (tl) << "last teardown line appears here"; - } - - // If there is no user-supplied id, use the line number (prefixed with - // include id) as the scope id. Note that we use the same id for all - // scopes in the chain. - // - const string& id ( - d && !d->id.empty () - ? d->id - : insert_id (id_prefix_ + to_string (sl.line), sl)); - - unique_ptr<scope> root; - - // Parse the if-else scope chain. - // - line_type bt (line_type::cmd_if); // Current block. - - for (unique_ptr<scope>* ps (&root);; ps = &(*ps)->if_chain) - { - next (t, tt); // Get '{'. - - { - unique_ptr<group> g (pre_parse_scope_block (t, tt, id)); - - // If-condition. - // - g->if_cond_ = move (ls.back ()); - ls.clear (); - - // Description. For now we just duplicate it through the entire - // chain. - // - g->desc = (ps == &root ? d : root->desc); - - *ps = move (g); - } - - // See if what comes next is another chain element. - // - line_type lt (line_type::cmd_end); - - type pt (peek (lexer_mode::first_token)); - const token& p (peeked ()); - const location ll (get_location (p)); - - if (pt == type::word && p.qtype == quote_type::unquoted) - { - if (p.value == "elif") lt = line_type::cmd_elif; - else if (p.value == "elif!") lt = line_type::cmd_elifn; - else if (p.value == "else") lt = line_type::cmd_else; - } - - if (lt == line_type::cmd_end) - break; - - // Check if-else block sequencing. - // - if (bt == line_type::cmd_else) - { - if (lt == line_type::cmd_else || - lt == line_type::cmd_elif || - lt == line_type::cmd_elifn) - fail (ll) << lt << " after " << bt; - } - - // Parse just the condition line using pre_parse_line() in the "one" - // mode and into ls so that it is naturally picked up as if_cond_ on - // the next iteration. - // - optional<description> td; - bool semi (pre_parse_line (t, (tt = pt), td, &ls, true)); - assert (ls.size () == 1 && ls.back ().type == lt); - assert (tt == type::newline); - - // For any of these lines trailing semi or description is illegal. - // - // @@ Not the exact location of semi/colon. - // - if (semi) - fail (ll) << "';' after " << lt; - - if (td) - fail (ll) << "description after " << lt; - - // Make sure what comes next is another scope. - // - tt = peek (lexer_mode::first_token); - - if (tt != type::lcbrace) - fail (ll) << "expected scope after " << lt; - - // Update current if-else block. - // - switch (lt) - { - case line_type::cmd_elif: - case line_type::cmd_elifn: bt = line_type::cmd_elif; break; - case line_type::cmd_else: bt = line_type::cmd_else; break; - default: break; - } - } - - pre_parse_demote_group_scope (root); - group_->scopes.push_back (move (root)); - return false; // We never end with a semi. - } - - bool parser:: - pre_parse_if_else_command (token& t, type& tt, - optional<description>& d, - lines& ls) - { - // enter: peeked first token of next line (type in tt) - // leave: newline - - // Parse lines until we see closing 'end'. Nested if-else blocks are - // handled recursively. - // - for (line_type bt (line_type::cmd_if); // Current block. - ; - tt = peek (lexer_mode::first_token)) - { - const location ll (get_location (peeked ())); - - switch (tt) - { - case type::colon: - fail (ll) << "description inside " << bt << endf; - case type::eos: - case type::rcbrace: - case type::lcbrace: - fail (ll) << "expected closing 'end'" << endf; - case type::plus: - fail (ll) << "setup command inside " << bt << endf; - case type::minus: - fail (ll) << "teardown command inside " << bt << endf; - } - - // Parse one line. Note that this one line can still be multiple - // lines in case of if-else. In this case we want to view it as - // cmd_if, not cmd_end. Thus remember the start position of the - // next logical line. - // - size_t i (ls.size ()); - - optional<description> td; - bool semi (pre_parse_line (t, tt, td, &ls, true)); - assert (tt == type::newline); - - line_type lt (ls[i].type); - - // First take care of 'end'. - // - if (lt == line_type::cmd_end) - { - if (td) - { - if (d) - fail (ll) << "both leading and trailing descriptions"; - - d = move (td); - } - - return semi; - } - - // For any other line trailing semi or description is illegal. - // - // @@ Not the exact location of semi/colon. - // - if (semi) - fail (ll) << "';' inside " << bt; - - if (td) - fail (ll) << "description inside " << bt; - - // Check if-else block sequencing. - // - if (bt == line_type::cmd_else) - { - if (lt == line_type::cmd_else || - lt == line_type::cmd_elif || - lt == line_type::cmd_elifn) - fail (ll) << lt << " after " << bt; - } - - // Update current if-else block. - // - switch (lt) - { - case line_type::cmd_elif: - case line_type::cmd_elifn: bt = line_type::cmd_elif; break; - case line_type::cmd_else: bt = line_type::cmd_else; break; - default: break; - } - } - } - - void parser:: - pre_parse_directive (token& t, type& tt) - { - // enter: directive name - // leave: newline - - string d (t.value); - location l (get_location (t)); - next (t, tt); - - // Suspend pre-parsing since we want to really parse the line, with - // expansion, etc. Also parse the whole line in one go. - // - names args; - - if (tt != type::newline) - { - pre_parse_ = false; - args = parse_names (t, tt, - pattern_mode::expand, - false, - "directive argument", - nullptr); - pre_parse_ = true; - } - - if (tt != type::newline) - fail (t) << t << " after directive"; - - if (d == "include") - pre_parse_include_line (move (args), move (l)); - else - assert (false); // Unhandled directive. - } - - void parser:: - pre_parse_include_line (names args, location dl) - { - auto i (args.begin ()); - - // Process options. - // - bool once (false); - for (; i != args.end () && i->simple (); ++i) - { - if (i->value == "--once") - once = true; - else - break; - } - - // Process arguments. - // - auto include = [&dl, once, this] (string n) // throw invalid_path - { - // It may be tempting to use relative paths in diagnostics but it - // most likely will be misguided. - // - auto enter_path = [this] (string n) -> const path& - { - path p (move (n)); - - if (p.relative ()) - p = path_->directory () / p; - - p.normalize (); - - return *script_->paths_.insert (move (p)).first; - }; - - const path& p (enter_path (move (n))); - - if (include_set_->insert (p).second || !once) - { - try - { - ifdstream ifs (p); - lexer l (ifs, p, lexer_mode::command_line); - - const path* op (path_); - path_ = &p; - - lexer* ol (lexer_); - set_lexer (&l); - - string oip (id_prefix_); - id_prefix_ += to_string (dl.line); - id_prefix_ += '-'; - id_prefix_ += p.leaf ().base ().string (); - id_prefix_ += '-'; - - token t (pre_parse_scope_body ()); - - if (t.type != type::eos) - fail (t) << "stray " << t; - - id_prefix_ = oip; - set_lexer (ol); - path_ = op; - } - catch (const io_error& e) - { - fail (dl) << "unable to read testscript " << p << ": " << e; - } - } - }; - - for (; i != args.end (); ++i) - { - name& n (*i); - - try - { - if (n.simple () && !n.empty ()) - { - include (move (n.value)); - continue; - } - } - catch (const invalid_path&) {} // Fall through. - - diag_record dr (fail (dl)); - dr << "invalid testscript include path "; - to_stream (dr.os, n, true); // Quote. - } - } - - description parser:: - pre_parse_leading_description (token& t, type& tt) - { - // enter: peeked at colon (type in tt) - // leave: peeked at in the first_token mode (type in tt) - - assert (tt == type::colon); - - description r; - location loc (get_location (peeked ())); - - string sp; // Strip prefix. - size_t sn (0); // Strip prefix length. - - for (size_t ln (1); tt == type::colon; ++ln) - { - next (t, tt); // Get ':'. - - mode (lexer_mode::description_line); - next (t, tt); - - // If it is empty, then we get newline right away. - // - const string& l (tt == type::word ? t.value : string ()); - - if (tt == type::word) - next (t, tt); // Get newline. - - assert (tt == type::newline); - - // If this is the first line, then get the "strip prefix", i.e., - // the beginning of the line that contains only whitespaces. If - // the subsequent lines start with the same prefix, then we strip - // it. - // - if (ln == 1) - { - sn = l.find_first_not_of (" \t"); - sp.assign (l, 0, sn == string::npos ? (sn = 0) : sn); - } - - // Apply strip prefix. - // - size_t i (l.compare (0, sn, sp) == 0 ? sn : 0); - - // Strip trailing whitespaces, as a courtesy to the user. - // - size_t j (l.find_last_not_of (" \t")); - j = j != string::npos ? j + 1 : i; - - size_t n (j - i); // [i, j) is our data. - - if (ln == 1) - { - // First line. Ignore if it's blank. - // - if (n == 0) - --ln; // Stay as if on the first line. - else - { - // Otherwise, see if it is the id. Failed that we assume it is - // the summary until we see the next line. - // - (l.find_first_of (" \t.", i) >= j ? r.id : r.summary). - assign (l, i, n); - - // If this is an id then validate it. - // - if (!r.id.empty ()) - { - for (char c: r.id) - { - if (!(alnum (c) || c == '_' || c == '-' || c == '+')) - fail (loc) << "illegal character '" << c - << "' in test id '" << r.id << "'"; - } - } - } - } - else if (ln == 2) - { - // If this is a blank then whatever we have in id/summary is good. - // Otherwise, if we have id, then assume this is summary until we - // see the next line. And if not, then move what we (wrongly) - // assumed to be the summary to details. - // - if (n != 0) - { - if (!r.id.empty ()) - r.summary.assign (l, i, n); - else - { - r.details = move (r.summary); - r.details += '\n'; - r.details.append (l, i, n); - - r.summary.clear (); - } - } - } - // Don't treat line 3 as special if we have given up on id/summary. - // - else if (ln == 3 && r.details.empty ()) - { - // If this is a blank and we have id and/or summary, then we are - // good. Otherwise, if we have both, then move what we (wrongly) - // assumed to be id and summary to details. - // - if (n != 0) - { - if (!r.id.empty () && !r.summary.empty ()) - { - r.details = move (r.id); - r.details += '\n'; - r.details += r.summary; - r.details += '\n'; - - r.id.clear (); - r.summary.clear (); - } - - r.details.append (l, i, n); - } - } - else - { - if (!r.details.empty ()) - r.details += '\n'; - - r.details.append (l, i, n); - } - - tt = peek (lexer_mode::first_token); - } - - // Zap trailing newlines in the details. - // - size_t p (r.details.find_last_not_of ('\n')); - if (p != string::npos && ++p != r.details.size ()) - r.details.resize (p); - - if (r.empty ()) - fail (loc) << "empty description"; - - // Insert id into the id map if we have one. - // - if (!r.id.empty ()) - insert_id (r.id, loc); - - return r; - } - - description parser:: - parse_trailing_description (token& t, type& tt) - { - // enter: colon - // leave: newline - - // Parse one-line trailing description. - // - description r; - - // @@ Would be nice to omit trailing description from replay. - // - const location loc (get_location (t)); - - mode (lexer_mode::description_line); - next (t, tt); - - // If it is empty, then we will get newline right away. - // - if (tt == type::word) - { - string l (move (t.value)); - trim (l); // Strip leading/trailing whitespaces. - - // Decide whether this is id or summary. - // - (l.find_first_of (" \t") == string::npos ? r.id : r.summary) = - move (l); - - next (t, tt); // Get newline. - } - - assert (tt == type::newline); // Lexer mode invariant. - - if (r.empty ()) - fail (loc) << "empty description"; - - // Insert id into the id map if we have one. - // - if (pre_parse_ && !r.id.empty ()) - insert_id (r.id, loc); - - return r; - } - - value parser:: - parse_variable_line (token& t, type& tt) - { - // enter: assignment - // leave: newline or semi - - // We cannot reuse the value mode since it will recognize { which we - // want to treat as a literal. - // - mode (lexer_mode::variable_line); - next (t, tt); - - // Parse value attributes if any. Note that it's ok not to have - // anything after the attributes (e.g., foo=[null]). - // - attributes_push (t, tt, true); - - // @@ PAT: Should we expand patterns? Note that it will only be - // simple ones since we have disabled {}. Also, what would be the - // pattern base directory? - // - return tt != type::newline && tt != type::semi - ? parse_value (t, tt, - pattern_mode::ignore, - "variable value", - nullptr) - : value (names ()); - } - - command_expr parser:: - parse_command_line (token& t, type& tt) - { - // enter: first token of the command line - // leave: <newline> - - // Note: this one is only used during execution. - - pair<command_expr, here_docs> p (parse_command_expr (t, tt)); - - switch (tt) - { - case type::colon: parse_trailing_description (t, tt); break; - case type::semi: next (t, tt); break; // Get newline. - } - - assert (tt == type::newline); - - parse_here_documents (t, tt, p); - assert (tt == type::newline); - - return move (p.first); - } - - // Parse the regular expression representation (non-empty string value - // framed with introducer characters and optionally followed by flag - // characters from the {di} set, for example '/foo/id') into - // components. Also return end-of-parsing position if requested, - // otherwise treat any unparsed characters left as an error. - // - struct regex_parts - { - string value; - char intro; - string flags; // Combination of characters from {di} set. - - // Create a special empty object. - // - regex_parts (): intro ('\0') {} - - regex_parts (string v, char i, string f) - : value (move (v)), intro (i), flags (move (f)) {} - }; - - static regex_parts - parse_regex (const string& s, - const location& l, - const char* what, - size_t* end = nullptr) - { - if (s.empty ()) - fail (l) << "no introducer character in " << what; - - size_t p (s.find (s[0], 1)); // Find terminating introducer. - - if (p == string::npos) - fail (l) << "no closing introducer character in " << what; - - size_t rn (p - 1); // Regex length. - if (rn == 0) - fail (l) << what << " is empty"; - - // Find end-of-flags position. - // - size_t fp (++p); // Save flags starting position. - for (char c; (c = s[p]) == 'd' || c == 'i'; ++p) ; - - // If string end is not reached then report invalid flags, unless - // end-of-parsing position is requested (which means regex is just a - // prefix). - // - if (s[p] != '\0' && end == nullptr) - fail (l) << "junk at the end of " << what; - - if (end != nullptr) - *end = p; - - return regex_parts (string (s, 1, rn), s[0], string (s, fp, p - fp)); - } - - pair<command_expr, parser::here_docs> parser:: - parse_command_expr (token& t, type& tt) - { - // enter: first token of the command line - // leave: <newline> - - command_expr expr; - - // OR-ed to an implied false for the first term. - // - expr.push_back ({expr_operator::log_or, command_pipe ()}); - - command c; // Command being assembled. - - // Make sure the command makes sense. - // - auto check_command = [&c, this] (const location& l, bool last) - { - if (c.out.type == redirect_type::merge && - c.err.type == redirect_type::merge) - fail (l) << "stdout and stderr redirected to each other"; - - if (!last && c.out.type != redirect_type::none) - fail (l) << "stdout is both redirected and piped"; - }; - - // Check that the introducer character differs from '/' if the - // portable path modifier is specified. Must be called before - // parse_regex() (see below) to make sure its diagnostics is - // meaningful. - // - // Note that the portable path modifier assumes '/' to be a valid - // regex character and so makes it indistinguishable from the - // terminating introducer. - // - auto check_regex_mod = [this] (const string& mod, - const string& re, - const location& l, - const char* what) - { - // Handles empty regex properly. - // - if (mod.find ('/') != string::npos && re[0] == '/') - fail (l) << "portable path modifier and '/' introducer in " - << what; - }; - - // Pending positions where the next word should go. - // - enum class pending - { - none, - program, - in_string, - in_document, - in_file, - out_merge, - out_string, - out_str_regex, - out_document, - out_doc_regex, - out_file, - err_merge, - err_string, - err_str_regex, - err_document, - err_doc_regex, - err_file, - clean - }; - pending p (pending::program); - string mod; // Modifiers for pending in_* and out_* positions. - here_docs hd; // Expected here-documents. - - // Add the next word to either one of the pending positions or to - // program arguments by default. - // - auto add_word = [&c, &p, &mod, &check_regex_mod, this] ( - string&& w, const location& l) - { - auto add_merge = [&l, this] (redirect& r, const string& w, int fd) - { - try - { - size_t n; - if (stoi (w, &n) == fd && n == w.size ()) - { - r.fd = fd; - return; - } - } - catch (const exception&) {} // Fall through. - - fail (l) << (fd == 1 ? "stderr" : "stdout") << " merge redirect " - << "file descriptor must be " << fd; - }; - - auto add_here_str = [] (redirect& r, string&& w) - { - if (r.modifiers.find (':') == string::npos) - w += '\n'; - r.str = move (w); - }; - - auto add_here_str_regex = [&l, &check_regex_mod] ( - redirect& r, int fd, string&& w) - { - const char* what (nullptr); - switch (fd) - { - case 1: what = "stdout regex redirect"; break; - case 2: what = "stderr regex redirect"; break; - } - - check_regex_mod (r.modifiers, w, l, what); - - regex_parts rp (parse_regex (w, l, what)); - - regex_lines& re (r.regex); - re.intro = rp.intro; - - re.lines.emplace_back ( - l.line, l.column, move (rp.value), move (rp.flags)); - - // Add final blank line unless suppressed. - // - // Note that the position is synthetic, but that's ok as we don't - // expect any diagnostics to refer this line. - // - if (r.modifiers.find (':') == string::npos) - re.lines.emplace_back (l.line, l.column, string (), false); - }; - - auto parse_path = [&l, this] (string&& w, const char* what) -> path - { - try - { - path p (move (w)); - - if (!p.empty ()) - { - p.normalize (); - return p; - } - - fail (l) << "empty " << what << endf; - } - catch (const invalid_path& e) - { - fail (l) << "invalid " << what << " '" << e.path << "'" << endf; - } - }; - - auto add_file = [&parse_path] (redirect& r, int fd, string&& w) - { - const char* what (nullptr); - switch (fd) - { - case 0: what = "stdin redirect path"; break; - case 1: what = "stdout redirect path"; break; - case 2: what = "stderr redirect path"; break; - } - - r.file.path = parse_path (move (w), what); - }; - - switch (p) - { - case pending::none: c.arguments.push_back (move (w)); break; - case pending::program: - c.program = parse_path (move (w), "program path"); - break; - - case pending::out_merge: add_merge (c.out, w, 2); break; - case pending::err_merge: add_merge (c.err, w, 1); break; - - case pending::in_string: add_here_str (c.in, move (w)); break; - case pending::out_string: add_here_str (c.out, move (w)); break; - case pending::err_string: add_here_str (c.err, move (w)); break; - - case pending::out_str_regex: - { - add_here_str_regex (c.out, 1, move (w)); - break; - } - case pending::err_str_regex: - { - add_here_str_regex (c.err, 2, move (w)); - break; - } - - // These are handled specially below. - // - case pending::in_document: - case pending::out_document: - case pending::err_document: - case pending::out_doc_regex: - case pending::err_doc_regex: assert (false); break; - - case pending::in_file: add_file (c.in, 0, move (w)); break; - case pending::out_file: add_file (c.out, 1, move (w)); break; - case pending::err_file: add_file (c.err, 2, move (w)); break; - - case pending::clean: - { - cleanup_type t; - switch (mod[0]) // Ok, if empty - { - case '!': t = cleanup_type::never; break; - case '?': t = cleanup_type::maybe; break; - default: t = cleanup_type::always; break; - } - - c.cleanups.push_back ( - {t, parse_path (move (w), "cleanup path")}); - break; - } - } - - p = pending::none; - mod.clear (); - }; - - // Make sure we don't have any pending positions to fill. - // - auto check_pending = [&p, this] (const location& l) - { - const char* what (nullptr); - - switch (p) - { - case pending::none: break; - case pending::program: what = "program"; break; - case pending::in_string: what = "stdin here-string"; break; - case pending::in_document: what = "stdin here-document end"; break; - case pending::in_file: what = "stdin file"; break; - case pending::out_merge: what = "stdout file descriptor"; break; - case pending::out_string: what = "stdout here-string"; break; - case pending::out_document: what = "stdout here-document end"; break; - case pending::out_file: what = "stdout file"; break; - case pending::err_merge: what = "stderr file descriptor"; break; - case pending::err_string: what = "stderr here-string"; break; - case pending::err_document: what = "stderr here-document end"; break; - case pending::err_file: what = "stderr file"; break; - case pending::clean: what = "cleanup path"; break; - - case pending::out_str_regex: - { - what = "stdout here-string regex"; - break; - } - case pending::err_str_regex: - { - what = "stderr here-string regex"; - break; - } - case pending::out_doc_regex: - { - what = "stdout here-document regex end"; - break; - } - case pending::err_doc_regex: - { - what = "stderr here-document regex end"; - break; - } - } - - if (what != nullptr) - fail (l) << "missing " << what; - }; - - // Parse the redirect operator. - // - auto parse_redirect = - [&c, &expr, &p, &mod, this] (token& t, const location& l) - { - // Our semantics is the last redirect seen takes effect. - // - assert (p == pending::none && mod.empty ()); - - // See if we have the file descriptor. - // - unsigned long fd (3); - if (!t.separated) - { - if (c.arguments.empty ()) - fail (l) << "missing redirect file descriptor"; - - const string& s (c.arguments.back ()); - - try - { - size_t n; - fd = stoul (s, &n); - - if (n != s.size () || fd > 2) - throw invalid_argument (string ()); - } - catch (const exception&) - { - fail (l) << "invalid redirect file descriptor '" << s << "'"; - } - - c.arguments.pop_back (); - } - - type tt (t.type); - - // Validate/set default file descriptor. - // - switch (tt) - { - case type::in_pass: - case type::in_null: - case type::in_str: - case type::in_doc: - case type::in_file: - { - if ((fd = fd == 3 ? 0 : fd) != 0) - fail (l) << "invalid in redirect file descriptor " << fd; - - if (!expr.back ().pipe.empty ()) - fail (l) << "stdin is both piped and redirected"; - - break; - } - case type::out_pass: - case type::out_null: - case type::out_trace: - case type::out_merge: - case type::out_str: - case type::out_doc: - case type::out_file_cmp: - case type::out_file_ovr: - case type::out_file_app: - { - if ((fd = fd == 3 ? 1 : fd) == 0) - fail (l) << "invalid out redirect file descriptor " << fd; - - break; - } - } - - mod = move (t.value); - - redirect_type rt (redirect_type::none); - switch (tt) - { - case type::in_pass: - case type::out_pass: rt = redirect_type::pass; break; - - case type::in_null: - case type::out_null: rt = redirect_type::null; break; - - case type::out_trace: rt = redirect_type::trace; break; - - case type::out_merge: rt = redirect_type::merge; break; - - case type::in_str: - case type::out_str: - { - bool re (mod.find ('~') != string::npos); - assert (tt == type::out_str || !re); - - rt = re - ? redirect_type::here_str_regex - : redirect_type::here_str_literal; - - break; - } - - case type::in_doc: - case type::out_doc: - { - bool re (mod.find ('~') != string::npos); - assert (tt == type::out_doc || !re); - - rt = re - ? redirect_type::here_doc_regex - : redirect_type::here_doc_literal; - - break; - } - - case type::in_file: - case type::out_file_cmp: - case type::out_file_ovr: - case type::out_file_app: rt = redirect_type::file; break; - } - - redirect& r (fd == 0 ? c.in : fd == 1 ? c.out : c.err); - r = redirect (rt); - - // Don't move as still may be used for pending here-document end - // marker processing. - // - r.modifiers = mod; - - switch (rt) - { - case redirect_type::none: - case redirect_type::pass: - case redirect_type::null: - case redirect_type::trace: - break; - case redirect_type::merge: - switch (fd) - { - case 0: assert (false); break; - case 1: p = pending::out_merge; break; - case 2: p = pending::err_merge; break; - } - break; - case redirect_type::here_str_literal: - switch (fd) - { - case 0: p = pending::in_string; break; - case 1: p = pending::out_string; break; - case 2: p = pending::err_string; break; - } - break; - case redirect_type::here_str_regex: - switch (fd) - { - case 0: assert (false); break; - case 1: p = pending::out_str_regex; break; - case 2: p = pending::err_str_regex; break; - } - break; - case redirect_type::here_doc_literal: - switch (fd) - { - case 0: p = pending::in_document; break; - case 1: p = pending::out_document; break; - case 2: p = pending::err_document; break; - } - break; - case redirect_type::here_doc_regex: - switch (fd) - { - case 0: assert (false); break; - case 1: p = pending::out_doc_regex; break; - case 2: p = pending::err_doc_regex; break; - } - break; - case redirect_type::file: - switch (fd) - { - case 0: p = pending::in_file; break; - case 1: p = pending::out_file; break; - case 2: p = pending::err_file; break; - } - - // Also sets for stdin, but this is harmless. - // - r.file.mode = tt == type::out_file_ovr - ? redirect_fmode::overwrite - : (tt == type::out_file_app - ? redirect_fmode::append - : redirect_fmode::compare); - - break; - - case redirect_type::here_doc_ref: assert (false); break; - } - }; - - // Set pending cleanup type. - // - auto parse_clean = [&p, &mod] (token& t) - { - p = pending::clean; - mod = move (t.value); - }; - - const location ll (get_location (t)); // Line location. - - // Keep parsing chunks of the command line until we see one of the - // "terminators" (newline, semicolon, exit status comparison, etc). - // - location l (ll); - names ns; // Reuse to reduce allocations. - - for (bool done (false); !done; l = get_location (t)) - { - switch (tt) - { - case type::semi: - case type::colon: - case type::newline: - { - done = true; - break; - } - - case type::equal: - case type::not_equal: - { - if (!pre_parse_) - check_pending (l); - - c.exit = parse_command_exit (t, tt); - - // Only a limited set of things can appear after the exit status - // so we check this here. - // - switch (tt) - { - case type::semi: - case type::colon: - case type::newline: - - case type::pipe: - case type::log_or: - case type::log_and: - break; - default: - fail (t) << "unexpected " << t << " after command exit status"; - } - - break; - } - - case type::pipe: - case type::log_or: - case type::log_and: - - case type::in_pass: - case type::out_pass: - - case type::in_null: - case type::out_null: - - case type::out_trace: - - case type::out_merge: - - case type::in_str: - case type::in_doc: - case type::out_str: - case type::out_doc: - - case type::in_file: - case type::out_file_cmp: - case type::out_file_ovr: - case type::out_file_app: - - case type::clean: - { - 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. - // - switch (tt) - { - case type::in_doc: - case type::out_doc: - mod = move (t.value); - - bool re (mod.find ('~') != string::npos); - const char* what (re - ? "here-document regex end marker" - : "here-document end marker"); - - // We require the end marker to be a literal, unquoted word. - // In particularm, we don't allow quoted because of cases - // like foo"$bar" (where we will see word 'foo'). - // - next (t, tt); - - // We require the end marker to be an unquoted or completely - // quoted word. The complete quoting becomes important for - // cases like foo"$bar" (where we will see word 'foo'). - // - // For good measure we could have also required it to be - // separated from the following token, but out grammar - // allows one to write >>EOO;. The problematic sequence - // would be >>FOO$bar -- on reparse it will be expanded - // as a single word. - // - if (tt != type::word || t.value.empty ()) - fail (t) << "expected " << what; - - peek (); - const token& p (peeked ()); - if (!p.separated) - { - switch (p.type) - { - case type::dollar: - case type::lparen: - fail (p) << what << " must be literal"; - } - } - - quote_type qt (t.qtype); - switch (qt) - { - case quote_type::unquoted: - qt = quote_type::single; // Treat as single-quoted. - break; - case quote_type::single: - case quote_type::double_: - if (t.qcomp) - break; - // Fall through. - case quote_type::mixed: - fail (t) << "partially-quoted " << what; - } - - regex_parts r; - string end (move (t.value)); - - if (re) - { - check_regex_mod (mod, end, l, what); - - r = parse_regex (end, l, what); - end = move (r.value); // The "cleared" end marker. - } - - bool literal (qt == quote_type::single); - bool shared (false); - - for (const auto& d: hd) - { - if (d.end == end) - { - auto check = [&t, &end, &re, this] (bool c, - const char* what) - { - if (!c) - fail (t) << "different " << what - << " for shared here-document " - << (re ? "regex '" : "'") << end << "'"; - }; - - check (d.modifiers == mod, "modifiers"); - check (d.literal == literal, "quoting"); - - if (re) - { - check (d.regex == r.intro, "introducers"); - check (d.regex_flags == r.flags, "global flags"); - } - - shared = true; - break; - } - } - - if (!shared) - hd.push_back ( - here_doc { - {}, - move (end), - literal, - move (mod), - r.intro, move (r.flags)}); - - break; - } - - next (t, tt); - break; - } - - // If this is one of the operators/separators, check that we - // don't have any pending locations to be filled. - // - check_pending (l); - - // Note: there is another one in the inner loop below. - // - switch (tt) - { - case type::pipe: - case type::log_or: - case type::log_and: - { - // Check that the previous command makes sense. - // - check_command (l, tt != type::pipe); - expr.back ().pipe.push_back (move (c)); - - c = command (); - p = pending::program; - - if (tt != type::pipe) - { - expr_operator o (tt == type::log_or - ? expr_operator::log_or - : expr_operator::log_and); - expr.push_back ({o, command_pipe ()}); - } - - break; - } - - case type::in_pass: - case type::out_pass: - - case type::in_null: - case type::out_null: - - case type::out_trace: - - case type::out_merge: - - case type::in_str: - case type::in_doc: - case type::out_str: - case type::out_doc: - - case type::in_file: - case type::out_file_cmp: - case type::out_file_ovr: - case type::out_file_app: - { - parse_redirect (t, l); - break; - } - - case type::clean: - { - parse_clean (t); - break; - } - - default: assert (false); break; - } - - next (t, tt); - break; - } - default: - { - // Here-document end markers are literal (we verified that above - // during pre-parsing) and we need to know whether they were - // quoted. So handle this case specially. - // - { - int fd; - switch (p) - { - case pending::in_document: fd = 0; break; - case pending::out_document: - case pending::out_doc_regex: fd = 1; break; - case pending::err_document: - case pending::err_doc_regex: fd = 2; break; - default: fd = -1; break; - } - - if (fd != -1) - { - here_redirect rd { - expr.size () - 1, expr.back ().pipe.size (), fd}; - - string end (move (t.value)); - - regex_parts r; - - if (p == pending::out_doc_regex || - p == pending::err_doc_regex) - { - // We can't fail here as we already parsed all the end - // markers during pre-parsing stage, and so no need in the - // description. - // - r = parse_regex (end, l, ""); - end = move (r.value); // The "cleared" end marker. - } - - bool shared (false); - for (auto& d: hd) - { - // No need to check that redirects that share here-document - // have the same modifiers, etc. That have been done during - // pre-parsing. - // - if (d.end == end) - { - d.redirects.emplace_back (rd); - shared = true; - break; - } - } - - if (!shared) - hd.push_back ( - here_doc { - {rd}, - move (end), - (t.qtype == quote_type::unquoted || - t.qtype == quote_type::single), - move (mod), - r.intro, move (r.flags)}); - - p = pending::none; - mod.clear (); - - next (t, tt); - break; - } - } - - // 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. - // - // @@ 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. - break; - - // Process what we got. Determine whether anything inside was - // quoted (note that the current token is "next" and is not part - // of this). - // - bool q ((quoted () - - (t.qtype != quote_type::unquoted ? 1 : 0)) != 0); - - for (name& n: ns) - { - string s; - - try - { - s = value_traits<string>::convert (move (n), nullptr); - } - catch (const invalid_argument&) - { - diag_record dr (fail (l)); - dr << "invalid string value "; - to_stream (dr.os, n, true); // Quote. - } - - // If it is a quoted chunk, then we add the word as is. - // Otherwise we re-lex it. But if the word doesn't contain any - // interesting characters (operators plus quotes/escapes), - // then no need to re-lex. - // - // NOTE: update quoting (script.cxx:to_stream_q()) if adding - // any new characters. - // - if (q || s.find_first_of ("|&<>\'\"\\") == string::npos) - add_word (move (s), l); - else - { - // If the chunk re-parsing results in error, our diagnostics - // will look like this: - // - // <string>:1:4: error: stdout merge redirect file descriptor must be 2 - // testscript:2:5: info: while parsing string '1>&a' - // - auto df = make_diag_frame ( - [s, &l](const diag_record& dr) - { - dr << info (l) << "while parsing string '" << s << "'"; - }); - - // When re-lexing we do "effective escaping" and only for - // ['"\] (quotes plus the backslash itself). In particular, - // there is no way to escape redirects, operators, etc. The - // idea is to prefer quoting except for passing literal - // quotes, for example: - // - // args = \"&foo\" - // cmd $args # cmd &foo - // - // args = 'x=\"foo bar\"' - // cmd $args # cmd x="foo bar" - // - - path name ("<string>"); - istringstream is (s); - lexer lex (is, name, - lexer_mode::command_expansion, - "\'\"\\"); - - // Treat the first "sub-token" as always separated from what - // we saw earlier. - // - // Note that this is not "our" token so we cannot do - // fail(t). Rather we should do fail(l). - // - token t (lex.next ()); - location l (build2::get_location (t, name)); - t.separated = true; - - string w; - bool f (t.type == type::eos); // If the whole thing is empty. - - for (; t.type != type::eos; t = lex.next ()) - { - type tt (t.type); - l = build2::get_location (t, name); - - // Re-lexing double-quotes will recognize $, ( inside as - // tokens so we have to reverse them back. Since we don't - // treat spaces as separators we can be sure we will get - // it right. - // - switch (tt) - { - case type::dollar: w += '$'; continue; - case type::lparen: w += '('; continue; - } - - // Retire the current word. We need to distinguish between - // empty and non-existent (e.g., > vs >""). - // - if (!w.empty () || f) - { - add_word (move (w), l); - f = false; - } - - if (tt == type::word) - { - w = move (t.value); - f = true; - continue; - } - - // If this is one of the operators/separators, check that - // we don't have any pending locations to be filled. - // - check_pending (l); - - // Note: there is another one in the outer loop above. - // - switch (tt) - { - case type::pipe: - case type::log_or: - case type::log_and: - { - // Check that the previous command makes sense. - // - check_command (l, tt != type::pipe); - expr.back ().pipe.push_back (move (c)); - - c = command (); - p = pending::program; - - if (tt != type::pipe) - { - expr_operator o (tt == type::log_or - ? expr_operator::log_or - : expr_operator::log_and); - expr.push_back ({o, command_pipe ()}); - } - - break; - } - - case type::in_pass: - case type::out_pass: - - case type::in_null: - case type::out_null: - - case type::out_trace: - - case type::out_merge: - - case type::in_str: - case type::out_str: - - case type::in_file: - case type::out_file_cmp: - case type::out_file_ovr: - case type::out_file_app: - { - parse_redirect (t, l); - break; - } - - case type::clean: - { - parse_clean (t); - break; - } - - case type::in_doc: - case type::out_doc: - { - fail (l) << "here-document redirect in expansion"; - break; - } - } - } - - // Don't forget the last word. - // - if (!w.empty () || f) - add_word (move (w), l); - } - } - - ns.clear (); - break; - } - } - } - - if (!pre_parse_) - { - // Verify we don't have anything pending to be filled and the - // command makes sense. - // - check_pending (l); - check_command (l, true); - - expr.back ().pipe.push_back (move (c)); - } - - return make_pair (move (expr), move (hd)); - } - - command_exit parser:: - parse_command_exit (token& t, type& tt) - { - // enter: equal/not_equal - // leave: token after exit status (one parse_names() chunk) - - exit_comparison comp (tt == type::equal - ? exit_comparison::eq - : exit_comparison::ne); - - // The next chunk should be the exit status. - // - next (t, tt); - location l (get_location (t)); - names ns (parse_names (t, tt, - pattern_mode::ignore, - true, - "exit status", - nullptr)); - unsigned long es (256); - - if (!pre_parse_) - { - try - { - if (ns.size () == 1 && ns[0].simple () && !ns[0].empty ()) - es = stoul (ns[0].value); - } - catch (const exception&) {} // Fall through. - - if (es > 255) - { - diag_record dr; - - dr << fail (l) << "expected exit status instead of "; - to_stream (dr.os, ns, true); // Quote. - - dr << info << "exit status is an unsigned integer less than 256"; - } - } - - return command_exit {comp, static_cast<uint8_t> (es)}; - } - - void parser:: - parse_here_documents (token& t, type& tt, - pair<command_expr, here_docs>& p) - { - // enter: newline - // leave: newline - - // Parse here-document fragments in the order they were mentioned on - // the command line. - // - for (here_doc& h: p.second) - { - // Switch to the here-line mode which is like single/double-quoted - // string but recognized the newline as a separator. - // - mode (h.literal - ? lexer_mode::here_line_single - : lexer_mode::here_line_double); - next (t, tt); - - parsed_doc v ( - parse_here_document (t, tt, h.end, h.modifiers, h.regex)); - - if (!pre_parse_) - { - assert (!h.redirects.empty ()); - auto i (h.redirects.cbegin ()); - - command& c (p.first[i->expr].pipe[i->pipe]); - redirect& r (i->fd == 0 ? c.in : i->fd == 1 ? c.out : c.err); - - if (v.re) - { - r.regex = move (v.regex); - r.regex.flags = move (h.regex_flags); - } - else - r.str = move (v.str); - - r.end = move (h.end); - r.end_line = v.end_line; - r.end_column = v.end_column; - - // Note that our references cannot be invalidated because the - // command_expr/command-pipe vectors already contain all their - // elements. - // - for (++i; i != h.redirects.cend (); ++i) - { - command& c (p.first[i->expr].pipe[i->pipe]); - - (i->fd == 0 ? c.in : i->fd == 1 ? c.out : c.err) = - redirect (redirect_type::here_doc_ref, r); - } - } - - expire_mode (); - } - } - - parser::parsed_doc parser:: - parse_here_document (token& t, type& tt, - const string& em, - const string& mod, - char re) - { - // enter: first token on first line - // leave: newline (after end marker) - - // String literal. Note that when decide if to terminate the previously - // added line with a newline, we need to distinguish a yet empty result - // and the one that has a single blank line added. - // - optional<string> rs; - - regex_lines rre; - - // Here-documents can be indented. The leading whitespaces of the end - // marker line (called strip prefix) determine the indentation. Every - // other line in the here-document should start with this prefix which - // is automatically stripped. The only exception is a blank line. - // - // The fact that the strip prefix is only known at the end, after - // seeing all the lines, is rather inconvenient. As a result, the way - // we implement this is a bit hackish (though there is also something - // elegant about it): at the end of the pre-parse stage we are going - // re-examine the sequence of tokens that comprise this here-document - // and "fix up" the first token of each line by stripping the prefix. - // - string sp; - - // Remember the position of the first token in this here-document. - // - size_t ri (pre_parse_ ? replay_data_.size () - 1 : 0); - - // We will use the location of the first token on the line for the - // regex diagnostics. At the end of the loop it will point to the - // beginning of the end marker. - // - location l; - - while (tt != type::eos) - { - l = get_location (t); - - // Check if this is the end marker. For starters, it should be a - // single, unquoted word followed by a newline. - // - if (tt == type::word && - t.qtype == quote_type::unquoted && - peek () == type::newline) - { - const string& v (t.value); - - size_t vn (v.size ()); - size_t en (em.size ()); - - // Then check that it ends with the end marker. - // - if (vn >= en && v.compare (vn - en, en, em) == 0) - { - // Now check that the prefix only contains whitespaces. - // - size_t n (vn - en); - - if (v.find_first_not_of (" \t") >= n) - { - assert (pre_parse_ || n == 0); // Should have been stripped. - - if (n != 0) - sp.assign (v, 0, n); // Save the strip prefix. - - next (t, tt); // Get the newline. - break; - } - } - } - - // Expand the line (can be blank). - // - // @@ PAT: one could argue that if we do it in variables, then we - // should do it here as well. Though feels bizarre. - // - names ns (tt != type::newline - ? parse_names (t, tt, - pattern_mode::ignore, - false, - "here-document line", - nullptr) - : names ()); - - if (!pre_parse_) - { - // What shall we do if the expansion results in multiple names? - // For, example if the line contains just the variable expansion - // and it is of type strings. Adding all the elements space- - // separated seems like the natural thing to do. - // - string s; - for (auto b (ns.begin ()), i (b); i != ns.end (); ++i) - { - string n; - - try - { - n = value_traits<string>::convert (move (*i), nullptr); - } - catch (const invalid_argument&) - { - fail (l) << "invalid string value '" << *i << "'"; - } - - if (i == b) - s = move (n); - else - { - s += ' '; - s += n; - } - } - - if (!re) - { - // Add newline after previous line. - // - if (rs) - { - *rs += '\n'; - *rs += s; - } - else - rs = move (s); - } - else - { - // Due to expansion we can end up with multiple lines. If empty - // then will add a blank textual literal. - // - for (size_t p (0); p != string::npos; ) - { - string ln; - size_t np (s.find ('\n', p)); - - if (np != string::npos) - { - ln = string (s, p, np - p); - p = np + 1; - } - else - { - ln = string (s, p); - p = np; - } - - if (ln[0] != re) // Line doesn't start with regex introducer. - { - // This is a line-char literal (covers blank lines as well). - // - // Append textual literal. - // - rre.lines.emplace_back (l.line, l.column, move (ln), false); - } - else // Line starts with the regex introducer. - { - // This is a char-regex, or a sequence of line-regex syntax - // characters or both (in this specific order). So we will - // add regex (with optional special characters) or special - // literal. - // - size_t p (ln.find (re, 1)); - if (p == string::npos) - { - // No regex, just a sequence of syntax characters. - // - string spec (ln, 1); - if (spec.empty ()) - fail (l) << "no syntax line characters"; - - // Append special literal. - // - rre.lines.emplace_back ( - l.line, l.column, move (spec), true); - } - else - { - // Regex (probably with syntax characters). - // - regex_parts re; - - // Empty regex is a special case repesenting a blank line. - // - if (p == 1) - // Position to optional specal characters of an empty - // regex. - // - ++p; - else - // Can't fail as all the pre-conditions verified - // (non-empty with both introducers in place), so no - // description required. - // - re = parse_regex (ln, l, "", &p); - - // Append regex with optional special characters. - // - rre.lines.emplace_back (l.line, l.column, - move (re.value), move (re.flags), - string (ln, p)); - } - } - } - } - } - - // We should expand the whole line at once so this would normally be - // a newline but can also be an end-of-stream. - // - if (tt == type::newline) - next (t, tt); - else - assert (tt == type::eos); - } - - if (tt == type::eos) - fail (t) << "missing here-document end marker '" << em << "'"; - - if (pre_parse_) - { - // Strip the indentation prefix if there is one. - // - assert (replay_ == replay::save); - - if (!sp.empty ()) - { - size_t sn (sp.size ()); - - for (; ri != replay_data_.size (); ++ri) - { - token& rt (replay_data_[ri].token); - - if (rt.type == type::newline) // Blank - continue; - - if (rt.type != type::word || rt.value.compare (0, sn, sp) != 0) - fail (rt) << "unindented here-document line"; - - // If the word is equal to the strip prefix then we have to drop - // the token. Note that simply making it an empty word won't - // have the same semantics. For instance, it would trigger - // concatenated expansion. - // - if (rt.value.size () == sn) - replay_data_.erase (replay_data_.begin () + ri); - else - { - rt.value.erase (0, sn); - rt.column += sn; - ++ri; - } - - // Skip until next newline. - // - for (; replay_data_[ri].token.type != type::newline; ++ri) ; - } - } - } - else - { - // Add final newline unless suppressed. - // - if (mod.find (':') == string::npos) - { - if (re) - // Note that the position is synthetic, but that's ok as we don't - // expect any diagnostics to refer this line. - // - rre.lines.emplace_back (l.line, l.column, string (), false); - else if (rs) - *rs += '\n'; - else - rs = "\n"; - } - - // Finalize regex lines. - // - if (re) - { - // Empty regex matches nothing, so not of much use. - // - if (rre.lines.empty ()) - fail (l) << "empty here-document regex"; - - rre.intro = re; - } - } - - return re - ? parsed_doc (move (rre), l.line, l.column) - : parsed_doc (rs ? move (*rs) : string (), l.line, l.column); - } - - // - // Execute. - // - - void parser:: - execute (script& s, runner& r) - { - assert (s.state == scope_state::unknown); - - auto g ( - make_exception_guard ( - [&s] () {s.state = scope_state::failed;})); - - if (!s.empty ()) - execute (s, s, r); - else - s.state = scope_state::passed; - } - - void parser:: - execute (scope& sc, script& s, runner& r) - { - path_ = nullptr; // Set by replays. - - pre_parse_ = false; - - set_lexer (nullptr); - - script_ = &s; - runner_ = &r; - group_ = nullptr; - id_map_ = nullptr; - include_set_ = nullptr; - scope_ = ≻ - - //@@ PAT TODO: set pbase_? - - exec_scope_body (); - } - - static void - execute_impl (scope& s, script& scr, runner& r) - { - try - { - parser p; - p.execute (s, scr, r); - } - catch (const failed&) - { - s.state = scope_state::failed; - } - } - - void parser:: - exec_scope_body () - { - size_t li (0); - - runner_->enter (*scope_, scope_->start_loc_); - - if (test* t = dynamic_cast<test*> (scope_)) - { - exec_lines ( - t->tests_.begin (), t->tests_.end (), li, command_type::test); - } - else if (group* g = dynamic_cast<group*> (scope_)) - { - bool exec_scope ( - exec_lines ( - g->setup_.begin (), g->setup_.end (), li, command_type::setup)); - - if (exec_scope) - { - atomic_count task_count (0); - wait_guard wg (task_count); - - // Start asynchronous execution of inner scopes keeping track of - // how many we have handled. - // - for (unique_ptr<scope>& chain: g->scopes) - { - // Check if this scope is ignored (e.g., via config.test). - // - if (!runner_->test (*chain) || !exec_scope) - { - chain = nullptr; - continue; - } - - // Pick a scope from the if-else chain. - // - // In fact, we are going to drop all but the selected (if any) - // scope. This way we can re-examine the scope states later. It - // will also free some memory. - // - unique_ptr<scope>* ps; - for (ps = &chain; *ps != nullptr; ps = &ps->get ()->if_chain) - { - scope& s (**ps); - - if (!s.if_cond_) // Unconditional. - { - assert (s.if_chain == nullptr); - break; - } - - line l (move (*s.if_cond_)); - line_type lt (l.type); - - replay_data (move (l.tokens)); - - token t; - type tt; - - next (t, tt); - const location ll (get_location (t)); - next (t, tt); // Skip to start of command. - - bool take; - if (lt != line_type::cmd_else) - { - // Note: the line index count continues from setup. - // - command_expr ce (parse_command_line (t, tt)); - - try - { - take = runner_->run_if (*scope_, ce, ++li, ll); - } - catch (const exit_scope& e) - { - // Bail out if the scope is exited with the failure status. - // Otherwise leave the scope normally. - // - if (!e.status) - throw failed (); - - // Stop iterating through if conditions, and stop executing - // inner scopes. - // - exec_scope = false; - replay_stop (); - break; - } - - if (lt == line_type::cmd_ifn || lt == line_type::cmd_elifn) - take = !take; - } - else - { - assert (tt == type::newline); - take = true; - } - - replay_stop (); - - if (take) - { - // Count the remaining conditions for the line index. - // - for (scope* r (s.if_chain.get ()); - r != nullptr && - r->if_cond_->type != line_type::cmd_else; - r = r->if_chain.get ()) - ++li; - - s.if_chain.reset (); // Drop remaining scopes. - break; - } - } - - chain.reset (*ps == nullptr || (*ps)->empty () || !exec_scope - ? nullptr - : ps->release ()); - - if (chain != nullptr) - { - // Hand it off to a sub-parser potentially in another thread. - // But we could also have handled it serially in this parser: - // - // scope* os (scope_); - // scope_ = chain.get (); - // exec_scope_body (); - // scope_ = os; - - // Pass our diagnostics stack (this is safe since we are going - // to wait for completion before unwinding the diag stack). - // - // If the scope was executed synchronously, check the status - // and bail out if we weren't asked to keep going. - // - // UBSan workaround. - // - const diag_frame* df (diag_frame::stack ()); - if (!sched.async (task_count, - [] (const diag_frame* ds, - scope& s, - script& scr, - runner& r) - { - diag_frame::stack_guard dsg (ds); - execute_impl (s, scr, r); - }, - df, - ref (*chain), - ref (*script_), - ref (*runner_))) - { - // Bail out if the scope has failed and we weren't instructed - // to keep going. - // - if (chain->state == scope_state::failed && !keep_going) - throw failed (); - } - } - } - - wg.wait (); - - // Re-examine the scopes we have executed collecting their state. - // - for (const unique_ptr<scope>& chain: g->scopes) - { - if (chain == nullptr) - continue; - - switch (chain->state) - { - case scope_state::passed: break; - case scope_state::failed: throw failed (); - default: assert (false); - } - } - } - - exec_lines ( - g->tdown_.begin (), g->tdown_.end (), li, command_type::teardown); - } - else - assert (false); - - runner_->leave (*scope_, scope_->end_loc_); - - scope_->state = scope_state::passed; - } - - bool parser:: - exec_lines (lines::iterator i, lines::iterator e, - size_t& li, - command_type ct) - { - try - { - token t; - type tt; - - for (; i != e; ++i) - { - line& ln (*i); - line_type lt (ln.type); - - assert (path_ == nullptr); - - // Set the tokens and start playing. - // - replay_data (move (ln.tokens)); - - // We don't really need to change the mode since we already know - // the line type. - // - next (t, tt); - const location ll (get_location (t)); - - switch (lt) - { - case line_type::var: - { - // Parse. - // - string name (move (t.value)); - - next (t, tt); - type kind (tt); // Assignment kind. - - value rhs (parse_variable_line (t, tt)); - - if (tt == type::semi) - next (t, tt); - - assert (tt == type::newline); - - // Assign. - // - const variable& var (*ln.var); - - value& lhs (kind == type::assign - ? scope_->assign (var) - : scope_->append (var)); - - build2::parser::apply_value_attributes ( - &var, lhs, move (rhs), kind); - - // If we changes any of the test.* values, then reset the $*, - // $N special aliases. - // - if (var.name == script_->test_var.name || - var.name == script_->options_var.name || - var.name == script_->arguments_var.name || - var.name == script_->redirects_var.name || - var.name == script_->cleanups_var.name) - { - scope_->reset_special (); - } - - replay_stop (); - break; - } - case line_type::cmd: - { - // We use the 0 index to signal that this is the only command. - // Note that we only do this for test commands. - // - if (ct == command_type::test && li == 0) - { - lines::iterator j (i); - for (++j; j != e && j->type == line_type::var; ++j) ; - - if (j != e) // We have another command. - ++li; - } - else - ++li; - - command_expr ce (parse_command_line (t, tt)); - runner_->run (*scope_, ce, ct, li, ll); - - replay_stop (); - break; - } - case line_type::cmd_if: - case line_type::cmd_ifn: - case line_type::cmd_elif: - case line_type::cmd_elifn: - case line_type::cmd_else: - { - next (t, tt); // Skip to start of command. - - bool take; - if (lt != line_type::cmd_else) - { - // Assume if-else always involves multiple commands. - // - command_expr ce (parse_command_line (t, tt)); - take = runner_->run_if (*scope_, ce, ++li, ll); - - if (lt == line_type::cmd_ifn || lt == line_type::cmd_elifn) - take = !take; - } - else - { - assert (tt == type::newline); - take = true; - } - - replay_stop (); - - // If end is true, then find the 'end' line. Otherwise, find - // the next if-else line. If skip is true then increment the - // command line index. - // - auto next = [e, &li] - (lines::iterator j, bool end, bool skip) -> lines::iterator - { - // We need to be aware of nested if-else chains. - // - size_t n (0); - - for (++j; j != e; ++j) - { - line_type lt (j->type); - - if (lt == line_type::cmd_if || - lt == line_type::cmd_ifn) - ++n; - - // If we are nested then we just wait until we get back - // to the surface. - // - if (n == 0) - { - switch (lt) - { - case line_type::cmd_elif: - case line_type::cmd_elifn: - case line_type::cmd_else: - if (end) break; - // Fall through. - case line_type::cmd_end: return j; - default: break; - } - } - - if (lt == line_type::cmd_end) - --n; - - if (skip) - { - // Note that we don't count else and end as commands. - // - switch (lt) - { - case line_type::cmd: - case line_type::cmd_if: - case line_type::cmd_ifn: - case line_type::cmd_elif: - case line_type::cmd_elifn: ++li; break; - default: break; - } - } - } - - assert (false); // Missing end. - return e; - }; - - // If we are taking this branch then we need to parse all the - // lines until the next if-else line and then skip all the - // lines until the end (unless next is already end). - // - // Otherwise, we need to skip all the lines until the next - // if-else line and then continue parsing. - // - if (take) - { - lines::iterator j (next (i, false, false)); // Next if-else. - if (!exec_lines (i + 1, j, li, ct)) - return false; - - i = j->type == line_type::cmd_end ? j : next (j, true, true); - } - else - { - i = next (i, false, true); - if (i->type != line_type::cmd_end) - --i; // Continue with this line (e.g., elif or else). - } - - break; - } - case line_type::cmd_end: - { - assert (false); - } - } - } - - return true; - } - catch (const exit_scope& e) - { - // Bail out if the scope is exited with the failure status. Otherwise - // leave the scope normally. - // - if (!e.status) - throw failed (); - - replay_stop (); - return false; - } - } - - // - // The rest. - // - - lookup parser:: - lookup_variable (name&& qual, string&& name, const location& loc) - { - assert (!pre_parse_); - - if (!qual.empty ()) - fail (loc) << "qualified variable name"; - - // If we have no scope (happens when pre-parsing directives), then we - // only look for buildfile variables. - // - // Otherwise, every variable that is ever set in a script has been - // pre-entered during pre-parse or introduced with the set builtin - // during test execution. Which means that if one is not found in the - // script pool then it can only possibly be set in the buildfile. - // - // Note that we need to acquire the variable pool lock. The pool can - // be changed from multiple threads by the set builtin. The obtained - // variable pointer can safelly be used with no locking as the variable - // pool is an associative container (underneath) and we are only adding - // new variables into it. - // - const variable* pvar (nullptr); - - if (scope_ != nullptr) - { - slock sl (script_->var_pool_mutex); - pvar = script_->var_pool.find (name); - } - - return pvar != nullptr - ? scope_->find (*pvar) - : script_->find_in_buildfile (name); - } - - size_t parser:: - quoted () const - { - size_t r (0); - - if (replay_ != replay::play) - r = lexer_->quoted (); - else - { - // Examine tokens we have replayed since last reset. - // - for (size_t i (replay_quoted_); i != replay_i_; ++i) - if (replay_data_[i].token.qtype != quote_type::unquoted) - ++r; - } - - return r; - } - - void parser:: - reset_quoted (token& cur) - { - if (replay_ != replay::play) - lexer_->reset_quoted (cur.qtype != quote_type::unquoted ? 1 : 0); - else - { - replay_quoted_ = replay_i_ - 1; - - // Must be the same token. - // - assert (replay_data_[replay_quoted_].token.qtype == cur.qtype); - } - } - - const string& parser:: - insert_id (string id, location l) - { - auto p (id_map_->emplace (move (id), move (l))); - - if (!p.second) - fail (l) << "duplicate id " << p.first->first << - info (p.first->second) << "previously used here"; - - return p.first->first; - } - - void parser:: - set_lexer (lexer* l) - { - lexer_ = l; - base_parser::lexer_ = l; - } - - void parser:: - apply_value_attributes (const variable* var, - value& lhs, - value&& rhs, - const string& attributes, - token_type kind, - const path& name) - { - path_ = &name; - - istringstream is (attributes); - lexer l (is, name, lexer_mode::attribute); - set_lexer (&l); - - token t; - type tt; - next (t, tt); - - if (tt != type::lsbrace && tt != type::eos) - fail (t) << "expected '[' instead of " << t; - - attributes_push (t, tt, true); - - if (tt != type::eos) - fail (t) << "trailing junk after ']'"; - - build2::parser::apply_value_attributes (var, lhs, move (rhs), kind); - } - - // parser::parsed_doc - // - parser::parsed_doc:: - parsed_doc (string s, uint64_t l, uint64_t c) - : str (move (s)), re (false), end_line (l), end_column (c) - { - } - - parser::parsed_doc:: - parsed_doc (regex_lines&& r, uint64_t l, uint64_t c) - : regex (move (r)), re (true), end_line (l), end_column (c) - { - } - - parser::parsed_doc:: - parsed_doc (parsed_doc&& d) - : re (d.re), end_line (d.end_line), end_column (d.end_column) - { - if (re) - new (®ex) regex_lines (move (d.regex)); - else - new (&str) string (move (d.str)); - } - - parser::parsed_doc:: - ~parsed_doc () - { - if (re) - regex.~regex_lines (); - else - str.~string (); - } - } - } -} diff --git a/build2/test/script/parser.hxx b/build2/test/script/parser.hxx deleted file mode 100644 index dfa1126..0000000 --- a/build2/test/script/parser.hxx +++ /dev/null @@ -1,250 +0,0 @@ -// file : build2/test/script/parser.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_SCRIPT_PARSER_HXX -#define BUILD2_TEST_SCRIPT_PARSER_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/parser.hxx> -#include <libbuild2/diagnostics.hxx> - -#include <build2/test/script/token.hxx> -#include <build2/test/script/script.hxx> - -namespace build2 -{ - namespace test - { - namespace script - { - class lexer; - class runner; - - class parser: protected build2::parser - { - // Pre-parse. Issue diagnostics and throw failed in case of an error. - // - public: - void - pre_parse (script&); - - void - pre_parse (istream&, script&); - - // Helpers. - // - // Parse attribute string and perform attribute-guided assignment. - // Issue diagnostics and throw failed in case of an error. - // - void - apply_value_attributes (const variable*, // Optional. - value& lhs, - value&& rhs, - const string& attributes, - token_type assign_kind, - const path& name); // For diagnostics. - - // Recursive descent parser. - // - // Usually (but not always) parse functions receive the token/type - // from which it should start consuming and in return the token/type - // should contain the first token that has not been consumed. - // - // Functions that are called parse_*() rather than pre_parse_*() are - // used for both stages. - // - protected: - bool - pre_parse_demote_group_scope (unique_ptr<scope>&); - - token - pre_parse_scope_body (); - - unique_ptr<group> - pre_parse_scope_block (token&, token_type&, const string&); - - bool - pre_parse_line (token&, token_type&, - optional<description>&, - lines* = nullptr, - bool one = false); - - bool - pre_parse_if_else (token&, token_type&, - optional<description>&, - lines&); - - bool - pre_parse_if_else_scope (token&, token_type&, - optional<description>&, - lines&); - - bool - pre_parse_if_else_command (token&, token_type&, - optional<description>&, - lines&); - - void - pre_parse_directive (token&, token_type&); - - void - pre_parse_include_line (names, location); - - description - pre_parse_leading_description (token&, token_type&); - - description - parse_trailing_description (token&, token_type&); - - value - parse_variable_line (token&, token_type&); - - command_expr - parse_command_line (token&, token_type&); - - // Ordered sequence of here-document redirects that we can expect to - // see after the command line. - // - struct here_redirect - { - size_t expr; // Index in command_expr. - size_t pipe; // Index in command_pipe. - int fd; // Redirect fd (0 - in, 1 - out, 2 - err). - }; - - struct here_doc - { - // Redirects that share here_doc. Most of the time we will have no - // more than 2 (2 - for the roundtrip test cases). - // - small_vector<here_redirect, 2> redirects; - - string end; - bool literal; // Literal (single-quote). - string modifiers; - - // Regex introducer ('\0' if not a regex, so can be used as bool). - // - char regex; - - // Regex global flags. Meaningful if regex != '\0'. - // - string regex_flags; - }; - using here_docs = vector<here_doc>; - - pair<command_expr, here_docs> - parse_command_expr (token&, token_type&); - - command_exit - parse_command_exit (token&, token_type&); - - void - parse_here_documents (token&, token_type&, - pair<command_expr, here_docs>&); - - struct parsed_doc - { - union - { - string str; // Here-document literal. - regex_lines regex; // Here-document regex. - }; - - bool re; // True if regex. - uint64_t end_line; // Here-document end marker location. - uint64_t end_column; - - parsed_doc (string, uint64_t line, uint64_t column); - parsed_doc (regex_lines&&, uint64_t line, uint64_t column); - parsed_doc (parsed_doc&&); // Note: move constuctible-only type. - ~parsed_doc (); - }; - - parsed_doc - parse_here_document (token&, token_type&, - const string&, - const string& mode, - char re_intro); // '\0' if not a regex. - - // Execute. Issue diagnostics and throw failed in case of an error. - // - public: - void - execute (script& s, runner& r); - - void - execute (scope&, script&, runner&); - - protected: - void - exec_scope_body (); - - // Return false if the execution of the scope should be terminated - // with the success status (e.g., as a result of encountering the exit - // builtin). For unsuccessful termination the failed exception should - // be thrown. - // - bool - exec_lines (lines::iterator, lines::iterator, size_t&, command_type); - - // Customization hooks. - // - protected: - virtual lookup - lookup_variable (name&&, string&&, const location&) override; - - // Number of quoted tokens since last reset. Note that this includes - // the peeked token, if any. - // - protected: - size_t - quoted () const; - - void - reset_quoted (token& current); - - size_t replay_quoted_; - - // Insert id into the id map checking for duplicates. - // - protected: - const string& - insert_id (string, location); - - // Set lexer pointers for both the current and the base classes. - // - protected: - void - set_lexer (lexer* l); - - protected: - using base_parser = build2::parser; - - script* script_; - - // Pre-parse state. - // - using id_map = std::unordered_map<string, location>; - using include_set = std::set<path>; - - group* group_; - id_map* id_map_; - include_set* include_set_; // Testscripts already included in this - // scope. Must be absolute and normalized. - lexer* lexer_; - string id_prefix_; // Auto-derived id prefix. - - // Execute state. - // - runner* runner_; - scope* scope_; - }; - } - } -} - -#endif // BUILD2_TEST_SCRIPT_PARSER_HXX diff --git a/build2/test/script/parser.test.cxx b/build2/test/script/parser.test.cxx deleted file mode 100644 index 352941a..0000000 --- a/build2/test/script/parser.test.cxx +++ /dev/null @@ -1,245 +0,0 @@ -// file : build2/test/script/parser.test.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <cassert> -#include <iostream> - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/target.hxx> -#include <libbuild2/context.hxx> // reset() -#include <libbuild2/scheduler.hxx> - -#include <build2/test/target.hxx> - -#include <build2/test/script/token.hxx> -#include <build2/test/script/parser.hxx> -#include <build2/test/script/runner.hxx> - -using namespace std; - -namespace build2 -{ - namespace test - { - namespace script - { - // Here we assume we are running serially. - // - class print_runner: public runner - { - public: - print_runner (bool scope, bool id, bool line) - : scope_ (scope), id_ (id), line_ (line) {} - - virtual bool - test (scope&) const override - { - return true; - } - - virtual void - enter (scope& s, const location&) override - { - if (s.desc) - { - const auto& d (*s.desc); - - if (!d.id.empty ()) - cout << ind_ << ": id:" << d.id << endl; - - if (!d.summary.empty ()) - cout << ind_ << ": sm:" << d.summary << endl; - - if (!d.details.empty ()) - { - if (!d.id.empty () || !d.summary.empty ()) - cout << ind_ << ":" << endl; // Blank. - - const auto& s (d.details); - for (size_t b (0), e (0), n; e != string::npos; b = e + 1) - { - e = s.find ('\n', b); - n = ((e != string::npos ? e : s.size ()) - b); - - cout << ind_ << ':'; - if (n != 0) - { - cout << ' '; - cout.write (s.c_str () + b, static_cast<streamsize> (n)); - } - cout << endl; - } - } - } - - if (scope_) - { - cout << ind_ << "{"; - - if (id_ && !s.id_path.empty ()) // Skip empty root scope id. - cout << " # " << s.id_path.string (); - - cout << endl; - - ind_ += " "; - } - } - - virtual void - run (scope&, - const command_expr& e, command_type t, - size_t i, - const location&) override - { - const char* s (nullptr); - - switch (t) - { - case command_type::test: s = ""; break; - case command_type::setup: s = "+"; break; - case command_type::teardown: s = "-"; break; - } - - cout << ind_ << s << e; - - if (line_) - cout << " # " << i; - - cout << endl; - } - - virtual bool - run_if (scope&, - const command_expr& e, - size_t i, - const location&) override - { - cout << ind_ << "? " << e; - - if (line_) - cout << " # " << i; - - cout << endl; - - return e.back ().pipe.back ().program.string () == "true"; - } - - virtual void - leave (scope&, const location&) override - { - if (scope_) - { - ind_.resize (ind_.size () - 2); - cout << ind_ << "}" << endl; - } - } - - private: - bool scope_; - bool id_; - bool line_; - string ind_; - }; - - // Usage: argv[0] [-s] [-i] [-l] [<testscript-name>] - // - int - main (int argc, char* argv[]) - { - tracer trace ("main"); - - // Fake build system driver, default verbosity. - // - init_diag (1); - init (argv[0]); - sched.startup (1); // Serial execution. - reset (strings ()); // No command line variables. - - bool scope (false); - bool id (false); - bool line (false); - path name; - - for (int i (1); i != argc; ++i) - { - string a (argv[i]); - - if (a == "-s") - scope = true; - else if (a == "-i") - id = true; - else if (a == "-l") - line = true; - else - { - name = path (move (a)); - break; - } - } - - if (name.empty ()) - name = path ("testscript"); - - assert (!id || scope); // Id can only be printed with scope. - - try - { - cin.exceptions (istream::failbit | istream::badbit); - - // Enter mock targets. Use fixed names and paths so that we can use - // them in expected results. Strictly speaking target paths should - // be absolute. However, the testscript implementation doesn't - // really care. - // - file& tt ( - targets.insert<file> (work, - dir_path (), - "driver", - string (), - trace)); - - value& v ( - tt.assign ( - var_pool.rw ().insert<target_triplet> ( - "test.target", variable_visibility::project))); - - v = cast<target_triplet> ((*global_scope)["build.host"]); - - testscript& st ( - targets.insert<testscript> (work, - dir_path (), - name.leaf ().base ().string (), - name.leaf ().extension (), - trace)); - - tt.path (path ("driver")); - st.path (name); - - // Parse and run. - // - parser p; - script s (tt, st, dir_path (work) /= "test-driver"); - p.pre_parse (cin, s); - - print_runner r (scope, id, line); - p.execute (s, r); - } - catch (const failed&) - { - return 1; - } - - return 0; - } - } - } -} - -int -main (int argc, char* argv[]) -{ - return build2::test::script::main (argc, argv); -} diff --git a/build2/test/script/regex.cxx b/build2/test/script/regex.cxx deleted file mode 100644 index bbd1738..0000000 --- a/build2/test/script/regex.cxx +++ /dev/null @@ -1,440 +0,0 @@ -// file : build2/test/script/regex.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <locale> - -#include <build2/test/script/regex.hxx> - -using namespace std; - -namespace build2 -{ - namespace test - { - namespace script - { - namespace regex - { - static_assert (alignof (char_string) % 4 == 0, - "unexpected char_string alignment"); - - static_assert (alignof (char_regex) % 4 == 0, - "unexpected char_regex alignment"); - - static_assert (sizeof (uintptr_t) > sizeof (int16_t), - "unexpected uintptr_t size"); - - const line_char line_char::nul (0); - const line_char line_char::eof (-1); - - // line_char - // - // We package the special character into uintptr_t with the following - // steps: - // - // - narrow down int value to int16_t (preserves all the valid values) - // - // - convert to uint16_t (bitwise representation stays the same, but no - // need to bother with signed value widening, leftmost bits loss on - // left shift, etc) - // - // - convert to uintptr_t (storage type) - // - // - shift left by two bits (the operation is fully reversible as - // uintptr_t is wider then uint16_t) - // - line_char:: - line_char (int c) - : data_ ( - (static_cast <uintptr_t> ( - static_cast<uint16_t> ( - static_cast<int16_t> (c))) << 2) | - static_cast <uintptr_t> (line_type::special)) - { - // @@ How can we allow anything for basic_regex but only subset - // for our own code? - // - const char ex[] = "pn\n\r"; - - assert (c == 0 || // Null character. - - // EOF. Note that is also passed by msvcrt as _Meta_eos - // enum value. - // - c == -1 || - - // libstdc++ line/paragraph separators. - // - c == u'\u2028' || c == u'\u2029' || - - (c > 0 && c <= 255 && ( - // Supported regex special characters. - // - syntax (c) || - - // libstdc++ look-ahead tokens, newline chars. - // - string::traits_type::find (ex, 4, c) != nullptr))); - } - - line_char:: - line_char (const char_string& s, line_pool& p) - : line_char (&(*p.strings.emplace (s).first)) - { - } - - line_char:: - line_char (char_string&& s, line_pool& p) - : line_char (&(*p.strings.emplace (move (s)).first)) - { - } - - line_char:: - line_char (char_regex r, line_pool& p) - // Note: in C++17 can write as p.regexes.emplace_front(move (r)) - // - : line_char (&(*p.regexes.emplace (p.regexes.begin (), move (r)))) - { - } - - bool - line_char::syntax (char c) - { - return string::traits_type::find ( - "()|.*+?{}\\0123456789,=!", 23, c) != nullptr; - } - - bool - operator== (const line_char& l, const line_char& r) - { - line_type lt (l.type ()); - line_type rt (r.type ()); - - if (lt == rt) - { - bool res (true); - - switch (lt) - { - case line_type::special: res = l.special () == r.special (); break; - case line_type::regex: assert (false); break; - - // Note that we use pointers (rather than vales) comparison - // assuming that the strings must belong to the same pool. - // - case line_type::literal: res = l.literal () == r.literal (); break; - } - - return res; - } - - // Match literal with regex. - // - if (lt == line_type::literal && rt == line_type::regex) - return regex_match (*l.literal (), *r.regex ()); - else if (rt == line_type::literal && lt == line_type::regex) - return regex_match (*r.literal (), *l.regex ()); - - return false; - } - - bool - operator< (const line_char& l, const line_char& r) - { - if (l == r) - return false; - - line_type lt (l.type ()); - line_type rt (r.type ()); - - if (lt != rt) - return lt < rt; - - bool res (false); - - switch (lt) - { - case line_type::special: res = l.special () < r.special (); break; - case line_type::literal: res = *l.literal () < *r.literal (); break; - case line_type::regex: assert (false); break; - } - - return res; - } - - // line_char_locale - // - - // An exemplar locale with the std::ctype<line_char> facet. It is - // used for the subsequent line char locale objects creation (see - // below) which normally ends up with a shallow copy of a reference- - // counted object. - // - // Note that creating the line char locales from the exemplar is not - // merely an optimization: there is a data race in the libstdc++ (at - // least as of GCC 9.1) implementation of the locale(const locale&, - // Facet*) constructor (bug #91057). - // - // Also note that we install the facet in init() rather than during - // the object creation to avoid a race with the std::locale-related - // global variables initialization. - // - static locale line_char_locale_exemplar; - - void - init () - { - line_char_locale_exemplar = - locale (locale (), - new std::ctype<line_char> ()); // Hidden by ctype bitmask. - } - - line_char_locale:: - line_char_locale () - : locale (line_char_locale_exemplar) - { - // Make sure init() has been called. - // - // Note: has_facet() is hidden by a private function in libc++. - // - assert (std::has_facet<std::ctype<line_char>> (*this)); - } - - // char_regex - // - // Transform regex according to the extended flags {idot}. If regex is - // malformed then keep transforming, so the resulting string is - // malformed the same way. We expect the error to be reported by the - // char_regex ctor. - // - static string - transform (const string& s, char_flags f) - { - assert ((f & char_flags::idot) != char_flags::none); - - string r; - bool escape (false); - bool cclass (false); - - for (char c: s) - { - // Inverse escaping for a dot which is out of the char class - // brackets. - // - bool inverse (c == '.' && !cclass); - - // Handle the escape case. Note that we delay adding the backslash - // since we may have to inverse things. - // - if (escape) - { - if (!inverse) - r += '\\'; - - r += c; - escape = false; - - continue; - } - else if (c == '\\') - { - escape = true; - continue; - } - - // Keep track of being inside the char class brackets, escape if - // inversion. Note that we never inverse square brackets. - // - if (c == '[' && !cclass) - cclass = true; - else if (c == ']' && cclass) - cclass = false; - else if (inverse) - r += '\\'; - - r += c; - } - - if (escape) // Regex is malformed but that's not our problem. - r += '\\'; - - return r; - } - - static char_regex::flag_type - to_std_flags (char_flags f) - { - // Note that ECMAScript flag is implied in the absense of a grammar - // flag. - // - return (f & char_flags::icase) != char_flags::none - ? char_regex::icase - : char_regex::flag_type (); - } - - char_regex:: - char_regex (const char_string& s, char_flags f) - : base_type ((f & char_flags::idot) != char_flags::none - ? transform (s, f) - : s, - to_std_flags (f)) - { - } - } - } - } -} - -namespace std -{ - using namespace build2::test::script::regex; - - // char_traits<line_char> - // - line_char* char_traits<line_char>:: - assign (char_type* s, size_t n, char_type c) - { - for (size_t i (0); i != n; ++i) - s[i] = c; - return s; - } - - line_char* char_traits<line_char>:: - move (char_type* d, const char_type* s, size_t n) - { - if (n > 0 && d != s) - { - // If d < s then it can't be in [s, s + n) range and so using copy() is - // safe. Otherwise d + n is out of (s, s + n] range and so using - // copy_backward() is safe. - // - if (d < s) - std::copy (s, s + n, d); // Hidden by char_traits<line_char>::copy(). - else - copy_backward (s, s + n, d + n); - } - - return d; - } - - line_char* char_traits<line_char>:: - copy (char_type* d, const char_type* s, size_t n) - { - std::copy (s, s + n, d); // Hidden by char_traits<line_char>::copy(). - return d; - } - - int char_traits<line_char>:: - compare (const char_type* s1, const char_type* s2, size_t n) - { - for (size_t i (0); i != n; ++i) - { - if (s1[i] < s2[i]) - return -1; - else if (s2[i] < s1[i]) - return 1; - } - - return 0; - } - - size_t char_traits<line_char>:: - length (const char_type* s) - { - size_t i (0); - while (s[i] != char_type::nul) - ++i; - - return i; - } - - const line_char* char_traits<line_char>:: - find (const char_type* s, size_t n, const char_type& c) - { - for (size_t i (0); i != n; ++i) - { - if (s[i] == c) - return s + i; - } - - return nullptr; - } - - // ctype<line_char> - // - locale::id ctype<line_char>::id; - - const line_char* ctype<line_char>:: - is (const char_type* b, const char_type* e, mask* m) const - { - while (b != e) - { - const char_type& c (*b++); - - *m++ = c.type () == line_type::special && c.special () >= 0 && - build2::digit (static_cast<char> (c.special ())) - ? digit - : 0; - } - - return e; - } - - const line_char* ctype<line_char>:: - scan_is (mask m, const char_type* b, const char_type* e) const - { - for (; b != e; ++b) - { - if (is (m, *b)) - return b; - } - - return e; - } - - const line_char* ctype<line_char>:: - scan_not (mask m, const char_type* b, const char_type* e) const - { - for (; b != e; ++b) - { - if (!is (m, *b)) - return b; - } - - return e; - } - - const char* ctype<line_char>:: - widen (const char* b, const char* e, char_type* c) const - { - while (b != e) - *c++ = widen (*b++); - - return e; - } - - const line_char* ctype<line_char>:: - narrow (const char_type* b, const char_type* e, char def, char* c) const - { - while (b != e) - *c++ = narrow (*b++, def); - - return e; - } - - // regex_traits<line_char> - // - int regex_traits<line_char>:: - value (char_type c, int radix) const - { - assert (radix == 8 || radix == 10 || radix == 16); - - if (c.type () != line_type::special) - return -1; - - const char digits[] = "0123456789ABCDEF"; - const char* d (string::traits_type::find (digits, radix, c.special ())); - return d != nullptr ? static_cast<int> (d - digits) : -1; - } -} diff --git a/build2/test/script/regex.hxx b/build2/test/script/regex.hxx deleted file mode 100644 index 33a4cba..0000000 --- a/build2/test/script/regex.hxx +++ /dev/null @@ -1,703 +0,0 @@ -// file : build2/test/script/regex.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_SCRIPT_REGEX_HXX -#define BUILD2_TEST_SCRIPT_REGEX_HXX - -#include <list> -#include <regex> -#include <locale> -#include <string> // basic_string -#include <type_traits> // make_unsigned, enable_if, is_* -#include <unordered_set> - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -namespace build2 -{ - namespace test - { - namespace script - { - namespace regex - { - using char_string = std::basic_string<char>; - - enum class char_flags: uint16_t - { - icase = 0x1, // Case-insensitive match. - idot = 0x2, // Invert '.' escaping. - - none = 0 - }; - - // Restricts valid standard flags to just {icase}, extends with custom - // flags {idot}. - // - class char_regex: public std::basic_regex<char> - { - public: - using base_type = std::basic_regex<char>; - - char_regex (const char_string&, char_flags = char_flags::none); - }; - - // Newlines are line separators and are not part of the line: - // - // line<newline>line<newline> - // - // Specifically, this means that a customary trailing newline creates a - // trailing blank line. - // - // All characters can inter-compare (though there cannot be regex - // characters in the output, only in line_regex). - // - // Note that we assume that line_regex and the input to regex_match() - // use the same pool. - // - struct line_pool - { - // Note that we assume the pool can be moved without invalidating - // pointers to any already pooled entities. - // - std::unordered_set<char_string> strings; - std::list<char_regex> regexes; - }; - - enum class line_type - { - special, - literal, - regex - }; - - struct line_char - { - // Steal last two bits from the pointer to store the type. - // - private: - std::uintptr_t data_; - - public: - line_type - type () const {return static_cast<line_type> (data_ & 0x3);} - - int - special () const - { - // Stored as (shifted) int16_t. Perform steps reversed to those - // that are described in the comment for the corresponding ctor. - // Note that the intermediate cast to uint16_t is required to - // portably preserve the -1 special character. - // - return static_cast<int16_t> (static_cast<uint16_t> (data_ >> 2)); - } - - const char_string* - literal () const - { - // Note that 2 rightmost bits are used for packaging line_char - // type. Read the comment for the corresponding ctor for details. - // - return reinterpret_cast<const char_string*> ( - data_ & ~std::uintptr_t (0x3)); - } - - const char_regex* - regex () const - { - // Note that 2 rightmost bits are used for packaging line_char - // type. Read the comment for the corresponding ctor for details. - // - return reinterpret_cast<const char_regex*> ( - data_ & ~std::uintptr_t (0x3)); - } - - static const line_char nul; - static const line_char eof; - - // Note: creates an uninitialized value. - // - line_char () = default; - - // Create a special character. The argument value must be one of the - // following ones: - // - // 0 (nul character) - // -1 (EOF) - // [()|.*+?{}\0123456789,=!] (excluding []) - // - // Note that the constructor is implicit to allow basic_regex to - // implicitly construct line_chars from special char literals (in - // particular libstdc++ appends them to an internal line_string). - // - // Also note that we extend the valid characters set (see above) with - // 'p', 'n' (used by libstdc++ for positive/negative look-ahead - // tokens representation), and '\n', '\r', u'\u2028', u'\u2029' (used - // by libstdc++ for newline/newparagraph matching). - // - line_char (int); - - // Create a literal character. - // - // Don't copy string if already pooled. - // - explicit - line_char (const char_string&, line_pool&); - - explicit - line_char (char_string&&, line_pool&); - - explicit - line_char (const char_string* s) // Assume already pooled. - // - // Steal two bits from the pointer to package line_char type. - // Assume (and statically assert) that char_string address is a - // multiple of four. - // - : data_ (reinterpret_cast <std::uintptr_t> (s) | - static_cast <std::uintptr_t> (line_type::literal)) {} - - // Create a regex character. - // - explicit - line_char (char_regex, line_pool&); - - explicit - line_char (const char_regex* r) // Assume already pooled. - // - // Steal two bits from the pointer to package line_char type. - // Assume (and statically assert) that char_regex address is a - // multiple of four. - // - : data_ (reinterpret_cast <std::uintptr_t> (r) | - static_cast <std::uintptr_t> (line_type::regex)) {} - - // Provide basic_regex with the ability to use line_char in a context - // where a char value is expected (e.g., as a function argument). - // - // libstdc++ seems to cast special line_chars only (and such a - // conversion is meanigfull). - // - // msvcrt casts line_chars of arbitrary types instead. The only - // reasonable strategy is to return a value that differs from any - // other that can be encountered in a regex expression and so will - // unlikelly be misinterpreted. - // - operator char () const - { - return type () == line_type::special ? special () : '\a'; // BELL. - } - - // Return true if the character is a syntax (special) one. - // - static bool - syntax (char); - - // Provide basic_regex (such as from msvcrt) with the ability to - // explicitly cast line_chars to implementation-specific enums. - // - template <typename T> - explicit - operator T () const - { - assert (type () == line_type::special); - return static_cast<T> (special ()); - } - }; - - // Perform "deep" characters comparison (for example match literal - // character with a regex character), rather than just compare them - // literally. At least one argument must be of a type other than regex - // as there is no operator==() defined to compare regexes. Characters - // of the literal type must share the same pool (strings are compared - // by pointers not by values). - // - bool - operator== (const line_char&, const line_char&); - - // Return false if arguments are equal (operator==() returns true). - // Otherwise if types are different return the value implying that - // special < literal < regex. If types are special or literal return - // the result of the respective characters or strings comparison. At - // least one argument must be of a type other than regex as there is no - // operator<() defined to compare regexes. - // - // While not very natural operation for the class we have, we have to - // provide some meaningfull semantics for such a comparison as it is - // required by the char_traits<line_char> specialization. While we - // could provide it right in that specialization, let's keep it here - // for basic_regex implementations that potentially can compare - // line_chars as they compare them with expressions of other types (see - // below). - // - bool - operator< (const line_char&, const line_char&); - - inline bool - operator!= (const line_char& l, const line_char& r) - { - return !(l == r); - } - - inline bool - operator<= (const line_char& l, const line_char& r) - { - return l < r || l == r; - } - - // Provide basic_regex (such as from msvcrt) with the ability to - // compare line_char to a value of an integral or - // implementation-specific enum type. In the absense of the following - // template operators, such a comparisons would be ambigious for - // integral types (given that there are implicit conversions - // int->line_char and line_char->char) and impossible for enums. - // - // Note that these == and < operators can succeed only for a line_char - // of the special type. For other types they always return false. That - // in particular leads to the following case: - // - // (lc != c) != (lc < c || c < lc). - // - // Note that we can not assert line_char is of the special type as - // basic_regex (such as from libc++) may need the ability to check if - // arbitrary line_char belongs to some special characters range (like - // ['0', '9']). - // - template <typename T> - struct line_char_cmp - : public std::enable_if<std::is_integral<T>::value || - (std::is_enum<T>::value && - !std::is_same<T, char_flags>::value)> {}; - - template <typename T, typename = typename line_char_cmp<T>::type> - bool - operator== (const line_char& l, const T& r) - { - return l.type () == line_type::special && - static_cast<T> (l.special ()) == r; - } - - template <typename T, typename = typename line_char_cmp<T>::type> - bool - operator== (const T& l, const line_char& r) - { - return r.type () == line_type::special && - static_cast<T> (r.special ()) == l; - } - - template <typename T, typename = typename line_char_cmp<T>::type> - bool - operator!= (const line_char& l, const T& r) - { - return !(l == r); - } - - template <typename T, typename = typename line_char_cmp<T>::type> - bool - operator!= (const T& l, const line_char& r) - { - return !(l == r); - } - - template <typename T, typename = typename line_char_cmp<T>::type> - bool - operator< (const line_char& l, const T& r) - { - return l.type () == line_type::special && - static_cast<T> (l.special ()) < r; - } - - template <typename T, typename = typename line_char_cmp<T>::type> - bool - operator< (const T& l, const line_char& r) - { - return r.type () == line_type::special && - l < static_cast<T> (r.special ()); - } - - template <typename T, typename = typename line_char_cmp<T>::type> - inline bool - operator<= (const line_char& l, const T& r) - { - return l < r || l == r; - } - - template <typename T, typename = typename line_char_cmp<T>::type> - inline bool - operator<= (const T& l, const line_char& r) - { - return l < r || l == r; - } - - using line_string = std::basic_string<line_char>; - - // Locale that has ctype<line_char> facet installed. Used in the - // regex_traits<line_char> specialization (see below). - // - class line_char_locale: public std::locale - { - public: - // Create a copy of the global C++ locale. - // - line_char_locale (); - }; - - // Initialize the testscript regex global state. Should be called once - // prior to creating objects of types from this namespace. Note: not - // thread-safe. - // - void - init (); - } - } - } -} - -// Standard template specializations for line_char that are required for the -// basic_regex<line_char> instantiation. -// -namespace std -{ - template <> - class char_traits<build2::test::script::regex::line_char> - { - public: - using char_type = build2::test::script::regex::line_char; - using int_type = char_type; - using off_type = char_traits<char>::off_type; - using pos_type = char_traits<char>::pos_type; - using state_type = char_traits<char>::state_type; - - static void - assign (char_type& c1, const char_type& c2) {c1 = c2;} - - static char_type* - assign (char_type*, size_t, char_type); - - // Note that eq() and lt() are not constexpr (as required by C++11) - // because == and < operators for char_type are not constexpr. - // - static bool - eq (const char_type& l, const char_type& r) {return l == r;} - - static bool - lt (const char_type& l, const char_type& r) {return l < r;} - - static char_type* - move (char_type*, const char_type*, size_t); - - static char_type* - copy (char_type*, const char_type*, size_t); - - static int - compare (const char_type*, const char_type*, size_t); - - static size_t - length (const char_type*); - - static const char_type* - find (const char_type*, size_t, const char_type&); - - static constexpr char_type - to_char_type (const int_type& c) {return c;} - - static constexpr int_type - to_int_type (const char_type& c) {return int_type (c);} - - // Note that the following functions are not constexpr (as required by - // C++11) because their return expressions are not constexpr. - // - static bool - eq_int_type (const int_type& l, const int_type& r) {return l == r;} - - static int_type eof () {return char_type::eof;} - - static int_type - not_eof (const int_type& c) - { - return c != char_type::eof ? c : char_type::nul; - } - }; - - // ctype<> must be derived from both ctype_base and locale::facet (the later - // supports ref-counting used by the std::locale implementation internally). - // - // msvcrt for some reason also derives ctype_base from locale::facet which - // produces "already a base-class" warning and effectivelly breaks the - // reference counting. So we derive from ctype_base only in this case. - // - template <> - class ctype<build2::test::script::regex::line_char>: public ctype_base -#if !defined(_MSC_VER) || _MSC_VER >= 2000 - , public locale::facet -#endif - { - // Used by the implementation only. - // - using line_type = build2::test::script::regex::line_type; - - public: - using char_type = build2::test::script::regex::line_char; - - static locale::id id; - -#if !defined(_MSC_VER) || _MSC_VER >= 2000 - explicit - ctype (size_t refs = 0): locale::facet (refs) {} -#else - explicit - ctype (size_t refs = 0): ctype_base (refs) {} -#endif - - // While unnecessary, let's keep for completeness. - // - virtual - ~ctype () override = default; - - // The C++ standard requires the following functions to call their virtual - // (protected) do_*() counterparts that provide the real implementations. - // The only purpose for this indirection is to provide a user with the - // ability to customize existing (standard) ctype facets. As we do not - // provide such an ability, for simplicity we will omit the do_*() - // functions and provide the implementations directly. This should be safe - // as nobody except us could call those protected functions. - // - bool - is (mask m, char_type c) const - { - return m == - (c.type () == line_type::special && c.special () >= 0 && - build2::digit (static_cast<char> (c.special ())) - ? digit - : 0); - } - - const char_type* - is (const char_type*, const char_type*, mask*) const; - - const char_type* - scan_is (mask, const char_type*, const char_type*) const; - - const char_type* - scan_not (mask, const char_type*, const char_type*) const; - - char_type - toupper (char_type c) const {return c;} - - const char_type* - toupper (char_type*, const char_type* e) const {return e;} - - char_type - tolower (char_type c) const {return c;} - - const char_type* - tolower (char_type*, const char_type* e) const {return e;} - - char_type - widen (char c) const {return char_type (c);} - - const char* - widen (const char*, const char*, char_type*) const; - - char - narrow (char_type c, char def) const - { - return c.type () == line_type::special ? c.special () : def; - } - - const char_type* - narrow (const char_type*, const char_type*, char, char*) const; - }; - - // Note: the current application locale must be the POSIX one. Otherwise the - // behavior is undefined. - // - template <> - class regex_traits<build2::test::script::regex::line_char> - { - public: - using char_type = build2::test::script::regex::line_char; - using string_type = build2::test::script::regex::line_string; - using locale_type = build2::test::script::regex::line_char_locale; - using char_class_type = regex_traits<char>::char_class_type; - - // Workaround for msvcrt bugs. For some reason it assumes such a members - // to be present in a regex_traits specialization. - // -#if defined(_MSC_VER) && _MSC_VER < 2000 - static const ctype_base::mask _Ch_upper = ctype_base::upper; - static const ctype_base::mask _Ch_alpha = ctype_base::alpha; - - // Unsigned char_type. msvcrt statically asserts the _Uelem type is - // unsigned, so we specialize is_unsigned<line_char> as well (see below). - // - using _Uelem = char_type; -#endif - - regex_traits () = default; // Unnecessary but let's keep for completeness. - - static size_t - length (const char_type* p) {return string_type::traits_type::length (p);} - - char_type - translate (char_type c) const {return c;} - - // Case-insensitive matching is not supported by line_regex. So there is no - // reason for the function to be called. - // - char_type - translate_nocase (char_type c) const {assert (false); return c;} - - // Return a sort-key - the exact copy of [b, e). - // - template <typename I> - string_type - transform (I b, I e) const {return string_type (b, e);} - - // Return a case-insensitive sort-key. Case-insensitive matching is not - // supported by line_regex. So there is no reason for the function to be - // called. - // - template <typename I> - string_type - transform_primary (I b, I e) const - { - assert (false); - return string_type (b, e); - } - - // POSIX regex grammar and collating elements (e.g., [.tilde.]) in - // particular are not supported. So there is no reason for the function to - // be called. - // - template <typename I> - string_type - lookup_collatename (I, I) const {assert (false); return string_type ();} - - // Character classes (e.g., [:lower:]) are not supported. So there is no - // reason for the function to be called. - // - template <typename I> - char_class_type - lookup_classname (I, I, bool = false) const - { - assert (false); - return char_class_type (); - } - - // Return false as we don't support character classes (e.g., [:lower:]). - // - bool - isctype (char_type, char_class_type) const {return false;} - - int - value (char_type, int) const; - - // Return the locale passed as an argument as we do not expect anything - // other than POSIX locale, that we also assume to be imbued by default. - // - locale_type - imbue (locale_type l) {return l;} - - locale_type - getloc () const {return locale_type ();} - }; - - // We assume line_char to be an unsigned type and express that with the - // following specializations used by basic_regex implementations. - // - // libstdc++ defines unsigned CharT type (regex_traits template parameter) - // to use as an index in some internal cache regardless if the cache is used - // for this specialization (and the cache is used only if CharT is char). - // - template <> - struct make_unsigned<build2::test::script::regex::line_char> - { - using type = build2::test::script::regex::line_char; - }; - - // msvcrt assumes regex_traits<line_char>::_Uelem to be present (see above) - // and statically asserts it is unsigned. - // - // And starting from VC 16.1, is_unsigned_v is not implemented in terms of - // is_unsigned so we have to get deeper into the implementation details. - // -#if defined(_MSC_VER) && _MSC_VER >= 1921 - template <> - struct _Sign_base<build2::test::script::regex::line_char, false> - { - static constexpr bool _Signed = false; - static constexpr bool _Unsigned = true; - }; -#else - template <> - struct is_unsigned<build2::test::script::regex::line_char> - { - static const bool value = true; - }; -#endif - - // When used with libc++ the linker complains that it can't find - // __match_any_but_newline<line_char>::__exec() function. The problem is - // that the function is only specialized for char and wchar_t - // (LLVM bug #31409). As line_char has no notion of the newline character we - // specialize the class template to behave as the __match_any<line_char> - // instantiation does (that luckily has all the functions in place). - // -#if defined(_LIBCPP_VERSION) && _LIBCPP_VERSION <= 8000 - template <> - class __match_any_but_newline<build2::test::script::regex::line_char> - : public __match_any<build2::test::script::regex::line_char> - { - public: - using base = __match_any<build2::test::script::regex::line_char>; - using base::base; - }; -#endif -} - -namespace build2 -{ - namespace test - { - namespace script - { - namespace regex - { - class line_regex: public std::basic_regex<line_char> - { - public: - using base_type = std::basic_regex<line_char>; - - using base_type::base_type; - - line_regex () = default; - - // Move string regex together with the pool used to create it. - // - line_regex (line_string&& s, line_pool&& p) - // No move-string ctor for base_type, so emulate it. - // - : base_type (s), pool (move (p)) {s.clear ();} - - // Move constuctible/assignable-only type. - // - line_regex (line_regex&&) = default; - line_regex (const line_regex&) = delete; - line_regex& operator= (line_regex&&) = default; - line_regex& operator= (const line_regex&) = delete; - - public: - line_pool pool; - }; - } - } - } -} - -#include <build2/test/script/regex.ixx> - -#endif // BUILD2_TEST_SCRIPT_REGEX_HXX diff --git a/build2/test/script/regex.ixx b/build2/test/script/regex.ixx deleted file mode 100644 index a83f58f..0000000 --- a/build2/test/script/regex.ixx +++ /dev/null @@ -1,35 +0,0 @@ -// file : build2/test/script/regex.ixx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -namespace build2 -{ - namespace test - { - namespace script - { - namespace regex - { - inline char_flags - operator&= (char_flags& x, char_flags y) - { - return x = static_cast<char_flags> ( - static_cast<uint16_t> (x) & static_cast<uint16_t> (y)); - } - - inline char_flags - operator|= (char_flags& x, char_flags y) - { - return x = static_cast<char_flags> ( - static_cast<uint16_t> (x) | static_cast<uint16_t> (y)); - } - - inline char_flags - operator& (char_flags x, char_flags y) {return x &= y;} - - inline char_flags - operator| (char_flags x, char_flags y) {return x |= y;} - } - } - } -} diff --git a/build2/test/script/regex.test.cxx b/build2/test/script/regex.test.cxx deleted file mode 100644 index 1e48f97..0000000 --- a/build2/test/script/regex.test.cxx +++ /dev/null @@ -1,302 +0,0 @@ -// file : build2/test/script/regex.test.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <regex> -#include <type_traits> // is_pod, is_array - -#include <build2/test/script/regex.hxx> - -using namespace std; -using namespace build2::test::script::regex; - -int -main () -{ - using lc = line_char; - using ls = line_string; - using lr = line_regex; - using cf = char_flags; - using cr = char_regex; - - init (); // Initializes the testscript regex global state. - - // Test line_char. - // - { - static_assert (is_pod<lc>::value && !is_array<lc>::value, - "line_char must be char-like"); - - // Zero-initialed line_char should be the null-char as required by - // char_traits<>::length() specification. - // - assert (lc () == lc::nul); - - line_pool p; - - assert (lc::eof == -1); - assert (lc::nul == 0); - - enum meta {mn = 'n', mp = 'p'}; - - // Special roundtrip. - // - assert (lc ('0').special () == '0'); - assert (lc (0).special () == 0); - assert (lc (-1).special () == -1); - assert (lc ('p').special () == 'p'); - assert (lc (u'\u2028').special () == u'\u2028'); - - // Special comparison. - // - assert (lc ('0') == lc ('0')); - assert (lc ('0') == '0'); - assert (lc ('n') == mn); - assert (mn == static_cast<meta> (lc ('n'))); - - assert (lc ('0') != lc ('1')); - assert (lc ('0') != '1'); - assert (lc ('n') != mp); - assert (lc ('0') != lc ("0", p)); - assert (lc ('0') != lc (cr ("0"), p)); - - assert (lc ('0') < lc ('1')); - assert (lc ('0') < '1'); - assert (lc ('1') < lc ("0", p)); - assert (lc ('n') < mp); - - assert (lc ('0') <= '1'); - assert (lc ('0') <= lc ('1')); - assert (lc ('n') <= mn); - assert (lc ('1') <= lc ("0", p)); - - // Literal roundtrip. - // - assert (*lc ("abc", p).literal () == "abc"); - - // Literal comparison. - // - assert (lc ("a", p) == lc ("a", p)); - assert (lc ("a", p).literal () == lc ("a", p).literal ()); - assert (char (lc ("a", p)) == '\a'); - - assert (lc ("a", p) != lc ("b", p)); - assert (!(lc ("a", p) != lc (cr ("a"), p))); - assert (lc ("a", p) != lc (cr ("b"), p)); - - assert (lc ("a", p) < lc ("b", p)); - assert (!(lc ("a", p) < lc (cr ("a"), p))); - - assert (lc ("a", p) <= lc ("b", p)); - assert (lc ("a", p) <= lc (cr ("a"), p)); - assert (lc ("a", p) < lc (cr ("c"), p)); - - // Regex roundtrip. - // - assert (regex_match ("abc", *lc (cr ("abc"), p).regex ())); - - // Regex flags. - // - // icase - // - assert (regex_match ("ABC", cr ("abc", cf::icase))); - - // idot - // - assert (!regex_match ("a", cr ("[.]", cf::idot))); - assert (!regex_match ("a", cr ("[\\.]", cf::idot))); - - assert (regex_match ("a", cr ("."))); - assert (!regex_match ("a", cr (".", cf::idot))); - assert (regex_match ("a", cr ("\\.", cf::idot))); - assert (!regex_match ("a", cr ("\\."))); - - // regex::transform() - // - // The function is static and we can't test it directly. So we will test - // it indirectly via regex matches. - // - // @@ Would be nice to somehow address the inability to test internals (not - // exposed via headers). As a part of utility library support? - // - assert (regex_match (".a[.", cr (".\\.\\[[.]", cf::idot))); - assert (regex_match (".a[.", cr (".\\.\\[[\\.]", cf::idot))); - assert (!regex_match ("ba[.", cr (".\\.\\[[.]", cf::idot))); - assert (!regex_match (".a[b", cr (".\\.\\[[.]", cf::idot))); - assert (!regex_match (".a[b", cr (".\\.\\[[\\.]", cf::idot))); - - // Regex comparison. - // - assert (lc ("a", p) == lc (cr ("a|b"), p)); - assert (lc (cr ("a|b"), p) == lc ("a", p)); - } - - // Test char_traits<line_char>. - // - { - using ct = char_traits<lc>; - using vc = vector<lc>; - - lc c; - ct::assign (c, '0'); - assert (c == ct::char_type ('0')); - - assert (ct::to_char_type (c) == c); - assert (ct::to_int_type (c) == c); - - assert (ct::eq_int_type (c, c)); - assert (!ct::eq_int_type (c, lc::eof)); - - assert (ct::eof () == lc::eof); - - assert (ct::not_eof (c) == c); - assert (ct::not_eof (lc::eof) != lc::eof); - - ct::assign (&c, 1, '1'); - assert (c == ct::int_type ('1')); - - assert (ct::eq (lc ('0'), lc ('0'))); - assert (ct::lt (lc ('0'), lc ('1'))); - - vc v1 ({'0', '1', '2'}); - vc v2 (3, lc::nul); - - assert (ct::find (v1.data (), 3, '1') == v1.data () + 1); - - ct::copy (v2.data (), v1.data (), 3); - assert (v2 == v1); - - v2.push_back (lc::nul); - assert (ct::length (v2.data ()) == 3); - - // Overlaping ranges. - // - ct::move (v1.data () + 1, v1.data (), 2); - assert (v1 == vc ({'0', '0', '1'})); - - v1 = vc ({'0', '1', '2'}); - ct::move (v1.data (), v1.data () + 1, 2); - assert (v1 == vc ({'1', '2', '2'})); - } - - // Test line_char_locale and ctype<line_char> (only non-trivial functions). - // - { - using ct = ctype<lc>; - - line_char_locale l; - - // It is better not to create q facet on stack as it is - // reference-countable. - // - const ct& t (use_facet<ct> (l)); - line_pool p; - - assert (t.is (ct::digit, '0')); - assert (!t.is (ct::digit, '?')); - assert (!t.is (ct::digit, lc ("0", p))); - - const lc chars[] = { '0', '?' }; - ct::mask m[2]; - - const lc* b (chars); - const lc* e (chars + 2); - - // Cast flag value to mask type and compare to mask. - // - auto fl = [] (ct::mask m, ct::mask f) {return m == f;}; - - t.is (b, e, m); - assert (fl (m[0], ct::digit) && fl (m[1], 0)); - - assert (t.scan_is (ct::digit, b, e) == b); - assert (t.scan_is (0, b, e) == b + 1); - - assert (t.scan_not (ct::digit, b, e) == b + 1); - assert (t.scan_not (0, b, e) == b); - - { - char nr[] = "0?"; - lc wd[2]; - t.widen (nr, nr + 2, wd); - assert (wd[0] == b[0] && wd[1] == b[1]); - } - - { - lc wd[] = {'0', lc ("a", p)}; - char nr[2]; - t.narrow (wd, wd + 2, '-', nr); - assert (nr[0] == '0' && nr[1] == '-'); - } - } - - // Test regex_traits<line_char>. Functions other that value() are trivial. - // - { - regex_traits<lc> t; - - const int radix[] = {8, 10}; // Radix 16 is not supported by line_char. - const char digits[] = "0123456789ABCDEF"; - - for (size_t r (0); r < 2; ++r) - { - for (int i (0); i < radix[r]; ++i) - assert (t.value (digits[i], radix[r]) == i); - } - } - - // Test line_regex construction. - // - { - line_pool p; - lr r1 ({lc ("foo", p), lc (cr ("ba(r|z)"), p)}, move (p)); - - lr r2 (move (r1)); - assert (regex_match (ls ({lc ("foo", r2.pool), lc ("bar", r2.pool)}), r2)); - assert (!regex_match (ls ({lc ("foo", r2.pool), lc ("ba", r2.pool)}), r2)); - } - - // Test line_regex match. - // - { - line_pool p; - - const lc foo ("foo", p); - const lc bar ("bar", p); - const lc baz ("baz", p); - const lc blank ("", p); - - assert (regex_match (ls ({foo, bar}), lr ({foo, bar}))); - assert (!regex_match (ls ({foo, baz}), lr ({foo, bar}))); - - assert (regex_match (ls ({bar, foo}), - lr ({'(', foo, '|', bar, ')', '+'}))); - - assert (regex_match (ls ({foo, foo, bar}), - lr ({'(', foo, ')', '\\', '1', bar}))); - - assert (regex_match (ls ({foo}), lr ({lc (cr ("fo+"), p)}))); - assert (regex_match (ls ({foo}), lr ({lc (cr (".*"), p)}))); - assert (regex_match (ls ({blank}), lr ({lc (cr (".*"), p)}))); - - assert (regex_match (ls ({blank, blank, foo}), - lr ({blank, '*', foo, blank, '*'}))); - - assert (regex_match (ls ({blank, blank, foo}), lr ({'.', '*'}))); - - assert (regex_match (ls ({blank, blank}), - lr ({blank, '*', foo, '?', blank, '*'}))); - - assert (regex_match (ls ({foo}), lr ({foo, '{', '1', '}'}))); - assert (regex_match (ls ({foo, foo}), lr ({foo, '{', '1', ',', '}'}))); - - assert (regex_match (ls ({foo, foo}), - lr ({foo, '{', '1', ',', '2', '}'}))); - - assert (!regex_match (ls ({foo, foo}), - lr ({foo, '{', '3', ',', '4', '}'}))); - - assert (regex_match (ls ({foo}), lr ({'(', '?', '=', foo, ')', foo}))); - assert (regex_match (ls ({foo}), lr ({'(', '?', '!', bar, ')', foo}))); - } -} diff --git a/build2/test/script/runner.cxx b/build2/test/script/runner.cxx deleted file mode 100644 index 9031211..0000000 --- a/build2/test/script/runner.cxx +++ /dev/null @@ -1,1891 +0,0 @@ -// file : build2/test/script/runner.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/test/script/runner.hxx> - -#include <set> -#include <ios> // streamsize - -#include <libbutl/regex.mxx> -#include <libbutl/fdstream.mxx> // fdopen_mode, fdnull(), fddup() - -#include <libbuild2/variable.hxx> -#include <libbuild2/filesystem.hxx> -#include <libbuild2/diagnostics.hxx> - -#include <build2/test/common.hxx> - -#include <build2/test/script/regex.hxx> -#include <build2/test/script/parser.hxx> -#include <build2/test/script/builtin.hxx> - -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace test - { - namespace script - { - // Normalize a path. Also make the relative path absolute using the - // scope's working directory unless it is already absolute. - // - static path - normalize (path p, const scope& sp, const location& l) - { - path r (p.absolute () ? move (p) : sp.wd_path / move (p)); - - try - { - r.normalize (); - } - catch (const invalid_path& e) - { - fail (l) << "invalid file path " << e.path; - } - - return r; - } - - // Check if a path is not empty, the referenced file exists and is not - // empty. - // - static bool - non_empty (const path& p, const location& ll) - { - if (p.empty () || !exists (p)) - return false; - - try - { - ifdstream is (p); - return is.peek () != ifdstream::traits_type::eof (); - } - catch (const io_error& e) - { - // While there can be no fault of the test command being currently - // executed let's add the location anyway to ease the - // troubleshooting. And let's stick to that principle down the road. - // - fail (ll) << "unable to read " << p << ": " << e << endf; - } - } - - // If the file exists, not empty and not larger than 4KB print it to the - // diag record. The file content goes from the new line and is not - // indented. - // - static void - print_file (diag_record& d, const path& p, const location& ll) - { - if (exists (p)) - { - try - { - ifdstream is (p, ifdstream::in, ifdstream::badbit); - - if (is.peek () != ifdstream::traits_type::eof ()) - { - char buf[4096 + 1]; // Extra byte is for terminating '\0'. - - // Note that the string is always '\0'-terminated with a maximum - // sizeof (buf) - 1 bytes read. - // - is.getline (buf, sizeof (buf), '\0'); - - // Print if the file fits 4KB-size buffer. Note that if it - // doesn't the failbit is set. - // - if (is.eof ()) - { - // Suppress the trailing newline character as the diag record - // adds it's own one when flush. - // - streamsize n (is.gcount ()); - assert (n > 0); - - // Note that if the file contains '\0' it will also be counted - // by gcount(). But even in the worst case we will stay in the - // buffer boundaries (and so not crash). - // - if (buf[n - 1] == '\n') - buf[n - 1] = '\0'; - - d << '\n' << buf; - } - } - } - catch (const io_error& e) - { - fail (ll) << "unable to read " << p << ": " << e; - } - } - } - - // Print first 10 directory sub-entries to the diag record. The directory - // must exist. - // - static void - print_dir (diag_record& d, const dir_path& p, const location& ll) - { - try - { - size_t n (0); - for (const dir_entry& de: dir_iterator (p, - false /* ignore_dangling */)) - { - if (n++ < 10) - d << '\n' << (de.ltype () == entry_type::directory - ? path_cast<dir_path> (de.path ()) - : de.path ()); - } - - if (n > 10) - d << "\nand " << n - 10 << " more file(s)"; - } - catch (const system_error& e) - { - fail (ll) << "unable to iterate over " << p << ": " << e; - } - } - - // Save a string to the file. Fail if exception is thrown by underlying - // operations. - // - static void - save (const path& p, const string& s, const location& ll) - { - try - { - ofdstream os (p); - os << s; - os.close (); - } - catch (const io_error& e) - { - fail (ll) << "unable to write " << p << ": " << e; - } - } - - // Return the value of the test.target variable. - // - static inline const target_triplet& - test_target (const script& s) - { - // @@ Would be nice to use cached value from test::common_data. - // - if (auto r = cast_null<target_triplet> (s.test_target["test.target"])) - return *r; - - // We set it to default value in init() so it can only be NULL if the - // user resets it. - // - fail << "invalid test.target value" << endf; - } - - // Transform string according to here-* redirect modifiers from the {/} - // set. - // - static string - transform (const string& s, - bool regex, - const string& modifiers, - const script& scr) - { - if (modifiers.find ('/') == string::npos) - return s; - - // For targets other than Windows leave the string intact. - // - if (test_target (scr).class_ != "windows") - return s; - - // Convert forward slashes to Windows path separators (escape for - // regex). - // - string r; - for (size_t p (0);;) - { - size_t sp (s.find ('/', p)); - - if (sp != string::npos) - { - r.append (s, p, sp - p); - r.append (regex ? "\\\\" : "\\"); - p = sp + 1; - } - else - { - r.append (s, p, sp); - break; - } - } - - return r; - } - - // Check if the test command output matches the expected result (redirect - // value). Noop for redirect types other than none, here_*. - // - static bool - check_output (const path& pr, - const path& op, - const path& ip, - const redirect& rd, - const location& ll, - scope& sp, - bool diag, - const char* what) - { - auto input_info = [&ip, &ll] (diag_record& d) - { - if (non_empty (ip, ll)) - d << info << "stdin: " << ip; - }; - - auto output_info = [&what, &ll] (diag_record& d, - const path& p, - const char* prefix = "", - const char* suffix = "") - { - if (non_empty (p, ll)) - d << info << prefix << what << suffix << ": " << p; - else - d << info << prefix << what << suffix << " is empty"; - }; - - if (rd.type == redirect_type::none) - { - // Check that there is no output produced. - // - assert (!op.empty ()); - - if (!non_empty (op, ll)) - return true; - - if (diag) - { - diag_record d (error (ll)); - d << pr << " unexpectedly writes to " << what << - info << what << ": " << op; - - input_info (d); - - // Print cached output. - // - print_file (d, op, ll); - } - - // Fall through (to return false). - // - } - else if (rd.type == redirect_type::here_str_literal || - rd.type == redirect_type::here_doc_literal || - (rd.type == redirect_type::file && - rd.file.mode == redirect_fmode::compare)) - { - // The expected output is provided as a file or as a string. Save the - // string to a file in the later case. - // - assert (!op.empty ()); - - path eop; - - if (rd.type == redirect_type::file) - eop = normalize (rd.file.path, sp, ll); - else - { - eop = path (op + ".orig"); - save (eop, transform (rd.str, false, rd.modifiers, *sp.root), ll); - sp.clean_special (eop); - } - - // Use the diff utility for comparison. - // - path dp ("diff"); - process_path pp (run_search (dp, true)); - - cstrings args {pp.recall_string (), "-u"}; - - // Ignore Windows newline fluff if that's what we are running on. - // - if (test_target (*sp.root).class_ == "windows") - args.push_back ("--strip-trailing-cr"); - - args.push_back (eop.string ().c_str ()); - args.push_back (op.string ().c_str ()); - args.push_back (nullptr); - - if (verb >= 2) - print_process (args); - - try - { - // Save diff's stdout to a file for troubleshooting and for the - // optional (if not too large) printing (at the end of - // diagnostics). - // - path ep (op + ".diff"); - auto_fd efd; - - try - { - efd = fdopen (ep, fdopen_mode::out | fdopen_mode::create); - sp.clean_special (ep); - } - catch (const io_error& e) - { - fail (ll) << "unable to write " << ep << ": " << e; - } - - // Diff utility prints the differences to stdout. But for the - // user it is a part of the test failure diagnostics so let's - // redirect stdout to stderr. - // - process p (pp, args.data (), 0, 2, efd.get ()); - efd.reset (); - - if (p.wait ()) - return true; - - assert (p.exit); - const process_exit& pe (*p.exit); - - // Note that both POSIX and GNU diff report error by exiting with - // the code > 1. - // - if (!pe.normal () || pe.code () > 1) - { - diag_record d (fail (ll)); - print_process (d, args); - d << " " << pe; - } - - // Output doesn't match the expected result. - // - if (diag) - { - diag_record d (error (ll)); - d << pr << " " << what << " doesn't match expected"; - - output_info (d, op); - output_info (d, eop, "expected "); - output_info (d, ep, "", " diff"); - input_info (d); - - print_file (d, ep, ll); - } - - // Fall through (to return false). - // - } - catch (const process_error& e) - { - error (ll) << "unable to execute " << pp << ": " << e; - - if (e.child) - exit (1); - - throw failed (); - } - } - else if (rd.type == redirect_type::here_str_regex || - rd.type == redirect_type::here_doc_regex) - { - // The overall plan is: - // - // 1. Create regex line string. While creating it's line characters - // transform regex lines according to the redirect modifiers. - // - // 2. Create line regex using the line string. If creation fails - // then save the (transformed) regex redirect to a file for - // troubleshooting. - // - // 3. Parse the output into the literal line string. - // - // 4. Match the output line string with the line regex. - // - // 5. If match fails save the (transformed) regex redirect to a file - // for troubleshooting. - // - using namespace regex; - - assert (!op.empty ()); - - // Create regex line string. - // - line_pool pool; - line_string rls; - const regex_lines rl (rd.regex); - - // Parse regex flags. - // - // When add support for new flags don't forget to update - // parse_regex(). - // - auto parse_flags = [] (const string& f) -> char_flags - { - char_flags r (char_flags::none); - - for (char c: f) - { - switch (c) - { - case 'd': r |= char_flags::idot; break; - case 'i': r |= char_flags::icase; break; - default: assert (false); // Error so should have been checked. - } - } - - return r; - }; - - // Return original regex line with the transformation applied. - // - auto line = [&rl, &rd, &sp] (const regex_line& l) -> string - { - string r; - if (l.regex) // Regex (possibly empty), - { - r += rl.intro; - r += transform (l.value, true, rd.modifiers, *sp.root); - r += rl.intro; - r += l.flags; - } - else if (!l.special.empty ()) // Special literal. - r += rl.intro; - else // Textual literal. - r += transform (l.value, false, rd.modifiers, *sp.root); - - r += l.special; - return r; - }; - - // Return regex line location. - // - // Note that we rely on the fact that the command and regex lines - // are always belong to the same testscript file. - // - auto loc = [&ll] (uint64_t line, uint64_t column) -> location - { - location r (ll); - r.line = line; - r.column = column; - return r; - }; - - // Save the regex to file for troubleshooting, return the file path - // it have been saved to. - // - // Note that we save the regex on line regex creation failure or if - // the program output doesn't match. - // - auto save_regex = [&op, &rl, &rd, &ll, &line] () -> path - { - path rp (op + ".regex"); - - // Encode here-document regex global flags if present as a file - // name suffix. For example if icase and idot flags are specified - // the name will look like: - // - // test/1/stdout.regex-di - // - if (rd.type == redirect_type::here_doc_regex && !rl.flags.empty ()) - rp += '-' + rl.flags; - - // Note that if would be more efficient to directly write chunks - // to file rather than to compose a string first. Hower we don't - // bother (about performance) for the sake of the code as we - // already failed. - // - string s; - for (auto b (rl.lines.cbegin ()), i (b), e (rl.lines.cend ()); - i != e; ++i) - { - if (i != b) s += '\n'; - s += line (*i); - } - - save (rp, s, ll); - return rp; - }; - - // Finally create regex line string. - // - // Note that diagnostics doesn't refer to the program path as it is - // irrelevant to failures at this stage. - // - char_flags gf (parse_flags (rl.flags)); // Regex global flags. - - for (const auto& l: rl.lines) - { - if (l.regex) // Regex (with optional special characters). - { - line_char c; - - // Empty regex is a special case repesenting the blank line. - // - if (l.value.empty ()) - c = line_char ("", pool); - else - { - try - { - string s (transform (l.value, true, rd.modifiers, *sp.root)); - - c = line_char ( - char_regex (s, gf | parse_flags (l.flags)), pool); - } - catch (const regex_error& e) - { - // Print regex_error description if meaningful. - // - diag_record d (fail (loc (l.line, l.column))); - - if (rd.type == redirect_type::here_str_regex) - d << "invalid " << what << " regex redirect" << e << - info << "regex: '" << line (l) << "'"; - else - d << "invalid char-regex in " << what << " regex redirect" - << e << - info << "regex line: '" << line (l) << "'"; - - d << endf; - } - } - - rls += c; // Append blank literal or regex line char. - } - else if (!l.special.empty ()) // Special literal. - { - // Literal can not be followed by special characters in the same - // line. - // - assert (l.value.empty ()); - } - else // Textual literal. - { - // Append literal line char. - // - rls += line_char ( - transform (l.value, false, rd.modifiers, *sp.root), pool); - } - - for (char c: l.special) - { - if (line_char::syntax (c)) - rls += line_char (c); // Append special line char. - else - fail (loc (l.line, l.column)) - << "invalid syntax character '" << c << "' in " << what - << " regex redirect" << - info << "regex line: '" << line (l) << "'"; - } - } - - // Create line regex. - // - line_regex regex; - - try - { - regex = line_regex (move (rls), move (pool)); - } - catch (const regex_error& e) - { - // Note that line regex creation can not fail for here-string - // redirect as it doesn't have syntax line chars. That in - // particular means that end_line and end_column are meaningful. - // - assert (rd.type == redirect_type::here_doc_regex); - - diag_record d (fail (loc (rd.end_line, rd.end_column))); - - // Print regex_error description if meaningful. - // - d << "invalid " << what << " regex redirect" << e; - - output_info (d, save_regex (), "", " regex"); - } - - // Parse the output into the literal line string. - // - line_string ls; - - try - { - // Do not throw when eofbit is set (end of stream reached), and - // when failbit is set (getline() failed to extract any character). - // - // Note that newlines are treated as line-chars separators. That - // in particular means that the trailing newline produces a blank - // line-char (empty literal). Empty output produces the zero-length - // line-string. - // - // Also note that we strip the trailing CR characters (otherwise - // can mismatch when cross-test). - // - ifdstream is (op, ifdstream::in, ifdstream::badbit); - is.peek (); // Sets eofbit for an empty stream. - - while (!is.eof ()) - { - string s; - getline (is, s); - - // It is safer to strip CRs in cycle, as msvcrt unexplainably - // adds too much trailing junk to the system_error descriptions, - // and so it can appear in programs output. For example: - // - // ...: Invalid data.\r\r\n - // - // Note that our custom operator<<(ostream&, const exception&) - // removes this junk. - // - while (!s.empty () && s.back () == '\r') - s.pop_back (); - - ls += line_char (move (s), regex.pool); - } - } - catch (const io_error& e) - { - fail (ll) << "unable to read " << op << ": " << e; - } - - // Match the output with the regex. - // - if (regex_match (ls, regex)) // Doesn't throw. - return true; - - // Output doesn't match the regex. We save the regex to file for - // troubleshooting regardless of whether we print the diagnostics or - // not. - // - path rp (save_regex ()); - - if (diag) - { - diag_record d (error (ll)); - d << pr << " " << what << " doesn't match regex"; - - output_info (d, op); - output_info (d, rp, "", " regex"); - input_info (d); - - // Print cached output. - // - print_file (d, op, ll); - } - - // Fall through (to return false). - // - } - else // Noop. - return true; - - return false; - } - - bool default_runner:: - test (scope& s) const - { - return common_.test (s.root->test_target, s.id_path); - } - - void default_runner:: - enter (scope& sp, const location&) - { - auto df = make_diag_frame ( - [&sp](const diag_record& dr) - { - // Let's not depend on how the path representation can be improved - // for readability on printing. - // - dr << info << "test id: " << sp.id_path.posix_string (); - }); - - // Scope working directory shall be empty (the script working - // directory is cleaned up by the test rule prior the script - // execution). - // - // Create the root working directory containing the .buildignore file - // to make sure that it is ignored by name patterns (see buildignore - // description for details). - // - // @@ Shouldn't we add an optional location parameter to mkdir() and - // alike utility functions so the failure message can contain - // location info? - // - fs_status<mkdir_status> r ( - sp.parent == nullptr - ? mkdir_buildignore ( - sp.wd_path, - sp.root->target_scope.root_scope ()->root_extra->buildignore_file, - 2) - : mkdir (sp.wd_path, 2)); - - if (r == mkdir_status::already_exists) - fail << "working directory " << sp.wd_path << " already exists" << - info << "are tests stomping on each other's feet?"; - - // We don't change the current directory here but indicate that the - // scope test commands will be executed in that directory. - // - if (verb >= 2) - text << "cd " << sp.wd_path; - - sp.clean ({cleanup_type::always, sp.wd_path}, true); - } - - void default_runner:: - leave (scope& sp, const location& ll) - { - auto df = make_diag_frame ( - [&sp](const diag_record& dr) - { - // Let's not depend on how the path representation can be improved - // for readability on printing. - // - dr << info << "test id: " << sp.id_path.posix_string (); - }); - - // Perform registered cleanups if requested. - // - if (common_.after == output_after::clean) - { - // Note that we operate with normalized paths here. - // - // Remove special files. The order is not important as we don't - // expect directories here. - // - for (const auto& p: sp.special_cleanups) - { - // Remove the file if exists. Fail otherwise. - // - if (rmfile (p, 3) == rmfile_status::not_exist) - fail (ll) << "registered for cleanup special file " << p - << " does not exist"; - } - - // Remove files and directories in the order opposite to the order of - // cleanup registration. - // - for (const auto& c: reverse_iterate (sp.cleanups)) - { - cleanup_type t (c.type); - - // Skip whenever the path exists or not. - // - if (t == cleanup_type::never) - continue; - - const path& cp (c.path); - - // Wildcard with the last component being '***' (without trailing - // separator) matches all files and sub-directories recursively as - // well as the start directories itself. So we will recursively - // remove the directories that match the parent (for the original - // path) directory wildcard. - // - bool recursive (cp.leaf ().representation () == "***"); - const path& p (!recursive ? cp : cp.directory ()); - - // Remove files or directories using wildcard. - // - if (p.string ().find_first_of ("?*") != string::npos) - { - bool removed (false); - - auto rm = [&cp, recursive, &removed, &sp, &ll] (path&& pe, - const string&, - bool interm) - { - if (!interm) - { - // While removing the entry we can get not_exist due to - // racing conditions, but that's ok if somebody did our job. - // Note that we still set the removed flag to true in this - // case. - // - removed = true; // Will be meaningless on failure. - - if (pe.to_directory ()) - { - dir_path d (path_cast<dir_path> (pe)); - - if (!recursive) - { - rmdir_status r (rmdir (d, 3)); - - if (r != rmdir_status::not_empty) - return true; - - diag_record dr (fail (ll)); - dr << "registered for cleanup directory " << d - << " is not empty"; - - print_dir (dr, d, ll); - dr << info << "wildcard: '" << cp << "'"; - } - else - { - // Don't remove the working directory (it will be removed - // by the dedicated cleanup). - // - // Cast to uint16_t to avoid ambiguity with - // libbutl::rmdir_r(). - // - rmdir_status r (rmdir_r (d, - d != sp.wd_path, - static_cast<uint16_t> (3))); - - if (r != rmdir_status::not_empty) - return true; - - // The directory is unlikely to be current but let's keep - // for completeness. - // - fail (ll) << "registered for cleanup wildcard " << cp - << " matches the current directory"; - } - } - else - rmfile (pe, 3); - } - - return true; - }; - - // Note that here we rely on the fact that recursive iterating - // goes depth-first (which make sense for the cleanup). - // - try - { - // Doesn't follow symlinks. - // - path_search (p, - rm, - dir_path () /* start */, - path_match_flags::none); - } - catch (const system_error& e) - { - fail (ll) << "unable to cleanup wildcard " << cp << ": " << e; - } - - // Removal of no filesystem entries is not an error for 'maybe' - // cleanup type. - // - if (removed || t == cleanup_type::maybe) - continue; - - fail (ll) << "registered for cleanup wildcard " << cp - << " doesn't match any " - << (recursive - ? "path" - : p.to_directory () - ? "directory" - : "file"); - } - - // Remove the directory if exists and empty. Fail otherwise. - // Removal of non-existing directory is not an error for 'maybe' - // cleanup type. - // - if (p.to_directory ()) - { - dir_path d (path_cast<dir_path> (p)); - bool wd (d == sp.wd_path); - - // Trace the scope working directory removal with the verbosity - // level 2 (that was used for its creation). For other - // directories use level 3 (as for other cleanups). - // - int v (wd ? 2 : 3); - - // Don't remove the working directory for the recursive cleanup - // (it will be removed by the dedicated one). - // - // Note that the root working directory contains the - // .buildignore file (see above). - // - // @@ If 'd' is a file then will fail with a diagnostics having - // no location info. Probably need to add an optional location - // parameter to rmdir() function. The same problem exists for - // a file cleanup when try to rmfile() directory instead of - // file. - // - rmdir_status r ( - recursive - ? rmdir_r (d, !wd, static_cast <uint16_t> (v)) - : (wd && sp.parent == nullptr - ? rmdir_buildignore ( - d, - sp.root->target_scope.root_scope ()->root_extra->buildignore_file, - v) - : rmdir (d, v))); - - if (r == rmdir_status::success || - (r == rmdir_status::not_exist && t == cleanup_type::maybe)) - continue; - - diag_record dr (fail (ll)); - dr << "registered for cleanup directory " << d - << (r == rmdir_status::not_exist - ? " does not exist" - : !recursive - ? " is not empty" - : " is current"); - - if (r == rmdir_status::not_empty) - print_dir (dr, d, ll); - } - - // Remove the file if exists. Fail otherwise. Removal of - // non-existing file is not an error for 'maybe' cleanup type. - // - if (rmfile (p, 3) == rmfile_status::not_exist && - t == cleanup_type::always) - fail (ll) << "registered for cleanup file " << p - << " does not exist"; - } - } - - // Return to the parent scope directory or to the out_base one for the - // script scope. - // - if (verb >= 2) - text << "cd " << (sp.parent != nullptr - ? sp.parent->wd_path - : sp.wd_path.directory ()); - } - - // The exit pseudo-builtin: exit the current scope successfully, or - // print the diagnostics and exit the current scope and all the outer - // scopes unsuccessfully. Always throw exit_scope exception. - // - // exit [<diagnostics>] - // - [[noreturn]] static void - exit_builtin (const strings& args, const location& ll) - { - auto i (args.begin ()); - auto e (args.end ()); - - // Process arguments. - // - // If no argument is specified, then exit successfully. Otherwise, - // print the diagnostics and exit unsuccessfully. - // - if (i == e) - throw exit_scope (true); - - const string& s (*i++); - - if (i != e) - fail (ll) << "unexpected argument '" << *i << "'"; - - error (ll) << s; - throw exit_scope (false); - } - - // The set pseudo-builtin: set variable from the stdin input. - // - // set [-e|--exact] [(-n|--newline)|(-w|--whitespace)] [<attr>] <var> - // - static void - set_builtin (scope& sp, - const strings& args, - auto_fd in, - const location& ll) - { - try - { - // Do not throw when eofbit is set (end of stream reached), and - // when failbit is set (read operation failed to extract any - // character). - // - ifdstream cin (move (in), ifdstream::badbit); - - auto i (args.begin ()); - auto e (args.end ()); - - // Process options. - // - bool exact (false); - bool newline (false); - bool whitespace (false); - - for (; i != e; ++i) - { - const string& o (*i); - - if (o == "-e" || o == "--exact") - exact = true; - else if (o == "-n" || o == "--newline") - newline = true; - else if (o == "-w" || o == "--whitespace") - whitespace = true; - else - { - if (*i == "--") - ++i; - - break; - } - } - - // Process arguments. - // - if (i == e) - fail (ll) << "missing variable name"; - - const string& a (*i++); // Either attributes or variable name. - const string* ats (i == e ? nullptr : &a); - const string& vname (i == e ? a : *i++); - - if (i != e) - fail (ll) << "unexpected argument '" << *i << "'"; - - if (ats != nullptr && ats->empty ()) - fail (ll) << "empty variable attributes"; - - if (vname.empty ()) - fail (ll) << "empty variable name"; - - // Read the input. - // - cin.peek (); // Sets eofbit for an empty stream. - - names ns; - while (!cin.eof ()) - { - // Read next element that depends on the whitespace mode being - // enabled or not. For the later case it also make sense to strip - // the trailing CRs that can appear while cross-testing Windows - // target or as a part of msvcrt junk production (see above). - // - string s; - if (whitespace) - cin >> s; - else - { - getline (cin, s); - - while (!s.empty () && s.back () == '\r') - s.pop_back (); - } - - // If failbit is set then we read nothing into the string as eof is - // reached. That in particular means that the stream has trailing - // whitespaces (possibly including newlines) if the whitespace mode - // is enabled, or the trailing newline otherwise. If so then - // we append the "blank" to the variable value in the exact mode - // prior to bailing out. - // - if (cin.fail ()) - { - if (exact) - { - if (whitespace || newline) - ns.emplace_back (move (s)); // Reuse empty string. - else if (ns.empty ()) - ns.emplace_back ("\n"); - else - ns[0].value += '\n'; - } - - break; - } - - if (whitespace || newline || ns.empty ()) - ns.emplace_back (move (s)); - else - { - ns[0].value += '\n'; - ns[0].value += s; - } - } - - cin.close (); - - // Set the variable value and attributes. Note that we need to aquire - // unique lock before potentially changing the script's variable - // pool. The obtained variable reference can safelly be used with no - // locking as the variable pool is an associative container - // (underneath) and we are only adding new variables into it. - // - ulock ul (sp.root->var_pool_mutex); - const variable& var (sp.root->var_pool.insert (move (vname))); - ul.unlock (); - - value& lhs (sp.assign (var)); - - // If there are no attributes specified then the variable assignment - // is straightforward. Otherwise we will use the build2 parser helper - // function. - // - if (ats == nullptr) - lhs.assign (move (ns), &var); - else - { - // If there is an error in the attributes string, our diagnostics - // will look like this: - // - // <attributes>:1:1 error: unknown value attribute x - // testscript:10:1 info: while parsing attributes '[x]' - // - auto df = make_diag_frame ( - [ats, &ll](const diag_record& dr) - { - dr << info (ll) << "while parsing attributes '" << *ats << "'"; - }); - - parser p; - p.apply_value_attributes (&var, - lhs, - value (move (ns)), - *ats, - token_type::assign, - path ("<attributes>")); - } - } - catch (const io_error& e) - { - fail (ll) << "set: " << e; - } - } - - static bool - run_pipe (scope& sp, - command_pipe::const_iterator bc, - command_pipe::const_iterator ec, - auto_fd ifd, - size_t ci, size_t li, const location& ll, - bool diag) - { - if (bc == ec) // End of the pipeline. - return true; - - // The overall plan is to run the first command in the pipe, reading - // its input from the file descriptor passed (or, for the first - // command, according to stdin redirect specification) and redirecting - // its output to the right-hand part of the pipe recursively. Fail if - // the right-hand part fails. Otherwise check the process exit code, - // match stderr (and stdout for the last command in the pipe) according - // to redirect specification(s) and fail if any of the above fails. - // - const command& c (*bc); - - // Register the command explicit cleanups. Verify that the path being - // cleaned up is a sub-path of the testscript working directory. Fail - // if this is not the case. - // - for (const auto& cl: c.cleanups) - { - const path& p (cl.path); - path np (normalize (p, sp, ll)); - - const string& ls (np.leaf ().string ()); - bool wc (ls == "*" || ls == "**" || ls == "***"); - const path& cp (wc ? np.directory () : np); - const dir_path& wd (sp.root->wd_path); - - if (!cp.sub (wd)) - fail (ll) << (wc - ? "wildcard" - : p.to_directory () - ? "directory" - : "file") - << " cleanup " << p << " is out of working directory " - << wd; - - sp.clean ({cl.type, move (np)}, false); - } - - const redirect& in (c.in.effective ()); - const redirect& out (c.out.effective ()); - const redirect& err (c.err.effective ()); - bool eq (c.exit.comparison == exit_comparison::eq); - - // If stdin file descriptor is not open then this is the first pipeline - // command. - // - bool first (ifd.get () == -1); - - command_pipe::const_iterator nc (bc + 1); - bool last (nc == ec); - - // Prior to opening file descriptors for command input/output - // redirects let's check if the command is the exit builtin. Being a - // builtin syntactically it differs from the regular ones in a number - // of ways. It doesn't communicate with standard streams, so - // redirecting them is meaningless. It may appear only as a single - // command in a pipeline. It doesn't return any value and stops the - // scope execution, so checking its exit status is meaningless as - // well. That all means we can short-circuit here calling the builtin - // and bailing out right after that. Checking that the user didn't - // specify any redirects or exit code check sounds like a right thing - // to do. - // - if (c.program.string () == "exit") - { - // In case the builtin is erroneously pipelined from the other - // command, we will close stdin gracefully (reading out the stream - // content), to make sure that the command doesn't print any - // unwanted diagnostics about IO operation failure. - // - // Note that dtor will ignore any errors (which is what we want). - // - ifdstream is (move (ifd), fdstream_mode::skip); - - if (!first || !last) - fail (ll) << "exit builtin must be the only pipe command"; - - if (in.type != redirect_type::none) - fail (ll) << "exit builtin stdin cannot be redirected"; - - if (out.type != redirect_type::none) - fail (ll) << "exit builtin stdout cannot be redirected"; - - if (err.type != redirect_type::none) - fail (ll) << "exit builtin stderr cannot be redirected"; - - // We can't make sure that there is not exit code check. Let's, at - // least, check that non-zero code is not expected. - // - if (eq != (c.exit.code == 0)) - fail (ll) << "exit builtin exit code cannot be non-zero"; - - exit_builtin (c.arguments, ll); // Throws exit_scope exception. - } - - // Create a unique path for a command standard stream cache file. - // - auto std_path = [&sp, &ci, &li, &ll] (const char* n) -> path - { - path p (n); - - // 0 if belongs to a single-line test scope, otherwise is the - // command line number (start from one) in the test scope. - // - if (li > 0) - p += "-" + to_string (li); - - // 0 if belongs to a single-command expression, otherwise is the - // command number (start from one) in the expression. - // - // Note that the name like stdin-N can relate to N-th command of a - // single-line test or to N-th single-command line of multi-line - // test. These cases are mutually exclusive and so are unambiguous. - // - if (ci > 0) - p += "-" + to_string (ci); - - return normalize (move (p), sp, ll); - }; - - // If this is the first pipeline command, then open stdin descriptor - // according to the redirect specified. - // - path isp; - - if (!first) - assert (in.type == redirect_type::none); // No redirect expected. - else - { - // Open a file for passing to the command stdin. - // - auto open_stdin = [&isp, &ifd, &ll] () - { - assert (!isp.empty ()); - - try - { - ifd = fdopen (isp, fdopen_mode::in); - } - catch (const io_error& e) - { - fail (ll) << "unable to read " << isp << ": " << e; - } - }; - - switch (in.type) - { - case redirect_type::pass: - { - try - { - ifd = fddup (0); - } - catch (const io_error& e) - { - fail (ll) << "unable to duplicate stdin: " << e; - } - - break; - } - - case redirect_type::none: - // Somehow need to make sure that the child process doesn't read - // from stdin. That is tricky to do in a portable way. Here we - // suppose that the program which (erroneously) tries to read some - // data from stdin being redirected to /dev/null fails not being - // able to read the expected data, and so the test doesn't pass - // through. - // - // @@ Obviously doesn't cover the case when the process reads - // whatever available. - // @@ Another approach could be not to redirect stdin and let the - // process to hang which can be interpreted as a test failure. - // @@ Both ways are quite ugly. Is there some better way to do - // this? - // - // Fall through. - // - case redirect_type::null: - { - try - { - ifd = fdnull (); - } - catch (const io_error& e) - { - fail (ll) << "unable to write to null device: " << e; - } - - break; - } - - case redirect_type::file: - { - isp = normalize (in.file.path, sp, ll); - - open_stdin (); - break; - } - - case redirect_type::here_str_literal: - case redirect_type::here_doc_literal: - { - // We could write to the command stdin directly but instead will - // cache the data for potential troubleshooting. - // - isp = std_path ("stdin"); - - save ( - isp, transform (in.str, false, in.modifiers, *sp.root), ll); - - sp.clean_special (isp); - - open_stdin (); - break; - } - - case redirect_type::trace: - case redirect_type::merge: - case redirect_type::here_str_regex: - case redirect_type::here_doc_regex: - case redirect_type::here_doc_ref: assert (false); break; - } - } - - assert (ifd.get () != -1); - - // Prior to opening file descriptors for command outputs redirects - // let's check if the command is the set builtin. Being a builtin - // syntactically it differs from the regular ones in a number of ways. - // It either succeeds or terminates abnormally, so redirecting stderr - // is meaningless. It also never produces any output and may appear - // only as a terminal command in a pipeline. That means we can - // short-circuit here calling the builtin and returning right after - // that. Checking that the user didn't specify any meaningless - // redirects or exit code check sounds as a right thing to do. - // - if (c.program.string () == "set") - { - if (!last) - fail (ll) << "set builtin must be the last pipe command"; - - if (out.type != redirect_type::none) - fail (ll) << "set builtin stdout cannot be redirected"; - - if (err.type != redirect_type::none) - fail (ll) << "set builtin stderr cannot be redirected"; - - if (eq != (c.exit.code == 0)) - fail (ll) << "set builtin exit code cannot be non-zero"; - - set_builtin (sp, c.arguments, move (ifd), ll); - return true; - } - - // Open a file for command output redirect if requested explicitly - // (file overwrite/append redirects) or for the purpose of the output - // validation (none, here_*, file comparison redirects), register the - // file for cleanup, return the file descriptor. Interpret trace - // redirect according to the verbosity level (as null if below 2, as - // pass otherwise). Return nullfd, standard stream descriptor duplicate - // or null-device descriptor for merge, pass or null redirects - // respectively (not opening any file). - // - auto open = [&sp, &ll, &std_path] (const redirect& r, - int dfd, - path& p) -> auto_fd - { - assert (dfd == 1 || dfd == 2); - const char* what (dfd == 1 ? "stdout" : "stderr"); - - fdopen_mode m (fdopen_mode::out | fdopen_mode::create); - - auto_fd fd; - redirect_type rt (r.type != redirect_type::trace - ? r.type - : verb < 2 - ? redirect_type::null - : redirect_type::pass); - switch (rt) - { - case redirect_type::pass: - { - try - { - fd = fddup (dfd); - } - catch (const io_error& e) - { - fail (ll) << "unable to duplicate " << what << ": " << e; - } - - return fd; - } - - case redirect_type::null: - { - try - { - fd = fdnull (); - } - catch (const io_error& e) - { - fail (ll) << "unable to write to null device: " << e; - } - - return fd; - } - - case redirect_type::merge: - { - // Duplicate the paired file descriptor later. - // - return fd; // nullfd - } - - case redirect_type::file: - { - // For the cmp mode the user-provided path refers a content to - // match against, rather than a content to be produced (as for - // overwrite and append modes). And so for cmp mode we redirect - // the process output to a temporary file. - // - p = r.file.mode == redirect_fmode::compare - ? std_path (what) - : normalize (r.file.path, sp, ll); - - m |= r.file.mode == redirect_fmode::append - ? fdopen_mode::at_end - : fdopen_mode::truncate; - - break; - } - - case redirect_type::none: - case redirect_type::here_str_literal: - case redirect_type::here_doc_literal: - case redirect_type::here_str_regex: - case redirect_type::here_doc_regex: - { - p = std_path (what); - m |= fdopen_mode::truncate; - break; - } - - case redirect_type::trace: - case redirect_type::here_doc_ref: assert (false); break; - } - - try - { - fd = fdopen (p, m); - - if ((m & fdopen_mode::at_end) != fdopen_mode::at_end) - { - if (rt == redirect_type::file) - sp.clean ({cleanup_type::always, p}, true); - else - sp.clean_special (p); - } - } - catch (const io_error& e) - { - fail (ll) << "unable to write " << p << ": " << e; - } - - return fd; - }; - - path osp; - fdpipe ofd; - - // If this is the last command in the pipeline than redirect the - // command process stdout to a file. Otherwise create a pipe and - // redirect the stdout to the write-end of the pipe. The read-end will - // be passed as stdin for the next command in the pipeline. - // - // @@ Shouldn't we allow the here-* and file output redirects for a - // command with pipelined output? Say if such redirect is present - // then the process output is redirected to a file first (as it is - // when no output pipelined), and only after the process exit code - // and the output are validated the next command in the pipeline is - // executed taking the file as an input. This could be usefull for - // test failures investigation and for tests "tightening". - // - if (last) - ofd.out = open (out, 1, osp); - else - { - assert (out.type == redirect_type::none); // No redirect expected. - - try - { - ofd = fdopen_pipe (); - } - catch (const io_error& e) - { - fail (ll) << "unable to open pipe: " << e; - } - } - - path esp; - auto_fd efd (open (err, 2, esp)); - - // Merge standard streams. - // - bool mo (out.type == redirect_type::merge); - if (mo || err.type == redirect_type::merge) - { - auto_fd& self (mo ? ofd.out : efd); - auto_fd& other (mo ? efd : ofd.out); - - try - { - assert (self.get () == -1 && other.get () != -1); - self = fddup (other.get ()); - } - catch (const io_error& e) - { - fail (ll) << "unable to duplicate " << (mo ? "stderr" : "stdout") - << ": " << e; - } - } - - // All descriptors should be open to the date. - // - assert (ofd.out.get () != -1 && efd.get () != -1); - - optional<process_exit> exit; - builtin_func* bf (builtins.find (c.program.string ())); - - bool success; - - auto process_args = [&c] () -> cstrings - { - cstrings args {c.program.string ().c_str ()}; - - for (const auto& a: c.arguments) - args.push_back (a.c_str ()); - - args.push_back (nullptr); - return args; - }; - - if (bf != nullptr) - { - // Execute the builtin. - // - if (verb >= 2) - print_process (process_args ()); - - try - { - uint8_t r; // Storage. - builtin b ( - bf (sp, r, c.arguments, move (ifd), move (ofd.out), move (efd))); - - success = run_pipe (sp, - nc, - ec, - move (ofd.in), - ci + 1, li, ll, diag); - - exit = process_exit (b.wait ()); - } - catch (const system_error& e) - { - fail (ll) << "unable to execute " << c.program << " builtin: " - << e << endf; - } - } - else - { - // Execute the process. - // - cstrings args (process_args ()); - - // Resolve the relative not simple program path against the scope's - // working directory. The simple one will be left for the process - // path search machinery. - // - path p; - - try - { - p = path (args[0]); - - if (p.relative () && !p.simple ()) - { - p = sp.wd_path / p; - args[0] = p.string ().c_str (); - } - } - catch (const invalid_path& e) - { - fail (ll) << "invalid program path " << e.path; - } - - try - { - process_path pp (process::path_search (args[0])); - - if (verb >= 2) - print_process (args); - - process pr ( - pp, - args.data (), - {ifd.get (), -1}, process::pipe (ofd), {-1, efd.get ()}, - sp.wd_path.string ().c_str ()); - - ifd.reset (); - ofd.out.reset (); - efd.reset (); - - success = run_pipe (sp, - nc, - ec, - move (ofd.in), - ci + 1, li, ll, diag); - - pr.wait (); - - exit = move (pr.exit); - } - catch (const process_error& e) - { - error (ll) << "unable to execute " << args[0] << ": " << e; - - if (e.child) - std::exit (1); - - throw failed (); - } - } - - assert (exit); - - // If the righ-hand side pipeline failed than the whole pipeline fails, - // and no further checks are required. - // - if (!success) - return false; - - const path& pr (c.program); - - // If there is no valid exit code available by whatever reason then we - // print the proper diagnostics, dump stderr (if cached and not too - // large) and fail the whole test. Otherwise if the exit code is not - // correct then we print diagnostics if requested and fail the - // pipeline. - // - bool valid (exit->normal ()); - - // On Windows the exit code can be out of the valid codes range being - // defined as uint16_t. - // -#ifdef _WIN32 - if (valid) - valid = exit->code () < 256; -#endif - - success = valid && eq == (exit->code () == c.exit.code); - - if (!valid || (!success && diag)) - { - // In the presense of a valid exit code we print the diagnostics and - // return false rather than throw. - // - diag_record d (valid ? error (ll) : fail (ll)); - - if (!exit->normal ()) - d << pr << " " << *exit; - else - { - uint16_t ec (exit->code ()); // Make sure is printed as integer. - - if (!valid) - d << pr << " exit code " << ec << " out of 0-255 range"; - else if (!success) - { - if (diag) - d << pr << " exit code " << ec << (eq ? " != " : " == ") - << static_cast<uint16_t> (c.exit.code); - } - else - assert (false); - } - - if (non_empty (esp, ll)) - d << info << "stderr: " << esp; - - if (non_empty (osp, ll)) - d << info << "stdout: " << osp; - - if (non_empty (isp, ll)) - d << info << "stdin: " << isp; - - // Print cached stderr. - // - print_file (d, esp, ll); - } - - // If exit code is correct then check if the standard outputs match the - // expectations. Note that stdout is only redirected to file for the - // last command in the pipeline. - // - if (success) - success = - (!last || - check_output (pr, osp, isp, out, ll, sp, diag, "stdout")) && - check_output (pr, esp, isp, err, ll, sp, diag, "stderr"); - - return success; - } - - static bool - run_expr (scope& sp, - const command_expr& expr, - size_t li, const location& ll, - bool diag) - { - // Print test id once per test expression. - // - auto df = make_diag_frame ( - [&sp](const diag_record& dr) - { - // Let's not depend on how the path representation can be improved - // for readability on printing. - // - dr << info << "test id: " << sp.id_path.posix_string (); - }); - - // Commands are numbered sequentially throughout the expression - // starting with 1. Number 0 means the command is a single one. - // - size_t ci (expr.size () == 1 && expr.back ().pipe.size () == 1 - ? 0 - : 1); - - // If there is no ORs to the right of a pipe then the pipe failure is - // fatal for the whole expression. In particular, the pipe must print - // the diagnostics on failure (if generally allowed). So we find the - // pipe that "switches on" the diagnostics potential printing. - // - command_expr::const_iterator trailing_ands; // Undefined if diag is - // disallowed. - if (diag) - { - auto i (expr.crbegin ()); - for (; i != expr.crend () && i->op == expr_operator::log_and; ++i) ; - trailing_ands = i.base (); - } - - bool r (false); - bool print (false); - - for (auto b (expr.cbegin ()), i (b), e (expr.cend ()); i != e; ++i) - { - if (diag && i + 1 == trailing_ands) - print = true; - - const command_pipe& p (i->pipe); - bool or_op (i->op == expr_operator::log_or); - - // Short-circuit if the pipe result must be OR-ed with true or AND-ed - // with false. - // - if (!((or_op && r) || (!or_op && !r))) - r = run_pipe ( - sp, p.begin (), p.end (), auto_fd (), ci, li, ll, print); - - ci += p.size (); - } - - return r; - } - - void default_runner:: - run (scope& sp, - const command_expr& expr, command_type ct, - size_t li, - const location& ll) - { - // Noop for teardown commands if keeping tests output is requested. - // - if (ct == command_type::teardown && - common_.after == output_after::keep) - return; - - if (verb >= 3) - { - char c ('\0'); - - switch (ct) - { - case command_type::test: c = ' '; break; - case command_type::setup: c = '+'; break; - case command_type::teardown: c = '-'; break; - } - - text << ": " << c << expr; - } - - if (!run_expr (sp, expr, li, ll, true)) - throw failed (); // Assume diagnostics is already printed. - } - - bool default_runner:: - run_if (scope& sp, - const command_expr& expr, - size_t li, const location& ll) - { - if (verb >= 3) - text << ": ?" << expr; - - return run_expr (sp, expr, li, ll, false); - } - } - } -} diff --git a/build2/test/script/runner.hxx b/build2/test/script/runner.hxx deleted file mode 100644 index 5f70dcc..0000000 --- a/build2/test/script/runner.hxx +++ /dev/null @@ -1,101 +0,0 @@ -// file : build2/test/script/runner.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_SCRIPT_RUNNER_HXX -#define BUILD2_TEST_SCRIPT_RUNNER_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <build2/test/script/script.hxx> - -namespace build2 -{ - namespace test - { - struct common; - - namespace script - { - // An exception that can be thrown by a runner to exit the scope (for - // example, as a result of executing the exit builtin). The status - // indicates whether the scope should be considered to have succeeded - // or failed. - // - struct exit_scope - { - bool status; - - explicit - exit_scope (bool s): status (s) {} - }; - - class runner - { - public: - // Return false if this test/group should be skipped. - // - virtual bool - test (scope&) const = 0; - - // Location is the scope start location (for diagnostics, etc). - // - virtual void - enter (scope&, const location&) = 0; - - // Index is the 1-base index of this command line in the command list - // (e.g., in a compound test). If it is 0 then it means there is only - // one command (e.g., a simple test). This information can be used, - // for example, to derive file names. - // - // Location is the start position of this command line in the - // testscript. It can be used in diagnostics. - // - virtual void - run (scope&, - const command_expr&, command_type, - size_t index, - const location&) = 0; - - virtual bool - run_if (scope&, const command_expr&, size_t, const location&) = 0; - - // Location is the scope end location (for diagnostics, etc). - // - virtual void - leave (scope&, const location&) = 0; - }; - - class default_runner: public runner - { - public: - explicit - default_runner (const common& c): common_ (c) {} - - virtual bool - test (scope& s) const override; - - virtual void - enter (scope&, const location&) override; - - virtual void - run (scope&, - const command_expr&, command_type, - size_t, - const location&) override; - - virtual bool - run_if (scope&, const command_expr&, size_t, const location&) override; - - virtual void - leave (scope&, const location&) override; - - private: - const common& common_; - }; - } - } -} - -#endif // BUILD2_TEST_SCRIPT_RUNNER_HXX diff --git a/build2/test/script/script.cxx b/build2/test/script/script.cxx deleted file mode 100644 index 8e6351f..0000000 --- a/build2/test/script/script.cxx +++ /dev/null @@ -1,741 +0,0 @@ -// file : build2/test/script/script.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/test/script/script.hxx> - -#include <sstream> - -#include <libbuild2/target.hxx> -#include <libbuild2/algorithm.hxx> - -using namespace std; - -namespace build2 -{ - namespace test - { - namespace script - { - ostream& - operator<< (ostream& o, line_type lt) - { - const char* s (nullptr); - - switch (lt) - { - case line_type::var: s = "variable"; break; - case line_type::cmd: s = "command"; break; - case line_type::cmd_if: s = "'if'"; break; - case line_type::cmd_ifn: s = "'if!'"; break; - case line_type::cmd_elif: s = "'elif'"; break; - case line_type::cmd_elifn: s = "'elif!'"; break; - case line_type::cmd_else: s = "'else'"; break; - case line_type::cmd_end: s = "'end'"; break; - } - - return o << s; - } - - // Quote if empty or contains spaces or any of the special characters. - // Note that we use single quotes since double quotes still allow - // expansion. - // - // @@ What if it contains single quotes? - // - static void - to_stream_q (ostream& o, const string& s) - { - if (s.empty () || s.find_first_of (" |&<>=\\\"") != string::npos) - o << '\'' << s << '\''; - else - o << s; - }; - - void - to_stream (ostream& o, const command& c, command_to_stream m) - { - auto print_path = [&o] (const path& p) - { - using build2::operator<<; - - ostringstream s; - stream_verb (s, stream_verb (o)); - s << p; - - to_stream_q (o, s.str ()); - }; - - auto print_redirect = - [&o, print_path] (const redirect& r, const char* prefix) - { - o << ' ' << prefix; - - size_t n (string::traits_type::length (prefix)); - assert (n > 0); - - char d (prefix[n - 1]); // Redirect direction. - - switch (r.type) - { - case redirect_type::none: assert (false); break; - case redirect_type::pass: o << '|'; break; - case redirect_type::null: o << '-'; break; - case redirect_type::trace: o << '!'; break; - case redirect_type::merge: o << '&' << r.fd; break; - - case redirect_type::here_str_literal: - case redirect_type::here_doc_literal: - { - bool doc (r.type == redirect_type::here_doc_literal); - - // For here-document add another '>' or '<'. Note that here end - // marker never needs to be quoted. - // - if (doc) - o << d; - - o << r.modifiers; - - if (doc) - o << r.end; - else - { - const string& v (r.str); - to_stream_q (o, - r.modifiers.find (':') == string::npos - ? string (v, 0, v.size () - 1) // Strip newline. - : v); - } - - break; - } - - case redirect_type::here_str_regex: - case redirect_type::here_doc_regex: - { - bool doc (r.type == redirect_type::here_doc_regex); - - // For here-document add another '>' or '<'. Note that here end - // marker never needs to be quoted. - // - if (doc) - o << d; - - o << r.modifiers; - - const regex_lines& re (r.regex); - - if (doc) - o << re.intro + r.end + re.intro + re.flags; - else - { - assert (!re.lines.empty ()); // Regex can't be empty. - - regex_line l (re.lines[0]); - to_stream_q (o, re.intro + l.value + re.intro + l.flags); - } - - break; - } - - case redirect_type::file: - { - // For stdin or stdout-comparison redirect add '>>' or '<<' (and - // so make it '<<<' or '>>>'). Otherwise add '+' or '=' (and so - // make it '>+' or '>='). - // - if (d == '<' || r.file.mode == redirect_fmode::compare) - o << d << d; - else - o << (r.file.mode == redirect_fmode::append ? '+' : '='); - - print_path (r.file.path); - break; - } - - case redirect_type::here_doc_ref: assert (false); break; - } - }; - - auto print_doc = [&o] (const redirect& r) - { - o << endl; - - if (r.type == redirect_type::here_doc_literal) - o << r.str; - else - { - assert (r.type == redirect_type::here_doc_regex); - - const regex_lines& rl (r.regex); - - for (auto b (rl.lines.cbegin ()), i (b), e (rl.lines.cend ()); - i != e; ++i) - { - if (i != b) - o << endl; - - const regex_line& l (*i); - - if (l.regex) // Regex (possibly empty), - o << rl.intro << l.value << rl.intro << l.flags; - else if (!l.special.empty ()) // Special literal. - o << rl.intro; - else // Textual literal. - o << l.value; - - o << l.special; - } - } - - o << (r.modifiers.find (':') == string::npos ? "" : "\n") << r.end; - }; - - if ((m & command_to_stream::header) == command_to_stream::header) - { - // Program. - // - to_stream_q (o, c.program.string ()); - - // Arguments. - // - for (const string& a: c.arguments) - { - o << ' '; - to_stream_q (o, a); - } - - // Redirects. - // - if (c.in.effective ().type != redirect_type::none) - print_redirect (c.in.effective (), "<"); - - if (c.out.effective ().type != redirect_type::none) - print_redirect (c.out.effective (), ">"); - - if (c.err.effective ().type != redirect_type::none) - print_redirect (c.err.effective (), "2>"); - - for (const auto& p: c.cleanups) - { - o << " &"; - - if (p.type != cleanup_type::always) - o << (p.type == cleanup_type::maybe ? '?' : '!'); - - print_path (p.path); - } - - if (c.exit.comparison != exit_comparison::eq || c.exit.code != 0) - { - switch (c.exit.comparison) - { - case exit_comparison::eq: o << " == "; break; - case exit_comparison::ne: o << " != "; break; - } - - o << static_cast<uint16_t> (c.exit.code); - } - } - - if ((m & command_to_stream::here_doc) == command_to_stream::here_doc) - { - // Here-documents. - // - if (c.in.type == redirect_type::here_doc_literal || - c.in.type == redirect_type::here_doc_regex) - print_doc (c.in); - - if (c.out.type == redirect_type::here_doc_literal || - c.out.type == redirect_type::here_doc_regex) - print_doc (c.out); - - if (c.err.type == redirect_type::here_doc_literal || - c.err.type == redirect_type::here_doc_regex) - print_doc (c.err); - } - } - - void - to_stream (ostream& o, const command_pipe& p, command_to_stream m) - { - if ((m & command_to_stream::header) == command_to_stream::header) - { - for (auto b (p.begin ()), i (b); i != p.end (); ++i) - { - if (i != b) - o << " | "; - - to_stream (o, *i, command_to_stream::header); - } - } - - if ((m & command_to_stream::here_doc) == command_to_stream::here_doc) - { - for (const command& c: p) - to_stream (o, c, command_to_stream::here_doc); - } - } - - void - to_stream (ostream& o, const command_expr& e, command_to_stream m) - { - if ((m & command_to_stream::header) == command_to_stream::header) - { - for (auto b (e.begin ()), i (b); i != e.end (); ++i) - { - if (i != b) - { - switch (i->op) - { - case expr_operator::log_or: o << " || "; break; - case expr_operator::log_and: o << " && "; break; - } - } - - to_stream (o, i->pipe, command_to_stream::header); - } - } - - if ((m & command_to_stream::here_doc) == command_to_stream::here_doc) - { - for (const expr_term& t: e) - to_stream (o, t.pipe, command_to_stream::here_doc); - } - } - - // redirect - // - redirect:: - redirect (redirect_type t) - : type (t) - { - switch (type) - { - case redirect_type::none: - case redirect_type::pass: - case redirect_type::null: - case redirect_type::trace: - case redirect_type::merge: break; - - case redirect_type::here_str_literal: - case redirect_type::here_doc_literal: new (&str) string (); break; - - case redirect_type::here_str_regex: - case redirect_type::here_doc_regex: - { - new (®ex) regex_lines (); - break; - } - - case redirect_type::file: new (&file) file_type (); break; - - case redirect_type::here_doc_ref: assert (false); break; - } - } - - redirect:: - redirect (redirect&& r) - : type (r.type), - modifiers (move (r.modifiers)), - end (move (r.end)), - end_line (r.end_line), - end_column (r.end_column) - { - switch (type) - { - case redirect_type::none: - case redirect_type::pass: - case redirect_type::null: - case redirect_type::trace: break; - - case redirect_type::merge: fd = r.fd; break; - - case redirect_type::here_str_literal: - case redirect_type::here_doc_literal: - { - new (&str) string (move (r.str)); - break; - } - case redirect_type::here_str_regex: - case redirect_type::here_doc_regex: - { - new (®ex) regex_lines (move (r.regex)); - break; - } - case redirect_type::file: - { - new (&file) file_type (move (r.file)); - break; - } - case redirect_type::here_doc_ref: - { - new (&ref) reference_wrapper<const redirect> (r.ref); - break; - } - } - } - - redirect:: - ~redirect () - { - switch (type) - { - case redirect_type::none: - case redirect_type::pass: - case redirect_type::null: - case redirect_type::trace: - case redirect_type::merge: break; - - case redirect_type::here_str_literal: - case redirect_type::here_doc_literal: str.~string (); break; - - case redirect_type::here_str_regex: - case redirect_type::here_doc_regex: regex.~regex_lines (); break; - - case redirect_type::file: file.~file_type (); break; - - case redirect_type::here_doc_ref: - { - ref.~reference_wrapper<const redirect> (); - break; - } - } - } - - redirect& redirect:: - operator= (redirect&& r) - { - if (this != &r) - { - this->~redirect (); - new (this) redirect (move (r)); // Assume noexcept move-constructor. - } - return *this; - } - - // scope - // - scope:: - scope (const string& id, scope* p, script* r) - : parent (p), - root (r), - vars (false /* global */), - id_path (cast<path> (assign (root->id_var) = path ())), - wd_path (cast<dir_path> (assign (root->wd_var) = dir_path ())) - - { - // Construct the id_path as a string to ensure POSIX form. In fact, - // the only reason we keep it as a path is to be able to easily get id - // by calling leaf(). - // - { - string s (p != nullptr ? p->id_path.string () : string ()); - - if (!s.empty () && !id.empty ()) - s += '/'; - - s += id; - const_cast<path&> (id_path) = path (move (s)); - } - - // Calculate the working directory path unless this is the root scope - // (handled in an ad hoc way). - // - if (p != nullptr) - const_cast<dir_path&> (wd_path) = dir_path (p->wd_path) /= id; - } - - void scope:: - clean (cleanup c, bool implicit) - { - using std::find; // Hidden by scope::find(). - - assert (!implicit || c.type == cleanup_type::always); - - const path& p (c.path); - if (!p.sub (root->wd_path)) - { - if (implicit) - return; - else - assert (false); // Error so should have been checked. - } - - auto pr = [&p] (const cleanup& v) -> bool {return v.path == p;}; - auto i (find_if (cleanups.begin (), cleanups.end (), pr)); - - if (i == cleanups.end ()) - cleanups.emplace_back (move (c)); - else if (!implicit) - i->type = c.type; - } - - void scope:: - clean_special (path p) - { - special_cleanups.emplace_back (move (p)); - } - - // script_base - // - script_base:: - script_base () - : // Enter the test.* variables with the same variable types as in - // buildfiles except for test: while in buildfiles it can be a - // target name, in testscripts it should be resolved to a path. - // - // Note: entering in a custom variable pool. - // - test_var (var_pool.insert<path> ("test")), - options_var (var_pool.insert<strings> ("test.options")), - arguments_var (var_pool.insert<strings> ("test.arguments")), - redirects_var (var_pool.insert<strings> ("test.redirects")), - cleanups_var (var_pool.insert<strings> ("test.cleanups")), - - wd_var (var_pool.insert<dir_path> ("~")), - id_var (var_pool.insert<path> ("@")), - cmd_var (var_pool.insert<strings> ("*")), - cmdN_var { - &var_pool.insert<path> ("0"), - &var_pool.insert<string> ("1"), - &var_pool.insert<string> ("2"), - &var_pool.insert<string> ("3"), - &var_pool.insert<string> ("4"), - &var_pool.insert<string> ("5"), - &var_pool.insert<string> ("6"), - &var_pool.insert<string> ("7"), - &var_pool.insert<string> ("8"), - &var_pool.insert<string> ("9")} {} - - // script - // - script:: - script (const target& tt, - const testscript& st, - const dir_path& rwd) - : group (st.name == "testscript" ? string () : st.name, this), - test_target (tt), - target_scope (tt.base_scope ()), - script_target (st) - { - // Set the script working dir ($~) to $out_base/test/<id> (id_path - // for root is just the id which is empty if st is 'testscript'). - // - const_cast<dir_path&> (wd_path) = dir_path (rwd) /= id_path.string (); - - // Set the test variable at the script level. We do it even if it's - // set in the buildfile since they use different types. - // - { - value& v (assign (test_var)); - - // Note that the test variable's visibility is target. - // - lookup l (find_in_buildfile ("test", false)); - - // Note that we have similar code for simple tests. - // - const target* t (nullptr); - - if (l.defined ()) - { - const name* n (cast_null<name> (l)); - - if (n == nullptr) - v = nullptr; - else if (n->empty ()) - v = path (); - else if (n->simple ()) - { - // Ignore the special 'true' value. - // - if (n->value != "true") - v = path (n->value); - else - t = &tt; - } - else if (n->directory ()) - v = path (n->dir); - else - { - // Must be a target name. - // - // @@ OUT: what if this is a @-qualified pair of names? - // - t = search_existing (*n, target_scope); - - if (t == nullptr) - fail << "unknown target '" << *n << "' in test variable"; - } - } - else - // By default we set it to the test target's path. - // - t = &tt; - - // If this is a path-based target, then we use the path. If this - // is an alias target (e.g., dir{}), then we use the directory - // path. Otherwise, we leave it NULL expecting the testscript to - // set it to something appropriate, if used. - // - if (t != nullptr) - { - if (auto* pt = t->is_a<path_target> ()) - { - // Do some sanity checks: the target better be up-to-date with - // an assigned path. - // - v = pt->path (); - - if (v.empty ()) - fail << "target " << *pt << " specified in the test variable " - << "is out of date" << - info << "consider specifying it as a prerequisite of " << tt; - } - else if (t->is_a<alias> ()) - v = path (t->dir); - else if (t != &tt) - fail << "target " << *t << " specified in the test variable " - << "is not path-based"; - } - } - - // Set the special $*, $N variables. - // - reset_special (); - } - - lookup scope:: - find (const variable& var) const - { - // Search script scopes until we hit the root. - // - const scope* s (this); - - do - { - auto p (s->vars.find (var)); - if (p.first != nullptr) - return lookup (*p.first, p.second, s->vars); - } - while ((s->parent != nullptr ? (s = s->parent) : nullptr) != nullptr); - - return find_in_buildfile (var.name); - } - - - lookup scope:: - find_in_buildfile (const string& n, bool target_only) const - { - // Switch to the corresponding buildfile variable. Note that we don't - // want to insert a new variable into the pool (we might be running - // in parallel). Plus, if there is no such variable, then we cannot - // possibly find any value. - // - const variable* pvar (build2::var_pool.find (n)); - - if (pvar == nullptr) - return lookup (); - - const script& s (static_cast<const script&> (*root)); - const variable& var (*pvar); - - // First check the target we are testing. - // - { - // Note that we skip applying the override if we did not find any - // value. In this case, presumably the override also affects the - // script target and we will pick it up there. A bit fuzzy. - // - auto p (s.test_target.find_original (var, target_only)); - - if (p.first) - { - if (var.overrides != nullptr) - p = s.target_scope.find_override (var, move (p), true); - - return p.first; - } - } - - // Then the script target followed by the scopes it is in. Note that - // while unlikely it is possible the test and script targets will be - // in different scopes which brings the question of which scopes we - // should search. - // - return s.script_target[var]; - } - - value& scope:: - append (const variable& var) - { - lookup l (find (var)); - - if (l.defined () && l.belongs (*this)) // Existing var in this scope. - return vars.modify (l); - - value& r (assign (var)); // NULL. - - if (l.defined ()) - r = *l; // Copy value (and type) from the outer scope. - - return r; - } - - void scope:: - reset_special () - { - // First assemble the $* value. - // - strings s; - - auto append = [&s] (const strings& v) - { - s.insert (s.end (), v.begin (), v.end ()); - }; - - if (lookup l = find (root->test_var)) - s.push_back (cast<path> (l).representation ()); - - if (lookup l = find (root->options_var)) - append (cast<strings> (l)); - - if (lookup l = find (root->arguments_var)) - append (cast<strings> (l)); - - // Keep redirects/cleanups out of $N. - // - size_t n (s.size ()); - - if (lookup l = find (root->redirects_var)) - append (cast<strings> (l)); - - if (lookup l = find (root->cleanups_var)) - append (cast<strings> (l)); - - // Set the $N values if present. - // - for (size_t i (0); i <= 9; ++i) - { - value& v (assign (*root->cmdN_var[i])); - - if (i < n) - { - if (i == 0) - v = path (s[i]); - else - v = s[i]; - } - else - v = nullptr; // Clear any old values. - } - - // Set $*. - // - assign (root->cmd_var) = move (s); - } - } - } -} diff --git a/build2/test/script/script.hxx b/build2/test/script/script.hxx deleted file mode 100644 index cc162cb..0000000 --- a/build2/test/script/script.hxx +++ /dev/null @@ -1,559 +0,0 @@ -// file : build2/test/script/script.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_SCRIPT_SCRIPT_HXX -#define BUILD2_TEST_SCRIPT_SCRIPT_HXX - -#include <set> - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/variable.hxx> - -#include <build2/test/target.hxx> - -#include <build2/test/script/token.hxx> // replay_tokens - -namespace build2 -{ - class target; - - namespace test - { - namespace script - { - class parser; // Required by VC for 'friend class parser' declaration. - - // Pre-parse representation. - // - - enum class line_type - { - var, - cmd, - cmd_if, - cmd_ifn, - cmd_elif, - cmd_elifn, - cmd_else, - cmd_end - }; - - ostream& - operator<< (ostream&, line_type); - - struct line - { - line_type type; - replay_tokens tokens; - - union - { - const variable* var; // Pre-entered for line_type::var. - }; - }; - - // Most of the time we will have just one line (test command). - // - using lines = small_vector<line, 1>; - - // Parse object model. - // - - // redirect - // - enum class redirect_type - { - none, - pass, - null, - trace, - merge, - here_str_literal, - here_str_regex, - here_doc_literal, - here_doc_regex, - here_doc_ref, // Reference to here_doc literal or regex. - file, - }; - - // Pre-parsed (but not instantiated) regex lines. The idea here is that - // we should be able to re-create their (more or less) exact text - // representation for diagnostics but also instantiate without any - // re-parsing. - // - struct regex_line - { - // If regex is true, then value is the regex expression. Otherwise, it - // is a literal. Note that special characters can be present in both - // cases. For example, //+ is a regex, while /+ is a literal, both - // with '+' as a special character. Flags are only valid for regex. - // Literals falls apart into textual (has no special characters) and - // special (has just special characters instead) ones. For example - // foo is a textual literal, while /.+ is a special one. Note that - // literal must not have value and special both non-empty. - // - bool regex; - - string value; - string flags; - string special; - - uint64_t line; - uint64_t column; - - // Create regex with optional special characters. - // - regex_line (uint64_t l, uint64_t c, - string v, string f, string s = string ()) - : regex (true), - value (move (v)), - flags (move (f)), - special (move (s)), - line (l), - column (c) {} - - // Create a literal, either text or special. - // - regex_line (uint64_t l, uint64_t c, string v, bool s) - : regex (false), - value (s ? string () : move (v)), - special (s ? move (v) : string ()), - line (l), - column (c) {} - }; - - struct regex_lines - { - char intro; // Introducer character. - string flags; // Global flags (here-document). - - small_vector<regex_line, 8> lines; - }; - - // Output file redirect mode. - // - enum class redirect_fmode - { - compare, - overwrite, - append - }; - - struct redirect - { - redirect_type type; - - struct file_type - { - using path_type = build2::path; - path_type path; - redirect_fmode mode; // Meaningless for input redirect. - }; - - union - { - int fd; // Merge-to descriptor. - string str; // Note: with trailing newline, if requested. - regex_lines regex; // Note: with trailing blank, if requested. - file_type file; - reference_wrapper<const redirect> ref; // Note: no chains. - }; - - string modifiers; // Redirect modifiers. - string end; // Here-document end marker (no regex intro/flags). - uint64_t end_line; // Here-document end marker location. - uint64_t end_column; - - // Create redirect of a type other than reference. - // - explicit - redirect (redirect_type = redirect_type::none); - - // Create redirect of the reference type. - // - redirect (redirect_type t, const redirect& r) - : type (redirect_type::here_doc_ref), ref (r) - { - // There is no support (and need) for reference chains. - // - assert (t == redirect_type::here_doc_ref && - r.type != redirect_type::here_doc_ref); - } - - // Move constuctible/assignable-only type. - // - redirect (redirect&&); - redirect& operator= (redirect&&); - - ~redirect (); - - const redirect& - effective () const noexcept - { - return type == redirect_type::here_doc_ref ? ref.get () : *this; - } - }; - - // cleanup - // - enum class cleanup_type - { - always, // &foo - cleanup, fail if does not exist. - maybe, // &?foo - cleanup, ignore if does not exist. - never // &!foo - don’t cleanup, ignore if doesn’t exist. - }; - - // File or directory to be automatically cleaned up at the end of the - // scope. If the path ends with a trailing slash, then it is assumed to - // be a directory, otherwise -- a file. A directory that is about to be - // cleaned up must be empty. - // - // The last component in the path may contain a wildcard that have the - // following semantics: - // - // dir/* - remove all immediate files - // dir/*/ - remove all immediate sub-directories (must be empty) - // dir/** - remove all files recursively - // dir/**/ - remove all sub-directories recursively (must be empty) - // dir/*** - remove directory dir with all files and sub-directories - // recursively - // - struct cleanup - { - cleanup_type type; - build2::path path; - }; - using cleanups = vector<cleanup>; - - // command_exit - // - enum class exit_comparison {eq, ne}; - - struct command_exit - { - // C/C++ don't apply constraints on program exit code other than it - // being of type int. - // - // POSIX specifies that only the least significant 8 bits shall be - // available from wait() and waitpid(); the full value shall be - // available from waitid() (read more at _Exit, _exit Open Group - // spec). - // - // While the Linux man page for waitid() doesn't mention any - // deviations from the standard, the FreeBSD implementation (as of - // version 11.0) only returns 8 bits like the other wait*() calls. - // - // Windows supports 32-bit exit codes. - // - // Note that in shells some exit values can have special meaning so - // using them can be a source of confusion. For bash values in the - // [126, 255] range are such a special ones (see Appendix E, "Exit - // Codes With Special Meanings" in the Advanced Bash-Scripting Guide). - // - exit_comparison comparison; - uint8_t code; - }; - - // command - // - struct command - { - path program; - strings arguments; - - redirect in; - redirect out; - redirect err; - - script::cleanups cleanups; - - command_exit exit {exit_comparison::eq, 0}; - }; - - enum class command_to_stream: uint16_t - { - header = 0x01, - here_doc = 0x02, // Note: printed on a new line. - all = header | here_doc - }; - - void - to_stream (ostream&, const command&, command_to_stream); - - ostream& - operator<< (ostream&, const command&); - - // command_pipe - // - using command_pipe = vector<command>; - - void - to_stream (ostream&, const command_pipe&, command_to_stream); - - ostream& - operator<< (ostream&, const command_pipe&); - - // command_expr - // - enum class expr_operator {log_or, log_and}; - - struct expr_term - { - expr_operator op; // OR-ed to an implied false for the first term. - command_pipe pipe; - }; - - using command_expr = vector<expr_term>; - - void - to_stream (ostream&, const command_expr&, command_to_stream); - - ostream& - operator<< (ostream&, const command_expr&); - - // command_type - // - enum class command_type {test, setup, teardown}; - - // description - // - struct description - { - string id; - string summary; - string details; - - bool - empty () const - { - return id.empty () && summary.empty () && details.empty (); - } - }; - - // scope - // - class script; - - enum class scope_state {unknown, passed, failed}; - - class scope - { - public: - scope* const parent; // NULL for the root (script) scope. - script* const root; // Self for the root (script) scope. - - // The chain of if-else scope alternatives. See also if_cond_ below. - // - unique_ptr<scope> if_chain; - - // Note that if we pass the variable name as a string, then it will - // be looked up in the wrong pool. - // - variable_map vars; - - const path& id_path; // Id path ($@, relative in POSIX form). - const dir_path& wd_path; // Working dir ($~, absolute and normalized). - - optional<description> desc; - - scope_state state = scope_state::unknown; - test::script::cleanups cleanups; - paths special_cleanups; - - // Variables. - // - public: - // Lookup the variable starting from this scope, continuing with outer - // scopes, then the target being tested, then the testscript target, - // and then outer buildfile scopes (including testscript-type/pattern - // specific). - // - lookup - find (const variable&) const; - - // As above but only look for buildfile variables. If target_only is - // false then also look in scopes of the test target (this should only - // be done if the variable's visibility is target). - // - lookup - find_in_buildfile (const string&, bool target_only = true) const; - - // Return a value suitable for assignment. If the variable does not - // exist in this scope's map, then a new one with the NULL value is - // added and returned. Otherwise the existing value is returned. - // - value& - assign (const variable& var) {return vars.assign (var);} - - // Return a value suitable for append/prepend. If the variable does - // not exist in this scope's map, then outer scopes are searched for - // the same variable. If found then a new variable with the found - // value is added to this scope and returned. Otherwise this function - // proceeds as assign() above. - // - value& - append (const variable&); - - // Reset special $*, $N variables based on the test.* values. - // - void - reset_special (); - - // Cleanup. - // - public: - // Register a cleanup. If the cleanup is explicit, then override the - // cleanup type if this path is already registered. Ignore implicit - // registration of a path outside script working directory. - // - void - clean (cleanup, bool implicit); - - // Register cleanup of a special file. Such files are created to - // maintain testscript machinery and must be removed first, not to - // interfere with the user-defined wildcard cleanups. - // - void - clean_special (path p); - - public: - virtual - ~scope () = default; - - protected: - scope (const string& id, scope* parent, script* root); - - // Pre-parse data. - // - public: - virtual bool - empty () const = 0; - - protected: - friend class parser; - - location start_loc_; - location end_loc_; - - optional<line> if_cond_; - }; - - // group - // - class group: public scope - { - public: - vector<unique_ptr<scope>> scopes; - - public: - group (const string& id, group& p): scope (id, &p, p.root) {} - - protected: - group (const string& id, script* r): scope (id, nullptr, r) {} - - // Pre-parse data. - // - public: - virtual bool - empty () const override - { - return - !if_cond_ && // The condition expression can have side-effects. - setup_.empty () && - tdown_.empty () && - find_if (scopes.begin (), scopes.end (), - [] (const unique_ptr<scope>& s) - { - return !s->empty (); - }) == scopes.end (); - } - - private: - friend class parser; - - lines setup_; - lines tdown_; - }; - - // test - // - class test: public scope - { - public: - test (const string& id, group& p): scope (id, &p, p.root) {} - - // Pre-parse data. - // - public: - virtual bool - empty () const override - { - return tests_.empty (); - } - - private: - friend class parser; - - lines tests_; - }; - - // script - // - class script_base // Make sure certain things are initialized early. - { - protected: - script_base (); - - public: - variable_pool var_pool; - mutable shared_mutex var_pool_mutex; - - const variable& test_var; // test - const variable& options_var; // test.options - const variable& arguments_var; // test.arguments - const variable& redirects_var; // test.redirects - const variable& cleanups_var; // test.cleanups - - const variable& wd_var; // $~ - const variable& id_var; // $@ - const variable& cmd_var; // $* - const variable* cmdN_var[10]; // $N - }; - - class script: public script_base, public group - { - public: - script (const target& test_target, - const testscript& script_target, - const dir_path& root_wd); - - script (script&&) = delete; - script (const script&) = delete; - script& operator= (script&&) = delete; - script& operator= (const script&) = delete; - - public: - const target& test_target; // Target we are testing. - const build2::scope& target_scope; // Base scope of test target. - const testscript& script_target; // Target of the testscript file. - - // Pre-parse data. - // - private: - friend class parser; - - // Testscript file paths. Specifically, replay_token::file points to - // these paths. - // - std::set<path> paths_; - }; - } - } -} - -#include <build2/test/script/script.ixx> - -#endif // BUILD2_TEST_SCRIPT_SCRIPT_HXX diff --git a/build2/test/script/script.ixx b/build2/test/script/script.ixx deleted file mode 100644 index 80fbcb0..0000000 --- a/build2/test/script/script.ixx +++ /dev/null @@ -1,60 +0,0 @@ -// file : build2/test/script/script.ixx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -namespace build2 -{ - namespace test - { - namespace script - { - inline command_to_stream - operator&= (command_to_stream& x, command_to_stream y) - { - return x = static_cast<command_to_stream> ( - static_cast<uint16_t> (x) & static_cast<uint16_t> (y)); - } - - inline command_to_stream - operator|= (command_to_stream& x, command_to_stream y) - { - return x = static_cast<command_to_stream> ( - static_cast<uint16_t> (x) | static_cast<uint16_t> (y)); - } - - inline command_to_stream - operator& (command_to_stream x, command_to_stream y) {return x &= y;} - - inline command_to_stream - operator| (command_to_stream x, command_to_stream y) {return x |= y;} - - - // command - // - inline ostream& - operator<< (ostream& o, const command& c) - { - to_stream (o, c, command_to_stream::all); - return o; - } - - // command_pipe - // - inline ostream& - operator<< (ostream& o, const command_pipe& p) - { - to_stream (o, p, command_to_stream::all); - return o; - } - - // command_expr - // - inline ostream& - operator<< (ostream& o, const command_expr& e) - { - to_stream (o, e, command_to_stream::all); - return o; - } - } - } -} diff --git a/build2/test/script/token.cxx b/build2/test/script/token.cxx deleted file mode 100644 index 2d14701..0000000 --- a/build2/test/script/token.cxx +++ /dev/null @@ -1,57 +0,0 @@ -// file : build2/test/script/token.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/test/script/token.hxx> - -using namespace std; - -namespace build2 -{ - namespace test - { - namespace script - { - void - token_printer (ostream& os, const token& t, bool d) - { - const string& v (t.value); - - // Only quote non-name tokens for diagnostics. - // - const char* q (d ? "'" : ""); - - switch (t.type) - { - case token_type::semi: os << q << ';' << q; break; - - case token_type::dot: os << q << '.' << q; break; - - case token_type::plus: os << q << '+' << q; break; - case token_type::minus: os << q << '-' << q; break; - - case token_type::clean: os << q << '&' << v << q; break; - case token_type::pipe: os << q << '|' << q; break; - - case token_type::in_pass: os << q << "<|" << q; break; - case token_type::in_null: os << q << "<-" << q; break; - case token_type::in_str: os << q << '<' << v << q; break; - case token_type::in_doc: os << q << "<<" << v << q; break; - case token_type::in_file: os << q << "<<<" << q; break; - - case token_type::out_pass: os << q << ">|" << q; break; - case token_type::out_null: os << q << ">-" << q; break; - case token_type::out_trace: os << q << ">!" << q; break; - case token_type::out_merge: os << q << ">&" << q; break; - case token_type::out_str: os << q << '>' << v << q; break; - case token_type::out_doc: os << q << ">>" << v << q; break; - case token_type::out_file_cmp: os << q << ">>>" << v << q; break; - case token_type::out_file_ovr: os << q << ">=" << v << q; break; - case token_type::out_file_app: os << q << ">+" << v << q; break; - - default: build2::token_printer (os, t, d); - } - } - } - } -} diff --git a/build2/test/script/token.hxx b/build2/test/script/token.hxx deleted file mode 100644 index c79ef1b..0000000 --- a/build2/test/script/token.hxx +++ /dev/null @@ -1,65 +0,0 @@ -// file : build2/test/script/token.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_SCRIPT_TOKEN_HXX -#define BUILD2_TEST_SCRIPT_TOKEN_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/token.hxx> - -namespace build2 -{ - namespace test - { - namespace script - { - struct token_type: build2::token_type - { - using base_type = build2::token_type; - - enum - { - // NOTE: remember to update token_printer()! - - semi = base_type::value_next, // ; - - dot, // . - - plus, // + - minus, // - - - pipe, // | - clean, // &{?!} (modifiers in value) - - in_pass, // <| - in_null, // <- - in_str, // <{:} (modifiers in value) - in_doc, // <<{:} (modifiers in value) - in_file, // <<< - - out_pass, // >| - out_null, // >- - out_trace, // >! - out_merge, // >& - out_str, // >{:~} (modifiers in value) - out_doc, // >>{:~} (modifiers in value) - out_file_cmp, // >>> - out_file_ovr, // >= - out_file_app // >+ - }; - - token_type () = default; - token_type (value_type v): base_type (v) {} - token_type (base_type v): base_type (v) {} - }; - - void - token_printer (ostream&, const token&, bool); - } - } -} - -#endif // BUILD2_TEST_SCRIPT_TOKEN_HXX diff --git a/build2/test/target.cxx b/build2/test/target.cxx deleted file mode 100644 index f75b556..0000000 --- a/build2/test/target.cxx +++ /dev/null @@ -1,63 +0,0 @@ -// file : build2/test/target.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <build2/test/target.hxx> - -using namespace std; -using namespace butl; - -namespace build2 -{ - namespace test - { - static const char* - testscript_target_extension (const target_key& tk, const scope*) - { - // If the name is special 'testscript', then there is no extension, - // otherwise it is .testscript. - // - return *tk.name == "testscript" ? "" : "testscript"; - } - - static bool - testscript_target_pattern (const target_type&, - const scope&, - string& v, - optional<string>& e, - const location& l, - bool r) - { - if (r) - { - assert (e); - e = nullopt; - } - else - { - e = target::split_name (v, l); - - if (!e && v != "testscript") - { - e = "testscript"; - return true; - } - } - - return false; - } - - const target_type testscript::static_type - { - "testscript", - &file::static_type, - &target_factory<testscript>, - &testscript_target_extension, - nullptr, /* default_extension */ - &testscript_target_pattern, - nullptr, - &file_search, - false - }; - } -} diff --git a/build2/test/target.hxx b/build2/test/target.hxx deleted file mode 100644 index 6cd07b9..0000000 --- a/build2/test/target.hxx +++ /dev/null @@ -1,29 +0,0 @@ -// file : build2/test/target.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD2_TEST_TARGET_HXX -#define BUILD2_TEST_TARGET_HXX - -#include <libbuild2/types.hxx> -#include <libbuild2/utility.hxx> - -#include <libbuild2/target.hxx> - -namespace build2 -{ - namespace test - { - class testscript: 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_TEST_TARGET_HXX diff --git a/build2/version/init.cxx b/build2/version/init.cxx index 8c9cd42..30f0f45 100644 --- a/build2/version/init.cxx +++ b/build2/version/init.cxx @@ -11,9 +11,9 @@ #include <libbuild2/variable.hxx> #include <libbuild2/diagnostics.hxx> -#include <build2/config/utility.hxx> +#include <libbuild2/config/utility.hxx> -#include <build2/dist/module.hxx> +#include <libbuild2/dist/module.hxx> #include <build2/version/rule.hxx> #include <build2/version/module.hxx> diff --git a/build2/version/rule.hxx b/build2/version/rule.hxx index 8eb4830..6d841df 100644 --- a/build2/version/rule.hxx +++ b/build2/version/rule.hxx @@ -8,8 +8,9 @@ #include <libbuild2/types.hxx> #include <libbuild2/utility.hxx> +#include <libbuild2/install/rule.hxx> + #include <build2/in/rule.hxx> -#include <build2/install/rule.hxx> namespace build2 { |