main.py 6.8 KB

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