aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2023-05-08 10:43:40 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2023-05-09 15:37:33 +0200
commit00df206af5c80aba31bf7d180bdf03d617071e94 (patch)
tree00a637e6d78e7f8e9de323a0804df59c403120ef
parent7a458f210f296cb3cc1551a4606f0cf025003f3a (diff)
Add support for dumping build system state in JSON format (GH issue #182)
Specifically: 1. New --dump-format option. Valid values are `buildfile` and `json-v0.1`. 2. The --dump option now recognizes two additional values: `match-pre` and `match-post` to dump the state of pre/post-operations. The `match` value now only triggers dumping of the main operation.
-rw-r--r--build2/b.cxx74
-rwxr-xr-xdoc/cli.sh2
-rw-r--r--libbuild2/b-options.cxx48
-rw-r--r--libbuild2/b-options.hxx8
-rw-r--r--libbuild2/b-options.ixx12
-rw-r--r--libbuild2/b.cli35
-rw-r--r--libbuild2/dump.cxx1035
-rw-r--r--libbuild2/dump.hxx29
-rw-r--r--libbuild2/parser.cxx8
-rw-r--r--libbuild2/rule.cxx2
-rw-r--r--libbuild2/target.cxx4
-rw-r--r--libbuild2/variable.hxx4
12 files changed, 1151 insertions, 110 deletions
diff --git a/build2/b.cxx b/build2/b.cxx
index add27d4..163a89e 100644
--- a/build2/b.cxx
+++ b/build2/b.cxx
@@ -148,32 +148,20 @@ namespace build2
s.begin_object ();
- // Target.
+ // Display target.
//
- {
- // Change the stream verbosity (see print_lines() for details).
- //
- ostringstream os;
- stream_verb (os, stream_verbosity (1, 0));
- os << t;
- s.member ("target", os.str ());
- }
+ s.member_name ("target");
+ dump_display_target_name (s, t);
// Quoted target.
//
- {
- names ns (t.as_name ()); // Note: potentially adds an extension.
+ s.member_name ("quoted_target");
+ dump_quoted_target_name (s, t);
- ostringstream os;
- stream_verb (os, stream_verbosity (1, 0));
- to_stream (os, ns, quote_mode::effective, '@');
- s.member ("quoted_target", os.str ());
- }
-
- s.member ("target_type", t.key ().type->name, false /* check */);
+ s.member ("target_type", t.type ().name, false /* check */);
if (t.is_a<dir> ())
- s.member ("target_path", t.key ().dir->string ());
+ s.member ("target_path", t.dir.string ());
else if (const auto* pt = t.is_a<path_target> ())
s.member ("target_path", pt->path ().string ());
@@ -497,20 +485,47 @@ main (int argc, char* argv[])
bool dump_load (false);
bool dump_match (false);
+ bool dump_match_pre (false);
+ bool dump_match_post (false);
for (const string& p: ops.dump ())
{
- if (p == "load") dump_load = true;
- else if (p == "match") dump_match = true;
+ if (p == "load") dump_load = true;
+ else if (p == "match") dump_match = true;
+ else if (p == "match-pre") dump_match_pre = true;
+ else if (p == "match-post") dump_match_post = true;
else fail << "unknown phase '" << p << "' specified with --dump";
}
- auto dump = [&trace, &ops] (context& ctx, optional<action> a)
+ dump_format dump_fmt (dump_format::buildfile);
+ if (ops.dump_format_specified ())
+ {
+ const string& f (ops.dump_format ());
+
+ if (f == "json-v0.1")
+ {
+#ifdef BUILD2_BOOTSTRAP
+ fail << "json dump not supported in bootstrap build system";
+#endif
+ dump_fmt = dump_format::json;
+ }
+ else if (f != "buildfile")
+ {
+ diag_record dr (fail);
+
+ dr << "unsupported format '" << f << "' specified with --dump-format";
+
+ if (f.compare (0, 4, "json") == 0)
+ dr << info << "supported json format version is json-v0.1";
+ }
+ }
+
+ auto dump = [&trace, &ops, dump_fmt] (context& ctx, optional<action> a)
{
const dir_paths& scopes (ops.dump_scope ());
const vector<pair<name, optional<name>>>& targets (ops.dump_target ());
if (scopes.empty () && targets.empty ())
- build2::dump (ctx, a);
+ build2::dump (ctx, a, dump_fmt);
else
{
auto comp_norm = [] (dir_path& d, const char* what)
@@ -557,7 +572,7 @@ main (int argc, char* argv[])
l5 ([&]{trace << "unknown target scope " << d
<< " specified with --dump-scope";});
- build2::dump (s, a);
+ build2::dump (s, a, dump_fmt);
}
// Dump targets.
@@ -623,7 +638,7 @@ main (int argc, char* argv[])
l5 ([&]{trace << "unknown target scope " << d
<< " specified with --dump-target";});
- build2::dump (t, a);
+ build2::dump (t, a, dump_fmt);
}
}
};
@@ -644,7 +659,10 @@ main (int argc, char* argv[])
// Note that this constructor is cheap and so we rather call it always
// instead of resorting to dynamic allocations.
//
- json::stream_serializer js (cout);
+ // Note also that we disable pretty-printing if there is also the JSON
+ // dump and thus we need to combine the two in the JSON Lines format.
+ //
+ json::stream_serializer js (cout, dump_fmt == dump_format::json ? 0 : 2);
if (ops.structured_result_specified () &&
ops.structured_result () == structured_result_format::json)
@@ -1484,7 +1502,7 @@ main (int argc, char* argv[])
if (mif->match != nullptr)
mif->match (mparams, a, tgs, diag, true /* progress */);
- if (dump_match)
+ if (dump_match_pre)
dump (ctx, a);
if (mif->execute != nullptr && !ctx.match_only)
@@ -1572,7 +1590,7 @@ main (int argc, char* argv[])
if (mif->match != nullptr)
mif->match (mparams, a, tgs, diag, true /* progress */);
- if (dump_match)
+ if (dump_match_post)
dump (ctx, a);
if (mif->execute != nullptr && !ctx.match_only)
diff --git a/doc/cli.sh b/doc/cli.sh
index 7aa1aff..8d7d70a 100755
--- a/doc/cli.sh
+++ b/doc/cli.sh
@@ -47,6 +47,7 @@ function compile ()
--generate-html --html-suffix .xhtml \
--html-prologue-file man-prologue.xhtml \
--html-epilogue-file man-epilogue.xhtml \
+--link-regex '%b(#.+)?%build2-build-system-manual.xhtml$1%' \
../libbuild2/$n.cli
cli -I .. \
@@ -58,6 +59,7 @@ function compile ()
--generate-man --man-suffix .1 --ascii-tree \
--man-prologue-file man-prologue.1 \
--man-epilogue-file man-epilogue.1 \
+--link-regex '%b(#.+)?%$1%' \
../libbuild2/$n.cli
}
diff --git a/libbuild2/b-options.cxx b/libbuild2/b-options.cxx
index 1fbc5d9..0ff788e 100644
--- a/libbuild2/b-options.cxx
+++ b/libbuild2/b-options.cxx
@@ -361,6 +361,8 @@ namespace build2
no_mtime_check_ (),
dump_ (),
dump_specified_ (false),
+ dump_format_ (),
+ dump_format_specified_ (false),
dump_scope_ (),
dump_scope_specified_ (false),
dump_target_ (),
@@ -618,6 +620,13 @@ namespace build2
this->dump_specified_ = true;
}
+ if (a.dump_format_specified_)
+ {
+ ::build2::build::cli::parser< string>::merge (
+ this->dump_format_, a.dump_format_);
+ this->dump_format_specified_ = true;
+ }
+
if (a.dump_scope_specified_)
{
::build2::build::cli::parser< dir_paths>::merge (
@@ -975,11 +984,37 @@ namespace build2
os << std::endl
<< "\033[1m--dump\033[0m \033[4mphase\033[0m Dump the build system state after the specified phase." << ::std::endl
<< " Valid \033[4mphase\033[0m values are \033[1mload\033[0m (after loading \033[1mbuildfiles\033[0m)" << ::std::endl
- << " and \033[1mmatch\033[0m (after matching rules to targets). Repeat" << ::std::endl
- << " this option to dump the state after multiple phases. By" << ::std::endl
- << " default the entire build state is dumped but this" << ::std::endl
- << " behavior can be altered with the --dump-scope\033[0m and" << ::std::endl
- << " \033[1m--dump-target\033[0m options." << ::std::endl;
+ << " and \033[1mmatch\033[0m (after matching rules to targets). The \033[1mmatch\033[0m" << ::std::endl
+ << " value also has the \033[1mmatch-pre\033[0m and \033[1mmatch-post\033[0m variants to" << ::std::endl
+ << " dump the state for the pre/post-operations (\033[1mmatch\033[0m dumps" << ::std::endl
+ << " the main operation only). Repeat this option to dump" << ::std::endl
+ << " the state after multiple phases/variants. By default" << ::std::endl
+ << " the entire build state is dumped but this behavior can" << ::std::endl
+ << " be altered with the \033[1m--dump-scope\033[0m and \033[1m--dump-target\033[0m" << ::std::endl
+ << " options." << ::std::endl;
+
+ os << std::endl
+ << "\033[1m--dump-format\033[0m \033[4mformat\033[0m Representation format and output stream to use when" << ::std::endl
+ << " dumping the build system state. Valid values for this" << ::std::endl
+ << " option are \033[1mbuildfile\033[0m (a human-readable, Buildfile-like" << ::std::endl
+ << " format written to \033[1mstderr\033[0m; this is the default), and" << ::std::endl
+ << " \033[1mjson-v0.1\033[0m (machine-readable, JSON-based format written" << ::std::endl
+ << " to \033[1mstdout\033[0m). For details on the \033[1mbuildfile\033[0m format, see" << ::std::endl
+ << " Diagnostics and Debugging (b#intro-diag-debug). For" << ::std::endl
+ << " details on the \033[1mjson-v0.1\033[0m format, see the JSON OUTPUT" << ::std::endl
+ << " section below (overall properties) and Build System" << ::std::endl
+ << " State JSON Dump Format (b#@@) (format specifics). Note" << ::std::endl
+ << " that the JSON format is currently unstable (thus the" << ::std::endl
+ << " temporary \033[1m-v0.1\033[0m suffix)." << ::std::endl
+ << ::std::endl
+ << " Note that because it's possible to end up with multiple" << ::std::endl
+ << " dumps (for example, by specifying the \033[1m--dump-scope\033[0m" << ::std::endl
+ << " and/or \033[1m--dump-target\033[0m options multiple times), the JSON" << ::std::endl
+ << " output is in the \"JSON Lines\" form, that is, without" << ::std::endl
+ << " pretty-printing and with the top-level JSON objects" << ::std::endl
+ << " delimited by newlines. Note also that if the JSON dump" << ::std::endl
+ << " output is combined with \033[1m--structured-result=json\033[0m, then" << ::std::endl
+ << " the structured result is the last line." << ::std::endl;
os << std::endl
<< "\033[1m--dump-scope\033[0m \033[4mdir\033[0m Dump the build system state for the specified scope" << ::std::endl
@@ -1168,6 +1203,9 @@ namespace build2
_cli_b_options_map_["--dump"] =
&::build2::build::cli::thunk< b_options, strings, &b_options::dump_,
&b_options::dump_specified_ >;
+ _cli_b_options_map_["--dump-format"] =
+ &::build2::build::cli::thunk< b_options, string, &b_options::dump_format_,
+ &b_options::dump_format_specified_ >;
_cli_b_options_map_["--dump-scope"] =
&::build2::build::cli::thunk< b_options, dir_paths, &b_options::dump_scope_,
&b_options::dump_scope_specified_ >;
diff --git a/libbuild2/b-options.hxx b/libbuild2/b-options.hxx
index 9afef25..37239ce 100644
--- a/libbuild2/b-options.hxx
+++ b/libbuild2/b-options.hxx
@@ -171,6 +171,12 @@ namespace build2
bool
dump_specified () const;
+ const string&
+ dump_format () const;
+
+ bool
+ dump_format_specified () const;
+
const dir_paths&
dump_scope () const;
@@ -305,6 +311,8 @@ namespace build2
bool no_mtime_check_;
strings dump_;
bool dump_specified_;
+ string dump_format_;
+ bool dump_format_specified_;
dir_paths dump_scope_;
bool dump_scope_specified_;
vector<pair<name, optional<name>>> dump_target_;
diff --git a/libbuild2/b-options.ixx b/libbuild2/b-options.ixx
index d19edb8..99e73d0 100644
--- a/libbuild2/b-options.ixx
+++ b/libbuild2/b-options.ixx
@@ -218,6 +218,18 @@ namespace build2
return this->dump_specified_;
}
+ inline const string& b_options::
+ dump_format () const
+ {
+ return this->dump_format_;
+ }
+
+ inline bool b_options::
+ dump_format_specified () const
+ {
+ return this->dump_format_specified_;
+ }
+
inline const dir_paths& b_options::
dump_scope () const
{
diff --git a/libbuild2/b.cli b/libbuild2/b.cli
index 768bcd0..deb6a26 100644
--- a/libbuild2/b.cli
+++ b/libbuild2/b.cli
@@ -777,10 +777,37 @@ namespace build2
"<phase>",
"Dump the build system state after the specified phase. Valid <phase>
values are \cb{load} (after loading \cb{buildfiles}) and \cb{match}
- (after matching rules to targets). Repeat this option to dump the state
- after multiple phases. By default the entire build state is dumped but
- this behavior can be altered with the \c{--dump-scope} and
- \cb{--dump-target} options."
+ (after matching rules to targets). The \cb{match} value also has the
+ \cb{match-pre} and \cb{match-post} variants to dump the state for the
+ pre/post-operations (\cb{match} dumps the main operation only). Repeat
+ this option to dump the state after multiple phases/variants. By
+ default the entire build state is dumped but this behavior can be
+ altered with the \cb{--dump-scope} and \cb{--dump-target} options."
+ }
+
+ string --dump-format
+ {
+ // NOTE: fix all references to json-v0.1
+ //
+ "<format>",
+ "Representation format and output stream to use when dumping the build
+ system state. Valid values for this option are \cb{buildfile} (a
+ human-readable, Buildfile-like format written to \cb{stderr}; this is
+ the default), and \cb{json-v0.1} (machine-readable, JSON-based format
+ written to \cb{stdout}). For details on the \cb{buildfile} format, see
+ \l{b#intro-diag-debug Diagnostics and Debugging}. For details on the
+ \cb{json-v0.1} format, see the JSON OUTPUT section below (overall
+ properties) and \l{b#@@ Build System State JSON Dump Format} (format
+ specifics). Note that the JSON format is currently unstable (thus
+ the temporary \cb{-v0.1} suffix).
+
+ Note that because it's possible to end up with multiple dumps (for
+ example, by specifying the \cb{--dump-scope} and/or \cb{--dump-target}
+ options multiple times), the JSON output is in the \"JSON Lines\" form,
+ that is, without pretty-printing and with the top-level JSON objects
+ delimited by newlines. Note also that if the JSON dump output is
+ combined with \cb{--structured-result=json}, then the structured
+ result is the last line."
}
dir_paths --dump-scope
diff --git a/libbuild2/dump.cxx b/libbuild2/dump.cxx
index e00d1b9..ca7254a 100644
--- a/libbuild2/dump.cxx
+++ b/libbuild2/dump.cxx
@@ -3,6 +3,11 @@
#include <libbuild2/dump.hxx>
+#ifndef BUILD2_BOOTSTRAP
+# include <iostream> // cout
+# include <unordered_map>
+#endif
+
#include <libbuild2/rule.hxx>
#include <libbuild2/scope.hxx>
#include <libbuild2/target.hxx>
@@ -11,6 +16,7 @@
#include <libbuild2/diagnostics.hxx>
using namespace std;
+using namespace butl;
namespace build2
{
@@ -53,6 +59,317 @@ namespace build2
}
}
+#ifndef BUILD2_BOOTSTRAP
+
+ static string
+ quoted_target_name (const names_view& ns, bool rel)
+ {
+ ostringstream os;
+ stream_verb (os, stream_verbosity (rel ? 0 : 1, 0));
+ to_stream (os, ns, quote_mode::effective, '@');
+ return os.str ();
+ }
+
+ static void
+ dump_quoted_target_name (json::stream_serializer& j,
+ const names_view& ns,
+ bool rel)
+ {
+ j.value (quoted_target_name (ns, rel));
+ }
+
+ static string
+ quoted_target_name (const target& t, bool rel)
+ {
+ names ns (t.as_name ()); // Note: potentially adds an extension.
+
+ // Don't print target names relative if the target is in src and out!=src.
+ // Failed that, we will end up with pointless ../../../... paths.
+ //
+ // It may also seem that we can omit @-qualification in this case, since
+ // it is implied by the containing scope. However, keep in mind that the
+ // target may not be directly in this scope. We could make it relative,
+ // though.
+ //
+ if (rel && !t.out.empty ())
+ {
+ // Make the out relative ourselves and then disable relative for src.
+ //
+ dir_path& o (ns.back ().dir);
+ o = relative (o); // Note: may return empty path.
+ if (o.empty ())
+ o = dir_path (".");
+
+ rel = false;
+ }
+
+ return quoted_target_name (ns, rel);
+ }
+
+ void
+ dump_quoted_target_name (json::stream_serializer& j,
+ const target& t,
+ bool rel)
+ {
+ j.value (quoted_target_name (t, rel));
+ }
+
+ using target_name_cache = unordered_map<const target*, string>;
+
+ static void
+ dump_quoted_target_name (json::stream_serializer& j,
+ const target& t,
+ target_name_cache& tc)
+ {
+ auto i (tc.find (&t));
+ if (i == tc.end ())
+ i = tc.emplace (&t, quoted_target_name (t, false /* relative */)).first;
+
+ j.value (i->second);
+ }
+
+ void
+ dump_display_target_name (json::stream_serializer& j,
+ const target& t,
+ bool rel)
+ {
+ // Note: see the quoted version above for details.
+
+ target_key tk (t.key ());
+
+ dir_path o;
+ if (rel && !tk.out->empty ())
+ {
+ o = relative (*tk.out);
+ if (o.empty ())
+ o = dir_path (".");
+ tk.out = &o;
+
+ rel = false;
+ }
+
+ // Change the stream verbosity to print relative if requested and omit
+ // extension.
+ //
+ ostringstream os;
+ stream_verb (os, stream_verbosity (rel ? 0 : 1, 0));
+ os << tk;
+ j.value (os.str ());
+ }
+
+ static void
+ dump_value (json::stream_serializer& j, const value& v)
+ {
+ // Hints.
+ //
+ // Note that the pair hint should only be used for simple names.
+ //
+ optional<bool> h_array;
+ optional<bool> h_pair; // true/false - second/first is optional.
+
+ if (v.null)
+ {
+ j.value (nullptr);
+ return;
+ }
+ else if (v.type != nullptr)
+ {
+ const value_type& t (*v.type);
+
+ auto s_array = [&j] (const auto& vs)
+ {
+ j.begin_array ();
+ for (const auto& v: vs) j.value (v);
+ j.end_array ();
+ };
+
+ auto s_array_string = [&j] (const auto& vs)
+ {
+ j.begin_array ();
+ for (const auto& v: vs) j.value (v.string ());
+ j.end_array ();
+ };
+
+ // Note: check in the derived-first order.
+ //
+ if (t.is_a<bool> ()) j.value (v.as<bool> ());
+ else if (t.is_a<int64_t> ()) j.value (v.as<int64_t> ());
+ else if (t.is_a<uint64_t> ()) j.value (v.as<uint64_t> ());
+ else if (t.is_a<string> ()) j.value (v.as<string> ());
+ else if (t.is_a<path> ()) j.value (v.as<path> ().string ());
+ else if (t.is_a<dir_path> ()) j.value (v.as<dir_path> ().string ());
+ else if (t.is_a<target_triplet> ()) j.value (v.as<target_triplet> ().string ());
+ else if (t.is_a<project_name> ()) j.value (v.as<project_name> ().string ());
+ else if (t.is_a<int64s> ()) s_array (v.as<int64s> ());
+ else if (t.is_a<uint64s> ()) s_array (v.as<uint64s> ());
+ else if (t.is_a<strings> ()) s_array (v.as<strings> ());
+ else if (t.is_a<paths> ()) s_array_string (v.as<paths> ());
+ else if (t.is_a<dir_paths> ()) s_array_string (v.as<dir_paths> ());
+ else
+ {
+ // Note: check in the derived-first order.
+ //
+ if (t.is_a<name> ()) h_array = false;
+ else if (t.is_a<name_pair> ())
+ {
+ h_array = false;
+ h_pair = true;
+ }
+ else if (t.is_a<process_path_ex> ())
+ {
+ // Decide on array dynamically.
+ h_pair = true;
+ }
+ else if (t.is_a<process_path> ())
+ {
+ h_array = false;
+ h_pair = true;
+ }
+ else if (t.is_a<cmdline> () ||
+ t.is_a<vector<name>> ())
+ {
+ h_array = true;
+ }
+ else if (t.is_a<vector<pair<string, string>>> () ||
+ t.is_a<vector<pair<string, optional<string>>>> () ||
+ t.is_a<vector<pair<string, optional<bool>>>> () ||
+ t.is_a<map<string, string>> () ||
+ t.is_a<map<string, optional<string>>> () ||
+ t.is_a<map<string, optional<bool>>> () ||
+ t.is_a<map<project_name, dir_path>> ())
+ {
+ h_array = true;
+ h_pair = true;
+ }
+ else if (t.is_a<map<optional<string>, string>> () ||
+ t.is_a<vector<pair<optional<string>, string>>> ())
+ {
+ h_array = true;
+ h_pair = false;
+ }
+
+ goto fall_through;
+ }
+
+ return;
+
+ fall_through:
+ ;
+ }
+
+ names storage;
+ names_view ns (reverse (v, storage, true /* reduce */));
+
+ if (ns.empty ())
+ {
+ // When it comes to representing an empty value, our options are: empty
+ // array ([]), empty object ({}), or an absent member. The latter feels
+ // closer to null than empty, so that's out. After some experimentation,
+ // it feels the best choice is to use array unless we know for sure it
+ // is not, in which case we use an object if it's a pair and empty
+ // string otherwise (the empty string makes sense because we serialize
+ // complex names as target names; see below).
+ //
+ if (!h_array || *h_array)
+ {
+ j.begin_array ();
+ j.end_array ();
+ }
+ else
+ {
+ if (h_pair)
+ {
+ j.begin_object ();
+ j.end_object ();
+ }
+ else
+ j.value ("");
+ }
+ }
+ else
+ {
+ if (!h_array)
+ h_array = ns.size () > 2 || (ns.size () == 2 && !ns.front ().pair);
+
+ if (*h_array)
+ j.begin_array ();
+
+ // While it may be tempting to try to provide a heterogeneous array
+ // (i.e., all strings, all objects, all pairs), in case of pairs we
+ // actually don't know whether a non-pair element is first or second
+ // (it's up to interpretation; though we do hint which one is optional
+ // for typed values above). So we serialize each name in its most
+ // appropriate form.
+ //
+ auto simple = [] (const name& n)
+ {
+ return n.simple () || n.directory () || n.file ();
+ };
+
+ auto s_simple = [&j] (const name& n)
+ {
+ if (n.simple ())
+ j.value (n.value);
+ else if (n.directory ())
+ j.value (n.dir.string ());
+ else if (n.file ())
+ {
+ // Note: both must be present due to earlier checks.
+ //
+ j.value ((n.dir / n.value).string ());
+ }
+ else
+ return false;
+
+ return true;
+ };
+
+ for (auto i (ns.begin ()), e (ns.end ()); i != e; )
+ {
+ const name& l (*i++);
+ const name* r (l.pair ? &*i++ : nullptr);
+
+ optional<bool> hp (h_pair);
+
+ if (!hp && r != nullptr && simple (l) && simple (*r))
+ hp = true;
+
+ if (hp)
+ {
+ // Pair of simple names.
+ //
+ j.begin_object ();
+
+ if (r != nullptr)
+ {
+ j.member_name ("first"); s_simple (l);
+ j.member_name ("second"); s_simple (*r);
+ }
+ else
+ {
+ j.member_name (*hp ? "first" : "second"); s_simple (l);
+ }
+
+ j.end_object ();
+ }
+ else if (r == nullptr && s_simple (l))
+ ;
+ else
+ {
+ // If complex name (or pair thereof), then assume a target name.
+ //
+ dump_quoted_target_name (j,
+ names_view (&l, r != nullptr ? 2 : 1),
+ false /* relative */);
+ }
+ }
+
+ if (*h_array)
+ j.end_array ();
+ }
+ }
+#endif
+
enum class variable_kind {scope, tt_pat, target, rule, prerequisite};
static void
@@ -127,6 +444,68 @@ namespace build2
}
}
+#ifndef BUILD2_BOOTSTRAP
+ static void
+ dump_variable (json::stream_serializer& j,
+ const variable_map& vm,
+ const variable_map::const_iterator& vi,
+ const scope& s,
+ variable_kind k)
+ {
+ // Note: see the buildfile version above for comments.
+
+ assert (k != variable_kind::tt_pat); // TODO
+
+ const auto& p (*vi);
+ const variable& var (p.first);
+ const value& v (p.second);
+
+ lookup l (v, var, vm);
+ if (k != variable_kind::prerequisite)
+ {
+ if (var.override ())
+ return; // Ignore.
+
+ if (var.overrides != nullptr)
+ {
+ l = s.lookup_override (
+ var,
+ make_pair (l, 1),
+ k == variable_kind::target || k == variable_kind::rule,
+ k == variable_kind::rule).first;
+
+ assert (l.defined ()); // We at least have the original.
+ }
+ }
+
+ // Note that we do not distinguish between variable/value type.
+ //
+ // An empty value of a non-array type is represented as an empty object
+ // ({}).
+ //
+#if 0
+ struct variable
+ {
+ string name;
+ optional<string> type;
+ json_value value; // string|number|boolean|null|object|array
+ };
+#endif
+
+ j.begin_object ();
+
+ j.member ("name", var.name);
+
+ if (l->type != nullptr)
+ j.member ("type", l->type->name);
+
+ j.member_name ("value");
+ dump_value (j, *l);
+
+ j.end_object ();
+ }
+#endif
+
static void
dump_variables (ostream& os,
string& ind,
@@ -143,6 +522,20 @@ namespace build2
}
}
+#ifndef BUILD2_BOOTSTRAP
+ static void
+ dump_variables (json::stream_serializer& j,
+ const variable_map& vars,
+ const scope& s,
+ variable_kind k)
+ {
+ for (auto i (vars.begin ()), e (vars.end ()); i != e; ++i)
+ {
+ dump_variable (j, vars, i, s, k);
+ }
+ }
+#endif
+
// Dump target type/pattern-specific variables.
//
static void
@@ -248,10 +641,27 @@ namespace build2
}
}
+ // Similar to target::matched() but for the load phase.
+ //
+ static inline bool
+ matched (const target& t, action a)
+ {
+ // Note: running serial and task_count is 0 before any operation has
+ // started.
+ //
+ if (size_t c = t[a].task_count.load (memory_order_relaxed))
+ {
+ if (c == t.ctx.count_applied () || c == t.ctx.count_executed ())
+ return true;
+ }
+
+ return false;
+ }
+
static void
- dump_target (optional<action> a,
- ostream& os,
+ dump_target (ostream& os,
string& ind,
+ optional<action> a,
const target& t,
const scope& s,
bool rel)
@@ -260,6 +670,9 @@ namespace build2
// scope. To achieve this we are going to temporarily lower the stream
// path verbosity to level 0.
//
+ // @@ Not if in src and out != src? Otherwise end up with ../../../...
+ // See JSON version for the state of the art.
+ //
stream_verbosity osv, nsv;
if (rel)
{
@@ -321,32 +734,26 @@ namespace build2
// If the target has been matched to a rule, we also print resolved
// prerequisite targets.
//
- // Note: running serial and task_count is 0 before any operation has
- // started.
- //
const prerequisite_targets* pts (nullptr);
{
action inner; // @@ Only for the inner part of the action currently.
- if (size_t c = t[inner].task_count.load (memory_order_relaxed))
+ if (matched (t, inner))
{
- if (c == t.ctx.count_applied () || c == t.ctx.count_executed ())
- {
- pts = &t.prerequisite_targets[inner];
+ pts = &t.prerequisite_targets[inner];
- bool f (false);
- for (const target* pt: *pts)
+ bool f (false);
+ for (const target* pt: *pts)
+ {
+ if (pt != nullptr)
{
- if (pt != nullptr)
- {
- f = true;
- break;
- }
+ f = true;
+ break;
}
-
- if (!f)
- pts = nullptr;
}
+
+ if (!f)
+ pts = nullptr;
}
}
@@ -510,10 +917,318 @@ namespace build2
stream_verb (os, osv);
}
+#ifndef BUILD2_BOOTSTRAP
+ static void
+ dump_target (json::stream_serializer& j,
+ optional<action> a,
+ const target& t,
+ const scope& s,
+ bool rel,
+ target_name_cache& tcache)
+ {
+ // Note: see the buildfile version above for comments.
+
+ // Note that the target name (and display_name) are relative to the
+ // containing scope (if any).
+ //
+#if 0
+ struct prerequisite
+ {
+ string name; // Quoted/qualified name.
+ string type;
+ vector<variable> variables; // Prerequisite variables.
+ };
+
+ struct loaded_target
+ {
+ string name; // Quoted/qualified name.
+ string display_name;
+ string type;
+ //string declaration;
+ optional<string> group; // Quoted/qualified group target name.
+
+ vector<variable> variables; // Target variables.
+
+ vector<prerequisite> prerequisites;
+ };
+
+ // @@ TODO: target attributes (rule_hint)
+
+ struct prerequisite_target
+ {
+ string name; // Target name (always absolute).
+ string type;
+ bool adhoc;
+ };
+
+ struct operation_state
+ {
+ string rule; // null if direct recipe match
+
+ optional<string> state; // unchanged|changed|group
+
+ vector<variable> variables; // Rule variables.
+
+ vector<prerequisite_target> prerequisite_targets;
+ };
+
+ struct matched_target
+ {
+ string name;
+ string display_name;
+ string type;
+ //string declaration;
+ optional<string> group;
+
+ optional<path> path; // Absent of not path-based target, not assigned.
+
+ vector<variable> variables;
+
+ optional<operation_state> outer_operation; // null if not matched.
+ operation_state inner_operation; // null if not matched.
+ };
+#endif
+
+ j.begin_object ();
+
+ j.member_name ("name");
+ dump_quoted_target_name (j, t, rel /* relative */);
+
+ j.member_name ("display_name");
+ dump_display_target_name (j, t, rel /* relative */);
+
+ j.member ("type", t.type ().name);
+
+ // @@ This value currently doesn't make much sense:
+ //
+ // - why are all the system headers prereq-new?
+ //
+ // - why is synthesized obje{} prereq-new?
+ //
+#if 0
+ {
+ const char* v (nullptr);
+ switch (t.decl)
+ {
+ case target_decl::prereq_new: v = "prerequisite-new"; break;
+ case target_decl::prereq_file: v = "prerequisite-file"; break;
+ case target_decl::implied: v = "implied"; break;
+ case target_decl::real: v = "real"; break;
+ }
+ j.member ("declaration", v);
+ }
+#endif
+
+ if (t.group != nullptr)
+ {
+ j.member_name ("group");
+ dump_quoted_target_name (j, *t.group, tcache);
+ }
+
+ if (a)
+ {
+ const string* v (nullptr);
+
+ if (t.is_a<dir> () || t.is_a<fsdir> ())
+ {
+ v = &t.dir.string ();
+ }
+ else if (const auto* pt = t.is_a<path_target> ())
+ {
+ const path& p (pt->path ());
+
+ if (!p.empty ())
+ v = &p.string ();
+ }
+
+ if (v != nullptr)
+ j.member ("path", *v);
+ }
+
+ // Target variables.
+ //
+ if (!t.vars.empty ())
+ {
+ j.member_begin_array ("variables");
+ dump_variables (j, t.vars, s, variable_kind::target);
+ j.end_array ();
+ }
+
+ // Prerequisites.
+ //
+ if (!a)
+ {
+ const prerequisites& ps (t.prerequisites ());
+
+ if (!ps.empty ())
+ {
+ j.member_begin_array ("prerequisites");
+
+ for (const prerequisite& p: ps)
+ {
+ j.begin_object ();
+
+ {
+ // Cobble together an equivalent of dump_quoted_target_name().
+ //
+ prerequisite_key pk (p.key ());
+ target_key& tk (pk.tk);
+
+ // It's possible that the containing scope differs from
+ // prerequisite's. This, for example, happens when we copy the
+ // prerequisite for a synthesized obj{} dependency that happens to
+ // be in a subdirectory, as in exe{foo}:src/cxx{foo}. In this
+ // case, we need to rebase relative paths to the containing scope.
+ //
+ dir_path d, o;
+ if (p.scope != s)
+ {
+ if (tk.out->empty ())
+ {
+ if (tk.dir->relative ())
+ {
+ d = (p.scope.out_path () / *tk.dir).relative (s.out_path ());
+ tk.dir = &d;
+ }
+ }
+ else
+ {
+ if (tk.dir->relative ())
+ {
+ d = (p.scope.src_path () / *tk.dir).relative (s.src_path ());
+ tk.dir = &d;
+ }
+
+ if (tk.out->relative ())
+ {
+ o = (p.scope.out_path () / *tk.out).relative (s.out_path ());
+ if (o.empty ())
+ o = dir_path (".");
+ tk.out = &o;
+ }
+ }
+ }
+
+ // If prerequisite paths are absolute, keep them absolute.
+ //
+ ostringstream os;
+ stream_verb (os, stream_verbosity (1, 0));
+
+ if (pk.proj)
+ os << *pk.proj << '%';
+
+ to_stream (os, pk.tk.as_name (), quote_mode::effective, '@');
+
+ j.member ("name", os.str ());
+ }
+
+ j.member ("type", p.type.name);
+
+ if (!p.vars.empty ())
+ {
+ j.member_begin_array ("variables");
+ dump_variables (j, p.vars, s, variable_kind::prerequisite);
+ j.end_array ();
+ }
+
+ j.end_object ();
+ }
+
+ j.end_array ();
+ }
+ }
+ else
+ {
+ // Matched rules and their state (prerequisite_targets, vars, etc).
+ //
+ auto dump_opstate = [&tcache, &j, &s, &t] (action a)
+ {
+ const target::opstate& o (t[a]);
+
+ j.begin_object ();
+
+ j.member ("rule", o.rule != nullptr ? o.rule->first.c_str () : nullptr);
+
+ // It feels natural to omit the unknown state, as if it corresponded
+ // to absent in optional<target_state>.
+ //
+ if (o.state != target_state::unknown)
+ {
+ assert (o.state == target_state::unchanged ||
+ o.state == target_state::changed ||
+ o.state == target_state::group);
+
+ j.member ("state", to_string (o.state));
+ }
+
+ if (!o.vars.empty ())
+ {
+ j.member_begin_array ("variables");
+ dump_variables (j, o.vars, s, variable_kind::rule);
+ j.end_array ();
+ }
+
+ {
+ bool first (true);
+ for (const prerequisite_target& pt: t.prerequisite_targets[a])
+ {
+ if (pt.target == nullptr)
+ continue;
+
+ if (first)
+ {
+ j.member_begin_array ("prerequisite_targets");
+ first = false;
+ }
+
+ j.begin_object ();
+
+ j.member_name ("name");
+ dump_quoted_target_name (j, *pt.target, tcache);
+
+ j.member ("type", pt.target->type ().name);
+
+ if (pt.adhoc ())
+ j.member ("adhoc", true);
+
+ j.end_object ();
+ }
+
+ if (!first)
+ j.end_array ();
+ }
+
+ j.end_object ();
+ };
+
+ if (a->outer ())
+ {
+ j.member_name ("outer_operation");
+ if (matched (t, *a))
+ dump_opstate (*a);
+ else
+ j.value (nullptr);
+ }
+
+ {
+ action ia (a->inner_action ());
+
+ j.member_name ("inner_operation");
+ if (matched (t, ia))
+ dump_opstate (ia);
+ else
+ j.value (nullptr);
+ }
+ }
+
+ j.end_object ();
+ }
+#endif
+
static void
- dump_scope (optional<action> a,
- ostream& os,
+ dump_scope (ostream& os,
string& ind,
+ optional<action> a,
scope_map::const_iterator& i,
bool rel)
{
@@ -588,21 +1303,25 @@ namespace build2
// disabled amalgamation will be printed directly inside the global
// scope).
//
- for (auto e (p.ctx.scopes.end ());
- (i != e &&
- i->second.front () != nullptr &&
- i->second.front ()->parent_scope () == &p); )
+ for (auto e (p.ctx.scopes.end ()); i != e; )
{
- if (vb || rb || sb)
+ if (i->second.front () == nullptr)
+ ++i; // Skip over src paths.
+ else if (i->second.front ()->parent_scope () != &p)
+ break; // Moved past our parent.
+ else
{
- os << endl;
- vb = rb = false;
- }
+ if (vb || rb || sb)
+ {
+ os << endl;
+ vb = rb = false;
+ }
- os << endl; // Extra newline between scope blocks.
+ os << endl; // Extra newline between scope blocks.
- dump_scope (a, os, ind, i, true /* relative */);
- sb = true;
+ dump_scope (os, ind, a, i, true /* relative */);
+ sb = true;
+ }
}
// Targets.
@@ -624,7 +1343,7 @@ namespace build2
}
os << endl; // Extra newline between targets.
- dump_target (a, os, ind, t, p, true /* relative */);
+ dump_target (os, ind, a, t, p, true /* relative */);
tb = true;
}
@@ -635,59 +1354,245 @@ namespace build2
<< ind << '}';
}
+#ifndef BUILD2_BOOTSTRAP
+ static void
+ dump_scope (json::stream_serializer& j,
+ optional<action> a,
+ scope_map::const_iterator& i,
+ bool rel,
+ target_name_cache& tcache)
+ {
+ // Note: see the buildfile version above for additional comments.
+
+ const scope& p (*i->second.front ());
+ const dir_path& d (i->first);
+ ++i;
+
+#if 0
+ struct scope
+ {
+ // The out_path member is relative to parent scopes. It is empty for the
+ // special global scope. The src_path member is absent if the same as
+ // out_path (in-source build or scope outside of project).
+ //
+ string out_path;
+ optional<string> src_path;
+
+ vector<variable> variables; // Non-type/pattern scope variables.
+
+ vector<scope> scopes; // Immediate children.
+
+ vector<loaded_target|matched_target> targets;
+ };
+#endif
+
+ j.begin_object ();
+
+ if (d.empty ())
+ j.member ("out_path", ""); // Global scope.
+ else
+ {
+ const dir_path& rd (rel ? relative (d) : d);
+ j.member ("out_path", rd.empty () ? string (".") : rd.string ());
+
+ if (!p.out_eq_src ())
+ j.member ("src_path", p.src_path ().string ());
+ }
+
+ const dir_path* orb (relative_base);
+ relative_base = &d;
+
+ // Scope variables.
+ //
+ if (!p.vars.empty ())
+ {
+ j.member_begin_array ("variables");
+ dump_variables (j, p.vars, p, variable_kind::scope);
+ j.end_array ();
+ }
+
+ // Nested scopes of which we are an immediate parent.
+ //
+ {
+ bool first (true);
+ for (auto e (p.ctx.scopes.end ()); i != e; )
+ {
+ if (i->second.front () == nullptr)
+ ++i;
+ else if (i->second.front ()->parent_scope () != &p)
+ break;
+ else
+ {
+ if (first)
+ {
+ j.member_begin_array ("scopes");
+ first = false;
+ }
+
+ dump_scope (j, a, i, true /* relative */, tcache);
+ }
+ }
+
+ if (!first)
+ j.end_array ();
+ }
+
+ // Targets.
+ //
+ {
+ bool first (true);
+ for (const auto& pt: p.ctx.targets)
+ {
+ const target& t (*pt);
+
+ if (&p != &t.base_scope ()) // @@ PERF
+ continue;
+
+ // Skip targets that haven't been matched for this action.
+ //
+ if (a)
+ {
+ if (!(matched (t, a->inner_action ()) ||
+ (a->outer () && matched (t, *a))))
+ continue;
+ }
+
+ if (first)
+ {
+ j.member_begin_array ("targets");
+ first = false;
+ }
+
+ dump_target (j, a, t, p, true /* relative */, tcache);
+ }
+
+ if (!first)
+ j.end_array ();
+ }
+
+ relative_base = orb;
+ j.end_object ();
+ }
+#endif
+
void
- dump (const context& c, optional<action> a)
+ dump (const context& c, optional<action> a, dump_format fmt)
{
auto i (c.scopes.begin ());
assert (i->second.front () == &c.global_scope);
- // We don't lock diag_stream here as dump() is supposed to be called from
- // the main thread prior/after to any other threads being spawned.
- //
- string ind;
- ostream& os (*diag_stream);
- dump_scope (a, os, ind, i, false /* relative */);
- os << endl;
+ switch (fmt)
+ {
+ case dump_format::buildfile:
+ {
+ // We don't lock diag_stream here as dump() is supposed to be called
+ // from the main thread prior/after to any other threads being
+ // spawned.
+ //
+ string ind;
+ ostream& os (*diag_stream);
+ dump_scope (os, ind, a, i, false /* relative */);
+ os << endl;
+ break;
+ }
+ case dump_format::json:
+ {
+#ifndef BUILD2_BOOTSTRAP
+ target_name_cache tc;
+ json::stream_serializer j (cout, 0 /* indent */);
+ dump_scope (j, a, i, false /* relative */, tc);
+ cout << endl;
+#else
+ assert (false);
+#endif
+ break;
+ }
+ }
}
void
- dump (const scope* s, optional<action> a, const char* cind)
+ dump (const scope* s, optional<action> a, dump_format fmt, const char* cind)
{
- string ind (cind);
- ostream& os (*diag_stream);
-
+ scope_map::const_iterator i;
if (s != nullptr)
{
const scope_map& m (s->ctx.scopes);
- auto i (m.find_exact (s->out_path ()));
+ i = m.find_exact (s->out_path ());
assert (i != m.end () && i->second.front () == s);
-
- dump_scope (a, os, ind, i, false /* relative */);
}
- else
- os << ind << "<no known scope to dump>";
- os << endl;
+ switch (fmt)
+ {
+ case dump_format::buildfile:
+ {
+ string ind (cind);
+ ostream& os (*diag_stream);
+
+ if (s != nullptr)
+ dump_scope (os, ind, a, i, false /* relative */);
+ else
+ os << ind << "<no known scope to dump>";
+
+ os << endl;
+ break;
+ }
+ case dump_format::json:
+ {
+#ifndef BUILD2_BOOTSTRAP
+ target_name_cache tc;
+ json::stream_serializer j (cout, 0 /* indent */);
+
+ if (s != nullptr)
+ dump_scope (j, a, i, false /* relative */, tc);
+ else
+ j.value (nullptr);
+
+ cout << endl;
+#else
+ assert (false);
+#endif
+ break;
+ }
+ }
}
void
- dump (const target* t, optional<action> a, const char* cind)
+ dump (const target* t, optional<action> a, dump_format fmt, const char* cind)
{
- string ind (cind);
- ostream& os (*diag_stream);
+ const scope* bs (t != nullptr ? &t->base_scope () : nullptr);
- if (t != nullptr)
+ switch (fmt)
{
- dump_target (a,
- os,
- ind,
- *t,
- t->base_scope (),
- false /* relative */);
- }
- else
- os << ind << "<no known target to dump>";
+ case dump_format::buildfile:
+ {
+ string ind (cind);
+ ostream& os (*diag_stream);
- os << endl;
+ if (t != nullptr)
+ dump_target (os, ind, a, *t, *bs, false /* relative */);
+ else
+ os << ind << "<no known target to dump>";
+
+ os << endl;
+ break;
+ }
+ case dump_format::json:
+ {
+#ifndef BUILD2_BOOTSTRAP
+ target_name_cache tc;
+ json::stream_serializer j (cout, 0 /* indent */);
+
+ if (t != nullptr)
+ dump_target (j, a, *t, *bs, false /* relative */, tc);
+ else
+ j.value (nullptr);
+
+ cout << endl;
+#else
+ assert (false);
+#endif
+ break;
+ }
+ }
}
}
diff --git a/libbuild2/dump.hxx b/libbuild2/dump.hxx
index 4e08634..1a1a080 100644
--- a/libbuild2/dump.hxx
+++ b/libbuild2/dump.hxx
@@ -4,6 +4,10 @@
#ifndef LIBBUILD2_DUMP_HXX
#define LIBBUILD2_DUMP_HXX
+#ifndef BUILD2_BOOTSTRAP
+# include <libbutl/json/serializer.hxx>
+#endif
+
#include <libbuild2/types.hxx>
#include <libbuild2/forward.hxx>
#include <libbuild2/utility.hxx>
@@ -14,6 +18,8 @@
namespace build2
{
+ enum class dump_format {buildfile, json};
+
// Dump the build state to diag_stream. If action is specified, then assume
// rules have been matched for this action and dump action-specific
// information (like rule-specific variables).
@@ -22,13 +28,30 @@ namespace build2
// appropriate indication.
//
LIBBUILD2_SYMEXPORT void
- dump (const context&, optional<action>);
+ dump (const context&, optional<action>, dump_format);
+
+ LIBBUILD2_SYMEXPORT void
+ dump (const scope*, optional<action>, dump_format, const char* ind = "");
LIBBUILD2_SYMEXPORT void
- dump (const scope*, optional<action>, const char* ind = "");
+ dump (const target*, optional<action>, dump_format, const char* ind = "");
+#ifndef BUILD2_BOOTSTRAP
+ // Dump (effectively) quoted target name, optionally relative (to the out
+ // tree).
+ //
+ LIBBUILD2_SYMEXPORT void
+ dump_quoted_target_name (butl::json::stream_serializer&,
+ const target&,
+ bool relative = false);
+
+ // Dump display target name, optionally relative (to the out tree).
+ //
LIBBUILD2_SYMEXPORT void
- dump (const target*, optional<action>, const char* ind = "");
+ dump_display_target_name (butl::json::stream_serializer&,
+ const target&,
+ bool relative = false);
+#endif
}
#endif // LIBBUILD2_DUMP_HXX
diff --git a/libbuild2/parser.cxx b/libbuild2/parser.cxx
index 6f212da..3e8b30a 100644
--- a/libbuild2/parser.cxx
+++ b/libbuild2/parser.cxx
@@ -4702,8 +4702,10 @@ namespace build2
if (ns.empty ())
{
+ // Indent two spaces.
+ //
if (scope_ != nullptr)
- dump (scope_, nullopt /* action */, " "); // Indent two spaces.
+ dump (scope_, nullopt /* action */, dump_format::buildfile, " ");
else
os << " <no current scope>" << endl;
}
@@ -4721,8 +4723,10 @@ namespace build2
const target* t (enter_target::find_target (*this, n, o, l, trace));
+ // Indent two spaces.
+ //
if (t != nullptr)
- dump (t, nullopt /* action */, " "); // Indent two spaces.
+ dump (t, nullopt /* action */, dump_format::buildfile, " ");
else
{
os << " <no target " << n;
diff --git a/libbuild2/rule.cxx b/libbuild2/rule.cxx
index c5366a6..5d456d5 100644
--- a/libbuild2/rule.cxx
+++ b/libbuild2/rule.cxx
@@ -201,7 +201,7 @@ namespace build2
}
const file_rule file_rule::instance;
- const rule_match file_rule::rule_match ("file", file_rule::instance);
+ const rule_match file_rule::rule_match ("build.file", file_rule::instance);
// alias_rule
//
diff --git a/libbuild2/target.cxx b/libbuild2/target.cxx
index 6d083d9..5859e61 100644
--- a/libbuild2/target.cxx
+++ b/libbuild2/target.cxx
@@ -38,7 +38,9 @@ namespace build2
if (!name->empty ())
{
v = *name;
- target::combine_name (v, ext, false /* @@ TODO: what to do? */);
+ // @@ TMP: see also other calls to combine_name() -- need to fix.
+ //
+ target::combine_name (v, ext, false /* @@ TMP: what to do? */);
}
else
assert (!ext || ext->empty ()); // Unspecified or none.
diff --git a/libbuild2/variable.hxx b/libbuild2/variable.hxx
index b137789..3220a62 100644
--- a/libbuild2/variable.hxx
+++ b/libbuild2/variable.hxx
@@ -944,7 +944,7 @@ namespace build2
// pair of two empties).
//
// @@ Maybe we should redo this with optional<> to signify which half can
- // be missing?
+ // be missing? See also dump_value(json).
//
template <>
struct LIBBUILD2_SYMEXPORT value_traits<name_pair>
@@ -1213,6 +1213,8 @@ namespace build2
// value type objects for the same traits type (and we use their addressed
// as identity; see cast(const value&) for an example).
//
+ // NOTE: REMEMBER TO UPDATE dump_value(json) IF CHANGING ANYTHING HERE!
+ //
extern template struct LIBBUILD2_DECEXPORT value_traits<strings>;
extern template struct LIBBUILD2_DECEXPORT value_traits<vector<name>>;
extern template struct LIBBUILD2_DECEXPORT value_traits<paths>;