]> git.proxmox.com Git - ceph.git/blob - ceph/src/boost/libs/python/config/tools/sphinx4scons.py
add subtree-ish sources for 12.0.3
[ceph.git] / ceph / src / boost / libs / python / config / tools / sphinx4scons.py
1 """SCons.Tool.spinx4scons
2
3 Tool-specific initialization for the Sphinx document build system.
4
5 There normally shouldn't be any need to import this module directly.
6 It will usually be imported through the generic SCons.Tool.Tool()
7 selection method.
8
9 It should be placed in e.g. ~/site_scons/site_tools/sphinx4scons/
10 directory. Then it may be loaded by placing
11
12 sphinx = Tool('sphinx4scons')
13 sphinx(env)
14
15 in your SConstruct file.
16
17 For further details, please see the SCons documentation on how to
18 install and enable custom tools.
19 """
20
21 #
22 # This package is provided under the Expat license
23 #
24 # Copyright (c) 2012 Orlando Wingbrant
25 #
26 # Permission is hereby granted, free of charge, to any person obtaining
27 # a copy of this software and associated documentation files (the
28 # "Software"), to deal in the Software without restriction, including
29 # without limitation the rights to use, copy, modify, merge, publish,
30 # distribute, sublicense, and/or sell copies of the Software, and to
31 # permit persons to whom the Software is furnished to do so, subject to
32 # the following conditions:
33 #
34 # The above copyright notice and this permission notice shall be included
35 # in all copies or substantial portions of the Software.
36 #
37 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
38 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
39 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
40 # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
41 # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
42 # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
43 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
44 #
45
46 __author__ = "Orlando Wingbrant"
47 __email__ = "orlando@widesite.org"
48 __url__ = "https://bitbucket.org/wingbrant/sphinx4scons"
49 __license__ = "Expat license"
50
51 import SCons.Action
52 import SCons.Builder
53 import SCons.Defaults
54 import SCons.Util
55 import SCons.Node.FS
56 import os
57
58 from sphinx.util.matching import patfilter, compile_matchers
59 from sphinx.util.osutil import make_filename
60
61
62 class ToolSphinxWarning(SCons.Warnings.Warning):
63 pass
64
65
66 class SphinxBuilderNotFound(ToolSphinxWarning):
67 pass
68
69 SCons.Warnings.enableWarningClass(ToolSphinxWarning)
70
71
72 def exists(env):
73 return _detect(env)
74
75
76 def _detect(env):
77 """Try to detect the sphinx-build script."""
78 try:
79 return env['SPHINXBUILD']
80 except KeyError:
81 pass
82
83 sphinx = env.WhereIs('sphinx-build')
84 if sphinx:
85 return sphinx
86
87 raise SCons.Errors.StopError(
88 SphinxBuilderNotFound,
89 "Could not detect sphinx-build script")
90 return None
91
92
93 def generate(env):
94 """Add Builders and construction variables to the Environment."""
95
96 env['SPHINXBUILD'] = _detect(env)
97 sphinx = _create_sphinx_builder(env)
98
99 env.SetDefault(
100 # Additional command-line flags
101 SPHINXFLAGS = '',
102
103 # Tag definitions, each entry will appear on the command line preceded by -t
104 SPHINXTAGS = [],
105
106 # Directory for doctrees
107 SPHINXDOCTREE = '',
108
109 # Path to sphinx configuration file
110 SPHINXCONFIG = '',
111
112 # Config file override settings, each entry will be preceded by -D
113 SPHINXSETTINGS = {},
114
115 # Default sphinx builder,
116 SPHINXBUILDER = 'html',
117
118 # Sphinx command
119 SPHINXCOM = "$SPHINXBUILD $_SPHINXOPTIONS ${SOURCE.attributes.root} ${TARGET.attributes.root}",
120
121 # Alternate console output when building sphinx documents
122 SPHINXCOMSTR = ""
123 )
124
125 try:
126 env.AddMethod(Sphinx, "Sphinx")
127 except AttributeError:
128 # Looks like we use a pre-0.98 version of SCons...
129 from SCons.Script.SConscript import SConsEnvironment
130 SConsEnvironment.Sphinx = Sphinx
131
132
133 def Sphinx(env, target, source, **kw):
134 """A pseudo-builder wrapper for the sphinx builder."""
135 builder = env['BUILDERS']['Sphinx4Scons']
136 env_kw = env.Override(kw)
137 options = _get_sphinxoptions(env_kw, target, source)
138 output = builder(env, target, source, _SPHINXOPTIONS=options, **kw)
139 return output
140
141
142 def _get_sphinxoptions(env, target, source):
143 """Concatenates all the options for the sphinx command line."""
144 options = []
145
146 builder = _get_sphinxbuilder(env)
147 options.append("-b %s" % env.subst(builder, target=target, source=source))
148
149 flags = env.get('options', env.get('SPHINXFLAGS', ''))
150 options.append(env.subst(flags, target=target, source=source))
151
152 tags = env.get('tags', env.get('SPHINXTAGS', None))
153 if tags is not None:
154 if not SCons.SCons.Util.is_List(tags):
155 tags = [tags]
156 for tag in tags:
157 if tag != '':
158 tag = env.subst(tag, target=target, source=source)
159 options.append("-t %s" % tag)
160
161 settings = env.get('settings', env.get('SPHINXSETTINGS', None))
162 if settings is not None:
163 if not SCons.SCons.Util.is_Dict(settings):
164 raise TypeError('SPHINXSETTINGS and/or settings argument must be a dictionary')
165 for key, value in settings.iteritems():
166 if value != '':
167 value = env.subst(value, target=target, source=source)
168 options.append('-D "%s=%s"' % (key, value))
169
170 doctree = env.get('doctree', env.get("SPHINXDOCTREE", None))
171 if isinstance(doctree, SCons.Node.FS.Dir):
172 options.append("-d %s" % doctree.get_abspath())
173 elif doctree is not None and doctree != '':
174 doctree = env.subst(doctree, target=target, source=source)
175 options.append("-d %s" % env.Dir(doctree).get_abspath())
176
177 config = _get_sphinxconfig_path(env, None)
178 if config is not None and config != '':
179 config = env.subst(config, target=target, source=source)
180 options.append("-c %s" % env.Dir(config).File('conf.py').rfile().dir.get_abspath())
181 return " ".join(options)
182
183
184 def _create_sphinx_builder(env):
185 try:
186 sphinx = env['BUILDERS']['Sphinx4Scons']
187 except KeyError:
188 fs = SCons.Node.FS.get_default_fs()
189 sphinx_com = SCons.Action.Action('$SPHINXCOM', '$SPHINXCOMSTR')
190 sphinx = SCons.Builder.Builder(action=sphinx_com,
191 emitter=sphinx_emitter,
192 target_factory=fs.Dir,
193 source_factory=fs.Dir
194 )
195 env['BUILDERS']['Sphinx4Scons'] = sphinx
196 return sphinx
197
198
199 def sphinx_emitter(target, source, env):
200 target[0].must_be_same(SCons.Node.FS.Dir)
201 targetnode = target[0]
202
203 source[0].must_be_same(SCons.Node.FS.Dir)
204 srcnode = source[0]
205
206 configdir = _get_sphinxconfig_path(env, None)
207 if not configdir:
208 confignode = srcnode
209 else:
210 confignode = env.Dir(configdir)
211
212 srcinfo = SourceInfo(srcnode, confignode, env)
213 targets, sources = _get_emissions(env, target, srcinfo)
214 env.Clean(targets, target[0])
215
216 return targets, sources
217
218
219 def sphinx_path(os_path):
220 """Create sphinx-style path from os-style path."""
221 return os_path.replace(os.sep, "/")
222
223
224 def os_path(sphinx_path):
225 """Create os-style path from sphinx-style path."""
226 return sphinx_path.replace("/", os.sep)
227
228
229 class SourceInfo(object):
230 """
231 Data container for all different kinds of source files used in
232 a sphinx project.
233 """
234 def __init__(self, srcnode, confignode, env):
235 self.confignode = confignode
236 self.config = self._get_config(self.confignode, env)
237 self.templates = self._get_templates(self.confignode, self.config)
238 self.statics = self._get_statics(self.confignode, self.config)
239 self.srcnode = srcnode
240 self.sources = self._get_sources(self.srcnode, self.config)
241
242 self.srcroot = srcnode
243 if not srcnode.duplicate:
244 self.srcroot = srcnode.srcnode().rdir()
245
246
247 def _get_config(self, confignode, env):
248 config = {}
249 execfile(confignode.File('conf.py').rfile().get_abspath(), config)
250 return config
251
252
253 def _get_templates(self, confignode, config):
254 """Returns template files defined in the project."""
255 templates = []
256 for path in config.get('templates_path', []):
257 # Check if path is dir or file.
258 # We can't use FS.Entry since that will create nodes, and
259 # these nodes don't know about the source tree and will
260 # get disambiguated to files even if they are directories in the
261 # source tree.
262 p = confignode.File('conf.py').rfile().dir.srcnode().get_abspath()
263 p = os.path.join(p, os_path(path))
264 if os.path.isfile(p):
265 templates.append(confignode.File(path))
266 elif os.path.isdir(p):
267 node = confignode.Dir(path)
268 for root, dirs, files in os.walk(p):
269 mydir = node.Dir(os.path.relpath(root, p))
270 templates += [mydir.File(f) for f in files]
271 return templates
272
273
274 def _get_statics(self, confignode, config):
275 """Returns static files, filtered through exclude_patterns."""
276 statics = []
277 matchers = compile_matchers(config.get('exclude_patterns', []))
278
279 for path in config.get('html_static_path', []):
280 # Check _get_templates() why we use this construction.
281 p = confignode.File('conf.py').rfile().dir.srcnode().get_abspath()
282 p = os.path.join(p, os_path(path))
283 if os.path.isfile(p):
284 statics.append(confignode.File(path))
285 elif os.path.isdir(p):
286 node = confignode.Dir(path)
287 for root, dirs, files in os.walk(p):
288 relpath = os.path.relpath(root, p)
289 for entry in [d for d in dirs if
290 self._anymatch(matchers,
291 sphinx_path(os.path.join(relpath, d)))]:
292 dirs.remove(entry)
293 statics += [node.File(os_path(f)) for f in
294 self._exclude(matchers,
295 [sphinx_path(os.path.join(relpath, name))
296 for name in files])]
297 return statics
298
299
300 def _get_sources(self, srcnode, config):
301 """Returns all source files in the project filtered through exclude_patterns."""
302 suffix = config.get('source_suffix', '.rst')
303 matchers = compile_matchers(config.get('exclude_patterns', []))
304
305 srcfiles = []
306 scannode = srcnode.srcnode().rdir()
307
308 for root, dirs, files in os.walk(scannode.get_abspath()):
309 relpath = os.path.relpath(root, scannode.get_abspath())
310 for entry in [d for d in dirs if
311 self._anymatch(matchers,
312 sphinx_path(os.path.join(relpath, d)))]:
313 dirs.remove(entry)
314 srcfiles += [srcnode.File(os_path(f)) for f in
315 self._exclude(matchers,
316 [sphinx_path(os.path.join(relpath, name))
317 for name in files if name.endswith(suffix)])]
318 return srcfiles
319
320
321 def _exclude(self, matchers, items):
322 result = items
323 for matcher in matchers:
324 result = filter(lambda x: not matcher(x), result)
325 return result
326
327
328 def _anymatch(self, matchers, item):
329 for matcher in matchers:
330 if matcher(item):
331 return True
332 return False
333
334
335 def _get_sphinxconfig_path(env, default):
336 path = env.get('config', env.get('SPHINXCONFIG', None))
337 if path is None or path == '':
338 path = default
339 return path
340
341
342 def _get_emissions(env, target, srcinfo):
343 targets = []
344 sources = []
345 builder = _get_sphinxbuilder(env)
346 if builder == 'changes':
347 targets, sources = _get_changes_emissions(env, target, srcinfo)
348 if builder == 'devhelp':
349 targets, sources = _get_help_emissions(env, target, srcinfo,
350 ['.devhelp.gz'])
351 elif builder == 'dirhtml':
352 targets, sources = _get_dirhtml_emissions(env, target, srcinfo)
353 elif builder == 'doctest':
354 targets, sources = _get_doctest_emissions(env, target, srcinfo)
355 elif builder == 'epub':
356 targets, sources = _get_epub_emissions(env, target, srcinfo)
357 elif builder == 'html':
358 targets, sources = _get_serialize_emissions(env, target, srcinfo)
359 elif builder == 'htmlhelp':
360 targets, sources = _get_help_emissions(env, target, srcinfo,
361 ['.hhp'], 'htmlhelp_basename')
362 elif builder == 'gettext':
363 targets, sources = _get_gettext_emissions(env, target, srcinfo)
364 elif builder == 'json':
365 targets, sources = _get_serialize_emissions(env, target, srcinfo,
366 '.fjson',
367 ['globalcontext.json',
368 'searchindex.json',
369 'self.environment.pickle'])
370 elif builder == 'latex':
371 targets, sources = _get_latex_emissions(env, target, srcinfo)
372 elif builder == 'linkcheck':
373 targets, sources = _get_linkcheck_emissions(env, target, srcinfo)
374 elif builder == 'man':
375 targets, sources = _get_man_emissions(env, target, srcinfo)
376 elif builder == 'pickle':
377 targets, sources = _get_serialize_emissions(env, target, srcinfo,
378 '.fpickle',
379 ['globalcontext.pickle',
380 'searchindex.pickle',
381 'environment.pickle'])
382 elif builder == 'qthelp':
383 targets, sources = _get_help_emissions(env, target, srcinfo,
384 ['.qhp', '.qhcp'])
385 elif builder == 'singlehtml':
386 targets, sources = _get_singlehtml_emissions(env, target, srcinfo)
387 elif builder == 'texinfo':
388 targets, sources = _get_texinfo_emissions(env, target, srcinfo)
389 elif builder == 'text':
390 targets, sources = _get_text_emissions(env, target, srcinfo)
391
392 sources.append(srcinfo.confignode.File('conf.py'))
393
394 for s in sources:
395 s.attributes.root = srcinfo.srcroot
396
397 for t in targets:
398 t.attributes.root = target[0]
399
400 return targets, sources
401
402
403 def _get_sphinxbuilder(env):
404 builder = env.get('builder', env["SPHINXBUILDER"])
405 if builder is None or builder == '':
406 raise SCons.Errors.UserError(("Missing construction variable " +
407 "SPHINXBUILDER or variable is empty."))
408 return builder
409
410
411 def _get_changes_emissions(env, target, srcinfo):
412 sources = []
413 sources.extend(srcinfo.sources)
414 targets = [target[0].File("changes.html")]
415 return targets, sources
416
417
418 def _get_dirhtml_emissions(env, target, srcinfo):
419 suffix = srcinfo.config.get('html_file_suffix', ".html")
420
421 def get_outfilename(pagename):
422 pagename = os.path.splitext(pagename)[0]
423
424 #Special treatment of files named "index". Don't create directory.
425 if pagename == 'index' or pagename.endswith(os.sep + 'index'):
426 outfilename = pagename + suffix
427 else:
428 outfilename = os.path.join(pagename, 'index' + suffix)
429 return outfilename
430
431 sources = []
432 sources.extend(srcinfo.sources)
433 sources.extend(srcinfo.templates)
434 sources.extend(srcinfo.statics)
435
436 targets = []
437 for s in srcinfo.sources:
438 t = os.path.relpath(str(s), str(srcinfo.srcroot))
439 targets.append(target[0].File(get_outfilename(t)))
440
441 for key in srcinfo.config.get('html_additional_pages', {}):
442 t = target[0].File(get_outfilename(key))
443 targets.append(t)
444
445 return targets, sources
446
447
448 def _get_doctest_emissions(env, target, srcinfo):
449 sources = []
450 sources.extend(srcinfo.sources)
451 targets = [target[0].File("output.txt")]
452 return targets, sources
453
454
455 def _get_epub_emissions(env, target, srcinfo):
456 epubPreFiles = srcinfo.config.get('epub_pre_files', [])
457 epubPostFiles = srcinfo.config.get('epub_post_files', [])
458 epubCover = srcinfo.config.get('epub_cover', (None, None))
459
460 sources = []
461 sources.extend(srcinfo.sources)
462 sources.extend([srcinfo.srcroot.File(os_path(f[0])) for f in epubPreFiles])
463 sources.extend([srcinfo.srcroot.File(os_path(f[0])) for f in epubPostFiles])
464 if not (epubCover[0] is None or epubCover[0] == ''):
465 sources.append(srcinfo.srcroot.File(os_path(epubCover[0])))
466 if not (epubCover[1] is None or epubCover[1] == ''):
467 sources.append(srcinfo.srcroot.File(os_path(epubCover[1])))
468
469 t = srcinfo.config.get('epub_basename',
470 srcinfo.config.get('project',
471 'Python'))
472
473 targets = [target[0].File("%s.epub" % make_filename(t))]
474
475 return targets, sources
476
477
478 def _get_gettext_emissions(env, target, srcinfo):
479 sources = []
480 sources.extend(srcinfo.sources)
481
482 targets = [os.path.relpath(str(s), str(srcinfo.srcroot)) for s in sources]
483 targets = [os.path.splitext(t)[0] for t in targets]
484 targets = set([t.split(os.sep)[0] for t in targets])
485 targets = [target[0].File(t + ".pot") for t in targets]
486
487 return targets, sources
488
489
490 def _get_help_emissions(env, target, srcinfo, suffixes, basenameConfigKey='project'):
491 basename = make_filename(
492 srcinfo.config.get(basenameConfigKey, srcinfo.config['project']))
493
494 sources = []
495 sources.extend(srcinfo.sources)
496 sources.extend(srcinfo.templates)
497 sources.extend(srcinfo.statics)
498
499 targets = [target[0].File(basename + s) for s in suffixes]
500
501 return targets, sources
502
503
504 def _get_latex_emissions(env, target, srcinfo):
505 sources = []
506 sources.extend(srcinfo.sources)
507
508 targets = map(lambda x: target[0].File(os_path(x[1])),
509 srcinfo.config.get('latex_documents'))
510
511 return targets, sources
512
513
514 def _get_linkcheck_emissions(env, target, srcinfo):
515 sources = []
516 sources.extend(srcinfo.sources)
517 targets = [target[0].File("output.txt")]
518 return targets, sources
519
520
521 def _get_man_emissions(env, target, srcinfo):
522 sources = []
523 sources.extend(srcinfo.sources)
524 targets = map(lambda x: target[0].File(os_path("%s.%s" % (x[1], x[4]))),
525 srcinfo.config.get('man_pages'))
526 return targets, sources
527
528
529 def _get_serialize_emissions(env, target, srcinfo, suffix=None, extrafiles=[]):
530 if suffix is None:
531 suffix = srcinfo.config.get('html_file_suffix', '.html')
532
533 sources = []
534 sources.extend(srcinfo.sources)
535 sources.extend(srcinfo.templates)
536 sources.extend(srcinfo.statics)
537
538 targets = []
539 for s in srcinfo.sources:
540 t = os.path.splitext(str(s))[0] + suffix
541 t = os.path.relpath(t, str(srcinfo.srcroot))
542 targets.append(t)
543
544 for key in srcinfo.config.get('html_additional_pages', {}):
545 targets.append(os_path("%s%s" % (key, suffix)))
546
547 targets.extend(extrafiles)
548 targets = [target[0].File(t) for t in targets]
549
550 return targets, sources
551
552
553 def _get_singlehtml_emissions(env, target, srcinfo):
554 suffix = srcinfo.config.get('html_file_suffix', ".html")
555
556 sources = []
557 sources.extend(srcinfo.sources)
558 sources.extend(srcinfo.templates)
559 sources.extend(srcinfo.statics)
560
561 t = os.path.relpath(srcinfo.config['master_doc'] + suffix,
562 str(srcinfo.srcroot))
563 targets = [target[0].File(t)]
564
565 return targets, sources
566
567
568 def _get_texinfo_emissions(env, target, srcinfo):
569 suffix = srcinfo.config.get('source_suffix', '.rst')
570
571 sources = []
572 sources.extend(srcinfo.sources)
573 sources.extend(map(lambda x: source[0].File(os_path(x + suffix)),
574 srcinfo.config.get('texinfo_appendices', [])))
575
576 targets = map(lambda x: target[0].File(os_path("%s.texi" % x[1])),
577 srcinfo.config.get('texinfo_documents'))
578
579 return targets, sources
580
581
582 def _get_text_emissions(env, target, srcinfo):
583 sources = []
584 sources.extend(srcinfo.sources)
585
586 targets = []
587 for s in sources:
588 t = os.path.relpath(str(s), str(srcinfo.srcroot))
589 t = os.path.splitext(t)[0] + ".txt"
590 targets.append(target[0].File(t))
591
592 return targets, sources