| #!/usr/bin/env python3 | |
| # Copyright lowRISC contributors. | |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| import argparse | |
| import json | |
| import logging as log | |
| import re | |
| import shutil | |
| import subprocess | |
| import sys | |
| import tempfile | |
| from pathlib import Path | |
| from urllib.request import urlopen, urlretrieve | |
| log.basicConfig(level=log.INFO, format="%(levelname)s: %(message)s") | |
| # the keys in this dictionary specify valid toolchain kinds | |
| ASSET_PREFIXES = { | |
| # kind : prefix, | |
| "combined": "lowrisc-toolchain-rv32imcb-", | |
| "gcc-only": "lowrisc-toolchain-gcc-rv32imcb-", | |
| } | |
| ASSET_SUFFIX = ".tar.xz" | |
| RELEASES_URL_BASE = 'https://api.github.com/repos/lowRISC/lowrisc-toolchains/releases' | |
| INSTALL_DIR = '/tools/riscv' | |
| TOOLCHAIN_VERSION = 'latest' | |
| TOOLCHAIN_KIND = 'combined' | |
| FILE_PATTERNS_TO_REWRITE = [ | |
| "riscv32-unknown-elf-*.cmake", | |
| "meson-riscv32-unknown-elf-*.txt", | |
| ] | |
| def get_available_toolchain_info(version, kind): | |
| assert kind in ASSET_PREFIXES | |
| if version == 'latest': | |
| releases_url = '%s/%s' % (RELEASES_URL_BASE, version) | |
| else: | |
| releases_url = '%s/tags/%s' % (RELEASES_URL_BASE, version) | |
| with urlopen(releases_url) as f: | |
| release_info = json.loads(f.read().decode('utf-8')) | |
| for asset in release_info["assets"]: | |
| if (asset["name"].startswith(ASSET_PREFIXES[kind]) and | |
| asset["name"].endswith(ASSET_SUFFIX)): | |
| return { | |
| 'download_url': asset['browser_download_url'], | |
| 'name': asset['name'], | |
| 'version': release_info['tag_name'], | |
| 'kind': kind | |
| } | |
| # No matching asset found for the toolchain kind requested | |
| log.error("No available downloads found for %s toolchain version: %s", | |
| kind, release_info['tag_name']) | |
| raise SystemExit(1) | |
| def get_installed_toolchain_info(unpack_dir): | |
| # Try new-style buildinfo.json first | |
| try: | |
| buildinfo = {} | |
| with open(str(unpack_dir / 'buildinfo.json'), 'r') as f: | |
| buildinfo = json.loads(f.read()) | |
| # Toolchains before 20200602-4 contained a `buildinfo.json` without a | |
| # 'kind' field. Setting it to 'unknown' will ensure we never skip | |
| # updating because we think it's the same as the existing toolchain. | |
| if 'kind' not in buildinfo: | |
| buildinfo['kind'] = 'unknown' | |
| return buildinfo | |
| except Exception as e: | |
| # buildinfo.json might not exist in older builds | |
| log.info("Unable to parse buildinfo.json: %s", str(e)) | |
| pass | |
| # If that wasn't successful, try old-style plaintext buildinfo | |
| version_re = r"(lowRISC toolchain version|Version):\s*\n?(?P<version>[^\n\s]+)" | |
| buildinfo_txt_path = unpack_dir / 'buildinfo' | |
| try: | |
| with open(str(buildinfo_txt_path), 'r') as f: | |
| match = re.match(version_re, f.read(), re.M) | |
| if not match: | |
| log.warning("Unable extract version from %s", | |
| str(buildinfo_txt_path)) | |
| return None | |
| return {'version': match.group("version"), 'kind': 'unknown'} | |
| except Exception as e: | |
| log.error("Unable to read %s: %s", str(buildinfo_txt_path), str(e)) | |
| return None | |
| def download(url): | |
| log.info("Downloading toolchain from %s", url) | |
| tmpfile = tempfile.mkstemp()[1] | |
| urlretrieve(url, tmpfile) | |
| log.info("Download complete") | |
| return Path(tmpfile) | |
| def install(archive_file, unpack_dir): | |
| unpack_dir.mkdir(parents=True, exist_ok=True) | |
| cmd = [ | |
| 'tar', | |
| '-x', | |
| '-f', | |
| str(archive_file), | |
| '--strip-components=1', | |
| '-C', | |
| str(unpack_dir), | |
| ] | |
| subprocess.run(cmd, check=True) | |
| def postinstall_rewrite_configs(unpack_dir, install_dir): | |
| """Rewrites the toolchain configuration files to point to install_dir. | |
| 'unpack_dir' is where the toolchain is unpacked by this script. | |
| 'install_dir' is where the toolchain is eventually invoked from. Typically, | |
| these are the same, unless a staged installation is being performed by | |
| supplying both, --install-dir and --dest-dir switches. Regardless, if the | |
| 'install_dir' is different from the default, the config files need to be | |
| updated to reflect the correct paths. | |
| """ | |
| if str(install_dir) == INSTALL_DIR: | |
| return | |
| for file_pattern in FILE_PATTERNS_TO_REWRITE: | |
| for config_file_path in unpack_dir.glob(file_pattern): | |
| # Rewrite INSTALL_DIR to the requested target dir. | |
| log.info("Updating toolchain paths in %s", str(config_file_path)) | |
| with open(str(config_file_path)) as f: | |
| original = f.read() | |
| with open(str(config_file_path), "w") as f: | |
| f.write(original.replace(INSTALL_DIR, str(install_dir))) | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument('--install-dir', | |
| '-i', | |
| required=False, | |
| default=INSTALL_DIR, | |
| help="Installation directory (default: %(default)s)") | |
| parser.add_argument('--dest-dir', | |
| '-d', | |
| required=False, | |
| help="""Destination directory if performing a staged | |
| installation. This is the staging directory where the | |
| toolchain is unpacked.""") | |
| parser.add_argument('--release-version', | |
| '-r', | |
| required=False, | |
| default=TOOLCHAIN_VERSION, | |
| help="Toolchain version (default: %(default)s)") | |
| parser.add_argument('--latest-available-version', | |
| '-l', | |
| required=False, | |
| default=False, | |
| action='store_true', | |
| help="Return the latest available toolchain version.") | |
| parser.add_argument('--kind', | |
| required=False, | |
| default=TOOLCHAIN_KIND, | |
| choices=ASSET_PREFIXES.keys(), | |
| help="Toolchain kind (default: %(default)s)") | |
| parser.add_argument( | |
| '--update', | |
| '-u', | |
| required=False, | |
| default=False, | |
| action='store_true', | |
| help="Update to target version if needed (default: %(default)s)") | |
| args = parser.parse_args() | |
| available_toolchain = get_available_toolchain_info(args.release_version, | |
| args.kind) | |
| if args.latest_available_version: | |
| print(available_toolchain['version']) | |
| sys.exit(0) | |
| log.info("Found available %s toolchain version %s, %s", | |
| available_toolchain['kind'], available_toolchain['version'], | |
| available_toolchain['name']) | |
| install_dir = Path(args.install_dir) | |
| if args.dest_dir is None: | |
| unpack_dir = install_dir | |
| else: | |
| unpack_dir = Path(args.dest_dir) | |
| if args.update and unpack_dir.is_dir(): | |
| installed_toolchain = get_installed_toolchain_info(unpack_dir) | |
| if installed_toolchain is None: | |
| sys.exit('Unable to extract current toolchain version. ' | |
| 'Delete target directory %s and try again.' % | |
| str(unpack_dir)) | |
| version_matches = available_toolchain[ | |
| 'version'] == installed_toolchain['version'] | |
| kind_matches = available_toolchain['kind'] == installed_toolchain[ | |
| 'kind'] | |
| if installed_toolchain[ | |
| 'kind'] != 'unknown' and version_matches and kind_matches: | |
| log.info( | |
| 'Downloaded %s toolchain is version %s, ' | |
| 'same as the %s toolchain installed at %s (version %s).', | |
| available_toolchain['kind'], available_toolchain['version'], | |
| installed_toolchain['kind'], installed_toolchain['version'], | |
| str(unpack_dir)) | |
| log.warning("Skipping install.") | |
| sys.exit(0) | |
| log.info( | |
| "Found installed %s toolchain version %s, updating to %s toolchain " | |
| "version %s.", | |
| installed_toolchain['kind'], installed_toolchain['version'], | |
| available_toolchain['kind'], available_toolchain['version']) | |
| else: | |
| if unpack_dir.exists(): | |
| sys.exit('Target directory %s already exists. ' | |
| 'Delete it first, or use --update.' % str(unpack_dir)) | |
| archive_file = None | |
| try: | |
| archive_file = download(available_toolchain['download_url']) | |
| if args.update and unpack_dir.exists(): | |
| # We only reach this point if |unpack_dir| contained a toolchain | |
| # before, so removing it is reasonably safe. | |
| shutil.rmtree(str(unpack_dir)) | |
| install(archive_file, unpack_dir) | |
| postinstall_rewrite_configs(unpack_dir.resolve(), | |
| install_dir.resolve()) | |
| finally: | |
| if archive_file: | |
| archive_file.unlink() | |
| log.info('Installed %s toolchain version %s to %s.', | |
| available_toolchain['kind'], available_toolchain['version'], | |
| str(unpack_dir)) | |
| if __name__ == "__main__": | |
| main() |