Error reporting guidelines for Boost.Build Python port
Goals
- For every error, we should be able to print the context of the error in user-visible terms, like "error when building target XXX". It should be necessary to associate error messages with Jamfiles, as much as possible -- for example, any error about building a target should say where that target is declared.
- For every error, likewise, we should be able to print the context of the error as stacktrace.
- The stacktrace should ideally show also Jamfiles from where Python is invoked.
- Python exceptions should never escape into Jam, as Jam cannot handle them in any way.
Concepts
We'll introduce 'user context', which is string description of what is being done now. The code will be pushing and popping user context as it does things. At any time, we'll have a list of currently active, nested, user context.
For accurate error reporting, all errors that are likely to be caused by user action should be reported via special function, that will record the current user context, so that it can be presented to user.
It's possible that we detect an error that is not directly traceable to user action, or Python interpreter itself detects an error and raise an Exception. We'll have try/except blocks at strategic places to associate such exceptions with user contexts.
The error handling documented here is implemented by the boost.build.build.errors
module. The main class there is Errors
, and an instance of that call can be obtained by calling the errors()
method on the Manager class.
Reporting errors
Whenever we detect error that is traceable to user's mistake, we report it like this:
self.manager().errors()("something bad happened")
This call with create a raise an instance of ExceptionWithUserContext
class, which keeps a record of what user-visible actions were performed at the point of error detection. This class also can print intself in a nice way.
When we detect an error that is totally unclear -- and could possibly be an internal error -- we just raise an exception.
Maintaining user context and handling stray exceptions
Any function that does something on interest to user must be written like this:
try: self.manager().errors().push_user_context("Doing something") do_something() except ExceptionWithUserContext, e: # We have exception with associated user context, just pass along raise except Exception, e: # We have exception with no user context. Throw new exception # that will be associated with the current user context, so that # we have at least something to report. self.mananger().errors().handle_stray_exception(e) finally: # In all cases, restore user context. self.manager().errors().pop_user_context()
Since writing this all over is not fun, the errors
module provides a function decorator user_error_checkpoint
, that allows to write your code like this:
@user_error_checkpoint def do_something(): self.manager().errors()("doing something") ....
Both approaches above assume that a function only pushes a user context once, if it pushes two nested context the first approach must be extended to use two try/except blocks, or the function split in two.
Handling Jam context
Most errors are result of some code in Jamfile, so it's important to show Jamfile lines in error
reports, too. To that effect, each function intended to be called from Jamfiles will call
Errors.push_jamfile_context
and Errors.pop_jamfile_context
. The first call will
include current jam stacktrace in the list of context, and that stacktrace will be printed
on errors. Note that this logic is completely implemented in project loading code, you
don't need to call those functions yourself, ever.
Capturing context
The targets are created when we parse Jamfiles, and built as a separate step. For all errors during building, it's desirable to report from where the target was declared. To that end:
- When the target is declared, we grab the current user context, calling
Errors.capture_user_context
. - When actually building, the call to
Errors.push_user_context
should provide a message describing what is done, and additionally, pass the previously captured context as the value of thenested
parameter.
Top-level error reporting
All functions called from Jam, including the main function of Boost.Build, will have try/catch blocks. When
ExceptionWithUserContext
is caught, its report
method will be printed. Should stray exception be
caught, despite all efforts, we'll also nicely print it.
Raw backtrace
[The behaviour described in this section is not yet implemented]
If a --stacktrace
parameter is specified on the command line, we'll print raw Python backtrace.
Such a backtrace can include Python code called from Jam code, called from Python code etc. Technically,
it appears possible to interleave stacktraces, something like:
build_system.py:100 Jamroot:12 project.py:70
an so we'll try to do that.