f140743e38ee75b1147a785697a0c481e8f8e41a
[scons.git] / bin / scons-doc.py
1 #!/usr/bin/env python
2 #
3 # Copyright (c) 2010 The SCons Foundation
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
12 #
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
15 #
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
24 #
25 # scons-doc.py -    an SGML preprocessor for capturing SCons output
26 #                   and inserting it into examples in our DocBook
27 #                   documentation
28 #
29 # This script looks for some SGML tags that describe SCons example
30 # configurations and commands to execute in those configurations, and
31 # uses TestCmd.py to execute the commands and insert the output from
32 # those commands into the SGML that we output.  This way, we can run a
33 # script and update all of our example documentation output without
34 # a lot of laborious by-hand checking.
35 #
36 # An "SCons example" looks like this, and essentially describes a set of
37 # input files (program source files as well as SConscript files):
38 #
39 #       <scons_example name="ex1">
40 #         <file name="SConstruct" printme="1">
41 #           env = Environment()
42 #           env.Program('foo')
43 #         </file>
44 #         <file name="foo.c">
45 #           int main() { printf("foo.c\n"); }
46 #         </file>
47 #       </scons_example>
48 #
49 # The <file> contents within the <scons_example> tag will get written
50 # into a temporary directory whenever example output needs to be
51 # generated.  By default, the <file> contents are not inserted into text
52 # directly, unless you set the "printme" attribute on one or more files,
53 # in which case they will get inserted within a <programlisting> tag.
54 # This makes it easy to define the example at the appropriate
55 # point in the text where you intend to show the SConstruct file.
56 #
57 # Note that you should usually give the <scons_example> a "name"
58 # attribute so that you can refer to the example configuration later to
59 # run SCons and generate output.
60 #
61 # If you just want to show a file's contents without worry about running
62 # SCons, there's a shorter <sconstruct> tag:
63 #
64 #       <sconstruct>
65 #         env = Environment()
66 #         env.Program('foo')
67 #       </sconstruct>
68 #
69 # This is essentially equivalent to <scons_example><file printme="1">,
70 # but it's more straightforward.
71 #
72 # SCons output is generated from the following sort of tag:
73 #
74 #       <scons_output example="ex1" os="posix">
75 #         <scons_output_command>scons -Q foo</scons_output_command>
76 #         <scons_output_command>scons -Q foo</scons_output_command>
77 #       </scons_output>
78 #
79 # You tell it which example to use with the "example" attribute, and then
80 # give it a list of <scons_output_command> tags to execute.  You can also
81 # supply an "os" tag, which specifies the type of operating system this
82 # example is intended to show; if you omit this, default value is "posix".
83 #
84 # The generated SGML will show the command line (with the appropriate
85 # command-line prompt for the operating system), execute the command in
86 # a temporary directory with the example files, capture the standard
87 # output from SCons, and insert it into the text as appropriate.
88 # Error output gets passed through to your error output so you
89 # can see if there are any problems executing the command.
90 #
91 from __future__ import generators  ### KEEP FOR COMPATIBILITY FIXERS
92
93 import optparse
94 import os
95 import re
96 import sgmllib
97 import sys
98 import time
99
100 sys.path.append(os.path.join(os.getcwd(), 'QMTest'))
101 sys.path.append(os.path.join(os.getcwd(), 'build', 'QMTest'))
102
103 scons_py = os.path.join('bootstrap', 'src', 'script', 'scons.py')
104 if not os.path.exists(scons_py):
105     scons_py = os.path.join('src', 'script', 'scons.py')
106
107 scons_lib_dir = os.path.join(os.getcwd(), 'bootstrap', 'src', 'engine')
108 if not os.path.exists(scons_lib_dir):
109     scons_lib_dir = os.path.join(os.getcwd(), 'src', 'engine')
110
111 os.environ['SCONS_LIB_DIR'] = scons_lib_dir
112
113 import TestCmd
114
115 # The regular expression that identifies entity references in the
116 # standard sgmllib omits the underscore from the legal characters.
117 # Override it with our own regular expression that adds underscore.
118 sgmllib.entityref = re.compile('&([a-zA-Z][-_.a-zA-Z0-9]*)[^-_a-zA-Z0-9]')
119
120 # Classes for collecting different types of data we're interested in.
121 class DataCollector:
122     """Generic class for collecting data between a start tag and end
123     tag.  We subclass for various types of tags we care about."""
124     def __init__(self):
125         self.data = ""
126     def afunc(self, data):
127         self.data = self.data + data
128
129 class Example(DataCollector):
130     """An SCons example.  This is essentially a list of files that
131     will get written to a temporary directory to collect output
132     from one or more SCons runs."""
133     def __init__(self):
134         DataCollector.__init__(self)
135         self.files = []
136         self.dirs = []
137
138 class File(DataCollector):
139     """A file, that will get written out to a temporary directory
140     for one or more SCons runs."""
141     def __init__(self, name):
142         DataCollector.__init__(self)
143         self.name = name
144
145 class Directory(DataCollector):
146     """A directory, that will get created in a temporary directory
147     for one or more SCons runs."""
148     def __init__(self, name):
149         DataCollector.__init__(self)
150         self.name = name
151
152 class Output(DataCollector):
153     """Where the command output goes.  This is essentially
154     a list of commands that will get executed."""
155     def __init__(self):
156         DataCollector.__init__(self)
157         self.commandlist = []
158
159 class Command(DataCollector):
160     """A tag for where the command output goes.  This is essentially
161     a list of commands that will get executed."""
162     def __init__(self):
163         DataCollector.__init__(self)
164         self.output = None
165
166 Prompt = {
167     'posix' : '% ',
168     'win32' : 'C:\\>'
169 }
170
171 # The magick SCons hackery that makes this work.
172 #
173 # So that our examples can still use the default SConstruct file, we
174 # actually feed the following into SCons via stdin and then have it
175 # SConscript() the SConstruct file.  This stdin wrapper creates a set
176 # of ToolSurrogates for the tools for the appropriate platform.  These
177 # Surrogates print output like the real tools and behave like them
178 # without actually having to be on the right platform or have the right
179 # tool installed.
180 #
181 # The upshot:  The wrapper transparently changes the world out from
182 # under the top-level SConstruct file in an example just so we can get
183 # the command output.
184
185 Stdin = """\
186 import os
187 import re
188 import SCons.Action
189 import SCons.Defaults
190 import SCons.Node.FS
191
192 platform = '%(osname)s'
193
194 Sep = {
195     'posix' : '/',
196     'win32' : '\\\\',
197 }[platform]
198
199
200 #  Slip our own __str__() method into the EntryProxy class used to expand
201 #  $TARGET{S} and $SOURCE{S} to translate the path-name separators from
202 #  what's appropriate for the system we're running on to what's appropriate
203 #  for the example system.
204 orig = SCons.Node.FS.EntryProxy
205 class MyEntryProxy(orig):
206     def __str__(self):
207         return string.replace(str(self._Proxy__subject), os.sep, Sep)
208 SCons.Node.FS.EntryProxy = MyEntryProxy
209
210 # Slip our own RDirs() method into the Node.FS.File class so that the
211 # expansions of $_{CPPINC,F77INC,LIBDIR}FLAGS will have the path-name
212 # separators translated from what's appropriate for the system we're
213 # running on to what's appropriate for the example system.
214 orig_RDirs = SCons.Node.FS.File.RDirs
215 def my_RDirs(self, pathlist, orig_RDirs=orig_RDirs):
216     return map(lambda x: string.replace(str(x), os.sep, Sep),
217                orig_RDirs(self, pathlist))
218 SCons.Node.FS.File.RDirs = my_RDirs
219
220 class Curry:
221     def __init__(self, fun, *args, **kwargs):
222         self.fun = fun
223         self.pending = args[:]
224         self.kwargs = kwargs.copy()
225
226     def __call__(self, *args, **kwargs):
227         if kwargs and self.kwargs:
228             kw = self.kwargs.copy()
229             kw.update(kwargs)
230         else:
231             kw = kwargs or self.kwargs
232
233         return self.fun(*self.pending + args, **kw)
234
235 def Str(target, source, env, cmd=""):
236     result = []
237     for cmd in env.subst_list(cmd, target=target, source=source):
238         result.append(string.join(map(str, cmd)))
239     return string.join(result, '\\n')
240
241 class ToolSurrogate:
242     def __init__(self, tool, variable, func, varlist):
243         self.tool = tool
244         if not type(variable) is type([]):
245             variable = [variable]
246         self.variable = variable
247         self.func = func
248         self.varlist = varlist
249     def __call__(self, env):
250         t = Tool(self.tool)
251         t.generate(env)
252         for v in self.variable:
253             orig = env[v]
254             try:
255                 strfunction = orig.strfunction
256             except AttributeError:
257                 strfunction = Curry(Str, cmd=orig)
258             # Don't call Action() through its global function name, because
259             # that leads to infinite recursion in trying to initialize the
260             # Default Environment.
261             env[v] = SCons.Action.Action(self.func,
262                                          strfunction=strfunction,
263                                          varlist=self.varlist)
264     def __repr__(self):
265         # This is for the benefit of printing the 'TOOLS'
266         # variable through env.Dump().
267         return repr(self.tool)
268
269 def Null(target, source, env):
270     pass
271
272 def Cat(target, source, env):
273     target = str(target[0])
274     f = open(target, "wb")
275     for src in map(str, source):
276         f.write(open(src, "rb").read())
277     f.close()
278
279 def CCCom(target, source, env):
280     target = str(target[0])
281     fp = open(target, "wb")
282     def process(source_file, fp=fp):
283         for line in open(source_file, "rb").readlines():
284             m = re.match(r'#include\s[<"]([^<"]+)[>"]', line)
285             if m:
286                 include = m.group(1)
287                 for d in [str(env.Dir('$CPPPATH')), '.']:
288                     f = os.path.join(d, include)
289                     if os.path.exists(f):
290                         process(f)
291                         break
292             elif line[:11] != "STRIP CCCOM":
293                 fp.write(line)
294     for src in map(str, source):
295         process(src)
296         fp.write('debug = ' + ARGUMENTS.get('debug', '0') + '\\n')
297     fp.close()
298
299 public_class_re = re.compile('^public class (\S+)', re.MULTILINE)
300
301 def JavaCCom(target, source, env):
302     # This is a fake Java compiler that just looks for
303     #   public class FooBar
304     # lines in the source file(s) and spits those out
305     # to .class files named after the class.
306     tlist = list(map(str, target))
307     not_copied = {}
308     for t in tlist:
309        not_copied[t] = 1
310     for src in map(str, source):
311         contents = open(src, "rb").read()
312         classes = public_class_re.findall(contents)
313         for c in classes:
314             for t in [x for x in tlist if x.find(c) != -1]:
315                 open(t, "wb").write(contents)
316                 del not_copied[t]
317     for t in not_copied.keys():
318         open(t, "wb").write("\\n")
319
320 def JavaHCom(target, source, env):
321     tlist = map(str, target)
322     slist = map(str, source)
323     for t, s in zip(tlist, slist):
324         open(t, "wb").write(open(s, "rb").read())
325
326 def find_class_files(arg, dirname, names):
327     class_files = filter(lambda n: n[-6:] == '.class', names)
328     paths = map(lambda n: os.path.join(dirname, n), class_files)
329     arg.extend(paths)
330
331 def JarCom(target, source, env):
332     target = str(target[0])
333     class_files = []
334     for src in map(str, source):
335         os.path.walk(src, find_class_files, class_files)
336     f = open(target, "wb")
337     for cf in class_files:
338         f.write(open(cf, "rb").read())
339     f.close()
340
341 # XXX Adding COLOR, COLORS and PACKAGE to the 'cc' varlist(s) by hand
342 # here is bogus.  It's for the benefit of doc/user/command-line.in, which
343 # uses examples that want  to rebuild based on changes to these variables.
344 # It would be better to figure out a way to do it based on the content of
345 # the generated command-line, or else find a way to let the example markup
346 # language in doc/user/command-line.in tell this script what variables to
347 # add, but that's more difficult than I want to figure out how to do right
348 # now, so let's just use the simple brute force approach for the moment.
349
350 ToolList = {
351     'posix' :   [('cc', ['CCCOM', 'SHCCCOM'], CCCom, ['CCFLAGS', 'CPPDEFINES', 'COLOR', 'COLORS', 'PACKAGE']),
352                  ('link', ['LINKCOM', 'SHLINKCOM'], Cat, []),
353                  ('ar', ['ARCOM', 'RANLIBCOM'], Cat, []),
354                  ('tar', 'TARCOM', Null, []),
355                  ('zip', 'ZIPCOM', Null, []),
356                  ('BitKeeper', 'BITKEEPERCOM', Cat, []),
357                  ('CVS', 'CVSCOM', Cat, []),
358                  ('RCS', 'RCS_COCOM', Cat, []),
359                  ('SCCS', 'SCCSCOM', Cat, []),
360                  ('javac', 'JAVACCOM', JavaCCom, []),
361                  ('javah', 'JAVAHCOM', JavaHCom, []),
362                  ('jar', 'JARCOM', JarCom, []),
363                  ('rmic', 'RMICCOM', Cat, []),
364                 ],
365     'win32' :   [('msvc', ['CCCOM', 'SHCCCOM', 'RCCOM'], CCCom, ['CCFLAGS', 'CPPDEFINES', 'COLOR', 'COLORS', 'PACKAGE']),
366                  ('mslink', ['LINKCOM', 'SHLINKCOM'], Cat, []),
367                  ('mslib', 'ARCOM', Cat, []),
368                  ('tar', 'TARCOM', Null, []),
369                  ('zip', 'ZIPCOM', Null, []),
370                  ('BitKeeper', 'BITKEEPERCOM', Cat, []),
371                  ('CVS', 'CVSCOM', Cat, []),
372                  ('RCS', 'RCS_COCOM', Cat, []),
373                  ('SCCS', 'SCCSCOM', Cat, []),
374                  ('javac', 'JAVACCOM', JavaCCom, []),
375                  ('javah', 'JAVAHCOM', JavaHCom, []),
376                  ('jar', 'JARCOM', JarCom, []),
377                  ('rmic', 'RMICCOM', Cat, []),
378                 ],
379 }
380
381 toollist = ToolList[platform]
382 filter_tools = string.split('%(tools)s')
383 if filter_tools:
384     toollist = [x for x in toollist if x[0] in filter_tools]
385
386 toollist = [ToolSurrogate(*t) for t in toollist]
387
388 toollist.append('install')
389
390 def surrogate_spawn(sh, escape, cmd, args, env):
391     pass
392
393 def surrogate_pspawn(sh, escape, cmd, args, env, stdout, stderr):
394     pass
395
396 SCons.Defaults.ConstructionEnvironment.update({
397     'PLATFORM' : platform,
398     'TOOLS'    : toollist,
399     'SPAWN'    : surrogate_spawn,
400     'PSPAWN'   : surrogate_pspawn,
401 })
402
403 SConscript('SConstruct')
404 """
405
406 # "Commands" that we will execute in our examples.
407 def command_scons(args, c, test, dict):
408     save_vals = {}
409     delete_keys = []
410     try:
411         ce = c.environment
412     except AttributeError:
413         pass
414     else:
415         for arg in c.environment.split():
416             key, val = arg.split('=')
417             try:
418                 save_vals[key] = os.environ[key]
419             except KeyError:
420                 delete_keys.append(key)
421             os.environ[key] = val
422     test.run(interpreter = sys.executable,
423              program = scons_py,
424              # We use ToolSurrogates to capture win32 output by "building"
425              # examples using a fake win32 tool chain.  Suppress the
426              # warnings that come from the new revamped VS support so
427              # we can build doc on (Linux) systems that don't have
428              # Visual C installed.
429              arguments = '--warn=no-visual-c-missing -f - ' + ' '.join(args),
430              chdir = test.workpath('WORK'),
431              stdin = Stdin % dict)
432     os.environ.update(save_vals)
433     for key in delete_keys:
434         del(os.environ[key])
435     out = test.stdout()
436     out = out.replace(test.workpath('ROOT'), '')
437     out = out.replace(test.workpath('WORK/SConstruct'),
438                               '/home/my/project/SConstruct')
439     lines = out.split('\n')
440     if lines:
441         while lines[-1] == '':
442             lines = lines[:-1]
443     #err = test.stderr()
444     #if err:
445     #    sys.stderr.write(err)
446     return lines
447
448 def command_touch(args, c, test, dict):
449     if args[0] == '-t':
450         t = int(time.mktime(time.strptime(args[1], '%Y%m%d%H%M')))
451         times = (t, t)
452         args = args[2:]
453     else:
454         time.sleep(1)
455         times = None
456     for file in args:
457         if not os.path.isabs(file):
458             file = os.path.join(test.workpath('WORK'), file)
459         if not os.path.exists(file):
460             open(file, 'wb')
461         os.utime(file, times)
462     return []
463
464 def command_edit(args, c, test, dict):
465     try:
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'
471     for file in args:
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)
476     return []
477
478 def command_ls(args, c, test, dict):
479     def ls(a):
480         files = os.listdir(a)
481         files = [x for x in files if x[0] != '.']
482         files.sort()
483         return ['  '.join(files)]
484     if args:
485         l = []
486         for a in args:
487             l.extend(ls(test.workpath('WORK', a)))
488         return l
489     else:
490         return ls(test.workpath('WORK'))
491
492 def command_sleep(args, c, test, dict):
493     time.sleep(int(args[0]))
494
495 CommandDict = {
496     'scons' : command_scons,
497     'touch' : command_touch,
498     'edit'  : command_edit,
499     'ls'    : command_ls,
500     'sleep' : command_sleep,
501 }
502
503 def ExecuteCommand(args, c, t, dict):
504     try:
505         func = CommandDict[args[0]]
506     except KeyError:
507         func = lambda args, c, t, dict: []
508     return func(args[1:], c, t, dict)
509
510 class MySGML(sgmllib.SGMLParser):
511     """A subclass of the standard Python 2.2 sgmllib SGML parser.
512
513     This extends the standard sgmllib parser to recognize, and do cool
514     stuff with, the added tags that describe our SCons examples,
515     commands, and other stuff.
516
517     Note that this doesn't work with the 1.5.2 sgmllib module, because
518     that didn't have the ability to work with ENTITY declarations.
519     """
520     def __init__(self, outfp):
521         sgmllib.SGMLParser.__init__(self)
522         self.examples = {}
523         self.afunclist = []
524         self.outfp = outfp
525
526     # The first set of methods here essentially implement pass-through
527     # handling of most of the stuff in an SGML file.  We're really
528     # only concerned with the tags specific to SCons example processing,
529     # the methods for which get defined below.
530
531     def handle_data(self, data):
532         try:
533             f = self.afunclist[-1]
534         except IndexError:
535             self.outfp.write(data)
536         else:
537             f(data)
538
539     def handle_comment(self, data):
540         self.outfp.write('<!--' + data + '-->')
541
542     def handle_decl(self, data):
543         self.outfp.write('<!' + data + '>')
544
545     def unknown_starttag(self, tag, attrs):
546         try:
547             f = self.example.afunc
548         except AttributeError:
549             f = self.outfp.write
550         if not attrs:
551             f('<' + tag + '>')
552         else:
553             f('<' + tag)
554             for name, value in attrs:
555                 f(' ' + name + '=' + '"' + value + '"')
556             f('>')
557
558     def unknown_endtag(self, tag):
559         self.outfp.write('</' + tag + '>')
560
561     def unknown_entityref(self, ref):
562         self.outfp.write('&' + ref + ';')
563
564     def unknown_charref(self, ref):
565         self.outfp.write('&#' + ref + ';')
566
567     # Here is where the heavy lifting begins.  The following methods
568     # handle the begin-end tags of our SCons examples.
569
570     def start_scons_example(self, attrs):
571         t = [t for t in attrs if t[0] == 'name']
572         if t:
573             name = t[0][1]
574             try:
575                e = self.examples[name]
576             except KeyError:
577                e = self.examples[name] = Example()
578         else:
579             e = Example()
580         for name, value in attrs:
581             setattr(e, name, value)
582         self.e = e
583         self.afunclist.append(e.afunc)
584
585     def end_scons_example(self):
586         e = self.e
587         files = [f for f in e.files if f.printme]
588         if files:
589             self.outfp.write('<programlisting>')
590             for f in files:
591                 if f.printme:
592                     i = len(f.data) - 1
593                     while f.data[i] == ' ':
594                         i = i - 1
595                     output = f.data[:i+1].replace('__ROOT__', '')
596                     output = output.replace('<', '&lt;')
597                     output = output.replace('>', '&gt;')
598                     self.outfp.write(output)
599             if e.data and e.data[0] == '\n':
600                 e.data = e.data[1:]
601             self.outfp.write(e.data + '</programlisting>')
602         delattr(self, 'e')
603         self.afunclist = self.afunclist[:-1]
604
605     def start_file(self, attrs):
606         try:
607             e = self.e
608         except AttributeError:
609             self.error("<file> tag outside of <scons_example>")
610         t = [t for t in attrs if t[0] == 'name']
611         if not t:
612             self.error("no <file> name attribute found")
613         try:
614             e.prefix
615         except AttributeError:
616             e.prefix = e.data
617             e.data = ""
618         f = File(t[0][1])
619         f.printme = None
620         for name, value in attrs:
621             setattr(f, name, value)
622         e.files.append(f)
623         self.afunclist.append(f.afunc)
624
625     def end_file(self):
626         self.e.data = ""
627         self.afunclist = self.afunclist[:-1]
628
629     def start_directory(self, attrs):
630         try:
631             e = self.e
632         except AttributeError:
633             self.error("<directory> tag outside of <scons_example>")
634         t = [t for t in attrs if t[0] == 'name']
635         if not t:
636             self.error("no <directory> name attribute found")
637         try:
638             e.prefix
639         except AttributeError:
640             e.prefix = e.data
641             e.data = ""
642         d = Directory(t[0][1])
643         for name, value in attrs:
644             setattr(d, name, value)
645         e.dirs.append(d)
646         self.afunclist.append(d.afunc)
647
648     def end_directory(self):
649         self.e.data = ""
650         self.afunclist = self.afunclist[:-1]
651
652     def start_scons_example_file(self, attrs):
653         t = [t for t in attrs if t[0] == 'example']
654         if not t:
655             self.error("no <scons_example_file> example attribute found")
656         exname = t[0][1]
657         try:
658             e = self.examples[exname]
659         except KeyError:
660             self.error("unknown example name '%s'" % exname)
661         fattrs = [t for t in attrs if t[0] == 'name']
662         if not fattrs:
663             self.error("no <scons_example_file> name attribute found")
664         fname = fattrs[0][1]
665         f = [f for f in e.files if f.name == fname]
666         if not f:
667             self.error("example '%s' does not have a file named '%s'" % (exname, fname))
668         self.f = f[0]
669
670     def end_scons_example_file(self):
671         f = self.f
672         self.outfp.write('<programlisting>')
673         self.outfp.write(f.data + '</programlisting>')
674         delattr(self, 'f')
675
676     def start_scons_output(self, attrs):
677         t = [t for t in attrs if t[0] == 'example']
678         if not t:
679             self.error("no <scons_output> example attribute found")
680         exname = t[0][1]
681         try:
682             e = self.examples[exname]
683         except KeyError:
684             self.error("unknown example name '%s'" % exname)
685         # Default values for an example.
686         o = Output()
687         o.preserve = None
688         o.os = 'posix'
689         o.tools = ''
690         o.e = e
691         # Locally-set.
692         for name, value in attrs:
693             setattr(o, name, value)
694         self.o = o
695         self.afunclist.append(o.afunc)
696
697     def end_scons_output(self):
698         # The real raison d'etre for this script, this is where we
699         # actually execute SCons to fetch the output.
700         o = self.o
701         e = o.e
702         t = TestCmd.TestCmd(workdir='', combine=1)
703         if o.preserve:
704             t.preserve()
705         t.subdir('ROOT', 'WORK')
706         t.rootpath = t.workpath('ROOT').replace('\\', '\\\\')
707
708         for d in e.dirs:
709             dir = t.workpath('WORK', d.name)
710             if not os.path.exists(dir):
711                 os.makedirs(dir)
712
713         for f in e.files:
714             i = 0
715             while f.data[i] == '\n':
716                 i = i + 1
717             lines = f.data[i:].split('\n')
718             i = 0
719             while lines[0][i] == ' ':
720                 i = i + 1
721             lines = [l[i:] for l in lines]
722             path = f.name.replace('__ROOT__', t.rootpath)
723             if not os.path.isabs(path):
724                 path = t.workpath('WORK', path)
725             dir, name = os.path.split(path)
726             if dir and not os.path.exists(dir):
727                 os.makedirs(dir)
728             content = '\n'.join(lines)
729             content = content.replace('__ROOT__', t.rootpath)
730             path = t.workpath('WORK', path)
731             t.write(path, content)
732             if hasattr(f, 'chmod'):
733                 os.chmod(path, int(f.chmod, 0))
734
735         i = len(o.prefix)
736         while o.prefix[i-1] != '\n':
737             i = i - 1
738
739         self.outfp.write('<screen>' + o.prefix[:i])
740         p = o.prefix[i:]
741
742         # Regular expressions for making the doc output consistent,
743         # regardless of reported addresses or Python version.
744
745         # Massage addresses in object repr strings to a constant.
746         address_re = re.compile(r' at 0x[0-9a-fA-F]*\>')
747
748         # Massage file names in stack traces (sometimes reported as absolute
749         # paths) to a consistent relative path.
750         engine_re = re.compile(r' File ".*/src/engine/SCons/')
751
752         # Python 2.5 changed the stack trace when the module is read
753         # from standard input from read "... line 7, in ?" to
754         # "... line 7, in <module>".
755         file_re = re.compile(r'^( *File ".*", line \d+, in) \?$', re.M)
756
757         # Python 2.6 made UserList a new-style class, which changes the
758         # AttributeError message generated by our NodeList subclass.
759         nodelist_re = re.compile(r'(AttributeError:) NodeList instance (has no attribute \S+)')
760
761         for c in o.commandlist:
762             self.outfp.write(p + Prompt[o.os])
763             d = c.data.replace('__ROOT__', '')
764             self.outfp.write('<userinput>' + d + '</userinput>\n')
765
766             e = c.data.replace('__ROOT__', t.workpath('ROOT'))
767             args = e.split()
768             lines = ExecuteCommand(args, c, t, {'osname':o.os, 'tools':o.tools})
769             content = None
770             if c.output:
771                 content = c.output
772             elif lines:
773                 content = ( '\n' + p).join(lines)
774             if content:
775                 content = address_re.sub(r' at 0x700000&gt;', content)
776                 content = engine_re.sub(r' File "bootstrap/src/engine/SCons/', content)
777                 content = file_re.sub(r'\1 <module>', content)
778                 content = nodelist_re.sub(r"\1 'NodeList' object \2", content)
779                 content = content.replace('<', '&lt;')
780                 content = content.replace('>', '&gt;')
781                 self.outfp.write(p + content + '\n')
782
783         if o.data[0] == '\n':
784             o.data = o.data[1:]
785         self.outfp.write(o.data + '</screen>')
786         delattr(self, 'o')
787         self.afunclist = self.afunclist[:-1]
788
789     def start_scons_output_command(self, attrs):
790         try:
791             o = self.o
792         except AttributeError:
793             self.error("<scons_output_command> tag outside of <scons_output>")
794         try:
795             o.prefix
796         except AttributeError:
797             o.prefix = o.data
798             o.data = ""
799         c = Command()
800         for name, value in attrs:
801             setattr(c, name, value)
802         o.commandlist.append(c)
803         self.afunclist.append(c.afunc)
804
805     def end_scons_output_command(self):
806         self.o.data = ""
807         self.afunclist = self.afunclist[:-1]
808
809     def start_sconstruct(self, attrs):
810         f = File('')
811         self.f = f
812         self.afunclist.append(f.afunc)
813
814     def end_sconstruct(self):
815         f = self.f
816         self.outfp.write('<programlisting>')
817         output = f.data.replace('__ROOT__', '')
818         self.outfp.write(output + '</programlisting>')
819         delattr(self, 'f')
820         self.afunclist = self.afunclist[:-1]
821
822 def process(filename):
823     if filename == '-':
824         f = sys.stdin
825     else:
826         try:
827             f = open(filename, 'r')
828         except EnvironmentError, e:
829             sys.stderr.write('%s: %s\n' % (filename, msg))
830             return 1
831
832     data = f.read()
833     if f is not sys.stdin:
834         f.close()
835
836     if data.startswith('<?xml '):
837         first_line, data = data.split('\n', 1)
838         sys.stdout.write(first_line + '\n')
839
840     x = MySGML(sys.stdout)
841     for c in data:
842         x.feed(c)
843     x.close()
844
845     return 0
846
847 def main(argv=None):
848     if argv is None:
849         argv = sys.argv
850
851     parser = optparse.OptionParser()
852     opts, args = parser.parse_args(argv[1:])
853
854     if not args:
855         args = ['-']
856
857     for arg in args:
858         process(arg)
859
860 if __name__ == "__main__":
861     sys.exit(main())
862
863 # Local Variables:
864 # tab-width:4
865 # indent-tabs-mode:nil
866 # End:
867 # vim: set expandtab tabstop=4 shiftwidth=4: