#!/usr/bin/env python"""Download and install a CmdStan release from GitHub.Downloads the release tar.gz file to temporary storage.Retries GitHub requests in order to allow for transient network outages.Builds CmdStan executables and tests the compiler by buildingexample model ``bernoulli.stan``.Optional command line arguments: -i, --interactive: flag, when specified ignore other arguments and ask user for settings on STDIN -v, --version <release> : version, defaults to latest release version -d, --dir <path> : install directory, defaults to '$HOME/.cmdstan --overwrite: flag, when specified re-installs existing version --progress: flag, when specified show progress bar for CmdStan download --verbose: flag, when specified prints output from CmdStan build process --cores: int, number of cores to use when building, defaults to 1 -c, --compiler : flag, add C++ compiler to path (Windows only)"""importargparseimportjsonimportosimportplatformimportreimportshutilimportsysimporttarfileimporturllib.errorimporturllib.requestfromcollectionsimportOrderedDictfrompathlibimportPathfromtimeimportsleepfromtypingimportTYPE_CHECKING,Any,Callable,Dict,Optional,Unionfromtqdm.autoimporttqdmfromcmdstanpyimport_DOT_CMDSTANfromcmdstanpy.utilsimport(cmdstan_path,do_command,pushd,validate_dir,wrap_url_progress_hook,)fromcmdstanpy.utils.cmdstanimportget_download_urlfrom.importprogressasprogbarifsys.version_info>=(3,8)orTYPE_CHECKING:# mypy only knows about the new built-in cached_propertyfromfunctoolsimportcached_propertyelse:# on older Python versions, this is the recommended# way to get the same effectfromfunctoolsimportlru_cachedefcached_property(fun):returnproperty(lru_cache(maxsize=None)(fun))try:# on MacOS and Linux, importing this# improves the UX of the input() functionimportreadline# dummy statement to use import for flake8/pylint_=readline.__doc__exceptImportError:passclassCmdStanRetrieveError(RuntimeError):passclassCmdStanInstallError(RuntimeError):passdefis_windows()->bool:returnplatform.system()=='Windows'MAKE=os.getenv('MAKE','make'ifnotis_windows()else'mingw32-make')EXTENSION='.exe'ifis_windows()else''defget_headers()->Dict[str,str]:"""Create headers dictionary."""headers={}GITHUB_PAT=os.environ.get("GITHUB_PAT")# pylint:disable=invalid-nameifGITHUB_PATisnotNone:headers["Authorization"]="token {}".format(GITHUB_PAT)returnheadersdeflatest_version()->str:"""Report latest CmdStan release version."""url='https://api.github.com/repos/stan-dev/cmdstan/releases/latest'request=urllib.request.Request(url,headers=get_headers())foriinrange(6):try:response=urllib.request.urlopen(request).read()breakexcepturllib.error.URLErrorase:print('Cannot connect to github.')print(e)ifi<5:print('retry ({}/5)'.format(i+1))sleep(1)continueraiseCmdStanRetrieveError('Cannot connect to CmdStan github repo.')fromecontent=json.loads(response.decode('utf-8'))tag=content['tag_name']match=re.search(r'v?(.+)',tag)ifmatchisnotNone:tag=match.group(1)returntag# type: ignoredefhome_cmdstan()->str:returnos.path.expanduser(os.path.join('~',_DOT_CMDSTAN))# pylint: disable=too-few-public-methodsclassInstallationSettings:""" A static installation settings object """def__init__(self,*,version:Optional[str]=None,dir:Optional[str]=None,progress:bool=False,verbose:bool=False,overwrite:bool=False,cores:int=1,compiler:bool=False,**kwargs:Any,):self.version=versionifversionelselatest_version()self.dir=dirifdirelsehome_cmdstan()self.progress=progressself.verbose=verboseself.overwrite=overwriteself.cores=coresself.compiler=compilerandis_windows()_=kwargs# ignore all other inputs.# Useful if initialized from a dictionary like **dictdefyes_no(answer:str,default:bool)->bool:answer=answer.lower()ifanswerin('y','yes'):returnTrueifanswerin('n','no'):returnFalsereturndefaultclassInteractiveSettings:""" Installation settings provided on-demand in an interactive format. This provides the same set of properties as the ``InstallationSettings`` object, but rather than them being fixed by the constructor the user is asked for input whenever they are accessed for the first time. """@cached_propertydefversion(self)->str:latest=latest_version()print("Which version would you like to install?")print(f"Default: {latest}")answer=input("Type version or hit enter to continue: ")returnanswerifanswerelselatest@cached_propertydefdir(self)->str:directory=home_cmdstan()print("Where would you like to install CmdStan?")print(f"Default: {directory}")answer=input("Type full path or hit enter to continue: ")returnos.path.expanduser(answer)ifanswerelsedirectory@cached_propertydefprogress(self)->bool:print("Show installation progress bars?")print("Default: y")answer=input("[y/n]: ")returnyes_no(answer,True)@cached_propertydefverbose(self)->bool:print("Show verbose output of the installation process?")print("Default: n")answer=input("[y/n]: ")returnyes_no(answer,False)@cached_propertydefoverwrite(self)->bool:print("Overwrite existing CmdStan installation?")print("Default: n")answer=input("[y/n]: ")returnyes_no(answer,False)@cached_propertydefcompiler(self)->bool:ifnotis_windows():returnFalseprint("Would you like to install the RTools40 C++ toolchain?")print("A C++ toolchain is required for CmdStan.")print("If you are not sure if you need the toolchain or not, ""the most likely case is you do need it, and should answer 'y'.")print("Default: n")answer=input("[y/n]: ")returnyes_no(answer,False)@cached_propertydefcores(self)->int:max_cpus=os.cpu_count()or1print("How many CPU cores would you like to use for installing ""and compiling CmdStan?")print(f"Default: 1, Max: {max_cpus}")answer=input("Enter a number or hit enter to continue: ")try:returnmin(max_cpus,max(int(answer),1))exceptValueError:return1defclean_all(verbose:bool=False)->None:""" Run `make clean-all` in the current directory (must be a cmdstan library). :param verbose: Boolean value; when ``True``, show output from make command. """cmd=[MAKE,'clean-all']try:ifverbose:do_command(cmd)else:do_command(cmd,fd_out=None)exceptRuntimeErrorase:# pylint: disable=raise-missing-fromraiseCmdStanInstallError(f'Command "make clean-all" failed\n{str(e)}')defbuild(verbose:bool=False,progress:bool=True,cores:int=1)->None:""" Run command ``make build`` in the current directory, which must be the home directory of a CmdStan version (or GitHub repo). By default, displays a progress bar which tracks make command outputs. If argument ``verbose=True``, instead of a progress bar, streams make command outputs to sys.stdout. When both ``verbose`` and ``progress`` are ``False``, runs silently. :param verbose: Boolean value; when ``True``, show output from make command. Default is ``False``. :param progress: Boolean value; when ``True`` display progress progress bar. Default is ``True``. :param cores: Integer, number of cores to use in the ``make`` command. Default is 1 core. """cmd=[MAKE,'build',f'-j{cores}']try:ifverbose:do_command(cmd)elifprogressandprogbar.allow_show_progress():progress_hook:Any=_wrap_build_progress_hook()do_command(cmd,fd_out=None,pbar=progress_hook)else:do_command(cmd,fd_out=None)exceptRuntimeErrorase:# pylint: disable=raise-missing-fromraiseCmdStanInstallError(f'Command "make build" failed\n{str(e)}')ifnotos.path.exists(os.path.join('bin','stansummary'+EXTENSION)):raiseCmdStanInstallError(f'bin/stansummary{EXTENSION} not found'', please rebuild or report a bug!')ifnotos.path.exists(os.path.join('bin','diagnose'+EXTENSION)):raiseCmdStanInstallError(f'bin/stansummary{EXTENSION} not found'', please rebuild or report a bug!')ifis_windows():# Add tbb to the $PATH on Windowslibtbb=os.path.join(os.getcwd(),'stan','lib','stan_math','lib','tbb')os.environ['PATH']=';'.join(list(OrderedDict.fromkeys([libtbb]+os.environ.get('PATH','').split(';'))))@progbar.wrap_callbackdef_wrap_build_progress_hook()->Optional[Callable[[str],None]]:"""Sets up tqdm callback for CmdStan sampler console msgs."""pad=' '*20msgs_expected=150# hack: 2.27 make build send ~140 msgs to consolepbar:tqdm=tqdm(total=msgs_expected,bar_format="{desc} ({elapsed}) | {bar} | {postfix[0][value]}",postfix=[{"value":f'Building CmdStan {pad}'}],colour='blue',desc='',position=0,)defbuild_progress_hook(line:str)->None:ifline.startswith('--- CmdStan'):pbar.set_description('Done')pbar.postfix[0]["value"]=linepbar.update(msgs_expected-pbar.n)pbar.close()else:ifline.startswith('--'):pbar.postfix[0]["value"]=lineelse:pbar.postfix[0]["value"]=f'{line[:8]} ... {line[-20:]}'pbar.set_description('Compiling')pbar.update(1)returnbuild_progress_hookdefcompile_example(verbose:bool=False)->None:""" Compile the example model. The current directory must be a cmdstan installation, i.e., contains the makefile, Stanc compiler, and all libraries. :param verbose: Boolean value; when ``True``, show output from make command. """path=Path('examples','bernoulli','bernoulli').with_suffix(EXTENSION)ifpath.is_file():path.unlink()cmd=[MAKE,path.as_posix()]try:ifverbose:do_command(cmd)else:do_command(cmd,fd_out=None)exceptRuntimeErrorase:# pylint: disable=raise-missing-fromraiseCmdStanInstallError(f'Command "{" ".join(cmd)}" failed:\n{e}')ifnotpath.is_file():raiseCmdStanInstallError("Failed to generate example binary")
[docs]defrebuild_cmdstan(verbose:bool=False,progress:bool=True,cores:int=1)->None:""" Rebuilds the existing CmdStan installation. This assumes CmdStan has already been installed, though it need not be installed via CmdStanPy for this function to work. :param verbose: Boolean value; when ``True``, show output from make command. Default is ``False``. :param progress: Boolean value; when ``True`` display progress progress bar. Default is ``True``. :param cores: Integer, number of cores to use in the ``make`` command. Default is 1 core. """try:withpushd(cmdstan_path()):clean_all(verbose)build(verbose,progress,cores)compile_example(verbose)exceptValueErrorase:raiseCmdStanInstallError("Failed to rebuild CmdStan. Are you sure it is installed?")frome
definstall_version(cmdstan_version:str,overwrite:bool=False,verbose:bool=False,progress:bool=True,cores:int=1,)->None:""" Build specified CmdStan version by spawning subprocesses to run the Make utility on the downloaded CmdStan release src files. Assumes that current working directory is parent of release dir. :param cmdstan_version: CmdStan release, corresponds to release dirname. :param overwrite: when ``True``, run ``make clean-all`` before building. :param verbose: Boolean value; when ``True``, show output from make command. """withpushd(cmdstan_version):print('Building version {}, may take several minutes, ''depending on your system.'.format(cmdstan_version))ifoverwriteandos.path.exists('.'):print('Overwrite requested, remove existing build of version ''{}'.format(cmdstan_version))clean_all(verbose)print('Rebuilding version {}'.format(cmdstan_version))build(verbose,progress=progress,cores=cores)print('Installed {}'.format(cmdstan_version))defis_version_available(version:str)->bool:if'git:'inversion:returnTrue# no good way in general to check if a git tag existsis_available=Trueurl=get_download_url(version)foriinrange(6):try:urllib.request.urlopen(url)excepturllib.error.HTTPErroraserr:print(f'Release {version} is unavailable from URL {url}')print(f'HTTPError: {err.code}')is_available=Falsebreakexcepturllib.error.URLErrorase:ifi<5:print('checking version {} availability, retry ({}/5)'.format(version,i+1))sleep(1)continueprint('Release {} is unavailable from URL {}'.format(version,url))print('URLError: {}'.format(e.reason))is_available=Falsereturnis_availabledefretrieve_version(version:str,progress:bool=True)->None:"""Download specified CmdStan version."""ifversionisNoneorversion=='':raiseValueError('Argument "version" unspecified.')if'git:'inversion:tag=version.split(':')[1]tag_folder=version.replace(':','-').replace('/','_')print(f"Cloning CmdStan branch '{tag}' from stan-dev/cmdstan on GitHub")do_command(['git','clone','--depth','1','--branch',tag,'--recursive','--shallow-submodules','https://github.com/stan-dev/cmdstan.git',f'cmdstan-{tag_folder}',])returnprint('Downloading CmdStan version {}'.format(version))url=get_download_url(version)foriinrange(6):# always retry to allow for transient URLErrorstry:ifprogressandprogbar.allow_show_progress():progress_hook:Optional[Callable[[int,int,int],None]]=wrap_url_progress_hook()else:progress_hook=Nonefile_tmp,_=urllib.request.urlretrieve(url,filename=None,reporthook=progress_hook)breakexcepturllib.error.HTTPErrorase:raiseCmdStanRetrieveError('HTTPError: {}\n''Version {} not available from github.com.'.format(e.code,version))fromeexcepturllib.error.URLErrorase:print('Failed to download CmdStan version {} from github.com'.format(version))print(e)ifi<5:print('retry ({}/5)'.format(i+1))sleep(1)continueprint('Version {} not available from github.com.'.format(version))raiseCmdStanRetrieveError('Version {} not available from github.com.'.format(version))fromeprint('Download successful, file: {}'.format(file_tmp))try:print('Extracting distribution')tar=tarfile.open(file_tmp)first=tar.next()iffirstisnotNone:top_dir=first.nameelse:top_dir=''cmdstan_dir=f'cmdstan-{version}'iftop_dir!=cmdstan_dir:raiseCmdStanInstallError('tarfile should contain top-level dir {},''but found dir {} instead.'.format(cmdstan_dir,top_dir))target=os.getcwd()ifis_windows():# fixes long-path limitation on Windowstarget=r'\\?\{}'.format(target)ifprogressandprogbar.allow_show_progress():formemberintqdm(iterable=tar.getmembers(),total=len(tar.getmembers()),colour='blue',leave=False,):tar.extract(member=member)else:tar.extractall()exceptExceptionase:# pylint: disable=broad-exceptraiseCmdStanInstallError(f'Failed to unpack file {file_tmp}, error:\n\t{str(e)}')fromefinally:tar.close()print(f'Unpacked download as {cmdstan_dir}')defrun_compiler_install(dir:str,verbose:bool,progress:bool)->None:from.install_cxx_toolchainimportis_installedas_is_installed_cxxfrom.install_cxx_toolchainimportrun_rtools_installas_main_cxxfrom.utilsimportcxx_toolchain_pathcompiler_found=Falsertools40_home=os.environ.get('RTOOLS40_HOME')forcxx_locin([rtools40_home]ifrtools40_homeisnotNoneelse[])+[home_cmdstan(),os.path.join(os.path.abspath("/"),"RTools40"),os.path.join(os.path.abspath("/"),"RTools"),os.path.join(os.path.abspath("/"),"RTools35"),os.path.join(os.path.abspath("/"),"RBuildTools"),]:forcxx_versionin['40','35']:if_is_installed_cxx(cxx_loc,cxx_version):compiler_found=Truebreakifcompiler_found:breakifnotcompiler_found:print('Installing RTools40')# copy argv and clear sys.argv_main_cxx({'dir':dir,'progress':progress,'version':None,'verbose':verbose,})cxx_version='40'# Add toolchain to $PATHcxx_toolchain_path(cxx_version,dir)defrun_install(args:Union[InteractiveSettings,InstallationSettings])->None:""" Run a (potentially interactive) installation """validate_dir(args.dir)print('CmdStan install directory: {}'.format(args.dir))# these accesses just 'warm up' the interactive install_=args.progress_=args.verboseifargs.compiler:run_compiler_install(args.dir,args.verbose,args.progress)if'git:'inargs.version:tag=args.version.replace(':','-').replace('/','_')cmdstan_version=f'cmdstan-{tag}'else:cmdstan_version=f'cmdstan-{args.version}'withpushd(args.dir):already_installed=os.path.exists(cmdstan_version)andos.path.exists(os.path.join(cmdstan_version,'examples','bernoulli','bernoulli'+EXTENSION,))ifnotalready_installedorargs.overwrite:ifis_version_available(args.version):print('Installing CmdStan version: {}'.format(args.version))else:raiseValueError(f'Version {args.version} cannot be downloaded. ''Connection to GitHub failed. ''Check firewall settings or ensure this version exists.')shutil.rmtree(cmdstan_version,ignore_errors=True)retrieve_version(args.version,args.progress)install_version(cmdstan_version=cmdstan_version,overwrite=already_installedandargs.overwrite,verbose=args.verbose,progress=args.progress,cores=args.cores,)else:print('CmdStan version {} already installed'.format(args.version))withpushd(cmdstan_version):print('Test model compilation')compile_example(args.verbose)defparse_cmdline_args()->Dict[str,Any]:parser=argparse.ArgumentParser("install_cmdstan")parser.add_argument('--interactive','-i',action='store_true',help="Ignore other arguments and run the installation in "+"interactive mode",)parser.add_argument('--version','-v',help="version, defaults to latest release version. ""If git is installed, you can also specify a git tag or branch, ""e.g. git:develop",)parser.add_argument('--dir','-d',help="install directory, defaults to '$HOME/.cmdstan")parser.add_argument('--overwrite',action='store_true',help="flag, when specified re-installs existing version",)parser.add_argument('--verbose',action='store_true',help="flag, when specified prints output from CmdStan build process",)parser.add_argument('--progress',action='store_true',help="flag, when specified show progress bar for CmdStan download",)parser.add_argument("--cores",default=1,type=int,help="number of cores to use while building",)ifis_windows():# use compiler installed with install_cxx_toolchain# Install a new compiler if compiler not found# Search order is RTools40, RTools35parser.add_argument('--compiler','-c',dest='compiler',action='store_true',help="flag, add C++ compiler to path (Windows only)",)returnvars(parser.parse_args(sys.argv[1:]))def__main__()->None:args=parse_cmdline_args()ifargs.get('interactive',False):run_install(InteractiveSettings())else:run_install(InstallationSettings(**args))if__name__=='__main__':__main__()