Prusa-Firmware/utils/bootstrap.py

203 lines
7.1 KiB
Python
Executable File

#!/usr/bin/env python3
#
# Bootstrap Script
#
# This script
# 1) records the recommended versions of dependencies, and
# 2) when run, checks that all of them are present and downloads
# them if they are not.
#
# pylint: disable=line-too-long
import json
import os
import platform
import shutil
import stat
import subprocess
import sys
import tarfile
import zipfile
from argparse import ArgumentParser
from pathlib import Path
from urllib.request import urlretrieve
project_root_dir = Path(__file__).resolve().parent.parent
dependencies_dir = project_root_dir / '.dependencies'
# All dependencies of this project.
#
# yapf: disable
dependencies = {
'ninja': {
'version': '1.10.2',
'url': {
'Linux': 'https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-linux.zip',
'Windows': 'https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-win.zip',
'Darwin': 'https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-mac.zip',
},
},
'cmake': {
'version': '3.22.5',
'url': {
'Linux': 'https://github.com/Kitware/CMake/releases/download/v3.22.5/cmake-3.22.5-linux-x86_64.tar.gz',
'Windows': 'https://github.com/Kitware/CMake/releases/download/v3.22.5/cmake-3.22.5-windows-x86_64.zip',
'Darwin': 'https://github.com/Kitware/CMake/releases/download/v3.22.5/cmake-3.22.5-macos-universal.tar.gz',
},
},
'avr-gcc': {
'version': '7.3.0',
'url': {
'Linux': 'https://ww1.microchip.com/downloads/aemDocuments/documents/DEV/ProductDocuments/SoftwareTools/avr8-gnu-toolchain-3.7.0.1796-linux.any.x86_64.tar.gz',
'Windows': 'https://ww1.microchip.com/downloads/aemDocuments/documents/DEV/ProductDocuments/SoftwareTools/avr8-gnu-toolchain-3.7.0.1796-win32.any.x86_64.zip',
'Darwin': 'https://ww1.microchip.com/downloads/aemDocuments/documents/DEV/ProductDocuments/SoftwareTools/avr8-gnu-toolchain-osx-3.7.0.518-darwin.any.x86_64.tar.gz',
},
},
'prusa3dboards': {
'version': '1.0.6',
'url': {
'Linux': 'https://raw.githubusercontent.com/prusa3d/Arduino_Boards/master/IDE_Board_Manager/prusa3dboards-1.0.6.tar.bz2',
'Windows': 'https://raw.githubusercontent.com/prusa3d/Arduino_Boards/master/IDE_Board_Manager/prusa3dboards-1.0.6.tar.bz2',
'Darwin': 'https://raw.githubusercontent.com/prusa3d/Arduino_Boards/master/IDE_Board_Manager/prusa3dboards-1.0.6.tar.bz2',
}
},
}
pip_dependencies = ["pyelftools","polib","regex"]
# yapf: enable
def directory_for_dependency(dependency, version):
return dependencies_dir / (dependency + '-' + version)
def find_single_subdir(path: Path):
members = list(path.iterdir())
if path.is_dir() and len(members) > 1:
return path
elif path.is_dir() and len(members) == 1:
return find_single_subdir(members[0]) if members[0].is_dir() else path
else:
raise RuntimeError
def download_and_unzip(url: str, directory: Path):
"""Download a compressed file and extract it at `directory`."""
extract_dir = directory.with_suffix('.temp')
shutil.rmtree(directory, ignore_errors=True)
shutil.rmtree(extract_dir, ignore_errors=True)
print('Downloading ' + directory.name)
f, _ = urlretrieve(url, filename=None)
print('Extracting ' + directory.name)
if '.tar.bz2' in url or '.tar.gz' in url or '.tar.xz' in url:
obj = tarfile.open(f)
else:
obj = zipfile.ZipFile(f, 'r')
obj.extractall(path=str(extract_dir))
subdir = find_single_subdir(extract_dir)
shutil.move(str(subdir), str(directory))
shutil.rmtree(extract_dir, ignore_errors=True)
def run(*cmd):
process = subprocess.run([str(a) for a in cmd],
stdout=subprocess.PIPE,
check=True,
encoding='utf-8')
return process.stdout.strip()
def fix_executable_permissions(dependency, installation_directory):
to_fix = ('ninja', 'clang-format')
if dependency not in to_fix:
return
for fpath in installation_directory.iterdir():
if fpath.is_file and fpath.with_suffix('').name in to_fix:
st = os.stat(fpath)
os.chmod(fpath, st.st_mode | stat.S_IEXEC)
def recommended_version_is_available(dependency):
version = dependencies[dependency]['version']
directory = directory_for_dependency(dependency, version)
return directory.exists() and directory.is_dir()
def get_installed_pip_packages():
result = run(sys.executable, '-m', 'pip', 'list',
'--disable-pip-version-check', '--format', 'json')
data = json.loads(result)
return [(pkg['name'].lower(), pkg['version']) for pkg in data]
def install_dependency(dependency):
specs = dependencies[dependency]
installation_directory = directory_for_dependency(dependency,
specs['version'])
url = specs['url']
if isinstance(url, dict):
url = url[platform.system()]
download_and_unzip(url=url, directory=installation_directory)
fix_executable_permissions(dependency, installation_directory)
def get_dependency_version(dependency):
return dependencies[dependency]['version']
def get_dependency_directory(dependency) -> Path:
version = dependencies[dependency]['version']
return Path(directory_for_dependency(dependency, version))
def main() -> int:
parser = ArgumentParser()
# yapf: disable
parser.add_argument(
'--print-dependency-version', type=str,
help='Prints recommended version of given dependency and exits.')
parser.add_argument(
'--print-dependency-directory', type=str,
help='Prints installation directory of given dependency and exits.')
args = parser.parse_args(sys.argv[1:])
# yapf: enable
if args.print_dependency_version:
try:
print(get_dependency_version(args.print_dependency_version))
return 0
except KeyError:
print('Unknown dependency "%s"' % args.print_dependency_version)
return 1
if args.print_dependency_directory:
try:
print(get_dependency_directory(args.print_dependency_directory))
return 0
except KeyError:
print('Unknown dependency "%s"' % args.print_dependency_directory)
return 1
# if no argument present, check and install dependencies
for dependency in dependencies:
if recommended_version_is_available(dependency):
continue
install_dependency(dependency)
# also, install pip packages
installed_pip_packages = get_installed_pip_packages()
for package in pip_dependencies:
is_installed = any(installed[0] == package
for installed in installed_pip_packages)
if is_installed:
continue
print('Installing Python package %s' % package)
run(sys.executable, '-m', 'pip', 'install', package,
'--disable-pip-version-check')
return 0
if __name__ == "__main__":
sys.exit(main())