aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2019-01-14 22:00:36 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2019-01-16 18:21:44 +0300
commit78be8e1604f183b28527159047bab69a5cbe9232 (patch)
tree40bc9c7990992257dd2bdaf7dea27d186bc9437d
parentf7dc4934b04c062b1ce8aad09725a30707255e69 (diff)
Add b_info() that runs `b info` command and parses and returns build2 project info
-rw-r--r--libbutl/b.cxx287
-rw-r--r--libbutl/b.mxx110
-rw-r--r--tests/b-info/buildfile8
-rw-r--r--tests/b-info/driver.cxx118
-rw-r--r--tests/b-info/testscript83
5 files changed, 606 insertions, 0 deletions
diff --git a/libbutl/b.cxx b/libbutl/b.cxx
new file mode 100644
index 0000000..bcf8d05
--- /dev/null
+++ b/libbutl/b.cxx
@@ -0,0 +1,287 @@
+// file : libbutl/b.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef __cpp_modules
+#include <libbutl/b.mxx>
+#endif
+
+// C includes.
+
+#include <cassert>
+
+#ifndef __cpp_lib_modules
+#include <string>
+#include <vector>
+#include <cstddef>
+#include <cstdint>
+#include <stdexcept>
+#include <functional>
+
+#include <ios> // ios::failure
+#include <utility> // move()
+#include <sstream>
+#endif
+
+// Other includes.
+
+#ifdef __cpp_modules
+module butl.b;
+
+// Only imports additional to interface.
+#ifdef __clang__
+#ifdef __cpp_lib_modules
+import std.core;
+import std.io;
+#endif
+import butl.url;
+import butl.path;
+import butl.process;
+import butl.optional;
+import butl.project_name;
+import butl.standard_version;
+#endif
+
+import butl.utility; // next_word(), eof(), etc
+import butl.path_io;
+import butl.fdstream;
+import butl.process_io; // operator<<(ostream, process_path)
+import butl.small_vector;
+#else
+#include <libbutl/utility.mxx>
+#include <libbutl/path-io.mxx>
+#include <libbutl/fdstream.mxx>
+#include <libbutl/process-io.mxx>
+#include <libbutl/small-vector.mxx>
+#endif
+
+using namespace std;
+
+namespace butl
+{
+ b_error::
+ b_error (const string& d, optional<process_exit> e)
+ : runtime_error (d),
+ exit (move (e))
+ {
+ }
+
+ [[noreturn]] static inline void
+ bad_value (const string& d)
+ {
+ throw runtime_error ("invalid " + d);
+ }
+
+ b_project_info
+ b_info (const dir_path& project,
+ uint16_t verb,
+ const function<b_callback>& cmd_callback,
+ const path& program,
+ const dir_path& search_fallback,
+ const vector<string>& ops)
+ {
+ try
+ {
+ process_path pp (
+ process::path_search (program, true /* init */, search_fallback));
+
+ process pr;
+ try
+ {
+ fdpipe pipe (fdopen_pipe ()); // Text mode seems appropriate.
+
+ small_vector<string, 2> vops;
+
+ // Let's suppress warnings the `b info` command may issue, unless the
+ // verbosity level is 2 (-v) or higher. For example:
+ //
+ // warning: configured src_root prj/ does not match forwarded prj/prj/
+ //
+ if (verb >= 2)
+ {
+ vops.push_back ("--verbose");
+ vops.push_back (std::to_string (verb));
+ }
+ else
+ vops.push_back ("-q");
+
+ pr = process_start_callback (
+ cmd_callback ? cmd_callback : [] (const char* const*, size_t) {},
+ 0 /* stdin */,
+ pipe,
+ 2 /* stderr */,
+ pp,
+ vops,
+ ops,
+ "info:", project.representation ());
+
+ pipe.out.close ();
+ ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit);
+
+ auto parse_name = [] (string&& s, const char* what)
+ {
+ try
+ {
+ return project_name (move (s));
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_value (string (what) + " name '" + s + "': " + e.what ());
+ }
+ };
+
+ auto parse_dir = [] (string&& s, const char* what)
+ {
+ try
+ {
+ return dir_path (move (s));
+ }
+ catch (const invalid_path& e)
+ {
+ bad_value (
+ string (what) + " directory '" + e.path + "': " + e.what ());
+ }
+ };
+
+ b_project_info r;
+ for (string l; !eof (getline (is, l)); )
+ {
+ if (l.compare (0, 9, "project: ") == 0)
+ {
+ string v (l, 9);
+ if (!v.empty ())
+ r.project = parse_name (move (v), "project");
+ }
+ else if (l.compare (0, 9, "version: ") == 0)
+ {
+ string v (l, 9);
+ if (!v.empty ())
+ try
+ {
+ r.version = standard_version (v);
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_value ("version '" + v + "': " + e.what ());
+ }
+ }
+ else if (l.compare (0, 9, "summary: ") == 0)
+ {
+ r.summary = string (l, 9);
+ }
+ else if (l.compare (0, 5, "url: ") == 0)
+ {
+ string v (l, 5);
+ if (!v.empty ())
+ try
+ {
+ r.url = url (v);
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_value ("url '" + v + "': " + e.what ());
+ }
+ }
+ else if (l.compare (0, 10, "src_root: ") == 0)
+ {
+ r.src_root = parse_dir (string (l, 10), "src_root");
+ }
+ else if (l.compare (0, 10, "out_root: ") == 0)
+ {
+ r.out_root = parse_dir (string (l, 10), "out_root");
+ }
+ else if (l.compare (0, 14, "amalgamation: ") == 0)
+ {
+ string v (l, 14);
+ if (!v.empty ())
+ r.amalgamation = parse_dir (move (v), "amalgamation");
+ }
+ else if (l.compare (0, 13, "subprojects: ") == 0)
+ {
+ string v (l, 13);
+
+ for (size_t b (0), e (0); next_word (v, b, e); )
+ {
+ string s (v, b, e - b);
+ size_t p (s.find ('@'));
+
+ if (p == string::npos)
+ bad_value ("subproject '" + s + "': missing '@'");
+
+ project_name sn;
+ if (p != 0)
+ sn = parse_name (string (s, 0, p), "subproject");
+
+ r.subprojects.push_back (
+ b_project_info::subproject {move (sn),
+ parse_dir (string (s, p + 1),
+ "subproject")});
+ }
+ }
+ else if (l.compare (0, 12, "operations: ") == 0)
+ {
+ string v (l, 12);
+ for (size_t b (0), e (0); next_word (v, b, e); )
+ r.operations.push_back (string (v, b, e - b));
+ }
+ else if (l.compare (0, 17, "meta-operations: ") == 0)
+ {
+ string v (l, 17);
+ for (size_t b (0), e (0); next_word (v, b, e); )
+ r.meta_operations.push_back (string (v, b, e - b));
+ }
+ }
+
+ is.close (); // Detect errors.
+
+ if (pr.wait ())
+ return r;
+ }
+ // Note that ios::failure inherits from std::runtime_error, so this
+ // catch-clause must go last.
+ //
+ catch (const ios::failure& e)
+ {
+ // Note: using ostream to get sanitized representation.
+ //
+ if (pr.wait ())
+ {
+ ostringstream os;
+ os << "error reading " << pp << " output: " << e;
+ throw b_error (os.str (), move (pr.exit));
+ }
+ else if (!pr.exit) // Pipe opening failed before the process started.
+ {
+ ostringstream os;
+ os << "unable to open pipe: " << e;
+ throw b_error (os.str ());
+ }
+
+ // Fall through.
+ }
+ catch (const runtime_error& e)
+ {
+ // Re-throw as b_error if the process exited normally with zero code.
+ //
+ if (pr.wait ())
+ throw b_error (e.what (), move (pr.exit));
+
+ // Fall through.
+ }
+
+ // We should only get here if the child exited with an error status.
+ //
+ assert (!pr.wait ());
+
+ throw b_error (
+ string ("process ") + pp.recall_string () + " " + to_string (*pr.exit),
+ move (pr.exit));
+ }
+ catch (const process_error& e)
+ {
+ ostringstream os;
+ os << "unable to execute " << program << ": " << e;
+ throw b_error (os.str ());
+ }
+ }
+}
diff --git a/libbutl/b.mxx b/libbutl/b.mxx
new file mode 100644
index 0000000..cd574f0
--- /dev/null
+++ b/libbutl/b.mxx
@@ -0,0 +1,110 @@
+// file : libbutl/b.mxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef __cpp_modules
+#pragma once
+#endif
+
+// C includes.
+
+#ifndef __cpp_lib_modules
+#include <string>
+#include <vector>
+#include <cstddef> // size_tu
+#include <cstdint> // uint16_t
+#include <stdexcept> // runtime_error
+#include <functional>
+#endif
+
+// Other includes.
+
+#ifdef __cpp_modules
+export module butl.b;
+#ifdef __cpp_lib_modules
+import std.core;
+#endif
+import butl.url;
+import butl.path;
+import butl.process;
+import butl.optional;
+import butl.project_name;
+import butl.standard_version;
+#else
+#include <libbutl/url.mxx>
+#include <libbutl/path.mxx>
+#include <libbutl/process.mxx>
+#include <libbutl/optional.mxx>
+#include <libbutl/project-name.mxx>
+#include <libbutl/standard-version.mxx>
+#endif
+
+#include <libbutl/export.hxx>
+
+LIBBUTL_MODEXPORT namespace butl
+{
+ class LIBBUTL_SYMEXPORT b_error: public std::runtime_error
+ {
+ public:
+ // Build system program exit information. May be absent if the error
+ // occured before the process has been started.
+ //
+ // Can be used by the caller to decide if to print the error message to
+ // stderr. Normally, it is not required if the process exited normally
+ // with non-zero code, since presumably it has issued diagnostics. Note
+ // that the normal() function can be used to check for this.
+ //
+ optional<process_exit> exit;
+
+ // Return true if the build2 process exited normally with non-zero code.
+ //
+ bool
+ normal () const {return exit && exit->normal () && !*exit;}
+
+ explicit
+ b_error (const std::string& description, optional<process_exit> = nullopt);
+ };
+
+ // Run `b info: <project-dir>` command and parse and return the build2
+ // project information it prints to stdout. Throw b_error on error.
+ //
+ // You can also specify the build2 verbosity level, command line callback
+ // (see process_run_callback() for details), build program search details
+ // and additional options.
+ //
+ struct b_project_info
+ {
+ using url_type = butl::url;
+
+ struct subproject
+ {
+ project_name name; // Empty if anonymous.
+ dir_path path; // Relative to the project root.
+ };
+
+ project_name project;
+ standard_version version;
+ std::string summary;
+ url_type url;
+
+ dir_path src_root;
+ dir_path out_root;
+
+ dir_path amalgamation; // Relative to project root and
+ // empty if not amalgmated.
+ std::vector<subproject> subprojects;
+
+ std::vector<std::string> operations;
+ std::vector<std::string> meta_operations;
+ };
+
+ using b_callback = void (const char* const args[], std::size_t n);
+
+ LIBBUTL_SYMEXPORT b_project_info
+ b_info (const dir_path& project,
+ std::uint16_t verb = 1,
+ const std::function<b_callback>& cmd_callback = {},
+ const path& program = path ("b"),
+ const dir_path& search_fallback = {},
+ const std::vector<std::string>& options = {});
+}
diff --git a/tests/b-info/buildfile b/tests/b-info/buildfile
new file mode 100644
index 0000000..6395d63
--- /dev/null
+++ b/tests/b-info/buildfile
@@ -0,0 +1,8 @@
+# file : tests/b-info/buildfile
+# copyright : Copyright (c) 2014-2018 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+import libs = libbutl%lib{butl}
+libs += $stdmod_lib
+
+exe{driver}: {hxx cxx}{*} $libs testscript
diff --git a/tests/b-info/driver.cxx b/tests/b-info/driver.cxx
new file mode 100644
index 0000000..9432551
--- /dev/null
+++ b/tests/b-info/driver.cxx
@@ -0,0 +1,118 @@
+// file : tests/b-info/driver.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <cassert>
+
+#ifndef __cpp_lib_modules
+#include <string>
+#include <iostream>
+#endif
+
+// Other includes.
+
+#ifdef __cpp_modules
+#ifdef __cpp_lib_modules
+import std.core;
+import std.io;
+#endif
+import butl.b;
+import butl.path;
+import butl.utility; // operator<<(ostream,exception)
+#else
+#include <libbutl/b.mxx>
+#include <libbutl/path.mxx>
+#include <libbutl/utility.mxx>
+#endif
+
+using namespace std;
+using namespace butl;
+
+// Usage: argv[0] [-b <path>] <project-dir>
+//
+// Print the build2 project information to stdout.
+//
+// -b <path> the build program to be used to retrieve the project information
+//
+int
+main (int argc, char* argv[])
+try
+{
+ path b ("b");
+ dir_path project;
+
+ for (int i (1); i != argc; ++i)
+ {
+ string a (argv[i]);
+
+ if (a == "-b")
+ {
+ ++i;
+
+ assert (i != argc);
+ b = path (argv[i]);
+ }
+ else
+ {
+ assert (project.empty ());
+ project = dir_path (move (a));
+ }
+ }
+
+ assert (!project.empty ());
+
+ cout.exceptions (ios::failbit | ios::badbit);
+
+ b_project_info pi (b_info (project, 1 /* verb */, {}, b));
+
+ cout << "project: " << pi.project << endl
+ << "version: " << pi.version << endl
+ << "summary: " << pi.summary << endl
+ << "url: " << pi.url << endl
+ << "src_root: " << pi.src_root.representation () << endl
+ << "out_root: " << pi.out_root.representation () << endl
+ << "amalgamation: " << pi.amalgamation.representation () << endl
+ << "subprojects: ";
+
+ for (auto b (pi.subprojects.begin ()), i (b);
+ i != pi.subprojects.end ();
+ ++i)
+ {
+ if (i != b)
+ cout << ' ';
+
+ cout << i->name << '@' << i->path.representation ();
+ }
+ cout << endl
+ << "operations: ";
+
+ for (auto b (pi.operations.begin ()), i (b); i != pi.operations.end (); ++i)
+ {
+ if (i != b)
+ cout << ' ';
+
+ cout << *i;
+ }
+ cout << endl
+ << "meta-operations: ";
+
+ for (auto b (pi.meta_operations.begin ()), i (b);
+ i != pi.meta_operations.end ();
+ ++i)
+ {
+ if (i != b)
+ cout << ' ';
+
+ cout << *i;
+ }
+ cout << endl;
+
+ return 0;
+}
+catch (const b_error& e)
+{
+ if (!e.normal ())
+ cerr << e << endl;
+
+ return 1;
+}
diff --git a/tests/b-info/testscript b/tests/b-info/testscript
new file mode 100644
index 0000000..9d1cfeb
--- /dev/null
+++ b/tests/b-info/testscript
@@ -0,0 +1,83 @@
+# file : tests/b-info/testscript
+# copyright : Copyright (c) 2014-2018 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+# Note that when cross-testing we unlikely be able to run build2 on the
+# target platform.
+#
++if ($test.target != $build.host)
+ exit
+end
+
+sp = ' '
+test.options += -b $recall($build.path)
+
+: basic
+:
+{
+ mkdir -p prj/build;
+
+ cat <<EOI >=prj/build/bootstrap.build;
+ project = prj
+
+ using version
+ using config
+ using dist
+ EOI
+
+ cat <<EOI >=prj/buildfile;
+ ./: subprj/
+ EOI
+
+ cat <<EOI >=prj/manifest;
+ : 1
+ name: prj
+ version: 1.2.3-a.0.z
+ summary: test project
+ license: MIT
+ EOI
+
+ mkdir -p prj/subprj/build;
+
+ cat <<EOI >=prj/subprj/build/bootstrap.build;
+ project =
+
+ using config
+ using dist
+ EOI
+
+ touch prj/subprj/buildfile;
+
+ $* prj >>/~"%EOO%";
+ project: prj
+ version: 1.2.3-a.0.z
+ summary: test project
+ url:$sp
+ %src_root: .+/prj/%
+ %out_root: .+/prj/%
+ amalgamation: ../../../../
+ subprojects: @subprj/
+ operations: update clean
+ meta-operations: perform configure disfigure dist info
+ EOO
+
+ $* prj/subprj >>/~"%EOO%"
+ project:$sp
+ version:$sp
+ summary:$sp
+ url:$sp
+ %src_root: .+/subprj/%
+ %out_root: .+/subprj/%
+ amalgamation: ../
+ subprojects:$sp
+ operations: update clean
+ meta-operations: perform configure disfigure dist info
+ EOO
+}
+
+: error
+:
+$* prj 2>>/~%EOE% != 0
+ %error: .+%
+ % info: .+%?
+ EOE