"""Utilities for finding and installing CmdStan"""importosimportplatformimportsubprocessimportsysfromcollectionsimportOrderedDictfromtypingimportCallable,Dict,Optional,Tuple,Unionfromtqdm.autoimporttqdmfromcmdstanpyimport_DOT_CMDSTANfrom..importprogressasprogbarfrom.loggingimportget_loggerEXTENSION='.exe'ifplatform.system()=='Windows'else''defdetermine_linux_arch()->str:machine=platform.machine()arch=""ifmachine=="aarch64":arch="arm64"elifmachine=="armv7l":# Telling armel and armhf apart is nontrivial# c.f. https://forums.raspberrypi.com/viewtopic.php?t=20873readelf=subprocess.run(["readelf","-A","/proc/self/exe"],check=True,stdout=subprocess.PIPE,text=True,)if"Tag_ABI_VFP_args"inreadelf.stdout:arch="armel"else:arch="armhf"elifmachine=="mips64":arch="mips64el"elifmachine=="ppc64el"ormachine=="ppc64le":arch="ppc64el"elifmachine=="s390x":arch="s390x"returnarchdefget_download_url(version:str)->str:arch=os.environ.get("CMDSTAN_ARCH","")ifnotarchandplatform.system()=="Linux":arch=determine_linux_arch()ifarchandarch.lower()!="false":url_end=f'v{version}/cmdstan-{version}-linux-{arch}.tar.gz'else:url_end=f'v{version}/cmdstan-{version}.tar.gz'returnf'https://github.com/stan-dev/cmdstan/releases/download/{url_end}'defvalidate_dir(install_dir:str)->None:"""Check that specified install directory exists, can write."""ifnotos.path.exists(install_dir):try:os.makedirs(install_dir)except(IOError,OSError,PermissionError)ase:raiseValueError('Cannot create directory: {}'.format(install_dir))fromeelse:ifnotos.path.isdir(install_dir):raiseValueError('File exists, should be a directory: {}'.format(install_dir))try:withopen('tmp_test_w','w'):passos.remove('tmp_test_w')# cleanupexceptOSErrorase:raiseValueError('Cannot write files to directory {}'.format(install_dir))fromedefget_latest_cmdstan(cmdstan_dir:str)->Optional[str]:""" Given a valid directory path, find all installed CmdStan versions and return highest (i.e., latest) version number. Assumes directory consists of CmdStan releases, created by function `install_cmdstan`, and therefore dirnames have format "cmdstan-<maj>.<min>.<patch>" or "cmdstan-<maj>.<min>.<patch>-rc<num>", which is CmdStan release practice as of v 2.24. """versions=[name[8:]fornameinos.listdir(cmdstan_dir)ifos.path.isdir(os.path.join(cmdstan_dir,name))andname.startswith('cmdstan-')]iflen(versions)==0:returnNoneiflen(versions)==1:return'cmdstan-'+versions[0]# we can only compare numeric versionsversions=[vforvinversionsifv[0].isdigit()andv.count('.')==2]# munge rc for sort, e.g. 2.25.0-rc1 -> 2.25.-99foriinrange(len(versions)):# # pylint: disable=C0200if'-rc'inversions[i]:comps=versions[i].split('-rc')mmp=comps[0].split('.')rc_num=comps[1]patch=str(int(rc_num)-100)versions[i]='.'.join([mmp[0],mmp[1],patch])versions.sort(key=lambdas:list(map(int,s.split('.'))))latest=versions[len(versions)-1]# unmunge as neededmmp=latest.split('.')ifint(mmp[2])<0:rc_num=str(int(mmp[2])+100)mmp[2]="0-rc"+rc_numlatest='.'.join(mmp)return'cmdstan-'+latestdefvalidate_cmdstan_path(path:str)->None:""" Validate that CmdStan directory exists and binaries have been built. Throws exception if specified path is invalid. """ifnotos.path.isdir(path):raiseValueError(f'No CmdStan directory, path {path} does not exist.')ifnotos.path.exists(os.path.join(path,'bin','stanc'+EXTENSION)):raiseValueError(f'CmdStan installataion missing binaries in {path}/bin. ''Re-install cmdstan by running command "install_cmdstan ''--overwrite", or Python code "import cmdstanpy; ''cmdstanpy.install_cmdstan(overwrite=True)"')
[docs]defset_cmdstan_path(path:str)->None:""" Validate, then set CmdStan directory path. """validate_cmdstan_path(path)os.environ['CMDSTAN']=path
[docs]defset_make_env(make:str)->None:""" set MAKE environmental variable. """os.environ['MAKE']=make
[docs]defcmdstan_path()->str:""" Validate, then return CmdStan directory path. """cmdstan=''if'CMDSTAN'inos.environandlen(os.environ['CMDSTAN'])>0:cmdstan=os.environ['CMDSTAN']else:cmdstan_dir=os.path.expanduser(os.path.join('~',_DOT_CMDSTAN))ifnotos.path.exists(cmdstan_dir):raiseValueError('No CmdStan installation found, run command "install_cmdstan"''or (re)activate your conda environment!')latest_cmdstan=get_latest_cmdstan(cmdstan_dir)iflatest_cmdstanisNone:raiseValueError('No CmdStan installation found, run command "install_cmdstan"''or (re)activate your conda environment!')cmdstan=os.path.join(cmdstan_dir,latest_cmdstan)os.environ['CMDSTAN']=cmdstanvalidate_cmdstan_path(cmdstan)returnos.path.normpath(cmdstan)
[docs]defcmdstan_version()->Optional[Tuple[int,...]]:""" Parses version string out of CmdStan makefile variable CMDSTAN_VERSION, returns Tuple(Major, minor). If CmdStan installation is not found or cannot parse version from makefile logs warning and returns None. Lenient behavoir required for CI tests, per comment: https://github.com/stan-dev/cmdstanpy/pull/321#issuecomment-733817554 """try:makefile=os.path.join(cmdstan_path(),'makefile')exceptValueErrorase:get_logger().info('No CmdStan installation found.')get_logger().debug("%s",e)returnNoneifnotos.path.exists(makefile):get_logger().info('CmdStan installation %s missing makefile, cannot get version.',cmdstan_path(),)returnNonewithopen(makefile,'r')asfd:contents=fd.read()start_idx=contents.find('CMDSTAN_VERSION := ')ifstart_idx<0:get_logger().info('Cannot parse version from makefile: %s.',makefile,)returnNonestart_idx+=len('CMDSTAN_VERSION := ')end_idx=contents.find('\n',start_idx)version=contents[start_idx:end_idx]splits=version.split('.')iflen(splits)!=3:get_logger().info('Cannot parse version, expected "<major>.<minor>.<patch>", ''found: "%s".',version,)returnNonereturntuple(int(x)forxinsplits[0:2])
defcmdstan_version_before(major:int,minor:int,info:Optional[Dict[str,str]]=None)->bool:""" Check that CmdStan version is less than Major.minor version. :param major: Major version number :param minor: Minor version number :return: True if version at or above major.minor, else False. """cur_version=NoneifinfoisNoneor'stan_version_major'notininfo:cur_version=cmdstan_version()else:cur_version=(int(info['stan_version_major']),int(info['stan_version_minor']),)ifcur_versionisNone:get_logger().info('Cannot determine whether version is before %d.%d.',major,minor)returnFalseifcur_version[0]<majoror(cur_version[0]==majorandcur_version[1]<minor):returnTruereturnFalsedefcxx_toolchain_path(version:Optional[str]=None,install_dir:Optional[str]=None)->Tuple[str,...]:""" Validate, then activate C++ toolchain directory path. """ifplatform.system()!='Windows':raiseRuntimeError('Functionality is currently only supported on Windows')ifversionisnotNoneandnotisinstance(version,str):raiseTypeError('Format version number as a string')logger=get_logger()if'CMDSTAN_TOOLCHAIN'inos.environ:toolchain_root=os.environ['CMDSTAN_TOOLCHAIN']ifos.path.exists(os.path.join(toolchain_root,'mingw64')):compiler_path=os.path.join(toolchain_root,'mingw64'if(sys.maxsize>2**32)else'mingw32','bin',)ifos.path.exists(compiler_path):tool_path=os.path.join(toolchain_root,'usr','bin')ifnotos.path.exists(tool_path):tool_path=''compiler_path=''logger.warning('Found invalid installion for RTools40 on %s',toolchain_root,)toolchain_root=''else:compiler_path=''logger.warning('Found invalid installion for RTools40 on %s',toolchain_root,)toolchain_root=''elifos.path.exists(os.path.join(toolchain_root,'mingw_64')):compiler_path=os.path.join(toolchain_root,'mingw_64'if(sys.maxsize>2**32)else'mingw_32','bin',)ifos.path.exists(compiler_path):tool_path=os.path.join(toolchain_root,'bin')ifnotos.path.exists(tool_path):tool_path=''compiler_path=''logger.warning('Found invalid installion for RTools35 on %s',toolchain_root,)toolchain_root=''else:compiler_path=''logger.warning('Found invalid installion for RTools35 on %s',toolchain_root,)toolchain_root=''else:rtools40_home=os.environ.get('RTOOLS40_HOME')cmdstan_dir=os.path.expanduser(os.path.join('~',_DOT_CMDSTAN))fortoolchain_rootin(([rtools40_home]ifrtools40_homeisnotNoneelse[])+([os.path.join(install_dir,'RTools40'),os.path.join(install_dir,'RTools35'),os.path.join(install_dir,'RTools30'),os.path.join(install_dir,'RTools'),]ifinstall_dirisnotNoneelse[])+[os.path.join(cmdstan_dir,'RTools40'),os.path.join(os.path.abspath("/"),"RTools40"),os.path.join(cmdstan_dir,'RTools35'),os.path.join(os.path.abspath("/"),"RTools35"),os.path.join(cmdstan_dir,'RTools'),os.path.join(os.path.abspath("/"),"RTools"),os.path.join(os.path.abspath("/"),"RBuildTools"),]):compiler_path=''tool_path=''ifos.path.exists(toolchain_root):ifversionnotin('35','3.5','3'):compiler_path=os.path.join(toolchain_root,'mingw64'if(sys.maxsize>2**32)else'mingw32','bin',)ifos.path.exists(compiler_path):tool_path=os.path.join(toolchain_root,'usr','bin')ifnotos.path.exists(tool_path):tool_path=''compiler_path=''logger.warning('Found invalid installation for RTools40 on %s',toolchain_root,)toolchain_root=''else:breakelse:compiler_path=''logger.warning('Found invalid installation for RTools40 on %s',toolchain_root,)toolchain_root=''else:compiler_path=os.path.join(toolchain_root,'mingw_64'if(sys.maxsize>2**32)else'mingw_32','bin',)ifos.path.exists(compiler_path):tool_path=os.path.join(toolchain_root,'bin')ifnotos.path.exists(tool_path):tool_path=''compiler_path=''logger.warning('Found invalid installation for RTools35 on %s',toolchain_root,)toolchain_root=''else:breakelse:compiler_path=''logger.warning('Found invalid installation for RTools35 on %s',toolchain_root,)toolchain_root=''else:toolchain_root=''ifnottoolchain_root:raiseValueError('no RTools toolchain installation found, ''run command line script ''"python -m cmdstanpy.install_cxx_toolchain"')logger.info('Add C++ toolchain to $PATH: %s',toolchain_root)os.environ['PATH']=';'.join(list(OrderedDict.fromkeys([compiler_path,tool_path]+os.getenv('PATH','').split(';'))))returncompiler_path,tool_path
[docs]definstall_cmdstan(version:Optional[str]=None,dir:Optional[str]=None,overwrite:bool=False,compiler:bool=False,progress:bool=False,verbose:bool=False,cores:int=1,*,interactive:bool=False,)->bool:""" 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 building example model ``bernoulli.stan``. :param version: CmdStan version string, e.g. "2.29.2". Defaults to latest CmdStan release. If ``git`` is installed, a git tag or branch of stan-dev/cmdstan can be specified, e.g. "git:develop". :param dir: Path to install directory. Defaults to hidden directory ``$HOME/.cmdstan``. If no directory is specified and the above directory does not exist, directory ``$HOME/.cmdstan`` will be created and populated. :param overwrite: Boolean value; when ``True``, will overwrite and rebuild an existing CmdStan installation. Default is ``False``. :param compiler: Boolean value; when ``True`` on WINDOWS ONLY, use the C++ compiler from the ``install_cxx_toolchain`` command or install one if none is found. :param progress: Boolean value; when ``True``, show a progress bar for downloading and unpacking CmdStan. Default is ``False``. :param verbose: Boolean value; when ``True``, show console output from all intallation steps, i.e., download, build, and test CmdStan release. Default is ``False``. :param cores: Integer, number of cores to use in the ``make`` command. Default is 1 core. :param interactive: Boolean value; if true, ignore all other arguments to this function and run in an interactive mode, prompting the user to provide the other information manually through the standard input. This flag should only be used in interactive environments, e.g. on the command line. :return: Boolean value; ``True`` for success. """logger=get_logger()try:from..install_cmdstanimport(InstallationSettings,InteractiveSettings,run_install,)args:Union[InstallationSettings,InteractiveSettings]ifinteractive:ifany([version,dir,overwrite,compiler,progress,verbose,cores!=1,]):logger.warning("Interactive installation requested but other arguments"" were used.\n\tThese values will be ignored!")args=InteractiveSettings()else:args=InstallationSettings(version=version,overwrite=overwrite,verbose=verbose,compiler=compiler,progress=progress,dir=dir,cores=cores,)run_install(args)# pylint: disable=broad-exceptexceptExceptionase:logger.warning('CmdStan installation failed.\n%s',str(e))returnFalseif'git:'inargs.version:folder=f"cmdstan-{args.version.replace(':','-').replace('/','_')}"else:folder=f"cmdstan-{args.version}"set_cmdstan_path(os.path.join(args.dir,folder))returnTrue
@progbar.wrap_callbackdefwrap_url_progress_hook()->Optional[Callable[[int,int,int],None]]:"""Sets up tqdm callback for url downloads."""pbar:tqdm=tqdm(unit='B',unit_scale=True,unit_divisor=1024,colour='blue',leave=False,)defdownload_progress_hook(count:int,block_size:int,total_size:int)->None:ifpbar.totalisNone:pbar.total=total_sizepbar.reset()downloaded_size=count*block_sizepbar.update(downloaded_size-pbar.n)ifpbar.n>=total_size:pbar.close()returndownload_progress_hook