main.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. # -*- coding: utf-8 -*-
  2. import itertools
  3. import argparse
  4. import json
  5. import logging
  6. import os
  7. import pathlib
  8. import re
  9. import string
  10. import subprocess
  11. import sys
  12. import tempfile
  13. from sphinx.cmd import build as sphinx_build
  14. from sphinx import config as sphinx_config
  15. from sphinx import project as sphinx_project
  16. from . import sphinx
  17. from . import git
  18. def main(argv=None):
  19. if not argv:
  20. argv = sys.argv[1:]
  21. parser = argparse.ArgumentParser()
  22. parser.add_argument("sourcedir", help="path to documentation source files")
  23. parser.add_argument("outputdir", help="path to output directory")
  24. parser.add_argument(
  25. "filenames",
  26. nargs="*",
  27. help="a list of specific files to rebuild. Ignored if -a is specified",
  28. )
  29. parser.add_argument(
  30. "-c",
  31. metavar="PATH",
  32. dest="confdir",
  33. help=(
  34. "path where configuration file (conf.py) is located "
  35. "(default: same as SOURCEDIR)"
  36. ),
  37. )
  38. parser.add_argument(
  39. "-C",
  40. action="store_true",
  41. dest="noconfig",
  42. help="use no config file at all, only -D options",
  43. )
  44. parser.add_argument(
  45. "-D",
  46. metavar="setting=value",
  47. action="append",
  48. dest="define",
  49. default=[],
  50. help="override a setting in configuration file",
  51. )
  52. parser.add_argument(
  53. "--dump-metadata",
  54. action="store_true",
  55. help="dump generated metadata and exit",
  56. )
  57. args, argv = parser.parse_known_args(argv)
  58. if args.noconfig:
  59. return 1
  60. # Conf-overrides
  61. confoverrides = {}
  62. for d in args.define:
  63. key, _, value = d.partition("=")
  64. confoverrides[key] = value
  65. # Parse config
  66. config = sphinx_config.Config.read(
  67. os.path.abspath(args.confdir if args.confdir else args.sourcedir),
  68. confoverrides,
  69. )
  70. config.add("smv_tag_whitelist", sphinx.DEFAULT_TAG_WHITELIST, "html", str)
  71. config.add(
  72. "smv_branch_whitelist", sphinx.DEFAULT_TAG_WHITELIST, "html", str
  73. )
  74. config.add(
  75. "smv_remote_whitelist", sphinx.DEFAULT_REMOTE_WHITELIST, "html", str
  76. )
  77. config.add(
  78. "smv_released_pattern", sphinx.DEFAULT_RELEASED_PATTERN, "html", str
  79. )
  80. config.add(
  81. "smv_outputdir_format", sphinx.DEFAULT_OUTPUTDIR_FORMAT, "html", str
  82. )
  83. config.add("smv_prefer_remote_refs", False, "html", bool)
  84. config.pre_init_values()
  85. config.init_values()
  86. # Get git references
  87. gitroot = pathlib.Path(".").resolve()
  88. gitrefs = git.get_refs(
  89. str(gitroot),
  90. config.smv_tag_whitelist,
  91. config.smv_branch_whitelist,
  92. config.smv_remote_whitelist,
  93. )
  94. # Order git refs
  95. if config.smv_prefer_remote_refs:
  96. gitrefs = sorted(gitrefs, key=lambda x: (not x.is_remote, *x))
  97. else:
  98. gitrefs = sorted(gitrefs, key=lambda x: (x.is_remote, *x))
  99. logger = logging.getLogger(__name__)
  100. # Get Sourcedir
  101. sourcedir = os.path.relpath(args.sourcedir, str(gitroot))
  102. if args.confdir:
  103. confdir = os.path.relpath(args.confdir, str(gitroot))
  104. else:
  105. confdir = sourcedir
  106. with tempfile.TemporaryDirectory() as tmp:
  107. # Generate Metadata
  108. metadata = {}
  109. outputdirs = set()
  110. for gitref in gitrefs:
  111. # Clone Git repo
  112. repopath = os.path.join(tmp, gitref.commit)
  113. try:
  114. git.copy_tree(gitroot.as_uri(), repopath, gitref)
  115. except (OSError, subprocess.CalledProcessError):
  116. logger.error(
  117. "Failed to copy git tree for %s to %s",
  118. gitref.refname,
  119. repopath,
  120. )
  121. continue
  122. # Find config
  123. confpath = os.path.join(repopath, confdir)
  124. try:
  125. current_config = sphinx_config.Config.read(
  126. confpath, confoverrides,
  127. )
  128. except (OSError, sphinx_config.ConfigError):
  129. logger.error(
  130. "Failed load config for %s from %s",
  131. gitref.refname,
  132. confpath,
  133. )
  134. continue
  135. current_config.pre_init_values()
  136. current_config.init_values()
  137. # Ensure that there are not duplicate output dirs
  138. outputdir = config.smv_outputdir_format.format(
  139. ref=gitref, config=current_config,
  140. )
  141. if outputdir in outputdirs:
  142. logger.warning(
  143. "outputdir '%s' for %s conflicts with other versions",
  144. outputdir,
  145. gitref.refname,
  146. )
  147. continue
  148. outputdirs.add(outputdir)
  149. # Get List of files
  150. source_suffixes = current_config.source_suffix
  151. if isinstance(source_suffixes, str):
  152. source_suffixes = [current_config.source_suffix]
  153. current_sourcedir = os.path.join(repopath, sourcedir)
  154. project = sphinx_project.Project(
  155. current_sourcedir, source_suffixes
  156. )
  157. metadata[gitref.name] = {
  158. "name": gitref.name,
  159. "version": current_config.version,
  160. "release": current_config.release,
  161. "is_released": bool(
  162. re.match(config.smv_released_pattern, gitref.refname)
  163. ),
  164. "source": gitref.source,
  165. "creatordate": gitref.creatordate.strftime(sphinx.DATE_FMT),
  166. "sourcedir": current_sourcedir,
  167. "outputdir": os.path.join(
  168. os.path.abspath(args.outputdir), outputdir
  169. ),
  170. "confdir": os.path.abspath(confdir),
  171. "docnames": list(project.discover()),
  172. }
  173. if args.dump_metadata:
  174. print(json.dumps(metadata, indent=2))
  175. return
  176. if not metadata:
  177. logger.error("No matching refs found!")
  178. return
  179. # Write Metadata
  180. metadata_path = os.path.abspath(os.path.join(tmp, "versions.json"))
  181. with open(metadata_path, mode="w") as fp:
  182. json.dump(metadata, fp, indent=2)
  183. # Run Sphinx
  184. argv.extend(["-D", "smv_metadata_path={}".format(metadata_path)])
  185. for version_name, data in metadata.items():
  186. os.makedirs(data["outputdir"], exist_ok=True)
  187. defines = itertools.chain(
  188. *(
  189. ("-D", string.Template(d).safe_substitute(data))
  190. for d in args.define
  191. )
  192. )
  193. current_argv = argv.copy()
  194. current_argv.extend(
  195. [
  196. *defines,
  197. "-D",
  198. "smv_current_version={}".format(version_name),
  199. "-c",
  200. data["confdir"],
  201. data["sourcedir"],
  202. data["outputdir"],
  203. *args.filenames,
  204. ]
  205. )
  206. logger.debug("Running sphinx-build with args: %r", current_argv)
  207. status = sphinx_build.build_main(current_argv)
  208. if status not in (0, None):
  209. break