Merged revisions 1784-1824 via svnmerge from
[scons.git] / bin / sconsexamples.py
1 #!/usr/bin/env python2
2 #
3 # scons_examples.py -   an SGML preprocessor for capturing SCons output
4 #                       and inserting into examples in our DocBook
5 #                       documentation
6 #
7
8 # This script looks for some SGML tags that describe SCons example
9 # configurations and commands to execute in those configurations, and
10 # uses TestCmd.py to execute the commands and insert the output into
11 # the output SGML.  This way, we can run a script and update all of
12 # our example output without having to do a lot of laborious by-hand
13 # checking.
14 #
15 # An "SCons example" looks like this, and essentially describes a set of
16 # input files (program source files as well as SConscript files):
17 #
18 #       <scons_example name="ex1">
19 #         <file name="SConstruct" printme="1">
20 #           env = Environment()
21 #           env.Program('foo')
22 #         </file>
23 #         <file name="foo.c">
24 #           int main() { printf("foo.c\n"); }
25 #         </file>
26 #       </scons_example>
27 #
28 # The <file> contents within the <scons_example> tag will get written
29 # into a temporary directory whenever example output needs to be
30 # generated.  By default, the <file> contents are not inserted into text
31 # directly, unless you set the "printme" attribute on one or more files,
32 # in which case they will get inserted within a <programlisting> tag.
33 # This makes it easy to define the example at the appropriate
34 # point in the text where you intend to show the SConstruct file.
35 #
36 # Note that you should usually give the <scons_example> a "name"
37 # attribute so that you can refer to the example configuration later to
38 # run SCons and generate output.
39 #
40 # If you just want to show a file's contents without worry about running
41 # SCons, there's a shorter <sconstruct> tag:
42 #
43 #       <sconstruct>
44 #         env = Environment()
45 #         env.Program('foo')
46 #       </sconstruct>
47 #
48 # This is essentially equivalent to <scons_example><file printme="1">,
49 # but it's more straightforward.
50 #
51 # SCons output is generated from the following sort of tag:
52 #
53 #       <scons_output example="ex1" os="posix">
54 #         <command>scons -Q foo</command>
55 #         <command>scons -Q foo</command>
56 #       </scons_output>
57 #
58 # You tell it which example to use with the "example" attribute, and
59 # then give it a list of <command> tags to execute.  You can also supply
60 # an "os" tag, which specifies the type of operating system this example
61 # is intended to show; if you omit this, default value is "posix".
62 #
63 # The generated SGML will show the command line (with the appropriate
64 # command-line prompt for the operating system), execute the command in
65 # a temporary directory with the example files, capture the standard
66 # output from SCons, and insert it into the text as appropriate.
67 # Error output gets passed through to your error output so you
68 # can see if there are any problems executing the command.
69 #
70
71 import os
72 import os.path
73 import re
74 import sgmllib
75 import string
76 import sys
77
78 sys.path.append(os.path.join(os.getcwd(), 'etc'))
79 sys.path.append(os.path.join(os.getcwd(), 'build', 'etc'))
80
81 scons_py = os.path.join('bootstrap', 'src', 'script', 'scons.py')
82 if not os.path.exists(scons_py):
83     scons_py = os.path.join('src', 'script', 'scons.py')
84
85 scons_lib_dir = os.path.join(os.getcwd(), 'bootstrap', 'src', 'engine')
86 if not os.path.exists(scons_lib_dir):
87     scons_lib_dir = os.path.join(os.getcwd(), 'src', 'engine')
88
89 import TestCmd
90
91 # The regular expression that identifies entity references in the
92 # standard sgmllib omits the underscore from the legal characters.
93 # Override it with our own regular expression that adds underscore.
94 sgmllib.entityref = re.compile('&([a-zA-Z][-_.a-zA-Z0-9]*)[^-_a-zA-Z0-9]')
95
96 class DataCollector:
97     """Generic class for collecting data between a start tag and end
98     tag.  We subclass for various types of tags we care about."""
99     def __init__(self):
100         self.data = ""
101     def afunc(self, data):
102         self.data = self.data + data
103
104 class Example(DataCollector):
105     """An SCons example.  This is essentially a list of files that
106     will get written to a temporary directory to collect output
107     from one or more SCons runs."""
108     def __init__(self):
109         DataCollector.__init__(self)
110         self.files = []
111         self.dirs = []
112
113 class File(DataCollector):
114     """A file, that will get written out to a temporary directory
115     for one or more SCons runs."""
116     def __init__(self, name):
117         DataCollector.__init__(self)
118         self.name = name
119
120 class Directory(DataCollector):
121     """A directory, that will get created in a temporary directory
122     for one or more SCons runs."""
123     def __init__(self, name):
124         DataCollector.__init__(self)
125         self.name = name
126
127 class Output(DataCollector):
128     """Where the command output goes.  This is essentially
129     a list of commands that will get executed."""
130     def __init__(self):
131         DataCollector.__init__(self)
132         self.commandlist = []
133
134 class Command(DataCollector):
135     """A tag for where the command output goes.  This is essentially
136     a list of commands that will get executed."""
137     pass
138
139 Prompt = {
140     'posix' : '% ',
141     'win32' : 'C:\\>'
142 }
143
144 # Magick SCons hackery.
145 #
146 # So that our examples can still use the default SConstruct file, we
147 # actually feed the following into SCons via stdin and then have it
148 # SConscript() the SConstruct file.  This stdin wrapper creates a set
149 # of ToolSurrogates for the tools for the appropriate platform.  These
150 # Surrogates print output like the real tools and behave like them
151 # without actually having to be on the right platform or have the right
152 # tool installed.
153 #
154 # The upshot:  We transparently change the world out from under the
155 # top-level SConstruct file in an example just so we can get the
156 # command output.
157
158 Stdin = """\
159 import SCons.Defaults
160
161 platform = '%s'
162
163 class Curry:
164     def __init__(self, fun, *args, **kwargs):
165         self.fun = fun
166         self.pending = args[:]
167         self.kwargs = kwargs.copy()
168
169     def __call__(self, *args, **kwargs):
170         if kwargs and self.kwargs:
171             kw = self.kwargs.copy()
172             kw.update(kwargs)
173         else:
174             kw = kwargs or self.kwargs
175
176         return apply(self.fun, self.pending + args, kw)
177
178 def Str(target, source, env, cmd=""):
179     return env.subst(cmd, target=target, source=source)
180
181 class ToolSurrogate:
182     def __init__(self, tool, variable, func):
183         self.tool = tool
184         self.variable = variable
185         self.func = func
186     def __call__(self, env):
187         t = Tool(self.tool)
188         t.generate(env)
189         orig = env[self.variable]
190         env[self.variable] = Action(self.func, strfunction=Curry(Str, cmd=orig))
191
192 def Null(target, source, env):
193     pass
194
195 def Cat(target, source, env):
196     target = str(target[0])
197     f = open(target, "wb")
198     for src in map(str, source):
199         f.write(open(src, "rb").read())
200     f.close()
201
202 ToolList = {
203     'posix' :   [('cc', 'CCCOM', Cat),
204                  ('link', 'LINKCOM', Cat),
205                  ('tar', 'TARCOM', Null),
206                  ('zip', 'ZIPCOM', Null)],
207     'win32' :   [('msvc', 'CCCOM', Cat),
208                  ('mslink', 'LINKCOM', Cat)]
209 }
210
211 tools = map(lambda t: apply(ToolSurrogate, t), ToolList[platform])
212
213 SCons.Defaults.ConstructionEnvironment.update({
214     'PLATFORM' : platform,
215     'TOOLS'    : tools,
216 })
217
218 SConscript('SConstruct')
219 """
220
221 class MySGML(sgmllib.SGMLParser):
222     """A subclass of the standard Python 2.2 sgmllib SGML parser.
223
224     Note that this doesn't work with the 1.5.2 sgmllib module, because
225     that didn't have the ability to work with ENTITY declarations.
226     """
227     def __init__(self):
228         sgmllib.SGMLParser.__init__(self)
229         self.examples = {}
230         self.afunclist = []
231
232     def handle_data(self, data):
233         try:
234             f = self.afunclist[-1]
235         except IndexError:
236             sys.stdout.write(data)
237         else:
238             f(data)
239
240     def handle_comment(self, data):
241         sys.stdout.write('<!--' + data + '-->')
242
243     def handle_decl(self, data):
244         sys.stdout.write('<!' + data + '>')
245
246     def unknown_starttag(self, tag, attrs):
247         try:
248             f = self.example.afunc
249         except AttributeError:
250             f = sys.stdout.write
251         if not attrs:
252             f('<' + tag + '>')
253         else:
254             f('<' + tag)
255             for name, value in attrs:
256                 f(' ' + name + '=' + '"' + value + '"')
257             f('>')
258
259     def unknown_endtag(self, tag):
260         sys.stdout.write('</' + tag + '>')
261
262     def unknown_entityref(self, ref):
263         sys.stdout.write('&' + ref + ';')
264
265     def unknown_charref(self, ref):
266         sys.stdout.write('&#' + ref + ';')
267
268     def start_scons_example(self, attrs):
269         t = filter(lambda t: t[0] == 'name', attrs)
270         if t:
271             name = t[0][1]
272             try:
273                e = self.examples[name]
274             except KeyError:
275                e = self.examples[name] = Example()
276         else:
277             e = Example()
278         for name, value in attrs:
279             setattr(e, name, value)
280         self.e = e
281         self.afunclist.append(e.afunc)
282
283     def end_scons_example(self):
284         e = self.e
285         files = filter(lambda f: f.printme, e.files)
286         if files:
287             sys.stdout.write('<programlisting>')
288             for f in files:
289                 if f.printme:
290                     i = len(f.data) - 1
291                     while f.data[i] == ' ':
292                         i = i - 1
293                     output = string.replace(f.data[:i+1], '__ROOT__', '')
294                     sys.stdout.write(output)
295             if e.data and e.data[0] == '\n':
296                 e.data = e.data[1:]
297             sys.stdout.write(e.data + '</programlisting>')
298         delattr(self, 'e')
299         self.afunclist = self.afunclist[:-1]
300
301     def start_file(self, attrs):
302         try:
303             e = self.e
304         except AttributeError:
305             self.error("<file> tag outside of <scons_example>")
306         t = filter(lambda t: t[0] == 'name', attrs)
307         if not t:
308             self.error("no <file> name attribute found")
309         try:
310             e.prefix
311         except AttributeError:
312             e.prefix = e.data
313             e.data = ""
314         f = File(t[0][1])
315         f.printme = None
316         for name, value in attrs:
317             setattr(f, name, value)
318         e.files.append(f)
319         self.afunclist.append(f.afunc)
320
321     def end_file(self):
322         self.e.data = ""
323         self.afunclist = self.afunclist[:-1]
324
325     def start_directory(self, attrs):
326         try:
327             e = self.e
328         except AttributeError:
329             self.error("<directory> tag outside of <scons_example>")
330         t = filter(lambda t: t[0] == 'name', attrs)
331         if not t:
332             self.error("no <directory> name attribute found")
333         try:
334             e.prefix
335         except AttributeError:
336             e.prefix = e.data
337             e.data = ""
338         d = Directory(t[0][1])
339         for name, value in attrs:
340             setattr(d, name, value)
341         e.dirs.append(d)
342         self.afunclist.append(d.afunc)
343
344     def end_directory(self):
345         self.e.data = ""
346         self.afunclist = self.afunclist[:-1]
347
348     def start_scons_example_file(self, attrs):
349         t = filter(lambda t: t[0] == 'example', attrs)
350         if not t:
351             self.error("no <scons_example_file> example attribute found")
352         exname = t[0][1]
353         try:
354             e = self.examples[exname]
355         except KeyError:
356             self.error("unknown example name '%s'" % exname)
357         fattrs = filter(lambda t: t[0] == 'name', attrs)
358         if not fattrs:
359             self.error("no <scons_example_file> name attribute found")
360         fname = fattrs[0][1]
361         f = filter(lambda f, fname=fname: f.name == fname, e.files)
362         if not f:
363             self.error("example '%s' does not have a file named '%s'" % (exname, fname))
364         self.f = f[0]
365
366     def end_scons_example_file(self):
367         f = self.f
368         sys.stdout.write('<programlisting>')
369         i = len(f.data) - 1
370         while f.data[i] == ' ':
371             i = i - 1
372         sys.stdout.write(f.data[:i+1] + '</programlisting>')
373         delattr(self, 'f')
374
375     def start_scons_output(self, attrs):
376         t = filter(lambda t: t[0] == 'example', attrs)
377         if not t:
378             self.error("no <scons_output> example attribute found")
379         exname = t[0][1]
380         try:
381             e = self.examples[exname]
382         except KeyError:
383             self.error("unknown example name '%s'" % exname)
384         # Default values for an example.
385         o = Output()
386         o.os = 'posix'
387         o.e = e
388         # Locally-set.
389         for name, value in attrs:
390             setattr(o, name, value)
391         self.o = o
392         self.afunclist.append(o.afunc)
393
394     def end_scons_output(self):
395         o = self.o
396         e = o.e
397         t = TestCmd.TestCmd(workdir='', combine=1)
398         t.subdir('ROOT', 'WORK')
399         for d in e.dirs:
400             dir = t.workpath('WORK', d.name)
401             if not os.path.exists(dir):
402                 os.makedirs(dir)
403         for f in e.files:
404             i = 0
405             while f.data[i] == '\n':
406                 i = i + 1
407             lines = string.split(f.data[i:], '\n')
408             i = 0
409             while lines[0][i] == ' ':
410                 i = i + 1
411             lines = map(lambda l, i=i: l[i:], lines)
412             path = string.replace(f.name, '__ROOT__', t.workpath('ROOT'))
413             dir, name = os.path.split(f.name)
414             if dir:
415                 dir = t.workpath('WORK', dir)
416                 if not os.path.exists(dir):
417                     os.makedirs(dir)
418             content = string.join(lines, '\n')
419             content = string.replace(content,
420                                      '__ROOT__',
421                                      t.workpath('ROOT'))
422             t.write(t.workpath('WORK', f.name), content)
423         i = len(o.prefix)
424         while o.prefix[i-1] != '\n':
425             i = i - 1
426         sys.stdout.write('<literallayout>' + o.prefix[:i])
427         p = o.prefix[i:]
428         for c in o.commandlist:
429             sys.stdout.write(p + Prompt[o.os])
430             d = string.replace(c.data, '__ROOT__', '')
431             sys.stdout.write('<userinput>' + d + '</userinput>\n')
432             e = string.replace(c.data, '__ROOT__', t.workpath('ROOT'))
433             args = string.split(e)[1:]
434             os.environ['SCONS_LIB_DIR'] = scons_lib_dir
435             t.run(interpreter = sys.executable,
436                   program = scons_py,
437                   arguments = '-f - ' + string.join(args),
438                   chdir = t.workpath('WORK'),
439                   stdin = Stdin % o.os)
440             out = string.replace(t.stdout(), t.workpath('ROOT'), '')
441             if out:
442                 lines = string.split(out, '\n')
443                 if lines:
444                     while lines[-1] == '':
445                         lines = lines[:-1]
446                     for l in lines:
447                         sys.stdout.write(p + l + '\n')
448             #err = t.stderr()
449             #if err:
450             #    sys.stderr.write(err)
451         if o.data[0] == '\n':
452             o.data = o.data[1:]
453         sys.stdout.write(o.data + '</literallayout>')
454         delattr(self, 'o')
455         self.afunclist = self.afunclist[:-1]
456
457     def start_command(self, attrs):
458         try:
459             o = self.o
460         except AttributeError:
461             self.error("<command> tag outside of <scons_output>")
462         try:
463             o.prefix
464         except AttributeError:
465             o.prefix = o.data
466             o.data = ""
467         c = Command()
468         o.commandlist.append(c)
469         self.afunclist.append(c.afunc)
470
471     def end_command(self):
472         self.o.data = ""
473         self.afunclist = self.afunclist[:-1]
474
475     def start_sconstruct(self, attrs):
476         sys.stdout.write('<programlisting>')
477
478     def end_sconstruct(self):
479         sys.stdout.write('</programlisting>')
480
481 try:
482     file = sys.argv[1]
483 except IndexError:
484     file = '-'
485
486 if file == '-':
487     f = sys.stdin
488 else:
489     try:
490         f = open(file, 'r')
491     except IOError, msg:
492         print file, ":", msg
493         sys.exit(1)
494
495 data = f.read()
496 if f is not sys.stdin:
497     f.close()
498
499 x = MySGML()
500 for c in data:
501     x.feed(c)
502 x.close()