[ci skip] multi-user should be multiuser
[scons.git] / doc / SConscript
bloba15c71e87fcdd9c71184fcdbf64ea2c3dc4b4d19
1 # MIT License
3 # Copyright 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
24 """Build SCons documentation."""
26 import glob
27 import os.path
28 import re
29 import shutil
30 import sys
31 import time
33 import SCons.Builder
34 import SCons.Util
36 Import('command_line', 'env', 'whereis', 'revaction')
38 # Check prerequisites and flags for building the documentation.  There are
39 # several combinations in play. Conceptually there are five builds:
40 # - manpage and user guide output in html (and manpage in roff-style "source")
41 # - manpage and user guide output in pdf
42 # - API docs with Sphinx output in html
43 # - API docs with Sphinx output in pdf
44 # - Bundle up the built bits into the tarball for upload to the website.
46 # These are sometimes a bit in tension. For example, we shouldn't need any
47 # doc bits to build the wheel for testing or uploading, except that the
48 # manpages (.1 format) are built and dropped into the top directory for
49 # use by distribution packagers - even though that's not really a suitable
50 # place for them.  And since we're often building the wheel to make a release,
51 # we actually may end up wanting the docs anyway.
53 # We want to be able to have some choice in combinations, so that for example
54 # there's a command to build just the manpages for distros without having
55 # to have the whole fop (for pdf) and Sphinx (for API docs) chains setup
56 # just to do a successful build, since those won't be part of those
57 # packages anyway.
59 skip_doc_build = False
60 skip_pdf_build = False
61 skip_api_build = False
63 # SKIP_DOC is a csv with various options. It doesn't seem necessary
64 # to do a very sophisticated decode of it, but could add that later.
65 skip_doc_args = ARGUMENTS.get('SKIP_DOC', 'none').split(',')
66 if 'none' not in skip_doc_args:
67     if 'all' in skip_doc_args:
68         skip_doc_build = skip_pdf_build = skip_api_build = True
69     if 'api' in skip_doc_args:
70         skip_api_build = True
71     if 'pdf' in skip_doc_args:
72         skip_pdf_build = True
74 if not skip_doc_build:
75     try:
76         import SConsDoc
77         import SConsExamples
78     except ImportError as exc:
79         print("doc: SConsDoc failed to import, the error was:")
80         print(f"         ImportError: {exc}")
81         print("     Please make sure the Python lxml package is installed.")
82         print("     Skipping documentation build.")
83         skip_doc_build = skip_pdf_build = skip_api_build = True
85 if not skip_pdf_build:
86     fop = whereis('fop')
87     xep = whereis('xep')
89     if not fop and not xep:
90         print("doc: No PDF renderer found (fop|xep)!")
91         print("     Skipping PDF generation.")
92         skip_pdf_build = True
94 if not skip_api_build:
95     ctx = env.Configure()
96     sphinx = ctx.CheckProg("sphinx-build")
97     if sphinx is None:
98         print("doc: Configure did not find sphinx-build")
99         print("     Skipping API docs generation.")
100         skip_api_build = True
101     ctx.Finish()
104 # --- Configure build
106 build = os.path.join(command_line.build_dir, 'doc')
107 gs = whereis('gs')
108 lynx = whereis('lynx')
109 dist_doc_tar_gz = '$DISTDIR/scons-doc-${VERSION}.tar.gz'
110 tar_deps = []
111 tar_list = []
113 orig_env = env
114 env = orig_env.Clone(SCONS_PY=File('#/scripts/scons.py').rfile())
117 # --- Helpers ---
119 def writeVersionXml(verfile, date, ver, rev, copyright_years):
120     """Helper function: Write a version.xml file."""
121     try:
122         os.unlink(verfile)
123     except OSError:
124         pass  # okay if the file didn't exist
125     dir, f = os.path.split(verfile)
126     os.makedirs(dir, exist_ok=True)
127     with open(verfile, "w") as vf:
128         vf.write(f"""\
129 <!--
130 THIS IS AN AUTOMATICALLY-GENERATED FILE.  DO NOT EDIT.
132 <!ENTITY builddate "{date}">
133 <!ENTITY buildversion "{ver}">
134 <!ENTITY buildrevision "{rev}">
135 <!ENTITY copyright_years "{copyright_years}">
136 """)
139 # The names of the target files for the MAN pages
140 man_page_list = ['scons.1', 'scons-time.1', 'sconsign.1']
142 # Template for the MAN page texts when we can't properly
143 # create them because the skip_doc_build flag is set (required
144 # modules/tools aren't installed in the current system)
145 man_replace_tpl = r""".TH "%(uctitle)s" "1" "%(today)s" "SCons %(version)s" "SCons %(version)s"
146 .ie \n(.g .ds Aq \(aq
147 .el       .ds Aq '
149 .ad l
150 .SH "NOTE"
151 %(title)s \- This is a replacement file, stemming from an incomplete
152 packaging process without the required doc modules installed. Please
153 update the system and try running the build again.
157 # --- Processing ---
159 if skip_doc_build:
160     print("doc: ...skipping building User Guide.")
161     print("     ...creating fake MAN pages.")
163     # Since the top-level SConstruct requires the MAN
164     # pages to exist for the basic packaging, we create simple
165     # stub texts here as replacement...
166     scdir = os.path.join(build, 'man')
167     if not os.path.isdir(scdir):
168         os.makedirs(scdir)
170     today = time.strftime(
171         "%Y-%m-%d", time.gmtime(int(os.environ.get('SOURCE_DATE_EPOCH', time.time())))
172     )
173     version = env.subst('$VERSION')
174     for m in man_page_list:
175         man, _ = os.path.splitext(m)
176         # TODO: add these to Alias?
177         with open(os.path.join(scdir, m), "w") as fman:
178             fman.write(
179                 man_replace_tpl
180                 % {
181                     'uctitle': man.upper().replace("-", "\\-"),
182                     'today': today,
183                     'title': man,
184                     'version': version,
185                 }
186             )
187 else:
188     if not lynx:
189         print(
190             "doc: Warning, lynx is not installed. "
191             "Created release packages will not be complete!"
192         )
194     # Always create a version.xml file containing the version information
195     # for this run.  Ignore it for dependency purposes so we don't
196     # rebuild all the docs every time just because the date changes.
197     # TODO: couldn't we use Textfile + Ignore?
198     date, ver, rev, copyright_years = env.Dictionary(
199         'DATE', 'VERSION', 'REVISION', 'COPYRIGHT_YEARS'
200     )
201     version_xml = File(os.path.join(build, "version.xml"))
202     writeVersionXml(str(version_xml), date, ver, rev, copyright_years)
204     def _glob_install_action(target, source, env):
205         """Builder for copying files to an Install dir.
207         Selection is based on globbing for filename extension.
208         """
209         if not SCons.Util.is_List(target):
210             target = [target]
211         if not SCons.Util.is_List(source):
212             source = [source]
213         for t, s in zip(target, source):
214             shutil.copy(str(s), str(t))
216     def _glob_install_emitter(target, source, env):
217         """Emitter for GlobInstall Builder."""
218         if not SCons.Util.is_List(target):
219             target = [target]
220         if not SCons.Util.is_List(source):
221             source = [source]
223         res = []
224         res_src = []
225         tdir = env.Dir(target[0])
226         for g in glob.glob(str(source[0])):
227             head, tail = os.path.split(g)
228             res.append(os.path.join(str(tdir), tail))
229             res_src.append(g)
230         return res, res_src
232     _glob_install_builder = SCons.Builder.Builder(
233         action=_glob_install_action, emitter=_glob_install_emitter
234     )
235     env['BUILDERS']['GlobInstall'] = _glob_install_builder
237     def _chunked_install_action(target, source, env):
238         """Builder for copying ChunkedHTML files to an Install dir."""
239         if not SCons.Util.is_List(target):
240             target = [target]
241         if not SCons.Util.is_List(source):
242             source = [source]
243         tdir, tail = os.path.split(str(target[0]))
244         spattern = os.path.join(os.path.split(str(source[0]))[0], '*.html')
245         for g in glob.glob(spattern):
246             shutil.copy(g, tdir)
248     def _chunked_install_emitter(target, source, env):
249         """Emitter for ChunkedInstall Builder."""
250         if not SCons.Util.is_List(target):
251             target = [target]
252         if not SCons.Util.is_List(source):
253             source = [source]
255         tdir = env.Dir(target[0])
256         head, tail = os.path.split(str(source[0]))
257         return os.path.join(str(tdir), tail), source
259     _chunked_install_builder = SCons.Builder.Builder(
260         action=_chunked_install_action, emitter=_chunked_install_emitter
261     )
262     env['BUILDERS']['ChunkedInstall'] = _chunked_install_builder
264     if not env.GetOption('clean'):
265         #
266         # Ensure that all XML files are valid against our XSD, and
267         # that all example names and example output suffixes are unique
268         #
269         print("Validating files against SCons XSD...")
270         if SConsDoc.validate_all_xml(['SCons'], xsdfile='xsd/scons.xsd'):
271             print("OK")
272         else:
273             print("Validation failed! Please correct the errors above and try again.")
274             sys.exit(1)
276         print("Checking whether all example names are unique...")
277         if SConsExamples.exampleNamesAreUnique(os.path.join('doc', 'user')):
278             print("OK")
279         else:
280             print(
281                 "Not all example names and suffixes are unique! "
282                 "Please correct the errors listed above and try again."
283             )
284             sys.exit(1)
286     # List of prerequisite files in the build/doc folder
287     buildsuite = []
289     def copy_dbfiles(env, toolpath, paths, fpattern, use_builddir=True):
290         """Helper function, copies a bunch of files matching
291         the given fpattern to a target directory.
292         """
293         global buildsuite
294         if not SCons.Util.is_List(toolpath):
295             toolpath = [toolpath]
296         if not SCons.Util.is_List(paths):
297             paths = [paths]
298         if not SCons.Util.is_List(fpattern):
299             fpattern = [fpattern]
301         if use_builddir:
302             target_dir = env.Dir(
303                 os.path.join(command_line.build_dir, *(toolpath + paths))
304             )
305             buildsuite.extend(
306                 env.GlobInstall(
307                     target_dir, os.path.join('..', *(toolpath + paths + fpattern))
308                 )
309             )
310         else:
311             target_dir = env.Dir(os.path.join(*(toolpath + paths)))
312             buildsuite.extend(
313                 env.GlobInstall(target_dir, os.path.join(*(paths + fpattern)))
314             )
316     #
317     # Copy generated files (.gen/.mod/.xml) to the build folder
318     #
319     copy_dbfiles(env, build, 'generated', '*.gen', False)
320     copy_dbfiles(env, build, 'generated', '*.mod', False)
321     copy_dbfiles(env, build, ['generated', 'examples'], '*', False)
323     #
324     # Copy XSLT files (.xslt) to the build folder
325     #
326     copy_dbfiles(env, build, 'xslt', '*.*', False)
328     #
329     # Copy DocBook stylesheets and Tool to the build folder
330     #
331     dbtoolpath = ['SCons', 'Tool', 'docbook']
332     copy_dbfiles(env, dbtoolpath, [], '__init__.py')
333     copy_dbfiles(env, dbtoolpath, 'utils', 'xmldepend.xsl')
334     dbpath = dbtoolpath + ['docbook-xsl-1.76.1']
335     copy_dbfiles(env, dbpath, [], 'VERSION')
336     copy_dbfiles(env, dbpath, ['common'], '*.*')
337     copy_dbfiles(env, dbpath, ['lib'], '*.*')
338     copy_dbfiles(env, dbpath, ['html'], '*.*')
339     copy_dbfiles(env, dbpath, ['fo'], '*.*')
340     copy_dbfiles(env, dbpath, ['manpages'], '*.*')
341     copy_dbfiles(env, dbpath, ['epub'], '*.xsl')
342     copy_dbfiles(env, dbpath, ['xhtml-1_1'], '*.*')
344     #
345     # Copy additional Tools (gs, zip)
346     #
347     toolpath = ['SCons', 'Tool']
348     copy_dbfiles(env, toolpath, [], 'gs.py')
349     copy_dbfiles(env, toolpath, [], 'zip.py')
351     # Each document will build in its own subdirectory of "build/doc/".
352     # The *docs* dictionary entries have the document (and thus directory)
353     # name as the key, and a tuple of lists as the value.
354     #
355     # The first list is the document formats enabled ("targets").  Note this
356     # isn't what gets built, but what gets installed into the build folder
357     # source/target lists.
358     #
359     # The second list ("depends") is for dependency information. Dependencies
360     # are extracted from each local "MANIFEST" and added to this list.
361     # This basically links the original sources to the respective build folder.
362     #
363     # The third list ("nodes") stores the created PDF and HTML files,
364     # so that we can  then install them in the proper places for getting
365     # picked up by the archiving/packaging stages.
366     #
367     docs = {
368         # 'design': (['chunked', 'pdf'], [], []),
369         # 'python10' : (['chunked','html','pdf'], [], []),
370         # 'reference': (['chunked', 'html', 'pdf'], [], []),
371         # 'developer' : (['chunked','html','pdf'], [], []),
372         'user': (['chunked', 'html', 'pdf', 'epub', 'text'], [], []),
373         'man': (['man', 'epub', 'text'], [], []),
374     }
376     #
377     # We have to tell SCons to scan the top-level XML files which
378     # get included by the document XML files in the subdirectories.
379     #
381     def _parse_manifest_lines(basedir, manifest) -> list:
382         """
383         Scans a MANIFEST file, and returns the list of source files.
385         Has basic support for recursive globs '**',
386         filename wildcards of the form '*.xml' and
387         comment lines, starting with a '#'.
389         Args:
390            basedir: base path to find files in. Note this does not
391               run in an SCons context so path must not need
392               further processing (e.g. no '#' signs)
393            manifest: path to manifest file
394         """
395         sources = []
396         basewd = os.path.abspath(basedir)
397         with open(manifest) as m:
398             lines = m.readlines()
399         for l in lines:
400             if l.startswith('#'):
401                 # Skip comments
402                 continue
403             l = l.rstrip('\n')
404             if l.endswith('**'):
405                 # Glob all files recursively
406                 globwd = os.path.dirname(os.path.join(basewd, l))
407                 for path, dirs, files in os.walk(globwd):
408                     for f in files:
409                         fpath = os.path.join(globwd, path, f)
410                         sources.append(os.path.relpath(fpath, basewd))
411             elif '*' in l:
412                 # Glob file pattern
413                 files = glob.glob(os.path.join(basewd, l))
414                 for f in files:
415                     sources.append(os.path.relpath(f, basewd))
416             else:
417                 sources.append(l)
419         return sources
422     manifest = File('MANIFEST').rstr()
423     src_files = _parse_manifest_lines('.', manifest)
424     for s in src_files:
425         if not s:
426             continue
427         base, ext = os.path.splitext(s)
428         if ext in ['.fig', '.jpg']:
429             buildsuite.extend(
430                 env.Command(os.path.join(build, s), s, Copy("$TARGET", "$SOURCE"))
431             )
432         else:
433             revaction([env.File(os.path.join(build, s))], [env.File(s)], env)
435     for doc, (targets, depends, nodes) in docs.items():
436         # Read MANIFEST file and copy the listed files to the build directory,
437         # while branding them with the SCons copyright and the current
438         # revision number...
439         if not os.path.exists(os.path.join(build, doc)):
440             env.Execute(Mkdir(os.path.join(build, doc)))
441         if not os.path.exists(os.path.join(build, doc, 'titlepage')):
442             env.Execute(Mkdir(os.path.join(build, doc, 'titlepage')))
443         manifest = File(os.path.join(doc, 'MANIFEST')).rstr()
444         src_files = _parse_manifest_lines(doc, manifest)
445         for s in src_files:
446             if not s:
447                 continue
448             doc_s = os.path.join(doc, s)
449             build_s = os.path.join(build, doc, s)
450             base, ext = os.path.splitext(doc_s)
451             head, tail = os.path.split(s)
452             if head:
453                 target_dir = os.path.join(build, doc, head)
454             else:
455                 target_dir = os.path.join(build, doc)
456             if ext in ['.fig', '.jpg', '.svg']:
457                 depends.extend(
458                     env.Command(build_s, doc_s, Copy("$TARGET", "$SOURCE"))
459                 )
460             else:
461                 btarget = env.File(build_s)
462                 depends.append(btarget)
463                 revaction([btarget], [env.File(doc_s)], env)
465     # For each document, add targets for each of the selected formats
466     for doc, (targets, depends, nodes) in docs.items():
467         # Call SCons in each local doc folder
468         cleanopt = ''
469         if env.GetOption('clean'):
470             cleanopt = ' -c'
471         scdir = os.path.join(build, doc)
472         sctargets = []
473         if 'html' in targets:
474             sctargets.append(env.File(os.path.join(scdir, 'index.html')))
475         if 'chunked' in targets:
476             sctargets.append(
477                 env.File(os.path.join(scdir, f'scons-{doc}', 'index.html'))
478             )
479         if 'pdf' in targets and not skip_pdf_build:
480             sctargets.append(env.File(os.path.join(scdir, f'scons-{doc}.pdf')))
481         if 'epub' in targets:
482             sctargets.append(env.File(os.path.join(scdir, f'scons-{doc}.epub')))
484         if 'man' in targets:
485             for m in man_page_list:
486                 # TODO: add targets to an alias?
487                 sctargets.append(os.path.join(scdir, m))
488                 man, _1 = os.path.splitext(m)
489                 if not skip_pdf_build:
490                     sctargets.append(os.path.join(scdir, f'scons-{man}.pdf'))
491                 sctargets.append(os.path.join(scdir, f'scons-{man}.html'))
493         # pass on the information to skip PDF/EPUB when calling man/guide SConstruct
494         skip_str = "SKIP_PDF=1" if skip_pdf_build else ""
495         nodes.extend(
496             env.Command(
497                 target=sctargets,
498                 source=buildsuite + depends,
499                 action="cd %s && $PYTHON ${SCONS_PY.abspath}%s %s" % (scdir, cleanopt, skip_str),
500             )
501         )
503     install_css = False
504     for doc, (targets, depends, nodes) in docs.items():
505         # Collect the output files for this subfolder
506         htmldir = os.path.join(build, 'HTML', f'scons-{doc}')
507         htmlindex = os.path.join(htmldir, 'index.html')
508         html = os.path.join(build, 'HTML', f'scons-{doc}.html')
509         pdf = os.path.join(build, 'PDF', f'scons-{doc}.pdf')
510         epub = os.path.join(build, 'EPUB', f'scons-{doc}.epub')
511         text = os.path.join(build, 'TEXT', f'scons-{doc}.txt')
512         if 'chunked' in targets:
513             installed_chtml = env.ChunkedInstall(
514                 env.Dir(htmldir),
515                 os.path.join(build, doc, f'scons-{doc}', 'index.html'),
516             )
517             installed_chtml_css = env.Install(
518                 env.Dir(htmldir), os.path.join(build, doc, 'scons.css')
519             )
520             env.Depends(installed_chtml, nodes)
521             env.Depends(installed_chtml_css, nodes)
523             tar_deps.extend([htmlindex, installed_chtml_css])
524             tar_list.extend([htmldir])
525             Local(htmlindex)
526             env.Ignore(htmlindex, version_xml)
528         if 'html' in targets:
529             env.InstallAs(
530                 target=env.File(html),
531                 source=env.File(os.path.join(build, doc, 'index.html')),
532             )
533             tar_deps.append(html)
534             tar_list.append(html)
535             Local(html)
536             env.Ignore(html, version_xml)
537             install_css = True
539         if 'pdf' in targets and not skip_pdf_build:
540             env.InstallAs(
541                 target=env.File(pdf),
542                 source=env.File(os.path.join(build, doc, f'scons-{doc}.pdf')),
543             )
544             Local(pdf)
545             env.Ignore(pdf, version_xml)
547             tar_deps.append(pdf)
548             tar_list.append(pdf)
550         if 'epub' in targets and not skip_pdf_build and gs:
551             env.InstallAs(
552                 target=env.File(epub),
553                 source=env.File(os.path.join(build, doc, f'scons-{doc}.epub')),
554             )
555             Local(epub)
556             env.Ignore(epub, version_xml)
558             tar_deps.append(epub)
559             tar_list.append(epub)
561         if (
562             'text' in targets
563             and lynx
564             and ('html' in targets or doc == 'man')
565         ):
566             texthtml = os.path.join(build, doc, 'index.html')
567             if doc == 'man':
568                 # Special handling for single MAN file
569                 texthtml = os.path.join(build, doc, 'scons-scons.html')
571             env.Command(
572                 target=text,
573                 source=env.File(texthtml),
574                 action="lynx -dump ${SOURCE.abspath} > $TARGET",
575             )
576             Local(text)
578             env.Ignore(text, version_xml)
580             tar_deps.append(text)
581             tar_list.append(text)
583         if 'man' in targets:
584             for m in man_page_list:
585                 man, _1 = os.path.splitext(m)
587                 pdf = os.path.join(build, 'PDF', f'{man}-man.pdf')
588                 html = os.path.join(build, 'HTML', f'{man}-man.html')
590                 if not skip_pdf_build:
591                     env.InstallAs(
592                         target=env.File(pdf),
593                         source=env.File(os.path.join(build, 'man', f'scons-{man}.pdf')),
594                     )
595                 env.InstallAs(
596                     target=env.File(html),
597                     source=env.File(os.path.join(build, 'man', f'scons-{man}.html')),
598                 )
600                 tar_deps.append(html)
601                 tar_list.append(html)
602                 if not skip_pdf_build:
603                     tar_deps.append(pdf)
604                     tar_list.append(pdf)
606     # Install CSS file, common to all single HTMLs
607     if install_css:
608         css_file = os.path.join(build, 'HTML', 'scons.css')
609         env.InstallAs(
610             target=env.File(css_file),
611             source=env.File(os.path.join(build, 'user', 'scons.css')),
612         )
613         tar_deps.append(css_file)
614         tar_list.append(css_file)
615         Local(css_file)
617 if skip_api_build:
618     Alias("apidoc")
619 else:
620     # Build API DOCS
621     # TODO: Better specify dependencies on source files
622     if not skip_pdf_build:
623         pdf_file = env.Command(
624             target='#/build/doc/api/scons-api.pdf',
625             source=env.Glob('#/SCons/*'),
626             action=[Delete("#/build/doc/api"), "cd doc && make pdf"],
627         )
628         pdf_install = os.path.join(build, 'PDF', 'scons-api.pdf')
629         env.InstallAs(target=pdf_install, source=pdf_file)
630         tar_deps.append(pdf_install)
631         tar_list.append(pdf_install)
632         Alias('apidoc', pdf_file)
634     htmldir = os.path.join(build, 'HTML', 'scons-api')
635     html_files = env.Command(
636         target='#/build/doc/HTML/scons-api/index.html',
637         source=env.Glob('#/SCons/*'),
638         action="cd doc && make dirhtml BUILDDIR=${HTMLDIR}",
639         HTMLDIR=htmldir,
640     )
641     tar_deps.append(htmldir)
642     tar_list.append(htmldir)
643     Alias('apidoc', html_files)
646 # Now actually create the tar file of the documentation,
647 # for easy distribution to the web site.
649 if tar_deps:
650     tar_list = ' '.join([x.replace(build + '/', '') for x in tar_list])
651     t = env.Command(
652         target=dist_doc_tar_gz,
653         source=tar_deps,
654         action="tar cf${TAR_HFLAG} - -C %s %s | gzip > $TARGET" % (build, tar_list),
655     )
656     AddPostAction(dist_doc_tar_gz, Chmod(dist_doc_tar_gz, 0o644))
657     Local(t)
658     Alias('doc', t)
659 else:
660     Alias('doc', os.path.join(command_line.build_dir, 'doc'))