3 # Copyright (c) 2010 The SCons Foundation
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:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
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.
25 # scons-doc.py - an SGML preprocessor for capturing SCons output
26 # and inserting it into examples in our DocBook
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.
36 # An "SCons example" looks like this, and essentially describes a set of
37 # input files (program source files as well as SConscript files):
39 # <scons_example name="ex1">
40 # <file name="SConstruct" printme="1">
45 # int main() { printf("foo.c\n"); }
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.
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.
61 # If you just want to show a file's contents without worry about running
62 # SCons, there's a shorter <sconstruct> tag:
69 # This is essentially equivalent to <scons_example><file printme="1">,
70 # but it's more straightforward.
72 # SCons output is generated from the following sort of tag:
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>
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".
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.
100 sys.path.append(os.path.join(os.getcwd(), 'QMTest'))
101 sys.path.append(os.path.join(os.getcwd(), 'build', 'QMTest'))
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')
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')
111 os.environ['SCONS_LIB_DIR'] = scons_lib_dir
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]')
120 # Classes for collecting different types of data we're interested in.
122 """Generic class for collecting data between a start tag and end
123 tag. We subclass for various types of tags we care about."""
126 def afunc(self, data):
127 self.data = self.data + data
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."""
134 DataCollector.__init__(self)
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)
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)
152 class Output(DataCollector):
153 """Where the command output goes. This is essentially
154 a list of commands that will get executed."""
156 DataCollector.__init__(self)
157 self.commandlist = []
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."""
163 DataCollector.__init__(self)
171 # The magick SCons hackery that makes this work.
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
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.
190 import SCons.Defaults
193 platform = '%(osname)s'
201 # Slip our own __str__() method into the EntryProxy class used to expand
202 # $TARGET{S} and $SOURCE{S} to translate the path-name separators from
203 # what's appropriate for the system we're running on to what's appropriate
204 # for the example system.
205 orig = SCons.Node.FS.EntryProxy
206 class MyEntryProxy(orig):
208 return string.replace(str(self._Proxy__subject), os.sep, Sep)
209 SCons.Node.FS.EntryProxy = MyEntryProxy
211 # Slip our own RDirs() method into the Node.FS.File class so that the
212 # expansions of $_{CPPINC,F77INC,LIBDIR}FLAGS will have the path-name
213 # separators translated from what's appropriate for the system we're
214 # running on to what's appropriate for the example system.
215 orig_RDirs = SCons.Node.FS.File.RDirs
216 def my_RDirs(self, pathlist, orig_RDirs=orig_RDirs):
217 return map(lambda x: string.replace(str(x), os.sep, Sep),
218 orig_RDirs(self, pathlist))
219 SCons.Node.FS.File.RDirs = my_RDirs
222 def __init__(self, fun, *args, **kwargs):
224 self.pending = args[:]
225 self.kwargs = kwargs.copy()
227 def __call__(self, *args, **kwargs):
228 if kwargs and self.kwargs:
229 kw = self.kwargs.copy()
232 kw = kwargs or self.kwargs
234 return apply(self.fun, self.pending + args, kw)
236 def Str(target, source, env, cmd=""):
238 for cmd in env.subst_list(cmd, target=target, source=source):
239 result.append(string.join(map(str, cmd)))
240 return string.join(result, '\\n')
243 def __init__(self, tool, variable, func, varlist):
245 if not type(variable) is type([]):
246 variable = [variable]
247 self.variable = variable
249 self.varlist = varlist
250 def __call__(self, env):
253 for v in self.variable:
256 strfunction = orig.strfunction
257 except AttributeError:
258 strfunction = Curry(Str, cmd=orig)
259 # Don't call Action() through its global function name, because
260 # that leads to infinite recursion in trying to initialize the
261 # Default Environment.
262 env[v] = SCons.Action.Action(self.func,
263 strfunction=strfunction,
264 varlist=self.varlist)
266 # This is for the benefit of printing the 'TOOLS'
267 # variable through env.Dump().
268 return repr(self.tool)
270 def Null(target, source, env):
273 def Cat(target, source, env):
274 target = str(target[0])
275 f = open(target, "wb")
276 for src in map(str, source):
277 f.write(open(src, "rb").read())
280 def CCCom(target, source, env):
281 target = str(target[0])
282 fp = open(target, "wb")
283 def process(source_file, fp=fp):
284 for line in open(source_file, "rb").readlines():
285 m = re.match(r'#include\s[<"]([^<"]+)[>"]', line)
288 for d in [str(env.Dir('$CPPPATH')), '.']:
289 f = os.path.join(d, include)
290 if os.path.exists(f):
293 elif line[:11] != "STRIP CCCOM":
295 for src in map(str, source):
297 fp.write('debug = ' + ARGUMENTS.get('debug', '0') + '\\n')
300 public_class_re = re.compile('^public class (\S+)', re.MULTILINE)
302 def JavaCCom(target, source, env):
303 # This is a fake Java compiler that just looks for
304 # public class FooBar
305 # lines in the source file(s) and spits those out
306 # to .class files named after the class.
307 tlist = map(str, target)
311 for src in map(str, source):
312 contents = open(src, "rb").read()
313 classes = public_class_re.findall(contents)
315 for t in filter(lambda x: string.find(x, c) != -1, tlist):
316 open(t, "wb").write(contents)
318 for t in not_copied.keys():
319 open(t, "wb").write("\\n")
321 def JavaHCom(target, source, env):
322 tlist = map(str, target)
323 slist = map(str, source)
324 for t, s in zip(tlist, slist):
325 open(t, "wb").write(open(s, "rb").read())
327 def find_class_files(arg, dirname, names):
328 class_files = filter(lambda n: n[-6:] == '.class', names)
329 paths = map(lambda n, d=dirname: os.path.join(d, n), class_files)
332 def JarCom(target, source, env):
333 target = str(target[0])
335 for src in map(str, source):
336 os.path.walk(src, find_class_files, class_files)
337 f = open(target, "wb")
338 for cf in class_files:
339 f.write(open(cf, "rb").read())
342 # XXX Adding COLOR, COLORS and PACKAGE to the 'cc' varlist(s) by hand
343 # here is bogus. It's for the benefit of doc/user/command-line.in, which
344 # uses examples that want to rebuild based on changes to these variables.
345 # It would be better to figure out a way to do it based on the content of
346 # the generated command-line, or else find a way to let the example markup
347 # language in doc/user/command-line.in tell this script what variables to
348 # add, but that's more difficult than I want to figure out how to do right
349 # now, so let's just use the simple brute force approach for the moment.
352 'posix' : [('cc', ['CCCOM', 'SHCCCOM'], CCCom, ['CCFLAGS', 'CPPDEFINES', 'COLOR', 'COLORS', 'PACKAGE']),
353 ('link', ['LINKCOM', 'SHLINKCOM'], Cat, []),
354 ('ar', ['ARCOM', 'RANLIBCOM'], Cat, []),
355 ('tar', 'TARCOM', Null, []),
356 ('zip', 'ZIPCOM', Null, []),
357 ('BitKeeper', 'BITKEEPERCOM', Cat, []),
358 ('CVS', 'CVSCOM', Cat, []),
359 ('RCS', 'RCS_COCOM', Cat, []),
360 ('SCCS', 'SCCSCOM', Cat, []),
361 ('javac', 'JAVACCOM', JavaCCom, []),
362 ('javah', 'JAVAHCOM', JavaHCom, []),
363 ('jar', 'JARCOM', JarCom, []),
364 ('rmic', 'RMICCOM', Cat, []),
366 'win32' : [('msvc', ['CCCOM', 'SHCCCOM', 'RCCOM'], CCCom, ['CCFLAGS', 'CPPDEFINES', 'COLOR', 'COLORS', 'PACKAGE']),
367 ('mslink', ['LINKCOM', 'SHLINKCOM'], Cat, []),
368 ('mslib', 'ARCOM', Cat, []),
369 ('tar', 'TARCOM', Null, []),
370 ('zip', 'ZIPCOM', Null, []),
371 ('BitKeeper', 'BITKEEPERCOM', Cat, []),
372 ('CVS', 'CVSCOM', Cat, []),
373 ('RCS', 'RCS_COCOM', Cat, []),
374 ('SCCS', 'SCCSCOM', Cat, []),
375 ('javac', 'JAVACCOM', JavaCCom, []),
376 ('javah', 'JAVAHCOM', JavaHCom, []),
377 ('jar', 'JARCOM', JarCom, []),
378 ('rmic', 'RMICCOM', Cat, []),
382 toollist = ToolList[platform]
383 filter_tools = string.split('%(tools)s')
385 toollist = filter(lambda x, ft=filter_tools: x[0] in ft, toollist)
387 toollist = map(lambda t: apply(ToolSurrogate, t), toollist)
389 toollist.append('install')
391 def surrogate_spawn(sh, escape, cmd, args, env):
394 def surrogate_pspawn(sh, escape, cmd, args, env, stdout, stderr):
397 SCons.Defaults.ConstructionEnvironment.update({
398 'PLATFORM' : platform,
400 'SPAWN' : surrogate_spawn,
401 'PSPAWN' : surrogate_pspawn,
404 SConscript('SConstruct')
407 # "Commands" that we will execute in our examples.
408 def command_scons(args, c, test, dict):
413 except AttributeError:
416 for arg in string.split(c.environment):
417 key, val = string.split(arg, '=')
419 save_vals[key] = os.environ[key]
421 delete_keys.append(key)
422 os.environ[key] = val
423 test.run(interpreter = sys.executable,
425 # We use ToolSurrogates to capture win32 output by "building"
426 # examples using a fake win32 tool chain. Suppress the
427 # warnings that come from the new revamped VS support so
428 # we can build doc on (Linux) systems that don't have
429 # Visual C installed.
430 arguments = '--warn=no-visual-c-missing -f - ' + string.join(args),
431 chdir = test.workpath('WORK'),
432 stdin = Stdin % dict)
433 os.environ.update(save_vals)
434 for key in delete_keys:
437 out = string.replace(out, test.workpath('ROOT'), '')
438 out = string.replace(out, test.workpath('WORK/SConstruct'),
439 '/home/my/project/SConstruct')
440 lines = string.split(out, '\n')
442 while lines[-1] == '':
446 # sys.stderr.write(err)
449 def command_touch(args, c, test, dict):
451 t = int(time.mktime(time.strptime(args[1], '%Y%m%d%H%M')))
458 if not os.path.isabs(file):
459 file = os.path.join(test.workpath('WORK'), file)
460 if not os.path.exists(file):
462 os.utime(file, times)
465 def command_edit(args, c, test, dict):
467 add_string = c.edit[:]
468 except AttributeError:
469 add_string = 'void edit(void) { ; }\n'
470 if add_string[-1] != '\n':
471 add_string = add_string + '\n'
473 if not os.path.isabs(file):
474 file = os.path.join(test.workpath('WORK'), file)
475 contents = open(file, 'rb').read()
476 open(file, 'wb').write(contents + add_string)
479 def command_ls(args, c, test, dict):
481 files = os.listdir(a)
482 files = filter(lambda x: x[0] != '.', files)
484 return [string.join(files, ' ')]
488 l.extend(ls(test.workpath('WORK', a)))
491 return ls(test.workpath('WORK'))
493 def command_sleep(args, c, test, dict):
494 time.sleep(int(args[0]))
497 'scons' : command_scons,
498 'touch' : command_touch,
499 'edit' : command_edit,
501 'sleep' : command_sleep,
504 def ExecuteCommand(args, c, t, dict):
506 func = CommandDict[args[0]]
508 func = lambda args, c, t, dict: []
509 return func(args[1:], c, t, dict)
511 class MySGML(sgmllib.SGMLParser):
512 """A subclass of the standard Python 2.2 sgmllib SGML parser.
514 This extends the standard sgmllib parser to recognize, and do cool
515 stuff with, the added tags that describe our SCons examples,
516 commands, and other stuff.
518 Note that this doesn't work with the 1.5.2 sgmllib module, because
519 that didn't have the ability to work with ENTITY declarations.
521 def __init__(self, outfp):
522 sgmllib.SGMLParser.__init__(self)
527 # The first set of methods here essentially implement pass-through
528 # handling of most of the stuff in an SGML file. We're really
529 # only concerned with the tags specific to SCons example processing,
530 # the methods for which get defined below.
532 def handle_data(self, data):
534 f = self.afunclist[-1]
536 self.outfp.write(data)
540 def handle_comment(self, data):
541 self.outfp.write('<!--' + data + '-->')
543 def handle_decl(self, data):
544 self.outfp.write('<!' + data + '>')
546 def unknown_starttag(self, tag, attrs):
548 f = self.example.afunc
549 except AttributeError:
555 for name, value in attrs:
556 f(' ' + name + '=' + '"' + value + '"')
559 def unknown_endtag(self, tag):
560 self.outfp.write('</' + tag + '>')
562 def unknown_entityref(self, ref):
563 self.outfp.write('&' + ref + ';')
565 def unknown_charref(self, ref):
566 self.outfp.write('&#' + ref + ';')
568 # Here is where the heavy lifting begins. The following methods
569 # handle the begin-end tags of our SCons examples.
571 def start_scons_example(self, attrs):
572 t = filter(lambda t: t[0] == 'name', attrs)
576 e = self.examples[name]
578 e = self.examples[name] = Example()
581 for name, value in attrs:
582 setattr(e, name, value)
584 self.afunclist.append(e.afunc)
586 def end_scons_example(self):
588 files = filter(lambda f: f.printme, e.files)
590 self.outfp.write('<programlisting>')
594 while f.data[i] == ' ':
596 output = string.replace(f.data[:i+1], '__ROOT__', '')
597 output = string.replace(output, '<', '<')
598 output = string.replace(output, '>', '>')
599 self.outfp.write(output)
600 if e.data and e.data[0] == '\n':
602 self.outfp.write(e.data + '</programlisting>')
604 self.afunclist = self.afunclist[:-1]
606 def start_file(self, attrs):
609 except AttributeError:
610 self.error("<file> tag outside of <scons_example>")
611 t = filter(lambda t: t[0] == 'name', attrs)
613 self.error("no <file> name attribute found")
616 except AttributeError:
621 for name, value in attrs:
622 setattr(f, name, value)
624 self.afunclist.append(f.afunc)
628 self.afunclist = self.afunclist[:-1]
630 def start_directory(self, attrs):
633 except AttributeError:
634 self.error("<directory> tag outside of <scons_example>")
635 t = filter(lambda t: t[0] == 'name', attrs)
637 self.error("no <directory> name attribute found")
640 except AttributeError:
643 d = Directory(t[0][1])
644 for name, value in attrs:
645 setattr(d, name, value)
647 self.afunclist.append(d.afunc)
649 def end_directory(self):
651 self.afunclist = self.afunclist[:-1]
653 def start_scons_example_file(self, attrs):
654 t = filter(lambda t: t[0] == 'example', attrs)
656 self.error("no <scons_example_file> example attribute found")
659 e = self.examples[exname]
661 self.error("unknown example name '%s'" % exname)
662 fattrs = filter(lambda t: t[0] == 'name', attrs)
664 self.error("no <scons_example_file> name attribute found")
666 f = filter(lambda f, fname=fname: f.name == fname, e.files)
668 self.error("example '%s' does not have a file named '%s'" % (exname, fname))
671 def end_scons_example_file(self):
673 self.outfp.write('<programlisting>')
674 self.outfp.write(f.data + '</programlisting>')
677 def start_scons_output(self, attrs):
678 t = filter(lambda t: t[0] == 'example', attrs)
680 self.error("no <scons_output> example attribute found")
683 e = self.examples[exname]
685 self.error("unknown example name '%s'" % exname)
686 # Default values for an example.
693 for name, value in attrs:
694 setattr(o, name, value)
696 self.afunclist.append(o.afunc)
698 def end_scons_output(self):
699 # The real raison d'etre for this script, this is where we
700 # actually execute SCons to fetch the output.
703 t = TestCmd.TestCmd(workdir='', combine=1)
706 t.subdir('ROOT', 'WORK')
707 t.rootpath = string.replace(t.workpath('ROOT'), '\\', '\\\\')
710 dir = t.workpath('WORK', d.name)
711 if not os.path.exists(dir):
716 while f.data[i] == '\n':
718 lines = string.split(f.data[i:], '\n')
720 while lines[0][i] == ' ':
722 lines = map(lambda l, i=i: l[i:], lines)
723 path = string.replace(f.name, '__ROOT__', t.rootpath)
724 if not os.path.isabs(path):
725 path = t.workpath('WORK', path)
726 dir, name = os.path.split(path)
727 if dir and not os.path.exists(dir):
729 content = string.join(lines, '\n')
730 content = string.replace(content, '__ROOT__', t.rootpath)
731 path = t.workpath('WORK', path)
732 t.write(path, content)
733 if hasattr(f, 'chmod'):
734 os.chmod(path, int(f.chmod, 0))
737 while o.prefix[i-1] != '\n':
740 self.outfp.write('<screen>' + o.prefix[:i])
743 # Regular expressions for making the doc output consistent,
744 # regardless of reported addresses or Python version.
746 # Massage addresses in object repr strings to a constant.
747 address_re = re.compile(r' at 0x[0-9a-fA-F]*\>')
749 # Massage file names in stack traces (sometimes reported as absolute
750 # paths) to a consistent relative path.
751 engine_re = re.compile(r' File ".*/src/engine/SCons/')
753 # Python 2.5 changed the stack trace when the module is read
754 # from standard input from read "... line 7, in ?" to
755 # "... line 7, in <module>".
756 file_re = re.compile(r'^( *File ".*", line \d+, in) \?$', re.M)
758 # Python 2.6 made UserList a new-style class, which changes the
759 # AttributeError message generated by our NodeList subclass.
760 nodelist_re = re.compile(r'(AttributeError:) NodeList instance (has no attribute \S+)')
762 for c in o.commandlist:
763 self.outfp.write(p + Prompt[o.os])
764 d = string.replace(c.data, '__ROOT__', '')
765 self.outfp.write('<userinput>' + d + '</userinput>\n')
767 e = string.replace(c.data, '__ROOT__', t.workpath('ROOT'))
768 args = string.split(e)
769 lines = ExecuteCommand(args, c, t, {'osname':o.os, 'tools':o.tools})
774 content = string.join(lines, '\n' + p)
776 content = address_re.sub(r' at 0x700000>', content)
777 content = engine_re.sub(r' File "bootstrap/src/engine/SCons/', content)
778 content = file_re.sub(r'\1 <module>', content)
779 content = nodelist_re.sub(r"\1 'NodeList' object \2", content)
780 content = string.replace(content, '<', '<')
781 content = string.replace(content, '>', '>')
782 self.outfp.write(p + content + '\n')
784 if o.data[0] == '\n':
786 self.outfp.write(o.data + '</screen>')
788 self.afunclist = self.afunclist[:-1]
790 def start_scons_output_command(self, attrs):
793 except AttributeError:
794 self.error("<scons_output_command> tag outside of <scons_output>")
797 except AttributeError:
801 for name, value in attrs:
802 setattr(c, name, value)
803 o.commandlist.append(c)
804 self.afunclist.append(c.afunc)
806 def end_scons_output_command(self):
808 self.afunclist = self.afunclist[:-1]
810 def start_sconstruct(self, attrs):
813 self.afunclist.append(f.afunc)
815 def end_sconstruct(self):
817 self.outfp.write('<programlisting>')
818 output = string.replace(f.data, '__ROOT__', '')
819 self.outfp.write(output + '</programlisting>')
821 self.afunclist = self.afunclist[:-1]
823 def process(filename):
828 f = open(filename, 'r')
829 except EnvironmentError, e:
830 sys.stderr.write('%s: %s\n' % (filename, msg))
834 if f is not sys.stdin:
837 if data.startswith('<?xml '):
838 first_line, data = data.split('\n', 1)
839 sys.stdout.write(first_line + '\n')
841 x = MySGML(sys.stdout)
852 parser = optparse.OptionParser()
853 opts, args = parser.parse_args(argv[1:])
861 if __name__ == "__main__":
866 # indent-tabs-mode:nil
868 # vim: set expandtab tabstop=4 shiftwidth=4: