blob: ecdb68e880817558f6b073fc8f65673039a4b8a4 [file] [log] [blame]
Vinay Sajip42211422012-05-26 20:36:12 +01001"""
2Virtual environment (venv) package for Python. Based on PEP 405.
3
Vinay Sajip28952442012-06-25 00:47:46 +01004Copyright (C) 2011-2012 Vinay Sajip.
5Licensed to the PSF under a contributor agreement.
Vinay Sajip42211422012-05-26 20:36:12 +01006
Vinay Sajip44697462012-05-28 16:33:01 +01007usage: python -m venv [-h] [--system-site-packages] [--symlinks] [--clear]
8 [--upgrade]
Vinay Sajip42211422012-05-26 20:36:12 +01009 ENV_DIR [ENV_DIR ...]
10
11Creates virtual Python environments in one or more target directories.
12
13positional arguments:
14 ENV_DIR A directory to create the environment in.
15
16optional arguments:
17 -h, --help show this help message and exit
Vinay Sajip42211422012-05-26 20:36:12 +010018 --system-site-packages
19 Give the virtual environment access to the system
20 site-packages dir.
21 --symlinks Attempt to symlink rather than copy.
22 --clear Delete the environment directory if it already exists.
23 If not specified and the directory exists, an error is
24 raised.
25 --upgrade Upgrade the environment directory to use this version
26 of Python, assuming Python has been upgraded in-place.
Vinay Sajip42211422012-05-26 20:36:12 +010027"""
Vinay Sajip7ded1f02012-05-26 03:45:29 +010028import base64
29import io
30import logging
31import os
32import os.path
33import shutil
34import sys
Vinay Sajip44697462012-05-28 16:33:01 +010035import sysconfig
Vinay Sajip42211422012-05-26 20:36:12 +010036try:
37 import threading
38except ImportError:
39 threading = None
Vinay Sajip7ded1f02012-05-26 03:45:29 +010040
41logger = logging.getLogger(__name__)
42
43class Context:
44 """
Vinay Sajip42211422012-05-26 20:36:12 +010045 Holds information about a current venv creation/upgrade request.
Vinay Sajip7ded1f02012-05-26 03:45:29 +010046 """
47 pass
48
49
50class EnvBuilder:
51 """
52 This class exists to allow virtual environment creation to be
53 customised. The constructor parameters determine the builder's
54 behaviour when called upon to create a virtual environment.
55
56 By default, the builder makes the system (global) site-packages dir
Vinay Sajip87ed5992012-11-14 11:18:35 +000057 *un*available to the created environment.
Vinay Sajip7ded1f02012-05-26 03:45:29 +010058
Vinay Sajip87ed5992012-11-14 11:18:35 +000059 If invoked using the Python -m option, the default is to use copying
60 on Windows platforms but symlinks elsewhere. If instantiated some
61 other way, the default is to *not* use symlinks.
Vinay Sajip7ded1f02012-05-26 03:45:29 +010062
63 :param system_site_packages: If True, the system (global) site-packages
64 dir is available to created environments.
65 :param clear: If True and the target directory exists, it is deleted.
66 Otherwise, if the target directory exists, an error is
67 raised.
68 :param symlinks: If True, attempt to symlink rather than copy files into
69 virtual environment.
70 :param upgrade: If True, upgrade an existing virtual environment.
71 """
72
73 def __init__(self, system_site_packages=False, clear=False,
74 symlinks=False, upgrade=False):
75 self.system_site_packages = system_site_packages
76 self.clear = clear
77 self.symlinks = symlinks
78 self.upgrade = upgrade
79
80 def create(self, env_dir):
81 """
82 Create a virtual environment in a directory.
83
84 :param env_dir: The target directory to create an environment in.
85
86 """
Vinay Sajip7ded1f02012-05-26 03:45:29 +010087 env_dir = os.path.abspath(env_dir)
88 context = self.ensure_directories(env_dir)
89 self.create_configuration(context)
90 self.setup_python(context)
91 if not self.upgrade:
92 self.setup_scripts(context)
93 self.post_setup(context)
94
Vinay Sajipbd40d3e2012-10-11 17:22:45 +010095 def clear_directory(self, path):
96 for fn in os.listdir(path):
97 fn = os.path.join(path, fn)
98 if os.path.islink(fn) or os.path.isfile(fn):
99 os.remove(fn)
100 elif os.path.isdir(fn):
101 shutil.rmtree(fn)
102
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100103 def ensure_directories(self, env_dir):
104 """
105 Create the directories for the environment.
106
107 Returns a context object which holds paths in the environment,
108 for use by subsequent logic.
109 """
110
111 def create_if_needed(d):
112 if not os.path.exists(d):
113 os.makedirs(d)
Vinay Sajipbd40d3e2012-10-11 17:22:45 +0100114 elif os.path.islink(d) or os.path.isfile(d):
115 raise ValueError('Unable to create directory %r' % d)
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100116
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100117 if os.path.exists(env_dir) and self.clear:
Vinay Sajipbd40d3e2012-10-11 17:22:45 +0100118 self.clear_directory(env_dir)
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100119 context = Context()
120 context.env_dir = env_dir
121 context.env_name = os.path.split(env_dir)[1]
122 context.prompt = '(%s) ' % context.env_name
123 create_if_needed(env_dir)
124 env = os.environ
Vinay Sajip28952442012-06-25 00:47:46 +0100125 if sys.platform == 'darwin' and '__PYVENV_LAUNCHER__' in env:
126 executable = os.environ['__PYVENV_LAUNCHER__']
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100127 else:
128 executable = sys.executable
129 dirname, exename = os.path.split(os.path.abspath(executable))
130 context.executable = executable
131 context.python_dir = dirname
132 context.python_exe = exename
133 if sys.platform == 'win32':
134 binname = 'Scripts'
135 incpath = 'Include'
136 libpath = os.path.join(env_dir, 'Lib', 'site-packages')
137 else:
138 binname = 'bin'
139 incpath = 'include'
140 libpath = os.path.join(env_dir, 'lib', 'python%d.%d' % sys.version_info[:2], 'site-packages')
141 context.inc_path = path = os.path.join(env_dir, incpath)
142 create_if_needed(path)
143 create_if_needed(libpath)
144 context.bin_path = binpath = os.path.join(env_dir, binname)
145 context.bin_name = binname
146 context.env_exe = os.path.join(binpath, exename)
147 create_if_needed(binpath)
148 return context
149
150 def create_configuration(self, context):
151 """
152 Create a configuration file indicating where the environment's Python
153 was copied from, and whether the system site-packages should be made
154 available in the environment.
155
156 :param context: The information for the environment creation request
157 being processed.
158 """
159 context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg')
160 with open(path, 'w', encoding='utf-8') as f:
161 f.write('home = %s\n' % context.python_dir)
162 if self.system_site_packages:
163 incl = 'true'
164 else:
165 incl = 'false'
166 f.write('include-system-site-packages = %s\n' % incl)
167 f.write('version = %d.%d.%d\n' % sys.version_info[:3])
168
169 if os.name == 'nt':
170 def include_binary(self, f):
171 if f.endswith(('.pyd', '.dll')):
172 result = True
173 else:
174 result = f.startswith('python') and f.endswith('.exe')
175 return result
176
177 def symlink_or_copy(self, src, dst):
178 """
179 Try symlinking a file, and if that fails, fall back to copying.
180 """
181 force_copy = not self.symlinks
182 if not force_copy:
183 try:
184 if not os.path.islink(dst): # can't link to itself!
185 os.symlink(src, dst)
186 except Exception: # may need to use a more specific exception
187 logger.warning('Unable to symlink %r to %r', src, dst)
188 force_copy = True
189 if force_copy:
190 shutil.copyfile(src, dst)
191
192 def setup_python(self, context):
193 """
194 Set up a Python executable in the environment.
195
196 :param context: The information for the environment creation request
197 being processed.
198 """
199 binpath = context.bin_path
200 exename = context.python_exe
201 path = context.env_exe
202 copier = self.symlink_or_copy
203 copier(context.executable, path)
204 dirname = context.python_dir
205 if os.name != 'nt':
206 if not os.path.islink(path):
207 os.chmod(path, 0o755)
Vinay Sajip44697462012-05-28 16:33:01 +0100208 for suffix in ('python', 'python3'):
209 path = os.path.join(binpath, suffix)
210 if not os.path.exists(path):
211 os.symlink(exename, path)
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100212 else:
213 subdir = 'DLLs'
214 include = self.include_binary
215 files = [f for f in os.listdir(dirname) if include(f)]
216 for f in files:
217 src = os.path.join(dirname, f)
218 dst = os.path.join(binpath, f)
219 if dst != context.env_exe: # already done, above
220 copier(src, dst)
221 dirname = os.path.join(dirname, subdir)
222 if os.path.isdir(dirname):
223 files = [f for f in os.listdir(dirname) if include(f)]
224 for f in files:
225 src = os.path.join(dirname, f)
226 dst = os.path.join(binpath, f)
227 copier(src, dst)
228 # copy init.tcl over
229 for root, dirs, files in os.walk(context.python_dir):
230 if 'init.tcl' in files:
231 tcldir = os.path.basename(root)
232 tcldir = os.path.join(context.env_dir, 'Lib', tcldir)
233 os.makedirs(tcldir)
234 src = os.path.join(root, 'init.tcl')
235 dst = os.path.join(tcldir, 'init.tcl')
236 shutil.copyfile(src, dst)
237 break
238
239 def setup_scripts(self, context):
240 """
241 Set up scripts into the created environment from a directory.
242
243 This method installs the default scripts into the environment
244 being created. You can prevent the default installation by overriding
245 this method if you really need to, or if you need to specify
246 a different location for the scripts to install. By default, the
247 'scripts' directory in the venv package is used as the source of
248 scripts to install.
249 """
250 path = os.path.abspath(os.path.dirname(__file__))
251 path = os.path.join(path, 'scripts')
252 self.install_scripts(context, path)
253
254 def post_setup(self, context):
255 """
256 Hook for post-setup modification of the venv. Subclasses may install
257 additional packages or scripts here, add activation shell scripts, etc.
258
259 :param context: The information for the environment creation request
260 being processed.
261 """
262 pass
263
264 def replace_variables(self, text, context):
265 """
266 Replace variable placeholders in script text with context-specific
267 variables.
268
269 Return the text passed in , but with variables replaced.
270
271 :param text: The text in which to replace placeholder variables.
272 :param context: The information for the environment creation request
273 being processed.
274 """
275 text = text.replace('__VENV_DIR__', context.env_dir)
276 text = text.replace('__VENV_NAME__', context.prompt)
277 text = text.replace('__VENV_BIN_NAME__', context.bin_name)
278 text = text.replace('__VENV_PYTHON__', context.env_exe)
279 return text
280
281 def install_scripts(self, context, path):
282 """
283 Install scripts into the created environment from a directory.
284
285 :param context: The information for the environment creation request
286 being processed.
287 :param path: Absolute pathname of a directory containing script.
288 Scripts in the 'common' subdirectory of this directory,
289 and those in the directory named for the platform
290 being run on, are installed in the created environment.
291 Placeholder variables are replaced with environment-
292 specific values.
293 """
294 binpath = context.bin_path
295 plen = len(path)
296 for root, dirs, files in os.walk(path):
297 if root == path: # at top-level, remove irrelevant dirs
298 for d in dirs[:]:
299 if d not in ('common', os.name):
300 dirs.remove(d)
301 continue # ignore files in top level
302 for f in files:
303 srcfile = os.path.join(root, f)
304 suffix = root[plen:].split(os.sep)[2:]
305 if not suffix:
306 dstdir = binpath
307 else:
308 dstdir = os.path.join(binpath, *suffix)
309 if not os.path.exists(dstdir):
310 os.makedirs(dstdir)
311 dstfile = os.path.join(dstdir, f)
312 with open(srcfile, 'rb') as f:
313 data = f.read()
314 if srcfile.endswith('.exe'):
315 mode = 'wb'
316 else:
317 mode = 'w'
Vinay Sajipbdd13fd2012-10-28 12:39:39 +0000318 try:
319 data = data.decode('utf-8')
320 data = self.replace_variables(data, context)
321 except UnicodeDecodeError as e:
322 data = None
323 logger.warning('unable to copy script %r, '
324 'may be binary: %s', srcfile, e)
325 if data is not None:
326 with open(dstfile, mode) as f:
327 f.write(data)
328 shutil.copymode(srcfile, dstfile)
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100329
330
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100331def create(env_dir, system_site_packages=False, clear=False, symlinks=False):
332 """
333 Create a virtual environment in a directory.
334
Vinay Sajip87ed5992012-11-14 11:18:35 +0000335 By default, makes the system (global) site-packages dir *un*available to
336 the created environment, and uses copying rather than symlinking for files
337 obtained from the source Python installation.
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100338
339 :param env_dir: The target directory to create an environment in.
340 :param system_site_packages: If True, the system (global) site-packages
341 dir is available to the environment.
342 :param clear: If True and the target directory exists, it is deleted.
343 Otherwise, if the target directory exists, an error is
344 raised.
345 :param symlinks: If True, attempt to symlink rather than copy files into
346 virtual environment.
347 """
Vinay Sajip44697462012-05-28 16:33:01 +0100348 builder = EnvBuilder(system_site_packages=system_site_packages,
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100349 clear=clear, symlinks=symlinks)
350 builder.create(env_dir)
351
352def main(args=None):
353 compatible = True
354 if sys.version_info < (3, 3):
355 compatible = False
356 elif not hasattr(sys, 'base_prefix'):
357 compatible = False
358 if not compatible:
Vinay Sajip28952442012-06-25 00:47:46 +0100359 raise ValueError('This script is only for use with Python 3.3')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100360 else:
361 import argparse
362
363 parser = argparse.ArgumentParser(prog=__name__,
364 description='Creates virtual Python '
365 'environments in one or '
366 'more target '
Vinay Sajip4d378d82012-07-08 17:50:42 +0100367 'directories.',
368 epilog='Once an environment has been '
369 'created, you may wish to '
370 'activate it, e.g. by '
371 'sourcing an activate script '
372 'in its bin directory.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100373 parser.add_argument('dirs', metavar='ENV_DIR', nargs='+',
374 help='A directory to create the environment in.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100375 parser.add_argument('--system-site-packages', default=False,
376 action='store_true', dest='system_site',
Vinay Sajip44697462012-05-28 16:33:01 +0100377 help='Give the virtual environment access to the '
378 'system site-packages dir.')
Vinay Sajip90db6612012-07-17 17:33:46 +0100379 if os.name == 'nt':
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100380 use_symlinks = False
381 else:
382 use_symlinks = True
383 parser.add_argument('--symlinks', default=use_symlinks,
384 action='store_true', dest='symlinks',
Vinay Sajip4d378d82012-07-08 17:50:42 +0100385 help='Try to use symlinks rather than copies, '
386 'when symlinks are not the default for '
387 'the platform.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100388 parser.add_argument('--clear', default=False, action='store_true',
Vinay Sajipbd40d3e2012-10-11 17:22:45 +0100389 dest='clear', help='Delete the contents of the '
390 'environment directory if it '
391 'already exists, before '
392 'environment creation.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100393 parser.add_argument('--upgrade', default=False, action='store_true',
394 dest='upgrade', help='Upgrade the environment '
395 'directory to use this version '
Vinay Sajip42211422012-05-26 20:36:12 +0100396 'of Python, assuming Python '
397 'has been upgraded in-place.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100398 options = parser.parse_args(args)
399 if options.upgrade and options.clear:
400 raise ValueError('you cannot supply --upgrade and --clear together.')
Vinay Sajip44697462012-05-28 16:33:01 +0100401 builder = EnvBuilder(system_site_packages=options.system_site,
402 clear=options.clear, symlinks=options.symlinks,
403 upgrade=options.upgrade)
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100404 for d in options.dirs:
405 builder.create(d)
406
407if __name__ == '__main__':
408 rc = 1
409 try:
410 main()
411 rc = 0
412 except Exception as e:
413 print('Error: %s' % e, file=sys.stderr)
414 sys.exit(rc)