python (3.12.0)
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',)
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 context.lib_path = libpath
147 create_if_needed(libpath)
148 # Issue 21197: create lib64 as a symlink to lib on 64-bit non-OS X POSIX
149 if ((sys.maxsize > 2**32) and (os.name == 'posix') and
150 (sys.platform != 'darwin')):
151 link_path = os.path.join(env_dir, 'lib64')
152 if not os.path.exists(link_path): # Issue #21643
153 os.symlink('lib', link_path)
154 context.bin_path = binpath
155 context.bin_name = os.path.relpath(binpath, env_dir)
156 context.env_exe = os.path.join(binpath, exename)
157 create_if_needed(binpath)
158 # Assign and update the command to use when launching the newly created
159 # environment, in case it isn't simply the executable script (e.g. bpo-45337)
160 context.env_exec_cmd = context.env_exe
161 if sys.platform == 'win32':
162 # bpo-45337: Fix up env_exec_cmd to account for file system redirections.
163 # Some redirects only apply to CreateFile and not CreateProcess
164 real_env_exe = os.path.realpath(context.env_exe)
165 if os.path.normcase(real_env_exe) != os.path.normcase(context.env_exe):
166 logger.warning('Actual environment location may have moved due to '
167 'redirects, links or junctions.\n'
168 ' Requested location: "%s"\n'
169 ' Actual location: "%s"',
170 context.env_exe, real_env_exe)
171 context.env_exec_cmd = real_env_exe
172 return context
173
174 def create_configuration(self, context):
175 """
176 Create a configuration file indicating where the environment's Python
177 was copied from, and whether the system site-packages should be made
178 available in the environment.
179
180 :param context: The information for the environment creation request
181 being processed.
182 """
183 context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg')
184 with open(path, 'w', encoding='utf-8') as f:
185 f.write('home = %s\n' % context.python_dir)
186 if self.system_site_packages:
187 incl = 'true'
188 else:
189 incl = 'false'
190 f.write('include-system-site-packages = %s\n' % incl)
191 f.write('version = %d.%d.%d\n' % sys.version_info[:3])
192 if self.prompt is not None:
193 f.write(f'prompt = {self.prompt!r}\n')
194 f.write('executable = %s\n' % os.path.realpath(sys.executable))
195 args = []
196 nt = os.name == 'nt'
197 if nt and self.symlinks:
198 args.append('--symlinks')
199 if not nt and not self.symlinks:
200 args.append('--copies')
201 if not self.with_pip:
202 args.append('--without-pip')
203 if self.system_site_packages:
204 args.append('--system-site-packages')
205 if self.clear:
206 args.append('--clear')
207 if self.upgrade:
208 args.append('--upgrade')
209 if self.upgrade_deps:
210 args.append('--upgrade-deps')
211 if self.orig_prompt is not None:
212 args.append(f'--prompt="{self.orig_prompt}"')
213
214 args.append(context.env_dir)
215 args = ' '.join(args)
216 f.write(f'command = {sys.executable} -m venv {args}\n')
217
218 if os.name != 'nt':
219 def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
220 """
221 Try symlinking a file, and if that fails, fall back to copying.
222 """
223 force_copy = not self.symlinks
224 if not force_copy:
225 try:
226 if not os.path.islink(dst): # can't link to itself!
227 if relative_symlinks_ok:
228 assert os.path.dirname(src) == os.path.dirname(dst)
229 os.symlink(os.path.basename(src), dst)
230 else:
231 os.symlink(src, dst)
232 except Exception: # may need to use a more specific exception
233 logger.warning('Unable to symlink %r to %r', src, dst)
234 force_copy = True
235 if force_copy:
236 shutil.copyfile(src, dst)
237 else:
238 def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
239 """
240 Try symlinking a file, and if that fails, fall back to copying.
241 """
242 bad_src = os.path.lexists(src) and not os.path.exists(src)
243 if self.symlinks and not bad_src and not os.path.islink(dst):
244 try:
245 if relative_symlinks_ok:
246 assert os.path.dirname(src) == os.path.dirname(dst)
247 os.symlink(os.path.basename(src), dst)
248 else:
249 os.symlink(src, dst)
250 return
251 except Exception: # may need to use a more specific exception
252 logger.warning('Unable to symlink %r to %r', src, dst)
253
254 # On Windows, we rewrite symlinks to our base python.exe into
255 # copies of venvlauncher.exe
256 basename, ext = os.path.splitext(os.path.basename(src))
257 srcfn = os.path.join(os.path.dirname(__file__),
258 "scripts",
259 "nt",
260 basename + ext)
261 # Builds or venv's from builds need to remap source file
262 # locations, as we do not put them into Lib/venv/scripts
263 if sysconfig.is_python_build() or not os.path.isfile(srcfn):
264 if basename.endswith('_d'):
265 ext = '_d' + ext
266 basename = basename[:-2]
267 if basename == 'python':
268 basename = 'venvlauncher'
269 elif basename == 'pythonw':
270 basename = 'venvwlauncher'
271 src = os.path.join(os.path.dirname(src), basename + ext)
272 else:
273 src = srcfn
274 if not os.path.exists(src):
275 if not bad_src:
276 logger.warning('Unable to copy %r', src)
277 return
278
279 shutil.copyfile(src, dst)
280
281 def setup_python(self, context):
282 """
283 Set up a Python executable in the environment.
284
285 :param context: The information for the environment creation request
286 being processed.
287 """
288 binpath = context.bin_path
289 path = context.env_exe
290 copier = self.symlink_or_copy
291 dirname = context.python_dir
292 if os.name != 'nt':
293 copier(context.executable, path)
294 if not os.path.islink(path):
295 os.chmod(path, 0o755)
296 for suffix in ('python', 'python3', f'python3.{sys.version_info[1]}'):
297 path = os.path.join(binpath, suffix)
298 if not os.path.exists(path):
299 # Issue 18807: make copies if
300 # symlinks are not wanted
301 copier(context.env_exe, path, relative_symlinks_ok=True)
302 if not os.path.islink(path):
303 os.chmod(path, 0o755)
304 else:
305 if self.symlinks:
306 # For symlinking, we need a complete copy of the root directory
307 # If symlinks fail, you'll get unnecessary copies of files, but
308 # we assume that if you've opted into symlinks on Windows then
309 # you know what you're doing.
310 suffixes = [
311 f for f in os.listdir(dirname) if
312 os.path.normcase(os.path.splitext(f)[1]) in ('.exe', '.dll')
313 ]
314 if sysconfig.is_python_build():
315 suffixes = [
316 f for f in suffixes if
317 os.path.normcase(f).startswith(('python', 'vcruntime'))
318 ]
319 else:
320 suffixes = {'python.exe', 'python_d.exe', 'pythonw.exe', 'pythonw_d.exe'}
321 base_exe = os.path.basename(context.env_exe)
322 suffixes.add(base_exe)
323
324 for suffix in suffixes:
325 src = os.path.join(dirname, suffix)
326 if os.path.lexists(src):
327 copier(src, os.path.join(binpath, suffix))
328
329 if sysconfig.is_python_build():
330 # copy init.tcl
331 for root, dirs, files in os.walk(context.python_dir):
332 if 'init.tcl' in files:
333 tcldir = os.path.basename(root)
334 tcldir = os.path.join(context.env_dir, 'Lib', tcldir)
335 if not os.path.exists(tcldir):
336 os.makedirs(tcldir)
337 src = os.path.join(root, 'init.tcl')
338 dst = os.path.join(tcldir, 'init.tcl')
339 shutil.copyfile(src, dst)
340 break
341
342 def _call_new_python(self, context, *py_args, **kwargs):
343 """Executes the newly created Python using safe-ish options"""
344 # gh-98251: We do not want to just use '-I' because that masks
345 # legitimate user preferences (such as not writing bytecode). All we
346 # really need is to ensure that the path variables do not overrule
347 # normal venv handling.
348 args = [context.env_exec_cmd, *py_args]
349 kwargs['env'] = env = os.environ.copy()
350 env['VIRTUAL_ENV'] = context.env_dir
351 env.pop('PYTHONHOME', None)
352 env.pop('PYTHONPATH', None)
353 kwargs['cwd'] = context.env_dir
354 kwargs['executable'] = context.env_exec_cmd
355 subprocess.check_output(args, **kwargs)
356
357 def _setup_pip(self, context):
358 """Installs or upgrades pip in a virtual environment"""
359 self._call_new_python(context, '-m', 'ensurepip', '--upgrade',
360 '--default-pip', stderr=subprocess.STDOUT)
361
362 def setup_scripts(self, context):
363 """
364 Set up scripts into the created environment from a directory.
365
366 This method installs the default scripts into the environment
367 being created. You can prevent the default installation by overriding
368 this method if you really need to, or if you need to specify
369 a different location for the scripts to install. By default, the
370 'scripts' directory in the venv package is used as the source of
371 scripts to install.
372 """
373 path = os.path.abspath(os.path.dirname(__file__))
374 path = os.path.join(path, 'scripts')
375 self.install_scripts(context, path)
376
377 def post_setup(self, context):
378 """
379 Hook for post-setup modification of the venv. Subclasses may install
380 additional packages or scripts here, add activation shell scripts, etc.
381
382 :param context: The information for the environment creation request
383 being processed.
384 """
385 pass
386
387 def replace_variables(self, text, context):
388 """
389 Replace variable placeholders in script text with context-specific
390 variables.
391
392 Return the text passed in , but with variables replaced.
393
394 :param text: The text in which to replace placeholder variables.
395 :param context: The information for the environment creation request
396 being processed.
397 """
398 text = text.replace('__VENV_DIR__', context.env_dir)
399 text = text.replace('__VENV_NAME__', context.env_name)
400 text = text.replace('__VENV_PROMPT__', context.prompt)
401 text = text.replace('__VENV_BIN_NAME__', context.bin_name)
402 text = text.replace('__VENV_PYTHON__', context.env_exe)
403 return text
404
405 def install_scripts(self, context, path):
406 """
407 Install scripts into the created environment from a directory.
408
409 :param context: The information for the environment creation request
410 being processed.
411 :param path: Absolute pathname of a directory containing script.
412 Scripts in the 'common' subdirectory of this directory,
413 and those in the directory named for the platform
414 being run on, are installed in the created environment.
415 Placeholder variables are replaced with environment-
416 specific values.
417 """
418 binpath = context.bin_path
419 plen = len(path)
420 for root, dirs, files in os.walk(path):
421 if root == path: # at top-level, remove irrelevant dirs
422 for d in dirs[:]:
423 if d not in ('common', os.name):
424 dirs.remove(d)
425 continue # ignore files in top level
426 for f in files:
427 if (os.name == 'nt' and f.startswith('python')
428 and f.endswith(('.exe', '.pdb'))):
429 continue
430 srcfile = os.path.join(root, f)
431 suffix = root[plen:].split(os.sep)[2:]
432 if not suffix:
433 dstdir = binpath
434 else:
435 dstdir = os.path.join(binpath, *suffix)
436 if not os.path.exists(dstdir):
437 os.makedirs(dstdir)
438 dstfile = os.path.join(dstdir, f)
439 with open(srcfile, 'rb') as f:
440 data = f.read()
441 if not srcfile.endswith(('.exe', '.pdb')):
442 try:
443 data = data.decode('utf-8')
444 data = self.replace_variables(data, context)
445 data = data.encode('utf-8')
446 except UnicodeError as e:
447 data = None
448 logger.warning('unable to copy script %r, '
449 'may be binary: %s', srcfile, e)
450 if data is not None:
451 with open(dstfile, 'wb') as f:
452 f.write(data)
453 shutil.copymode(srcfile, dstfile)
454
455 def upgrade_dependencies(self, context):
456 logger.debug(
457 f'Upgrading {CORE_VENV_DEPS} packages in {context.bin_path}'
458 )
459 self._call_new_python(context, '-m', 'pip', 'install', '--upgrade',
460 *CORE_VENV_DEPS)
461
462
463 def create(env_dir, system_site_packages=False, clear=False,
464 symlinks=False, with_pip=False, prompt=None, upgrade_deps=False):
465 """Create a virtual environment in a directory."""
466 builder = EnvBuilder(system_site_packages=system_site_packages,
467 clear=clear, symlinks=symlinks, with_pip=with_pip,
468 prompt=prompt, upgrade_deps=upgrade_deps)
469 builder.create(env_dir)
470
471
472 def main(args=None):
473 import argparse
474
475 parser = argparse.ArgumentParser(prog=__name__,
476 description='Creates virtual Python '
477 'environments in one or '
478 'more target '
479 'directories.',
480 epilog='Once an environment has been '
481 'created, you may wish to '
482 'activate it, e.g. by '
483 'sourcing an activate script '
484 'in its bin directory.')
485 parser.add_argument('dirs', metavar='ENV_DIR', nargs='+',
486 help='A directory to create the environment in.')
487 parser.add_argument('--system-site-packages', default=False,
488 action='store_true', dest='system_site',
489 help='Give the virtual environment access to the '
490 'system site-packages dir.')
491 if os.name == 'nt':
492 use_symlinks = False
493 else:
494 use_symlinks = True
495 group = parser.add_mutually_exclusive_group()
496 group.add_argument('--symlinks', default=use_symlinks,
497 action='store_true', dest='symlinks',
498 help='Try to use symlinks rather than copies, '
499 'when symlinks are not the default for '
500 'the platform.')
501 group.add_argument('--copies', default=not use_symlinks,
502 action='store_false', dest='symlinks',
503 help='Try to use copies rather than symlinks, '
504 'even when symlinks are the default for '
505 'the platform.')
506 parser.add_argument('--clear', default=False, action='store_true',
507 dest='clear', help='Delete the contents of the '
508 'environment directory if it '
509 'already exists, before '
510 'environment creation.')
511 parser.add_argument('--upgrade', default=False, action='store_true',
512 dest='upgrade', help='Upgrade the environment '
513 'directory to use this version '
514 'of Python, assuming Python '
515 'has been upgraded in-place.')
516 parser.add_argument('--without-pip', dest='with_pip',
517 default=True, action='store_false',
518 help='Skips installing or upgrading pip in the '
519 'virtual environment (pip is bootstrapped '
520 'by default)')
521 parser.add_argument('--prompt',
522 help='Provides an alternative prompt prefix for '
523 'this environment.')
524 parser.add_argument('--upgrade-deps', default=False, action='store_true',
525 dest='upgrade_deps',
526 help=f'Upgrade core dependencies ({", ".join(CORE_VENV_DEPS)}) '
527 'to the latest version in PyPI')
528 options = parser.parse_args(args)
529 if options.upgrade and options.clear:
530 raise ValueError('you cannot supply --upgrade and --clear together.')
531 builder = EnvBuilder(system_site_packages=options.system_site,
532 clear=options.clear,
533 symlinks=options.symlinks,
534 upgrade=options.upgrade,
535 with_pip=options.with_pip,
536 prompt=options.prompt,
537 upgrade_deps=options.upgrade_deps)
538 for d in options.dirs:
539 builder.create(d)
540
541
542 if __name__ == '__main__':
543 rc = 1
544 try:
545 main()
546 rc = 0
547 except Exception as e:
548 print('Error: %s' % e, file=sys.stderr)
549 sys.exit(rc)