#!/usr/bin/env python """ Download and install a C++ toolchain. Currently implemented platforms (platform.system) Windows: RTools 3.5, 4.0 (default) Darwin (macOS): Not implemented Linux: Not implemented Optional command line arguments: -v, --version : version, defaults to latest -d, --dir : install directory, defaults to '~/.cmdstan -s (--silent) : install with /VERYSILENT instead of /SILENT for RTools -m --no-make : don't install mingw32-make (Windows RTools 4.0 only) --progress : flag, when specified show progress bar for RTools download """ import argparse import os import platform import shutil import subprocess import sys import urllib.request from collections import OrderedDict from time import sleep from typing import Any, Dict, List from cmdstanpy import _DOT_CMDSTAN from cmdstanpy.utils import pushd, validate_dir, wrap_url_progress_hook EXTENSION = '.exe' if platform.system() == 'Windows' else '' IS_64BITS = sys.maxsize > 2**32 def usage() -> None: """Print usage.""" print( """Arguments: -v (--version) :CmdStan version -d (--dir) : install directory -s (--silent) : install with /VERYSILENT instead of /SILENT for RTools -m (--no-make) : don't install mingw32-make (Windows RTools 4.0 only) --progress : flag, when specified show progress bar for RTools download -h (--help) : this message """ ) def get_config(dir: str, silent: bool) -> List[str]: """Assemble config info.""" config = [] if platform.system() == 'Windows': _, dir = os.path.splitdrive(os.path.abspath(dir)) if dir.startswith('\\'): dir = dir[1:] config = [ '/SP-', '/VERYSILENT' if silent else '/SILENT', '/SUPPRESSMSGBOXES', '/CURRENTUSER', 'LANG="English"', '/DIR="{}"'.format(dir), '/NOICONS', '/NORESTART', ] return config def install_version( installation_dir: str, installation_file: str, version: str, silent: bool, verbose: bool = False, ) -> None: """Install specified toolchain version.""" with pushd('.'): print( 'Installing the C++ toolchain: {}'.format( os.path.splitext(installation_file)[0] ) ) cmd = [installation_file] cmd.extend(get_config(installation_dir, silent)) print(' '.join(cmd)) proc = subprocess.Popen( cmd, cwd=None, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=os.environ, ) while proc.poll() is None: if proc.stdout: output = proc.stdout.readline().decode('utf-8').strip() if output and verbose: print(output, flush=True) _, stderr = proc.communicate() if proc.returncode: print('Installation failed: returncode={}'.format(proc.returncode)) if stderr: print(stderr.decode('utf-8').strip()) if is_installed(installation_dir, version): print('Installation files found at the installation location.') sys.exit(3) # check installation if is_installed(installation_dir, version): os.remove(installation_file) print('Installed {}'.format(os.path.splitext(installation_file)[0])) def install_mingw32_make(toolchain_loc: str, verbose: bool = False) -> None: """Install mingw32-make for Windows RTools 4.0.""" os.environ['PATH'] = ';'.join( list( OrderedDict.fromkeys( [ os.path.join( toolchain_loc, 'mingw_64' if IS_64BITS else 'mingw_32', 'bin', ), os.path.join(toolchain_loc, 'usr', 'bin'), ] + os.environ.get('PATH', '').split(';') ) ) ) cmd = [ 'pacman', '-Sy', 'mingw-w64-x86_64-make' if IS_64BITS else 'mingw-w64-i686-make', '--noconfirm', ] with pushd('.'): print(' '.join(cmd)) proc = subprocess.Popen( cmd, cwd=None, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=os.environ, ) while proc.poll() is None: if proc.stdout: output = proc.stdout.readline().decode('utf-8').strip() if output and verbose: print(output, flush=True) _, stderr = proc.communicate() if proc.returncode: print( 'mingw32-make installation failed: returncode={}'.format( proc.returncode ) ) if stderr: print(stderr.decode('utf-8').strip()) sys.exit(3) print('Installed mingw32-make.exe') def is_installed(toolchain_loc: str, version: str) -> bool: """Returns True is toolchain is installed.""" if platform.system() == 'Windows': if version in ['35', '3.5']: if not os.path.exists(os.path.join(toolchain_loc, 'bin')): return False return os.path.exists( os.path.join( toolchain_loc, 'mingw_64' if IS_64BITS else 'mingw_32', 'bin', 'g++' + EXTENSION, ) ) elif version in ['40', '4.0', '4']: return os.path.exists( os.path.join( toolchain_loc, 'mingw64' if IS_64BITS else 'mingw32', 'bin', 'g++' + EXTENSION, ) ) else: return False return False def latest_version() -> str: """Windows version hardcoded to 4.0.""" if platform.system() == 'Windows': return '4.0' return '' def retrieve_toolchain(filename: str, url: str, progress: bool = True) -> None: """Download toolchain from URL.""" print('Downloading C++ toolchain: {}'.format(filename)) for i in range(6): try: if progress: progress_hook = wrap_url_progress_hook() else: progress_hook = None _ = urllib.request.urlretrieve( url, filename=filename, reporthook=progress_hook ) break except urllib.error.URLError as err: print('Failed to download C++ toolchain') print(err) if i < 5: print('retry ({}/5)'.format(i + 1)) sleep(1) continue sys.exit(3) print('Download successful, file: {}'.format(filename)) def normalize_version(version: str) -> str: """Return maj.min part of version string.""" if platform.system() == 'Windows': if version in ['4', '40']: version = '4.0' elif version == '35': version = '3.5' return version def get_toolchain_name() -> str: """Return toolchain name.""" if platform.system() == 'Windows': return 'RTools' return '' # TODO(2.0): drop 3.5 support def get_url(version: str) -> str: """Return URL for toolchain.""" url = '' if platform.system() == 'Windows': if version == '4.0': # pylint: disable=line-too-long if IS_64BITS: url = 'https://cran.r-project.org/bin/windows/Rtools/rtools40-x86_64.exe' # noqa: disable=E501 else: url = 'https://cran.r-project.org/bin/windows/Rtools/rtools40-i686.exe' # noqa: disable=E501 elif version == '3.5': url = 'https://cran.r-project.org/bin/windows/Rtools/Rtools35.exe' return url def get_toolchain_version(name: str, version: str) -> str: """Toolchain version.""" toolchain_folder = '' if platform.system() == 'Windows': toolchain_folder = '{}{}'.format(name, version.replace('.', '')) return toolchain_folder def run_rtools_install(args: Dict[str, Any]) -> None: """Main.""" if platform.system() not in {'Windows'}: raise NotImplementedError( 'Download for the C++ toolchain ' 'on the current platform has not ' f'been implemented: {platform.system()}' ) toolchain = get_toolchain_name() version = args['version'] if version is None: version = latest_version() version = normalize_version(version) print("C++ toolchain '{}' version: {}".format(toolchain, version)) url = get_url(version) if 'verbose' in args: verbose = args['verbose'] else: verbose = False install_dir = args['dir'] if install_dir is None: install_dir = os.path.expanduser(os.path.join('~', _DOT_CMDSTAN)) validate_dir(install_dir) print('Install directory: {}'.format(install_dir)) if 'progress' in args: progress = args['progress'] else: progress = False if platform.system() == 'Windows': silent = 'silent' in args # force silent == False for 4.0 version if 'silent' not in args and version in ('4.0', '4', '40'): silent = False else: silent = False toolchain_folder = get_toolchain_version(toolchain, version) with pushd(install_dir): if is_installed(toolchain_folder, version): print('C++ toolchain {} already installed'.format(toolchain_folder)) else: if os.path.exists(toolchain_folder): shutil.rmtree(toolchain_folder, ignore_errors=False) retrieve_toolchain( toolchain_folder + EXTENSION, url, progress=progress ) install_version( toolchain_folder, toolchain_folder + EXTENSION, version, silent, verbose, ) if ( 'no-make' not in args and (platform.system() == 'Windows') and (version in ('4.0', '4', '40')) ): if os.path.exists( os.path.join( toolchain_folder, 'mingw64', 'bin', 'mingw32-make.exe' ) ): print('mingw32-make.exe already installed') else: install_mingw32_make(toolchain_folder, verbose) def parse_cmdline_args() -> Dict[str, Any]: parser = argparse.ArgumentParser() parser.add_argument('--version', '-v', help="version, defaults to latest") parser.add_argument( '--dir', '-d', help="install directory, defaults to '~/.cmdstan" ) parser.add_argument( '--silent', '-s', action='store_true', help="install with /VERYSILENT instead of /SILENT for RTools", ) parser.add_argument( '--no-make', '-m', action='store_false', help="don't install mingw32-make (Windows RTools 4.0 only)", ) parser.add_argument( '--verbose', action='store_true', help="flag, when specified prints output from RTools build process", ) parser.add_argument( '--progress', action='store_true', help="flag, when specified show progress bar for CmdStan download", ) return vars(parser.parse_args(sys.argv[1:])) def __main__() -> None: run_rtools_install(parse_cmdline_args()) if __name__ == '__main__': __main__()