| #!/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() |