From 78be8e1604f183b28527159047bab69a5cbe9232 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Mon, 14 Jan 2019 22:00:36 +0300 Subject: Add b_info() that runs `b info` command and parses and returns build2 project info --- libbutl/b.cxx | 287 ++++++++++++++++++++++++++++++++++++++++++++++++ libbutl/b.mxx | 110 +++++++++++++++++++ tests/b-info/buildfile | 8 ++ tests/b-info/driver.cxx | 118 ++++++++++++++++++++ tests/b-info/testscript | 83 ++++++++++++++ 5 files changed, 606 insertions(+) create mode 100644 libbutl/b.cxx create mode 100644 libbutl/b.mxx create mode 100644 tests/b-info/buildfile create mode 100644 tests/b-info/driver.cxx create mode 100644 tests/b-info/testscript 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 +#endif + +// C includes. + +#include + +#ifndef __cpp_lib_modules +#include +#include +#include +#include +#include +#include + +#include // ios::failure +#include // move() +#include +#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 +#include +#include +#include +#include +#endif + +using namespace std; + +namespace butl +{ + b_error:: + b_error (const string& d, optional 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& cmd_callback, + const path& program, + const dir_path& search_fallback, + const vector& 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 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 +#include +#include // size_tu +#include // uint16_t +#include // runtime_error +#include +#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 +#include +#include +#include +#include +#include +#endif + +#include + +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 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 = nullopt); + }; + + // Run `b info: ` 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 subprojects; + + std::vector operations; + std::vector 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& cmd_callback = {}, + const path& program = path ("b"), + const dir_path& search_fallback = {}, + const std::vector& 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 + +#ifndef __cpp_lib_modules +#include +#include +#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 +#include +#include +#endif + +using namespace std; +using namespace butl; + +// Usage: argv[0] [-b ] +// +// Print the build2 project information to stdout. +// +// -b 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 <=prj/build/bootstrap.build; + project = prj + + using version + using config + using dist + EOI + + cat <=prj/buildfile; + ./: subprj/ + EOI + + cat <=prj/manifest; + : 1 + name: prj + version: 1.2.3-a.0.z + summary: test project + license: MIT + EOI + + mkdir -p prj/subprj/build; + + cat <=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 -- cgit v1.1