From d20d2a641351b7f9e8c9bd9b841d8de4d824aa82 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Sat, 10 Aug 2019 17:14:37 +0300 Subject: Add default options loading and merging API --- libbutl/default-options.mxx | 115 ++++++++++++++++++++++++ libbutl/default-options.txx | 166 ++++++++++++++++++++++++++++++++++ tests/default-options/buildfile | 7 ++ tests/default-options/driver.cxx | 186 +++++++++++++++++++++++++++++++++++++++ tests/default-options/testscript | 109 +++++++++++++++++++++++ 5 files changed, 583 insertions(+) create mode 100644 libbutl/default-options.mxx create mode 100644 libbutl/default-options.txx create mode 100644 tests/default-options/buildfile create mode 100644 tests/default-options/driver.cxx create mode 100644 tests/default-options/testscript diff --git a/libbutl/default-options.mxx b/libbutl/default-options.mxx new file mode 100644 index 0000000..01d32c1 --- /dev/null +++ b/libbutl/default-options.mxx @@ -0,0 +1,115 @@ +// file : libbutl/default-options.mxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef __cpp_modules_ts +#pragma once +#endif + +#ifndef __cpp_lib_modules_ts +#include // move() +#endif + +// Other includes. + +#ifdef __cpp_modules_ts +export module butl.default_options; +#ifdef __cpp_lib_modules_ts +import std.core; +#endif +import butl.path; +import butl.optional; +import butl.small_vector; + +import butl.git; +import butl.filesystem; +#else +#include +#include +#include + +#include +#include +#endif + +#include + +LIBBUTL_MODEXPORT namespace butl +{ + // Default options files helper implementation. + // + struct default_options_files + { + small_vector files; + optional start_dir; + }; + + template + struct default_options_entry + { + path file; + O options; + bool remote; + }; + + template + using default_options = small_vector, 4>; + + // Search for and load (using scanner S and parsing in the U::fail mode for + // both options and arguments) the specified list of options files in the + // specified directories returning a vector of option class instances (O). + // Throw std::system_error on the underlying OS error and pass through + // exceptions thrown by the options scanner/parser. + // + // Search order: + // + // - sys_dir + // - home_dir + // - start_dir and outer until home_dir or root (both excluding) + // + // Except for sys_dir, the options files are looked for in the .build2/ and + // .build2/local/ subdirectories of each directory. For sys_dir they are + // looked for in the directory itself (e.g., /etc/build2/). + // + // Note that all the directories should be absolute and normalized. + // + // The presence of the .git filesystem entry causes the options files in + // this directory and any of its subdirectories to be considered remote + // (note that in the current implementation this is the case even for files + // from the .build2/local/ subdirectory since the mere location is not a + // sufficient ground to definititevly conclude that the file is not remote; + // to be sure we would need to query the VCS or some such). + // + template + default_options + load_default_options (const optional& sys_dir, + const optional& home_dir, + const default_options_files&); + + // Merge the default options and the command line options. + // + // Note that this is the default implementation and in some cases you may + // want to provide an options class-specific version that verifies/sanitizes + // the default options (e.g., you may not want to allow certain options to + // be specified in the default options files) or warns/prompts about + // potentially dangerous options if they came from the remote options files. + // + template + O + merge_default_options (const default_options&, const O& cmd_ops); + + // As above but pass each default option to the specified function prior to + // merging. The function signature is: + // + // void (const default_options_entry&, const O& cmd_ops) + // + // This version can be used to verify the default options. For example, you + // may want to disallow certain options from being specified in the default + // options files. + // + template + O + merge_default_options (const default_options&, const O&, F&&); +} + +#include diff --git a/libbutl/default-options.txx b/libbutl/default-options.txx new file mode 100644 index 0000000..42dd585 --- /dev/null +++ b/libbutl/default-options.txx @@ -0,0 +1,166 @@ +// file : libbutl/default-options.txx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +LIBBUTL_MODEXPORT namespace butl //@@ MOD Clang needs this for some reason. +{ + // Search for and parse the options files in the specified directory and + // its local/ subdirectory, if exists, and append the options to the + // resulting list. + // + // Note that we check for the local/ subdirectory even if we don't think it + // belongs to the remote directory; the user may move things around or it + // could be a VCS we don't yet recognize and there doesn't seem to be any + // harm in doing so. + // + // Also note that if the directory is remote, then for now the options files + // in both the directory itself and its local/ subdirectory are considered + // remote (see load_default_options() for details). + // + template + void + load_default_options_files (const dir_path& d, + bool remote, + const small_vector& fs, + default_options& r) + { + auto load = [&fs, &r] (const dir_path& d, bool remote) + { + using namespace std; + + for (const path& f: fs) + { + path p (d / f); + + if (file_exists (p)) // Follows symlinks. + { + S s (p.string ()); + + // @@ Note that the potentially thrown exceptions (unknown option, + // unexpected argument, etc) will not contain any location + // information. Intercepting exception handling to add the file + // attribution feels too hairy for now. Maybe we should support + // this in CLI. + // + O o; + o.parse (s, U::fail, U::fail); + + r.push_back (default_options_entry {move (p), move (o), remote}); + } + } + }; + + load (d, remote); + + dir_path ld (d / dir_path ("local")); + + if (dir_exists (ld)) + load (ld, remote); + } + + // Search for and parse the options files in the specified and outer + // directories until root/home directory (excluding) and append the options + // to the resulting list. Return true if the directory is "remote" (i.e., + // belongs to a VCS repository). + // + template + bool + load_default_options_files (const dir_path& start_dir, + const optional& home_dir, + const small_vector& fs, + default_options& r) + { + if (start_dir.root () || (home_dir && start_dir == *home_dir)) + return false; + + bool remote (load_default_options_files (start_dir.directory (), + home_dir, + fs, + r) || + git_repository (start_dir)); + + dir_path d (start_dir / dir_path (".build2")); + + if (dir_exists (d)) + load_default_options_files (d, remote, fs, r); + + return remote; + } + + template + default_options + load_default_options (const optional& sys_dir, + const optional& home_dir, + const default_options_files& ofs) + { + default_options r; + + if (sys_dir) + { + assert (sys_dir->absolute () && sys_dir->normalized ()); + + if (dir_exists (*sys_dir)) + load_default_options_files (*sys_dir, + false /* remote */, + ofs.files, + r); + } + + if (home_dir) + { + assert (home_dir->absolute () && home_dir->normalized ()); + + dir_path d (*home_dir / dir_path (".build2")); + + if (dir_exists (d)) + load_default_options_files (d, + false /* remote */, + ofs.files, + r); + } + + if (ofs.start_dir) + { + assert (ofs.start_dir->absolute () && ofs.start_dir->normalized ()); + + load_default_options_files (*ofs.start_dir, + home_dir, + ofs.files, + r); + } + + return r; + } + + template + O + merge_default_options (const default_options& def_ops, + const O& cmd_ops, + F&& f) + { + // Optimize for the common case. + // + if (def_ops.empty ()) + return cmd_ops; + + O r; + for (const default_options_entry& e: def_ops) + { + f (e, cmd_ops); + r.merge (e.options); + } + + r.merge (cmd_ops); + return r; + } + + template + inline O + merge_default_options (const default_options& def_ops, const O& cmd_ops) + { + return merge_default_options ( + def_ops, + cmd_ops, + [] (const default_options_entry&, const O&) {}); + } +} diff --git a/tests/default-options/buildfile b/tests/default-options/buildfile new file mode 100644 index 0000000..5530a10 --- /dev/null +++ b/tests/default-options/buildfile @@ -0,0 +1,7 @@ +# file : tests/default-options/buildfile +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import libs = libbutl%lib{butl} + +exe{driver}: {hxx cxx}{*} $libs testscript diff --git a/tests/default-options/driver.cxx b/tests/default-options/driver.cxx new file mode 100644 index 0000000..a2ed43d --- /dev/null +++ b/tests/default-options/driver.cxx @@ -0,0 +1,186 @@ +// file : tests/default-options/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#ifndef __cpp_lib_modules_ts +#include +#include +#include +#endif + +// Other includes. + +#ifdef __cpp_modules_ts +#ifdef __cpp_lib_modules_ts +import std.core; +import std.io; +#endif +import butl.path; +import butl.path_io; +import butl.optional; +import butl.fdstream; +import butl.default_options; +#else +#include +#include +#include // eof() +#include +#include +#include +#endif + +using namespace std; +using namespace butl; + +// Usage: argv[0] [-f ] [-d ] [-s ] [-h ] +// [-e] +// +// Parse default options files, merge them with the command line options, and +// print the resulting options to STDOUT one per line. Note that the options +// instance is a vector of arbitrary strings. +// +// -f +// Default options file name. Can be specified multiple times. +// +// -d +// Directory to start the default options files search from. +// +// -s +// System directory. +// +// -h +// Home directory. +// +// -e +// Print the default options entries (rather than the merged options) to +// STDOUT one per line in the following format: +// +// ,, +// +int +main (int argc, const char* argv[]) +{ + using butl::optional; + + class scanner + { + public: + scanner (const string& f): ifs_ (f, fdopen_mode::in, ifdstream::badbit) {} + + optional + next () + { + string s; + return !eof (getline (ifs_, s)) ? optional (move (s)) : nullopt; + } + + private: + ifdstream ifs_; + }; + + enum class unknow_mode + { + fail + }; + + class options: public vector + { + public: + bool + parse (scanner& s, unknow_mode, unknow_mode) + { + bool r (false); + while (optional o = s.next ()) + { + push_back (move (*o)); + r = true; + } + return r; + } + + void + merge (const options& o) + { + insert (end (), o.begin (), o.end ()); + } + }; + + // Parse and validate the arguments. + // + default_options_files fs; + optional sys_dir; + optional home_dir; + options cmd_ops; + bool print_entries (false); + + for (int i (1); i != argc; ++i) + { + string op (argv[i]); + + if (op == "-f") + { + assert (++i != argc); + fs.files.push_back (path (argv[i])); + } + else if (op == "-d") + { + assert (++i != argc); + fs.start_dir = dir_path (argv[i]); + } + else if (op == "-s") + { + assert (++i != argc); + sys_dir = dir_path (argv[i]); + } + else if (op == "-h") + { + assert (++i != argc); + home_dir = dir_path (argv[i]); + } + else if (op == "-e") + { + print_entries = true; + } + else + cmd_ops.push_back (argv[i]); + } + + // Load and print the default options. + // + default_options def_ops ( + load_default_options (sys_dir, + home_dir, + fs)); + + if (print_entries) + { + for (const default_options_entry& e: def_ops) + { + cout << e.file << ','; + + for (const string& o: e.options) + { + if (&o != &e.options[0]) + cout << ' '; + + cout << o; + } + + cout << (e.remote ? ",true" : ",false") << endl; + } + } + + // Merge the options and print the result. + // + options ops (merge_default_options (def_ops, cmd_ops)); + + if (!print_entries) + { + for (const string& o: ops) + cout << o << endl; + } + + return 0; +} diff --git a/tests/default-options/testscript b/tests/default-options/testscript new file mode 100644 index 0000000..c0c3816 --- /dev/null +++ b/tests/default-options/testscript @@ -0,0 +1,109 @@ +# file : tests/default-options/testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +# Note that when cross-testing the driver may not be able to run the command +# due to the meaningless program path. +# ++if ($test.target != $build.host) + exit +end + +sys_dir = $canonicalize([dir_path] $~/build2) ++mkdir -p $sys_dir/local + ++echo 'sys-foo' >=$sys_dir/foo ++echo 'sys-bar' >=$sys_dir/bar ++echo 'sys-local-foo' >=$sys_dir/local/foo ++echo 'sys-local-bar' >=$sys_dir/local/bar + +home_dir = $canonicalize([dir_path] $~/home) ++mkdir -p $home_dir/.build2/local/ + ++echo 'home-foo' >=$home_dir/.build2/foo ++echo 'home-bar' >=$home_dir/.build2/bar ++echo 'home-local-foo' >=$home_dir/.build2/local/foo ++echo 'home-local-bar' >=$home_dir/.build2/local/bar + +: in-home +: +{ + d = $home_dir/work/.build2 + +mkdir -p $d/local/ + + +echo 'work-foo' >=$d/foo + +echo 'work-bar' >=$d/bar + +echo 'work-local-foo' >=$d/local/foo + +echo 'work-local-bar' >=$d/local/bar + + d = $home_dir/work/project/.build2 + +mkdir -p $d/local/ + +touch $home_dir/work/project/.git + + +echo 'project-foo' >=$d/foo + +echo 'project-bar' >=$d/bar + +echo 'project-local-foo' >=$d/local/foo + +echo 'project-local-bar' >=$d/local/bar + + d = $home_dir/work/project/package/.build2 + +mkdir -p $d/local/ + + +echo 'package-foo' >=$d/foo + +echo 'package-bar' >=$d/bar + +echo 'package-local-foo' >=$d/local/foo + +echo 'package-local-bar' >=$d/local/bar + + start_dir = $canonicalize([dir_path] $home_dir/work/project/package) + + : entries + : + $* -e -f foo -f bar -d $start_dir -s $sys_dir -h $home_dir cmd-foo cmd-bar >>/~%EOO%d + %\.+/build2/foo,sys-foo,false% + %\.+/build2/bar,sys-bar,false% + %\.+/build2/local/foo,sys-local-foo,false% + %\.+/build2/local/bar,sys-local-bar,false% + %\.+/home/.build2/foo,home-foo,false% + %\.+/home/.build2/bar,home-bar,false% + %\.+/home/.build2/local/foo,home-local-foo,false% + %\.+/home/.build2/local/bar,home-local-bar,false% + %\.+/home/work/.build2/foo,work-foo,false% + %\.+/home/work/.build2/bar,work-bar,false% + %\.+/home/work/.build2/local/foo,work-local-foo,false% + %\.+/home/work/.build2/local/bar,work-local-bar,false% + %\.+/home/work/project/.build2/foo,project-foo,true% + %\.+/home/work/project/.build2/bar,project-bar,true% + %\.+/home/work/project/.build2/local/foo,project-local-foo,true% + %\.+/home/work/project/.build2/local/bar,project-local-bar,true% + %\.+/home/work/project/package/.build2/foo,package-foo,true% + %\.+/home/work/project/package/.build2/bar,package-bar,true% + %\.+/home/work/project/package/.build2/local/foo,package-local-foo,true% + %\.+/home/work/project/package/.build2/local/bar,package-local-bar,true% + EOO + + : merged + : + $* -f foo -f bar -d $start_dir -s $sys_dir -h $home_dir cmd-foo cmd-bar >>EOO + sys-foo + sys-bar + sys-local-foo + sys-local-bar + home-foo + home-bar + home-local-foo + home-local-bar + work-foo + work-bar + work-local-foo + work-local-bar + project-foo + project-bar + project-local-foo + project-local-bar + package-foo + package-bar + package-local-foo + package-local-bar + cmd-foo + cmd-bar + EOO +} -- cgit v1.1