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