deploy_docs.py 8.4 KB

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