1"""
2Virtual environment (venv) package for Python. Based on PEP 405.
3
4Copyright (C) 2011-2014 Vinay Sajip.
5Licensed to the PSF under a contributor agreement.
6"""
7import logging
8import os
9import shutil
10import subprocess
11import sys
12import sysconfig
13import types
14
15
16CORE_VENV_DEPS = ('pip', 'setuptools')
17logger = logging.getLogger(__name__)
18
19
20class EnvBuilder:
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
462def 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
470def 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
548if __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)
556