%scons; %builders-mod; %functions-mod; %tools-mod; %variables-mod; ]> Controlling Build Output A key aspect of creating a usable build configuration is providing good output from the build so its users can readily understand what the build is doing and get information about how to control the build. &SCons; provides several ways of controlling output from the build configuration to help make the build more useful and understandable.
Providing Build Help: the &Help; Function It's often very useful to be able to give users some help that describes the specific targets, build options, etc., that can be used for your build. &SCons; provides the &Help; function to allow you to specify this help text: Help(""" Type: 'scons program' to build the production program, 'scons debug' to build the debug version. """) Optionally, one can specify the append flag: Help(""" Type: 'scons program' to build the production program, 'scons debug' to build the debug version. """, append=True) (Note the above use of the Python triple-quote syntax, which comes in very handy for specifying multi-line strings like help text.) When the &SConstruct; or &SConscript; files contain such a call to the &Help; function, the specified help text will be displayed in response to the &SCons; -h option: scons -h The &SConscript; files may contain multiple calls to the &Help; function, in which case the specified text(s) will be concatenated when displayed. This allows you to split up the help text across multiple &SConscript; files. In this situation, the order in which the &SConscript; files are called will determine the order in which the &Help; functions are called, which will determine the order in which the various bits of text will get concatenated. When used with &AddOption; Help("text", append=False) will clobber any help output associated with AddOption(). To preserve the help output from AddOption(), set append=True. Another use would be to make the help text conditional on some variable. For example, suppose you only want to display a line about building a Windows-only version of a program when actually run on Windows. The following &SConstruct; file: env = Environment() Help("\nType: 'scons program' to build the production program.\n") if env['PLATFORM'] == 'win32': Help("\nType: 'scons windebug' to build the Windows debug version.\n") Will display the complete help text on Windows: scons -h But only show the relevant option on a Linux or UNIX system: scons -h If there is no &Help; text in the &SConstruct; or &SConscript; files, &SCons; will revert to displaying its standard list that describes the &SCons; command-line options. This list is also always displayed whenever the -H option is used.
Controlling How &SCons; Prints Build Commands: the <envar>$*COMSTR</envar> Variables Sometimes the commands executed to compile object files or link programs (or build other targets) can get very long, long enough to make it difficult for users to distinguish error messages or other important build output from the commands themselves. All of the default $*COM variables that specify the command lines used to build various types of target files have a corresponding $*COMSTR variable that can be set to an alternative string that will be displayed when the target is built. For example, suppose you want to have &SCons; display a "Compiling" message whenever it's compiling an object file, and a "Linking" when it's linking an executable. You could write a &SConstruct; file that looks like: env = Environment(CCCOMSTR = "Compiling $TARGET", LINKCOMSTR = "Linking $TARGET") env.Program('foo.c') foo.c Which would then yield the output: % scons -Q Compiling foo.o Linking foo &SCons; performs complete variable substitution on $*COMSTR variables, so they have access to all of the standard variables like &cv-TARGET; &cv-SOURCES;, etc., as well as any construction variables that happen to be configured in the construction environment used to build a specific target. Of course, sometimes it's still important to be able to see the exact command that &SCons; will execute to build a target. For example, you may simply need to verify that &SCons; is configured to supply the right options to the compiler, or a developer may want to cut-and-paste a compile command to add a few options for a custom test. One common way to give users control over whether or not &SCons; should print the actual command line or a short, configured summary is to add support for a VERBOSE command-line variable to your &SConstruct; file. A simple configuration for this might look like: env = Environment() if ARGUMENTS.get('VERBOSE') != "1': env['CCCOMSTR'] = "Compiling $TARGET" env['LINKCOMSTR'] = "Linking $TARGET" env.Program('foo.c') foo.c By only setting the appropriate $*COMSTR variables if the user specifies VERBOSE=1 on the command line, the user has control over how &SCons; displays these particular command lines: % scons -Q Compiling foo.o Linking foo % scons -Q -c Removed foo.o Removed foo % scons -Q VERBOSE=1 cc -o foo.o -c foo.c cc -o foo foo.o
Providing Build Progress Output: the &Progress; Function Another aspect of providing good build output is to give the user feedback about what &SCons; is doing even when nothing is being built at the moment. This can be especially true for large builds when most of the targets are already up-to-date. Because &SCons; can take a long time making absolutely sure that every target is, in fact, up-to-date with respect to a lot of dependency files, it can be easy for users to mistakenly conclude that &SCons; is hung or that there is some other problem with the build. One way to deal with this perception is to configure &SCons; to print something to let the user know what it's "thinking about." The &Progress; function allows you to specify a string that will be printed for every file that &SCons; is "considering" while it is traversing the dependency graph to decide what targets are or are not up-to-date. Progress('Evaluating $TARGET\n') Program('f1.c') Program('f2.c') f1.c f2.c Note that the &Progress; function does not arrange for a newline to be printed automatically at the end of the string (as does the Python print statement), and we must specify the \n that we want printed at the end of the configured string. This configuration, then, will have &SCons; print that it is Evaluating each file that it encounters in turn as it traverses the dependency graph: scons -Q Of course, normally you don't want to add all of these additional lines to your build output, as that can make it difficult for the user to find errors or other important messages. A more useful way to display this progress might be to have the file names printed directly to the user's screen, not to the same standard output stream where build output is printed, and to use a carriage return character (\r) so that each file name gets re-printed on the same line. Such a configuration would look like: Progress('$TARGET\r', file=open('/dev/tty', 'w'), overwrite=True) Program('f1.c') Program('f2.c') Note that we also specified the overwrite=True argument to the &Progress; function, which causes &SCons; to "wipe out" the previous string with space characters before printing the next &Progress; string. Without the overwrite=True argument, a shorter file name would not overwrite all of the charactes in a longer file name that precedes it, making it difficult to tell what the actual file name is on the output. Also note that we opened up the /dev/tty file for direct access (on POSIX) to the user's screen. On Windows, the equivalent would be to open the con: file name. Also, it's important to know that although you can use $TARGET to substitute the name of the node in the string, the &Progress; function does not perform general variable substitution (because there's not necessarily a construction environment involved in evaluating a node like a source file, for example). You can also specify a list of strings to the &Progress; function, in which case &SCons; will display each string in turn. This can be used to implement a "spinner" by having &SCons; cycle through a sequence of strings: Progress(['-\r', '\\\r', '|\r', '/\r'], interval=5) Program('f1.c') Program('f2.c') Note that here we have also used the interval= keyword argument to have &SCons; only print a new "spinner" string once every five evaluated nodes. Using an interval= count, even with strings that use $TARGET like our examples above, can be a good way to lessen the work that &SCons; expends printing &Progress; strings, while still giving the user feedback that indicates &SCons; is still working on evaluating the build. Lastly, you can have direct control over how to print each evaluated node by passing a Python function (or other Python callable) to the &Progress; function. Your function will be called for each evaluated node, allowing you to implement more sophisticated logic like adding a counter: screen = open('/dev/tty', 'w') count = 0 def progress_function(node) count += 1 screen.write('Node %4d: %s\r' % (count, node)) Progress(progress_function) Of course, if you choose, you could completely ignore the node argument to the function, and just print a count, or anything else you wish. (Note that there's an obvious follow-on question here: how would you find the total number of nodes that will be evaluated so you can tell the user how close the build is to finishing? Unfortunately, in the general case, there isn't a good way to do that, short of having &SCons; evaluate its dependency graph twice, first to count the total and the second time to actually build the targets. This would be necessary because you can't know in advance which target(s) the user actually requested to be built. The entire build may consist of thousands of Nodes, for example, but maybe the user specifically requested that only a single object file be built.)
Printing Detailed Build Status: the &GetBuildFailures; Function SCons, like most build tools, returns zero status to the shell on success and nonzero status on failure. Sometimes it's useful to give more information about the build status at the end of the run, for instance to print an informative message, send an email, or page the poor slob who broke the build. SCons provides a &GetBuildFailures; method that you can use in a python atexit function to get a list of objects describing the actions that failed while attempting to build targets. There can be more than one if you're using -j. Here's a simple example: import atexit def print_build_failures(): from SCons.Script import GetBuildFailures for bf in GetBuildFailures(): print "%s failed: %s" % (bf.node, bf.errstr) atexit.register(print_build_failures) The atexit.register call registers print_build_failures as an atexit callback, to be called before &SCons; exits. When that function is called, it calls &GetBuildFailures; to fetch the list of failed objects. See the man page for the detailed contents of the returned objects; some of the more useful attributes are .node, .errstr, .filename, and .command. The filename is not necessarily the same file as the node; the node is the target that was being built when the error occurred, while the filenameis the file or dir that actually caused the error. Note: only call &GetBuildFailures; at the end of the build; calling it at any other time is undefined. Here is a more complete example showing how to turn each element of &GetBuildFailures; into a string: # Make the build fail if we pass fail=1 on the command line if ARGUMENTS.get('fail', 0): Command('target', 'source', ['/bin/false']) def bf_to_str(bf): """Convert an element of GetBuildFailures() to a string in a useful way.""" import SCons.Errors if bf is None: # unknown targets product None in list return '(unknown tgt)' elif isinstance(bf, SCons.Errors.StopError): return str(bf) elif bf.node: return str(bf.node) + ': ' + bf.errstr elif bf.filename: return bf.filename + ': ' + bf.errstr return 'unknown failure: ' + bf.errstr import atexit def build_status(): """Convert the build status to a 2-tuple, (status, msg).""" from SCons.Script import GetBuildFailures bf = GetBuildFailures() if bf: # bf is normally a list of build failures; if an element is None, # it's because of a target that scons doesn't know anything about. status = 'failed' failures_message = "\n".join(["Failed building %s" % bf_to_str(x) for x in bf if x is not None]) else: # if bf is None, the build completed successfully. status = 'ok' failures_message = '' return (status, failures_message) def display_build_status(): """Display the build status. Called by atexit. Here you could do all kinds of complicated things.""" status, failures_message = build_status() if status == 'failed': print "FAILED!!!!" # could display alert, ring bell, etc. elif status == 'ok': print "Build succeeded." print failures_message atexit.register(display_build_status) When this runs, you'll see the appropriate output: scons -Q scons -Q fail=1