main.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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. logger = logging.getLogger(__name__)
  61. sourcedir_absolute = os.path.abspath(args.sourcedir)
  62. confdir_absolute = (
  63. os.path.abspath(args.confdir)
  64. if args.confdir is not None
  65. else sourcedir_absolute
  66. )
  67. # Conf-overrides
  68. confoverrides = {}
  69. for d in args.define:
  70. key, _, value = d.partition("=")
  71. confoverrides[key] = value
  72. # Parse config
  73. config = sphinx_config.Config.read(confdir_absolute, confoverrides)
  74. config.add("smv_tag_whitelist", sphinx.DEFAULT_TAG_WHITELIST, "html", str)
  75. config.add(
  76. "smv_branch_whitelist", sphinx.DEFAULT_TAG_WHITELIST, "html", str
  77. )
  78. config.add(
  79. "smv_remote_whitelist", sphinx.DEFAULT_REMOTE_WHITELIST, "html", str
  80. )
  81. config.add(
  82. "smv_released_pattern", sphinx.DEFAULT_RELEASED_PATTERN, "html", str
  83. )
  84. config.add(
  85. "smv_outputdir_format", sphinx.DEFAULT_OUTPUTDIR_FORMAT, "html", str
  86. )
  87. config.add("smv_prefer_remote_refs", False, "html", bool)
  88. config.pre_init_values()
  89. config.init_values()
  90. # Get relative paths to root of git repository
  91. gitroot = pathlib.Path(
  92. git.get_toplevel_path(cwd=sourcedir_absolute)
  93. ).resolve()
  94. logger.debug("Git toplevel path: %s", str(gitroot))
  95. sourcedir = os.path.relpath(sourcedir_absolute, str(gitroot))
  96. logger.debug(
  97. "Source dir (relative to git toplevel path): %s", str(sourcedir)
  98. )
  99. if args.confdir:
  100. confdir = os.path.relpath(confdir_absolute, str(gitroot))
  101. else:
  102. confdir = sourcedir
  103. logger.debug("Conf dir (relative to git toplevel path): %s", str(confdir))
  104. conffile = os.path.join(confdir, "conf.py")
  105. # Get git references
  106. gitrefs = git.get_refs(
  107. str(gitroot),
  108. config.smv_tag_whitelist,
  109. config.smv_branch_whitelist,
  110. config.smv_remote_whitelist,
  111. files=(sourcedir, conffile),
  112. )
  113. # Order git refs
  114. if config.smv_prefer_remote_refs:
  115. gitrefs = sorted(gitrefs, key=lambda x: (not x.is_remote, *x))
  116. else:
  117. gitrefs = sorted(gitrefs, key=lambda x: (x.is_remote, *x))
  118. logger = logging.getLogger(__name__)
  119. with tempfile.TemporaryDirectory() as tmp:
  120. # Generate Metadata
  121. metadata = {}
  122. outputdirs = set()
  123. for gitref in gitrefs:
  124. # Clone Git repo
  125. repopath = os.path.join(tmp, gitref.commit)
  126. try:
  127. git.copy_tree(str(gitroot), gitroot.as_uri(), repopath, gitref)
  128. except (OSError, subprocess.CalledProcessError):
  129. logger.error(
  130. "Failed to copy git tree for %s to %s",
  131. gitref.refname,
  132. repopath,
  133. )
  134. continue
  135. # Find config
  136. confpath = os.path.join(repopath, confdir)
  137. try:
  138. current_config = sphinx_config.Config.read(
  139. confpath, confoverrides,
  140. )
  141. except (OSError, sphinx_config.ConfigError):
  142. logger.error(
  143. "Failed load config for %s from %s",
  144. gitref.refname,
  145. confpath,
  146. )
  147. continue
  148. current_config.pre_init_values()
  149. current_config.init_values()
  150. # Ensure that there are not duplicate output dirs
  151. outputdir = config.smv_outputdir_format.format(
  152. ref=gitref, config=current_config,
  153. )
  154. if outputdir in outputdirs:
  155. logger.warning(
  156. "outputdir '%s' for %s conflicts with other versions",
  157. outputdir,
  158. gitref.refname,
  159. )
  160. continue
  161. outputdirs.add(outputdir)
  162. # Get List of files
  163. source_suffixes = current_config.source_suffix
  164. if isinstance(source_suffixes, str):
  165. source_suffixes = [current_config.source_suffix]
  166. current_sourcedir = os.path.join(repopath, sourcedir)
  167. project = sphinx_project.Project(
  168. current_sourcedir, source_suffixes
  169. )
  170. metadata[gitref.name] = {
  171. "name": gitref.name,
  172. "version": current_config.version,
  173. "release": current_config.release,
  174. "is_released": bool(
  175. re.match(config.smv_released_pattern, gitref.refname)
  176. ),
  177. "source": gitref.source,
  178. "creatordate": gitref.creatordate.strftime(sphinx.DATE_FMT),
  179. "sourcedir": current_sourcedir,
  180. "outputdir": os.path.join(
  181. os.path.abspath(args.outputdir), outputdir
  182. ),
  183. "confdir": confdir_absolute,
  184. "docnames": list(project.discover()),
  185. }
  186. if args.dump_metadata:
  187. print(json.dumps(metadata, indent=2))
  188. return 0
  189. if not metadata:
  190. logger.error("No matching refs found!")
  191. return 2
  192. # Write Metadata
  193. metadata_path = os.path.abspath(os.path.join(tmp, "versions.json"))
  194. with open(metadata_path, mode="w") as fp:
  195. json.dump(metadata, fp, indent=2)
  196. # Run Sphinx
  197. argv.extend(["-D", "smv_metadata_path={}".format(metadata_path)])
  198. for version_name, data in metadata.items():
  199. os.makedirs(data["outputdir"], exist_ok=True)
  200. defines = itertools.chain(
  201. *(
  202. ("-D", string.Template(d).safe_substitute(data))
  203. for d in args.define
  204. )
  205. )
  206. current_argv = argv.copy()
  207. current_argv.extend(
  208. [
  209. *defines,
  210. "-D",
  211. "smv_current_version={}".format(version_name),
  212. "-c",
  213. data["confdir"],
  214. data["sourcedir"],
  215. data["outputdir"],
  216. *args.filenames,
  217. ]
  218. )
  219. logger.debug("Running sphinx-build with args: %r", current_argv)
  220. status = sphinx_build.build_main(current_argv)
  221. if status not in (0, None):
  222. return 3
  223. return 0