build_docs.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. #!/usr/bin/env python
  2. #
  3. # Top-level docs builder
  4. #
  5. # This is just a front-end to sphinx-build that can call it multiple times for different language/target combinations
  6. #
  7. # Will build out to _build/LANG/TARGET by default
  8. #
  9. # Specific custom docs functionality should be added in conf_common.py or in a Sphinx extension, not here.
  10. #
  11. from __future__ import print_function
  12. import argparse
  13. import math
  14. import multiprocessing
  15. import os
  16. import os.path
  17. import subprocess
  18. import sys
  19. LANGUAGES = ["en", "zh_CN"]
  20. TARGETS = ["esp32", "esp32s2"]
  21. def main():
  22. # check Python dependencies for docs
  23. try:
  24. subprocess.check_call([sys.executable,
  25. os.path.join(os.environ["IDF_PATH"],
  26. "tools",
  27. "check_python_dependencies.py"),
  28. "-r",
  29. "{}/docs/requirements.txt".format(os.environ["IDF_PATH"])
  30. ])
  31. except subprocess.CalledProcessError:
  32. raise SystemExit(2) # stdout will already have these errors
  33. parser = argparse.ArgumentParser(description='build_docs.py: Build IDF docs',
  34. prog='build_docs.py')
  35. parser.add_argument("--language", "-l", choices=LANGUAGES,
  36. required=False)
  37. parser.add_argument("--target", "-t", choices=TARGETS, required=False)
  38. parser.add_argument("--build-dir", "-b", type=str, default="_build")
  39. parser.add_argument("--sphinx-parallel-builds", "-p", choices=["auto"] + [str(x) for x in range(8)],
  40. help="Parallel Sphinx builds - number of independent Sphinx builds to run", default="auto")
  41. parser.add_argument("--sphinx-parallel-jobs", "-j", choices=["auto"] + [str(x) for x in range(8)],
  42. help="Sphinx parallel jobs argument - number of threads for each Sphinx build to use", default="1")
  43. args = parser.parse_args()
  44. if args.language is None:
  45. print("Building all languages")
  46. languages = LANGUAGES
  47. else:
  48. languages = [args.language]
  49. if args.target is None:
  50. print("Building all targets")
  51. targets = TARGETS
  52. else:
  53. targets = [args.target]
  54. num_sphinx_builds = len(languages) * len(targets)
  55. num_cpus = multiprocessing.cpu_count()
  56. if args.sphinx_parallel_builds == "auto":
  57. # at most one sphinx build per CPU, up to the number of CPUs
  58. args.sphinx_parallel_builds = min(num_sphinx_builds, num_cpus)
  59. else:
  60. args.sphinx_parallel_builds = int(args.sphinx_parallel_builds)
  61. if args.sphinx_parallel_jobs == "auto":
  62. # N CPUs per build job, rounded up - (maybe smarter to round down to avoid contention, idk)
  63. args.sphinx_parallel_jobs = int(math.ceil(num_cpus / args.sphinx_parallel_builds))
  64. else:
  65. args.sphinx_parallel_jobs = int(args.sphinx_parallel_jobs)
  66. print("Will use %d parallel builds and %d jobs per build" % (args.sphinx_parallel_builds, args.sphinx_parallel_jobs))
  67. pool = multiprocessing.Pool(args.sphinx_parallel_builds)
  68. if args.sphinx_parallel_jobs > 1:
  69. print("WARNING: Sphinx parallel jobs currently produce incorrect docs output with Sphinx 1.8.5")
  70. # make a list of all combinations of build_docs() args as tuples
  71. #
  72. # there's probably a fancy way to do this with itertools but this way is actually readable
  73. entries = []
  74. for target in targets:
  75. for language in languages:
  76. build_dir = os.path.realpath(os.path.join(args.build_dir, language, target))
  77. entries.append((language, target, build_dir, args.sphinx_parallel_jobs))
  78. print(entries)
  79. failures = pool.map(call_build_docs, entries)
  80. if any(failures):
  81. if len(entries) > 1:
  82. print("The following language/target combinations failed to build:")
  83. for f in failures:
  84. if f is not None:
  85. print("language: %s target: %s" % (f[0], f[1]))
  86. raise SystemExit(2)
  87. def call_build_docs(entry):
  88. build_docs(*entry)
  89. def build_docs(language, target, build_dir, sphinx_parallel_jobs=1):
  90. # Note: because this runs in a multiprocessing Process, everything which happens here should be isolated to a single process
  91. # (ie it doesn't matter if Sphinx is using global variables, as they're it's own copy of the global variables)
  92. # wrap stdout & stderr in a way that lets us see which build_docs instance they come from
  93. #
  94. # this doesn't apply to subprocesses, they write to OS stdout & stderr so no prefix appears
  95. prefix = "%s/%s: " % (language, target)
  96. print("Building in build_dir:%s" % (build_dir))
  97. try:
  98. os.makedirs(build_dir)
  99. except OSError:
  100. pass
  101. environ = {}
  102. environ.update(os.environ)
  103. environ['BUILDDIR'] = build_dir
  104. args = [sys.executable, "-u", "-m", "sphinx.cmd.build",
  105. "-j", str(sphinx_parallel_jobs),
  106. "-b", "html", # TODO: PDFs
  107. "-d", os.path.join(build_dir, "doctrees"),
  108. # TODO: support multiple sphinx-warning.log files, somehow
  109. "-w", "sphinx-warning.log",
  110. "-t", target,
  111. "-D", "idf_target={}".format(target),
  112. os.path.join(os.path.abspath(os.path.dirname(__file__)), language), # srcdir for this language
  113. os.path.join(build_dir, "html") # build directory
  114. ]
  115. cwd = build_dir # also run sphinx in the build directory
  116. os.chdir(cwd)
  117. print("Running '%s'" % (" ".join(args)))
  118. try:
  119. # Note: we can't call sphinx.cmd.build.main() here as multiprocessing doesn't est >1 layer deep
  120. # and sphinx.cmd.build() also does a lot of work in the calling thread, especially for j ==1,
  121. # so using a Pyhthon thread for this part is a poor option (GIL)
  122. p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  123. for c in iter(lambda: p.stdout.readline(), ''):
  124. sys.stdout.write(prefix)
  125. sys.stdout.write(c)
  126. except KeyboardInterrupt: # this seems to be the only way to get Ctrl-C to kill everything?
  127. p.kill()
  128. return
  129. if __name__ == "__main__":
  130. main()