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."""
36 Import('command_line', 'env', 'whereis', 'revaction')
39 # -- Check prerequisites for building the documentation ---
46 except ImportError as exc:
47 print("doc: SConsDoc failed to import, the error was:")
48 print(f" ImportError: {exc}")
49 print(" Please make sure that python-lxml is installed.")
55 if not fop and not xep:
56 print("doc: No PDF renderer found (fop|xep)!")
59 skip_doc_arg = ARGUMENTS.get('SKIP_DOC')
60 if skip_doc_arg is not None:
61 skip_doc = skip_doc_arg in ['True', '1', 'true']
68 build = os.path.join(command_line.build_dir, 'doc')
71 lynx = whereis('lynx')
73 dist_doc_tar_gz = '$DISTDIR/scons-doc-${VERSION}.tar.gz'
79 env = orig_env.Clone(SCONS_PY=File('#/scripts/scons.py').rfile())
84 def writeVersionXml(verfile, date, ver, rev, copyright_years):
85 """Helper function: Write a version.xml file."""
89 pass # okay if the file didn't exist
90 dir, f = os.path.split(verfile)
91 os.makedirs(dir, exist_ok=True)
92 with open(verfile, "w") as vf:
95 THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT.
97 <!ENTITY builddate "{date}">
98 <!ENTITY buildversion "{ver}">
99 <!ENTITY buildrevision "{rev}">
100 <!ENTITY copyright_years "{copyright_years}">
104 # The names of the target files for the MAN pages
105 man_page_list = ['scons.1', 'scons-time.1', 'sconsign.1']
107 # Template for the MAN page texts when we can't properly
108 # create them because the skip_doc flag is set (required
109 # modules/tools aren't installed in the current system)
110 man_replace_tpl = r""".TH "%(uctitle)s" "1" "%(today)s" "SCons %(version)s" "SCons %(version)s"
111 .ie \n(.g .ds Aq \(aq
116 %(title)s \- This is a replacement file, stemming from an incomplete
117 packaging process without the required doc modules installed. Please
118 update the system and try running the build again.
125 print("doc: ...skipping building User Guide.")
126 print(" ...creating fake MAN pages.")
128 # Since the top-level SConstruct requires the MAN
129 # pages to exist for the basic packaging, we create simple
130 # stub texts here as replacement...
131 scdir = os.path.join(build, 'man')
132 if not os.path.isdir(scdir):
135 today = time.strftime(
136 "%Y-%m-%d", time.gmtime(int(os.environ.get('SOURCE_DATE_EPOCH', time.time())))
138 version = env.subst('$VERSION')
139 for m in man_page_list:
140 man, _ = os.path.splitext(m)
141 with open(os.path.join(scdir, m), "w") as fman:
145 'uctitle': man.upper().replace("-", "\\-"),
154 "doc: Warning, lynx is not installed. "
155 "Created release packages will not be complete!"
158 # Always create a version.xml file containing the version information
159 # for this run. Ignore it for dependency purposes so we don't
160 # rebuild all the docs every time just because the date changes.
161 date, ver, rev, copyright_years = env.Dictionary(
162 'DATE', 'VERSION', 'REVISION', 'COPYRIGHT_YEARS'
164 version_xml = File(os.path.join(build, "version.xml"))
165 writeVersionXml(str(version_xml), date, ver, rev, copyright_years)
167 def _glob_install_action(target, source, env):
168 """Builder for copying files to an Install dir.
170 Selection is based on globbing for filename extension.
172 if not SCons.Util.is_List(target):
174 if not SCons.Util.is_List(source):
176 for t, s in zip(target, source):
177 shutil.copy(str(s), str(t))
179 def _glob_install_emitter(target, source, env):
180 """Emitter for GlobInstall Builder."""
181 if not SCons.Util.is_List(target):
183 if not SCons.Util.is_List(source):
188 tdir = env.Dir(target[0])
189 for g in glob.glob(str(source[0])):
190 head, tail = os.path.split(g)
191 res.append(os.path.join(str(tdir), tail))
195 _glob_install_builder = SCons.Builder.Builder(
196 action=_glob_install_action, emitter=_glob_install_emitter
198 env['BUILDERS']['GlobInstall'] = _glob_install_builder
200 def _chunked_install_action(target, source, env):
201 """Builder for copying ChunkedHTML files to an Install dir."""
202 if not SCons.Util.is_List(target):
204 if not SCons.Util.is_List(source):
206 tdir, tail = os.path.split(str(target[0]))
207 spattern = os.path.join(os.path.split(str(source[0]))[0], '*.html')
208 for g in glob.glob(spattern):
211 def _chunked_install_emitter(target, source, env):
212 """Emitter for ChunkedInstall Builder."""
213 if not SCons.Util.is_List(target):
215 if not SCons.Util.is_List(source):
218 tdir = env.Dir(target[0])
219 head, tail = os.path.split(str(source[0]))
220 return os.path.join(str(tdir), tail), source
222 _chunked_install_builder = SCons.Builder.Builder(
223 action=_chunked_install_action, emitter=_chunked_install_emitter
225 env['BUILDERS']['ChunkedInstall'] = _chunked_install_builder
227 if not env.GetOption('clean'):
229 # Ensure that all XML files are valid against our XSD, and
230 # that all example names and example output suffixes are unique
232 print("Validating files against SCons XSD...")
233 if SConsDoc.validate_all_xml(['SCons'], xsdfile='xsd/scons.xsd'):
236 print("Validation failed! Please correct the errors above and try again.")
239 print("Checking whether all example names are unique...")
240 if SConsExamples.exampleNamesAreUnique(os.path.join('doc', 'user')):
244 "Not all example names and suffixes are unique! "
245 "Please correct the errors listed above and try again."
249 # List of prerequisite files in the build/doc folder
252 def copy_dbfiles(env, toolpath, paths, fpattern, use_builddir=True):
253 """Helper function, copies a bunch of files matching
254 the given fpattern to a target directory.
257 if not SCons.Util.is_List(toolpath):
258 toolpath = [toolpath]
259 if not SCons.Util.is_List(paths):
261 if not SCons.Util.is_List(fpattern):
262 fpattern = [fpattern]
265 target_dir = env.Dir(
266 os.path.join(command_line.build_dir, *(toolpath + paths))
270 target_dir, os.path.join('..', *(toolpath + paths + fpattern))
274 target_dir = env.Dir(os.path.join(*(toolpath + paths)))
276 env.GlobInstall(target_dir, os.path.join(*(paths + fpattern)))
280 # Copy generated files (.gen/.mod/.xml) to the build folder
282 copy_dbfiles(env, build, 'generated', '*.gen', False)
283 copy_dbfiles(env, build, 'generated', '*.mod', False)
284 copy_dbfiles(env, build, ['generated', 'examples'], '*', False)
287 # Copy XSLT files (.xslt) to the build folder
289 copy_dbfiles(env, build, 'xslt', '*.*', False)
292 # Copy DocBook stylesheets and Tool to the build folder
294 dbtoolpath = ['SCons', 'Tool', 'docbook']
295 copy_dbfiles(env, dbtoolpath, [], '__init__.py')
296 copy_dbfiles(env, dbtoolpath, 'utils', 'xmldepend.xsl')
297 dbpath = dbtoolpath + ['docbook-xsl-1.76.1']
298 copy_dbfiles(env, dbpath, [], 'VERSION')
299 copy_dbfiles(env, dbpath, ['common'], '*.*')
300 copy_dbfiles(env, dbpath, ['lib'], '*.*')
301 copy_dbfiles(env, dbpath, ['html'], '*.*')
302 copy_dbfiles(env, dbpath, ['fo'], '*.*')
303 copy_dbfiles(env, dbpath, ['manpages'], '*.*')
304 copy_dbfiles(env, dbpath, ['epub'], '*.xsl')
305 copy_dbfiles(env, dbpath, ['xhtml-1_1'], '*.*')
308 # Copy additional Tools (gs, zip)
310 toolpath = ['SCons', 'Tool']
311 copy_dbfiles(env, toolpath, [], 'gs.py')
312 copy_dbfiles(env, toolpath, [], 'zip.py')
314 # Each document will live in its own subdirectory "build/doc/xxx".
315 # List them here by their subfolder names. Note how the specifiers
316 # for each subdir (=DOCTARGETS) have nothing to do with which
317 # formats get created, but which of the outputs get installed
318 # to the build folder and added to the different source and binary
319 # packages in the end.
321 # In addition to the list of target formats (DOCTARGETS), we also
322 # store some dependency information in this dict. The DOCDEPENDS
323 # list contains all files from each local "MANIFEST", after
324 # installing/copying them to the build directory. It basically
325 # links the original sources to the respective build folder,
326 # such that a simple 'python bootstrap.py' rebuilds the
327 # documentation when a file, like 'doc/user/depends.xml'
328 # for example, changes.
330 # Finally, in DOCNODES we store the created PDF and HTML files,
331 # such that we can then install them in the proper places for
332 # getting picked up by the archiving/packaging stages.
337 # 'design': (['chunked', 'pdf'], [], []),
338 # 'python10' : (['chunked','html','pdf'], [], []),
339 # 'reference': (['chunked', 'html', 'pdf'], [], []),
340 # 'developer' : (['chunked','html','pdf'], [], []),
341 'user': (['chunked', 'html', 'pdf', 'epub', 'text'], [], []),
342 'man': (['man', 'epub', 'text'], [], []),
346 # We have to tell SCons to scan the top-level XML files which
347 # get included by the document XML files in the subdirectories.
350 def _parse_manifest_lines(basedir, manifest) -> list:
352 Scans a MANIFEST file, and returns the list of source files.
354 Has basic support for recursive globs '**',
355 filename wildcards of the form '*.xml' and
356 comment lines, starting with a '#'.
359 basedir: base path to find files in. Note this does not
360 run in an SCons context so path must not need
361 further processing (e.g. no '#' signs)
362 manifest: path to manifest file
365 basewd = os.path.abspath(basedir)
366 with open(manifest) as m:
367 lines = m.readlines()
369 if l.startswith('#'):
374 # Glob all files recursively
375 globwd = os.path.dirname(os.path.join(basewd, l))
376 for path, dirs, files in os.walk(globwd):
378 fpath = os.path.join(globwd, path, f)
379 sources.append(os.path.relpath(fpath, basewd))
382 files = glob.glob(os.path.join(basewd, l))
384 sources.append(os.path.relpath(f, basewd))
391 manifest = File('MANIFEST').rstr()
392 src_files = _parse_manifest_lines('.', manifest)
396 base, ext = os.path.splitext(s)
397 if ext in ['.fig', '.jpg']:
399 env.Command(os.path.join(build, s), s, Copy("$TARGET", "$SOURCE"))
402 revaction([env.File(os.path.join(build, s))], [env.File(s)], env)
405 # Read MANIFEST file and copy the listed files to the build directory,
406 # while branding them with the SCons copyright and the current
408 if not os.path.exists(os.path.join(build, doc)):
409 env.Execute(Mkdir(os.path.join(build, doc)))
410 if not os.path.exists(os.path.join(build, doc, 'titlepage')):
411 env.Execute(Mkdir(os.path.join(build, doc, 'titlepage')))
412 manifest = File(os.path.join(doc, 'MANIFEST')).rstr()
413 src_files = _parse_manifest_lines(doc, manifest)
417 doc_s = os.path.join(doc, s)
418 build_s = os.path.join(build, doc, s)
419 base, ext = os.path.splitext(doc_s)
420 head, tail = os.path.split(s)
422 target_dir = os.path.join(build, doc, head)
424 target_dir = os.path.join(build, doc)
425 if ext in ['.fig', '.jpg', '.svg']:
426 docs[doc][DOCDEPENDS].extend(
427 env.Command(build_s, doc_s, Copy("$TARGET", "$SOURCE"))
430 btarget = env.File(build_s)
431 docs[doc][DOCDEPENDS].append(btarget)
432 revaction([btarget], [env.File(doc_s)], env)
435 # For each document, build the document itself in HTML,
441 # Call SCons in each local doc folder
444 if env.GetOption('clean'):
446 scdir = os.path.join(build, doc)
448 if 'html' in docs[doc][DOCTARGETS]:
449 sctargets.append(env.File(os.path.join(scdir, 'index.html')))
450 if 'chunked' in docs[doc][DOCTARGETS]:
452 env.File(os.path.join(scdir, f'scons-{doc}', 'index.html'))
454 if 'pdf' in docs[doc][DOCTARGETS]:
455 sctargets.append(env.File(os.path.join(scdir, f'scons-{doc}.pdf')))
456 if 'epub' in docs[doc][DOCTARGETS]:
457 sctargets.append(env.File(os.path.join(scdir, f'scons-{doc}.epub')))
459 if 'man' in docs[doc][DOCTARGETS]:
460 for m in man_page_list:
461 sctargets.append(os.path.join(scdir, m))
462 man, _1 = os.path.splitext(m)
464 sctargets.append(os.path.join(scdir, f'scons-{man}.pdf'))
465 sctargets.append(os.path.join(scdir, f'scons-{man}.html'))
467 docs[doc][DOCNODES].extend(
470 source=buildsuite + docs[doc][DOCDEPENDS],
471 action="cd %s && $PYTHON ${SCONS_PY.abspath}%s" % (scdir, cleanopt),
478 # Collect the output files for this subfolder
479 htmldir = os.path.join(build, 'HTML', f'scons-{doc}')
480 htmlindex = os.path.join(htmldir, 'index.html')
481 html = os.path.join(build, 'HTML', f'scons-{doc}.html')
482 pdf = os.path.join(build, 'PDF', f'scons-{doc}.pdf')
483 epub = os.path.join(build, 'EPUB', f'scons-{doc}.epub')
484 text = os.path.join(build, 'TEXT', f'scons-{doc}.txt')
485 if 'chunked' in docs[doc][DOCTARGETS]:
486 installed_chtml = env.ChunkedInstall(
488 os.path.join(build, doc, f'scons-{doc}', 'index.html'),
490 installed_chtml_css = env.Install(
491 env.Dir(htmldir), os.path.join(build, doc, 'scons.css')
493 env.Depends(installed_chtml, docs[doc][DOCNODES])
494 env.Depends(installed_chtml_css, docs[doc][DOCNODES])
496 tar_deps.extend([htmlindex, installed_chtml_css])
497 tar_list.extend([htmldir])
499 env.Ignore(htmlindex, version_xml)
501 if 'html' in docs[doc][DOCTARGETS]:
503 target=env.File(html),
504 source=env.File(os.path.join(build, doc, 'index.html')),
506 tar_deps.extend([html])
507 tar_list.extend([html])
509 env.Ignore(html, version_xml)
512 if 'pdf' in docs[doc][DOCTARGETS]:
514 target=env.File(pdf),
515 source=env.File(os.path.join(build, doc, f'scons-{doc}.pdf')),
518 env.Ignore(pdf, version_xml)
523 if 'epub' in docs[doc][DOCTARGETS] and gs:
525 target=env.File(epub),
526 source=env.File(os.path.join(build, doc, f'scons-{doc}.epub')),
529 env.Ignore(epub, version_xml)
531 tar_deps.append(epub)
532 tar_list.append(epub)
535 'text' in docs[doc][DOCTARGETS]
537 and (('html' in docs[doc][DOCTARGETS]) or (doc == 'man'))
539 texthtml = os.path.join(build, doc, 'index.html')
541 # Special handling for single MAN file
542 texthtml = os.path.join(build, doc, 'scons-scons.html')
546 source=env.File(texthtml),
547 action="lynx -dump ${SOURCE.abspath} > $TARGET",
551 env.Ignore(text, version_xml)
553 tar_deps.append(text)
554 tar_list.append(text)
556 if 'man' in docs[doc][DOCTARGETS]:
557 for m in man_page_list:
558 man, _1 = os.path.splitext(m)
560 pdf = os.path.join(build, 'PDF', f'{man}-man.pdf')
561 html = os.path.join(build, 'HTML', f'{man}-man.html')
564 target=env.File(pdf),
565 source=env.File(os.path.join(build, 'man', f'scons-{man}.pdf')),
568 target=env.File(html),
569 source=env.File(os.path.join(build, 'man', f'scons-{man}.html')),
572 tar_deps.extend([pdf, html])
573 tar_list.extend([pdf, html])
575 # Install CSS file, common to all single HTMLs
577 css_file = os.path.join(build, 'HTML', 'scons.css')
579 target=env.File(css_file),
580 source=env.File(os.path.join(build, 'user', 'scons.css')),
582 tar_deps.extend([css_file])
583 tar_list.extend([css_file])
588 # TODO: Better specify dependencies on source files
589 pdf_file = env.Command(
590 target='#/build/doc/api/scons-api.pdf',
591 source=env.Glob('#/SCons/*'),
592 action=[Delete("#/build/doc/api"), "cd doc && make pdf"],
594 pdf_install = os.path.join(build, 'PDF', 'scons-api.pdf')
595 env.InstallAs(target=pdf_install, source=pdf_file)
596 tar_deps.append(pdf_install)
597 tar_list.append(pdf_install)
599 htmldir = os.path.join(build, 'HTML', 'scons-api')
600 html_files = env.Command(
601 target='#/build/doc/HTML/scons-api/index.html',
602 source=env.Glob('#/SCons/*'),
603 action="cd doc && make dirhtml BUILDDIR=${HTMLDIR}",
606 tar_deps.append(htmldir)
607 tar_list.append(htmldir)
610 # Now actually create the tar file of the documentation,
611 # for easy distribution to the web site.
614 tar_list = ' '.join([x.replace(build + '/', '') for x in tar_list])
616 target=dist_doc_tar_gz,
618 action="tar cf${TAR_HFLAG} - -C %s %s | gzip > $TARGET" % (build, tar_list),
620 AddPostAction(dist_doc_tar_gz, Chmod(dist_doc_tar_gz, 0o644))
624 Alias('doc', os.path.join(command_line.build_dir, 'doc'))