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=""):
242 for cmd in env.subst_list(cmd, target=target, source=source):
243 result.append(string.join(map(str, cmd)))
244 return string.join(result, '\\n')
247 def __init__(self, tool, variable, func, varlist):
249 if not type(variable) is type([]):
250 variable = [variable]
251 self.variable = variable
253 self.varlist = varlist
254 def __call__(self, env):
257 for v in self.variable:
260 strfunction = orig.strfunction
261 except AttributeError:
262 strfunction = Curry(Str, cmd=orig)
263 # Don't call Action() through its global function name, because
264 # that leads to infinite recursion in trying to initialize the
265 # Default Environment.
266 env[v] = SCons.Action.Action(self.func,
267 strfunction=strfunction,
268 varlist=self.varlist)
270 # This is for the benefit of printing the 'TOOLS'
271 # variable through env.Dump().
272 return repr(self.tool)
274 def Null(target, source, env):
277 def Cat(target, source, env):
278 target = str(target[0])
279 f = open(target, "wb")
280 for src in map(str, source):
281 f.write(open(src, "rb").read())
284 def CCCom(target, source, env):
285 target = str(target[0])
286 fp = open(target, "wb")
287 def process(source_file, fp=fp):
288 for line in open(source_file, "rb").readlines():
289 m = re.match(r'#include\s[<"]([^<"]+)[>"]', line)
292 for d in [str(env.Dir('$CPPPATH')), '.']:
293 f = os.path.join(d, include)
294 if os.path.exists(f):
297 elif line[:11] != "STRIP CCCOM":
299 for src in map(str, source):
301 fp.write('debug = ' + ARGUMENTS.get('debug', '0') + '\\n')
304 public_class_re = re.compile('^public class (\S+)', re.MULTILINE)
306 def JavaCCom(target, source, env):
307 # This is a fake Java compiler that just looks for
308 # public class FooBar
309 # lines in the source file(s) and spits those out
310 # to .class files named after the class.
311 tlist = map(str, target)
315 for src in map(str, source):
316 contents = open(src, "rb").read()
317 classes = public_class_re.findall(contents)
319 for t in filter(lambda x: string.find(x, c) != -1, tlist):
320 open(t, "wb").write(contents)
322 for t in not_copied.keys():
323 open(t, "wb").write("\\n")
325 def JavaHCom(target, source, env):
326 tlist = map(str, target)
327 slist = map(str, source)
328 for t, s in zip(tlist, slist):
329 open(t, "wb").write(open(s, "rb").read())
331 def find_class_files(arg, dirname, names):
332 class_files = filter(lambda n: n[-6:] == '.class', names)
333 paths = map(lambda n, d=dirname: os.path.join(d, n), class_files)
336 def JarCom(target, source, env):
337 target = str(target[0])
339 for src in map(str, source):
340 os.path.walk(src, find_class_files, class_files)
341 f = open(target, "wb")
342 for cf in class_files:
343 f.write(open(cf, "rb").read())
346 # XXX Adding COLOR, COLORS and PACKAGE to the 'cc' varlist(s) by hand
347 # here is bogus. It's for the benefit of doc/user/command-line.in, which
348 # uses examples that want to rebuild based on changes to these variables.
349 # It would be better to figure out a way to do it based on the content of
350 # the generated command-line, or else find a way to let the example markup
351 # language in doc/user/command-line.in tell this script what variables to
352 # add, but that's more difficult than I want to figure out how to do right
353 # now, so let's just use the simple brute force approach for the moment.
356 'posix' : [('cc', ['CCCOM', 'SHCCCOM'], CCCom, ['CCFLAGS', 'CPPDEFINES', 'COLOR', 'COLORS', 'PACKAGE']),
357 ('link', ['LINKCOM', 'SHLINKCOM'], Cat, []),
358 ('ar', ['ARCOM', 'RANLIBCOM'], Cat, []),
359 ('tar', 'TARCOM', Null, []),
360 ('zip', 'ZIPCOM', Null, []),
361 ('BitKeeper', 'BITKEEPERCOM', Cat, []),
362 ('CVS', 'CVSCOM', Cat, []),
363 ('RCS', 'RCS_COCOM', Cat, []),
364 ('SCCS', 'SCCSCOM', Cat, []),
365 ('javac', 'JAVACCOM', JavaCCom, []),
366 ('javah', 'JAVAHCOM', JavaHCom, []),
367 ('jar', 'JARCOM', JarCom, []),
368 ('rmic', 'RMICCOM', Cat, []),
370 'win32' : [('msvc', ['CCCOM', 'SHCCCOM', 'RCCOM'], CCCom, ['CCFLAGS', 'CPPDEFINES', 'COLOR', 'COLORS', 'PACKAGE']),
371 ('mslink', ['LINKCOM', 'SHLINKCOM'], Cat, []),
372 ('mslib', 'ARCOM', Cat, []),
373 ('tar', 'TARCOM', Null, []),
374 ('zip', 'ZIPCOM', Null, []),
375 ('BitKeeper', 'BITKEEPERCOM', Cat, []),
376 ('CVS', 'CVSCOM', Cat, []),
377 ('RCS', 'RCS_COCOM', Cat, []),
378 ('SCCS', 'SCCSCOM', Cat, []),
379 ('javac', 'JAVACCOM', JavaCCom, []),
380 ('javah', 'JAVAHCOM', JavaHCom, []),
381 ('jar', 'JARCOM', JarCom, []),
382 ('rmic', 'RMICCOM', Cat, []),
386 toollist = ToolList[platform]
387 filter_tools = string.split('%(tools)s')
389 toollist = filter(lambda x, ft=filter_tools: x[0] in ft, toollist)
391 toollist = map(lambda t: apply(ToolSurrogate, t), toollist)
393 toollist.append('install')
395 def surrogate_spawn(sh, escape, cmd, args, env):
398 def surrogate_pspawn(sh, escape, cmd, args, env, stdout, stderr):
401 SCons.Defaults.ConstructionEnvironment.update({
402 'PLATFORM' : platform,
404 'SPAWN' : surrogate_spawn,
405 'PSPAWN' : surrogate_pspawn,
408 SConscript('SConstruct')
411 # "Commands" that we will execute in our examples.
412 def command_scons(args, c, test, dict):
417 except AttributeError:
420 for arg in string.split(c.environment):
421 key, val = string.split(arg, '=')
423 save_vals[key] = os.environ[key]
425 delete_keys.append(key)
426 os.environ[key] = val
427 test.run(interpreter = sys.executable,
429 arguments = '-f - ' + string.join(args),
430 chdir = test.workpath('WORK'),
431 stdin = Stdin % dict)
432 os.environ.update(save_vals)
433 for key in delete_keys:
436 out = string.replace(out, test.workpath('ROOT'), '')
437 out = string.replace(out, test.workpath('WORK/SConstruct'),
438 '/home/my/project/SConstruct')
439 lines = string.split(out, '\n')
441 while lines[-1] == '':
445 # sys.stderr.write(err)
448 def command_touch(args, c, test, dict):
450 t = int(time.mktime(time.strptime(args[1], '%Y%m%d%H%M')))
457 if not os.path.isabs(file):
458 file = os.path.join(test.workpath('WORK'), file)
459 if not os.path.exists(file):
461 os.utime(file, times)
464 def command_edit(args, c, test, dict):
466 add_string = c.edit[:]
467 except AttributeError:
468 add_string = 'void edit(void) { ; }\n'
469 if add_string[-1] != '\n':
470 add_string = add_string + '\n'
472 if not os.path.isabs(file):
473 file = os.path.join(test.workpath('WORK'), file)
474 contents = open(file, 'rb').read()
475 open(file, 'wb').write(contents + add_string)
478 def command_ls(args, c, test, dict):
480 files = os.listdir(a)
481 files = filter(lambda x: x[0] != '.', files)
483 return [string.join(files, ' ')]
487 l.extend(ls(test.workpath('WORK', a)))
490 return ls(test.workpath('WORK'))
493 'scons' : command_scons,
494 'touch' : command_touch,
495 'edit' : command_edit,
499 def ExecuteCommand(args, c, t, dict):
501 func = CommandDict[args[0]]
503 func = lambda args, c, t, dict: []
504 return func(args[1:], c, t, dict)
506 class MySGML(sgmllib.SGMLParser):
507 """A subclass of the standard Python 2.2 sgmllib SGML parser.
509 This extends the standard sgmllib parser to recognize, and do cool
510 stuff with, the added tags that describe our SCons examples,
511 commands, and other stuff.
513 Note that this doesn't work with the 1.5.2 sgmllib module, because
514 that didn't have the ability to work with ENTITY declarations.
517 sgmllib.SGMLParser.__init__(self)
521 # The first set of methods here essentially implement pass-through
522 # handling of most of the stuff in an SGML file. We're really
523 # only concerned with the tags specific to SCons example processing,
524 # the methods for which get defined below.
526 def handle_data(self, data):
528 f = self.afunclist[-1]
530 sys.stdout.write(data)
534 def handle_comment(self, data):
535 sys.stdout.write('<!--' + data + '-->')
537 def handle_decl(self, data):
538 sys.stdout.write('<!' + data + '>')
540 def unknown_starttag(self, tag, attrs):
542 f = self.example.afunc
543 except AttributeError:
549 for name, value in attrs:
550 f(' ' + name + '=' + '"' + value + '"')
553 def unknown_endtag(self, tag):
554 sys.stdout.write('</' + tag + '>')
556 def unknown_entityref(self, ref):
557 sys.stdout.write('&' + ref + ';')
559 def unknown_charref(self, ref):
560 sys.stdout.write('&#' + ref + ';')
562 # Here is where the heavy lifting begins. The following methods
563 # handle the begin-end tags of our SCons examples.
565 def start_scons_example(self, attrs):
566 t = filter(lambda t: t[0] == 'name', attrs)
570 e = self.examples[name]
572 e = self.examples[name] = Example()
575 for name, value in attrs:
576 setattr(e, name, value)
578 self.afunclist.append(e.afunc)
580 def end_scons_example(self):
582 files = filter(lambda f: f.printme, e.files)
584 sys.stdout.write('<programlisting>')
588 while f.data[i] == ' ':
590 output = string.replace(f.data[:i+1], '__ROOT__', '')
591 output = string.replace(output, '<', '<')
592 output = string.replace(output, '>', '>')
593 sys.stdout.write(output)
594 if e.data and e.data[0] == '\n':
596 sys.stdout.write(e.data + '</programlisting>')
598 self.afunclist = self.afunclist[:-1]
600 def start_file(self, attrs):
603 except AttributeError:
604 self.error("<file> tag outside of <scons_example>")
605 t = filter(lambda t: t[0] == 'name', attrs)
607 self.error("no <file> name attribute found")
610 except AttributeError:
615 for name, value in attrs:
616 setattr(f, name, value)
618 self.afunclist.append(f.afunc)
622 self.afunclist = self.afunclist[:-1]
624 def start_directory(self, attrs):
627 except AttributeError:
628 self.error("<directory> tag outside of <scons_example>")
629 t = filter(lambda t: t[0] == 'name', attrs)
631 self.error("no <directory> name attribute found")
634 except AttributeError:
637 d = Directory(t[0][1])
638 for name, value in attrs:
639 setattr(d, name, value)
641 self.afunclist.append(d.afunc)
643 def end_directory(self):
645 self.afunclist = self.afunclist[:-1]
647 def start_scons_example_file(self, attrs):
648 t = filter(lambda t: t[0] == 'example', attrs)
650 self.error("no <scons_example_file> example attribute found")
653 e = self.examples[exname]
655 self.error("unknown example name '%s'" % exname)
656 fattrs = filter(lambda t: t[0] == 'name', attrs)
658 self.error("no <scons_example_file> name attribute found")
660 f = filter(lambda f, fname=fname: f.name == fname, e.files)
662 self.error("example '%s' does not have a file named '%s'" % (exname, fname))
665 def end_scons_example_file(self):
667 sys.stdout.write('<programlisting>')
668 sys.stdout.write(f.data + '</programlisting>')
671 def start_scons_output(self, attrs):
672 t = filter(lambda t: t[0] == 'example', attrs)
674 self.error("no <scons_output> example attribute found")
677 e = self.examples[exname]
679 self.error("unknown example name '%s'" % exname)
680 # Default values for an example.
687 for name, value in attrs:
688 setattr(o, name, value)
690 self.afunclist.append(o.afunc)
692 def end_scons_output(self):
693 # The real raison d'etre for this script, this is where we
694 # actually execute SCons to fetch the output.
697 t = TestCmd.TestCmd(workdir='', combine=1)
700 t.subdir('ROOT', 'WORK')
701 t.rootpath = string.replace(t.workpath('ROOT'), '\\', '\\\\')
704 dir = t.workpath('WORK', d.name)
705 if not os.path.exists(dir):
710 while f.data[i] == '\n':
712 lines = string.split(f.data[i:], '\n')
714 while lines[0][i] == ' ':
716 lines = map(lambda l, i=i: l[i:], lines)
717 path = string.replace(f.name, '__ROOT__', t.rootpath)
718 if not os.path.isabs(path):
719 path = t.workpath('WORK', path)
720 dir, name = os.path.split(path)
721 if dir and not os.path.exists(dir):
723 content = string.join(lines, '\n')
724 content = string.replace(content, '__ROOT__', t.rootpath)
725 path = t.workpath('WORK', path)
726 t.write(path, content)
727 if hasattr(f, 'chmod'):
728 os.chmod(path, int(f.chmod, 0))
731 while o.prefix[i-1] != '\n':
734 sys.stdout.write('<screen>' + o.prefix[:i])
737 for c in o.commandlist:
738 sys.stdout.write(p + Prompt[o.os])
739 d = string.replace(c.data, '__ROOT__', '')
740 sys.stdout.write('<userinput>' + d + '</userinput>\n')
742 e = string.replace(c.data, '__ROOT__', t.workpath('ROOT'))
743 args = string.split(e)
744 lines = ExecuteCommand(args, c, t, {'osname':o.os, 'tools':o.tools})
749 content = string.join(lines, '\n' + p)
751 content = re.sub(' at 0x[0-9a-fA-F]*\>', ' at 0x700000>', content)
752 content = string.replace(content, '<', '<')
753 content = string.replace(content, '>', '>')
754 sys.stdout.write(p + content + '\n')
756 if o.data[0] == '\n':
758 sys.stdout.write(o.data + '</screen>')
760 self.afunclist = self.afunclist[:-1]
762 def start_scons_output_command(self, attrs):
765 except AttributeError:
766 self.error("<scons_output_command> tag outside of <scons_output>")
769 except AttributeError:
773 for name, value in attrs:
774 setattr(c, name, value)
775 o.commandlist.append(c)
776 self.afunclist.append(c.afunc)
778 def end_scons_output_command(self):
780 self.afunclist = self.afunclist[:-1]
782 def start_sconstruct(self, attrs):
785 self.afunclist.append(f.afunc)
787 def end_sconstruct(self):
789 sys.stdout.write('<programlisting>')
790 output = string.replace(f.data, '__ROOT__', '')
791 sys.stdout.write(output + '</programlisting>')
793 self.afunclist = self.afunclist[:-1]
795 # The main portion of the program itself, short and simple.
811 if f is not sys.stdin:
814 if data.startswith('<?xml '):
815 first_line, data = data.split('\n', 1)
816 sys.stdout.write(first_line + '\n')