593 lines
20 KiB
Python
593 lines
20 KiB
Python
"""SCons.Tool.spinx4scons
|
|
|
|
Tool-specific initialization for the Sphinx document build system.
|
|
|
|
There normally shouldn't be any need to import this module directly.
|
|
It will usually be imported through the generic SCons.Tool.Tool()
|
|
selection method.
|
|
|
|
It should be placed in e.g. ~/site_scons/site_tools/sphinx4scons/
|
|
directory. Then it may be loaded by placing
|
|
|
|
sphinx = Tool('sphinx4scons')
|
|
sphinx(env)
|
|
|
|
in your SConstruct file.
|
|
|
|
For further details, please see the SCons documentation on how to
|
|
install and enable custom tools.
|
|
"""
|
|
|
|
#
|
|
# This package is provided under the Expat license
|
|
#
|
|
# Copyright (c) 2012 Orlando Wingbrant
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining
|
|
# a copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish,
|
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
# permit persons to whom the Software is furnished to do so, subject to
|
|
# the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included
|
|
# in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
#
|
|
|
|
__author__ = "Orlando Wingbrant"
|
|
__email__ = "orlando@widesite.org"
|
|
__url__ = "https://bitbucket.org/wingbrant/sphinx4scons"
|
|
__license__ = "Expat license"
|
|
|
|
import SCons.Action
|
|
import SCons.Builder
|
|
import SCons.Defaults
|
|
import SCons.Util
|
|
import SCons.Node.FS
|
|
import os
|
|
|
|
from sphinx.util.matching import patfilter, compile_matchers
|
|
from sphinx.util.osutil import make_filename
|
|
|
|
|
|
class ToolSphinxWarning(SCons.Warnings.Warning):
|
|
pass
|
|
|
|
|
|
class SphinxBuilderNotFound(ToolSphinxWarning):
|
|
pass
|
|
|
|
SCons.Warnings.enableWarningClass(ToolSphinxWarning)
|
|
|
|
|
|
def exists(env):
|
|
return _detect(env)
|
|
|
|
|
|
def _detect(env):
|
|
"""Try to detect the sphinx-build script."""
|
|
try:
|
|
return env['SPHINXBUILD']
|
|
except KeyError:
|
|
pass
|
|
|
|
sphinx = env.WhereIs('sphinx-build')
|
|
if sphinx:
|
|
return sphinx
|
|
|
|
raise SCons.Errors.StopError(
|
|
SphinxBuilderNotFound,
|
|
"Could not detect sphinx-build script")
|
|
return None
|
|
|
|
|
|
def generate(env):
|
|
"""Add Builders and construction variables to the Environment."""
|
|
|
|
env['SPHINXBUILD'] = _detect(env)
|
|
sphinx = _create_sphinx_builder(env)
|
|
|
|
env.SetDefault(
|
|
# Additional command-line flags
|
|
SPHINXFLAGS = '',
|
|
|
|
# Tag definitions, each entry will appear on the command line preceded by -t
|
|
SPHINXTAGS = [],
|
|
|
|
# Directory for doctrees
|
|
SPHINXDOCTREE = '',
|
|
|
|
# Path to sphinx configuration file
|
|
SPHINXCONFIG = '',
|
|
|
|
# Config file override settings, each entry will be preceded by -D
|
|
SPHINXSETTINGS = {},
|
|
|
|
# Default sphinx builder,
|
|
SPHINXBUILDER = 'html',
|
|
|
|
# Sphinx command
|
|
SPHINXCOM = "$SPHINXBUILD $_SPHINXOPTIONS ${SOURCE.attributes.root} ${TARGET.attributes.root}",
|
|
|
|
# Alternate console output when building sphinx documents
|
|
SPHINXCOMSTR = ""
|
|
)
|
|
|
|
try:
|
|
env.AddMethod(Sphinx, "Sphinx")
|
|
except AttributeError:
|
|
# Looks like we use a pre-0.98 version of SCons...
|
|
from SCons.Script.SConscript import SConsEnvironment
|
|
SConsEnvironment.Sphinx = Sphinx
|
|
|
|
|
|
def Sphinx(env, target, source, **kw):
|
|
"""A pseudo-builder wrapper for the sphinx builder."""
|
|
builder = env['BUILDERS']['Sphinx4Scons']
|
|
env_kw = env.Override(kw)
|
|
options = _get_sphinxoptions(env_kw, target, source)
|
|
output = builder(env, target, source, _SPHINXOPTIONS=options, **kw)
|
|
return output
|
|
|
|
|
|
def _get_sphinxoptions(env, target, source):
|
|
"""Concatenates all the options for the sphinx command line."""
|
|
options = []
|
|
|
|
builder = _get_sphinxbuilder(env)
|
|
options.append("-b %s" % env.subst(builder, target=target, source=source))
|
|
|
|
flags = env.get('options', env.get('SPHINXFLAGS', ''))
|
|
options.append(env.subst(flags, target=target, source=source))
|
|
|
|
tags = env.get('tags', env.get('SPHINXTAGS', None))
|
|
if tags is not None:
|
|
if not SCons.SCons.Util.is_List(tags):
|
|
tags = [tags]
|
|
for tag in tags:
|
|
if tag != '':
|
|
tag = env.subst(tag, target=target, source=source)
|
|
options.append("-t %s" % tag)
|
|
|
|
settings = env.get('settings', env.get('SPHINXSETTINGS', None))
|
|
if settings is not None:
|
|
if not SCons.SCons.Util.is_Dict(settings):
|
|
raise TypeError('SPHINXSETTINGS and/or settings argument must be a dictionary')
|
|
for key, value in settings.iteritems():
|
|
if value != '':
|
|
value = env.subst(value, target=target, source=source)
|
|
options.append('-D "%s=%s"' % (key, value))
|
|
|
|
doctree = env.get('doctree', env.get("SPHINXDOCTREE", None))
|
|
if isinstance(doctree, SCons.Node.FS.Dir):
|
|
options.append("-d %s" % doctree.get_abspath())
|
|
elif doctree is not None and doctree != '':
|
|
doctree = env.subst(doctree, target=target, source=source)
|
|
options.append("-d %s" % env.Dir(doctree).get_abspath())
|
|
|
|
config = _get_sphinxconfig_path(env, None)
|
|
if config is not None and config != '':
|
|
config = env.subst(config, target=target, source=source)
|
|
options.append("-c %s" % env.Dir(config).File('conf.py').rfile().dir.get_abspath())
|
|
return " ".join(options)
|
|
|
|
|
|
def _create_sphinx_builder(env):
|
|
try:
|
|
sphinx = env['BUILDERS']['Sphinx4Scons']
|
|
except KeyError:
|
|
fs = SCons.Node.FS.get_default_fs()
|
|
sphinx_com = SCons.Action.Action('$SPHINXCOM', '$SPHINXCOMSTR')
|
|
sphinx = SCons.Builder.Builder(action=sphinx_com,
|
|
emitter=sphinx_emitter,
|
|
target_factory=fs.Dir,
|
|
source_factory=fs.Dir
|
|
)
|
|
env['BUILDERS']['Sphinx4Scons'] = sphinx
|
|
return sphinx
|
|
|
|
|
|
def sphinx_emitter(target, source, env):
|
|
target[0].must_be_same(SCons.Node.FS.Dir)
|
|
targetnode = target[0]
|
|
|
|
source[0].must_be_same(SCons.Node.FS.Dir)
|
|
srcnode = source[0]
|
|
|
|
configdir = _get_sphinxconfig_path(env, None)
|
|
if not configdir:
|
|
confignode = srcnode
|
|
else:
|
|
confignode = env.Dir(configdir)
|
|
|
|
srcinfo = SourceInfo(srcnode, confignode, env)
|
|
targets, sources = _get_emissions(env, target, srcinfo)
|
|
env.Clean(targets, target[0])
|
|
|
|
return targets, sources
|
|
|
|
|
|
def sphinx_path(os_path):
|
|
"""Create sphinx-style path from os-style path."""
|
|
return os_path.replace(os.sep, "/")
|
|
|
|
|
|
def os_path(sphinx_path):
|
|
"""Create os-style path from sphinx-style path."""
|
|
return sphinx_path.replace("/", os.sep)
|
|
|
|
|
|
class SourceInfo(object):
|
|
"""
|
|
Data container for all different kinds of source files used in
|
|
a sphinx project.
|
|
"""
|
|
def __init__(self, srcnode, confignode, env):
|
|
self.confignode = confignode
|
|
self.config = self._get_config(self.confignode, env)
|
|
self.templates = self._get_templates(self.confignode, self.config)
|
|
self.statics = self._get_statics(self.confignode, self.config)
|
|
self.srcnode = srcnode
|
|
self.sources = self._get_sources(self.srcnode, self.config)
|
|
|
|
self.srcroot = srcnode
|
|
if not srcnode.duplicate:
|
|
self.srcroot = srcnode.srcnode().rdir()
|
|
|
|
|
|
def _get_config(self, confignode, env):
|
|
config = {}
|
|
execfile(confignode.File('conf.py').rfile().get_abspath(), config)
|
|
return config
|
|
|
|
|
|
def _get_templates(self, confignode, config):
|
|
"""Returns template files defined in the project."""
|
|
templates = []
|
|
for path in config.get('templates_path', []):
|
|
# Check if path is dir or file.
|
|
# We can't use FS.Entry since that will create nodes, and
|
|
# these nodes don't know about the source tree and will
|
|
# get disambiguated to files even if they are directories in the
|
|
# source tree.
|
|
p = confignode.File('conf.py').rfile().dir.srcnode().get_abspath()
|
|
p = os.path.join(p, os_path(path))
|
|
if os.path.isfile(p):
|
|
templates.append(confignode.File(path))
|
|
elif os.path.isdir(p):
|
|
node = confignode.Dir(path)
|
|
for root, dirs, files in os.walk(p):
|
|
mydir = node.Dir(os.path.relpath(root, p))
|
|
templates += [mydir.File(f) for f in files]
|
|
return templates
|
|
|
|
|
|
def _get_statics(self, confignode, config):
|
|
"""Returns static files, filtered through exclude_patterns."""
|
|
statics = []
|
|
matchers = compile_matchers(config.get('exclude_patterns', []))
|
|
|
|
for path in config.get('html_static_path', []):
|
|
# Check _get_templates() why we use this construction.
|
|
p = confignode.File('conf.py').rfile().dir.srcnode().get_abspath()
|
|
p = os.path.join(p, os_path(path))
|
|
if os.path.isfile(p):
|
|
statics.append(confignode.File(path))
|
|
elif os.path.isdir(p):
|
|
node = confignode.Dir(path)
|
|
for root, dirs, files in os.walk(p):
|
|
relpath = os.path.relpath(root, p)
|
|
for entry in [d for d in dirs if
|
|
self._anymatch(matchers,
|
|
sphinx_path(os.path.join(relpath, d)))]:
|
|
dirs.remove(entry)
|
|
statics += [node.File(os_path(f)) for f in
|
|
self._exclude(matchers,
|
|
[sphinx_path(os.path.join(relpath, name))
|
|
for name in files])]
|
|
return statics
|
|
|
|
|
|
def _get_sources(self, srcnode, config):
|
|
"""Returns all source files in the project filtered through exclude_patterns."""
|
|
suffix = config.get('source_suffix', '.rst')
|
|
matchers = compile_matchers(config.get('exclude_patterns', []))
|
|
|
|
srcfiles = []
|
|
scannode = srcnode.srcnode().rdir()
|
|
|
|
for root, dirs, files in os.walk(scannode.get_abspath()):
|
|
relpath = os.path.relpath(root, scannode.get_abspath())
|
|
for entry in [d for d in dirs if
|
|
self._anymatch(matchers,
|
|
sphinx_path(os.path.join(relpath, d)))]:
|
|
dirs.remove(entry)
|
|
srcfiles += [srcnode.File(os_path(f)) for f in
|
|
self._exclude(matchers,
|
|
[sphinx_path(os.path.join(relpath, name))
|
|
for name in files if name.endswith(suffix)])]
|
|
return srcfiles
|
|
|
|
|
|
def _exclude(self, matchers, items):
|
|
result = items
|
|
for matcher in matchers:
|
|
result = filter(lambda x: not matcher(x), result)
|
|
return result
|
|
|
|
|
|
def _anymatch(self, matchers, item):
|
|
for matcher in matchers:
|
|
if matcher(item):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _get_sphinxconfig_path(env, default):
|
|
path = env.get('config', env.get('SPHINXCONFIG', None))
|
|
if path is None or path == '':
|
|
path = default
|
|
return path
|
|
|
|
|
|
def _get_emissions(env, target, srcinfo):
|
|
targets = []
|
|
sources = []
|
|
builder = _get_sphinxbuilder(env)
|
|
if builder == 'changes':
|
|
targets, sources = _get_changes_emissions(env, target, srcinfo)
|
|
if builder == 'devhelp':
|
|
targets, sources = _get_help_emissions(env, target, srcinfo,
|
|
['.devhelp.gz'])
|
|
elif builder == 'dirhtml':
|
|
targets, sources = _get_dirhtml_emissions(env, target, srcinfo)
|
|
elif builder == 'doctest':
|
|
targets, sources = _get_doctest_emissions(env, target, srcinfo)
|
|
elif builder == 'epub':
|
|
targets, sources = _get_epub_emissions(env, target, srcinfo)
|
|
elif builder == 'html':
|
|
targets, sources = _get_serialize_emissions(env, target, srcinfo)
|
|
elif builder == 'htmlhelp':
|
|
targets, sources = _get_help_emissions(env, target, srcinfo,
|
|
['.hhp'], 'htmlhelp_basename')
|
|
elif builder == 'gettext':
|
|
targets, sources = _get_gettext_emissions(env, target, srcinfo)
|
|
elif builder == 'json':
|
|
targets, sources = _get_serialize_emissions(env, target, srcinfo,
|
|
'.fjson',
|
|
['globalcontext.json',
|
|
'searchindex.json',
|
|
'self.environment.pickle'])
|
|
elif builder == 'latex':
|
|
targets, sources = _get_latex_emissions(env, target, srcinfo)
|
|
elif builder == 'linkcheck':
|
|
targets, sources = _get_linkcheck_emissions(env, target, srcinfo)
|
|
elif builder == 'man':
|
|
targets, sources = _get_man_emissions(env, target, srcinfo)
|
|
elif builder == 'pickle':
|
|
targets, sources = _get_serialize_emissions(env, target, srcinfo,
|
|
'.fpickle',
|
|
['globalcontext.pickle',
|
|
'searchindex.pickle',
|
|
'environment.pickle'])
|
|
elif builder == 'qthelp':
|
|
targets, sources = _get_help_emissions(env, target, srcinfo,
|
|
['.qhp', '.qhcp'])
|
|
elif builder == 'singlehtml':
|
|
targets, sources = _get_singlehtml_emissions(env, target, srcinfo)
|
|
elif builder == 'texinfo':
|
|
targets, sources = _get_texinfo_emissions(env, target, srcinfo)
|
|
elif builder == 'text':
|
|
targets, sources = _get_text_emissions(env, target, srcinfo)
|
|
|
|
sources.append(srcinfo.confignode.File('conf.py'))
|
|
|
|
for s in sources:
|
|
s.attributes.root = srcinfo.srcroot
|
|
|
|
for t in targets:
|
|
t.attributes.root = target[0]
|
|
|
|
return targets, sources
|
|
|
|
|
|
def _get_sphinxbuilder(env):
|
|
builder = env.get('builder', env["SPHINXBUILDER"])
|
|
if builder is None or builder == '':
|
|
raise SCons.Errors.UserError(("Missing construction variable " +
|
|
"SPHINXBUILDER or variable is empty."))
|
|
return builder
|
|
|
|
|
|
def _get_changes_emissions(env, target, srcinfo):
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
targets = [target[0].File("changes.html")]
|
|
return targets, sources
|
|
|
|
|
|
def _get_dirhtml_emissions(env, target, srcinfo):
|
|
suffix = srcinfo.config.get('html_file_suffix', ".html")
|
|
|
|
def get_outfilename(pagename):
|
|
pagename = os.path.splitext(pagename)[0]
|
|
|
|
#Special treatment of files named "index". Don't create directory.
|
|
if pagename == 'index' or pagename.endswith(os.sep + 'index'):
|
|
outfilename = pagename + suffix
|
|
else:
|
|
outfilename = os.path.join(pagename, 'index' + suffix)
|
|
return outfilename
|
|
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
sources.extend(srcinfo.templates)
|
|
sources.extend(srcinfo.statics)
|
|
|
|
targets = []
|
|
for s in srcinfo.sources:
|
|
t = os.path.relpath(str(s), str(srcinfo.srcroot))
|
|
targets.append(target[0].File(get_outfilename(t)))
|
|
|
|
for key in srcinfo.config.get('html_additional_pages', {}):
|
|
t = target[0].File(get_outfilename(key))
|
|
targets.append(t)
|
|
|
|
return targets, sources
|
|
|
|
|
|
def _get_doctest_emissions(env, target, srcinfo):
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
targets = [target[0].File("output.txt")]
|
|
return targets, sources
|
|
|
|
|
|
def _get_epub_emissions(env, target, srcinfo):
|
|
epubPreFiles = srcinfo.config.get('epub_pre_files', [])
|
|
epubPostFiles = srcinfo.config.get('epub_post_files', [])
|
|
epubCover = srcinfo.config.get('epub_cover', (None, None))
|
|
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
sources.extend([srcinfo.srcroot.File(os_path(f[0])) for f in epubPreFiles])
|
|
sources.extend([srcinfo.srcroot.File(os_path(f[0])) for f in epubPostFiles])
|
|
if not (epubCover[0] is None or epubCover[0] == ''):
|
|
sources.append(srcinfo.srcroot.File(os_path(epubCover[0])))
|
|
if not (epubCover[1] is None or epubCover[1] == ''):
|
|
sources.append(srcinfo.srcroot.File(os_path(epubCover[1])))
|
|
|
|
t = srcinfo.config.get('epub_basename',
|
|
srcinfo.config.get('project',
|
|
'Python'))
|
|
|
|
targets = [target[0].File("%s.epub" % make_filename(t))]
|
|
|
|
return targets, sources
|
|
|
|
|
|
def _get_gettext_emissions(env, target, srcinfo):
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
|
|
targets = [os.path.relpath(str(s), str(srcinfo.srcroot)) for s in sources]
|
|
targets = [os.path.splitext(t)[0] for t in targets]
|
|
targets = set([t.split(os.sep)[0] for t in targets])
|
|
targets = [target[0].File(t + ".pot") for t in targets]
|
|
|
|
return targets, sources
|
|
|
|
|
|
def _get_help_emissions(env, target, srcinfo, suffixes, basenameConfigKey='project'):
|
|
basename = make_filename(
|
|
srcinfo.config.get(basenameConfigKey, srcinfo.config['project']))
|
|
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
sources.extend(srcinfo.templates)
|
|
sources.extend(srcinfo.statics)
|
|
|
|
targets = [target[0].File(basename + s) for s in suffixes]
|
|
|
|
return targets, sources
|
|
|
|
|
|
def _get_latex_emissions(env, target, srcinfo):
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
|
|
targets = map(lambda x: target[0].File(os_path(x[1])),
|
|
srcinfo.config.get('latex_documents'))
|
|
|
|
return targets, sources
|
|
|
|
|
|
def _get_linkcheck_emissions(env, target, srcinfo):
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
targets = [target[0].File("output.txt")]
|
|
return targets, sources
|
|
|
|
|
|
def _get_man_emissions(env, target, srcinfo):
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
targets = map(lambda x: target[0].File(os_path("%s.%s" % (x[1], x[4]))),
|
|
srcinfo.config.get('man_pages'))
|
|
return targets, sources
|
|
|
|
|
|
def _get_serialize_emissions(env, target, srcinfo, suffix=None, extrafiles=[]):
|
|
if suffix is None:
|
|
suffix = srcinfo.config.get('html_file_suffix', '.html')
|
|
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
sources.extend(srcinfo.templates)
|
|
sources.extend(srcinfo.statics)
|
|
|
|
targets = []
|
|
for s in srcinfo.sources:
|
|
t = os.path.splitext(str(s))[0] + suffix
|
|
t = os.path.relpath(t, str(srcinfo.srcroot))
|
|
targets.append(t)
|
|
|
|
for key in srcinfo.config.get('html_additional_pages', {}):
|
|
targets.append(os_path("%s%s" % (key, suffix)))
|
|
|
|
targets.extend(extrafiles)
|
|
targets = [target[0].File(t) for t in targets]
|
|
|
|
return targets, sources
|
|
|
|
|
|
def _get_singlehtml_emissions(env, target, srcinfo):
|
|
suffix = srcinfo.config.get('html_file_suffix', ".html")
|
|
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
sources.extend(srcinfo.templates)
|
|
sources.extend(srcinfo.statics)
|
|
|
|
t = os.path.relpath(srcinfo.config['master_doc'] + suffix,
|
|
str(srcinfo.srcroot))
|
|
targets = [target[0].File(t)]
|
|
|
|
return targets, sources
|
|
|
|
|
|
def _get_texinfo_emissions(env, target, srcinfo):
|
|
suffix = srcinfo.config.get('source_suffix', '.rst')
|
|
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
sources.extend(map(lambda x: source[0].File(os_path(x + suffix)),
|
|
srcinfo.config.get('texinfo_appendices', [])))
|
|
|
|
targets = map(lambda x: target[0].File(os_path("%s.texi" % x[1])),
|
|
srcinfo.config.get('texinfo_documents'))
|
|
|
|
return targets, sources
|
|
|
|
|
|
def _get_text_emissions(env, target, srcinfo):
|
|
sources = []
|
|
sources.extend(srcinfo.sources)
|
|
|
|
targets = []
|
|
for s in sources:
|
|
t = os.path.relpath(str(s), str(srcinfo.srcroot))
|
|
t = os.path.splitext(t)[0] + ".txt"
|
|
targets.append(target[0].File(t))
|
|
|
|
return targets, sources
|