comparison distribute_setup.py @ 39:a3f01c1ff7b4

installtion: install script first draft
author Goffi <goffi@goffi.org>
date Tue, 25 Jan 2011 16:59:18 +0100
parents
children
comparison
equal deleted inserted replaced
38:daa19e8abf49 39:a3f01c1ff7b4
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.14"
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:])