|  | """ | 
|  | Virtual environment (venv) package for Python. Based on PEP 405. | 
|  |  | 
|  | Copyright (C) 2011-2014 Vinay Sajip. | 
|  | Licensed to the PSF under a contributor agreement. | 
|  | """ | 
|  | import logging | 
|  | import os | 
|  | import shutil | 
|  | import subprocess | 
|  | import sys | 
|  | import sysconfig | 
|  | import types | 
|  |  | 
|  |  | 
|  | CORE_VENV_DEPS = ('pip', 'setuptools') | 
|  | logger = logging.getLogger(__name__) | 
|  |  | 
|  |  | 
|  | class EnvBuilder: | 
|  | """ | 
|  | This class exists to allow virtual environment creation to be | 
|  | customized. The constructor parameters determine the builder's | 
|  | behaviour when called upon to create a virtual environment. | 
|  |  | 
|  | By default, the builder makes the system (global) site-packages dir | 
|  | *un*available to the created environment. | 
|  |  | 
|  | If invoked using the Python -m option, the default is to use copying | 
|  | on Windows platforms but symlinks elsewhere. If instantiated some | 
|  | other way, the default is to *not* use symlinks. | 
|  |  | 
|  | :param system_site_packages: If True, the system (global) site-packages | 
|  | dir is available to created environments. | 
|  | :param clear: If True, delete the contents of the environment directory if | 
|  | it already exists, before environment creation. | 
|  | :param symlinks: If True, attempt to symlink rather than copy files into | 
|  | virtual environment. | 
|  | :param upgrade: If True, upgrade an existing virtual environment. | 
|  | :param with_pip: If True, ensure pip is installed in the virtual | 
|  | environment | 
|  | :param prompt: Alternative terminal prefix for the environment. | 
|  | :param upgrade_deps: Update the base venv modules to the latest on PyPI | 
|  | """ | 
|  |  | 
|  | def __init__(self, system_site_packages=False, clear=False, | 
|  | symlinks=False, upgrade=False, with_pip=False, prompt=None, | 
|  | upgrade_deps=False): | 
|  | self.system_site_packages = system_site_packages | 
|  | self.clear = clear | 
|  | self.symlinks = symlinks | 
|  | self.upgrade = upgrade | 
|  | self.with_pip = with_pip | 
|  | self.orig_prompt = prompt | 
|  | if prompt == '.':  # see bpo-38901 | 
|  | prompt = os.path.basename(os.getcwd()) | 
|  | self.prompt = prompt | 
|  | self.upgrade_deps = upgrade_deps | 
|  |  | 
|  | def create(self, env_dir): | 
|  | """ | 
|  | Create a virtual environment in a directory. | 
|  |  | 
|  | :param env_dir: The target directory to create an environment in. | 
|  |  | 
|  | """ | 
|  | env_dir = os.path.abspath(env_dir) | 
|  | context = self.ensure_directories(env_dir) | 
|  | # See issue 24875. We need system_site_packages to be False | 
|  | # until after pip is installed. | 
|  | true_system_site_packages = self.system_site_packages | 
|  | self.system_site_packages = False | 
|  | self.create_configuration(context) | 
|  | self.setup_python(context) | 
|  | if self.with_pip: | 
|  | self._setup_pip(context) | 
|  | if not self.upgrade: | 
|  | self.setup_scripts(context) | 
|  | self.post_setup(context) | 
|  | if true_system_site_packages: | 
|  | # We had set it to False before, now | 
|  | # restore it and rewrite the configuration | 
|  | self.system_site_packages = True | 
|  | self.create_configuration(context) | 
|  | if self.upgrade_deps: | 
|  | self.upgrade_dependencies(context) | 
|  |  | 
|  | def clear_directory(self, path): | 
|  | for fn in os.listdir(path): | 
|  | fn = os.path.join(path, fn) | 
|  | if os.path.islink(fn) or os.path.isfile(fn): | 
|  | os.remove(fn) | 
|  | elif os.path.isdir(fn): | 
|  | shutil.rmtree(fn) | 
|  |  | 
|  | def ensure_directories(self, env_dir): | 
|  | """ | 
|  | Create the directories for the environment. | 
|  |  | 
|  | Returns a context object which holds paths in the environment, | 
|  | for use by subsequent logic. | 
|  | """ | 
|  |  | 
|  | def create_if_needed(d): | 
|  | if not os.path.exists(d): | 
|  | os.makedirs(d) | 
|  | elif os.path.islink(d) or os.path.isfile(d): | 
|  | raise ValueError('Unable to create directory %r' % d) | 
|  |  | 
|  | if os.path.exists(env_dir) and self.clear: | 
|  | self.clear_directory(env_dir) | 
|  | context = types.SimpleNamespace() | 
|  | context.env_dir = env_dir | 
|  | context.env_name = os.path.split(env_dir)[1] | 
|  | prompt = self.prompt if self.prompt is not None else context.env_name | 
|  | context.prompt = '(%s) ' % prompt | 
|  | create_if_needed(env_dir) | 
|  | executable = sys._base_executable | 
|  | dirname, exename = os.path.split(os.path.abspath(executable)) | 
|  | context.executable = executable | 
|  | context.python_dir = dirname | 
|  | context.python_exe = exename | 
|  | if sys.platform == 'win32': | 
|  | binname = 'Scripts' | 
|  | incpath = 'Include' | 
|  | libpath = os.path.join(env_dir, 'Lib', 'site-packages') | 
|  | else: | 
|  | binname = 'bin' | 
|  | incpath = 'include' | 
|  | libpath = os.path.join(env_dir, 'lib', | 
|  | 'python%d.%d' % sys.version_info[:2], | 
|  | 'site-packages') | 
|  | context.inc_path = path = os.path.join(env_dir, incpath) | 
|  | create_if_needed(path) | 
|  | create_if_needed(libpath) | 
|  | # Issue 21197: create lib64 as a symlink to lib on 64-bit non-OS X POSIX | 
|  | if ((sys.maxsize > 2**32) and (os.name == 'posix') and | 
|  | (sys.platform != 'darwin')): | 
|  | link_path = os.path.join(env_dir, 'lib64') | 
|  | if not os.path.exists(link_path):   # Issue #21643 | 
|  | os.symlink('lib', link_path) | 
|  | context.bin_path = binpath = os.path.join(env_dir, binname) | 
|  | context.bin_name = binname | 
|  | context.env_exe = os.path.join(binpath, exename) | 
|  | create_if_needed(binpath) | 
|  | # Assign and update the command to use when launching the newly created | 
|  | # environment, in case it isn't simply the executable script (e.g. bpo-45337) | 
|  | context.env_exec_cmd = context.env_exe | 
|  | if sys.platform == 'win32': | 
|  | # bpo-45337: Fix up env_exec_cmd to account for file system redirections. | 
|  | # Some redirects only apply to CreateFile and not CreateProcess | 
|  | real_env_exe = os.path.realpath(context.env_exe) | 
|  | if os.path.normcase(real_env_exe) != os.path.normcase(context.env_exe): | 
|  | logger.warning('Actual environment location may have moved due to ' | 
|  | 'redirects, links or junctions.\n' | 
|  | '  Requested location: "%s"\n' | 
|  | '  Actual location:    "%s"', | 
|  | context.env_exe, real_env_exe) | 
|  | context.env_exec_cmd = real_env_exe | 
|  | return context | 
|  |  | 
|  | def create_configuration(self, context): | 
|  | """ | 
|  | Create a configuration file indicating where the environment's Python | 
|  | was copied from, and whether the system site-packages should be made | 
|  | available in the environment. | 
|  |  | 
|  | :param context: The information for the environment creation request | 
|  | being processed. | 
|  | """ | 
|  | context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg') | 
|  | with open(path, 'w', encoding='utf-8') as f: | 
|  | f.write('home = %s\n' % context.python_dir) | 
|  | if self.system_site_packages: | 
|  | incl = 'true' | 
|  | else: | 
|  | incl = 'false' | 
|  | f.write('include-system-site-packages = %s\n' % incl) | 
|  | f.write('version = %d.%d.%d\n' % sys.version_info[:3]) | 
|  | if self.prompt is not None: | 
|  | f.write(f'prompt = {self.prompt!r}\n') | 
|  | f.write('executable = %s\n' % os.path.realpath(sys.executable)) | 
|  | args = [] | 
|  | nt = os.name == 'nt' | 
|  | if nt and self.symlinks: | 
|  | args.append('--symlinks') | 
|  | if not nt and not self.symlinks: | 
|  | args.append('--copies') | 
|  | if not self.with_pip: | 
|  | args.append('--without-pip') | 
|  | if self.system_site_packages: | 
|  | args.append('--system-site-packages') | 
|  | if self.clear: | 
|  | args.append('--clear') | 
|  | if self.upgrade: | 
|  | args.append('--upgrade') | 
|  | if self.upgrade_deps: | 
|  | args.append('--upgrade-deps') | 
|  | if self.orig_prompt is not None: | 
|  | args.append(f'--prompt="{self.orig_prompt}"') | 
|  |  | 
|  | args.append(context.env_dir) | 
|  | args = ' '.join(args) | 
|  | f.write(f'command = {sys.executable} -m venv {args}\n') | 
|  |  | 
|  | if os.name != 'nt': | 
|  | def symlink_or_copy(self, src, dst, relative_symlinks_ok=False): | 
|  | """ | 
|  | Try symlinking a file, and if that fails, fall back to copying. | 
|  | """ | 
|  | force_copy = not self.symlinks | 
|  | if not force_copy: | 
|  | try: | 
|  | if not os.path.islink(dst): # can't link to itself! | 
|  | if relative_symlinks_ok: | 
|  | assert os.path.dirname(src) == os.path.dirname(dst) | 
|  | os.symlink(os.path.basename(src), dst) | 
|  | else: | 
|  | os.symlink(src, dst) | 
|  | except Exception:   # may need to use a more specific exception | 
|  | logger.warning('Unable to symlink %r to %r', src, dst) | 
|  | force_copy = True | 
|  | if force_copy: | 
|  | shutil.copyfile(src, dst) | 
|  | else: | 
|  | def symlink_or_copy(self, src, dst, relative_symlinks_ok=False): | 
|  | """ | 
|  | Try symlinking a file, and if that fails, fall back to copying. | 
|  | """ | 
|  | bad_src = os.path.lexists(src) and not os.path.exists(src) | 
|  | if self.symlinks and not bad_src and not os.path.islink(dst): | 
|  | try: | 
|  | if relative_symlinks_ok: | 
|  | assert os.path.dirname(src) == os.path.dirname(dst) | 
|  | os.symlink(os.path.basename(src), dst) | 
|  | else: | 
|  | os.symlink(src, dst) | 
|  | return | 
|  | except Exception:   # may need to use a more specific exception | 
|  | logger.warning('Unable to symlink %r to %r', src, dst) | 
|  |  | 
|  | # On Windows, we rewrite symlinks to our base python.exe into | 
|  | # copies of venvlauncher.exe | 
|  | basename, ext = os.path.splitext(os.path.basename(src)) | 
|  | srcfn = os.path.join(os.path.dirname(__file__), | 
|  | "scripts", | 
|  | "nt", | 
|  | basename + ext) | 
|  | # Builds or venv's from builds need to remap source file | 
|  | # locations, as we do not put them into Lib/venv/scripts | 
|  | if sysconfig.is_python_build(True) or not os.path.isfile(srcfn): | 
|  | if basename.endswith('_d'): | 
|  | ext = '_d' + ext | 
|  | basename = basename[:-2] | 
|  | if basename == 'python': | 
|  | basename = 'venvlauncher' | 
|  | elif basename == 'pythonw': | 
|  | basename = 'venvwlauncher' | 
|  | src = os.path.join(os.path.dirname(src), basename + ext) | 
|  | else: | 
|  | src = srcfn | 
|  | if not os.path.exists(src): | 
|  | if not bad_src: | 
|  | logger.warning('Unable to copy %r', src) | 
|  | return | 
|  |  | 
|  | shutil.copyfile(src, dst) | 
|  |  | 
|  | def setup_python(self, context): | 
|  | """ | 
|  | Set up a Python executable in the environment. | 
|  |  | 
|  | :param context: The information for the environment creation request | 
|  | being processed. | 
|  | """ | 
|  | binpath = context.bin_path | 
|  | path = context.env_exe | 
|  | copier = self.symlink_or_copy | 
|  | dirname = context.python_dir | 
|  | if os.name != 'nt': | 
|  | copier(context.executable, path) | 
|  | if not os.path.islink(path): | 
|  | os.chmod(path, 0o755) | 
|  | for suffix in ('python', 'python3', f'python3.{sys.version_info[1]}'): | 
|  | path = os.path.join(binpath, suffix) | 
|  | if not os.path.exists(path): | 
|  | # Issue 18807: make copies if | 
|  | # symlinks are not wanted | 
|  | copier(context.env_exe, path, relative_symlinks_ok=True) | 
|  | if not os.path.islink(path): | 
|  | os.chmod(path, 0o755) | 
|  | else: | 
|  | if self.symlinks: | 
|  | # For symlinking, we need a complete copy of the root directory | 
|  | # If symlinks fail, you'll get unnecessary copies of files, but | 
|  | # we assume that if you've opted into symlinks on Windows then | 
|  | # you know what you're doing. | 
|  | suffixes = [ | 
|  | f for f in os.listdir(dirname) if | 
|  | os.path.normcase(os.path.splitext(f)[1]) in ('.exe', '.dll') | 
|  | ] | 
|  | if sysconfig.is_python_build(True): | 
|  | suffixes = [ | 
|  | f for f in suffixes if | 
|  | os.path.normcase(f).startswith(('python', 'vcruntime')) | 
|  | ] | 
|  | else: | 
|  | suffixes = {'python.exe', 'python_d.exe', 'pythonw.exe', 'pythonw_d.exe'} | 
|  | base_exe = os.path.basename(context.env_exe) | 
|  | suffixes.add(base_exe) | 
|  |  | 
|  | for suffix in suffixes: | 
|  | src = os.path.join(dirname, suffix) | 
|  | if os.path.lexists(src): | 
|  | copier(src, os.path.join(binpath, suffix)) | 
|  |  | 
|  | if sysconfig.is_python_build(True): | 
|  | # copy init.tcl | 
|  | for root, dirs, files in os.walk(context.python_dir): | 
|  | if 'init.tcl' in files: | 
|  | tcldir = os.path.basename(root) | 
|  | tcldir = os.path.join(context.env_dir, 'Lib', tcldir) | 
|  | if not os.path.exists(tcldir): | 
|  | os.makedirs(tcldir) | 
|  | src = os.path.join(root, 'init.tcl') | 
|  | dst = os.path.join(tcldir, 'init.tcl') | 
|  | shutil.copyfile(src, dst) | 
|  | break | 
|  |  | 
|  | def _setup_pip(self, context): | 
|  | """Installs or upgrades pip in a virtual environment""" | 
|  | # We run ensurepip in isolated mode to avoid side effects from | 
|  | # environment vars, the current directory and anything else | 
|  | # intended for the global Python environment | 
|  | cmd = [context.env_exec_cmd, '-Im', 'ensurepip', '--upgrade', | 
|  | '--default-pip'] | 
|  | subprocess.check_output(cmd, stderr=subprocess.STDOUT) | 
|  |  | 
|  | def setup_scripts(self, context): | 
|  | """ | 
|  | Set up scripts into the created environment from a directory. | 
|  |  | 
|  | This method installs the default scripts into the environment | 
|  | being created. You can prevent the default installation by overriding | 
|  | this method if you really need to, or if you need to specify | 
|  | a different location for the scripts to install. By default, the | 
|  | 'scripts' directory in the venv package is used as the source of | 
|  | scripts to install. | 
|  | """ | 
|  | path = os.path.abspath(os.path.dirname(__file__)) | 
|  | path = os.path.join(path, 'scripts') | 
|  | self.install_scripts(context, path) | 
|  |  | 
|  | def post_setup(self, context): | 
|  | """ | 
|  | Hook for post-setup modification of the venv. Subclasses may install | 
|  | additional packages or scripts here, add activation shell scripts, etc. | 
|  |  | 
|  | :param context: The information for the environment creation request | 
|  | being processed. | 
|  | """ | 
|  | pass | 
|  |  | 
|  | def replace_variables(self, text, context): | 
|  | """ | 
|  | Replace variable placeholders in script text with context-specific | 
|  | variables. | 
|  |  | 
|  | Return the text passed in , but with variables replaced. | 
|  |  | 
|  | :param text: The text in which to replace placeholder variables. | 
|  | :param context: The information for the environment creation request | 
|  | being processed. | 
|  | """ | 
|  | text = text.replace('__VENV_DIR__', context.env_dir) | 
|  | text = text.replace('__VENV_NAME__', context.env_name) | 
|  | text = text.replace('__VENV_PROMPT__', context.prompt) | 
|  | text = text.replace('__VENV_BIN_NAME__', context.bin_name) | 
|  | text = text.replace('__VENV_PYTHON__', context.env_exe) | 
|  | return text | 
|  |  | 
|  | def install_scripts(self, context, path): | 
|  | """ | 
|  | Install scripts into the created environment from a directory. | 
|  |  | 
|  | :param context: The information for the environment creation request | 
|  | being processed. | 
|  | :param path:    Absolute pathname of a directory containing script. | 
|  | Scripts in the 'common' subdirectory of this directory, | 
|  | and those in the directory named for the platform | 
|  | being run on, are installed in the created environment. | 
|  | Placeholder variables are replaced with environment- | 
|  | specific values. | 
|  | """ | 
|  | binpath = context.bin_path | 
|  | plen = len(path) | 
|  | for root, dirs, files in os.walk(path): | 
|  | if root == path: # at top-level, remove irrelevant dirs | 
|  | for d in dirs[:]: | 
|  | if d not in ('common', os.name): | 
|  | dirs.remove(d) | 
|  | continue # ignore files in top level | 
|  | for f in files: | 
|  | if (os.name == 'nt' and f.startswith('python') | 
|  | and f.endswith(('.exe', '.pdb'))): | 
|  | continue | 
|  | srcfile = os.path.join(root, f) | 
|  | suffix = root[plen:].split(os.sep)[2:] | 
|  | if not suffix: | 
|  | dstdir = binpath | 
|  | else: | 
|  | dstdir = os.path.join(binpath, *suffix) | 
|  | if not os.path.exists(dstdir): | 
|  | os.makedirs(dstdir) | 
|  | dstfile = os.path.join(dstdir, f) | 
|  | with open(srcfile, 'rb') as f: | 
|  | data = f.read() | 
|  | if not srcfile.endswith(('.exe', '.pdb')): | 
|  | try: | 
|  | data = data.decode('utf-8') | 
|  | data = self.replace_variables(data, context) | 
|  | data = data.encode('utf-8') | 
|  | except UnicodeError as e: | 
|  | data = None | 
|  | logger.warning('unable to copy script %r, ' | 
|  | 'may be binary: %s', srcfile, e) | 
|  | if data is not None: | 
|  | with open(dstfile, 'wb') as f: | 
|  | f.write(data) | 
|  | shutil.copymode(srcfile, dstfile) | 
|  |  | 
|  | def upgrade_dependencies(self, context): | 
|  | logger.debug( | 
|  | f'Upgrading {CORE_VENV_DEPS} packages in {context.bin_path}' | 
|  | ) | 
|  | cmd = [context.env_exec_cmd, '-m', 'pip', 'install', '--upgrade'] | 
|  | cmd.extend(CORE_VENV_DEPS) | 
|  | subprocess.check_call(cmd) | 
|  |  | 
|  |  | 
|  | def create(env_dir, system_site_packages=False, clear=False, | 
|  | symlinks=False, with_pip=False, prompt=None, upgrade_deps=False): | 
|  | """Create a virtual environment in a directory.""" | 
|  | builder = EnvBuilder(system_site_packages=system_site_packages, | 
|  | clear=clear, symlinks=symlinks, with_pip=with_pip, | 
|  | prompt=prompt, upgrade_deps=upgrade_deps) | 
|  | builder.create(env_dir) | 
|  |  | 
|  | def main(args=None): | 
|  | compatible = True | 
|  | if sys.version_info < (3, 3): | 
|  | compatible = False | 
|  | elif not hasattr(sys, 'base_prefix'): | 
|  | compatible = False | 
|  | if not compatible: | 
|  | raise ValueError('This script is only for use with Python >= 3.3') | 
|  | else: | 
|  | import argparse | 
|  |  | 
|  | parser = argparse.ArgumentParser(prog=__name__, | 
|  | description='Creates virtual Python ' | 
|  | 'environments in one or ' | 
|  | 'more target ' | 
|  | 'directories.', | 
|  | epilog='Once an environment has been ' | 
|  | 'created, you may wish to ' | 
|  | 'activate it, e.g. by ' | 
|  | 'sourcing an activate script ' | 
|  | 'in its bin directory.') | 
|  | parser.add_argument('dirs', metavar='ENV_DIR', nargs='+', | 
|  | help='A directory to create the environment in.') | 
|  | parser.add_argument('--system-site-packages', default=False, | 
|  | action='store_true', dest='system_site', | 
|  | help='Give the virtual environment access to the ' | 
|  | 'system site-packages dir.') | 
|  | if os.name == 'nt': | 
|  | use_symlinks = False | 
|  | else: | 
|  | use_symlinks = True | 
|  | group = parser.add_mutually_exclusive_group() | 
|  | group.add_argument('--symlinks', default=use_symlinks, | 
|  | action='store_true', dest='symlinks', | 
|  | help='Try to use symlinks rather than copies, ' | 
|  | 'when symlinks are not the default for ' | 
|  | 'the platform.') | 
|  | group.add_argument('--copies', default=not use_symlinks, | 
|  | action='store_false', dest='symlinks', | 
|  | help='Try to use copies rather than symlinks, ' | 
|  | 'even when symlinks are the default for ' | 
|  | 'the platform.') | 
|  | parser.add_argument('--clear', default=False, action='store_true', | 
|  | dest='clear', help='Delete the contents of the ' | 
|  | 'environment directory if it ' | 
|  | 'already exists, before ' | 
|  | 'environment creation.') | 
|  | parser.add_argument('--upgrade', default=False, action='store_true', | 
|  | dest='upgrade', help='Upgrade the environment ' | 
|  | 'directory to use this version ' | 
|  | 'of Python, assuming Python ' | 
|  | 'has been upgraded in-place.') | 
|  | parser.add_argument('--without-pip', dest='with_pip', | 
|  | default=True, action='store_false', | 
|  | help='Skips installing or upgrading pip in the ' | 
|  | 'virtual environment (pip is bootstrapped ' | 
|  | 'by default)') | 
|  | parser.add_argument('--prompt', | 
|  | help='Provides an alternative prompt prefix for ' | 
|  | 'this environment.') | 
|  | parser.add_argument('--upgrade-deps', default=False, action='store_true', | 
|  | dest='upgrade_deps', | 
|  | help='Upgrade core dependencies: {} to the latest ' | 
|  | 'version in PyPI'.format( | 
|  | ' '.join(CORE_VENV_DEPS))) | 
|  | options = parser.parse_args(args) | 
|  | if options.upgrade and options.clear: | 
|  | raise ValueError('you cannot supply --upgrade and --clear together.') | 
|  | builder = EnvBuilder(system_site_packages=options.system_site, | 
|  | clear=options.clear, | 
|  | symlinks=options.symlinks, | 
|  | upgrade=options.upgrade, | 
|  | with_pip=options.with_pip, | 
|  | prompt=options.prompt, | 
|  | upgrade_deps=options.upgrade_deps) | 
|  | for d in options.dirs: | 
|  | builder.create(d) | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | rc = 1 | 
|  | try: | 
|  | main() | 
|  | rc = 0 | 
|  | except Exception as e: | 
|  | print('Error: %s' % e, file=sys.stderr) | 
|  | sys.exit(rc) |