diff options
Diffstat (limited to 'libbuild2/diagnostics.hxx')
-rw-r--r-- | libbuild2/diagnostics.hxx | 673 |
1 files changed, 603 insertions, 70 deletions
diff --git a/libbuild2/diagnostics.hxx b/libbuild2/diagnostics.hxx index f3d9db4..ef41f22 100644 --- a/libbuild2/diagnostics.hxx +++ b/libbuild2/diagnostics.hxx @@ -14,17 +14,239 @@ namespace build2 { - using butl::diag_record; + struct diag_record; // Throw this exception to terminate the build. The handler should // assume that the diagnostics has already been issued. // class failed: public std::exception {}; - // Print process commmand line. If the number of elements is specified - // (or the second version is used), then it will print the piped multi- - // process command line, if present. In this case, the expected format - // is as follows: + // Print low-verbosity recipe diagnostics in the forms: + // + // <prog> <l-target> <comb> <r-target> + // <prog> <r-target> + // + // Where <prog> is an abbreviated/generalized program name, such as c++ + // (rather than g++ or clang++) or yacc (rather than bison or byacc), + // <l-target> is typically the "main" prerequisite target, such as the C++ + // source file to compile, <r-target> is typically the target being + // produced, and <comb> is the combiner, typically "->". + // + // The second form (without <l-target> and <comb>) should be used when there + // is no natural "main" prerequisite, for example, for linking as well as + // for programs that act upon the target, such as mkdir, rm, test, etc. + // + // Note also that these functions omit the @.../ qualification in either + // <l-target> or <r-target> if it's implied by the other. + // + // For example: + // + // mkdir fsdir{details/} + // c++ cxx{hello} -> obje{hello} + // ld exe{hello} + // + // test exe{hello} + testscript + // + // install exe{hello} -> /usr/bin/ + // uninstall exe{hello} <- /usr/bin/ + // + // rm exe{hello} + // rm obje{hello} + // rmdir fsdir{details/} + // + // Examples of target groups: + // + // cli cli{foo} -> {hxx cxx}{foo} + // + // thrift thrift{foo} -> {hxx cxx}{foo-types} + // {hxx cxx}{foo-stubs} + // + // Potentially we could also support target groups for <l-target>: + // + // tool {hxx cxx}{foo} -> {hxx cxx}{foo-types} + // + // tool {hxx cxx}{foo-types} + // {hxx cxx}{foo-stubs} -> {hxx cxx}{foo-insts} + // {hxx cxx}{foo-impls} + // + // Currently we only support this for the `group -> dir_path` form (used + // by the backlink machinery). + // + // See also the `diag` Buildscript pseudo-builtin which is reduced to one of + // the print_diag() calls (adhoc_buildscript_rule::print_custom_diag()). In + // particular, if you are adding a new overload, also consider if/how it + // should handled there. + // + // Note: see GH issue #40 for additional background and rationale. + // + // If <comb> is not specified, then "->" is used by default. + + // prog target -> target + // prog target -> group + // + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const target& l, const target& r, + const char* comb = nullptr); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + target_key&& l, const target& r, + const char* comb = nullptr); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const target& l, target_key&& r, + const char* comb = nullptr); + + void + print_diag (const char* prog, + target_key&& l, target_key&& r, + const char* comb = nullptr); + + // Note: using small_vector would require target_key definition. + // + void + print_diag (const char* prog, + target_key&& l, vector<target_key>&& r, + const char* comb = nullptr); + + // prog path -> target + // prog path -> group + // + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const path& l, const target& r, + const char* comb = nullptr); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const path& l, target_key&& r, + const char* comb = nullptr); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const path& l, vector<target_key>&& r, + const char* comb = nullptr); + + // prog string -> target + // prog string -> group + // + // Use these versions if, for example, input information is passed as an + // argument. + // + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const string& l, const target& r, + const char* comb = nullptr); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const string& l, target_key&& r, + const char* comb = nullptr); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const string& l, vector<target_key>&& r, + const char* comb = nullptr); + + // prog target + // + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, const target&); + + void + print_diag (const char* prog, target_key&&); + + // prog group + // + void + print_diag (const char* prog, vector<target_key>&&); + + // prog path + // + // Special versions for cases like mkdir/rmdir, save, etc. + // + // Note: use path_name("-") if the result is written to stdout. + // + void + print_diag (const char* prog, const path&); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, const dir_path&); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, const path_name_view&); + + // Special versions for ln, cp, rm, install/unistall, dist, etc. + // + // Note: use path_name ("-") if the result is written to stdout. + + // prog target -> path + // + void + print_diag (const char* prog, + const target& l, const path& r, + const char* comb = nullptr); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const target& l, const dir_path& r, + const char* comb = nullptr); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const target& l, const path_name_view& r, + const char* comb = nullptr); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + target_key&& l, const dir_path& r, + const char* comb = nullptr); + + // prog group -> dir_path + // + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + vector<target_key>&& l, const dir_path& r, + const char* comb = nullptr); + + // prog path -> path + // + void + print_diag (const char* prog, + const path& l, const path& r, + const char* comb = nullptr); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const path& l, const dir_path& r, + const char* comb = nullptr); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const path& l, const path_name_view& r, + const char* comb = nullptr); + + // prog string -> path + // + // Use this version if, for example, input information is passed as an + // argument. + // + void + print_diag (const char* prog, + const string& l, const path& r, + const char* comb = nullptr); + + LIBBUILD2_SYMEXPORT void + print_diag (const char* prog, + const string& l, const path_name_view& r, + const char* comb = nullptr); + + // Print process commmand line. If the number of elements is specified (or + // the const cstrings& version is used), then it will print the piped multi- + // process command line, if present. In this case, the expected format is as + // follows: // // name1 arg arg ... nullptr // name2 arg arg ... nullptr @@ -179,92 +401,124 @@ namespace build2 using butl::diag_progress_lock; // Return true if progress is to be shown. The max_verb argument is the - // maximum verbosity level that this type of progress should be shown by - // default. + // maximum verbosity level that this type of progress should be shown at by + // default. If it is verb_never, then both min and max verbosity checks are + // omitted, assuming the caller takes care of that themselves. // inline bool show_progress (uint16_t max_verb) { return diag_progress_option ? *diag_progress_option - : stderr_term && verb >= 1 && verb <= max_verb; + : stderr_term && (max_verb == verb_never || + (verb >= 1 && verb <= max_verb)); } - // Diagnostic facility, base infrastructure. + // Diagnostics color. + // + inline bool + show_diag_color () + { + return diag_color_option ? *diag_color_option : stderr_term_color; + } + + // Diagnostic facility. + // + // Note that this is the "complex" case we we derive from (rather than + // alias) a number of butl::diag_* types and provide custom operator<< + // "overrides" in order to make ADL look in the build2 rather than butl + // namespace. // using butl::diag_stream_lock; using butl::diag_stream; using butl::diag_epilogue; + using butl::diag_frame; - // Diagnostics stack. Each frame is "applied" to the fail/error/warn/info - // diag record. - // - // Unfortunately most of our use-cases don't fit into the 2-pointer small - // object optimization of std::function. So we have to complicate things - // a bit here. - // - struct LIBBUILD2_SYMEXPORT diag_frame + template <typename> struct diag_prologue; + template <typename> struct diag_mark; + + struct diag_record: butl::diag_record { - explicit - diag_frame (void (*f) (const diag_frame&, const diag_record&)) - : func_ (f) + template <typename T> + const diag_record& + operator<< (const T& x) const { - if (func_ != nullptr) - prev_ = stack (this); + os << x; + return *this; } - diag_frame (diag_frame&& x) - : func_ (x.func_) - { - if (func_ != nullptr) - { - prev_ = x.prev_; - stack (this); + diag_record () = default; - x.func_ = nullptr; - } - } + template <typename B> + explicit + diag_record (const diag_prologue<B>& p): diag_record () { *this << p;} - diag_frame& operator= (diag_frame&&) = delete; + template <typename B> + explicit + diag_record (const diag_mark<B>& m): diag_record () { *this << m;} + }; - diag_frame (const diag_frame&) = delete; - diag_frame& operator= (const diag_frame&) = delete; + template <typename B> + struct diag_prologue: butl::diag_prologue<B> + { + using butl::diag_prologue<B>::diag_prologue; - ~diag_frame () + template <typename T> + diag_record + operator<< (const T& x) const { - if (func_ != nullptr ) - stack (prev_); + diag_record r; + r.append (this->indent, this->epilogue); + B::operator() (r); + r << x; + return r; } - static void - apply (const diag_record& r) + friend const diag_record& + operator<< (const diag_record& r, const diag_prologue& p) { - for (const diag_frame* f (stack ()); f != nullptr; f = f->prev_) - f->func_ (*f, r); + r.append (p.indent, p.epilogue); + p (r); + return r; } + }; - // Tip of the stack. - // - static const diag_frame* - stack () noexcept; + template <typename B> + struct diag_mark: butl::diag_mark<B> + { + using butl::diag_mark<B>::diag_mark; - // Set the new and return the previous tip of the stack. - // - static const diag_frame* - stack (const diag_frame*) noexcept; + template <typename T> + diag_record + operator<< (const T& x) const + { + return B::operator() () << x; + } - struct stack_guard + friend const diag_record& + operator<< (const diag_record& r, const diag_mark& m) { - explicit stack_guard (const diag_frame* s): s_ (stack (s)) {} - ~stack_guard () {stack (s_);} - const diag_frame* s_; - }; + return r << m (); + } + }; - private: - void (*func_) (const diag_frame&, const diag_record&); - const diag_frame* prev_; + template <typename B> + struct diag_noreturn_end: butl::diag_noreturn_end<B> + { + diag_noreturn_end () {} // For Clang 3.7 (const needs user default ctor). + + using butl::diag_noreturn_end<B>::diag_noreturn_end; + + [[noreturn]] friend void + operator<< (const diag_record& r, const diag_noreturn_end& e) + { + assert (r.full ()); + e.B::operator() (r); + } }; + // Note: diag frames are not applied to text/trace diagnostics. + // template <typename F> struct diag_frame_impl: diag_frame { @@ -273,9 +527,10 @@ namespace build2 private: static void - thunk (const diag_frame& f, const diag_record& r) + thunk (const diag_frame& f, const butl::diag_record& r) { - static_cast<const diag_frame_impl&> (f).func_ (r); + static_cast<const diag_frame_impl&> (f).func_ ( + static_cast<const diag_record&> (r)); } const F func_; @@ -288,8 +543,6 @@ namespace build2 return diag_frame_impl<F> (move (f)); } - // Diagnostic facility, project specifics. - // struct LIBBUILD2_SYMEXPORT simple_prologue_base { explicit @@ -352,8 +605,8 @@ namespace build2 struct basic_mark_base { - using simple_prologue = butl::diag_prologue<simple_prologue_base>; - using location_prologue = butl::diag_prologue<location_prologue_base>; + using simple_prologue = diag_prologue<simple_prologue_base>; + using location_prologue = diag_prologue<location_prologue_base>; explicit basic_mark_base (const char* type, @@ -427,7 +680,7 @@ namespace build2 const void* data_; diag_epilogue* const epilogue_; }; - using basic_mark = butl::diag_mark<basic_mark_base>; + using basic_mark = diag_mark<basic_mark_base>; LIBBUILD2_SYMEXPORT extern const basic_mark error; LIBBUILD2_SYMEXPORT extern const basic_mark warn; @@ -452,7 +705,7 @@ namespace build2 mod, name) {} }; - using trace_mark = butl::diag_mark<trace_mark_base>; + using trace_mark = diag_mark<trace_mark_base>; using tracer = trace_mark; // fail @@ -464,17 +717,17 @@ namespace build2 const void* data = nullptr) : basic_mark_base (type, data, - [](const diag_record& r) + [](const butl::diag_record& r, butl::diag_writer* w) { diag_frame::apply (r); - r.flush (); + r.flush (w); throw failed (); }, &stream_verb_map, nullptr, nullptr) {} }; - using fail_mark = butl::diag_mark<fail_mark_base>; + using fail_mark = diag_mark<fail_mark_base>; struct fail_end_base { @@ -488,11 +741,289 @@ namespace build2 throw failed (); } }; - using fail_end = butl::diag_noreturn_end<fail_end_base>; + using fail_end = diag_noreturn_end<fail_end_base>; LIBBUILD2_SYMEXPORT extern const fail_mark fail; LIBBUILD2_SYMEXPORT extern const fail_end endf; + // Diagnostics buffer. + // + // The purpose of this class is to handle diagnostics from child processes, + // where handle can mean: + // + // - Buffer it (to avoid interleaving in parallel builds). + // + // - Stream it (if the input can be split into diagnostic records). + // + // - Do nothing (in serial builds or if requested not to buffer). + // + // In the future this class may also be responsible for converting the + // diagnostics into the structured form (which means it may need to buffer + // even in serial builds). + // + // The typical usage is as follows: + // + // process pr (..., diag_buffer::pipe (ctx)); + // diag_buffer dbuf (ctx, args[0], pr); // Skip. + // ifdstream is (move (pr.in_ofd)); // No skip. + // ofdstream os (move (pr.out_fd)); + // + // The reason for this somewhat roundabout setup is to make sure the + // diag_buffer instance is destroyed before the process instance. This is + // important in case an exception is thrown where we want to make sure all + // our pipe ends are closed before we wait for the process exit (which + // happens in the process destructor). + // + // And speaking of the destruction order, another thing to keep in mind is + // that only one stream can use the skip mode (fdstream_mode::skip; because + // skipping is performed in the blocking mode) and the stream that skips + // should come first so that all other streams are destroyed/closed before + // it (failed that, we may end up in a deadlock). For example: + // + // process pr (..., diag_buffer::pipe (ctx)); + // ifdstream is (move (pr.in_ofd), fdstream_mode::skip); // Skip. + // diag_buffer dbuf (ctx, args[0], pr, fdstream_mode::none); // No skip. + // ofdstream os (move (pr.out_fd)); + // + class LIBBUILD2_SYMEXPORT diag_buffer + { + public: + // If buffering is necessary or force is true, return an "instruction" + // (-1) to the process class constructor to open a pipe and redirect + // stderr to it. Otherwise, return an "instruction" to inherit stderr (2). + // + // The force flag is normally used if custom diagnostics processing is + // required (filter, split, etc; see read() below). + // + // Note that the diagnostics buffer must be opened (see below) regardless + // of the pipe() result. + // + static int + pipe (context&, bool force = false); + + // Open the diagnostics buffer given the parent end of the pipe (normally + // process:in_efd). If it is nullfd, then assume no buffering is + // necessary. If mode is non_blocking, then make reading from the parent + // end of the pipe non-blocking. + // + // The args0 argument is the child process program name for diagnostics. + // It is expected to remain valid until the call to close() and should + // normally be the same as args[0] passed to close(). + // + // Note that the same buffer can go through multiple open-read-close + // sequences, for example, to execute multiple commands. + // + // All the below functions handle io errors, issue suitable diagnostics, + // and throw failed. If an exception is thrown from any of them, then the + // instance should not be used any further. + // + // Note that when reading from multiple streams in the non-blocking mode, + // only the last stream to be destroyed can normally have the skip mode + // since in case of an exception, skipping will be blocking. + // + diag_buffer (context&, + const char* args0, + auto_fd&&, + fdstream_mode = fdstream_mode::skip); + + // As above, but the parrent end of the pipe (process:in_efd) is passed + // via a process instance. + // + diag_buffer (context&, + const char* args0, + process&, + fdstream_mode = fdstream_mode::skip); + + // As above but with support for the underlying buffer reuse. + // + // Note that in most cases reusing the buffer is probably not worth the + // trouble because we normally don't expect any diagnostics in the common + // case. However, if needed, it can be arranged, for example: + // + // vector<char> buf; + // + // { + // process pr (...); + // diag_buffer dbuf (ctx, move (buf), args[0], pr); + // dbuf.read (); + // dbuf.close (); + // buf = move (dbuf.buf); + // } + // + // { + // ... + // } + // + // Note also that while there is no guarantee the underlying buffer is + // moved when, say, the vector is empty, all the main implementations + // always steal the buffer. + // + diag_buffer (context&, + vector<char>&& buf, + const char* args0, + auto_fd&&, + fdstream_mode = fdstream_mode::skip); + + diag_buffer (context&, + vector<char>&& buf, + const char* args0, + process&, + fdstream_mode = fdstream_mode::skip); + + // Separate construction and opening. + // + // Note: be careful with the destruction order (see above for details). + // + explicit + diag_buffer (context&); + + diag_buffer (context&, vector<char>&& buf); + + void + open (const char* args0, + auto_fd&&, + fdstream_mode = fdstream_mode::skip); + + // Open the buffer in the state as if after read() returned false, that + // is, the stream corresponding to the parent's end of the pipe reached + // EOF and has been closed. This is primarily useful when the diagnostics + // is being read in a custom way (for example, it has been merged to + // stdout) and all we want is to be able to call write() and close(). + // + void + open_eof (const char* args0); + + // Check whether the buffer has been opened with the open() call and + // hasn't yet been closed. + // + // Note that this function returning true does not mean that the pipe was + // opened (to check that, call is_open() on the stream member; see below). + // + bool + is_open () const + { + return state_ != state::closed; + } + + // Read the diagnostics from the parent's end of the pipe if one was + // opened and buffer/stream it as necessary or forced. Return true if + // there could be more diagnostics to read (only possible in the non- + // blocking mode) and false otherwise, in which case also close the + // stream. + // + // Note that the force argument here (as well as in write() below) and + // in open() above are independent. Specifically, force in open() forces + // the opening of the pipe while force in read() and write() forces + // the buffering of the diagnostics. + // + // Instead of calling this function you can perform custom reading and, if + // necessary, buffering of the diagnostics by accessing the input stream + // (is) and underlying buffer (buf) directly. This can be used to filter, + // split the diagnostics into records according to a certain format, etc. + // Note that such custom processing implementation should maintain the + // overall semantics of diagnostics buffering in that it may only omit + // buffering in the serial case or if the diagnostics can be streamed in + // atomic records. See also write() below. + // + // The input stream is opened in the text mode and has the badbit but not + // failbit exception mask. The custom processing should also be compatible + // with the stream mode (blocking or non). If buffering is performed, then + // depending on the expected diagnostics the custom processing may want to + // reserve an appropriate initial buffer size to avoid unnecessary + // reallocation. As a convenience, in the blocking mode only, if the + // stream still contains some diagnostics, then it can be handled by + // calling read(). This is useful when needing to process only the inital + // part of the diagnostics. The custom processing may also close the + // stream manually before calling close(). + // + bool + read (bool force = false); + + // Close the parent end of the pipe if one was opened and write out any + // buffered diagnostics. + // + // If the child process exited abnormally or normally with non-0 code, + // then print the error diagnostics to this effect. Additionally, if the + // verbosity level is between 1 and the specified value, then print the + // command line as info after the error. If omit_normal is true, then + // don't print either for the normal exit (usually used for custom + // diagnostics or when process failure can be tolerated). + // + // Normally the specified verbosity will be 1 and the command line args + // represent the verbosity level 2 (logical) command line. Note that args + // should only represent a single command in a pipe (see print_process() + // below for details). + // + // If the diag_buffer instance is destroyed before calling close(), then + // any buffered diagnostics is discarded. + // + // Note: see also run_finish(diag_buffer&). + // + // @@ TODO: need overload with process_env (see print_process). Also in + // run_finish_impl(). + // + void + close (const cstrings& args, + const process_exit&, + uint16_t verbosity, + bool omit_normal = false, + const location& = {}); + + void + close (const char* const* args, + const process_exit&, + uint16_t verbosity, + bool omit_normal = false, + const location& = {}); + + // As above but with a custom diag record for the child exit diagnostics, + // if any. Note that if the diag record has the fail epilogue, then this + // function will throw. + // + void + close (diag_record&& = {}); + + // Direct access to the underlying stream and buffer for custom processing + // (see read() above for details). + // + // If serial is true, then we are running serially. If nobuf is true, + // then we are running in parallel but diagnostics buffering has been + // disabled (--no-diag-buffer). Note that there is a difference: during + // the serial execution we are free to hold the diag_stream_lock for as + // long as convenient, for example, for the whole duration of child + // process execution. Doing the same during parallel execution is very + // bad idea and we should read/write the diagnostics in chunks, normally + // one line at a time. + // + public: + ifdstream is; + vector<char> buf; + const char* args0; + bool serial; + bool nobuf; + + // Buffer or stream a fragment of diagnostics as necessary or forced. If + // newline is true, also add a trailing newline. + // + // This function is normally called from a custom diagnostics processing + // implementation (see read() above for details). If nobuf is true, then + // the fragment should end on the line boundary to avoid interleaving. + // + void + write (const string&, bool newline, bool force = false); + + private: + // Note that we don't seem to need a custom destructor to achieve the + // desired semantics: we can assume the process has exited before we are + // destroyed (because we supply stderr to its constructor) which means + // closing fdstream without reading any futher should be ok. + // + enum class state {closed, opened, eof}; + + context& ctx_; + state state_ = state::closed; + }; + // Action phrases, e.g., "configure update exe{foo}", "updating exe{foo}", // and "updating exe{foo} is configured". Use like this: // @@ -558,4 +1089,6 @@ namespace build2 } } +#include <libbuild2/diagnostics.ixx> + #endif // LIBBUILD2_DIAGNOSTICS_HXX |