bootstrap.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. #!/usr/bin/env python3
  2. #
  3. # Bootstrap Script
  4. #
  5. # This script
  6. # 1) records the recommended versions of dependencies, and
  7. # 2) when run, checks that all of them are present and downloads
  8. # them if they are not.
  9. #
  10. # pylint: disable=line-too-long
  11. import json
  12. import os
  13. import platform
  14. import shutil
  15. import stat
  16. import subprocess
  17. import sys
  18. import tarfile
  19. import zipfile
  20. from argparse import ArgumentParser
  21. from pathlib import Path
  22. from urllib.request import urlretrieve
  23. project_root_dir = Path(__file__).resolve().parent.parent
  24. dependencies_dir = project_root_dir / '.dependencies'
  25. # All dependencies of this project.
  26. #
  27. # yapf: disable
  28. dependencies = {
  29. 'ninja': {
  30. 'version': '1.9.0',
  31. 'url': {
  32. 'Linux': 'https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-linux.zip',
  33. 'Windows': 'https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-win.zip',
  34. 'Darwin': 'https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-mac.zip',
  35. },
  36. },
  37. 'cmake': {
  38. 'version': '3.15.5',
  39. 'url': {
  40. 'Linux': 'https://github.com/Kitware/CMake/releases/download/v3.15.5/cmake-3.15.5-Linux-x86_64.tar.gz',
  41. 'Windows': 'https://github.com/Kitware/CMake/releases/download/v3.15.5/cmake-3.15.5-win64-x64.zip',
  42. 'Darwin': 'https://github.com/Kitware/CMake/releases/download/v3.15.5/cmake-3.15.5-Darwin-x86_64.tar.gz',
  43. },
  44. },
  45. 'gcc-avr': {
  46. # dummy placeholder (currently downloading cmake just for the sake of a valid url/zip archive)
  47. # ... we truly need the binaries! :)
  48. 'version': '0.0.0',
  49. 'url': {
  50. 'Linux': 'https://github.com/Kitware/CMake/releases/download/v3.15.5/cmake-3.15.5-Linux-x86_64.tar.gz',
  51. 'Windows': 'https://github.com/Kitware/CMake/releases/download/v3.15.5/cmake-3.15.5-win64-x64.zip',
  52. 'Darwin': 'https://github.com/Kitware/CMake/releases/download/v3.15.5/cmake-3.15.5-Darwin-x86_64.tar.gz',
  53. }
  54. },
  55. }
  56. pip_dependencies = []
  57. # yapf: enable
  58. def directory_for_dependency(dependency, version):
  59. return dependencies_dir / (dependency + '-' + version)
  60. def find_single_subdir(path: Path):
  61. members = list(path.iterdir())
  62. if path.is_dir() and len(members) > 1:
  63. return path
  64. elif path.is_dir() and len(members) == 1:
  65. return find_single_subdir(members[0]) if members[0].is_dir() else path
  66. else:
  67. raise RuntimeError
  68. def download_and_unzip(url: str, directory: Path):
  69. """Download a compressed file and extract it at `directory`."""
  70. extract_dir = directory.with_suffix('.temp')
  71. shutil.rmtree(directory, ignore_errors=True)
  72. shutil.rmtree(extract_dir, ignore_errors=True)
  73. print('Downloading ' + directory.name)
  74. f, _ = urlretrieve(url, filename=None)
  75. print('Extracting ' + directory.name)
  76. if '.tar.bz2' in url or '.tar.gz' in url or '.tar.xz' in url:
  77. obj = tarfile.open(f)
  78. else:
  79. obj = zipfile.ZipFile(f, 'r')
  80. obj.extractall(path=str(extract_dir))
  81. subdir = find_single_subdir(extract_dir)
  82. shutil.move(str(subdir), str(directory))
  83. shutil.rmtree(extract_dir, ignore_errors=True)
  84. def run(*cmd):
  85. process = subprocess.run([str(a) for a in cmd],
  86. stdout=subprocess.PIPE,
  87. check=True,
  88. encoding='utf-8')
  89. return process.stdout.strip()
  90. def fix_executable_permissions(dependency, installation_directory):
  91. to_fix = ('ninja', 'clang-format')
  92. if dependency not in to_fix:
  93. return
  94. for fpath in installation_directory.iterdir():
  95. if fpath.is_file and fpath.with_suffix('').name in to_fix:
  96. st = os.stat(fpath)
  97. os.chmod(fpath, st.st_mode | stat.S_IEXEC)
  98. def recommended_version_is_available(dependency):
  99. version = dependencies[dependency]['version']
  100. directory = directory_for_dependency(dependency, version)
  101. return directory.exists() and directory.is_dir()
  102. def get_installed_pip_packages():
  103. result = run(sys.executable, '-m', 'pip', 'list',
  104. '--disable-pip-version-check', '--format', 'json')
  105. data = json.loads(result)
  106. return [(pkg['name'].lower(), pkg['version']) for pkg in data]
  107. def install_dependency(dependency):
  108. specs = dependencies[dependency]
  109. installation_directory = directory_for_dependency(dependency,
  110. specs['version'])
  111. url = specs['url']
  112. if isinstance(url, dict):
  113. url = url[platform.system()]
  114. download_and_unzip(url=url, directory=installation_directory)
  115. fix_executable_permissions(dependency, installation_directory)
  116. def main() -> int:
  117. parser = ArgumentParser()
  118. # yapf: disable
  119. parser.add_argument(
  120. '--print-dependency-version', type=str,
  121. help='Prints recommended version of given dependency and exits.')
  122. parser.add_argument(
  123. '--print-dependency-directory', type=str,
  124. help='Prints installation directory of given dependency and exits.')
  125. args = parser.parse_args(sys.argv[1:])
  126. # yapf: enable
  127. if args.print_dependency_version:
  128. try:
  129. version = dependencies[args.print_dependency_version]['version']
  130. print(version)
  131. return 0
  132. except KeyError:
  133. print('Unknown dependency "%s"' % args.print_dependency_version)
  134. return 1
  135. if args.print_dependency_directory:
  136. try:
  137. dependency = args.print_dependency_directory
  138. version = dependencies[dependency]['version']
  139. install_dir = directory_for_dependency(dependency, version)
  140. print(install_dir)
  141. return 0
  142. except KeyError:
  143. print('Unknown dependency "%s"' % args.print_dependency_directory)
  144. return 1
  145. # if no argument present, check and install dependencies
  146. for dependency in dependencies:
  147. if recommended_version_is_available(dependency):
  148. continue
  149. install_dependency(dependency)
  150. # also, install pip packages
  151. installed_pip_packages = get_installed_pip_packages()
  152. for package in pip_dependencies:
  153. is_installed = any(installed[0] == package
  154. for installed in installed_pip_packages)
  155. if is_installed:
  156. continue
  157. print('Installing Python package %s' % package)
  158. run(sys.executable, '-m', 'pip', 'install', package,
  159. '--disable-pip-version-check')
  160. return 0
  161. if __name__ == "__main__":
  162. sys.exit(main())