4 # Copyright (c) 2003 Steven Knight
6 # Permission is hereby granted, free of charge, to any person obtaining
7 # a copy of this software and associated documentation files (the
8 # "Software"), to deal in the Software without restriction, including
9 # without limitation the rights to use, copy, modify, merge, publish,
10 # distribute, sublicense, and/or sell copies of the Software, and to
11 # permit persons to whom the Software is furnished to do so, subject to
12 # the following conditions:
14 # The above copyright notice and this permission notice shall be included
15 # in all copies or substantial portions of the Software.
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
18 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
19 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26 __revision__ = "/home/scons/sconsoutput/branch.0/baseline/src/sconsoutput.py 0.4.D001 2004/11/27 18:44:37 knight"
29 # sconsoutput.py - an SGML preprocessor for capturing SCons output
30 # and inserting it into examples in our DocBook
33 # This script looks for some SGML tags that describe SCons example
34 # configurations and commands to execute in those configurations, and
35 # uses TestCmd.py to execute the commands and insert the output from
36 # those commands into the SGML that we output. This way, we can run a
37 # script and update all of our example documentation output without
38 # a lot of laborious by-hand checking.
40 # An "SCons example" looks like this, and essentially describes a set of
41 # input files (program source files as well as SConscript files):
43 # <scons_example name="ex1">
44 # <file name="SConstruct" printme="1">
49 # int main() { printf("foo.c\n"); }
53 # The <file> contents within the <scons_example> tag will get written
54 # into a temporary directory whenever example output needs to be
55 # generated. By default, the <file> contents are not inserted into text
56 # directly, unless you set the "printme" attribute on one or more files,
57 # in which case they will get inserted within a <programlisting> tag.
58 # This makes it easy to define the example at the appropriate
59 # point in the text where you intend to show the SConstruct file.
61 # Note that you should usually give the <scons_example> a "name"
62 # attribute so that you can refer to the example configuration later to
63 # run SCons and generate output.
65 # If you just want to show a file's contents without worry about running
66 # SCons, there's a shorter <sconstruct> tag:
73 # This is essentially equivalent to <scons_example><file printme="1">,
74 # but it's more straightforward.
76 # SCons output is generated from the following sort of tag:
78 # <scons_output example="ex1" os="posix">
79 # <scons_output_command>scons -Q foo</scons_output_command>
80 # <scons_output_command>scons -Q foo</scons_output_command>
83 # You tell it which example to use with the "example" attribute, and then
84 # give it a list of <scons_output_command> tags to execute. You can also
85 # supply an "os" tag, which specifies the type of operating system this
86 # example is intended to show; if you omit this, default value is "posix".
88 # The generated SGML will show the command line (with the appropriate
89 # command-line prompt for the operating system), execute the command in
90 # a temporary directory with the example files, capture the standard
91 # output from SCons, and insert it into the text as appropriate.
92 # Error output gets passed through to your error output so you
93 # can see if there are any problems executing the command.
104 sys.path.append(os.path.join(os.getcwd(), 'QMTest'))
105 sys.path.append(os.path.join(os.getcwd(), 'build', 'QMTest'))
107 scons_py = os.path.join('bootstrap', 'src', 'script', 'scons.py')
108 if not os.path.exists(scons_py):
109 scons_py = os.path.join('src', 'script', 'scons.py')
111 scons_lib_dir = os.path.join(os.getcwd(), 'bootstrap', 'src', 'engine')
112 if not os.path.exists(scons_lib_dir):
113 scons_lib_dir = os.path.join(os.getcwd(), 'src', 'engine')
115 os.environ['SCONS_LIB_DIR'] = scons_lib_dir
119 # The regular expression that identifies entity references in the
120 # standard sgmllib omits the underscore from the legal characters.
121 # Override it with our own regular expression that adds underscore.
122 sgmllib.entityref = re.compile('&([a-zA-Z][-_.a-zA-Z0-9]*)[^-_a-zA-Z0-9]')
124 # Classes for collecting different types of data we're interested in.
126 """Generic class for collecting data between a start tag and end
127 tag. We subclass for various types of tags we care about."""
130 def afunc(self, data):
131 self.data = self.data + data
133 class Example(DataCollector):
134 """An SCons example. This is essentially a list of files that
135 will get written to a temporary directory to collect output
136 from one or more SCons runs."""
138 DataCollector.__init__(self)
142 class File(DataCollector):
143 """A file, that will get written out to a temporary directory
144 for one or more SCons runs."""
145 def __init__(self, name):
146 DataCollector.__init__(self)
149 class Directory(DataCollector):
150 """A directory, that will get created in a temporary directory
151 for one or more SCons runs."""
152 def __init__(self, name):
153 DataCollector.__init__(self)
156 class Output(DataCollector):
157 """Where the command output goes. This is essentially
158 a list of commands that will get executed."""
160 DataCollector.__init__(self)
161 self.commandlist = []
163 class Command(DataCollector):
164 """A tag for where the command output goes. This is essentially
165 a list of commands that will get executed."""
167 DataCollector.__init__(self)
175 # The magick SCons hackery that makes this work.
177 # So that our examples can still use the default SConstruct file, we
178 # actually feed the following into SCons via stdin and then have it
179 # SConscript() the SConstruct file. This stdin wrapper creates a set
180 # of ToolSurrogates for the tools for the appropriate platform. These
181 # Surrogates print output like the real tools and behave like them
182 # without actually having to be on the right platform or have the right
185 # The upshot: The wrapper transparently changes the world out from
186 # under the top-level SConstruct file in an example just so we can get
187 # the command output.
194 import SCons.Defaults
197 platform = '%(osname)s'
205 # Slip our own __str__() method into the EntryProxy class used to expand
206 # $TARGET{S} and $SOURCE{S} to translate the path-name separators from
207 # what's appropriate for the system we're running on to what's appropriate
208 # for the example system.
209 orig = SCons.Node.FS.EntryProxy
210 class MyEntryProxy(orig):
212 return string.replace(str(self._Proxy__subject), os.sep, Sep)
213 SCons.Node.FS.EntryProxy = MyEntryProxy
215 # Slip our own RDirs() method into the Node.FS.File class so that the
216 # expansions of $_{CPPINC,F77INC,LIBDIR}FLAGS will have the path-name
217 # separators translated from what's appropriate for the system we're
218 # running on to what's appropriate for the example system.
219 orig_RDirs = SCons.Node.FS.File.RDirs
220 def my_RDirs(self, pathlist, orig_RDirs=orig_RDirs):
221 return map(lambda x: string.replace(str(x), os.sep, Sep),
222 orig_RDirs(self, pathlist))
223 SCons.Node.FS.File.RDirs = my_RDirs
226 def __init__(self, fun, *args, **kwargs):
228 self.pending = args[:]
229 self.kwargs = kwargs.copy()
231 def __call__(self, *args, **kwargs):
232 if kwargs and self.kwargs:
233 kw = self.kwargs.copy()
236 kw = kwargs or self.kwargs
238 return apply(self.fun, self.pending + args, kw)
240 def Str(target, source, env, cmd=""):
241 return env.subst(cmd, target=target, source=source)
244 def __init__(self, tool, variable, func, varlist):
246 if not type(variable) is type([]):
247 variable = [variable]
248 self.variable = variable
250 self.varlist = varlist
251 def __call__(self, env):
254 for v in self.variable:
257 strfunction = orig.strfunction
258 except AttributeError:
259 strfunction = Curry(Str, cmd=orig)
260 # Don't call Action() through its global function name, because
261 # that leads to infinite recursion in trying to initialize the
262 # Default Environment.
263 env[v] = SCons.Action.Action(self.func,
264 strfunction=strfunction,
265 varlist=self.varlist)
267 # This is for the benefit of printing the 'TOOLS'
268 # variable through env.Dump().
269 return repr(self.tool)
271 def Null(target, source, env):
274 def Cat(target, source, env):
275 target = str(target[0])
276 f = open(target, "wb")
277 for src in map(str, source):
278 f.write(open(src, "rb").read())
281 def CCCom(target, source, env):
282 target = str(target[0])
283 fp = open(target, "wb")
284 def process(source_file, fp=fp):
285 for line in open(source_file, "rb").readlines():
286 m = re.match(r'#include\s[<"]([^<"]+)[>"]', line)
289 for d in [str(env.Dir('$CPPPATH')), '.']:
290 f = os.path.join(d, include)
291 if os.path.exists(f):
294 elif line[:11] != "STRIP CCCOM":
296 for src in map(str, source):
298 fp.write('debug = ' + ARGUMENTS.get('debug', '0') + '\\n')
301 public_class_re = re.compile('^public class (\S+)', re.MULTILINE)
303 def JavaCCom(target, source, env):
304 # This is a fake Java compiler that just looks for
305 # public class FooBar
306 # lines in the source file(s) and spits those out
307 # to .class files named after the class.
308 tlist = map(str, target)
312 for src in map(str, source):
313 contents = open(src, "rb").read()
314 classes = public_class_re.findall(contents)
316 for t in filter(lambda x: string.find(x, c) != -1, tlist):
317 open(t, "wb").write(contents)
319 for t in not_copied.keys():
320 open(t, "wb").write("\\n")
322 def JavaHCom(target, source, env):
323 tlist = map(str, target)
324 slist = map(str, source)
325 for t, s in zip(tlist, slist):
326 open(t, "wb").write(open(s, "rb").read())
328 def find_class_files(arg, dirname, names):
329 class_files = filter(lambda n: n[-6:] == '.class', names)
330 paths = map(lambda n, d=dirname: os.path.join(d, n), class_files)
333 def JarCom(target, source, env):
334 target = str(target[0])
336 for src in map(str, source):
337 os.path.walk(src, find_class_files, class_files)
338 f = open(target, "wb")
339 for cf in class_files:
340 f.write(open(cf, "rb").read())
343 # XXX Adding COLOR, COLORS and PACKAGE to the 'cc' varlist(s) by hand
344 # here is bogus. It's for the benefit of doc/user/command-line.in, which
345 # uses examples that want to rebuild based on changes to these variables.
346 # It would be better to figure out a way to do it based on the content of
347 # the generated command-line, or else find a way to let the example markup
348 # language in doc/user/command-line.in tell this script what variables to
349 # add, but that's more difficult than I want to figure out how to do right
350 # now, so let's just use the simple brute force approach for the moment.
353 'posix' : [('cc', ['CCCOM', 'SHCCCOM'], CCCom, ['CCFLAGS', 'CPPDEFINES', 'COLOR', 'COLORS', 'PACKAGE']),
354 ('link', ['LINKCOM', 'SHLINKCOM'], Cat, []),
355 ('ar', ['ARCOM', 'RANLIBCOM'], Cat, []),
356 ('tar', 'TARCOM', Null, []),
357 ('zip', 'ZIPCOM', Null, []),
358 ('BitKeeper', 'BITKEEPERCOM', Cat, []),
359 ('CVS', 'CVSCOM', Cat, []),
360 ('RCS', 'RCS_COCOM', Cat, []),
361 ('SCCS', 'SCCSCOM', Cat, []),
362 ('javac', 'JAVACCOM', JavaCCom, []),
363 ('javah', 'JAVAHCOM', JavaHCom, []),
364 ('jar', 'JARCOM', JarCom, []),
365 ('rmic', 'RMICCOM', Cat, []),
367 'win32' : [('msvc', ['CCCOM', 'SHCCCOM'], CCCom, ['CCFLAGS', 'CPPDEFINES', 'COLOR', 'COLORS', 'PACKAGE']),
368 ('mslink', ['LINKCOM', 'SHLINKCOM'], Cat, []),
369 ('mslib', 'ARCOM', Cat, []),
370 ('tar', 'TARCOM', Null, []),
371 ('zip', 'ZIPCOM', Null, []),
372 ('BitKeeper', 'BITKEEPERCOM', Cat, []),
373 ('CVS', 'CVSCOM', Cat, []),
374 ('RCS', 'RCS_COCOM', Cat, []),
375 ('SCCS', 'SCCSCOM', Cat, []),
376 ('javac', 'JAVACCOM', JavaCCom, []),
377 ('javah', 'JAVAHCOM', JavaHCom, []),
378 ('jar', 'JARCOM', JarCom, []),
379 ('rmic', 'RMICCOM', Cat, []),
383 toollist = ToolList[platform]
384 filter_tools = string.split('%(tools)s')
386 toollist = filter(lambda x, ft=filter_tools: x[0] in ft, toollist)
388 toollist = map(lambda t: apply(ToolSurrogate, t), toollist)
390 toollist.append('install')
392 def surrogate_spawn(sh, escape, cmd, args, env):
395 def surrogate_pspawn(sh, escape, cmd, args, env, stdout, stderr):
398 SCons.Defaults.ConstructionEnvironment.update({
399 'PLATFORM' : platform,
401 'SPAWN' : surrogate_spawn,
402 'PSPAWN' : surrogate_pspawn,
405 SConscript('SConstruct')
408 # "Commands" that we will execute in our examples.
409 def command_scons(args, c, test, dict):
414 except AttributeError:
417 for arg in string.split(c.environment):
418 key, val = string.split(arg, '=')
420 save_vals[key] = os.environ[key]
422 delete_keys.append(key)
423 os.environ[key] = val
424 test.run(interpreter = sys.executable,
426 arguments = '-f - ' + string.join(args),
427 chdir = test.workpath('WORK'),
428 stdin = Stdin % dict)
429 os.environ.update(save_vals)
430 for key in delete_keys:
433 out = string.replace(out, test.workpath('ROOT'), '')
434 out = string.replace(out, test.workpath('WORK/SConstruct'),
435 '/home/my/project/SConstruct')
436 lines = string.split(out, '\n')
438 while lines[-1] == '':
442 # sys.stderr.write(err)
445 def command_touch(args, c, test, dict):
447 t = int(time.mktime(time.strptime(args[1], '%Y%m%d%H%M')))
454 if not os.path.isabs(file):
455 file = os.path.join(test.workpath('WORK'), file)
456 if not os.path.exists(file):
458 os.utime(file, times)
461 def command_edit(args, c, test, dict):
463 add_string = c.edit[:]
464 except AttributeError:
465 add_string = 'void edit(void) { ; }\n'
466 if add_string[-1] != '\n':
467 add_string = add_string + '\n'
469 if not os.path.isabs(file):
470 file = os.path.join(test.workpath('WORK'), file)
471 contents = open(file, 'rb').read()
472 open(file, 'wb').write(contents + add_string)
475 def command_ls(args, c, test, dict):
477 files = os.listdir(a)
478 files = filter(lambda x: x[0] != '.', files)
480 return [string.join(files, ' ')]
484 l.extend(ls(test.workpath('WORK', a)))
487 return ls(test.workpath('WORK'))
490 'scons' : command_scons,
491 'touch' : command_touch,
492 'edit' : command_edit,
496 def ExecuteCommand(args, c, t, dict):
498 func = CommandDict[args[0]]
500 func = lambda args, c, t, dict: []
501 return func(args[1:], c, t, dict)
503 class MySGML(sgmllib.SGMLParser):
504 """A subclass of the standard Python 2.2 sgmllib SGML parser.
506 This extends the standard sgmllib parser to recognize, and do cool
507 stuff with, the added tags that describe our SCons examples,
508 commands, and other stuff.
510 Note that this doesn't work with the 1.5.2 sgmllib module, because
511 that didn't have the ability to work with ENTITY declarations.
514 sgmllib.SGMLParser.__init__(self)
518 # The first set of methods here essentially implement pass-through
519 # handling of most of the stuff in an SGML file. We're really
520 # only concerned with the tags specific to SCons example processing,
521 # the methods for which get defined below.
523 def handle_data(self, data):
525 f = self.afunclist[-1]
527 sys.stdout.write(data)
531 def handle_comment(self, data):
532 sys.stdout.write('<!--' + data + '-->')
534 def handle_decl(self, data):
535 sys.stdout.write('<!' + data + '>')
537 def unknown_starttag(self, tag, attrs):
539 f = self.example.afunc
540 except AttributeError:
546 for name, value in attrs:
547 f(' ' + name + '=' + '"' + value + '"')
550 def unknown_endtag(self, tag):
551 sys.stdout.write('</' + tag + '>')
553 def unknown_entityref(self, ref):
554 sys.stdout.write('&' + ref + ';')
556 def unknown_charref(self, ref):
557 sys.stdout.write('&#' + ref + ';')
559 # Here is where the heavy lifting begins. The following methods
560 # handle the begin-end tags of our SCons examples.
562 def start_scons_example(self, attrs):
563 t = filter(lambda t: t[0] == 'name', attrs)
567 e = self.examples[name]
569 e = self.examples[name] = Example()
572 for name, value in attrs:
573 setattr(e, name, value)
575 self.afunclist.append(e.afunc)
577 def end_scons_example(self):
579 files = filter(lambda f: f.printme, e.files)
581 sys.stdout.write('<programlisting>')
585 while f.data[i] == ' ':
587 output = string.replace(f.data[:i+1], '__ROOT__', '')
588 output = string.replace(output, '<', '<')
589 output = string.replace(output, '>', '>')
590 sys.stdout.write(output)
591 if e.data and e.data[0] == '\n':
593 sys.stdout.write(e.data + '</programlisting>')
595 self.afunclist = self.afunclist[:-1]
597 def start_file(self, attrs):
600 except AttributeError:
601 self.error("<file> tag outside of <scons_example>")
602 t = filter(lambda t: t[0] == 'name', attrs)
604 self.error("no <file> name attribute found")
607 except AttributeError:
612 for name, value in attrs:
613 setattr(f, name, value)
615 self.afunclist.append(f.afunc)
619 self.afunclist = self.afunclist[:-1]
621 def start_directory(self, attrs):
624 except AttributeError:
625 self.error("<directory> tag outside of <scons_example>")
626 t = filter(lambda t: t[0] == 'name', attrs)
628 self.error("no <directory> name attribute found")
631 except AttributeError:
634 d = Directory(t[0][1])
635 for name, value in attrs:
636 setattr(d, name, value)
638 self.afunclist.append(d.afunc)
640 def end_directory(self):
642 self.afunclist = self.afunclist[:-1]
644 def start_scons_example_file(self, attrs):
645 t = filter(lambda t: t[0] == 'example', attrs)
647 self.error("no <scons_example_file> example attribute found")
650 e = self.examples[exname]
652 self.error("unknown example name '%s'" % exname)
653 fattrs = filter(lambda t: t[0] == 'name', attrs)
655 self.error("no <scons_example_file> name attribute found")
657 f = filter(lambda f, fname=fname: f.name == fname, e.files)
659 self.error("example '%s' does not have a file named '%s'" % (exname, fname))
662 def end_scons_example_file(self):
664 sys.stdout.write('<programlisting>')
665 sys.stdout.write(f.data + '</programlisting>')
668 def start_scons_output(self, attrs):
669 t = filter(lambda t: t[0] == 'example', attrs)
671 self.error("no <scons_output> example attribute found")
674 e = self.examples[exname]
676 self.error("unknown example name '%s'" % exname)
677 # Default values for an example.
684 for name, value in attrs:
685 setattr(o, name, value)
687 self.afunclist.append(o.afunc)
689 def end_scons_output(self):
690 # The real raison d'etre for this script, this is where we
691 # actually execute SCons to fetch the output.
694 t = TestCmd.TestCmd(workdir='', combine=1)
697 t.subdir('ROOT', 'WORK')
698 t.rootpath = string.replace(t.workpath('ROOT'), '\\', '\\\\')
701 dir = t.workpath('WORK', d.name)
702 if not os.path.exists(dir):
707 while f.data[i] == '\n':
709 lines = string.split(f.data[i:], '\n')
711 while lines[0][i] == ' ':
713 lines = map(lambda l, i=i: l[i:], lines)
714 path = string.replace(f.name, '__ROOT__', t.rootpath)
715 if not os.path.isabs(path):
716 path = t.workpath('WORK', path)
717 dir, name = os.path.split(path)
718 if dir and not os.path.exists(dir):
720 content = string.join(lines, '\n')
721 content = string.replace(content, '__ROOT__', t.rootpath)
722 path = t.workpath('WORK', path)
723 t.write(path, content)
724 if hasattr(f, 'chmod'):
725 os.chmod(path, int(f.chmod, 0))
728 while o.prefix[i-1] != '\n':
731 sys.stdout.write('<screen>' + o.prefix[:i])
734 for c in o.commandlist:
735 sys.stdout.write(p + Prompt[o.os])
736 d = string.replace(c.data, '__ROOT__', '')
737 sys.stdout.write('<userinput>' + d + '</userinput>\n')
739 e = string.replace(c.data, '__ROOT__', t.workpath('ROOT'))
740 args = string.split(e)
741 lines = ExecuteCommand(args, c, t, {'osname':o.os, 'tools':o.tools})
746 content = string.join(lines, '\n' + p)
748 content = re.sub(' at 0x[0-9a-fA-F]*\>', ' at 0x700000>', content)
749 content = string.replace(content, '<', '<')
750 content = string.replace(content, '>', '>')
751 sys.stdout.write(p + content + '\n')
753 if o.data[0] == '\n':
755 sys.stdout.write(o.data + '</screen>')
757 self.afunclist = self.afunclist[:-1]
759 def start_scons_output_command(self, attrs):
762 except AttributeError:
763 self.error("<scons_output_command> tag outside of <scons_output>")
766 except AttributeError:
770 for name, value in attrs:
771 setattr(c, name, value)
772 o.commandlist.append(c)
773 self.afunclist.append(c.afunc)
775 def end_scons_output_command(self):
777 self.afunclist = self.afunclist[:-1]
779 def start_sconstruct(self, attrs):
782 self.afunclist.append(f.afunc)
784 def end_sconstruct(self):
786 sys.stdout.write('<programlisting>')
787 output = string.replace(f.data, '__ROOT__', '')
788 sys.stdout.write(output + '</programlisting>')
790 self.afunclist = self.afunclist[:-1]
792 # The main portion of the program itself, short and simple.
808 if f is not sys.stdin:
811 if data.startswith('<?xml '):
812 first_line, data = data.split('\n', 1)
813 sys.stdout.write(first_line + '\n')