blob: 3920747931110461ac36c0faccd9dea126a3553a [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
95 def ensure_directories(self, env_dir):
96 """
97 Create the directories for the environment.
98
99 Returns a context object which holds paths in the environment,
100 for use by subsequent logic.
101 """
102
103 def create_if_needed(d):
104 if not os.path.exists(d):
105 os.makedirs(d)
106
107 if os.path.exists(env_dir) and not (self.clear or self.upgrade):
108 raise ValueError('Directory exists: %s' % env_dir)
109 if os.path.exists(env_dir) and self.clear:
Vinay Sajipa6894ba2012-08-24 20:01:02 +0100110 shutil.rmtree(env_dir)
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100111 context = Context()
112 context.env_dir = env_dir
113 context.env_name = os.path.split(env_dir)[1]
114 context.prompt = '(%s) ' % context.env_name
115 create_if_needed(env_dir)
116 env = os.environ
Vinay Sajip28952442012-06-25 00:47:46 +0100117 if sys.platform == 'darwin' and '__PYVENV_LAUNCHER__' in env:
118 executable = os.environ['__PYVENV_LAUNCHER__']
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100119 else:
120 executable = sys.executable
121 dirname, exename = os.path.split(os.path.abspath(executable))
122 context.executable = executable
123 context.python_dir = dirname
124 context.python_exe = exename
125 if sys.platform == 'win32':
126 binname = 'Scripts'
127 incpath = 'Include'
128 libpath = os.path.join(env_dir, 'Lib', 'site-packages')
129 else:
130 binname = 'bin'
131 incpath = 'include'
132 libpath = os.path.join(env_dir, 'lib', 'python%d.%d' % sys.version_info[:2], 'site-packages')
133 context.inc_path = path = os.path.join(env_dir, incpath)
134 create_if_needed(path)
135 create_if_needed(libpath)
136 context.bin_path = binpath = os.path.join(env_dir, binname)
137 context.bin_name = binname
138 context.env_exe = os.path.join(binpath, exename)
139 create_if_needed(binpath)
140 return context
141
142 def create_configuration(self, context):
143 """
144 Create a configuration file indicating where the environment's Python
145 was copied from, and whether the system site-packages should be made
146 available in the environment.
147
148 :param context: The information for the environment creation request
149 being processed.
150 """
151 context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg')
152 with open(path, 'w', encoding='utf-8') as f:
153 f.write('home = %s\n' % context.python_dir)
154 if self.system_site_packages:
155 incl = 'true'
156 else:
157 incl = 'false'
158 f.write('include-system-site-packages = %s\n' % incl)
159 f.write('version = %d.%d.%d\n' % sys.version_info[:3])
160
161 if os.name == 'nt':
162 def include_binary(self, f):
163 if f.endswith(('.pyd', '.dll')):
164 result = True
165 else:
166 result = f.startswith('python') and f.endswith('.exe')
167 return result
168
169 def symlink_or_copy(self, src, dst):
170 """
171 Try symlinking a file, and if that fails, fall back to copying.
172 """
173 force_copy = not self.symlinks
174 if not force_copy:
175 try:
176 if not os.path.islink(dst): # can't link to itself!
177 os.symlink(src, dst)
178 except Exception: # may need to use a more specific exception
179 logger.warning('Unable to symlink %r to %r', src, dst)
180 force_copy = True
181 if force_copy:
182 shutil.copyfile(src, dst)
183
184 def setup_python(self, context):
185 """
186 Set up a Python executable in the environment.
187
188 :param context: The information for the environment creation request
189 being processed.
190 """
191 binpath = context.bin_path
192 exename = context.python_exe
193 path = context.env_exe
194 copier = self.symlink_or_copy
195 copier(context.executable, path)
196 dirname = context.python_dir
197 if os.name != 'nt':
198 if not os.path.islink(path):
199 os.chmod(path, 0o755)
Vinay Sajip44697462012-05-28 16:33:01 +0100200 for suffix in ('python', 'python3'):
201 path = os.path.join(binpath, suffix)
202 if not os.path.exists(path):
203 os.symlink(exename, path)
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100204 else:
205 subdir = 'DLLs'
206 include = self.include_binary
207 files = [f for f in os.listdir(dirname) if include(f)]
208 for f in files:
209 src = os.path.join(dirname, f)
210 dst = os.path.join(binpath, f)
211 if dst != context.env_exe: # already done, above
212 copier(src, dst)
213 dirname = os.path.join(dirname, subdir)
214 if os.path.isdir(dirname):
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 copier(src, dst)
220 # copy init.tcl over
221 for root, dirs, files in os.walk(context.python_dir):
222 if 'init.tcl' in files:
223 tcldir = os.path.basename(root)
224 tcldir = os.path.join(context.env_dir, 'Lib', tcldir)
225 os.makedirs(tcldir)
226 src = os.path.join(root, 'init.tcl')
227 dst = os.path.join(tcldir, 'init.tcl')
228 shutil.copyfile(src, dst)
229 break
230
231 def setup_scripts(self, context):
232 """
233 Set up scripts into the created environment from a directory.
234
235 This method installs the default scripts into the environment
236 being created. You can prevent the default installation by overriding
237 this method if you really need to, or if you need to specify
238 a different location for the scripts to install. By default, the
239 'scripts' directory in the venv package is used as the source of
240 scripts to install.
241 """
242 path = os.path.abspath(os.path.dirname(__file__))
243 path = os.path.join(path, 'scripts')
244 self.install_scripts(context, path)
245
246 def post_setup(self, context):
247 """
248 Hook for post-setup modification of the venv. Subclasses may install
249 additional packages or scripts here, add activation shell scripts, etc.
250
251 :param context: The information for the environment creation request
252 being processed.
253 """
254 pass
255
256 def replace_variables(self, text, context):
257 """
258 Replace variable placeholders in script text with context-specific
259 variables.
260
261 Return the text passed in , but with variables replaced.
262
263 :param text: The text in which to replace placeholder variables.
264 :param context: The information for the environment creation request
265 being processed.
266 """
267 text = text.replace('__VENV_DIR__', context.env_dir)
268 text = text.replace('__VENV_NAME__', context.prompt)
269 text = text.replace('__VENV_BIN_NAME__', context.bin_name)
270 text = text.replace('__VENV_PYTHON__', context.env_exe)
271 return text
272
273 def install_scripts(self, context, path):
274 """
275 Install scripts into the created environment from a directory.
276
277 :param context: The information for the environment creation request
278 being processed.
279 :param path: Absolute pathname of a directory containing script.
280 Scripts in the 'common' subdirectory of this directory,
281 and those in the directory named for the platform
282 being run on, are installed in the created environment.
283 Placeholder variables are replaced with environment-
284 specific values.
285 """
286 binpath = context.bin_path
287 plen = len(path)
288 for root, dirs, files in os.walk(path):
289 if root == path: # at top-level, remove irrelevant dirs
290 for d in dirs[:]:
291 if d not in ('common', os.name):
292 dirs.remove(d)
293 continue # ignore files in top level
294 for f in files:
295 srcfile = os.path.join(root, f)
296 suffix = root[plen:].split(os.sep)[2:]
297 if not suffix:
298 dstdir = binpath
299 else:
300 dstdir = os.path.join(binpath, *suffix)
301 if not os.path.exists(dstdir):
302 os.makedirs(dstdir)
303 dstfile = os.path.join(dstdir, f)
304 with open(srcfile, 'rb') as f:
305 data = f.read()
306 if srcfile.endswith('.exe'):
307 mode = 'wb'
308 else:
309 mode = 'w'
Vinay Sajipbdd13fd2012-10-28 12:39:39 +0000310 try:
311 data = data.decode('utf-8')
312 data = self.replace_variables(data, context)
313 except UnicodeDecodeError as e:
314 data = None
315 logger.warning('unable to copy script %r, '
316 'may be binary: %s', srcfile, e)
317 if data is not None:
318 with open(dstfile, mode) as f:
319 f.write(data)
320 shutil.copymode(srcfile, dstfile)
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100321
322
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100323def create(env_dir, system_site_packages=False, clear=False, symlinks=False):
324 """
325 Create a virtual environment in a directory.
326
Vinay Sajip87ed5992012-11-14 11:18:35 +0000327 By default, makes the system (global) site-packages dir *un*available to
328 the created environment, and uses copying rather than symlinking for files
329 obtained from the source Python installation.
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100330
331 :param env_dir: The target directory to create an environment in.
332 :param system_site_packages: If True, the system (global) site-packages
333 dir is available to the environment.
334 :param clear: If True and the target directory exists, it is deleted.
335 Otherwise, if the target directory exists, an error is
336 raised.
337 :param symlinks: If True, attempt to symlink rather than copy files into
338 virtual environment.
339 """
Vinay Sajip44697462012-05-28 16:33:01 +0100340 builder = EnvBuilder(system_site_packages=system_site_packages,
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100341 clear=clear, symlinks=symlinks)
342 builder.create(env_dir)
343
344def main(args=None):
345 compatible = True
346 if sys.version_info < (3, 3):
347 compatible = False
348 elif not hasattr(sys, 'base_prefix'):
349 compatible = False
350 if not compatible:
Vinay Sajip28952442012-06-25 00:47:46 +0100351 raise ValueError('This script is only for use with Python 3.3')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100352 else:
353 import argparse
354
355 parser = argparse.ArgumentParser(prog=__name__,
356 description='Creates virtual Python '
357 'environments in one or '
358 'more target '
Vinay Sajip4d378d82012-07-08 17:50:42 +0100359 'directories.',
360 epilog='Once an environment has been '
361 'created, you may wish to '
362 'activate it, e.g. by '
363 'sourcing an activate script '
364 'in its bin directory.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100365 parser.add_argument('dirs', metavar='ENV_DIR', nargs='+',
366 help='A directory to create the environment in.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100367 parser.add_argument('--system-site-packages', default=False,
368 action='store_true', dest='system_site',
Vinay Sajip44697462012-05-28 16:33:01 +0100369 help='Give the virtual environment access to the '
370 'system site-packages dir.')
Vinay Sajip90db6612012-07-17 17:33:46 +0100371 if os.name == 'nt':
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100372 use_symlinks = False
373 else:
374 use_symlinks = True
375 parser.add_argument('--symlinks', default=use_symlinks,
376 action='store_true', dest='symlinks',
Vinay Sajip4d378d82012-07-08 17:50:42 +0100377 help='Try to use symlinks rather than copies, '
378 'when symlinks are not the default for '
379 'the platform.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100380 parser.add_argument('--clear', default=False, action='store_true',
381 dest='clear', help='Delete the environment '
382 'directory if it already '
383 'exists. If not specified and '
384 'the directory exists, an error'
385 ' is raised.')
386 parser.add_argument('--upgrade', default=False, action='store_true',
387 dest='upgrade', help='Upgrade the environment '
388 'directory to use this version '
Vinay Sajip42211422012-05-26 20:36:12 +0100389 'of Python, assuming Python '
390 'has been upgraded in-place.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100391 options = parser.parse_args(args)
392 if options.upgrade and options.clear:
393 raise ValueError('you cannot supply --upgrade and --clear together.')
Vinay Sajip44697462012-05-28 16:33:01 +0100394 builder = EnvBuilder(system_site_packages=options.system_site,
395 clear=options.clear, symlinks=options.symlinks,
396 upgrade=options.upgrade)
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100397 for d in options.dirs:
398 builder.create(d)
399
400if __name__ == '__main__':
401 rc = 1
402 try:
403 main()
404 rc = 0
405 except Exception as e:
406 print('Error: %s' % e, file=sys.stderr)
407 sys.exit(rc)