bootstrap.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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.10.2',
  31. 'url': {
  32. 'Linux': 'https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-linux.zip',
  33. 'Windows': 'https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-win.zip',
  34. 'Darwin': 'https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-mac.zip',
  35. },
  36. },
  37. 'cmake': {
  38. 'version': '3.22.5',
  39. 'url': {
  40. 'Linux': 'https://github.com/Kitware/CMake/releases/download/v3.22.5/cmake-3.22.5-Linux-x86_64.tar.gz',
  41. 'Windows': 'https://github.com/Kitware/CMake/releases/download/v3.22.5/cmake-3.22.5-win64-x64.zip',
  42. 'Darwin': 'https://github.com/Kitware/CMake/releases/download/v3.22.5/cmake-3.22.5-Darwin-x86_64.tar.gz',
  43. },
  44. },
  45. 'avr-gcc': {
  46. 'version': '7.3.0',
  47. 'url': {
  48. 'Linux': 'http://downloads.arduino.cc/tools/avr-gcc-7.3.0-atmel3.6.1-arduino7-x86_64-pc-linux-gnu.tar.bz2',
  49. 'Windows': 'http://downloads.arduino.cc/tools/avr-gcc-7.3.0-atmel3.6.1-arduino7-i686-w64-mingw32.zip',
  50. 'Darwin': 'http://downloads.arduino.cc/tools/avr-gcc-7.3.0-atmel3.6.1-arduino7-x86_64-apple-darwin14.tar.bz2',
  51. },
  52. },
  53. 'prusa3dboards': {
  54. 'version': '1.0.5-2',
  55. 'url': {
  56. 'Linux': 'https://raw.githubusercontent.com/prusa3d/Arduino_Boards/devel/IDE_Board_Manager/prusa3dboards-1.0.5-2.tar.bz2',
  57. 'Windows': 'https://raw.githubusercontent.com/prusa3d/Arduino_Boards/devel/IDE_Board_Manager/prusa3dboards-1.0.5-2.tar.bz2',
  58. 'Darwin': 'https://raw.githubusercontent.com/prusa3d/Arduino_Boards/devel/IDE_Board_Manager/prusa3dboards-1.0.5-2.tar.bz2',
  59. }
  60. },
  61. }
  62. pip_dependencies = ["pyelftools"]
  63. # yapf: enable
  64. def directory_for_dependency(dependency, version):
  65. return dependencies_dir / (dependency + '-' + version)
  66. def find_single_subdir(path: Path):
  67. members = list(path.iterdir())
  68. if path.is_dir() and len(members) > 1:
  69. return path
  70. elif path.is_dir() and len(members) == 1:
  71. return find_single_subdir(members[0]) if members[0].is_dir() else path
  72. else:
  73. raise RuntimeError
  74. def download_and_unzip(url: str, directory: Path):
  75. """Download a compressed file and extract it at `directory`."""
  76. extract_dir = directory.with_suffix('.temp')
  77. shutil.rmtree(directory, ignore_errors=True)
  78. shutil.rmtree(extract_dir, ignore_errors=True)
  79. print('Downloading ' + directory.name)
  80. f, _ = urlretrieve(url, filename=None)
  81. print('Extracting ' + directory.name)
  82. if '.tar.bz2' in url or '.tar.gz' in url or '.tar.xz' in url:
  83. obj = tarfile.open(f)
  84. else:
  85. obj = zipfile.ZipFile(f, 'r')
  86. obj.extractall(path=str(extract_dir))
  87. subdir = find_single_subdir(extract_dir)
  88. shutil.move(str(subdir), str(directory))
  89. shutil.rmtree(extract_dir, ignore_errors=True)
  90. def run(*cmd):
  91. process = subprocess.run([str(a) for a in cmd],
  92. stdout=subprocess.PIPE,
  93. check=True,
  94. encoding='utf-8')
  95. return process.stdout.strip()
  96. def fix_executable_permissions(dependency, installation_directory):
  97. to_fix = ('ninja', 'clang-format')
  98. if dependency not in to_fix:
  99. return
  100. for fpath in installation_directory.iterdir():
  101. if fpath.is_file and fpath.with_suffix('').name in to_fix:
  102. st = os.stat(fpath)
  103. os.chmod(fpath, st.st_mode | stat.S_IEXEC)
  104. def recommended_version_is_available(dependency):
  105. version = dependencies[dependency]['version']
  106. directory = directory_for_dependency(dependency, version)
  107. return directory.exists() and directory.is_dir()
  108. def get_installed_pip_packages():
  109. result = run(sys.executable, '-m', 'pip', 'list',
  110. '--disable-pip-version-check', '--format', 'json')
  111. data = json.loads(result)
  112. return [(pkg['name'].lower(), pkg['version']) for pkg in data]
  113. def install_dependency(dependency):
  114. specs = dependencies[dependency]
  115. installation_directory = directory_for_dependency(dependency,
  116. specs['version'])
  117. url = specs['url']
  118. if isinstance(url, dict):
  119. url = url[platform.system()]
  120. download_and_unzip(url=url, directory=installation_directory)
  121. fix_executable_permissions(dependency, installation_directory)
  122. def main() -> int:
  123. parser = ArgumentParser()
  124. # yapf: disable
  125. parser.add_argument(
  126. '--print-dependency-version', type=str,
  127. help='Prints recommended version of given dependency and exits.')
  128. parser.add_argument(
  129. '--print-dependency-directory', type=str,
  130. help='Prints installation directory of given dependency and exits.')
  131. args = parser.parse_args(sys.argv[1:])
  132. # yapf: enable
  133. if args.print_dependency_version:
  134. try:
  135. version = dependencies[args.print_dependency_version]['version']
  136. print(version)
  137. return 0
  138. except KeyError:
  139. print('Unknown dependency "%s"' % args.print_dependency_version)
  140. return 1
  141. if args.print_dependency_directory:
  142. try:
  143. dependency = args.print_dependency_directory
  144. version = dependencies[dependency]['version']
  145. install_dir = directory_for_dependency(dependency, version)
  146. print(install_dir)
  147. return 0
  148. except KeyError:
  149. print('Unknown dependency "%s"' % args.print_dependency_directory)
  150. return 1
  151. # if no argument present, check and install dependencies
  152. for dependency in dependencies:
  153. if recommended_version_is_available(dependency):
  154. continue
  155. install_dependency(dependency)
  156. # also, install pip packages
  157. installed_pip_packages = get_installed_pip_packages()
  158. for package in pip_dependencies:
  159. is_installed = any(installed[0] == package
  160. for installed in installed_pip_packages)
  161. if is_installed:
  162. continue
  163. print('Installing Python package %s' % package)
  164. run(sys.executable, '-m', 'pip', 'install', package,
  165. '--disable-pip-version-check')
  166. return 0
  167. if __name__ == "__main__":
  168. sys.exit(main())