deploy_docs.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. #!/usr/bin/env python3
  2. #
  3. # CI script to deploy docs to a webserver. Not useful outside of CI environment
  4. #
  5. #
  6. # SPDX-FileCopyrightText: 2020-2022 Espressif Systems (Shanghai) CO LTD
  7. # SPDX-License-Identifier: Apache-2.0
  8. #
  9. import glob
  10. import os
  11. import os.path
  12. import re
  13. import stat
  14. import subprocess
  15. import sys
  16. import tarfile
  17. from typing import Any, List, Tuple
  18. import packaging.version
  19. def env(variable: str, default: str=None) -> str:
  20. """ Shortcut to return the expanded version of an environment variable """
  21. return os.path.expandvars(os.environ.get(variable, default) if default else os.environ[variable])
  22. # import sanitize_version from the docs directory, shared with here
  23. sys.path.append(os.path.join(env('IDF_PATH'), 'docs'))
  24. from sanitize_version import sanitize_version # noqa
  25. def main() -> None:
  26. # if you get KeyErrors on the following lines, it's probably because you're not running in Gitlab CI
  27. git_ver = env('GIT_VER') # output of git describe --always
  28. ci_ver = env('CI_COMMIT_REF_NAME', git_ver) # branch or tag we're building for (used for 'release' & URL)
  29. version = sanitize_version(ci_ver)
  30. print('Git version: {}'.format(git_ver))
  31. print('CI Version: {}'.format(ci_ver))
  32. print('Deployment version: {}'.format(version))
  33. if not version:
  34. raise RuntimeError('A version is needed to deploy')
  35. build_dir = env('DOCS_BUILD_DIR') # top-level local build dir, where docs have already been built
  36. if not build_dir:
  37. raise RuntimeError('Valid DOCS_BUILD_DIR is needed to deploy')
  38. url_base = env('DOCS_DEPLOY_URL_BASE') # base for HTTP URLs, used to print the URL to the log after deploying
  39. docs_server = env('DOCS_DEPLOY_SERVER') # ssh server to deploy to
  40. docs_user = env('DOCS_DEPLOY_SERVER_USER')
  41. docs_path = env('DOCS_DEPLOY_PATH') # filesystem path on DOCS_SERVER
  42. if not docs_server:
  43. raise RuntimeError('Valid DOCS_DEPLOY_SERVER is needed to deploy')
  44. if not docs_user:
  45. raise RuntimeError('Valid DOCS_DEPLOY_SERVER_USER is needed to deploy')
  46. docs_server = '{}@{}'.format(docs_user, docs_server)
  47. if not docs_path:
  48. raise RuntimeError('Valid DOCS_DEPLOY_PATH is needed to deploy')
  49. print('DOCS_DEPLOY_SERVER {} DOCS_DEPLOY_PATH {}'.format(docs_server, docs_path))
  50. tarball_path, version_urls = build_doc_tarball(version, git_ver, build_dir)
  51. deploy(version, tarball_path, docs_path, docs_server)
  52. print('Docs URLs:')
  53. doc_deploy_type = os.getenv('TYPE')
  54. for vurl in version_urls:
  55. language, _, target = vurl.split('/')
  56. tag = '{}_{}'.format(language, target)
  57. url = '{}/{}/index.html'.format(url_base, vurl) # (index.html needed for the preview server)
  58. url = re.sub(r'([^:])//', r'\1/', url) # get rid of any // that isn't in the https:// part
  59. print('[document {}][{}] {}'.format(doc_deploy_type, tag, url))
  60. # note: it would be neater to use symlinks for stable, but because of the directory order
  61. # (language first) it's kind of a pain to do on a remote server, so we just repeat the
  62. # process but call the version 'stable' this time
  63. if is_stable_version(version):
  64. print('Deploying again as stable version...')
  65. tarball_path, version_urls = build_doc_tarball('stable', git_ver, build_dir)
  66. deploy('stable', tarball_path, docs_path, docs_server)
  67. def deploy(version: str, tarball_path: str, docs_path: str, docs_server: str) -> None:
  68. def run_ssh(commands: List) -> None:
  69. """ Log into docs_server and run a sequence of commands using ssh """
  70. print('Running ssh: {}'.format(commands))
  71. subprocess.run(['ssh', '-o', 'BatchMode=yes', docs_server, '-x', ' && '.join(commands)], check=True)
  72. # copy the version tarball to the server
  73. run_ssh(['mkdir -p {}'.format(docs_path)])
  74. print('Running scp {} to {}'.format(tarball_path, '{}:{}'.format(docs_server, docs_path)))
  75. subprocess.run(['scp', '-B', tarball_path, '{}:{}'.format(docs_server, docs_path)], check=True)
  76. tarball_name = os.path.basename(tarball_path)
  77. run_ssh(['cd {}'.format(docs_path),
  78. 'rm -rf ./*/{}'.format(version), # remove any pre-existing docs matching this version
  79. 'tar -zxvf {}'.format(tarball_name), # untar the archive with the new docs
  80. 'rm {}'.format(tarball_name)])
  81. # Note: deleting and then extracting the archive is a bit awkward for updating stable/latest/etc
  82. # as the version will be invalid for a window of time. Better to do it atomically, but this is
  83. # another thing made much more complex by the directory structure putting language before version...
  84. def build_doc_tarball(version: str, git_ver: str, build_dir: str) -> Tuple[str, List]:
  85. """ Make a tar.gz archive of the docs, in the directory structure used to deploy as
  86. the given version """
  87. version_paths = []
  88. tarball_path = '{}/{}.tar.gz'.format(build_dir, version)
  89. # find all the 'html/' directories under build_dir
  90. html_dirs = glob.glob('{}/**/html/'.format(build_dir), recursive=True)
  91. print('Found %d html directories' % len(html_dirs))
  92. pdfs = glob.glob('{}/**/latex/build/*.pdf'.format(build_dir), recursive=True)
  93. print('Found %d PDFs in latex directories' % len(pdfs))
  94. # add symlink for stable and latest and adds them to PDF blob
  95. symlinks = create_and_add_symlinks(version, git_ver, pdfs)
  96. def not_sources_dir(ti: Any) -> Any:
  97. print(type(ti))
  98. """ Filter the _sources directories out of the tarballs """
  99. if ti.name.endswith('/_sources'):
  100. return None
  101. ti.mode |= stat.S_IWGRP # make everything group-writeable
  102. return ti
  103. try:
  104. os.remove(tarball_path)
  105. except OSError:
  106. pass
  107. with tarfile.open(tarball_path, 'w:gz') as tarball:
  108. for html_dir in html_dirs:
  109. # html_dir has the form '<ignored>/<language>/<target>/html/'
  110. target_dirname = os.path.dirname(os.path.dirname(html_dir))
  111. target = os.path.basename(target_dirname)
  112. language = os.path.basename(os.path.dirname(target_dirname))
  113. # when deploying, we want the top-level directory layout 'language/version/target'
  114. archive_path = '{}/{}/{}'.format(language, version, target)
  115. print("Archiving '{}' as '{}'...".format(html_dir, archive_path))
  116. tarball.add(html_dir, archive_path, filter=not_sources_dir)
  117. version_paths.append(archive_path)
  118. for pdf_path in pdfs:
  119. # pdf_path has the form '<ignored>/<language>/<target>/latex/build'
  120. latex_dirname = os.path.dirname(pdf_path)
  121. pdf_filename = os.path.basename(pdf_path)
  122. target_dirname = os.path.dirname(os.path.dirname(latex_dirname))
  123. target = os.path.basename(target_dirname)
  124. language = os.path.basename(os.path.dirname(target_dirname))
  125. # when deploying, we want the layout 'language/version/target/pdf'
  126. archive_path = '{}/{}/{}/{}'.format(language, version, target, pdf_filename)
  127. print("Archiving '{}' as '{}'...".format(pdf_path, archive_path))
  128. tarball.add(pdf_path, archive_path)
  129. for symlink in symlinks:
  130. os.unlink(symlink)
  131. return (os.path.abspath(tarball_path), version_paths)
  132. def create_and_add_symlinks(version: str, git_ver: str, pdfs: List) -> List:
  133. """ Create symbolic links for PDFs for 'latest' and 'stable' releases """
  134. symlinks = []
  135. if 'stable' in version or 'latest' in version:
  136. for pdf_path in pdfs:
  137. symlink_path = pdf_path.replace(git_ver, version)
  138. os.symlink(pdf_path, symlink_path)
  139. symlinks.append(symlink_path)
  140. pdfs.extend(symlinks)
  141. print('Found %d PDFs in latex directories after adding symlink' % len(pdfs))
  142. return symlinks
  143. def is_stable_version(version: str) -> bool:
  144. """ Heuristic for whether this is the latest stable release """
  145. if not version.startswith('v'):
  146. return False # branch name
  147. if '-' in version:
  148. return False # prerelease tag
  149. git_out = subprocess.check_output(['git', 'tag', '-l']).decode('utf-8')
  150. versions = [v.strip() for v in git_out.split('\n')]
  151. versions = [v for v in versions if re.match(r'^v[\d\.]+$', v.strip())] # include vX.Y.Z only
  152. versions_pack = [packaging.version.parse(v) for v in versions]
  153. max_version = max(versions_pack)
  154. if max_version.public != version[1:]:
  155. print('Stable version is v{}. This version is {}.'.format(max_version.public, version))
  156. return False
  157. else:
  158. print('This version {} is the stable version'.format(version))
  159. return True
  160. if __name__ == '__main__':
  161. main()