diff options
author | Boris Kolpackov <boris@codesynthesis.com> | 2015-06-24 13:53:28 +0200 |
---|---|---|
committer | Boris Kolpackov <boris@codesynthesis.com> | 2015-06-24 13:53:28 +0200 |
commit | e815af118562c68794efbd310c887acd8eae800c (patch) | |
tree | cedd8745cce259693c038c309d663a682c982e98 | |
parent | 4f52c4ed65883dacef32587cf066fbb1182c6628 (diff) |
First take on the cli module plus necessary infrastructure
33 files changed, 1065 insertions, 163 deletions
diff --git a/build/algorithm b/build/algorithm index bc756c2..59021f5 100644 --- a/build/algorithm +++ b/build/algorithm @@ -68,6 +68,13 @@ namespace build void search_and_match (action, target&, const dir_path& dir); + // Unless already available, match, and, if necessary, execute + // (not yet implemented) the group in order to obtain its members + // list. + // + group_view + resolve_group_members (action, target_group&); + // Inject dependency on the parent directory's fsdir{}, unless it is // the project's out_root (or is outside of any project; say, for // example, an install directory). Normally this function is called @@ -114,7 +121,7 @@ namespace build // template <typename T> T* - execute_find_prerequisites (action, target&, const timestamp&); + execute_prerequisites (action, target&, const timestamp&); // Return noop_recipe instead of using this function directly. // diff --git a/build/algorithm.cxx b/build/algorithm.cxx index abae023..c438fb8 100644 --- a/build/algorithm.cxx +++ b/build/algorithm.cxx @@ -34,9 +34,11 @@ namespace build return create_new_target (pk); } - void - match_impl (action a, target& t) + pair<const rule*, void*> + match_impl (action a, target& t, bool apply) { + pair<const rule*, void*> r (nullptr, nullptr); + // Clear the resolved targets list before calling match(). The rule // is free to, say, resize() this list in match() (provided that it // matches) in order to, for example, prepare it for apply(). @@ -133,17 +135,26 @@ namespace build if (!ambig) { - auto g ( - make_exception_guard ( - [](action a, target& t, const string& n) - { - info << "while applying rule " << n << " to " - << diag_do (a, t); - }, - a, t, n)); - - t.recipe (a, ru.apply (a, t, m)); - break; + if (apply) + { + auto g ( + make_exception_guard ( + [](action a, target& t, const string& n) + { + info << "while applying rule " << n << " to " + << diag_do (a, t); + }, + a, t, n)); + + t.recipe (a, ru.apply (a, t, m)); + break; + } + else + { + r.first = &ru; + r.second = m; + return r; + } } else dr << info << "use rule hint to disambiguate this match"; @@ -152,7 +163,50 @@ namespace build } if (!t.recipe (a)) - fail << "no rule to " << diag_do (a, t); + { + diag_record dr; + dr << fail << "no rule to " << diag_do (a, t); + + if (verb < 3) + dr << info << "re-run with --verbose 3 for more information"; + } + + return r; + } + + group_view + resolve_group_members_impl (action a, target_group& g) + { + group_view r; + + // Unless we already have a recipe, try matching the target to + // the rule. + // + if (!g.recipe (a)) + { + auto p (match_impl (a, g, false)); + + r = g.members (a); + if (r.members != nullptr) + return r; + + // That didn't help, so apply the rule and go to the building + // phase. + // + g.recipe (a, p.first->apply (a, g, p.second)); + } + + // Note the we use execute_impl() rather than execute() here + // to sidestep the dependents count logic. In this context, + // this is by definition the first attempt to execute this + // rule (otherwise we would have already known the members + // list) and we really do need to execute it now. + // + execute_impl (a, g); + + r = g.members (a); + assert (r.members != nullptr); // What "next step" did the group expect? + return r; } void diff --git a/build/algorithm.ixx b/build/algorithm.ixx index 3907209..3a6f183 100644 --- a/build/algorithm.ixx +++ b/build/algorithm.ixx @@ -2,6 +2,8 @@ // copyright : Copyright (c) 2014-2015 Code Synthesis Ltd // license : MIT; see accompanying LICENSE file +#include <utility> // pair + #include <build/prerequisite> #include <build/context> @@ -36,18 +38,28 @@ namespace build return static_cast<T&> (search (T::static_type, dir, name, ext, scope)); } - void - match_impl (action, target&); + std::pair<const rule*, void*> + match_impl (action, target&, bool apply); inline void match (action a, target& t) { if (!t.recipe (a)) - match_impl (a, t); + match_impl (a, t, true); t.dependents++; } + group_view + resolve_group_members_impl (action, target_group&); + + inline group_view + resolve_group_members (action a, target_group& g) + { + group_view r (g.members (a)); + return r.members != nullptr ? r : resolve_group_members_impl (a, g); + } + target_state execute_impl (action, target&); diff --git a/build/b.cxx b/build/b.cxx index 8e75c14..ef39446 100644 --- a/build/b.cxx +++ b/build/b.cxx @@ -78,14 +78,16 @@ namespace build #include <build/config/module> -#include <build/bin/target> -#include <build/bin/rule> +#include <build/bin/target> //@@ tmp +#include <build/bin/rule> //@@ tmp #include <build/bin/module> -#include <build/cxx/target> -#include <build/cxx/rule> +#include <build/cxx/target> //@@ tmp +#include <build/cxx/rule> //@@ tmp #include <build/cxx/module> +#include <build/cli/module> + using namespace build; int @@ -95,7 +97,7 @@ main (int argc, char* argv[]) { tracer trace ("main"); - cli::argv_scanner scan (argc, argv, true); + cl::argv_scanner scan (argc, argv, true); options ops (scan); // Version. @@ -134,6 +136,7 @@ main (int argc, char* argv[]) modules["config"] = &config::init; modules["bin"] = &bin::init; modules["cxx"] = &cxx::init; + modules["cli"] = &cli::init; // Register target types. // diff --git a/build/bin/target.cxx b/build/bin/target.cxx index 1d69063..66eadb7 100644 --- a/build/bin/target.cxx +++ b/build/bin/target.cxx @@ -28,6 +28,7 @@ namespace build "obja", &file::static_type, &obja_factory, + nullptr, &search_file }; @@ -49,6 +50,7 @@ namespace build "objso", &file::static_type, &objso_factory, + nullptr, &search_file }; @@ -74,6 +76,7 @@ namespace build "obj", &target::static_type, &obj_factory, + nullptr, &search_target }; @@ -83,6 +86,7 @@ namespace build "exe", &file::static_type, &target_factory<exe>, + nullptr, &search_file }; @@ -104,6 +108,7 @@ namespace build "liba", &file::static_type, &liba_factory, + nullptr, &search_file }; @@ -125,6 +130,7 @@ namespace build "libso", &file::static_type, &libso_factory, + nullptr, &search_file }; @@ -150,6 +156,7 @@ namespace build "lib", &target::static_type, &lib_factory, + nullptr, &search_target }; } diff --git a/build/buildfile b/build/buildfile index b9c1ca2..7885c1d 100644 --- a/build/buildfile +++ b/build/buildfile @@ -7,11 +7,13 @@ import libs += libbutl config = config/{operation module utility} bin = bin/{target rule module} cxx = cxx/{target rule module} +cli = cli/{target rule module} exe{b}: cxx{b algorithm name operation spec scope variable target \ prerequisite rule file module context search diagnostics token \ - lexer parser path-io utility dump options $config $bin $cxx} $libs + lexer parser path-io utility dump options $config $bin $cxx $cli} \ + $libs #@@ TODO # -# cli --include-with-brackets --include-prefix build --guard-prefix BUILD --hxx-suffix "" options.cli +# cli --cli-namespace cl --include-with-brackets --include-prefix build --guard-prefix BUILD --hxx-suffix "" options.cli diff --git a/build/cli/module b/build/cli/module new file mode 100644 index 0000000..c2bc0ab --- /dev/null +++ b/build/cli/module @@ -0,0 +1,20 @@ +// file : build/cli/module -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD_CLI_MODULE +#define BUILD_CLI_MODULE + +#include <build/types> +#include <build/module> + +namespace build +{ + namespace cli + { + void + init (scope&, scope&, const location&); + } +} + +#endif // BUILD_CLI_MODULE diff --git a/build/cli/module.cxx b/build/cli/module.cxx new file mode 100644 index 0000000..96da40d --- /dev/null +++ b/build/cli/module.cxx @@ -0,0 +1,147 @@ +// file : build/cli/module.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <build/cli/module> + +#include <butl/process> +#include <butl/fdstream> + +#include <build/scope> +#include <build/target> +#include <build/variable> +#include <build/diagnostics> + +#include <build/cxx/target> +#include <build/cxx/module> + +#include <build/config/utility> + +#include <build/cli/target> +#include <build/cli/rule> + +using namespace std; +using namespace butl; + +namespace build +{ + namespace cli + { + static compile compile_; + + void + init (scope& root, scope& base, const location& l) + { + //@@ TODO: avoid multiple inits (generally, for modules). + // + tracer trace ("cli::init"); + + //@@ Should it be this way? + // + if (&root != &base) + fail (l) << "cli module must be initialized in project root scope"; + + // Initialize the cxx module. We need its targets types (?xx{}). + // + cxx::init (root, base, l); + + const dir_path& out_root (root.path ()); + level4 ([&]{trace << "for " << out_root;}); + + // Register our target types. + // + target_types.insert (cli::static_type); + target_types.insert (cli_cxx::static_type); + + // Register our rules. + // + rules[default_id][typeid (cli_cxx)].emplace ("cli.compile", compile_); + rules[update_id][typeid (cli_cxx)].emplace ("cli.compile", compile_); + rules[clean_id][typeid (cli_cxx)].emplace ("cli.compile", compile_); + + rules[default_id][typeid (cxx::cxx)].emplace ("cli.compile", compile_); + rules[update_id][typeid (cxx::cxx)].emplace ("cli.compile", compile_); + rules[clean_id][typeid (cxx::cxx)].emplace ("cli.compile", compile_); + + rules[default_id][typeid (cxx::hxx)].emplace ("cli.compile", compile_); + rules[update_id][typeid (cxx::hxx)].emplace ("cli.compile", compile_); + rules[clean_id][typeid (cxx::hxx)].emplace ("cli.compile", compile_); + + rules[default_id][typeid (cxx::ixx)].emplace ("cli.compile", compile_); + rules[update_id][typeid (cxx::ixx)].emplace ("cli.compile", compile_); + rules[clean_id][typeid (cxx::ixx)].emplace ("cli.compile", compile_); + + // Configure. + // + + // config.cli + // + { + auto r (config::required (root, "config.cli", "cli")); + + // If we actually set a new value, test it by trying to execute. + // + if (r.second) + { + const string& cli (r.first); + const char* args[] = {cli.c_str (), "--version", nullptr}; + + if (verb) + print_process (args); + else + text << "test " << cli; + + string ver; + try + { + process pr (args, false, false, true); + ifdstream is (pr.in_ofd); + + for (bool first (true); !is.eof (); ) + { + string l; + getline (is, l); + + if (first) + { + // The version is the last word on the first line. + // + auto p (l.rfind (' ')); + if (p != string::npos) + ver = string (l, p + 1); + + first = false; + } + } + + if (!pr.wait ()) + throw failed (); + + if (ver.empty ()) + fail << "unexpected output from " << cli; + } + catch (const process_error& e) + { + error << "unable to execute " << cli << ": " << e.what (); + + if (e.child ()) + exit (1); + + throw failed (); + } + + if (verb) + text << cli << " " << ver; + } + } + + // config.cli.options + // + // This one is optional. We also merge it into the corresponding + // cli.* variables. + // + if (auto* v = config::optional<list_value> (root, "config.cli.options")) + root.append ("cli.options") += *v; + } + } +} diff --git a/build/cli/rule b/build/cli/rule new file mode 100644 index 0000000..0f38381 --- /dev/null +++ b/build/cli/rule @@ -0,0 +1,35 @@ +// file : build/cli/rule -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD_CLI_RULE +#define BUILD_CLI_RULE + +#include <build/rule> + +namespace build +{ + namespace cli + { + class compile: public rule + { + public: + virtual void* + match (action, target&, const std::string& hint) const; + + virtual recipe + apply (action, target&, void*) const; + + static target_state + perform_update (action, target&); + + static target_state + perform_clean (action, target&); + + static target_state + delegate (action, target&); + }; + } +} + +#endif // BUILD_CLI_RULE diff --git a/build/cli/rule.cxx b/build/cli/rule.cxx new file mode 100644 index 0000000..abcbb70 --- /dev/null +++ b/build/cli/rule.cxx @@ -0,0 +1,312 @@ +// file : build/cli/rule.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <build/cli/rule> + +#include <butl/process> + +#include <build/scope> +#include <build/target> +#include <build/algorithm> +#include <build/diagnostics> + +#include <build/cli/target> + +#include <build/config/utility> + +using namespace std; +using namespace butl; + +namespace build +{ + namespace cli + { + using config::append_options; + + void* compile:: + match (action a, target& xt, const std::string&) const + { + tracer trace ("cli::compile::match"); + + if (cli_cxx* pt = xt.is_a<cli_cxx> ()) + { + cli_cxx& t (*pt); + + // See if we have a .cli source file. + // + prerequisite* r (nullptr); + for (prerequisite& p: group_prerequisites (t)) + { + if (p.is_a<cli> ()) + { + //@@ Need to verify input and output stems match. + r = &p; + break; + } + } + + if (r == nullptr) + { + level3 ([&]{trace << "no .cli source file for target " << t;}); + return nullptr; + } + + // If we still haven't figured out the member list, we can do + // that now. Specifically, at this stage no further changes to + // cli.options are possible and we can determine whether the + // --suppress-inline option is present. + // + if (t.h () == nullptr) + { + cxx::hxx& h (search<cxx::hxx> (t.dir, t.name, nullptr, nullptr)); + h.group = &t; + t.h (h); + + cxx::cxx& c (search<cxx::cxx> (t.dir, t.name, nullptr, nullptr)); + c.group = &t; + t.c (c); + + bool inl (true); + if (auto val = t["cli.options"]) + { + for (const name& n: val.template as<const list_value&> ()) + { + if (n.value == "--suppress-inline") + { + inl = false; + break; + } + } + } + + if (inl) + { + cxx::ixx& i (search<cxx::ixx> (t.dir, t.name, nullptr, nullptr)); + i.group = &t; + t.i (i); + } + } + + return r; + } + else + { + // One of the ?xx{} members. + // + target& t (xt); + + // First see if we are already linked-up to the cli.cxx{} group. + // If it is some other group, then we are definitely not a match. + // + if (t.group != nullptr) + return t.group->is_a<cli_cxx> (); + + // Then see if there is a corresponding cli.cxx{} group. + // + cli_cxx* g (targets.find<cli_cxx> (t.dir, t.name)); + + // Finally, if this target has a cli{} prerequisite, synthesize + // the group. + // + if (g == nullptr) + { + for (prerequisite& p: group_prerequisites (t)) + { + if (p.is_a<cli> ()) // @@ Need to check that stems match. + { + g = &targets.insert<cli_cxx> (t.dir, t.name, trace); + g->prerequisites.emplace_back (p); + break; + } + } + } + + if (g != nullptr) + { + // Resolve the group's members. This should link us up to + // the group. + // + resolve_group_members (a, *g); + + // For ixx{}, verify it is part of the group. + // + if (t.is_a<cxx::ixx> () && g->i () == nullptr) + { + level3 ([&]{trace << "generation of inline file " << t + << " is disabled with --suppress-inline";}); + g = nullptr; + } + } + + assert (t.group == g); + return g; + } + } + + recipe compile:: + apply (action a, target& xt, void* vp) const + { + if (cli_cxx* pt = xt.is_a<cli_cxx> ()) + { + cli_cxx& t (*pt); + + // Derive file names for the members. + // + t.h ()->derive_path (); + t.c ()->derive_path (); + if (t.i () != nullptr) + t.i ()->derive_path (); + + // Inject dependency on the output directory. + // + inject_parent_fsdir (a, t); + + // Search and match prerequisites. + // + switch (a.operation ()) + { + case default_id: + case update_id: search_and_match (a, t); break; + case clean_id: search_and_match (a, t, t.dir); break; + default: assert (false); // We didn't register for this. + } + + switch (a) + { + case perform_update_id: return &perform_update; + case perform_clean_id: return &perform_clean; + default: return default_recipe; // Forward to prerequisites. + } + } + else + { + cli_cxx& g (*static_cast<cli_cxx*> (vp)); + build::match (a, g); + return &delegate; + } + } + + static void + append_extension (vector<const char*>& args, + path_target& t, + const char* opt, + const char* def) + { + assert (t.ext != nullptr); // Should have been figured out in apply(). + + if (*t.ext != def) + { + // CLI needs the extension with the leading dot (unless it is empty) + // while we store the extension without. But if there is an extension, + // then we can get it (with the dot) from the file name. + // + args.push_back (opt); + args.push_back (t.ext->empty () + ? t.ext->c_str () + : t.path ().extension () - 1); + } + } + + target_state compile:: + perform_update (action a, target& xt) + { + cli_cxx& t (static_cast<cli_cxx&> (xt)); + + // Execute our prerequsites and check if we are out of date. + // + cli* s (execute_prerequisites<cli> (a, t, t.h ()->mtime ())); + + if (s == nullptr) + return target_state::unchanged; + + // Translate source path to relative (to working directory). This + // results in easier to read diagnostics. + // + path relo (relative (t.dir)); + path rels (relative (s->path ())); + + scope& rs (t.root_scope ()); + const string& cli (rs["config.cli"].as<const string&> ()); + + vector<const char*> args {cli.c_str ()}; + + // See if we need to pass any --?xx-suffix options. + // + append_extension (args, *t.h (), "--hxx-suffix", "hxx"); + append_extension (args, *t.c (), "--cxx-suffix", "cxx"); + if (t.i () != nullptr) + append_extension (args, *t.i (), "--ixx-suffix", "ixx"); + + append_options (args, t, "cli.options"); + + if (!relo.empty ()) + { + args.push_back ("-o"); + args.push_back (relo.string ().c_str ()); + } + + args.push_back (rels.string ().c_str ()); + args.push_back (nullptr); + + if (verb) + print_process (args); + else + text << "cli " << *s; + + try + { + process pr (args.data ()); + + if (!pr.wait ()) + throw failed (); + + timestamp ts (system_clock::now ()); + + t.h ()->mtime (ts); + t.c ()->mtime (ts); + if (t.i () != nullptr) + t.i ()->mtime (ts); + + return target_state::changed; + } + catch (const process_error& e) + { + error << "unable to execute " << args[0] << ": " << e.what (); + + if (e.child ()) + exit (1); + + throw failed (); + } + } + + target_state compile:: + perform_clean (action a, target& xt) + { + cli_cxx& t (static_cast<cli_cxx&> (xt)); + + target_state ts (target_state::unchanged); + + if (t.i () != nullptr && + build::perform_clean (a, *t.i ()) == target_state::changed) + ts = target_state::changed; + + if (build::perform_clean (a, *t.c ()) == target_state::changed) + ts = target_state::changed; + + if (build::perform_clean (a, *t.h ()) == target_state::changed) + ts = target_state::changed; + + return ts; + } + + target_state compile:: + delegate (action a, target& t) + { + // Delegate to our group. + // + return execute (a, *t.group); + } + } +} diff --git a/build/cli/target b/build/cli/target new file mode 100644 index 0000000..e5bf16d --- /dev/null +++ b/build/cli/target @@ -0,0 +1,51 @@ +// file : build/cli/target -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD_CLI_TARGET +#define BUILD_CLI_TARGET + +#include <build/target> + +#include <build/cxx/target> + +namespace build +{ + namespace cli + { + class cli: public file + { + public: + using file::file; + + public: + virtual const target_type& type () const {return static_type;} + static const target_type static_type; + }; + + class cli_cxx: public target_group + { + public: + using target_group::target_group; + + target* m[3] {nullptr, nullptr, nullptr}; + + cxx::hxx* h () const {return static_cast<cxx::hxx*> (m[0]);} + cxx::cxx* c () const {return static_cast<cxx::cxx*> (m[1]);} + cxx::ixx* i () const {return static_cast<cxx::ixx*> (m[2]);} + + void h (cxx::hxx& t) {m[0] = &t;} + void c (cxx::cxx& t) {m[1] = &t;} + void i (cxx::ixx& t) {m[2] = &t;} + + virtual group_view + members (action) const; + + public: + virtual const target_type& type () const {return static_type;} + static const target_type static_type; + }; + } +} + +#endif // BUILD_CLI_TARGET diff --git a/build/cli/target.cxx b/build/cli/target.cxx new file mode 100644 index 0000000..2854818 --- /dev/null +++ b/build/cli/target.cxx @@ -0,0 +1,46 @@ +// file : build/cli/target.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <build/cli/target> + +using namespace std; + +namespace build +{ + namespace cli + { + // cli + // + constexpr const char cli_ext[] = "cli"; + const target_type cli::static_type + { + typeid (cli), + "cli", + &file::static_type, + &target_factory<cli>, + &target_extension_fix<cli_ext>, + &search_file + }; + + // cli.cxx + // + group_view cli_cxx:: + members (action) const + { + return m[0] != nullptr + ? group_view {m, (m[2] != nullptr ? 3U : 2U)} + : group_view {nullptr, 0}; + } + + const target_type cli_cxx::static_type + { + typeid (cli_cxx), + "cli.cxx", + &target_group::static_type, + &target_factory<cli_cxx>, + nullptr, + &search_target + }; + } +} diff --git a/build/config/operation.cxx b/build/config/operation.cxx index 89bef61..ecd805f 100644 --- a/build/config/operation.cxx +++ b/build/config/operation.cxx @@ -198,7 +198,7 @@ namespace build for (void* v: ts) { target& t (*static_cast<target*> (v)); - scope* rs (t.root_scope ()); + scope* rs (t.base_scope ().root_scope ()); if (rs == nullptr) fail << "out of project target " << t; diff --git a/build/config/utility b/build/config/utility index 83fcd75..3da990f 100644 --- a/build/config/utility +++ b/build/config/utility @@ -5,9 +5,13 @@ #ifndef BUILD_CONFIG_UTILITY #define BUILD_CONFIG_UTILITY +#include <vector> #include <string> #include <utility> // pair +#include <build/types> +#include <build/diagnostics> + namespace build { class scope; @@ -37,6 +41,26 @@ namespace build template <typename T> const T* optional (scope& root, const char* name); + + // Add all the values from a variable to the C-string list. T is + // either target or scope. + // + template <typename T> + void + append_options (std::vector<const char*>& args, T& s, const char* var) + { + if (auto val = s[var]) + { + for (const name& n: val.template as<const list_value&> ()) + { + if (!n.type.empty () || !n.dir.empty ()) + fail << "expected option instead of " << n << + info << "in variable " << var; + + args.push_back (n.value.c_str ()); + } + } + } } } diff --git a/build/context b/build/context index dc865d2..44948ec 100644 --- a/build/context +++ b/build/context @@ -97,7 +97,8 @@ namespace build // If possible and beneficial, translate an absolute, normalized path // into relative to the relative_base directory, which is normally - // work. + // work. Note that if the passed path is the same as relative_base, + // then this function returns empty path. // template <typename K> basic_path<char, K> diff --git a/build/cxx/module.cxx b/build/cxx/module.cxx index 21f1b85..495819d 100644 --- a/build/cxx/module.cxx +++ b/build/cxx/module.cxx @@ -87,7 +87,8 @@ namespace build throw failed (); } - //text << "toolchain version " << ver; + if (verb) + text << cxx << " " << ver; } } diff --git a/build/cxx/rule.cxx b/build/cxx/rule.cxx index e4c0dd7..cba1a1b 100644 --- a/build/cxx/rule.cxx +++ b/build/cxx/rule.cxx @@ -24,6 +24,8 @@ #include <build/bin/target> #include <build/cxx/target> +#include <build/config/utility> + using namespace std; using namespace butl; @@ -33,24 +35,7 @@ namespace build { using namespace bin; - // T is either target or scope. - // - template <typename T> - static void - append_options (vector<const char*>& args, T& s, const char* var) - { - if (auto val = s[var]) - { - for (const name& n: val.template as<const list_value&> ()) - { - if (!n.type.empty () || !n.dir.empty ()) - fail << "expected option instead of " << n << - info << "in variable " << var; - - args.push_back (n.value.c_str ()); - } - } - } + using config::append_options; static void append_std (vector<const char*>& args, target& t, string& opt) @@ -127,12 +112,7 @@ namespace build // Derive file name from target name. // if (t.path ().empty ()) - { - if (t.is_a <obja> ()) - t.path (t.derived_path ("o")); - else - t.path (t.derived_path ("o", nullptr, "-so")); - } + t.derive_path ("o", nullptr, (t.is_a<objso> () ? "-so" : nullptr)); // Inject dependency on the output directory. // @@ -236,7 +216,7 @@ namespace build { tracer trace ("cxx::compile::inject_prerequisites"); - scope& rs (*t.root_scope ()); // Shouldn't have matched if nullptr. + scope& rs (t.root_scope ()); const string& cxx (rs["config.cxx"].as<const string&> ()); vector<const char*> args {cxx.c_str ()}; @@ -346,7 +326,20 @@ namespace build // then assume it is a header. Otherwise, let the standard // mechanism derive the type from the extension. @@ TODO. // - path_target& pt (search<hxx> (d, n, e, &ds)); + const target_type* tt (&hxx::static_type); + + //@@ TMP + // + if (e != nullptr) + { + if (*e == "ixx") + tt = &ixx::static_type; + else if (*e == "txx") + tt = &txx::static_type; + } + + path_target& pt ( + static_cast<path_target&> (search (*tt, d, n, e, &ds))); // Assign path. // @@ -398,7 +391,7 @@ namespace build path relo (relative (t.path ())); path rels (relative (s->path ())); - scope& rs (*t.root_scope ()); // Shouldn't have matched if nullptr. + scope& rs (t.root_scope ()); const string& cxx (rs["config.cxx"].as<const string&> ()); vector<const char*> args {cxx.c_str ()}; @@ -580,9 +573,9 @@ namespace build { switch (lt) { - case type::e: t.path (t.derived_path ( )); break; - case type::a: t.path (t.derived_path ("a", "lib")); break; - case type::so: t.path (t.derived_path ("so", "lib")); break; + case type::e: t.derive_path ("" ); break; + case type::a: t.derive_path ("a", "lib"); break; + case type::so: t.derive_path ("so", "lib"); break; } } @@ -674,8 +667,7 @@ namespace build // but possible, the prerequisite is from a different project // altogether. So we are going to use the target's project. // - root = t.root_scope (); - assert (root != nullptr); // Otherwise shouldn't have matched. + root = &t.root_scope (); out_root = &root->path (); src_root = &root->src_path (); } @@ -837,7 +829,7 @@ namespace build // path relt (relative (t.path ())); - scope& rs (*t.root_scope ()); // Shouldn't have matched if nullptr. + scope& rs (t.root_scope ()); vector<const char*> args; string storage1; diff --git a/build/cxx/target.cxx b/build/cxx/target.cxx index e02801b..9fd5487 100644 --- a/build/cxx/target.cxx +++ b/build/cxx/target.cxx @@ -10,57 +10,69 @@ namespace build { namespace cxx { + constexpr const char hxx_ext_var[] = "hxx.ext"; const target_type hxx::static_type { typeid (hxx), "hxx", &file::static_type, &target_factory<hxx>, + &target_extension_var<hxx_ext_var>, &search_file }; + constexpr const char ixx_ext_var[] = "ixx.ext"; const target_type ixx::static_type { typeid (ixx), "ixx", &file::static_type, &target_factory<ixx>, + &target_extension_var<ixx_ext_var>, &search_file }; + constexpr const char txx_ext_var[] = "txx.ext"; const target_type txx::static_type { typeid (txx), "txx", &file::static_type, &target_factory<txx>, + &target_extension_var<txx_ext_var>, &search_file }; + constexpr const char cxx_ext_var[] = "cxx.ext"; const target_type cxx::static_type { typeid (cxx), "cxx", &file::static_type, &target_factory<cxx>, + &target_extension_var<cxx_ext_var>, &search_file }; + constexpr const char h_ext_var[] = "h.ext"; const target_type h::static_type { typeid (h), "h", &file::static_type, &target_factory<h>, + &target_extension_var<h_ext_var>, &search_file }; + constexpr const char c_ext_var[] = "c.ext"; const target_type c::static_type { typeid (c), "c", &file::static_type, &target_factory<c>, + &target_extension_var<c_ext_var>, &search_file }; } diff --git a/build/options b/build/options index 053c066..c0b9930 100644 --- a/build/options +++ b/build/options @@ -17,7 +17,7 @@ #include <cstddef> #include <exception> -namespace cli +namespace cl { class unknown_mode { @@ -211,34 +211,34 @@ class options options (int& argc, char** argv, bool erase = false, - ::cli::unknown_mode option = ::cli::unknown_mode::fail, - ::cli::unknown_mode argument = ::cli::unknown_mode::stop); + ::cl::unknown_mode option = ::cl::unknown_mode::fail, + ::cl::unknown_mode argument = ::cl::unknown_mode::stop); options (int start, int& argc, char** argv, bool erase = false, - ::cli::unknown_mode option = ::cli::unknown_mode::fail, - ::cli::unknown_mode argument = ::cli::unknown_mode::stop); + ::cl::unknown_mode option = ::cl::unknown_mode::fail, + ::cl::unknown_mode argument = ::cl::unknown_mode::stop); options (int& argc, char** argv, int& end, bool erase = false, - ::cli::unknown_mode option = ::cli::unknown_mode::fail, - ::cli::unknown_mode argument = ::cli::unknown_mode::stop); + ::cl::unknown_mode option = ::cl::unknown_mode::fail, + ::cl::unknown_mode argument = ::cl::unknown_mode::stop); options (int start, int& argc, char** argv, int& end, bool erase = false, - ::cli::unknown_mode option = ::cli::unknown_mode::fail, - ::cli::unknown_mode argument = ::cli::unknown_mode::stop); + ::cl::unknown_mode option = ::cl::unknown_mode::fail, + ::cl::unknown_mode argument = ::cl::unknown_mode::stop); - options (::cli::scanner&, - ::cli::unknown_mode option = ::cli::unknown_mode::fail, - ::cli::unknown_mode argument = ::cli::unknown_mode::stop); + options (::cl::scanner&, + ::cl::unknown_mode option = ::cl::unknown_mode::fail, + ::cl::unknown_mode argument = ::cl::unknown_mode::stop); options (); @@ -265,13 +265,13 @@ class options // protected: bool - _parse (const char*, ::cli::scanner&); + _parse (const char*, ::cl::scanner&); private: void - _parse (::cli::scanner&, - ::cli::unknown_mode option, - ::cli::unknown_mode argument); + _parse (::cl::scanner&, + ::cl::unknown_mode option, + ::cl::unknown_mode argument); public: bool help_; diff --git a/build/options.cxx b/build/options.cxx index 0d4248d..3c1e194 100644 --- a/build/options.cxx +++ b/build/options.cxx @@ -18,7 +18,7 @@ #include <ostream> #include <sstream> -namespace cli +namespace cl { // unknown_option // @@ -323,14 +323,14 @@ options:: options (int& argc, char** argv, bool erase, - ::cli::unknown_mode opt, - ::cli::unknown_mode arg) + ::cl::unknown_mode opt, + ::cl::unknown_mode arg) : help_ (), version_ (), v_ (), verbose_ (0) { - ::cli::argv_scanner s (argc, argv, erase); + ::cl::argv_scanner s (argc, argv, erase); _parse (s, opt, arg); } @@ -339,14 +339,14 @@ options (int start, int& argc, char** argv, bool erase, - ::cli::unknown_mode opt, - ::cli::unknown_mode arg) + ::cl::unknown_mode opt, + ::cl::unknown_mode arg) : help_ (), version_ (), v_ (), verbose_ (0) { - ::cli::argv_scanner s (start, argc, argv, erase); + ::cl::argv_scanner s (start, argc, argv, erase); _parse (s, opt, arg); } @@ -355,14 +355,14 @@ options (int& argc, char** argv, int& end, bool erase, - ::cli::unknown_mode opt, - ::cli::unknown_mode arg) + ::cl::unknown_mode opt, + ::cl::unknown_mode arg) : help_ (), version_ (), v_ (), verbose_ (0) { - ::cli::argv_scanner s (argc, argv, erase); + ::cl::argv_scanner s (argc, argv, erase); _parse (s, opt, arg); end = s.end (); } @@ -373,22 +373,22 @@ options (int start, char** argv, int& end, bool erase, - ::cli::unknown_mode opt, - ::cli::unknown_mode arg) + ::cl::unknown_mode opt, + ::cl::unknown_mode arg) : help_ (), version_ (), v_ (), verbose_ (0) { - ::cli::argv_scanner s (start, argc, argv, erase); + ::cl::argv_scanner s (start, argc, argv, erase); _parse (s, opt, arg); end = s.end (); } options:: -options (::cli::scanner& s, - ::cli::unknown_mode opt, - ::cli::unknown_mode arg) +options (::cl::scanner& s, + ::cl::unknown_mode opt, + ::cl::unknown_mode arg) : help_ (), version_ (), v_ (), @@ -411,7 +411,7 @@ print_usage (::std::ostream& os) } typedef -std::map<std::string, void (*) (options&, ::cli::scanner&)> +std::map<std::string, void (*) (options&, ::cl::scanner&)> _cli_options_map; static _cli_options_map _cli_options_map_; @@ -421,20 +421,20 @@ struct _cli_options_map_init _cli_options_map_init () { _cli_options_map_["--help"] = - &::cli::thunk< options, bool, &options::help_ >; + &::cl::thunk< options, bool, &options::help_ >; _cli_options_map_["--version"] = - &::cli::thunk< options, bool, &options::version_ >; + &::cl::thunk< options, bool, &options::version_ >; _cli_options_map_["-v"] = - &::cli::thunk< options, bool, &options::v_ >; + &::cl::thunk< options, bool, &options::v_ >; _cli_options_map_["--verbose"] = - &::cli::thunk< options, std::uint16_t, &options::verbose_ >; + &::cl::thunk< options, std::uint16_t, &options::verbose_ >; } }; static _cli_options_map_init _cli_options_map_init_; bool options:: -_parse (const char* o, ::cli::scanner& s) +_parse (const char* o, ::cl::scanner& s) { _cli_options_map::const_iterator i (_cli_options_map_.find (o)); @@ -448,9 +448,9 @@ _parse (const char* o, ::cli::scanner& s) } void options:: -_parse (::cli::scanner& s, - ::cli::unknown_mode opt_mode, - ::cli::unknown_mode arg_mode) +_parse (::cl::scanner& s, + ::cl::unknown_mode opt_mode, + ::cl::unknown_mode arg_mode) { bool opt = true; @@ -470,18 +470,18 @@ _parse (::cli::scanner& s, { switch (opt_mode) { - case ::cli::unknown_mode::skip: + case ::cl::unknown_mode::skip: { s.skip (); continue; } - case ::cli::unknown_mode::stop: + case ::cl::unknown_mode::stop: { break; } - case ::cli::unknown_mode::fail: + case ::cl::unknown_mode::fail: { - throw ::cli::unknown_option (o); + throw ::cl::unknown_option (o); } } @@ -491,18 +491,18 @@ _parse (::cli::scanner& s, { switch (arg_mode) { - case ::cli::unknown_mode::skip: + case ::cl::unknown_mode::skip: { s.skip (); continue; } - case ::cli::unknown_mode::stop: + case ::cl::unknown_mode::stop: { break; } - case ::cli::unknown_mode::fail: + case ::cl::unknown_mode::fail: { - throw ::cli::unknown_argument (o); + throw ::cl::unknown_argument (o); } } diff --git a/build/options.ixx b/build/options.ixx index d242ba5..671f9ec 100644 --- a/build/options.ixx +++ b/build/options.ixx @@ -9,7 +9,7 @@ // // End prologue. -namespace cli +namespace cl { // unknown_mode // diff --git a/build/rule.cxx b/build/rule.cxx index b3dfaeb..baed3ba 100644 --- a/build/rule.cxx +++ b/build/rule.cxx @@ -49,22 +49,13 @@ namespace build { case perform_update_id: { - // @@ TODO: - // - // - need to try all the target-type-specific extensions, just - // like search_existing_file(). - // path_target& pt (dynamic_cast<path_target&> (t)); - // Assign the path. While nromally we shouldn't do this in match(), + // Assign the path. While normally we shouldn't do this in match(), // no other rule should ever be ambiguous with the fallback one. // if (pt.path ().empty ()) - { - // @@ TMP: using target name as the default extension. - // - pt.path (pt.derived_path (pt.type ().name)); - } + pt.derive_path (); return pt.mtime () != timestamp_nonexistent ? &t : nullptr; } diff --git a/build/search.cxx b/build/search.cxx index feff123..dec86db 100644 --- a/build/search.cxx +++ b/build/search.cxx @@ -56,28 +56,48 @@ namespace build } target* - search_existing_file (const prerequisite_key& pk, const dir_paths& sp) + search_existing_file (const prerequisite_key& cpk, const dir_paths& sp) { tracer trace ("search_existing_file"); - const target_key& tk (pk.tk); + prerequisite_key pk (cpk); // Make a copy so we can update extension. + target_key& tk (pk.tk); assert (tk.dir->relative ()); - // Go over paths and extension looking for a file. + // Figure out the extension. Pretty similar logic to file::derive_path(). + // + const string* ext (*tk.ext); + + if (ext == nullptr) + { + if (auto f = tk.type->extension) + { + ext = &f (tk, *pk.scope); // Already from the pool. + tk.ext = &ext; + } + else + // What should we do here, fail or say we didn't find anything? + // Current think is that if the target type didn't provide the + // default extension, then it doesn't want us to search for an + // existing file (of course, if the user specified the extension + // explicitly, we will still do so). But let me know what you + // think. + // + //fail << "no default extension for prerequisite " << pk; + return nullptr; + } + + // Go over paths looking for a file. // for (const dir_path& d: sp) { path f (d / *tk.dir / path (*tk.name)); f.normalize (); - // @@ TMP: use target name as an extension. - // - const string& e (*tk.ext != nullptr ? **tk.ext : tk.type->name); - - if (!e.empty ()) + if (!ext->empty ()) { f += '.'; - f += e; + f += *ext; } timestamp mt (file_mtime (f)); @@ -88,11 +108,9 @@ namespace build level4 ([&]{trace << "found existing file " << f << " for prerequisite " << pk;}); - // Find or insert. + // Find or insert. Note: using our updated extension. // - auto r ( - targets.insert ( - *tk.type, f.directory (), *tk.name, *tk.ext, trace)); + auto r (targets.insert (*tk.type, f.directory (), *tk.name, ext, trace)); // Has to be a path_target. // @@ -108,6 +126,7 @@ namespace build return &t; } + level3 ([&]{trace << "no existing file found for prerequisite " << pk;}); return nullptr; } diff --git a/build/target b/build/target index 8eb7813..ddf5a2e 100644 --- a/build/target +++ b/build/target @@ -24,12 +24,12 @@ #include <build/operation> #include <build/target-key> #include <build/prerequisite> -#include <build/utility> // extension_pool namespace build { class scope; class target; + class target_group; // Target state. // @@ -111,12 +111,17 @@ namespace build const std::string* ext; // Extension, NULL means unspecified, // empty means no extension. + //@@ Make target_group. target* group {nullptr}; // Target group to which this target belongs, // if any. Note that we assume that the group // and all its members are in the same scope // (see, for example, variable lookup). // We also currently assume that there are // no multi-level groups. + + target_key + key () const {return target_key {&type (), &dir, &name, &ext};} + public: // Most qualified scope that contains this target. // @@ -125,9 +130,10 @@ namespace build // Root scope of a project that contains this target. Note that // a target can be out of any (known) project root in which case - // NULL is returned. + // this function asserts. If you need to detect this situation, + // then use base_scope().root_scope() expression instead. // - scope* + scope& root_scope () const; // Prerequisites. @@ -429,6 +435,14 @@ namespace build const std::string* ext, tracer&); + template <typename T> + T& + insert (const dir_path& dir, const std::string& name, tracer& t) + { + return static_cast<T&> ( + insert (T::static_type, dir, name, nullptr, t).first); + } + void clear () {map_.clear ();} @@ -463,6 +477,26 @@ namespace build extern target_type_map target_types; + // Target group. + // + struct group_view + { + target* const* members; // NULL means not yet known. + std::size_t count; + }; + + class target_group: public target + { + public: + using target::target; + + virtual group_view + members (action) const = 0; + + public: + static const target_type static_type; + }; + // Modification time-based target. // class mtime_target: public target @@ -508,16 +542,20 @@ namespace build void path (path_type p) {assert (path_.empty ()); path_ = std::move (p);} - // Return a path derived from target's dir, name, and, if specified, - // ext. If ext is not specified, then use default_ext. If name_prefix - // if not NULL, add it before the name part and after the directory. - // Similarly, if name_suffix if not NULL, add it after the name part - // and before the extension. + // Derive a path from target's dir, name, and, if specified, ext. + // If ext is not specified, then use default_ext and also update + // the target's extension (this becomes important if later we need + // to reliably determine whether this file has an extension; think + // hxx{foo.bar.} and hxx.ext is empty). // - path_type - derived_path (const char* default_ext = nullptr, - const char* name_prefix = nullptr, - const char* name_suffix = nullptr); + // If name_prefix is not NULL, add it before the name part and after + // the directory. Similarly, if name_suffix is not NULL, add it after + // the name part and before the extension. + // + void + derive_path (const char* default_ext = nullptr, + const char* name_prefix = nullptr, + const char* name_suffix = nullptr); public: static const target_type static_type; @@ -573,7 +611,8 @@ namespace build static const target_type static_type; }; - // Common implementation of the target factory and search functions. + // Common implementation of the target factory, extension, and + // search functions. // template <typename T> target* @@ -582,13 +621,25 @@ namespace build return new T (std::move (d), std::move (n), e); } + // Return fixed target extension. + // + template <const char* ext> + const std::string& + target_extension_fix (const target_key&, scope&); + + // Get the extension from the variable. + // + template <const char* var> + const std::string& + target_extension_var (const target_key&, scope&); + // The default behavior, that is, look for an existing target in the // prerequisite's directory scope. // target* search_target (const prerequisite_key&); - // First lookfor an existing target as above. If not found, then look + // First look for an existing target as above. If not found, then look // for an existing file in the target-type-specific list of paths. // target* @@ -597,5 +648,6 @@ namespace build } #include <build/target.ixx> +#include <build/target.txx> #endif // BUILD_TARGET diff --git a/build/target-key b/build/target-key index 04a5b26..3875a6a 100644 --- a/build/target-key +++ b/build/target-key @@ -26,6 +26,7 @@ namespace build const char* name; const target_type* base; target* (*const factory) (dir_path, std::string, const std::string*); + const std::string& (*const extension) (const target_key&, scope&); target* (*const search) (const prerequisite_key&); }; diff --git a/build/target.cxx b/build/target.cxx index 1040443..87194fb 100644 --- a/build/target.cxx +++ b/build/target.cxx @@ -4,6 +4,8 @@ #include <build/target> +#include <cassert> + #include <butl/filesystem> #include <build/scope> @@ -29,12 +31,14 @@ namespace build return scopes.find (dir); } - scope* target:: + scope& target:: root_scope () const { // This is tricky to cache so we do the lookup for now. // - return scopes.find (dir).root_scope (); + scope* r (scopes.find (dir).root_scope ()); + assert (r != nullptr); + return *r; } value_proxy target:: @@ -240,8 +244,8 @@ namespace build // path_target // - path path_target:: - derived_path (const char* de, const char* np, const char* ns) + void path_target:: + derive_path (const char* de, const char* np, const char* ns) { string n; @@ -253,21 +257,35 @@ namespace build if (ns != nullptr) n += ns; - if (ext != nullptr) + // Update the extension. + // + // See also search_existing_file() if updating anything here. + // + if (ext == nullptr) { - if (!ext->empty ()) - { - n += '.'; - n += *ext; - } + // If provided by the caller, then use that. + // + if (de != nullptr) + ext = &extension_pool.find (de); + // + // Otherwis see if the target type has function that will + // give us the default extension. + // + else if (auto f = type ().extension) + ext = &f (key (), base_scope ()); // Already from the pool. + else + fail << "no default extension for target " << *this; } - else if (de != nullptr) + + // Add the extension. + // + if (!ext->empty ()) { n += '.'; - n += de; + n += *ext; } - return dir / path_type (move (n)); + path (dir / path_type (move (n))); } // file_target @@ -337,6 +355,17 @@ namespace build "target", nullptr, nullptr, + nullptr, + &search_target, + }; + + const target_type target_group::static_type + { + typeid (target_group), + "target_group", + &target::static_type, + nullptr, + nullptr, &search_target }; @@ -346,6 +375,7 @@ namespace build "mtime_target", &target::static_type, nullptr, + nullptr, &search_target }; @@ -355,15 +385,29 @@ namespace build "path_target", &mtime_target::static_type, nullptr, + nullptr, &search_target }; + static target* + file_factory (dir_path d, string n, const string* e) + { + // The file target type doesn't imply any extension. So if one + // wasn't specified, set it to empty rather than unspecified. + // In other words, we always treat file{foo} as file{foo.}. + // + return new file (move (d), + move (n), + (e != nullptr ? e : &extension_pool.find (""))); + } + const target_type file::static_type { typeid (file), "file", &path_target::static_type, - &target_factory<file>, + &file_factory, + nullptr, // Factory always assigns an extension. &search_file }; @@ -373,6 +417,7 @@ namespace build "dir", &target::static_type, &target_factory<dir>, + nullptr, // Should never need. &search_alias }; @@ -382,6 +427,7 @@ namespace build "fsdir", &target::static_type, &target_factory<fsdir>, + nullptr, // Should never need. &search_target }; } diff --git a/build/target.txx b/build/target.txx new file mode 100644 index 0000000..6fe3f33 --- /dev/null +++ b/build/target.txx @@ -0,0 +1,44 @@ +// file : build/target.txx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <build/scope> +#include <build/utility> // extension_pool +#include <build/diagnostics> +#include <build/prerequisite> + +namespace build +{ + template <const char* ext> + const std::string& + target_extension_fix (const target_key&, scope&) + { + return extension_pool.find (ext); + } + + template <const char* var> + const std::string& + target_extension_var (const target_key& tk, scope& s) + { + auto val (s[var]); + + if (!val) + { + diag_record dr; + dr << fail << "no default extension in variable " << var + << info << "required to derive file name for "; + + // This is a bit hacky: we may be dealing with a target (see + // file::derive_path()) or prerequsite (see search_existing_file()). + // So we are going to check if dir is absolute. If it is, then + // we assume this is a target, otherwise -- prerequsite. + // + if (tk.dir->absolute ()) + dr << "target " << tk; + else + dr << "prerequisite " << prerequisite_key {tk, &s}; + } + + return extension_pool.find (val.as<const std::string&> ()); + } +} diff --git a/build/utility b/build/utility index 4135b37..90d1694 100644 --- a/build/utility +++ b/build/utility @@ -75,6 +75,9 @@ namespace build { const std::string& find (const char* s) {return *emplace (s).first;} + + const std::string& + find (const std::string& s) {return *emplace (s).first;} }; extern string_pool extension_pool; diff --git a/tests/cli/build/bootstrap.build b/tests/cli/build/bootstrap.build new file mode 100644 index 0000000..9e91c9a --- /dev/null +++ b/tests/cli/build/bootstrap.build @@ -0,0 +1,2 @@ +project = cli-test +using config diff --git a/tests/cli/build/root.build b/tests/cli/build/root.build new file mode 100644 index 0000000..8e910cf --- /dev/null +++ b/tests/cli/build/root.build @@ -0,0 +1,3 @@ +using cxx +using cli + diff --git a/tests/cli/buildfile b/tests/cli/buildfile new file mode 100644 index 0000000..705f302 --- /dev/null +++ b/tests/cli/buildfile @@ -0,0 +1,6 @@ +hxx.ext = hpp +cxx.ext = cpp +ixx.ext = ipp + +exe{test}: cxx{driver test} +cxx{test}: cli{test} diff --git a/tests/cli/driver.cpp b/tests/cli/driver.cpp new file mode 100644 index 0000000..70b4146 --- /dev/null +++ b/tests/cli/driver.cpp @@ -0,0 +1,4 @@ +int +main () +{ +} diff --git a/tests/cli/test.cli b/tests/cli/test.cli new file mode 100644 index 0000000..db3cfb8 --- /dev/null +++ b/tests/cli/test.cli @@ -0,0 +1,5 @@ +class options +{ + bool --help; + bool --version; +}; |