···11+#!/usr/bin/env python3
22+# WARNING: This is mostly a copy of code from the cpython library.
33+# flake8: noqa
44+# fmt: off
55+# isort:skip_file
66+"""
77+Virtual environment (venv) package for Python. Based on PEP 405.
88+99+Copyright (C) 2011-2014 Vinay Sajip.
1010+Licensed to the PSF under a contributor agreement.
1111+"""
1212+import logging
1313+import os
1414+import shutil
1515+import subprocess
1616+import sys
1717+import sysconfig
1818+import types
1919+2020+logger = logging.getLogger(__name__)
2121+2222+2323+class EnvBuilder:
2424+ """
2525+ This class exists to allow virtual environment creation to be
2626+ customized. The constructor parameters determine the builder's
2727+ behaviour when called upon to create a virtual environment.
2828+2929+ By default, the builder makes the system (global) site-packages dir
3030+ *un*available to the created environment.
3131+3232+ If invoked using the Python -m option, the default is to use copying
3333+ on Windows platforms but symlinks elsewhere. If instantiated some
3434+ other way, the default is to *not* use symlinks.
3535+3636+ :param system_site_packages: If True, the system (global) site-packages
3737+ dir is available to created environments.
3838+ :param clear: If True, delete the contents of the environment directory if
3939+ it already exists, before environment creation.
4040+ :param symlinks: If True, attempt to symlink rather than copy files into
4141+ virtual environment.
4242+ :param upgrade: If True, upgrade an existing virtual environment.
4343+ :param with_pip: If True, ensure pip is installed in the virtual
4444+ environment
4545+ :param prompt: Alternative terminal prefix for the environment.
4646+ """
4747+4848+ def __init__(self, system_site_packages=False, clear=False,
4949+ symlinks=False, upgrade=False, with_pip=False, prompt=None):
5050+ self.system_site_packages = system_site_packages
5151+ self.clear = clear
5252+ self.symlinks = symlinks
5353+ self.upgrade = upgrade
5454+ self.with_pip = with_pip
5555+ self.prompt = prompt
5656+5757+ def create(self, env_dir):
5858+ """
5959+ Create a virtual environment in a directory.
6060+6161+ :param env_dir: The target directory to create an environment in.
6262+6363+ """
6464+ env_dir = os.path.abspath(env_dir)
6565+ context = self.ensure_directories(env_dir)
6666+ # See issue 24875. We need system_site_packages to be False
6767+ # until after pip is installed.
6868+ true_system_site_packages = self.system_site_packages
6969+ self.system_site_packages = False
7070+ self.create_configuration(context)
7171+ self.setup_python(context)
7272+ if self.with_pip:
7373+ self._setup_pip(context)
7474+ if not self.upgrade:
7575+ self.setup_scripts(context)
7676+ self.post_setup(context)
7777+ if true_system_site_packages:
7878+ # We had set it to False before, now
7979+ # restore it and rewrite the configuration
8080+ self.system_site_packages = True
8181+ self.create_configuration(context)
8282+8383+ def clear_directory(self, path):
8484+ for fn in os.listdir(path):
8585+ fn = os.path.join(path, fn)
8686+ if os.path.islink(fn) or os.path.isfile(fn):
8787+ os.remove(fn)
8888+ elif os.path.isdir(fn):
8989+ shutil.rmtree(fn)
9090+9191+ def ensure_directories(self, env_dir):
9292+ """
9393+ Create the directories for the environment.
9494+9595+ Returns a context object which holds paths in the environment,
9696+ for use by subsequent logic.
9797+ """
9898+9999+ def create_if_needed(d):
100100+ if not os.path.exists(d):
101101+ os.makedirs(d)
102102+ elif os.path.islink(d) or os.path.isfile(d):
103103+ raise ValueError('Unable to create directory %r' % d)
104104+105105+ if os.path.exists(env_dir) and self.clear:
106106+ self.clear_directory(env_dir)
107107+ context = types.SimpleNamespace()
108108+ context.env_dir = env_dir
109109+ context.env_name = os.path.split(env_dir)[1]
110110+ prompt = self.prompt if self.prompt is not None else context.env_name
111111+ context.prompt = '(%s) ' % prompt
112112+ create_if_needed(env_dir)
113113+ executable = sys._base_executable
114114+ dirname, exename = os.path.split(os.path.abspath(executable))
115115+ context.executable = executable
116116+ context.python_dir = dirname
117117+ context.python_exe = exename
118118+ if sys.platform == 'win32':
119119+ binname = 'Scripts'
120120+ incpath = 'Include'
121121+ libpath = os.path.join(env_dir, 'Lib', 'site-packages')
122122+ else:
123123+ binname = 'bin'
124124+ incpath = 'include'
125125+ libpath = os.path.join(env_dir, 'lib',
126126+ 'python%d.%d' % sys.version_info[:2],
127127+ 'site-packages')
128128+ context.inc_path = path = os.path.join(env_dir, incpath)
129129+ create_if_needed(path)
130130+ create_if_needed(libpath)
131131+ # Issue 21197: create lib64 as a symlink to lib on 64-bit non-OS X POSIX
132132+ if ((sys.maxsize > 2**32) and (os.name == 'posix') and
133133+ (sys.platform != 'darwin')):
134134+ link_path = os.path.join(env_dir, 'lib64')
135135+ if not os.path.exists(link_path): # Issue #21643
136136+ os.symlink('lib', link_path)
137137+ context.bin_path = binpath = os.path.join(env_dir, binname)
138138+ context.bin_name = binname
139139+ context.env_exe = os.path.join(binpath, exename)
140140+ create_if_needed(binpath)
141141+ return context
142142+143143+ def create_configuration(self, context):
144144+ """
145145+ Create a configuration file indicating where the environment's Python
146146+ was copied from, and whether the system site-packages should be made
147147+ available in the environment.
148148+149149+ :param context: The information for the environment creation request
150150+ being processed.
151151+ """
152152+ context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg')
153153+ with open(path, 'w', encoding='utf-8') as f:
154154+ f.write('home = %s\n' % context.python_dir)
155155+ if self.system_site_packages:
156156+ incl = 'true'
157157+ else:
158158+ incl = 'false'
159159+ f.write('include-system-site-packages = %s\n' % incl)
160160+ f.write('version = %d.%d.%d\n' % sys.version_info[:3])
161161+ if self.prompt is not None:
162162+ f.write(f'prompt = {self.prompt!r}\n')
163163+164164+ if os.name != 'nt':
165165+ def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
166166+ """
167167+ Try symlinking a file, and if that fails, fall back to copying.
168168+ """
169169+ force_copy = not self.symlinks
170170+ if not force_copy:
171171+ try:
172172+ if not os.path.islink(dst): # can't link to itself!
173173+ if relative_symlinks_ok:
174174+ assert os.path.dirname(src) == os.path.dirname(dst)
175175+ os.symlink(os.path.basename(src), dst)
176176+ else:
177177+ os.symlink(src, dst)
178178+ except Exception: # may need to use a more specific exception
179179+ logger.warning('Unable to symlink %r to %r', src, dst)
180180+ force_copy = True
181181+ if force_copy:
182182+ shutil.copyfile(src, dst)
183183+ else:
184184+ def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
185185+ """
186186+ Try symlinking a file, and if that fails, fall back to copying.
187187+ """
188188+ bad_src = os.path.lexists(src) and not os.path.exists(src)
189189+ if self.symlinks and not bad_src and not os.path.islink(dst):
190190+ try:
191191+ if relative_symlinks_ok:
192192+ assert os.path.dirname(src) == os.path.dirname(dst)
193193+ os.symlink(os.path.basename(src), dst)
194194+ else:
195195+ os.symlink(src, dst)
196196+ return
197197+ except Exception: # may need to use a more specific exception
198198+ logger.warning('Unable to symlink %r to %r', src, dst)
199199+200200+ # On Windows, we rewrite symlinks to our base python.exe into
201201+ # copies of venvlauncher.exe
202202+ basename, ext = os.path.splitext(os.path.basename(src))
203203+ srcfn = os.path.join(os.path.dirname(__file__),
204204+ "scripts",
205205+ "nt",
206206+ basename + ext)
207207+ # Builds or venv's from builds need to remap source file
208208+ # locations, as we do not put them into Lib/venv/scripts
209209+ if sysconfig.is_python_build(True) or not os.path.isfile(srcfn):
210210+ if basename.endswith('_d'):
211211+ ext = '_d' + ext
212212+ basename = basename[:-2]
213213+ if basename == 'python':
214214+ basename = 'venvlauncher'
215215+ elif basename == 'pythonw':
216216+ basename = 'venvwlauncher'
217217+ src = os.path.join(os.path.dirname(src), basename + ext)
218218+ else:
219219+ src = srcfn
220220+ if not os.path.exists(src):
221221+ if not bad_src:
222222+ logger.warning('Unable to copy %r', src)
223223+ return
224224+225225+ shutil.copyfile(src, dst)
226226+227227+ def setup_python(self, context):
228228+ """
229229+ Set up a Python executable in the environment.
230230+231231+ :param context: The information for the environment creation request
232232+ being processed.
233233+ """
234234+ binpath = context.bin_path
235235+ path = context.env_exe
236236+ copier = self.symlink_or_copy
237237+ dirname = context.python_dir
238238+ if os.name != 'nt':
239239+ copier(context.executable, path)
240240+ if not os.path.islink(path):
241241+ os.chmod(path, 0o755)
242242+ for suffix in ('python', 'python3'):
243243+ path = os.path.join(binpath, suffix)
244244+ if not os.path.exists(path):
245245+ # Issue 18807: make copies if
246246+ # symlinks are not wanted
247247+ copier(context.env_exe, path, relative_symlinks_ok=True)
248248+ if not os.path.islink(path):
249249+ os.chmod(path, 0o755)
250250+ else:
251251+ if self.symlinks:
252252+ # For symlinking, we need a complete copy of the root directory
253253+ # If symlinks fail, you'll get unnecessary copies of files, but
254254+ # we assume that if you've opted into symlinks on Windows then
255255+ # you know what you're doing.
256256+ suffixes = [
257257+ f for f in os.listdir(dirname) if
258258+ os.path.normcase(os.path.splitext(f)[1]) in ('.exe', '.dll')
259259+ ]
260260+ if sysconfig.is_python_build(True):
261261+ suffixes = [
262262+ f for f in suffixes if
263263+ os.path.normcase(f).startswith(('python', 'vcruntime'))
264264+ ]
265265+ else:
266266+ suffixes = ['python.exe', 'python_d.exe', 'pythonw.exe',
267267+ 'pythonw_d.exe']
268268+269269+ for suffix in suffixes:
270270+ src = os.path.join(dirname, suffix)
271271+ if os.path.lexists(src):
272272+ copier(src, os.path.join(binpath, suffix))
273273+274274+ if sysconfig.is_python_build(True):
275275+ # copy init.tcl
276276+ for root, dirs, files in os.walk(context.python_dir):
277277+ if 'init.tcl' in files:
278278+ tcldir = os.path.basename(root)
279279+ tcldir = os.path.join(context.env_dir, 'Lib', tcldir)
280280+ if not os.path.exists(tcldir):
281281+ os.makedirs(tcldir)
282282+ src = os.path.join(root, 'init.tcl')
283283+ dst = os.path.join(tcldir, 'init.tcl')
284284+ shutil.copyfile(src, dst)
285285+ break
286286+287287+ def _setup_pip(self, context):
288288+ """Installs or upgrades pip in a virtual environment"""
289289+ # We run ensurepip in isolated mode to avoid side effects from
290290+ # environment vars, the current directory and anything else
291291+ # intended for the global Python environment
292292+ cmd = [context.env_exe, '-Im', 'ensurepip', '--upgrade',
293293+ '--default-pip']
294294+ subprocess.check_output(cmd, stderr=subprocess.STDOUT)
295295+296296+ def setup_scripts(self, context):
297297+ """
298298+ Set up scripts into the created environment from a directory.
299299+300300+ This method installs the default scripts into the environment
301301+ being created. You can prevent the default installation by overriding
302302+ this method if you really need to, or if you need to specify
303303+ a different location for the scripts to install. By default, the
304304+ 'scripts' directory in the venv package is used as the source of
305305+ scripts to install.
306306+ """
307307+ path = os.path.abspath(os.path.dirname(__file__))
308308+ path = os.path.join(path, 'scripts')
309309+ self.install_scripts(context, path)
310310+311311+ def post_setup(self, context):
312312+ """
313313+ Hook for post-setup modification of the venv. Subclasses may install
314314+ additional packages or scripts here, add activation shell scripts, etc.
315315+316316+ :param context: The information for the environment creation request
317317+ being processed.
318318+ """
319319+ pass
320320+321321+ def replace_variables(self, text, context):
322322+ """
323323+ Replace variable placeholders in script text with context-specific
324324+ variables.
325325+326326+ Return the text passed in , but with variables replaced.
327327+328328+ :param text: The text in which to replace placeholder variables.
329329+ :param context: The information for the environment creation request
330330+ being processed.
331331+ """
332332+ text = text.replace('__VENV_DIR__', context.env_dir)
333333+ text = text.replace('__VENV_NAME__', context.env_name)
334334+ text = text.replace('__VENV_PROMPT__', context.prompt)
335335+ text = text.replace('__VENV_BIN_NAME__', context.bin_name)
336336+ text = text.replace('__VENV_PYTHON__', context.env_exe)
337337+ return text
338338+339339+ def install_scripts(self, context, path):
340340+ """
341341+ Install scripts into the created environment from a directory.
342342+343343+ :param context: The information for the environment creation request
344344+ being processed.
345345+ :param path: Absolute pathname of a directory containing script.
346346+ Scripts in the 'common' subdirectory of this directory,
347347+ and those in the directory named for the platform
348348+ being run on, are installed in the created environment.
349349+ Placeholder variables are replaced with environment-
350350+ specific values.
351351+ """
352352+ binpath = context.bin_path
353353+ plen = len(path)
354354+ for root, dirs, files in os.walk(path):
355355+ if root == path: # at top-level, remove irrelevant dirs
356356+ for d in dirs[:]:
357357+ if d not in ('common', os.name):
358358+ dirs.remove(d)
359359+ continue # ignore files in top level
360360+ for f in files:
361361+ if (os.name == 'nt' and f.startswith('python')
362362+ and f.endswith(('.exe', '.pdb'))):
363363+ continue
364364+ srcfile = os.path.join(root, f)
365365+ suffix = root[plen:].split(os.sep)[2:]
366366+ if not suffix:
367367+ dstdir = binpath
368368+ else:
369369+ dstdir = os.path.join(binpath, *suffix)
370370+ if not os.path.exists(dstdir):
371371+ os.makedirs(dstdir)
372372+ dstfile = os.path.join(dstdir, f)
373373+ with open(srcfile, 'rb') as f:
374374+ data = f.read()
375375+ if not srcfile.endswith(('.exe', '.pdb')):
376376+ try:
377377+ data = data.decode('utf-8')
378378+ data = self.replace_variables(data, context)
379379+ data = data.encode('utf-8')
380380+ except UnicodeError as e:
381381+ data = None
382382+ logger.warning('unable to copy script %r, '
383383+ 'may be binary: %s', srcfile, e)
384384+ if data is not None:
385385+ with open(dstfile, 'wb') as f:
386386+ f.write(data)
387387+ shutil.copymode(srcfile, dstfile)
388388+389389+390390+def create(env_dir, system_site_packages=False, clear=False,
391391+ symlinks=False, with_pip=False, prompt=None):
392392+ """Create a virtual environment in a directory."""
393393+ builder = EnvBuilder(system_site_packages=system_site_packages,
394394+ clear=clear, symlinks=symlinks, with_pip=with_pip,
395395+ prompt=prompt)
396396+ builder.create(env_dir)
397397+398398+def main(args=None):
399399+ compatible = True
400400+ if sys.version_info < (3, 3):
401401+ compatible = False
402402+ elif not hasattr(sys, 'base_prefix'):
403403+ compatible = False
404404+ if not compatible:
405405+ raise ValueError('This script is only for use with Python >= 3.3')
406406+ else:
407407+ import argparse
408408+409409+ parser = argparse.ArgumentParser(prog=__name__,
410410+ description='Creates virtual Python '
411411+ 'environments in one or '
412412+ 'more target '
413413+ 'directories.',
414414+ epilog='Once an environment has been '
415415+ 'created, you may wish to '
416416+ 'activate it, e.g. by '
417417+ 'sourcing an activate script '
418418+ 'in its bin directory.')
419419+ parser.add_argument('dirs', metavar='ENV_DIR', nargs='+',
420420+ help='A directory to create the environment in.')
421421+ parser.add_argument('--system-site-packages', default=False,
422422+ action='store_true', dest='system_site',
423423+ help='Give the virtual environment access to the '
424424+ 'system site-packages dir.')
425425+ if os.name == 'nt':
426426+ use_symlinks = False
427427+ else:
428428+ use_symlinks = True
429429+ group = parser.add_mutually_exclusive_group()
430430+ group.add_argument('--symlinks', default=use_symlinks,
431431+ action='store_true', dest='symlinks',
432432+ help='Try to use symlinks rather than copies, '
433433+ 'when symlinks are not the default for '
434434+ 'the platform.')
435435+ group.add_argument('--copies', default=not use_symlinks,
436436+ action='store_false', dest='symlinks',
437437+ help='Try to use copies rather than symlinks, '
438438+ 'even when symlinks are the default for '
439439+ 'the platform.')
440440+ parser.add_argument('--clear', default=False, action='store_true',
441441+ dest='clear', help='Delete the contents of the '
442442+ 'environment directory if it '
443443+ 'already exists, before '
444444+ 'environment creation.')
445445+ parser.add_argument('--upgrade', default=False, action='store_true',
446446+ dest='upgrade', help='Upgrade the environment '
447447+ 'directory to use this version '
448448+ 'of Python, assuming Python '
449449+ 'has been upgraded in-place.')
450450+ parser.add_argument('--without-pip', dest='with_pip',
451451+ default=True, action='store_false',
452452+ help='Skips installing or upgrading pip in the '
453453+ 'virtual environment (pip is bootstrapped '
454454+ 'by default)')
455455+ parser.add_argument('--prompt',
456456+ help='Provides an alternative prompt prefix for '
457457+ 'this environment.')
458458+ options = parser.parse_args(args)
459459+ if options.upgrade and options.clear:
460460+ raise ValueError('you cannot supply --upgrade and --clear together.')
461461+ builder = EnvBuilder(system_site_packages=options.system_site,
462462+ clear=options.clear,
463463+ symlinks=options.symlinks,
464464+ upgrade=options.upgrade,
465465+ with_pip=options.with_pip,
466466+ prompt=options.prompt)
467467+ for d in options.dirs:
468468+ builder.create(d)
469469+470470+if __name__ == '__main__':
471471+ rc = 1
472472+ try:
473473+ main()
474474+ rc = 0
475475+ except Exception as e:
476476+ print('Error: %s' % e, file=sys.stderr)
477477+ sys.exit(rc)