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.
91 from __future__ import generators ### KEEP FOR COMPATIBILITY FIXERS
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.
189 import SCons.Defaults
192 platform = '%(osname)s'
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):
207 return str(self._Proxy__subject).replace(os.sep, Sep)
208 SCons.Node.FS.EntryProxy = MyEntryProxy
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 [str(x).replace(os.sep, Sep) for x in orig_RDirs(self, pathlist)]
217 SCons.Node.FS.File.RDirs = my_RDirs
220 def __init__(self, fun, *args, **kwargs):
222 self.pending = args[:]
223 self.kwargs = kwargs.copy()
225 def __call__(self, *args, **kwargs):
226 if kwargs and self.kwargs:
227 kw = self.kwargs.copy()
230 kw = kwargs or self.kwargs
232 return self.fun(*self.pending + args, **kw)
234 def Str(target, source, env, cmd=""):
236 for cmd in env.subst_list(cmd, target=target, source=source):
237 result.append(' '.join(map(str, cmd)))
238 return '\\n'.join(result)
241 def __init__(self, tool, variable, func, varlist):
243 if not isinstance(variable, list):
244 variable = [variable]
245 self.variable = variable
247 self.varlist = varlist
248 def __call__(self, env):
251 for v in self.variable:
254 strfunction = orig.strfunction
255 except AttributeError:
256 strfunction = Curry(Str, cmd=orig)
257 # Don't call Action() through its global function name, because
258 # that leads to infinite recursion in trying to initialize the
259 # Default Environment.
260 env[v] = SCons.Action.Action(self.func,
261 strfunction=strfunction,
262 varlist=self.varlist)
264 # This is for the benefit of printing the 'TOOLS'
265 # variable through env.Dump().
266 return repr(self.tool)
268 def Null(target, source, env):
271 def Cat(target, source, env):
272 target = str(target[0])
273 f = open(target, "wb")
274 for src in map(str, source):
275 f.write(open(src, "rb").read())
278 def CCCom(target, source, env):
279 target = str(target[0])
280 fp = open(target, "wb")
281 def process(source_file, fp=fp):
282 for line in open(source_file, "rb").readlines():
283 m = re.match(r'#include\s[<"]([^<"]+)[>"]', line)
286 for d in [str(env.Dir('$CPPPATH')), '.']:
287 f = os.path.join(d, include)
288 if os.path.exists(f):
291 elif line[:11] != "STRIP CCCOM":
293 for src in map(str, source):
295 fp.write('debug = ' + ARGUMENTS.get('debug', '0') + '\\n')
298 public_class_re = re.compile('^public class (\S+)', re.MULTILINE)
300 def JavaCCom(target, source, env):
301 # This is a fake Java compiler that just looks for
302 # public class FooBar
303 # lines in the source file(s) and spits those out
304 # to .class files named after the class.
305 tlist = list(map(str, target))
309 for src in map(str, source):
310 contents = open(src, "rb").read()
311 classes = public_class_re.findall(contents)
313 for t in [x for x in tlist if x.find(c) != -1]:
314 open(t, "wb").write(contents)
316 for t in not_copied.keys():
317 open(t, "wb").write("\\n")
319 def JavaHCom(target, source, env):
320 tlist = map(str, target)
321 slist = map(str, source)
322 for t, s in zip(tlist, slist):
323 open(t, "wb").write(open(s, "rb").read())
325 def JarCom(target, source, env):
326 target = str(target[0])
328 for src in map(str, source):
329 for dirpath, dirnames, filenames in os.walk(src):
330 class_files.extend([ os.path.join(dirpath, f)
331 for f in filenames if f.endswith('.class') ])
332 f = open(target, "wb")
333 for cf in class_files:
334 f.write(open(cf, "rb").read())
337 # XXX Adding COLOR, COLORS and PACKAGE to the 'cc' varlist(s) by hand
338 # here is bogus. It's for the benefit of doc/user/command-line.in, which
339 # uses examples that want to rebuild based on changes to these variables.
340 # It would be better to figure out a way to do it based on the content of
341 # the generated command-line, or else find a way to let the example markup
342 # language in doc/user/command-line.in tell this script what variables to
343 # add, but that's more difficult than I want to figure out how to do right
344 # now, so let's just use the simple brute force approach for the moment.
347 'posix' : [('cc', ['CCCOM', 'SHCCCOM'], CCCom, ['CCFLAGS', 'CPPDEFINES', 'COLOR', 'COLORS', 'PACKAGE']),
348 ('link', ['LINKCOM', 'SHLINKCOM'], Cat, []),
349 ('ar', ['ARCOM', 'RANLIBCOM'], Cat, []),
350 ('tar', 'TARCOM', Null, []),
351 ('zip', 'ZIPCOM', Null, []),
352 ('BitKeeper', 'BITKEEPERCOM', Cat, []),
353 ('CVS', 'CVSCOM', Cat, []),
354 ('RCS', 'RCS_COCOM', Cat, []),
355 ('SCCS', 'SCCSCOM', Cat, []),
356 ('javac', 'JAVACCOM', JavaCCom, []),
357 ('javah', 'JAVAHCOM', JavaHCom, []),
358 ('jar', 'JARCOM', JarCom, []),
359 ('rmic', 'RMICCOM', Cat, []),
361 'win32' : [('msvc', ['CCCOM', 'SHCCCOM', 'RCCOM'], CCCom, ['CCFLAGS', 'CPPDEFINES', 'COLOR', 'COLORS', 'PACKAGE']),
362 ('mslink', ['LINKCOM', 'SHLINKCOM'], Cat, []),
363 ('mslib', 'ARCOM', Cat, []),
364 ('tar', 'TARCOM', Null, []),
365 ('zip', 'ZIPCOM', Null, []),
366 ('BitKeeper', 'BITKEEPERCOM', Cat, []),
367 ('CVS', 'CVSCOM', Cat, []),
368 ('RCS', 'RCS_COCOM', Cat, []),
369 ('SCCS', 'SCCSCOM', Cat, []),
370 ('javac', 'JAVACCOM', JavaCCom, []),
371 ('javah', 'JAVAHCOM', JavaHCom, []),
372 ('jar', 'JARCOM', JarCom, []),
373 ('rmic', 'RMICCOM', Cat, []),
377 toollist = ToolList[platform]
378 filter_tools = '%(tools)s'.split()
380 toollist = [x for x in toollist if x[0] in filter_tools]
382 toollist = [ToolSurrogate(*t) for t in toollist]
384 toollist.append('install')
386 def surrogate_spawn(sh, escape, cmd, args, env):
389 def surrogate_pspawn(sh, escape, cmd, args, env, stdout, stderr):
392 SCons.Defaults.ConstructionEnvironment.update({
393 'PLATFORM' : platform,
395 'SPAWN' : surrogate_spawn,
396 'PSPAWN' : surrogate_pspawn,
399 SConscript('SConstruct')
402 # "Commands" that we will execute in our examples.
403 def command_scons(args, c, test, dict):
408 except AttributeError:
411 for arg in c.environment.split():
412 key, val = arg.split('=')
414 save_vals[key] = os.environ[key]
416 delete_keys.append(key)
417 os.environ[key] = val
418 test.run(interpreter = sys.executable,
420 # We use ToolSurrogates to capture win32 output by "building"
421 # examples using a fake win32 tool chain. Suppress the
422 # warnings that come from the new revamped VS support so
423 # we can build doc on (Linux) systems that don't have
424 # Visual C installed.
425 arguments = '--warn=no-visual-c-missing -f - ' + ' '.join(args),
426 chdir = test.workpath('WORK'),
427 stdin = Stdin % dict)
428 os.environ.update(save_vals)
429 for key in delete_keys:
432 out = out.replace(test.workpath('ROOT'), '')
433 out = out.replace(test.workpath('WORK/SConstruct'),
434 '/home/my/project/SConstruct')
435 lines = out.split('\n')
437 while lines[-1] == '':
441 # sys.stderr.write(err)
444 def command_touch(args, c, test, dict):
446 t = int(time.mktime(time.strptime(args[1], '%Y%m%d%H%M')))
453 if not os.path.isabs(file):
454 file = os.path.join(test.workpath('WORK'), file)
455 if not os.path.exists(file):
457 os.utime(file, times)
460 def command_edit(args, c, test, dict):
462 add_string = c.edit[:]
463 except AttributeError:
464 add_string = 'void edit(void) { ; }\n'
465 if add_string[-1] != '\n':
466 add_string = add_string + '\n'
468 if not os.path.isabs(file):
469 file = os.path.join(test.workpath('WORK'), file)
470 contents = open(file, 'rb').read()
471 open(file, 'wb').write(contents + add_string)
474 def command_ls(args, c, test, dict):
476 return [' '.join(sorted([x for x in os.listdir(a) if x[0] != '.']))]
480 l.extend(ls(test.workpath('WORK', a)))
483 return ls(test.workpath('WORK'))
485 def command_sleep(args, c, test, dict):
486 time.sleep(int(args[0]))
489 'scons' : command_scons,
490 'touch' : command_touch,
491 'edit' : command_edit,
493 'sleep' : command_sleep,
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.
513 def __init__(self, outfp):
514 sgmllib.SGMLParser.__init__(self)
519 # The first set of methods here essentially implement pass-through
520 # handling of most of the stuff in an SGML file. We're really
521 # only concerned with the tags specific to SCons example processing,
522 # the methods for which get defined below.
524 def handle_data(self, data):
526 f = self.afunclist[-1]
528 self.outfp.write(data)
532 def handle_comment(self, data):
533 self.outfp.write('<!--' + data + '-->')
535 def handle_decl(self, data):
536 self.outfp.write('<!' + data + '>')
538 def unknown_starttag(self, tag, attrs):
540 f = self.example.afunc
541 except AttributeError:
547 for name, value in attrs:
548 f(' ' + name + '=' + '"' + value + '"')
551 def unknown_endtag(self, tag):
552 self.outfp.write('</' + tag + '>')
554 def unknown_entityref(self, ref):
555 self.outfp.write('&' + ref + ';')
557 def unknown_charref(self, ref):
558 self.outfp.write('&#' + ref + ';')
560 # Here is where the heavy lifting begins. The following methods
561 # handle the begin-end tags of our SCons examples.
563 def for_display(self, contents):
564 contents = contents.replace('__ROOT__', '')
565 contents = contents.replace('<', '<')
566 contents = contents.replace('>', '>')
569 def start_scons_example(self, attrs):
570 t = [t for t in attrs if t[0] == 'name']
574 e = self.examples[name]
576 e = self.examples[name] = Example()
579 for name, value in attrs:
580 setattr(e, name, value)
582 self.afunclist.append(e.afunc)
584 def end_scons_example(self):
586 files = [f for f in e.files if f.printme]
588 self.outfp.write('<programlisting>')
592 while f.data[i] == ' ':
594 output = self.for_display(f.data[:i+1])
595 self.outfp.write(output)
596 if e.data and e.data[0] == '\n':
598 self.outfp.write(e.data + '</programlisting>')
600 self.afunclist = self.afunclist[:-1]
602 def start_file(self, attrs):
605 except AttributeError:
606 self.error("<file> tag outside of <scons_example>")
607 t = [t for t in attrs if t[0] == 'name']
609 self.error("no <file> name attribute found")
612 except AttributeError:
617 for name, value in attrs:
618 setattr(f, name, value)
620 self.afunclist.append(f.afunc)
624 self.afunclist = self.afunclist[:-1]
626 def start_directory(self, attrs):
629 except AttributeError:
630 self.error("<directory> tag outside of <scons_example>")
631 t = [t for t in attrs if t[0] == 'name']
633 self.error("no <directory> name attribute found")
636 except AttributeError:
639 d = Directory(t[0][1])
640 for name, value in attrs:
641 setattr(d, name, value)
643 self.afunclist.append(d.afunc)
645 def end_directory(self):
647 self.afunclist = self.afunclist[:-1]
649 def start_scons_example_file(self, attrs):
650 t = [t for t in attrs if t[0] == 'example']
652 self.error("no <scons_example_file> example attribute found")
655 e = self.examples[exname]
657 self.error("unknown example name '%s'" % exname)
658 fattrs = [t for t in attrs if t[0] == 'name']
660 self.error("no <scons_example_file> name attribute found")
662 f = [f for f in e.files if f.name == fname]
664 self.error("example '%s' does not have a file named '%s'" % (exname, fname))
667 def end_scons_example_file(self):
669 self.outfp.write('<programlisting>')
670 self.outfp.write(f.data + '</programlisting>')
673 def start_scons_output(self, attrs):
674 t = [t for t in attrs if t[0] == 'example']
676 self.error("no <scons_output> example attribute found")
679 e = self.examples[exname]
681 self.error("unknown example name '%s'" % exname)
682 # Default values for an example.
689 for name, value in attrs:
690 setattr(o, name, value)
692 self.afunclist.append(o.afunc)
694 def end_scons_output(self):
695 # The real raison d'etre for this script, this is where we
696 # actually execute SCons to fetch the output.
699 t = TestCmd.TestCmd(workdir='', combine=1)
702 t.subdir('ROOT', 'WORK')
703 t.rootpath = t.workpath('ROOT').replace('\\', '\\\\')
706 dir = t.workpath('WORK', d.name)
707 if not os.path.exists(dir):
712 while f.data[i] == '\n':
714 lines = f.data[i:].split('\n')
716 while lines[0][i] == ' ':
718 lines = [l[i:] for l in lines]
719 path = f.name.replace('__ROOT__', t.rootpath)
720 if not os.path.isabs(path):
721 path = t.workpath('WORK', path)
722 dir, name = os.path.split(path)
723 if dir and not os.path.exists(dir):
725 content = '\n'.join(lines)
726 content = content.replace('__ROOT__', t.rootpath)
727 path = t.workpath('WORK', path)
728 t.write(path, content)
729 if hasattr(f, 'chmod'):
730 os.chmod(path, int(f.chmod, 0))
733 while o.prefix[i-1] != '\n':
736 self.outfp.write('<screen>' + o.prefix[:i])
739 # Regular expressions for making the doc output consistent,
740 # regardless of reported addresses or Python version.
742 # Massage addresses in object repr strings to a constant.
743 address_re = re.compile(r' at 0x[0-9a-fA-F]*\>')
745 # Massage file names in stack traces (sometimes reported as absolute
746 # paths) to a consistent relative path.
747 engine_re = re.compile(r' File ".*/src/engine/SCons/')
749 # Python 2.5 changed the stack trace when the module is read
750 # from standard input from read "... line 7, in ?" to
751 # "... line 7, in <module>".
752 file_re = re.compile(r'^( *File ".*", line \d+, in) \?$', re.M)
754 # Python 2.6 made UserList a new-style class, which changes the
755 # AttributeError message generated by our NodeList subclass.
756 nodelist_re = re.compile(r'(AttributeError:) NodeList instance (has no attribute \S+)')
758 for c in o.commandlist:
759 self.outfp.write(p + Prompt[o.os])
760 d = c.data.replace('__ROOT__', '')
761 self.outfp.write('<userinput>' + d + '</userinput>\n')
763 e = c.data.replace('__ROOT__', t.workpath('ROOT'))
765 lines = ExecuteCommand(args, c, t, {'osname':o.os, 'tools':o.tools})
770 content = ( '\n' + p).join(lines)
772 content = address_re.sub(r' at 0x700000>', content)
773 content = engine_re.sub(r' File "bootstrap/src/engine/SCons/', content)
774 content = file_re.sub(r'\1 <module>', content)
775 content = nodelist_re.sub(r"\1 'NodeList' object \2", content)
776 content = self.for_display(content)
777 self.outfp.write(p + content + '\n')
779 if o.data[0] == '\n':
781 self.outfp.write(o.data + '</screen>')
783 self.afunclist = self.afunclist[:-1]
785 def start_scons_output_command(self, attrs):
788 except AttributeError:
789 self.error("<scons_output_command> tag outside of <scons_output>")
792 except AttributeError:
796 for name, value in attrs:
797 setattr(c, name, value)
798 o.commandlist.append(c)
799 self.afunclist.append(c.afunc)
801 def end_scons_output_command(self):
803 self.afunclist = self.afunclist[:-1]
805 def start_sconstruct(self, attrs):
808 self.afunclist.append(f.afunc)
810 def end_sconstruct(self):
812 self.outfp.write('<programlisting>')
813 output = self.for_display(f.data)
814 self.outfp.write(output + '</programlisting>')
816 self.afunclist = self.afunclist[:-1]
818 def process(filename):
823 f = open(filename, 'r')
824 except EnvironmentError, e:
825 sys.stderr.write('%s: %s\n' % (filename, msg))
829 if f is not sys.stdin:
832 if data.startswith('<?xml '):
833 first_line, data = data.split('\n', 1)
834 sys.stdout.write(first_line + '\n')
836 x = MySGML(sys.stdout)
847 parser = optparse.OptionParser()
848 opts, args = parser.parse_args(argv[1:])
856 if __name__ == "__main__":
861 # indent-tabs-mode:nil
863 # vim: set expandtab tabstop=4 shiftwidth=4: