python (3.11.7)
1 """
2 Virtual environment (venv) package for Python. Based on PEP 405.
3
4 Copyright (C) 2011-2014 Vinay Sajip.
5 Licensed to the PSF under a contributor agreement.
6 """
7 import logging
8 import os
9 import shutil
10 import subprocess
11 import sys
12 import sysconfig
13 import types
14
15
16 CORE_VENV_DEPS = ('pip', 'setuptools')
17 logger = logging.getLogger(__name__)
18
19
20 class ESC[4;38;5;81mEnvBuilder:
21 """
22 This class exists to allow virtual environment creation to be
23 customized. The constructor parameters determine the builder's
24 behaviour when called upon to create a virtual environment.
25
26 By default, the builder makes the system (global) site-packages dir
27 *un*available to the created environment.
28
29 If invoked using the Python -m option, the default is to use copying
30 on Windows platforms but symlinks elsewhere. If instantiated some
31 other way, the default is to *not* use symlinks.
32
33 :param system_site_packages: If True, the system (global) site-packages
34 dir is available to created environments.
35 :param clear: If True, delete the contents of the environment directory if
36 it already exists, before environment creation.
37 :param symlinks: If True, attempt to symlink rather than copy files into
38 virtual environment.
39 :param upgrade: If True, upgrade an existing virtual environment.
40 :param with_pip: If True, ensure pip is installed in the virtual
41 environment
42 :param prompt: Alternative terminal prefix for the environment.
43 :param upgrade_deps: Update the base venv modules to the latest on PyPI
44 """
45
46 def __init__(self, system_site_packages=False, clear=False,
47 symlinks=False, upgrade=False, with_pip=False, prompt=None,
48 upgrade_deps=False):
49 self.system_site_packages = system_site_packages
50 self.clear = clear
51 self.symlinks = symlinks
52 self.upgrade = upgrade
53 self.with_pip = with_pip
54 self.orig_prompt = prompt
55 if prompt == '.': # see bpo-38901
56 prompt = os.path.basename(os.getcwd())
57 self.prompt = prompt
58 self.upgrade_deps = upgrade_deps
59
60 def create(self, env_dir):
61 """
62 Create a virtual environment in a directory.
63
64 :param env_dir: The target directory to create an environment in.
65
66 """
67 env_dir = os.path.abspath(env_dir)
68 context = self.ensure_directories(env_dir)
69 # See issue 24875. We need system_site_packages to be False
70 # until after pip is installed.
71 true_system_site_packages = self.system_site_packages
72 self.system_site_packages = False
73 self.create_configuration(context)
74 self.setup_python(context)
75 if self.with_pip:
76 self._setup_pip(context)
77 if not self.upgrade:
78 self.setup_scripts(context)
79 self.post_setup(context)
80 if true_system_site_packages:
81 # We had set it to False before, now
82 # restore it and rewrite the configuration
83 self.system_site_packages = True
84 self.create_configuration(context)
85 if self.upgrade_deps:
86 self.upgrade_dependencies(context)
87
88 def clear_directory(self, path):
89 for fn in os.listdir(path):
90 fn = os.path.join(path, fn)
91 if os.path.islink(fn) or os.path.isfile(fn):
92 os.remove(fn)
93 elif os.path.isdir(fn):
94 shutil.rmtree(fn)
95
96 def _venv_path(self, env_dir, name):
97 vars = {
98 'base': env_dir,
99 'platbase': env_dir,
100 'installed_base': env_dir,
101 'installed_platbase': env_dir,
102 }
103 return sysconfig.get_path(name, scheme='venv', vars=vars)
104
105 def ensure_directories(self, env_dir):
106 """
107 Create the directories for the environment.
108
109 Returns a context object which holds paths in the environment,
110 for use by subsequent logic.
111 """
112
113 def create_if_needed(d):
114 if not os.path.exists(d):
115 os.makedirs(d)
116 elif os.path.islink(d) or os.path.isfile(d):
117 raise ValueError('Unable to create directory %r' % d)
118
119 if os.pathsep in os.fspath(env_dir):
120 raise ValueError(f'Refusing to create a venv in {env_dir} because '
121 f'it contains the PATH separator {os.pathsep}.')
122 if os.path.exists(env_dir) and self.clear:
123 self.clear_directory(env_dir)
124 context = types.SimpleNamespace()
125 context.env_dir = env_dir
126 context.env_name = os.path.split(env_dir)[1]
127 prompt = self.prompt if self.prompt is not None else context.env_name
128 context.prompt = '(%s) ' % prompt
129 create_if_needed(env_dir)
130 executable = sys._base_executable
131 if not executable: # see gh-96861
132 raise ValueError('Unable to determine path to the running '
133 'Python interpreter. Provide an explicit path or '
134 'check that your PATH environment variable is '
135 'correctly set.')
136 dirname, exename = os.path.split(os.path.abspath(executable))
137 context.executable = executable
138 context.python_dir = dirname
139 context.python_exe = exename
140 binpath = self._venv_path(env_dir, 'scripts')
141 incpath = self._venv_path(env_dir, 'include')
142 libpath = self._venv_path(env_dir, 'purelib')
143
144 context.inc_path = incpath
145 create_if_needed(incpath)
146 create_if_needed(libpath)
147 # Issue 21197: create lib64 as a symlink to lib on 64-bit non-OS X POSIX
148 if ((sys.maxsize > 2**32) and (os.name == 'posix') and
149 (sys.platform != 'darwin')):
150 link_path = os.path.join(env_dir, 'lib64')
151 if not os.path.exists(link_path): # Issue #21643
152 os.symlink('lib', link_path)
153 context.bin_path = binpath
154 context.bin_name = os.path.relpath(binpath, env_dir)
155 context.env_exe = os.path.join(binpath, exename)
156 create_if_needed(binpath)
157 # Assign and update the command to use when launching the newly created
158 # environment, in case it isn't simply the executable script (e.g. bpo-45337)
159 context.env_exec_cmd = context.env_exe
160 if sys.platform == 'win32':
161 # bpo-45337: Fix up env_exec_cmd to account for file system redirections.
162 # Some redirects only apply to CreateFile and not CreateProcess
163 real_env_exe = os.path.realpath(context.env_exe)
164 if os.path.normcase(real_env_exe) != os.path.normcase(context.env_exe):
165 logger.warning('Actual environment location may have moved due to '
166 'redirects, links or junctions.\n'
167 ' Requested location: "%s"\n'
168 ' Actual location: "%s"',
169 context.env_exe, real_env_exe)
170 context.env_exec_cmd = real_env_exe
171 return context
172
173 def create_configuration(self, context):
174 """
175 Create a configuration file indicating where the environment's Python
176 was copied from, and whether the system site-packages should be made
177 available in the environment.
178
179 :param context: The information for the environment creation request
180 being processed.
181 """
182 context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg')
183 with open(path, 'w', encoding='utf-8') as f:
184 f.write('home = %s\n' % context.python_dir)
185 if self.system_site_packages:
186 incl = 'true'
187 else:
188 incl = 'false'
189 f.write('include-system-site-packages = %s\n' % incl)
190 f.write('version = %d.%d.%d\n' % sys.version_info[:3])
191 if self.prompt is not None:
192 f.write(f'prompt = {self.prompt!r}\n')
193 f.write('executable = %s\n' % os.path.realpath(sys.executable))
194 args = []
195 nt = os.name == 'nt'
196 if nt and self.symlinks:
197 args.append('--symlinks')
198 if not nt and not self.symlinks:
199 args.append('--copies')
200 if not self.with_pip:
201 args.append('--without-pip')
202 if self.system_site_packages:
203 args.append('--system-site-packages')
204 if self.clear:
205 args.append('--clear')
206 if self.upgrade:
207 args.append('--upgrade')
208 if self.upgrade_deps:
209 args.append('--upgrade-deps')
210 if self.orig_prompt is not None:
211 args.append(f'--prompt="{self.orig_prompt}"')
212
213 args.append(context.env_dir)
214 args = ' '.join(args)
215 f.write(f'command = {sys.executable} -m venv {args}\n')
216
217 if os.name != 'nt':
218 def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
219 """
220 Try symlinking a file, and if that fails, fall back to copying.
221 """
222 force_copy = not self.symlinks
223 if not force_copy:
224 try:
225 if not os.path.islink(dst): # can't link to itself!
226 if relative_symlinks_ok:
227 assert os.path.dirname(src) == os.path.dirname(dst)
228 os.symlink(os.path.basename(src), dst)
229 else:
230 os.symlink(src, dst)
231 except Exception: # may need to use a more specific exception
232 logger.warning('Unable to symlink %r to %r', src, dst)
233 force_copy = True
234 if force_copy:
235 shutil.copyfile(src, dst)
236 else:
237 def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
238 """
239 Try symlinking a file, and if that fails, fall back to copying.
240 """
241 bad_src = os.path.lexists(src) and not os.path.exists(src)
242 if self.symlinks and not bad_src and not os.path.islink(dst):
243 try:
244 if relative_symlinks_ok:
245 assert os.path.dirname(src) == os.path.dirname(dst)
246 os.symlink(os.path.basename(src), dst)
247 else:
248 os.symlink(src, dst)
249 return
250 except Exception: # may need to use a more specific exception
251 logger.warning('Unable to symlink %r to %r', src, dst)
252
253 # On Windows, we rewrite symlinks to our base python.exe into
254 # copies of venvlauncher.exe
255 basename, ext = os.path.splitext(os.path.basename(src))
256 srcfn = os.path.join(os.path.dirname(__file__),
257 "scripts",
258 "nt",
259 basename + ext)
260 # Builds or venv's from builds need to remap source file
261 # locations, as we do not put them into Lib/venv/scripts
262 if sysconfig.is_python_build() or not os.path.isfile(srcfn):
263 if basename.endswith('_d'):
264 ext = '_d' + ext
265 basename = basename[:-2]
266 if basename == 'python':
267 basename = 'venvlauncher'
268 elif basename == 'pythonw':
269 basename = 'venvwlauncher'
270 src = os.path.join(os.path.dirname(src), basename + ext)
271 else:
272 src = srcfn
273 if not os.path.exists(src):
274 if not bad_src:
275 logger.warning('Unable to copy %r', src)
276 return
277
278 shutil.copyfile(src, dst)
279
280 def setup_python(self, context):
281 """
282 Set up a Python executable in the environment.
283
284 :param context: The information for the environment creation request
285 being processed.
286 """
287 binpath = context.bin_path
288 path = context.env_exe
289 copier = self.symlink_or_copy
290 dirname = context.python_dir
291 if os.name != 'nt':
292 copier(context.executable, path)
293 if not os.path.islink(path):
294 os.chmod(path, 0o755)
295 for suffix in ('python', 'python3', f'python3.{sys.version_info[1]}'):
296 path = os.path.join(binpath, suffix)
297 if not os.path.exists(path):
298 # Issue 18807: make copies if
299 # symlinks are not wanted
300 copier(context.env_exe, path, relative_symlinks_ok=True)
301 if not os.path.islink(path):
302 os.chmod(path, 0o755)
303 else:
304 if self.symlinks:
305 # For symlinking, we need a complete copy of the root directory
306 # If symlinks fail, you'll get unnecessary copies of files, but
307 # we assume that if you've opted into symlinks on Windows then
308 # you know what you're doing.
309 suffixes = [
310 f for f in os.listdir(dirname) if
311 os.path.normcase(os.path.splitext(f)[1]) in ('.exe', '.dll')
312 ]
313 if sysconfig.is_python_build():
314 suffixes = [
315 f for f in suffixes if
316 os.path.normcase(f).startswith(('python', 'vcruntime'))
317 ]
318 else:
319 suffixes = {'python.exe', 'python_d.exe', 'pythonw.exe', 'pythonw_d.exe'}
320 base_exe = os.path.basename(context.env_exe)
321 suffixes.add(base_exe)
322
323 for suffix in suffixes:
324 src = os.path.join(dirname, suffix)
325 if os.path.lexists(src):
326 copier(src, os.path.join(binpath, suffix))
327
328 if sysconfig.is_python_build():
329 # copy init.tcl
330 for root, dirs, files in os.walk(context.python_dir):
331 if 'init.tcl' in files:
332 tcldir = os.path.basename(root)
333 tcldir = os.path.join(context.env_dir, 'Lib', tcldir)
334 if not os.path.exists(tcldir):
335 os.makedirs(tcldir)
336 src = os.path.join(root, 'init.tcl')
337 dst = os.path.join(tcldir, 'init.tcl')
338 shutil.copyfile(src, dst)
339 break
340
341 def _call_new_python(self, context, *py_args, **kwargs):
342 """Executes the newly created Python using safe-ish options"""
343 # gh-98251: We do not want to just use '-I' because that masks
344 # legitimate user preferences (such as not writing bytecode). All we
345 # really need is to ensure that the path variables do not overrule
346 # normal venv handling.
347 args = [context.env_exec_cmd, *py_args]
348 kwargs['env'] = env = os.environ.copy()
349 env['VIRTUAL_ENV'] = context.env_dir
350 env.pop('PYTHONHOME', None)
351 env.pop('PYTHONPATH', None)
352 kwargs['cwd'] = context.env_dir
353 kwargs['executable'] = context.env_exec_cmd
354 subprocess.check_output(args, **kwargs)
355
356 def _setup_pip(self, context):
357 """Installs or upgrades pip in a virtual environment"""
358 self._call_new_python(context, '-m', 'ensurepip', '--upgrade',
359 '--default-pip', stderr=subprocess.STDOUT)
360
361 def setup_scripts(self, context):
362 """
363 Set up scripts into the created environment from a directory.
364
365 This method installs the default scripts into the environment
366 being created. You can prevent the default installation by overriding
367 this method if you really need to, or if you need to specify
368 a different location for the scripts to install. By default, the
369 'scripts' directory in the venv package is used as the source of
370 scripts to install.
371 """
372 path = os.path.abspath(os.path.dirname(__file__))
373 path = os.path.join(path, 'scripts')
374 self.install_scripts(context, path)
375
376 def post_setup(self, context):
377 """
378 Hook for post-setup modification of the venv. Subclasses may install
379 additional packages or scripts here, add activation shell scripts, etc.
380
381 :param context: The information for the environment creation request
382 being processed.
383 """
384 pass
385
386 def replace_variables(self, text, context):
387 """
388 Replace variable placeholders in script text with context-specific
389 variables.
390
391 Return the text passed in , but with variables replaced.
392
393 :param text: The text in which to replace placeholder variables.
394 :param context: The information for the environment creation request
395 being processed.
396 """
397 text = text.replace('__VENV_DIR__', context.env_dir)
398 text = text.replace('__VENV_NAME__', context.env_name)
399 text = text.replace('__VENV_PROMPT__', context.prompt)
400 text = text.replace('__VENV_BIN_NAME__', context.bin_name)
401 text = text.replace('__VENV_PYTHON__', context.env_exe)
402 return text
403
404 def install_scripts(self, context, path):
405 """
406 Install scripts into the created environment from a directory.
407
408 :param context: The information for the environment creation request
409 being processed.
410 :param path: Absolute pathname of a directory containing script.
411 Scripts in the 'common' subdirectory of this directory,
412 and those in the directory named for the platform
413 being run on, are installed in the created environment.
414 Placeholder variables are replaced with environment-
415 specific values.
416 """
417 binpath = context.bin_path
418 plen = len(path)
419 for root, dirs, files in os.walk(path):
420 if root == path: # at top-level, remove irrelevant dirs
421 for d in dirs[:]:
422 if d not in ('common', os.name):
423 dirs.remove(d)
424 continue # ignore files in top level
425 for f in files:
426 if (os.name == 'nt' and f.startswith('python')
427 and f.endswith(('.exe', '.pdb'))):
428 continue
429 srcfile = os.path.join(root, f)
430 suffix = root[plen:].split(os.sep)[2:]
431 if not suffix:
432 dstdir = binpath
433 else:
434 dstdir = os.path.join(binpath, *suffix)
435 if not os.path.exists(dstdir):
436 os.makedirs(dstdir)
437 dstfile = os.path.join(dstdir, f)
438 with open(srcfile, 'rb') as f:
439 data = f.read()
440 if not srcfile.endswith(('.exe', '.pdb')):
441 try:
442 data = data.decode('utf-8')
443 data = self.replace_variables(data, context)
444 data = data.encode('utf-8')
445 except UnicodeError as e:
446 data = None
447 logger.warning('unable to copy script %r, '
448 'may be binary: %s', srcfile, e)
449 if data is not None:
450 with open(dstfile, 'wb') as f:
451 f.write(data)
452 shutil.copymode(srcfile, dstfile)
453
454 def upgrade_dependencies(self, context):
455 logger.debug(
456 f'Upgrading {CORE_VENV_DEPS} packages in {context.bin_path}'
457 )
458 self._call_new_python(context, '-m', 'pip', 'install', '--upgrade',
459 *CORE_VENV_DEPS)
460
461
462 def create(env_dir, system_site_packages=False, clear=False,
463 symlinks=False, with_pip=False, prompt=None, upgrade_deps=False):
464 """Create a virtual environment in a directory."""
465 builder = EnvBuilder(system_site_packages=system_site_packages,
466 clear=clear, symlinks=symlinks, with_pip=with_pip,
467 prompt=prompt, upgrade_deps=upgrade_deps)
468 builder.create(env_dir)
469
470 def main(args=None):
471 compatible = True
472 if sys.version_info < (3, 3):
473 compatible = False
474 elif not hasattr(sys, 'base_prefix'):
475 compatible = False
476 if not compatible:
477 raise ValueError('This script is only for use with Python >= 3.3')
478 else:
479 import argparse
480
481 parser = argparse.ArgumentParser(prog=__name__,
482 description='Creates virtual Python '
483 'environments in one or '
484 'more target '
485 'directories.',
486 epilog='Once an environment has been '
487 'created, you may wish to '
488 'activate it, e.g. by '
489 'sourcing an activate script '
490 'in its bin directory.')
491 parser.add_argument('dirs', metavar='ENV_DIR', nargs='+',
492 help='A directory to create the environment in.')
493 parser.add_argument('--system-site-packages', default=False,
494 action='store_true', dest='system_site',
495 help='Give the virtual environment access to the '
496 'system site-packages dir.')
497 if os.name == 'nt':
498 use_symlinks = False
499 else:
500 use_symlinks = True
501 group = parser.add_mutually_exclusive_group()
502 group.add_argument('--symlinks', default=use_symlinks,
503 action='store_true', dest='symlinks',
504 help='Try to use symlinks rather than copies, '
505 'when symlinks are not the default for '
506 'the platform.')
507 group.add_argument('--copies', default=not use_symlinks,
508 action='store_false', dest='symlinks',
509 help='Try to use copies rather than symlinks, '
510 'even when symlinks are the default for '
511 'the platform.')
512 parser.add_argument('--clear', default=False, action='store_true',
513 dest='clear', help='Delete the contents of the '
514 'environment directory if it '
515 'already exists, before '
516 'environment creation.')
517 parser.add_argument('--upgrade', default=False, action='store_true',
518 dest='upgrade', help='Upgrade the environment '
519 'directory to use this version '
520 'of Python, assuming Python '
521 'has been upgraded in-place.')
522 parser.add_argument('--without-pip', dest='with_pip',
523 default=True, action='store_false',
524 help='Skips installing or upgrading pip in the '
525 'virtual environment (pip is bootstrapped '
526 'by default)')
527 parser.add_argument('--prompt',
528 help='Provides an alternative prompt prefix for '
529 'this environment.')
530 parser.add_argument('--upgrade-deps', default=False, action='store_true',
531 dest='upgrade_deps',
532 help='Upgrade core dependencies: {} to the latest '
533 'version in PyPI'.format(
534 ' '.join(CORE_VENV_DEPS)))
535 options = parser.parse_args(args)
536 if options.upgrade and options.clear:
537 raise ValueError('you cannot supply --upgrade and --clear together.')
538 builder = EnvBuilder(system_site_packages=options.system_site,
539 clear=options.clear,
540 symlinks=options.symlinks,
541 upgrade=options.upgrade,
542 with_pip=options.with_pip,
543 prompt=options.prompt,
544 upgrade_deps=options.upgrade_deps)
545 for d in options.dirs:
546 builder.create(d)
547
548 if __name__ == '__main__':
549 rc = 1
550 try:
551 main()
552 rc = 0
553 except Exception as e:
554 print('Error: %s' % e, file=sys.stderr)
555 sys.exit(rc)