main.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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. # Get git references
  46. gitroot = pathlib.Path('.').resolve()
  47. gitrefs = git.get_refs(
  48. str(gitroot),
  49. config.smv_tag_whitelist,
  50. config.smv_branch_whitelist,
  51. config.smv_remote_whitelist,
  52. )
  53. logger = logging.getLogger(__name__)
  54. # Get Sourcedir
  55. sourcedir = os.path.relpath(args.sourcedir, str(gitroot))
  56. if args.confdir:
  57. confdir = os.path.relpath(args.confdir, str(gitroot))
  58. else:
  59. confdir = sourcedir
  60. with tempfile.TemporaryDirectory() as tmp:
  61. # Generate Metadata
  62. metadata = {}
  63. outputdirs = set()
  64. for gitref in gitrefs:
  65. # Clone Git repo
  66. repopath = os.path.join(tmp, gitref.commit)
  67. try:
  68. git.copy_tree(gitroot.as_uri(), repopath, gitref)
  69. except (OSError, subprocess.CalledProcessError):
  70. logger.error(
  71. "Failed to copy git tree for %s to %s",
  72. gitref.refname, repopath)
  73. continue
  74. # Find config
  75. confpath = os.path.join(repopath, confdir)
  76. try:
  77. current_config = sphinx_config.Config.read(
  78. confpath,
  79. confoverrides,
  80. )
  81. except sphinx_config.ConfigError:
  82. logger.error(
  83. "Failed load config for %s from %s",
  84. gitref.refname, confpath)
  85. continue
  86. # Ensure that there are not duplicate output dirs
  87. outputdir = config.smv_outputdir_format.format(
  88. ref=gitref,
  89. config=current_config,
  90. )
  91. if outputdir in outputdirs:
  92. logger.warning(
  93. "outputdir '%s' for %s conflicts with other versions",
  94. outputdir, gitref.name)
  95. continue
  96. outputdirs.add(outputdir)
  97. # Get List of files
  98. source_suffixes = current_config.source_suffix
  99. if isinstance(source_suffixes, str):
  100. source_suffixes = [current_config.source_suffix]
  101. project = sphinx_project.Project(sourcedir, source_suffixes)
  102. metadata[gitref.name] = {
  103. "name": gitref.name,
  104. "version": current_config.version,
  105. "release": current_config.release,
  106. "is_released": bool(
  107. re.match(config.smv_released_pattern, gitref.refname)),
  108. "source": gitref.source,
  109. "sourcedir": sourcedir,
  110. "outputdir": outputdir,
  111. "docnames": list(project.discover())
  112. }
  113. if args.dump_metadata:
  114. print(json.dumps(metadata, indent=2))
  115. return
  116. # Write Metadata
  117. metadata_path = os.path.abspath(os.path.join(tmp, "versions.json"))
  118. with open(metadata_path, mode='w') as fp:
  119. json.dump(metadata, fp, indent=2)
  120. # Run Sphinx
  121. argv.extend(["-D", "smv_metadata_path={}".format(metadata_path)])
  122. for version_name, data in metadata.items():
  123. outdir = os.path.join(args.outputdir, data["outputdir"])
  124. os.makedirs(outdir, exist_ok=True)
  125. current_argv = argv.copy()
  126. current_argv.extend([
  127. *args.define,
  128. "-D", "smv_current_version={}".format(version_name),
  129. "-c", args.confdir,
  130. data["sourcedir"],
  131. outdir,
  132. *args.filenames,
  133. ])
  134. status = sphinx_build.build_main(current_argv)
  135. if status not in (0, None):
  136. break