blob: e219fe99e3e7cb5ffa996662ab4ec2981631eae3 [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 logging
29import os
Vinay Sajip7ded1f02012-05-26 03:45:29 +010030import shutil
31import sys
Vinay Sajip44697462012-05-28 16:33:01 +010032import sysconfig
Vinay Sajipc07aa9e2013-07-12 21:10:19 +010033import types
Vinay Sajip7ded1f02012-05-26 03:45:29 +010034
35logger = logging.getLogger(__name__)
36
Vinay Sajip7ded1f02012-05-26 03:45:29 +010037
38class EnvBuilder:
39 """
40 This class exists to allow virtual environment creation to be
41 customised. The constructor parameters determine the builder's
42 behaviour when called upon to create a virtual environment.
43
44 By default, the builder makes the system (global) site-packages dir
Vinay Sajip87ed5992012-11-14 11:18:35 +000045 *un*available to the created environment.
Vinay Sajip7ded1f02012-05-26 03:45:29 +010046
Vinay Sajip87ed5992012-11-14 11:18:35 +000047 If invoked using the Python -m option, the default is to use copying
48 on Windows platforms but symlinks elsewhere. If instantiated some
49 other way, the default is to *not* use symlinks.
Vinay Sajip7ded1f02012-05-26 03:45:29 +010050
51 :param system_site_packages: If True, the system (global) site-packages
52 dir is available to created environments.
53 :param clear: If True and the target directory exists, it is deleted.
54 Otherwise, if the target directory exists, an error is
55 raised.
56 :param symlinks: If True, attempt to symlink rather than copy files into
57 virtual environment.
58 :param upgrade: If True, upgrade an existing virtual environment.
59 """
60
61 def __init__(self, system_site_packages=False, clear=False,
62 symlinks=False, upgrade=False):
63 self.system_site_packages = system_site_packages
64 self.clear = clear
65 self.symlinks = symlinks
66 self.upgrade = upgrade
67
68 def create(self, env_dir):
69 """
70 Create a virtual environment in a directory.
71
72 :param env_dir: The target directory to create an environment in.
73
74 """
Vinay Sajip7ded1f02012-05-26 03:45:29 +010075 env_dir = os.path.abspath(env_dir)
76 context = self.ensure_directories(env_dir)
77 self.create_configuration(context)
78 self.setup_python(context)
79 if not self.upgrade:
80 self.setup_scripts(context)
81 self.post_setup(context)
82
83 def ensure_directories(self, env_dir):
84 """
85 Create the directories for the environment.
86
87 Returns a context object which holds paths in the environment,
88 for use by subsequent logic.
89 """
90
91 def create_if_needed(d):
92 if not os.path.exists(d):
93 os.makedirs(d)
94
95 if os.path.exists(env_dir) and not (self.clear or self.upgrade):
96 raise ValueError('Directory exists: %s' % env_dir)
97 if os.path.exists(env_dir) and self.clear:
Vinay Sajipa6894ba2012-08-24 20:01:02 +010098 shutil.rmtree(env_dir)
Vinay Sajipc07aa9e2013-07-12 21:10:19 +010099 context = types.SimpleNamespace()
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100100 context.env_dir = env_dir
101 context.env_name = os.path.split(env_dir)[1]
102 context.prompt = '(%s) ' % context.env_name
103 create_if_needed(env_dir)
104 env = os.environ
Vinay Sajip28952442012-06-25 00:47:46 +0100105 if sys.platform == 'darwin' and '__PYVENV_LAUNCHER__' in env:
106 executable = os.environ['__PYVENV_LAUNCHER__']
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100107 else:
108 executable = sys.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', 'python%d.%d' % sys.version_info[:2], 'site-packages')
121 context.inc_path = path = os.path.join(env_dir, incpath)
122 create_if_needed(path)
123 create_if_needed(libpath)
124 context.bin_path = binpath = os.path.join(env_dir, binname)
125 context.bin_name = binname
126 context.env_exe = os.path.join(binpath, exename)
127 create_if_needed(binpath)
128 return context
129
130 def create_configuration(self, context):
131 """
132 Create a configuration file indicating where the environment's Python
133 was copied from, and whether the system site-packages should be made
134 available in the environment.
135
136 :param context: The information for the environment creation request
137 being processed.
138 """
139 context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg')
140 with open(path, 'w', encoding='utf-8') as f:
141 f.write('home = %s\n' % context.python_dir)
142 if self.system_site_packages:
143 incl = 'true'
144 else:
145 incl = 'false'
146 f.write('include-system-site-packages = %s\n' % incl)
147 f.write('version = %d.%d.%d\n' % sys.version_info[:3])
148
149 if os.name == 'nt':
150 def include_binary(self, f):
151 if f.endswith(('.pyd', '.dll')):
152 result = True
153 else:
154 result = f.startswith('python') and f.endswith('.exe')
155 return result
156
157 def symlink_or_copy(self, src, dst):
158 """
159 Try symlinking a file, and if that fails, fall back to copying.
160 """
161 force_copy = not self.symlinks
162 if not force_copy:
163 try:
164 if not os.path.islink(dst): # can't link to itself!
165 os.symlink(src, dst)
166 except Exception: # may need to use a more specific exception
167 logger.warning('Unable to symlink %r to %r', src, dst)
168 force_copy = True
169 if force_copy:
170 shutil.copyfile(src, dst)
171
172 def setup_python(self, context):
173 """
174 Set up a Python executable in the environment.
175
176 :param context: The information for the environment creation request
177 being processed.
178 """
179 binpath = context.bin_path
180 exename = context.python_exe
181 path = context.env_exe
182 copier = self.symlink_or_copy
183 copier(context.executable, path)
184 dirname = context.python_dir
185 if os.name != 'nt':
186 if not os.path.islink(path):
187 os.chmod(path, 0o755)
Vinay Sajip44697462012-05-28 16:33:01 +0100188 for suffix in ('python', 'python3'):
189 path = os.path.join(binpath, suffix)
190 if not os.path.exists(path):
191 os.symlink(exename, path)
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100192 else:
193 subdir = 'DLLs'
194 include = self.include_binary
195 files = [f for f in os.listdir(dirname) if include(f)]
196 for f in files:
197 src = os.path.join(dirname, f)
198 dst = os.path.join(binpath, f)
199 if dst != context.env_exe: # already done, above
200 copier(src, dst)
201 dirname = os.path.join(dirname, subdir)
202 if os.path.isdir(dirname):
203 files = [f for f in os.listdir(dirname) if include(f)]
204 for f in files:
205 src = os.path.join(dirname, f)
206 dst = os.path.join(binpath, f)
207 copier(src, dst)
208 # copy init.tcl over
209 for root, dirs, files in os.walk(context.python_dir):
210 if 'init.tcl' in files:
211 tcldir = os.path.basename(root)
212 tcldir = os.path.join(context.env_dir, 'Lib', tcldir)
213 os.makedirs(tcldir)
214 src = os.path.join(root, 'init.tcl')
215 dst = os.path.join(tcldir, 'init.tcl')
216 shutil.copyfile(src, dst)
217 break
218
219 def setup_scripts(self, context):
220 """
221 Set up scripts into the created environment from a directory.
222
223 This method installs the default scripts into the environment
224 being created. You can prevent the default installation by overriding
225 this method if you really need to, or if you need to specify
226 a different location for the scripts to install. By default, the
227 'scripts' directory in the venv package is used as the source of
228 scripts to install.
229 """
230 path = os.path.abspath(os.path.dirname(__file__))
231 path = os.path.join(path, 'scripts')
232 self.install_scripts(context, path)
233
234 def post_setup(self, context):
235 """
236 Hook for post-setup modification of the venv. Subclasses may install
237 additional packages or scripts here, add activation shell scripts, etc.
238
239 :param context: The information for the environment creation request
240 being processed.
241 """
242 pass
243
244 def replace_variables(self, text, context):
245 """
246 Replace variable placeholders in script text with context-specific
247 variables.
248
249 Return the text passed in , but with variables replaced.
250
251 :param text: The text in which to replace placeholder variables.
252 :param context: The information for the environment creation request
253 being processed.
254 """
255 text = text.replace('__VENV_DIR__', context.env_dir)
256 text = text.replace('__VENV_NAME__', context.prompt)
257 text = text.replace('__VENV_BIN_NAME__', context.bin_name)
258 text = text.replace('__VENV_PYTHON__', context.env_exe)
259 return text
260
261 def install_scripts(self, context, path):
262 """
263 Install scripts into the created environment from a directory.
264
265 :param context: The information for the environment creation request
266 being processed.
267 :param path: Absolute pathname of a directory containing script.
268 Scripts in the 'common' subdirectory of this directory,
269 and those in the directory named for the platform
270 being run on, are installed in the created environment.
271 Placeholder variables are replaced with environment-
272 specific values.
273 """
274 binpath = context.bin_path
275 plen = len(path)
276 for root, dirs, files in os.walk(path):
277 if root == path: # at top-level, remove irrelevant dirs
278 for d in dirs[:]:
279 if d not in ('common', os.name):
280 dirs.remove(d)
281 continue # ignore files in top level
282 for f in files:
283 srcfile = os.path.join(root, f)
284 suffix = root[plen:].split(os.sep)[2:]
285 if not suffix:
286 dstdir = binpath
287 else:
288 dstdir = os.path.join(binpath, *suffix)
289 if not os.path.exists(dstdir):
290 os.makedirs(dstdir)
291 dstfile = os.path.join(dstdir, f)
292 with open(srcfile, 'rb') as f:
293 data = f.read()
294 if srcfile.endswith('.exe'):
295 mode = 'wb'
296 else:
297 mode = 'w'
Vinay Sajipbdd13fd2012-10-28 12:39:39 +0000298 try:
299 data = data.decode('utf-8')
300 data = self.replace_variables(data, context)
301 except UnicodeDecodeError as e:
302 data = None
303 logger.warning('unable to copy script %r, '
304 'may be binary: %s', srcfile, e)
305 if data is not None:
306 with open(dstfile, mode) as f:
307 f.write(data)
308 shutil.copymode(srcfile, dstfile)
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100309
310
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100311def create(env_dir, system_site_packages=False, clear=False, symlinks=False):
312 """
313 Create a virtual environment in a directory.
314
Vinay Sajip87ed5992012-11-14 11:18:35 +0000315 By default, makes the system (global) site-packages dir *un*available to
316 the created environment, and uses copying rather than symlinking for files
317 obtained from the source Python installation.
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100318
319 :param env_dir: The target directory to create an environment in.
320 :param system_site_packages: If True, the system (global) site-packages
321 dir is available to the environment.
322 :param clear: If True and the target directory exists, it is deleted.
323 Otherwise, if the target directory exists, an error is
324 raised.
325 :param symlinks: If True, attempt to symlink rather than copy files into
326 virtual environment.
327 """
Vinay Sajip44697462012-05-28 16:33:01 +0100328 builder = EnvBuilder(system_site_packages=system_site_packages,
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100329 clear=clear, symlinks=symlinks)
330 builder.create(env_dir)
331
332def main(args=None):
333 compatible = True
334 if sys.version_info < (3, 3):
335 compatible = False
336 elif not hasattr(sys, 'base_prefix'):
337 compatible = False
338 if not compatible:
Vinay Sajip0e6c66d2013-10-31 18:44:04 +0000339 raise ValueError('This script is only for use with Python >= 3.3')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100340 else:
341 import argparse
342
343 parser = argparse.ArgumentParser(prog=__name__,
344 description='Creates virtual Python '
345 'environments in one or '
346 'more target '
Vinay Sajip4d378d82012-07-08 17:50:42 +0100347 'directories.',
348 epilog='Once an environment has been '
349 'created, you may wish to '
350 'activate it, e.g. by '
351 'sourcing an activate script '
352 'in its bin directory.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100353 parser.add_argument('dirs', metavar='ENV_DIR', nargs='+',
354 help='A directory to create the environment in.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100355 parser.add_argument('--system-site-packages', default=False,
356 action='store_true', dest='system_site',
Vinay Sajip44697462012-05-28 16:33:01 +0100357 help='Give the virtual environment access to the '
358 'system site-packages dir.')
Vinay Sajip90db6612012-07-17 17:33:46 +0100359 if os.name == 'nt':
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100360 use_symlinks = False
361 else:
362 use_symlinks = True
363 parser.add_argument('--symlinks', default=use_symlinks,
364 action='store_true', dest='symlinks',
Vinay Sajip4d378d82012-07-08 17:50:42 +0100365 help='Try to use symlinks rather than copies, '
366 'when symlinks are not the default for '
367 'the platform.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100368 parser.add_argument('--clear', default=False, action='store_true',
369 dest='clear', help='Delete the environment '
370 'directory if it already '
371 'exists. If not specified and '
372 'the directory exists, an error'
373 ' is raised.')
374 parser.add_argument('--upgrade', default=False, action='store_true',
375 dest='upgrade', help='Upgrade the environment '
376 'directory to use this version '
Vinay Sajip42211422012-05-26 20:36:12 +0100377 'of Python, assuming Python '
378 'has been upgraded in-place.')
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100379 options = parser.parse_args(args)
380 if options.upgrade and options.clear:
381 raise ValueError('you cannot supply --upgrade and --clear together.')
Vinay Sajip44697462012-05-28 16:33:01 +0100382 builder = EnvBuilder(system_site_packages=options.system_site,
383 clear=options.clear, symlinks=options.symlinks,
384 upgrade=options.upgrade)
Vinay Sajip7ded1f02012-05-26 03:45:29 +0100385 for d in options.dirs:
386 builder.create(d)
387
388if __name__ == '__main__':
389 rc = 1
390 try:
391 main()
392 rc = 0
393 except Exception as e:
394 print('Error: %s' % e, file=sys.stderr)
395 sys.exit(rc)