main.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. # -*- coding: utf-8 -*-
  2. import argparse
  3. import json
  4. import logging
  5. import os
  6. import pathlib
  7. import re
  8. import subprocess
  9. import sys
  10. import tempfile
  11. from sphinx.cmd import build as sphinx_build
  12. from sphinx import config as sphinx_config
  13. from sphinx import project as sphinx_project
  14. from . import sphinx
  15. from . import git
  16. def main(argv=None):
  17. if not argv:
  18. argv = sys.argv[1:]
  19. parser = argparse.ArgumentParser()
  20. parser.add_argument('sourcedir', help='path to documentation source files')
  21. parser.add_argument('outputdir', help='path to output directory')
  22. parser.add_argument('filenames', nargs='*', help='a list of specific files to rebuild. Ignored if -a is specified')
  23. parser.add_argument('-c', metavar='PATH', dest='confdir', help='path where configuration file (conf.py) is located (default: same as SOURCEDIR)')
  24. parser.add_argument('-C', action='store_true', dest='noconfig', help='use no config file at all, only -D options')
  25. parser.add_argument('-D', metavar='setting=value', action='append', dest='define', default=[], help='override a setting in configuration file')
  26. parser.add_argument('--dump-metadata', action='store_true', help='dump generated metadata and exit')
  27. args, argv = parser.parse_known_args(argv)
  28. if args.noconfig:
  29. return 1
  30. # Conf-overrides
  31. confoverrides = {}
  32. for d in args.define:
  33. key, _, value = d.partition('=')
  34. confoverrides[key] = value
  35. # Parse config
  36. config = sphinx_config.Config.read(
  37. os.path.abspath(args.confdir if args.confdir else args.sourcedir),
  38. confoverrides,
  39. )
  40. config.add("smv_tag_whitelist", sphinx.DEFAULT_TAG_WHITELIST, "html", str)
  41. config.add("smv_branch_whitelist", sphinx.DEFAULT_TAG_WHITELIST, "html", str)
  42. config.add("smv_remote_whitelist", sphinx.DEFAULT_REMOTE_WHITELIST, "html", str)
  43. config.add("smv_released_pattern", sphinx.DEFAULT_RELEASED_PATTERN, "html", str)
  44. config.add("smv_outputdir_format", sphinx.DEFAULT_OUTPUTDIR_FORMAT, "html", str)
  45. config.pre_init_values()
  46. config.init_values()
  47. # Get git references
  48. gitroot = pathlib.Path('.').resolve()
  49. gitrefs = git.get_refs(
  50. str(gitroot),
  51. config.smv_tag_whitelist,
  52. config.smv_branch_whitelist,
  53. config.smv_remote_whitelist,
  54. )
  55. logger = logging.getLogger(__name__)
  56. # Get Sourcedir
  57. sourcedir = os.path.relpath(args.sourcedir, str(gitroot))
  58. if args.confdir:
  59. confdir = os.path.relpath(args.confdir, str(gitroot))
  60. else:
  61. confdir = sourcedir
  62. with tempfile.TemporaryDirectory() as tmp:
  63. # Generate Metadata
  64. metadata = {}
  65. outputdirs = set()
  66. for gitref in gitrefs:
  67. # Clone Git repo
  68. repopath = os.path.join(tmp, gitref.commit)
  69. try:
  70. git.copy_tree(gitroot.as_uri(), repopath, gitref)
  71. except (OSError, subprocess.CalledProcessError):
  72. logger.error(
  73. "Failed to copy git tree for %s to %s",
  74. gitref.refname, repopath)
  75. continue
  76. # Find config
  77. confpath = os.path.join(repopath, confdir)
  78. try:
  79. current_config = sphinx_config.Config.read(
  80. confpath,
  81. confoverrides,
  82. )
  83. except (OSError, sphinx_config.ConfigError):
  84. logger.error(
  85. "Failed load config for %s from %s",
  86. gitref.refname, confpath)
  87. continue
  88. current_config.pre_init_values()
  89. current_config.init_values()
  90. # Ensure that there are not duplicate output dirs
  91. outputdir = config.smv_outputdir_format.format(
  92. ref=gitref,
  93. config=current_config,
  94. )
  95. if outputdir in outputdirs:
  96. logger.warning(
  97. "outputdir '%s' for %s conflicts with other versions",
  98. outputdir, gitref.name)
  99. continue
  100. outputdirs.add(outputdir)
  101. # Get List of files
  102. source_suffixes = current_config.source_suffix
  103. if isinstance(source_suffixes, str):
  104. source_suffixes = [current_config.source_suffix]
  105. project = sphinx_project.Project(sourcedir, source_suffixes)
  106. metadata[gitref.name] = {
  107. "name": gitref.name,
  108. "version": current_config.version,
  109. "release": current_config.release,
  110. "is_released": bool(
  111. re.match(config.smv_released_pattern, gitref.refname)),
  112. "source": gitref.source,
  113. "sourcedir": sourcedir,
  114. "outputdir": outputdir,
  115. "docnames": list(project.discover())
  116. }
  117. if args.dump_metadata:
  118. print(json.dumps(metadata, indent=2))
  119. return
  120. # Write Metadata
  121. metadata_path = os.path.abspath(os.path.join(tmp, "versions.json"))
  122. with open(metadata_path, mode='w') as fp:
  123. json.dump(metadata, fp, indent=2)
  124. # Run Sphinx
  125. argv.extend(["-D", "smv_metadata_path={}".format(metadata_path)])
  126. for version_name, data in metadata.items():
  127. outdir = os.path.join(args.outputdir, data["outputdir"])
  128. os.makedirs(outdir, exist_ok=True)
  129. current_argv = argv.copy()
  130. current_argv.extend([
  131. *args.define,
  132. "-D", "smv_current_version={}".format(version_name),
  133. "-c", args.confdir,
  134. data["sourcedir"],
  135. outdir,
  136. *args.filenames,
  137. ])
  138. status = sphinx_build.build_main(current_argv)
  139. if status not in (0, None):
  140. break