Issue 2255: Handle scanning of UTF-8 and UTF-16 files. (Greg Spencer)
[scons.git] / doc / SConscript
1 #
2 # SConscript file for building SCons documentation.
3 #
4
5 #
6 # __COPYRIGHT__
7 #
8 # Permission is hereby granted, free of charge, to any person obtaining
9 # a copy of this software and associated documentation files (the
10 # "Software"), to deal in the Software without restriction, including
11 # without limitation the rights to use, copy, modify, merge, publish,
12 # distribute, sublicense, and/or sell copies of the Software, and to
13 # permit persons to whom the Software is furnished to do so, subject to
14 # the following conditions:
15 #
16 # The above copyright notice and this permission notice shall be included
17 # in all copies or substantial portions of the Software.
18 #
19 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
20 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
21 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26 #
27
28 import os.path
29 import re
30 import string
31
32 Import('build_dir', 'env', 'whereis')
33
34 env = env.Clone()
35
36 build = os.path.join(build_dir, 'doc')
37
38 #
39 #
40 #
41 dist_doc_tar_gz = '$DISTDIR/scons-doc-${VERSION}.tar.gz'
42
43 #
44 # We'll only try to build text files (for some documents)
45 # if lynx is available to do the dump.
46 #
47 fig2dev = whereis('fig2dev')
48 epydoc = whereis('epydoc')
49 groff = whereis('groff')
50 lynx = whereis('lynx')
51 man2html = whereis('man2html')
52 jade_original = whereis('jade')
53 jade = whereis('openjade') or jade_original
54 jadetex = whereis('jadetex')
55 pdfjadetex = whereis('pdfjadetex')
56 jw = whereis('jw')
57 tidy = whereis('tidy')
58
59 tar_deps = []
60 tar_list = []
61
62 entity_re = re.compile(r'<!entity\s+(?:%\s+)?(?:\S+)\s+SYSTEM\s+"([^"]*)">', re.I)
63 format_re = re.compile(r'<(?:graphic|imagedata)\s+fileref="([^"]*)"(?:\s+format="([^"]*)")?')
64
65 #
66 # Find internal dependencies in .xml files:
67 #
68 #   <!entity bground SYSTEM "bground.xml">
69 #   <graphic fileref="file.jpg">
70 #   <imagedata fileref="file.jpg">
71 #
72 # This only finds one per line, and assumes that anything
73 # defined as a SYSTEM entity is, in fact, a file included
74 # somewhere in the document.
75 #
76 def scanxml(node, env, target):
77     includes = []
78
79     contents = node.get_text_contents()
80
81     includes.extend(entity_re.findall(contents))
82
83     matches = format_re.findall(contents)
84     for m in matches:
85         file, format = m
86         if format and file[-len(format):] != format:
87             file = file + '.' + format
88         if not os.path.isabs(file):
89             a = []
90             f = file
91             while f:
92                 f, tail = os.path.split(f)
93                 if tail == 'doc':
94                     break
95                 a = [tail] + a
96             file = apply(os.path.join, a, {})
97         includes.append(file)
98
99     return includes
100
101 s = Scanner(name = 'xml', function = scanxml, skeys = ['.xml', '.mod'])
102
103 orig_env = env
104 env = orig_env.Clone(SCANNERS = [s],
105                      SCONS_PROC_PY = File('#bin/scons-proc.py').rfile(),
106                      SCONSOUTPUT_PY = File('#bin/sconsoutput.py').rfile())
107
108 # Fetch the list of files in the build engine that contain
109 # SCons documentation XML for processing.
110 def chop(s): return s[:-1]
111
112 # If we ever read doc from __scons_doc__ strings in *.py files again,
113 # here's how it's done:
114 #manifest_in = File('#src/engine/MANIFEST.in').rstr()
115 #manifest_xml_in = File('#src/engine/MANIFEST-xml.in').rstr()
116 #scons_doc_files = map(chop, open(manifest_in).readlines() +\
117 #                            open(manifest_xml_in).readlines())
118 #scons_doc_files = map(lambda x: '#src/engine/'+x, scons_doc_files)
119 #manifest_in = File('#src/engine/MANIFEST.in').rstr()
120
121 manifest_xml_in = File('#src/engine/MANIFEST-xml.in').rstr()
122 scons_doc_files = map(chop, open(manifest_xml_in).readlines())
123 scons_doc_files = map(lambda x: File('#src/engine/'+x).rstr(), scons_doc_files)
124
125 if not jw:
126     print "jw not found, skipping building User Guide."
127 else:
128     #
129     # Always create a version.xml file containing the version information
130     # for this run.  Ignore it for dependency purposes so we don't
131     # rebuild all the docs every time just because the date changes.
132     #
133     date, ver, rev = env.Dictionary('DATE', 'VERSION', 'REVISION')
134     version_xml = File(os.path.join(build, "version.xml"))
135     #version_xml = File("version.xml")
136     verfile = str(version_xml)
137     try:
138         os.unlink(verfile)
139     except OSError:
140         pass    # okay if the file didn't exist
141     dir, f = os.path.split(verfile)
142     try:
143         os.makedirs(dir)
144     except OSError:
145         pass    # okay if the directory already exists
146     open(verfile, "w").write("""<!--
147 THIS IS AN AUTOMATICALLY-GENERATED FILE.  DO NOT EDIT.
148 -->
149 <!ENTITY builddate "%s">
150 <!ENTITY buildversion "%s">
151 <!ENTITY buildrevision "%s">
152 """ % (date, ver, rev))
153
154     builders_gen = os.path.join(build, 'user', 'builders.gen')
155     builders_mod = os.path.join(build, 'user', 'builders.mod')
156     tools_gen = os.path.join(build, 'user', 'tools.gen')
157     tools_mod = os.path.join(build, 'user', 'tools.mod')
158     variables_gen = os.path.join(build, 'user', 'variables.gen')
159     variables_mod = os.path.join(build, 'user', 'variables.mod')
160
161     # We put $( - $) around $SOURCES in the command line below because
162     # the path names will change when a given input file is found in
163     # a repository one run and locally the next, and we don't want
164     # to rebuild documentation just because it's found in one location
165     # vs. the other.  The *.gen and *.mod targets will still be dependent
166     # on the list of the files themselves.
167     doc_output_files = [builders_gen, builders_mod,
168                         tools_gen, tools_mod,
169                         variables_gen, variables_mod]
170     b = env.Command(doc_output_files,
171                     scons_doc_files,
172                     "$PYTHON $SCONS_PROC_PY --xml -b ${TARGETS[0]},${TARGETS[1]} -t ${TARGETS[2]},${TARGETS[3]} -v ${TARGETS[4]},${TARGETS[5]} $( $SOURCES $)")
173     env.Depends(b, "$SCONS_PROC_PY")
174
175     env.Local(b)
176
177     #
178     # Each document will live in its own subdirectory.  List them here
179     # as hash keys, with a hash of the info to control its build.
180     #
181     docs = {
182         'design' : {
183                 'htmlindex' : 'book1.html',
184                 'ps'        : 1,
185                 'pdf'       : 1,
186                 'text'      : 0,
187         },
188         # This doesn't build on all systems, and the document is old
189         # enough that there's reallyno need to build it every time any
190         # more, so just comment it out for now.
191         #'python10' : {
192         #        'htmlindex' : 't1.html',
193         #        'html'      : 1,
194         #        'ps'        : 1,
195         #        'pdf'       : 0,
196         #        'text'      : 0,
197         #        'graphics'  : [
198         #                        'arch.fig',
199         #                        'builder.fig',
200         #                        'job-task.fig',
201         #                        'node.fig',
202         #                        'scanner.fig',
203         #                        'sig.fig'
204         #                      ],
205         #},
206         'reference' : {
207                 'htmlindex' : 'book1.html',
208                 'html'      : 1,
209                 'ps'        : 1,
210                 'pdf'       : 1,
211                 'text'      : 0,
212         },
213         # For whenever (if ever?) we start putting developer guide
214         # information in a printable document instead of the wiki.
215         #'developer' : {
216         #        'htmlindex' : 'book1.html',
217         #        'html'      : 1,
218         #        'ps'        : 1,
219         #        'pdf'       : 1,
220         #        'text'      : 0,
221         #},
222         'user' : {
223                 'htmlindex' : 'book1.html',
224                 'html'      : 1,
225                 'ps'        : 1,
226                 'pdf'       : 1,
227                 'text'      : 1,
228                 'graphics'  : [
229                                 'SCons-win32-install-1.jpg',
230                                 'SCons-win32-install-2.jpg',
231                                 'SCons-win32-install-3.jpg',
232                                 'SCons-win32-install-4.jpg',
233                               ],
234                 'sconsoutput' : 1,
235         },
236     }
237
238     #
239     # We have to tell SCons to scan the top-level XML files which
240     # get included by the document XML files in the subdirectories.
241     #
242     manifest = File('MANIFEST').rstr()
243     src_files = map(lambda x: x[:-1], open(manifest).readlines())
244     for s in src_files:
245         base, ext = os.path.splitext(s)
246         if ext in ['.fig', '.jpg']:
247             orig_env.Install(build, s)
248         else:
249             orig_env.SCons_revision(os.path.join(build, s), s)
250         Local(os.path.join(build, s))
251
252     #
253     # For each document, build the document itself in HTML, Postscript,
254     # and PDF formats.
255     #
256     for doc in docs.keys():
257         manifest = File(os.path.join(doc, 'MANIFEST')).rstr()
258         src_files = map(lambda x: x[:-1],
259                         open(manifest).readlines())
260         build_doc = docs[doc].get('sconsoutput') and int(ARGUMENTS.get('BUILDDOC', 0))
261         for s in src_files:
262             doc_s = os.path.join(doc, s)
263             build_s = os.path.join(build, doc, s)
264             base, ext = os.path.splitext(doc_s)
265             if ext in ['.fig', '.jpg']:
266                 orig_env.InstallAs(build_s, doc_s)
267             else:
268                 if build_doc and ext == '.xml':
269                     env.Command(doc_s,
270                                 base + '.in',
271                                 "$PYTHON $SCONSOUTPUT_PY $SOURCE > $TARGET")
272                 orig_env.SCons_revision(build_s, doc_s)
273             Local(build_s)
274
275         main = os.path.join(build, doc, 'main.xml')
276         out = 'main.out'
277
278         # Hard-coding the scons-src path is a bit of a hack.  This can
279         # be reworked when a better solution presents itself.
280         scons_src_main = os.path.join(build_dir, 'scons-src', 'doc', main)
281         env.Ignore(scons_src_main, version_xml)
282
283         htmldir = os.path.join(build, 'HTML', 'scons-%s' % doc)
284         htmlindex = os.path.join(htmldir, docs[doc]['htmlindex'])
285         html = os.path.join(build, 'HTML', 'scons-%s.html' % doc)
286         ps = os.path.join(build, 'PS', 'scons-%s.ps' % doc)
287         pdf = os.path.join(build, 'PDF', 'scons-%s.pdf' % doc)
288         text = os.path.join(build, 'TEXT', 'scons-%s.txt' % doc)
289
290         if docs[doc].get('html') and jade:
291             def copy_index_html(target, source, env):
292                 # Older versions of DocBook|jw|jade|whatever would
293                 # create a book1.html file, while newer versions create
294                 # an index.html file (logically enough).  The scons.org
295                 # web site links expect book1.html, so we're going to
296                 # leave the target as is, and run this post-processing
297                 # action function to check that the target really did
298                 # get created, and if it didn't, copy it from index.html.
299                 t = str(target[0])
300                 if not os.path.exists(t):
301                     i = os.path.join(os.path.split(t)[0], 'index.html')
302                     open(t, 'w').write(open(i, 'r').read())
303                 return None
304
305             cmds = [
306                 Delete("${TARGET.dir}/*.html"),
307                 "jw -b html -o ${TARGET.dir} $SOURCES",
308             ]
309             if tidy:
310                 cmds.append("tidy -m -q $TARGET || true")
311             cmds.append(Action(copy_index_html))
312             env.Command(htmlindex, File(main), cmds)
313             Local(htmlindex)
314
315             cmds = [
316                 Delete("${TARGET.dir}/main.html"),
317                 "jw -u -b html -o ${TARGET.dir} $SOURCES",
318                 Move("$TARGET", "${TARGET.dir}/main.html"),
319             ]
320             if tidy:
321                 cmds.append("tidy -m -q $TARGET || true")
322             env.Command(html, File(main), cmds)
323             Local(html)
324
325             env.Ignore([html, htmlindex], version_xml)
326
327             tar_deps.extend([html, htmlindex])
328             tar_list.extend([html, htmldir])
329
330             for g in docs[doc].get('graphics', []):
331                 base, ext = os.path.splitext(g)
332                 if ext == '.fig':
333                     jpg = base + '.jpg'
334                     htmldir_jpg = os.path.join(htmldir, jpg)
335                     if fig2dev:
336                         fig = os.path.join(build, doc, g)
337                         env.Command(htmldir_jpg, fig,
338                                     "%s -L jpeg -q 100 $SOURCES $TARGET" % fig2dev)
339                     else:
340                         env.InstallAs(htmldir_jpg, jpg)
341                     env.Depends(html, htmldir_jpg)
342                     Local(htmldir_jpg)
343                 else:
344                     src = os.path.join(build, doc, g)
345                     Local(env.Install(htmldir, src))
346
347         if docs[doc].get('ps') and jadetex and jade_original:
348             env.Command(ps, main, [
349                 Delete("${TARGET.dir}/%s" % out),
350                 "jw -b ps -o ${TARGET.dir} -p %s $SOURCES" % jade_original,
351                 "mv ${TARGET.dir}/main.ps $TARGET",
352                 Delete("${TARGET.dir}/%s" % out),
353             ])
354             Local(ps)
355
356             env.Ignore(ps, version_xml)
357
358             tar_deps.append(ps)
359             tar_list.append(ps)
360
361             for g in docs[doc].get('graphics', []):
362                 base, ext = os.path.splitext(g)
363                 if ext == '.fig':
364                     eps = base + '.eps'
365                     build_eps = os.path.join(build, 'PS', eps)
366                     if fig2dev:
367                         fig = os.path.join(build, doc, g)
368                         env.Command(build_eps, fig, "%s -L eps $SOURCES $TARGET" % fig2dev)
369                     else:
370                         env.InstallAs(build_eps, eps)
371                     env.Depends(ps, build_eps)
372                     Local(build_eps)
373                 else:
374                     src = os.path.join(build, doc, g)
375                     Local(env.Install(htmldir, src))
376
377         if docs[doc].get('pdf') and pdfjadetex and jade_original:
378             env.Command(pdf, main, [
379                 Delete("${TARGET.dir}/%s" % out),
380                 "jw -b pdf -o ${TARGET.dir} -p %s $SOURCES" % jade_original,
381                 "mv ${TARGET.dir}/main.pdf $TARGET",
382                 Delete("${TARGET.dir}/out"),
383             ])
384             Local(pdf)
385
386             env.Ignore(pdf, version_xml)
387
388             tar_deps.append(pdf)
389             tar_list.append(pdf)
390
391         if docs[doc].get('text') and jade and lynx:
392             env.Command(text, html, "lynx -dump ${SOURCE.abspath} > $TARGET")
393             Local(text)
394
395             env.Ignore(text, version_xml)
396
397             tar_deps.append(text)
398             tar_list.append(text)
399
400 #
401 # Man page(s), in good ol' troff format.
402 #
403 man_page_list = ['scons.1', 'sconsign.1', 'scons-time.1']
404
405 for m in man_page_list:
406     x = orig_env.SCons_revision(os.path.join(build, 'man', m),
407                             os.path.join('man', m))
408
409 man_i_files = ['builders.man', 'tools.man', 'variables.man']
410
411 man_intermediate_files = map(lambda x: os.path.join(build, 'man', x),
412                              man_i_files)
413
414 cmd = "$PYTHON $SCONS_PROC_PY --man -b ${TARGETS[0]} -t ${TARGETS[1]} -v ${TARGETS[2]} $( $SOURCES $)"
415 man_intermediate_files = env.Command(man_intermediate_files,
416                                      scons_doc_files,
417                                      cmd)
418 env.Depends(man_intermediate_files, "$SCONS_PROC_PY")
419 Local(man_intermediate_files)
420
421 for man_1 in man_page_list:
422     man, _1 = os.path.splitext(man_1)
423
424     man_1 = os.path.join(build, 'man', man_1)
425
426     if groff:
427         ps = os.path.join(build, 'PS', '%s-man.ps' % man)
428         text = os.path.join(build, 'TEXT', '%s-man.txt' % man)
429
430         b = env.Command(ps, man_1, "( cd ${SOURCES.dir} && groff -man -Tps ${SOURCES.file} ) > $TARGET")
431         Local(ps)
432         env.Depends(b, man_intermediate_files)
433
434         b = env.Command(text, man_1, "( cd ${SOURCES.dir} && groff -man -Tascii ${SOURCES.file} ) > $TARGET")
435         Local(text)
436         env.Depends(b, man_intermediate_files)
437
438         tar_deps.extend([ps, text])
439         tar_list.extend([ps, text])
440
441     if man2html:
442         html = os.path.join(build, 'HTML' , '%s-man.html' % man)
443
444         def strip_to_first_html_tag(target, source, env):
445             t = str(target[0])
446             contents = open(t).read()
447             contents = contents[string.find(contents, '<HTML>'):]
448             open(t, 'w').write(contents)
449             return 0
450
451         cmds = [
452             "( cd %s/man && cp %s .. )" % (build, string.join(man_i_files)),
453             "( cd ${SOURCE.dir} && man2html ${SOURCE.file} ) > $TARGET",
454             Action(strip_to_first_html_tag),
455         ]
456         if tidy:
457             cmds.append("tidy -m -q $TARGET || true")
458         b = env.Command(html, man_1, cmds)
459         Local(html)
460         env.Depends(b, man_intermediate_files)
461
462         tar_deps.append(html)
463         tar_list.append(html)
464
465 if not epydoc:
466     print "epydoc not found, skipping building API documentation."
467 else:
468     # XXX Should be in common with reading the same thing in
469     # the SConstruct file.
470     e = os.path.join('#src', 'engine')
471     manifest_in = File(os.path.join(e, 'MANIFEST.in')).rstr()
472     sources = map(lambda x: x[:-1], open(manifest_in).readlines())
473     sources = filter(lambda x: string.find(x, 'Optik') == -1, sources)
474     sources = filter(lambda x: string.find(x, 'Platform') == -1, sources)
475     sources = filter(lambda x: string.find(x, 'Tool') == -1, sources)
476     # XXX
477     sources = filter(lambda x: string.find(x, 'Options') == -1, sources)
478
479     e = os.path.join(build, '..', 'scons', 'engine')
480     sources = map(lambda x, e=e: os.path.join(e, x), sources)
481
482     epydoc_commands = [
483         Delete('$OUTDIR'),
484         '$EPYDOC $EPYDOCFLAGS --debug --output $OUTDIR --docformat=restructuredText --name SCons --url http://www.scons.org/ $SOURCES',
485         Touch('$TARGET'),
486     ]
487
488     htmldir = os.path.join(build, 'HTML', 'scons-api')
489     env.Command('${OUTDIR}/index.html', sources, epydoc_commands,
490                 EPYDOC=epydoc, EPYDOCFLAGS='--html', OUTDIR=htmldir)
491     tar_deps.append(htmldir)
492     tar_list.append(htmldir)
493
494     # PDF and PostScript and TeX are built from the
495     # same invocation.
496     api_dir = os.path.join(build, 'scons-api')
497     api_pdf = os.path.join(api_dir, 'api.pdf')
498     api_ps = os.path.join(api_dir, 'api.ps')
499     api_tex = os.path.join(api_dir, 'api.tex')
500     api_targets = [api_pdf, api_ps, api_tex]
501     env.Command(api_targets, sources, epydoc_commands,
502                 EPYDOC=epydoc, EPYDOCFLAGS='--pdf', OUTDIR=api_dir)
503     Local(api_targets)
504
505     pdf_install = os.path.join(build, 'PDF', 'scons-api.pdf')
506     env.InstallAs(pdf_install, api_pdf)
507     tar_deps.append(pdf_install)
508     tar_list.append(pdf_install)
509     Local(pdf_install)
510
511     ps_install = os.path.join(build, 'PS', 'scons-api.ps')
512     env.InstallAs(ps_install, api_ps)
513     tar_deps.append(ps_install)
514     tar_list.append(ps_install)
515     Local(ps_install)
516
517 #
518 # Now actually create the tar file of the documentation,
519 # for easy distribution to the web site.
520 #
521 if tar_deps:
522     tar_list = string.join(map(lambda x, b=build+'/': string.replace(x, b, ''),
523                            tar_list))
524     t = env.Command(dist_doc_tar_gz, tar_deps,
525                 "tar cf${TAR_HFLAG} - -C %s %s | gzip > $TARGET" % (build, tar_list))
526     AddPostAction(dist_doc_tar_gz, Chmod(dist_doc_tar_gz, 0644))
527     Local(t)
528     Alias('doc', t)
529 else:
530     Alias('doc', os.path.join(build_dir, 'doc'))