Merge branch 'packaging' into debian-0.8
[pithos] / snf-pithos-backend / distribute_setup.py
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.10"
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):
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'):
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     log.warn('Creating %s', pkg_info)
310     f = open(pkg_info, 'w')
311     try:
312         f.write(SETUPTOOLS_PKG_INFO)
313     finally:
314         f.close()
315
316     pth_file = os.path.join(placeholder, 'setuptools.pth')
317     log.warn('Creating %s', pth_file)
318     f = open(pth_file, 'w')
319     try:
320         f.write(os.path.join(os.curdir, setuptools_file))
321     finally:
322         f.close()
323
324 _create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info)
325
326 def _patch_egg_dir(path):
327     # let's check if it's already patched
328     pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
329     if os.path.exists(pkg_info):
330         if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
331             log.warn('%s already patched.', pkg_info)
332             return False
333     _rename_path(path)
334     os.mkdir(path)
335     os.mkdir(os.path.join(path, 'EGG-INFO'))
336     pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
337     f = open(pkg_info, 'w')
338     try:
339         f.write(SETUPTOOLS_PKG_INFO)
340     finally:
341         f.close()
342     return True
343
344 _patch_egg_dir = _no_sandbox(_patch_egg_dir)
345
346 def _before_install():
347     log.warn('Before install bootstrap.')
348     _fake_setuptools()
349
350
351 def _under_prefix(location):
352     if 'install' not in sys.argv:
353         return True
354     args = sys.argv[sys.argv.index('install')+1:]
355     for index, arg in enumerate(args):
356         for option in ('--root', '--prefix'):
357             if arg.startswith('%s=' % option):
358                 top_dir = arg.split('root=')[-1]
359                 return location.startswith(top_dir)
360             elif arg == option:
361                 if len(args) > index:
362                     top_dir = args[index+1]
363                     return location.startswith(top_dir)
364         if arg == '--user' and USER_SITE is not None:
365             return location.startswith(USER_SITE)
366     return True
367
368
369 def _fake_setuptools():
370     log.warn('Scanning installed packages')
371     try:
372         import pkg_resources
373     except ImportError:
374         # we're cool
375         log.warn('Setuptools or Distribute does not seem to be installed.')
376         return
377     ws = pkg_resources.working_set
378     try:
379         setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools',
380                                   replacement=False))
381     except TypeError:
382         # old distribute API
383         setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools'))
384
385     if setuptools_dist is None:
386         log.warn('No setuptools distribution found')
387         return
388     # detecting if it was already faked
389     setuptools_location = setuptools_dist.location
390     log.warn('Setuptools installation detected at %s', setuptools_location)
391
392     # if --root or --preix was provided, and if
393     # setuptools is not located in them, we don't patch it
394     if not _under_prefix(setuptools_location):
395         log.warn('Not patching, --root or --prefix is installing Distribute'
396                  ' in another location')
397         return
398
399     # let's see if its an egg
400     if not setuptools_location.endswith('.egg'):
401         log.warn('Non-egg installation')
402         res = _remove_flat_installation(setuptools_location)
403         if not res:
404             return
405     else:
406         log.warn('Egg installation')
407         pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
408         if (os.path.exists(pkg_info) and
409             _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
410             log.warn('Already patched.')
411             return
412         log.warn('Patching...')
413         # let's create a fake egg replacing setuptools one
414         res = _patch_egg_dir(setuptools_location)
415         if not res:
416             return
417     log.warn('Patched done.')
418     _relaunch()
419
420
421 def _relaunch():
422     log.warn('Relaunching...')
423     # we have to relaunch the process
424     # pip marker to avoid a relaunch bug
425     if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']:
426         sys.argv[0] = 'setup.py'
427     args = [sys.executable] + sys.argv
428     sys.exit(subprocess.call(args))
429
430
431 def _extractall(self, path=".", members=None):
432     """Extract all members from the archive to the current working
433        directory and set owner, modification time and permissions on
434        directories afterwards. `path' specifies a different directory
435        to extract to. `members' is optional and must be a subset of the
436        list returned by getmembers().
437     """
438     import copy
439     import operator
440     from tarfile import ExtractError
441     directories = []
442
443     if members is None:
444         members = self
445
446     for tarinfo in members:
447         if tarinfo.isdir():
448             # Extract directories with a safe mode.
449             directories.append(tarinfo)
450             tarinfo = copy.copy(tarinfo)
451             tarinfo.mode = 448 # decimal for oct 0700
452         self.extract(tarinfo, path)
453
454     # Reverse sort directories.
455     if sys.version_info < (2, 4):
456         def sorter(dir1, dir2):
457             return cmp(dir1.name, dir2.name)
458         directories.sort(sorter)
459         directories.reverse()
460     else:
461         directories.sort(key=operator.attrgetter('name'), reverse=True)
462
463     # Set correct owner, mtime and filemode on directories.
464     for tarinfo in directories:
465         dirpath = os.path.join(path, tarinfo.name)
466         try:
467             self.chown(tarinfo, dirpath)
468             self.utime(tarinfo, dirpath)
469             self.chmod(tarinfo, dirpath)
470         except ExtractError:
471             e = sys.exc_info()[1]
472             if self.errorlevel > 1:
473                 raise
474             else:
475                 self._dbg(1, "tarfile: %s" % e)
476
477
478 def main(argv, version=DEFAULT_VERSION):
479     """Install or upgrade setuptools and EasyInstall"""
480     tarball = download_setuptools()
481     _install(tarball)
482
483
484 if __name__ == '__main__':
485     main(sys.argv[1:])