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