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 string.replace(str(self._Proxy__subject), 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 map(lambda x: string.replace(str(x), os.sep, Sep),
217 orig_RDirs(self, pathlist))
218 SCons.Node.FS.File.RDirs = my_RDirs
221 def __init__(self, fun, *args, **kwargs):
223 self.pending = args[:]
224 self.kwargs = kwargs.copy()
226 def __call__(self, *args, **kwargs):
227 if kwargs and self.kwargs:
228 kw = self.kwargs.copy()
231 kw = kwargs or self.kwargs
233 return self.fun(*self.pending + args, **kw)
235 def Str(target, source, env, cmd=""):
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')
242 def __init__(self, tool, variable, func, varlist):
244 if not type(variable) is type([]):
245 variable = [variable]
246 self.variable = variable
248 self.varlist = varlist
249 def __call__(self, env):
252 for v in self.variable:
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)
265 # This is for the benefit of printing the 'TOOLS'
266 # variable through env.Dump().
267 return repr(self.tool)
269 def Null(target, source, env):
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())
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)
287 for d in [str(env.Dir('$CPPPATH')), '.']:
288 f = os.path.join(d, include)
289 if os.path.exists(f):
292 elif line[:11] != "STRIP CCCOM":
294 for src in map(str, source):
296 fp.write('debug = ' + ARGUMENTS.get('debug', '0') + '\\n')
299 public_class_re = re.compile('^public class (\S+)', re.MULTILINE)
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))
310 for src in map(str, source):
311 contents = open(src, "rb").read()
312 classes = public_class_re.findall(contents)
314 for t in [x for x in tlist if x.find(c) != -1]:
315 open(t, "wb").write(contents)
317 for t in not_copied.keys():
318 open(t, "wb").write("\\n")
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())
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)
331 def JarCom(target, source, env):
332 target = str(target[0])
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())
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.
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, []),
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, []),
381 toollist = ToolList[platform]
382 filter_tools = string.split('%(tools)s')
384 toollist = [x for x in toollist if x[0] in filter_tools]
386 toollist = [ToolSurrogate(*t) for t in toollist]
388 toollist.append('install')
390 def surrogate_spawn(sh, escape, cmd, args, env):
393 def surrogate_pspawn(sh, escape, cmd, args, env, stdout, stderr):
396 SCons.Defaults.ConstructionEnvironment.update({
397 'PLATFORM' : platform,
399 'SPAWN' : surrogate_spawn,
400 'PSPAWN' : surrogate_pspawn,
403 SConscript('SConstruct')
406 # "Commands" that we will execute in our examples.
407 def command_scons(args, c, test, dict):
412 except AttributeError:
415 for arg in c.environment.split():
416 key, val = arg.split('=')
418 save_vals[key] = os.environ[key]
420 delete_keys.append(key)
421 os.environ[key] = val
422 test.run(interpreter = sys.executable,
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:
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')
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 = [x for x in files if x[0] != '.']
483 return [' '.join(files)]
487 l.extend(ls(test.workpath('WORK', a)))
490 return ls(test.workpath('WORK'))
492 def command_sleep(args, c, test, dict):
493 time.sleep(int(args[0]))
496 'scons' : command_scons,
497 'touch' : command_touch,
498 'edit' : command_edit,
500 'sleep' : command_sleep,
503 def ExecuteCommand(args, c, t, dict):
505 func = CommandDict[args[0]]
507 func = lambda args, c, t, dict: []
508 return func(args[1:], c, t, dict)
510 class MySGML(sgmllib.SGMLParser):
511 """A subclass of the standard Python 2.2 sgmllib SGML parser.
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.
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.
520 def __init__(self, outfp):
521 sgmllib.SGMLParser.__init__(self)
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.
531 def handle_data(self, data):
533 f = self.afunclist[-1]
535 self.outfp.write(data)
539 def handle_comment(self, data):
540 self.outfp.write('<!--' + data + '-->')
542 def handle_decl(self, data):
543 self.outfp.write('<!' + data + '>')
545 def unknown_starttag(self, tag, attrs):
547 f = self.example.afunc
548 except AttributeError:
554 for name, value in attrs:
555 f(' ' + name + '=' + '"' + value + '"')
558 def unknown_endtag(self, tag):
559 self.outfp.write('</' + tag + '>')
561 def unknown_entityref(self, ref):
562 self.outfp.write('&' + ref + ';')
564 def unknown_charref(self, ref):
565 self.outfp.write('&#' + ref + ';')
567 # Here is where the heavy lifting begins. The following methods
568 # handle the begin-end tags of our SCons examples.
570 def start_scons_example(self, attrs):
571 t = [t for t in attrs if t[0] == 'name']
575 e = self.examples[name]
577 e = self.examples[name] = Example()
580 for name, value in attrs:
581 setattr(e, name, value)
583 self.afunclist.append(e.afunc)
585 def end_scons_example(self):
587 files = [f for f in e.files if f.printme]
589 self.outfp.write('<programlisting>')
593 while f.data[i] == ' ':
595 output = f.data[:i+1].replace('__ROOT__', '')
596 output = output.replace('<', '<')
597 output = output.replace('>', '>')
598 self.outfp.write(output)
599 if e.data and e.data[0] == '\n':
601 self.outfp.write(e.data + '</programlisting>')
603 self.afunclist = self.afunclist[:-1]
605 def start_file(self, attrs):
608 except AttributeError:
609 self.error("<file> tag outside of <scons_example>")
610 t = [t for t in attrs if t[0] == 'name']
612 self.error("no <file> name attribute found")
615 except AttributeError:
620 for name, value in attrs:
621 setattr(f, name, value)
623 self.afunclist.append(f.afunc)
627 self.afunclist = self.afunclist[:-1]
629 def start_directory(self, attrs):
632 except AttributeError:
633 self.error("<directory> tag outside of <scons_example>")
634 t = [t for t in attrs if t[0] == 'name']
636 self.error("no <directory> name attribute found")
639 except AttributeError:
642 d = Directory(t[0][1])
643 for name, value in attrs:
644 setattr(d, name, value)
646 self.afunclist.append(d.afunc)
648 def end_directory(self):
650 self.afunclist = self.afunclist[:-1]
652 def start_scons_example_file(self, attrs):
653 t = [t for t in attrs if t[0] == 'example']
655 self.error("no <scons_example_file> example attribute found")
658 e = self.examples[exname]
660 self.error("unknown example name '%s'" % exname)
661 fattrs = [t for t in attrs if t[0] == 'name']
663 self.error("no <scons_example_file> name attribute found")
665 f = [f for f in e.files if f.name == fname]
667 self.error("example '%s' does not have a file named '%s'" % (exname, fname))
670 def end_scons_example_file(self):
672 self.outfp.write('<programlisting>')
673 self.outfp.write(f.data + '</programlisting>')
676 def start_scons_output(self, attrs):
677 t = [t for t in attrs if t[0] == 'example']
679 self.error("no <scons_output> example attribute found")
682 e = self.examples[exname]
684 self.error("unknown example name '%s'" % exname)
685 # Default values for an example.
692 for name, value in attrs:
693 setattr(o, name, value)
695 self.afunclist.append(o.afunc)
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.
702 t = TestCmd.TestCmd(workdir='', combine=1)
705 t.subdir('ROOT', 'WORK')
706 t.rootpath = t.workpath('ROOT').replace('\\', '\\\\')
709 dir = t.workpath('WORK', d.name)
710 if not os.path.exists(dir):
715 while f.data[i] == '\n':
717 lines = f.data[i:].split('\n')
719 while lines[0][i] == ' ':
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):
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))
736 while o.prefix[i-1] != '\n':
739 self.outfp.write('<screen>' + o.prefix[:i])
742 # Regular expressions for making the doc output consistent,
743 # regardless of reported addresses or Python version.
745 # Massage addresses in object repr strings to a constant.
746 address_re = re.compile(r' at 0x[0-9a-fA-F]*\>')
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/')
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)
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+)')
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')
766 e = c.data.replace('__ROOT__', t.workpath('ROOT'))
768 lines = ExecuteCommand(args, c, t, {'osname':o.os, 'tools':o.tools})
773 content = ( '\n' + p).join(lines)
775 content = address_re.sub(r' at 0x700000>', 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('<', '<')
780 content = content.replace('>', '>')
781 self.outfp.write(p + content + '\n')
783 if o.data[0] == '\n':
785 self.outfp.write(o.data + '</screen>')
787 self.afunclist = self.afunclist[:-1]
789 def start_scons_output_command(self, attrs):
792 except AttributeError:
793 self.error("<scons_output_command> tag outside of <scons_output>")
796 except AttributeError:
800 for name, value in attrs:
801 setattr(c, name, value)
802 o.commandlist.append(c)
803 self.afunclist.append(c.afunc)
805 def end_scons_output_command(self):
807 self.afunclist = self.afunclist[:-1]
809 def start_sconstruct(self, attrs):
812 self.afunclist.append(f.afunc)
814 def end_sconstruct(self):
816 self.outfp.write('<programlisting>')
817 output = f.data.replace('__ROOT__', '')
818 self.outfp.write(output + '</programlisting>')
820 self.afunclist = self.afunclist[:-1]
822 def process(filename):
827 f = open(filename, 'r')
828 except EnvironmentError, e:
829 sys.stderr.write('%s: %s\n' % (filename, msg))
833 if f is not sys.stdin:
836 if data.startswith('<?xml '):
837 first_line, data = data.split('\n', 1)
838 sys.stdout.write(first_line + '\n')
840 x = MySGML(sys.stdout)
851 parser = optparse.OptionParser()
852 opts, args = parser.parse_args(argv[1:])
860 if __name__ == "__main__":
865 # indent-tabs-mode:nil
867 # vim: set expandtab tabstop=4 shiftwidth=4: