#!/usr/bin/env python """\ @file run_build_test.py @author Nat Goodspeed @date 2009-09-03 @brief Helper script to allow CMake to run some command after setting environment variables. CMake has commands to run an external program. But remember that each CMake command must be backed by multiple build-system implementations. Unfortunately it seems CMake can't promise that every target build system can set specified environment variables before running the external program of interest. This helper script is a workaround. It simply sets the requested environment variables and then executes the program specified on the rest of its command line. Example: python run_build_test.py -DFOO=bar myprog somearg otherarg sets environment variable FOO=bar, then runs: myprog somearg otherarg $LicenseInfo:firstyear=2009&license=viewerlgpl$ Second Life Viewer Source Code Copyright (C) 2009-2010, Linden Research, Inc. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; version 2.1 of the License only. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA $/LicenseInfo$ """ import os import sys import errno import HTMLParser import re import signal import subprocess def main(command, libpath=[], vars={}): """Pass: command is a sequence (e.g. a list) of strings. The first item in the list must be the command name, the rest are its arguments. libpath is a sequence of directory pathnames. These will be appended to the platform-specific dynamic library search path environment variable. vars is a dict of arbitrary (var, value) pairs to be added to the environment before running 'command'. This function runs the specified command, waits for it to terminate and returns its return code. This will be negative if the command terminated with a signal, else it will be the process's specified exit code. """ # Handle platform-dependent libpath first. if sys.platform == "win32": lpvars = ["PATH"] elif sys.platform == "darwin": lpvars = ["LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH"] elif sys.platform.startswith("linux"): lpvars = ["LD_LIBRARY_PATH"] else: # No idea what the right pathname might be! But only crump if this # feature is requested. if libpath: raise NotImplemented("run_build_test: unknown platform %s" % sys.platform) lpvars = [] for var in lpvars: # Split the existing path. Bear in mind that the variable in question # might not exist; instead of KeyError, just use an empty string. dirs = os.environ.get(var, "").split(os.pathsep) # Append the sequence in libpath print "%s += %r" % (var, libpath) for dir in libpath: # append system paths at the end if dir in ('/lib', '/usr/lib'): dirs.append(dir) # prepend non-system paths else: dirs.insert(0, dir) # Filter out some useless pieces clean_dirs = [] for dir in dirs: if dir and dir not in ('', '.'): clean_dirs.append(dir) # Now rebuild the path string. This way we use a minimum of separators # -- and we avoid adding a pointless separator when libpath is empty. os.environ[var] = os.pathsep.join(clean_dirs) print "%s = %r" % (var, os.environ[var]) # Now handle arbitrary environment variables. The tricky part is ensuring # that all the keys and values we try to pass are actually strings. if vars: print "Setting:" for key, value in vars.iteritems(): print "%s=%s" % (key, value) os.environ.update(dict([(str(key), str(value)) for key, value in vars.iteritems()])) # Run the child process. print "Running: %s" % " ".join(command) # Make sure we see all relevant output *before* child-process output. sys.stdout.flush() try: return subprocess.call(command) except OSError as err: # If the caller is trying to execute a test program that doesn't # exist, we want to produce a reasonable error message rather than a # traceback. This happens when the build is halted by errors, but # CMake tries to proceed with testing anyway . However, do # NOT attempt to handle any error but "doesn't exist." if err.errno != errno.ENOENT: raise # In practice, the pathnames into CMake's build tree are so long as to # obscure the name of the test program. Just print its basename. print "No such program %s; check for preceding build errors" % \ os.path.basename(command[0]) # What rc should we simulate for missing executable? Windows produces # 9009. return 9009 # swiped from vita, sigh, seems like a Bad Idea to introduce dependency def translate_rc(rc): """ Accept an rc encoded as for subprocess.Popen.returncode: None means still running int >= 0 means terminated voluntarily with specified rc int < 0 means terminated by signal (-rc) Return a string explaining the outcome. In case of a signal, try to name the corresponding symbol from the 'signal' module. """ if rc is None: return "still running" if rc >= 0: return "terminated with rc %s" % rc if sys.platform.startswith("win"): # From http://stackoverflow.com/questions/20629027/process-finished-with-exit-code-1073741571 # [-1073741571] is the signed integer representation of Microsoft's # "stack overflow/stack exhaustion" error code 0xC00000FD. # Anytime you see strange, large negative exit codes in windows, convert # them to hex and then look them up in the ntstatus error codes # http://msdn.microsoft.com/en-us/library/cc704588.aspx # Python bends over backwards to give you all the integer precision # you need, avoiding truncation. But only with 32-bit signed ints is # -1073741571 equivalent to 0xC00000FD! Explicitly truncate before # converting. hexrc = "0x%X" % (rc & 0xFFFFFFFF) # At this point, we're only trying to format the rc to make it easier # for a human being to understand. Any exception here -- file doesn't # exist, HTML parsing error, unrecognized table structure, unknown key # -- should NOT kill the script! It should only cause us to shrug and # present our caller with the best information available. try: table = get_windows_table() symbol, desc = table[hexrc] except Exception, err: print >>sys.stderr, "(%s -- carrying on)" % err return "terminated with rc %s (%s)" % (rc, hexrc) else: return "terminated with rc %s: %s: %s" % (hexrc, symbol, desc) else: # On Posix, negative rc means the child was terminated by signal -rc. rc = -rc for attr in dir(signal): if attr.startswith('SIG') and getattr(signal, attr) == rc: strc = attr break else: strc = str(rc) return "terminated by signal %s" % strc class TableParser(HTMLParser.HTMLParser): """ This HTMLParser subclass is designed to parse the table we know exists in windows-rcs.html, hopefully without building in too much knowledge of the specific way that table is currently formatted. """ # regular expression matching any string containing only whitespace whitespace = re.compile(r'\s*$') def __init__(self): # Because Python 2.x's HTMLParser is an old-style class, we must use # old-style syntax to forward the __init__() call -- not super(). HTMLParser.HTMLParser.__init__(self) # this will collect all the data, eventually self.table = [] # Stack whose top (last item) indicates where to append current # element data. When empty, don't collect data at all. self.dest = [] def handle_starttag(self, tag, attrs): if tag == "table": # This is the outermost tag we recognize. Collect nested elements # within self.table. self.dest.append(self.table) elif tag in ("tr", "td"): # Nested elements whose contents we want to capture as sublists. # To the list currently designated by the top of the dest stack, # append a new empty sublist. self.dest[-1].append([]) # Now push THAT new, empty list as the new top of the dest stack. self.dest.append(self.dest[-1][-1]) elif tag == "p": # We could handle

...

just like or , but that # introduces an unnecessary extra level of nesting. Just skip. pass else: # For any tag we don't recognize (notably ), push a new, empty # list to the top of the dest stack. This new list is NOT # referenced by anything in self.table; thus, when we pop it, any # data we've collected inside that list will be discarded. self.dest.append([]) def handle_endtag(self, tag): # Because we avoid pushing self.dest for

in handle_starttag(), we # must refrain from popping it for

here. if tag != "p": # For everything else, including unrecognized tags, pop the dest # stack, reverting to outer collection. self.dest.pop() def handle_startendtag(self, tag, attrs): # The table of interest contains entries of the form: #

0x00000000
STATUS_SUCCESS

# The
is very useful -- we definitely want two different data # items for "0x00000000" and "STATUS_SUCCESS" -- but we don't need or # want it to push, then discard, an empty list as it would if we let # the default HTMLParser.handle_startendtag() call handle_starttag() # followed by handle_endtag(). Just ignore
or any other # singleton tag. pass def handle_data(self, data): # Outside the of interest, self.dest is empty. Do not bother # collecting data when self.dest is empty. # HTMLParser calls handle_data() with every chunk of whitespace # between tags. That would be lovely if our eventual goal was to # reconstitute the original input stream with its existing formatting, # but for us, whitespace only clutters the table. Ignore it. if self.dest and not self.whitespace.match(data): # Here we're within our
and we have non-whitespace data. # Append it to the list designated by the top of the dest stack. self.dest[-1].append(data) # cache for get_windows_table() _windows_table = None def get_windows_table(): global _windows_table # If we already loaded _windows_table, no need to load it all over again. if _windows_table: return _windows_table # windows-rcs.html was fetched on 2015-03-24 with the following command: # curl -o windows-rcs.html \ # https://msdn.microsoft.com/en-us/library/cc704588.aspx parser = TableParser() with open(os.path.join(os.path.dirname(__file__), "windows-rcs.html")) as hf: # We tried feeding the file data to TableParser in chunks, to avoid # buffering the entire file as a single string. Unfortunately its # handle_data() cannot tell the difference between distinct calls # separated by HTML tags, and distinct calls necessitated by a chunk # boundary. Sigh! Read in the whole file. At the time this was # written, it was only 500KB anyway. parser.feed(hf.read()) parser.close() table = parser.table # With our parser, any row leaves a table entry # consisting only of an empty list. Remove any such. while table and not table[0]: table.pop(0) # We expect rows of the form: # [['0x00000000', 'STATUS_SUCCESS'], # ['The operation completed successfully.']] # The latter list will have multiple entries if Microsoft embedded
# or

...

in the text, in which case joining with '\n' is # appropriate. # Turn that into a dict whose key is the hex string, and whose value is # the pair (symbol, desc). _windows_table = dict((key, (symbol, '\n'.join(desc))) for (key, symbol), desc in table) return _windows_table if __name__ == "__main__": from optparse import OptionParser parser = OptionParser(usage="usage: %prog [options] command args...") # We want optparse support for the options we ourselves handle -- but we # DO NOT want it looking at options for the executable we intend to run, # rejecting them as invalid because we don't define them. So configure the # parser to stop looking for options as soon as it sees the first # positional argument (traditional Unix syntax). parser.disable_interspersed_args() parser.add_option("-D", "--define", dest="vars", default=[], action="append", metavar="VAR=value", help="Add VAR=value to the env variables defined") parser.add_option("-l", "--libpath", dest="libpath", default=[], action="append", metavar="DIR", help="Add DIR to the platform-dependent DLL search path") opts, args = parser.parse_args() # What we have in opts.vars is a list of strings of the form "VAR=value" # or possibly just "VAR". What we want is a dict. We can build that dict by # constructing a list of ["VAR", "value"] pairs -- so split each # "VAR=value" string on the '=' sign (but only once, in case we have # "VAR=some=user=string"). To handle the case of just "VAR", append "" to # the list returned by split(), then slice off anything after the pair we # want. rc = main(command=args, libpath=opts.libpath, vars=dict([(pair.split('=', 1) + [""])[:2] for pair in opts.vars])) if rc not in (None, 0): print >>sys.stderr, "Failure running: %s" % " ".join(args) print >>sys.stderr, "Error %s: %s" % (rc, translate_rc(rc)) sys.exit((rc < 0) and 255 or rc)
...