373 lines
11 KiB
Python
373 lines
11 KiB
Python
#!/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__()
|