comparison distribute_setup.py @ 0:2d587ea676bf 0.0.1a

Initial version
author Ben Croston <ben@croston.org>
date Mon, 03 Sep 2012 17:18:45 +0100
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:2d587ea676bf
1 #!python
2 """Bootstrap distribute installation
3
4 If you want to use setuptools in your package's setup.py, just include this
5 file in the same directory with it, and add this to the top of your setup.py::
6
7 from distribute_setup import use_setuptools
8 use_setuptools()
9
10 If you want to require a specific version of setuptools, set a download
11 mirror, or use an alternate download directory, you can do so by supplying
12 the appropriate options to ``use_setuptools()``.
13
14 This file can also be run as a script to install or upgrade setuptools.
15 """
16 import os
17 import sys
18 import time
19 import fnmatch
20 import tempfile
21 import tarfile
22 from distutils import log
23
24 try:
25 from site import USER_SITE
26 except ImportError:
27 USER_SITE = None
28
29 try:
30 import subprocess
31
32 def _python_cmd(*args):
33 args = (sys.executable,) + args
34 return subprocess.call(args) == 0
35
36 except ImportError:
37 # will be used for python 2.3
38 def _python_cmd(*args):
39 args = (sys.executable,) + args
40 # quoting arguments if windows
41 if sys.platform == 'win32':
42 def quote(arg):
43 if ' ' in arg:
44 return '"%s"' % arg
45 return arg
46 args = [quote(arg) for arg in args]
47 return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
48
49 DEFAULT_VERSION = "0.6.27"
50 DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
51 SETUPTOOLS_FAKED_VERSION = "0.6c11"
52
53 SETUPTOOLS_PKG_INFO = """\
54 Metadata-Version: 1.0
55 Name: setuptools
56 Version: %s
57 Summary: xxxx
58 Home-page: xxx
59 Author: xxx
60 Author-email: xxx
61 License: xxx
62 Description: xxx
63 """ % SETUPTOOLS_FAKED_VERSION
64
65
66 def _install(tarball, install_args=()):
67 # extracting the tarball
68 tmpdir = tempfile.mkdtemp()
69 log.warn('Extracting in %s', tmpdir)
70 old_wd = os.getcwd()
71 try:
72 os.chdir(tmpdir)
73 tar = tarfile.open(tarball)
74 _extractall(tar)
75 tar.close()
76
77 # going in the directory
78 subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
79 os.chdir(subdir)
80 log.warn('Now working in %s', subdir)
81
82 # installing
83 log.warn('Installing Distribute')
84 if not _python_cmd('setup.py', 'install', *install_args):
85 log.warn('Something went wrong during the installation.')
86 log.warn('See the error message above.')
87 finally:
88 os.chdir(old_wd)
89
90
91 def _build_egg(egg, tarball, to_dir):
92 # extracting the tarball
93 tmpdir = tempfile.mkdtemp()
94 log.warn('Extracting in %s', tmpdir)
95 old_wd = os.getcwd()
96 try:
97 os.chdir(tmpdir)
98 tar = tarfile.open(tarball)
99 _extractall(tar)
100 tar.close()
101
102 # going in the directory
103 subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
104 os.chdir(subdir)
105 log.warn('Now working in %s', subdir)
106
107 # building an egg
108 log.warn('Building a Distribute egg in %s', to_dir)
109 _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
110
111 finally:
112 os.chdir(old_wd)
113 # returning the result
114 log.warn(egg)
115 if not os.path.exists(egg):
116 raise IOError('Could not build the egg.')
117
118
119 def _do_download(version, download_base, to_dir, download_delay):
120 egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
121 % (version, sys.version_info[0], sys.version_info[1]))
122 if not os.path.exists(egg):
123 tarball = download_setuptools(version, download_base,
124 to_dir, download_delay)
125 _build_egg(egg, tarball, to_dir)
126 sys.path.insert(0, egg)
127 import setuptools
128 setuptools.bootstrap_install_from = egg
129
130
131 def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
132 to_dir=os.curdir, download_delay=15, no_fake=True):
133 # making sure we use the absolute path
134 to_dir = os.path.abspath(to_dir)
135 was_imported = 'pkg_resources' in sys.modules or \
136 'setuptools' in sys.modules
137 try:
138 try:
139 import pkg_resources
140 if not hasattr(pkg_resources, '_distribute'):
141 if not no_fake:
142 _fake_setuptools()
143 raise ImportError
144 except ImportError:
145 return _do_download(version, download_base, to_dir, download_delay)
146 try:
147 pkg_resources.require("distribute>="+version)
148 return
149 except pkg_resources.VersionConflict:
150 e = sys.exc_info()[1]
151 if was_imported:
152 sys.stderr.write(
153 "The required version of distribute (>=%s) is not available,\n"
154 "and can't be installed while this script is running. Please\n"
155 "install a more recent version first, using\n"
156 "'easy_install -U distribute'."
157 "\n\n(Currently using %r)\n" % (version, e.args[0]))
158 sys.exit(2)
159 else:
160 del pkg_resources, sys.modules['pkg_resources'] # reload ok
161 return _do_download(version, download_base, to_dir,
162 download_delay)
163 except pkg_resources.DistributionNotFound:
164 return _do_download(version, download_base, to_dir,
165 download_delay)
166 finally:
167 if not no_fake:
168 _create_fake_setuptools_pkg_info(to_dir)
169
170 def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
171 to_dir=os.curdir, delay=15):
172 """Download distribute from a specified location and return its filename
173
174 `version` should be a valid distribute version number that is available
175 as an egg for download under the `download_base` URL (which should end
176 with a '/'). `to_dir` is the directory where the egg will be downloaded.
177 `delay` is the number of seconds to pause before an actual download
178 attempt.
179 """
180 # making sure we use the absolute path
181 to_dir = os.path.abspath(to_dir)
182 try:
183 from urllib.request import urlopen
184 except ImportError:
185 from urllib2 import urlopen
186 tgz_name = "distribute-%s.tar.gz" % version
187 url = download_base + tgz_name
188 saveto = os.path.join(to_dir, tgz_name)
189 src = dst = None
190 if not os.path.exists(saveto): # Avoid repeated downloads
191 try:
192 log.warn("Downloading %s", url)
193 src = urlopen(url)
194 # Read/write all in one block, so we don't create a corrupt file
195 # if the download is interrupted.
196 data = src.read()
197 dst = open(saveto, "wb")
198 dst.write(data)
199 finally:
200 if src:
201 src.close()
202 if dst:
203 dst.close()
204 return os.path.realpath(saveto)
205
206 def _no_sandbox(function):
207 def __no_sandbox(*args, **kw):
208 try:
209 from setuptools.sandbox import DirectorySandbox
210 if not hasattr(DirectorySandbox, '_old'):
211 def violation(*args):
212 pass
213 DirectorySandbox._old = DirectorySandbox._violation
214 DirectorySandbox._violation = violation
215 patched = True
216 else:
217 patched = False
218 except ImportError:
219 patched = False
220
221 try:
222 return function(*args, **kw)
223 finally:
224 if patched:
225 DirectorySandbox._violation = DirectorySandbox._old
226 del DirectorySandbox._old
227
228 return __no_sandbox
229
230 def _patch_file(path, content):
231 """Will backup the file then patch it"""
232 existing_content = open(path).read()
233 if existing_content == content:
234 # already patched
235 log.warn('Already patched.')
236 return False
237 log.warn('Patching...')
238 _rename_path(path)
239 f = open(path, 'w')
240 try:
241 f.write(content)
242 finally:
243 f.close()
244 return True
245
246 _patch_file = _no_sandbox(_patch_file)
247
248 def _same_content(path, content):
249 return open(path).read() == content
250
251 def _rename_path(path):
252 new_name = path + '.OLD.%s' % time.time()
253 log.warn('Renaming %s into %s', path, new_name)
254 os.rename(path, new_name)
255 return new_name
256
257 def _remove_flat_installation(placeholder):
258 if not os.path.isdir(placeholder):
259 log.warn('Unkown installation at %s', placeholder)
260 return False
261 found = False
262 for file in os.listdir(placeholder):
263 if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
264 found = True
265 break
266 if not found:
267 log.warn('Could not locate setuptools*.egg-info')
268 return
269
270 log.warn('Removing elements out of the way...')
271 pkg_info = os.path.join(placeholder, file)
272 if os.path.isdir(pkg_info):
273 patched = _patch_egg_dir(pkg_info)
274 else:
275 patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
276
277 if not patched:
278 log.warn('%s already patched.', pkg_info)
279 return False
280 # now let's move the files out of the way
281 for element in ('setuptools', 'pkg_resources.py', 'site.py'):
282 element = os.path.join(placeholder, element)
283 if os.path.exists(element):
284 _rename_path(element)
285 else:
286 log.warn('Could not find the %s element of the '
287 'Setuptools distribution', element)
288 return True
289
290 _remove_flat_installation = _no_sandbox(_remove_flat_installation)
291
292 def _after_install(dist):
293 log.warn('After install bootstrap.')
294 placeholder = dist.get_command_obj('install').install_purelib
295 _create_fake_setuptools_pkg_info(placeholder)
296
297 def _create_fake_setuptools_pkg_info(placeholder):
298 if not placeholder or not os.path.exists(placeholder):
299 log.warn('Could not find the install location')
300 return
301 pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
302 setuptools_file = 'setuptools-%s-py%s.egg-info' % \
303 (SETUPTOOLS_FAKED_VERSION, pyver)
304 pkg_info = os.path.join(placeholder, setuptools_file)
305 if os.path.exists(pkg_info):
306 log.warn('%s already exists', pkg_info)
307 return
308
309 if not os.access(pkg_info, os.W_OK):
310 log.warn("Don't have permissions to write %s, skipping", pkg_info)
311
312 log.warn('Creating %s', pkg_info)
313 f = open(pkg_info, 'w')
314 try:
315 f.write(SETUPTOOLS_PKG_INFO)
316 finally:
317 f.close()
318
319 pth_file = os.path.join(placeholder, 'setuptools.pth')
320 log.warn('Creating %s', pth_file)
321 f = open(pth_file, 'w')
322 try:
323 f.write(os.path.join(os.curdir, setuptools_file))
324 finally:
325 f.close()
326
327 _create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info)
328
329 def _patch_egg_dir(path):
330 # let's check if it's already patched
331 pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
332 if os.path.exists(pkg_info):
333 if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
334 log.warn('%s already patched.', pkg_info)
335 return False
336 _rename_path(path)
337 os.mkdir(path)
338 os.mkdir(os.path.join(path, 'EGG-INFO'))
339 pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
340 f = open(pkg_info, 'w')
341 try:
342 f.write(SETUPTOOLS_PKG_INFO)
343 finally:
344 f.close()
345 return True
346
347 _patch_egg_dir = _no_sandbox(_patch_egg_dir)
348
349 def _before_install():
350 log.warn('Before install bootstrap.')
351 _fake_setuptools()
352
353
354 def _under_prefix(location):
355 if 'install' not in sys.argv:
356 return True
357 args = sys.argv[sys.argv.index('install')+1:]
358 for index, arg in enumerate(args):
359 for option in ('--root', '--prefix'):
360 if arg.startswith('%s=' % option):
361 top_dir = arg.split('root=')[-1]
362 return location.startswith(top_dir)
363 elif arg == option:
364 if len(args) > index:
365 top_dir = args[index+1]
366 return location.startswith(top_dir)
367 if arg == '--user' and USER_SITE is not None:
368 return location.startswith(USER_SITE)
369 return True
370
371
372 def _fake_setuptools():
373 log.warn('Scanning installed packages')
374 try:
375 import pkg_resources
376 except ImportError:
377 # we're cool
378 log.warn('Setuptools or Distribute does not seem to be installed.')
379 return
380 ws = pkg_resources.working_set
381 try:
382 setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools',
383 replacement=False))
384 except TypeError:
385 # old distribute API
386 setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools'))
387
388 if setuptools_dist is None:
389 log.warn('No setuptools distribution found')
390 return
391 # detecting if it was already faked
392 setuptools_location = setuptools_dist.location
393 log.warn('Setuptools installation detected at %s', setuptools_location)
394
395 # if --root or --preix was provided, and if
396 # setuptools is not located in them, we don't patch it
397 if not _under_prefix(setuptools_location):
398 log.warn('Not patching, --root or --prefix is installing Distribute'
399 ' in another location')
400 return
401
402 # let's see if its an egg
403 if not setuptools_location.endswith('.egg'):
404 log.warn('Non-egg installation')
405 res = _remove_flat_installation(setuptools_location)
406 if not res:
407 return
408 else:
409 log.warn('Egg installation')
410 pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
411 if (os.path.exists(pkg_info) and
412 _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
413 log.warn('Already patched.')
414 return
415 log.warn('Patching...')
416 # let's create a fake egg replacing setuptools one
417 res = _patch_egg_dir(setuptools_location)
418 if not res:
419 return
420 log.warn('Patched done.')
421 _relaunch()
422
423
424 def _relaunch():
425 log.warn('Relaunching...')
426 # we have to relaunch the process
427 # pip marker to avoid a relaunch bug
428 if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']:
429 sys.argv[0] = 'setup.py'
430 args = [sys.executable] + sys.argv
431 sys.exit(subprocess.call(args))
432
433
434 def _extractall(self, path=".", members=None):
435 """Extract all members from the archive to the current working
436 directory and set owner, modification time and permissions on
437 directories afterwards. `path' specifies a different directory
438 to extract to. `members' is optional and must be a subset of the
439 list returned by getmembers().
440 """
441 import copy
442 import operator
443 from tarfile import ExtractError
444 directories = []
445
446 if members is None:
447 members = self
448
449 for tarinfo in members:
450 if tarinfo.isdir():
451 # Extract directories with a safe mode.
452 directories.append(tarinfo)
453 tarinfo = copy.copy(tarinfo)
454 tarinfo.mode = 448 # decimal for oct 0700
455 self.extract(tarinfo, path)
456
457 # Reverse sort directories.
458 if sys.version_info < (2, 4):
459 def sorter(dir1, dir2):
460 return cmp(dir1.name, dir2.name)
461 directories.sort(sorter)
462 directories.reverse()
463 else:
464 directories.sort(key=operator.attrgetter('name'), reverse=True)
465
466 # Set correct owner, mtime and filemode on directories.
467 for tarinfo in directories:
468 dirpath = os.path.join(path, tarinfo.name)
469 try:
470 self.chown(tarinfo, dirpath)
471 self.utime(tarinfo, dirpath)
472 self.chmod(tarinfo, dirpath)
473 except ExtractError:
474 e = sys.exc_info()[1]
475 if self.errorlevel > 1:
476 raise
477 else:
478 self._dbg(1, "tarfile: %s" % e)
479
480 def _build_install_args(argv):
481 install_args = []
482 user_install = '--user' in argv
483 if user_install and sys.version_info < (2,6):
484 log.warn("--user requires Python 2.6 or later")
485 raise SystemExit(1)
486 if user_install:
487 install_args.append('--user')
488 return install_args
489
490 def main(argv, version=DEFAULT_VERSION):
491 """Install or upgrade setuptools and EasyInstall"""
492 tarball = download_setuptools()
493 _install(tarball, _build_install_args(argv))
494
495
496 if __name__ == '__main__':
497 main(sys.argv[1:])