123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549 |
- """
- 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',)
- 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 _venv_path(self, env_dir, name):
- vars = {
- 'base': env_dir,
- 'platbase': env_dir,
- 'installed_base': env_dir,
- 'installed_platbase': env_dir,
- }
- return sysconfig.get_path(name, scheme='venv', vars=vars)
- 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.pathsep in os.fspath(env_dir):
- raise ValueError(f'Refusing to create a venv in {env_dir} because '
- f'it contains the PATH separator {os.pathsep}.')
- 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
- if not executable: # see gh-96861
- raise ValueError('Unable to determine path to the running '
- 'Python interpreter. Provide an explicit path or '
- 'check that your PATH environment variable is '
- 'correctly set.')
- dirname, exename = os.path.split(os.path.abspath(executable))
- context.executable = executable
- context.python_dir = dirname
- context.python_exe = exename
- binpath = self._venv_path(env_dir, 'scripts')
- incpath = self._venv_path(env_dir, 'include')
- libpath = self._venv_path(env_dir, 'purelib')
- context.inc_path = incpath
- create_if_needed(incpath)
- context.lib_path = libpath
- 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
- context.bin_name = os.path.relpath(binpath, env_dir)
- 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() 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():
- 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():
- # 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 _call_new_python(self, context, *py_args, **kwargs):
- """Executes the newly created Python using safe-ish options"""
- # gh-98251: We do not want to just use '-I' because that masks
- # legitimate user preferences (such as not writing bytecode). All we
- # really need is to ensure that the path variables do not overrule
- # normal venv handling.
- args = [context.env_exec_cmd, *py_args]
- kwargs['env'] = env = os.environ.copy()
- env['VIRTUAL_ENV'] = context.env_dir
- env.pop('PYTHONHOME', None)
- env.pop('PYTHONPATH', None)
- kwargs['cwd'] = context.env_dir
- kwargs['executable'] = context.env_exec_cmd
- subprocess.check_output(args, **kwargs)
- def _setup_pip(self, context):
- """Installs or upgrades pip in a virtual environment"""
- self._call_new_python(context, '-m', 'ensurepip', '--upgrade',
- '--default-pip', 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}'
- )
- self._call_new_python(context, '-m', 'pip', 'install', '--upgrade',
- *CORE_VENV_DEPS)
- 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):
- 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=f'Upgrade core dependencies ({", ".join(CORE_VENV_DEPS)}) '
- 'to the latest version in PyPI')
- 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)
|