]> git.phdru.name Git - m_lib.git/blob - ez_setup.py
Use int() instead of atoi() for Py3 compatibility
[m_lib.git] / ez_setup.py
1 #!python
2 """Bootstrap setuptools 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 ez_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 shutil
18 import sys
19 import tempfile
20 import tarfile
21 import optparse
22 import subprocess
23 import platform
24
25 from distutils import log
26
27 try:
28     from site import USER_SITE
29 except ImportError:
30     USER_SITE = None
31
32 DEFAULT_VERSION = "1.4.2"
33 DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
34
35 def _python_cmd(*args):
36     args = (sys.executable,) + args
37     return subprocess.call(args) == 0
38
39 def _check_call_py24(cmd, *args, **kwargs):
40     res = subprocess.call(cmd, *args, **kwargs)
41     class CalledProcessError(Exception):
42         pass
43     if not res == 0:
44         msg = "Command '%s' return non-zero exit status %d" % (cmd, res)
45         raise CalledProcessError(msg)
46 vars(subprocess).setdefault('check_call', _check_call_py24)
47
48 def _install(tarball, install_args=()):
49     # extracting the tarball
50     tmpdir = tempfile.mkdtemp()
51     log.warn('Extracting in %s', tmpdir)
52     old_wd = os.getcwd()
53     try:
54         os.chdir(tmpdir)
55         tar = tarfile.open(tarball)
56         _extractall(tar)
57         tar.close()
58
59         # going in the directory
60         subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
61         os.chdir(subdir)
62         log.warn('Now working in %s', subdir)
63
64         # installing
65         log.warn('Installing Setuptools')
66         if not _python_cmd('setup.py', 'install', *install_args):
67             log.warn('Something went wrong during the installation.')
68             log.warn('See the error message above.')
69             # exitcode will be 2
70             return 2
71     finally:
72         os.chdir(old_wd)
73         shutil.rmtree(tmpdir)
74
75
76 def _build_egg(egg, tarball, to_dir):
77     # extracting the tarball
78     tmpdir = tempfile.mkdtemp()
79     log.warn('Extracting in %s', tmpdir)
80     old_wd = os.getcwd()
81     try:
82         os.chdir(tmpdir)
83         tar = tarfile.open(tarball)
84         _extractall(tar)
85         tar.close()
86
87         # going in the directory
88         subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
89         os.chdir(subdir)
90         log.warn('Now working in %s', subdir)
91
92         # building an egg
93         log.warn('Building a Setuptools egg in %s', to_dir)
94         _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
95
96     finally:
97         os.chdir(old_wd)
98         shutil.rmtree(tmpdir)
99     # returning the result
100     log.warn(egg)
101     if not os.path.exists(egg):
102         raise IOError('Could not build the egg.')
103
104
105 def _do_download(version, download_base, to_dir, download_delay):
106     egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg'
107                        % (version, sys.version_info[0], sys.version_info[1]))
108     if not os.path.exists(egg):
109         tarball = download_setuptools(version, download_base,
110                                       to_dir, download_delay)
111         _build_egg(egg, tarball, to_dir)
112     sys.path.insert(0, egg)
113
114     # Remove previously-imported pkg_resources if present (see
115     # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
116     if 'pkg_resources' in sys.modules:
117         del sys.modules['pkg_resources']
118
119     import setuptools
120     setuptools.bootstrap_install_from = egg
121
122
123 def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
124                    to_dir=os.curdir, download_delay=15):
125     # making sure we use the absolute path
126     to_dir = os.path.abspath(to_dir)
127     was_imported = 'pkg_resources' in sys.modules or \
128         'setuptools' in sys.modules
129     try:
130         import pkg_resources
131     except ImportError:
132         return _do_download(version, download_base, to_dir, download_delay)
133     try:
134         pkg_resources.require("setuptools>=" + version)
135         return
136     except pkg_resources.VersionConflict:
137         e = sys.exc_info()[1]
138         if was_imported:
139             sys.stderr.write(
140             "The required version of setuptools (>=%s) is not available,\n"
141             "and can't be installed while this script is running. Please\n"
142             "install a more recent version first, using\n"
143             "'easy_install -U setuptools'."
144             "\n\n(Currently using %r)\n" % (version, e.args[0]))
145             sys.exit(2)
146         else:
147             del pkg_resources, sys.modules['pkg_resources']    # reload ok
148             return _do_download(version, download_base, to_dir,
149                                 download_delay)
150     except pkg_resources.DistributionNotFound:
151         return _do_download(version, download_base, to_dir,
152                             download_delay)
153
154 def _clean_check(cmd, target):
155     """
156     Run the command to download target. If the command fails, clean up before
157     re-raising the error.
158     """
159     try:
160         subprocess.check_call(cmd)
161     except subprocess.CalledProcessError:
162         if os.access(target, os.F_OK):
163             os.unlink(target)
164         raise
165
166 def download_file_powershell(url, target):
167     """
168     Download the file at url to target using Powershell (which will validate
169     trust). Raise an exception if the command cannot complete.
170     """
171     target = os.path.abspath(target)
172     cmd = [
173         'powershell',
174         '-Command',
175         "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(),
176     ]
177     _clean_check(cmd, target)
178
179 def has_powershell():
180     if platform.system() != 'Windows':
181         return False
182     cmd = ['powershell', '-Command', 'echo test']
183     devnull = open(os.path.devnull, 'wb')
184     try:
185         try:
186             subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
187         except:
188             return False
189     finally:
190         devnull.close()
191     return True
192
193 download_file_powershell.viable = has_powershell
194
195 def download_file_curl(url, target):
196     cmd = ['curl', url, '--silent', '--output', target]
197     _clean_check(cmd, target)
198
199 def has_curl():
200     cmd = ['curl', '--version']
201     devnull = open(os.path.devnull, 'wb')
202     try:
203         try:
204             subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
205         except:
206             return False
207     finally:
208         devnull.close()
209     return True
210
211 download_file_curl.viable = has_curl
212
213 def download_file_wget(url, target):
214     cmd = ['wget', url, '--quiet', '--output-document', target]
215     _clean_check(cmd, target)
216
217 def has_wget():
218     cmd = ['wget', '--version']
219     devnull = open(os.path.devnull, 'wb')
220     try:
221         try:
222             subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
223         except:
224             return False
225     finally:
226         devnull.close()
227     return True
228
229 download_file_wget.viable = has_wget
230
231 def download_file_insecure(url, target):
232     """
233     Use Python to download the file, even though it cannot authenticate the
234     connection.
235     """
236     try:
237         from urllib.request import urlopen
238     except ImportError:
239         from urllib2 import urlopen
240     src = dst = None
241     try:
242         src = urlopen(url)
243         # Read/write all in one block, so we don't create a corrupt file
244         # if the download is interrupted.
245         data = src.read()
246         dst = open(target, "wb")
247         dst.write(data)
248     finally:
249         if src:
250             src.close()
251         if dst:
252             dst.close()
253
254 download_file_insecure.viable = lambda: True
255
256 def get_best_downloader():
257     downloaders = [
258         download_file_powershell,
259         download_file_curl,
260         download_file_wget,
261         download_file_insecure,
262     ]
263
264     for dl in downloaders:
265         if dl.viable():
266             return dl
267
268 def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
269                         to_dir=os.curdir, delay=15,
270                         downloader_factory=get_best_downloader):
271     """Download setuptools from a specified location and return its filename
272
273     `version` should be a valid setuptools version number that is available
274     as an egg for download under the `download_base` URL (which should end
275     with a '/'). `to_dir` is the directory where the egg will be downloaded.
276     `delay` is the number of seconds to pause before an actual download
277     attempt.
278
279     ``downloader_factory`` should be a function taking no arguments and
280     returning a function for downloading a URL to a target.
281     """
282     # making sure we use the absolute path
283     to_dir = os.path.abspath(to_dir)
284     tgz_name = "setuptools-%s.tar.gz" % version
285     url = download_base + tgz_name
286     saveto = os.path.join(to_dir, tgz_name)
287     if not os.path.exists(saveto):  # Avoid repeated downloads
288         log.warn("Downloading %s", url)
289         downloader = downloader_factory()
290         downloader(url, saveto)
291     return os.path.realpath(saveto)
292
293
294 def _extractall(self, path=".", members=None):
295     """Extract all members from the archive to the current working
296        directory and set owner, modification time and permissions on
297        directories afterwards. `path' specifies a different directory
298        to extract to. `members' is optional and must be a subset of the
299        list returned by getmembers().
300     """
301     import copy
302     import operator
303     from tarfile import ExtractError
304     directories = []
305
306     if members is None:
307         members = self
308
309     for tarinfo in members:
310         if tarinfo.isdir():
311             # Extract directories with a safe mode.
312             directories.append(tarinfo)
313             tarinfo = copy.copy(tarinfo)
314             tarinfo.mode = 448  # decimal for oct 0700
315         self.extract(tarinfo, path)
316
317     # Reverse sort directories.
318     if sys.version_info < (2, 4):
319         def sorter(dir1, dir2):
320             return cmp(dir1.name, dir2.name)
321         directories.sort(sorter)
322         directories.reverse()
323     else:
324         directories.sort(key=operator.attrgetter('name'), reverse=True)
325
326     # Set correct owner, mtime and filemode on directories.
327     for tarinfo in directories:
328         dirpath = os.path.join(path, tarinfo.name)
329         try:
330             self.chown(tarinfo, dirpath)
331             self.utime(tarinfo, dirpath)
332             self.chmod(tarinfo, dirpath)
333         except ExtractError:
334             e = sys.exc_info()[1]
335             if self.errorlevel > 1:
336                 raise
337             else:
338                 self._dbg(1, "tarfile: %s" % e)
339
340
341 def _build_install_args(options):
342     """
343     Build the arguments to 'python setup.py install' on the setuptools package
344     """
345     install_args = []
346     if options.user_install:
347         if sys.version_info < (2, 6):
348             log.warn("--user requires Python 2.6 or later")
349             raise SystemExit(1)
350         install_args.append('--user')
351     return install_args
352
353 def _parse_args():
354     """
355     Parse the command line for options
356     """
357     parser = optparse.OptionParser()
358     parser.add_option(
359         '--user', dest='user_install', action='store_true', default=False,
360         help='install in user site package (requires Python 2.6 or later)')
361     parser.add_option(
362         '--download-base', dest='download_base', metavar="URL",
363         default=DEFAULT_URL,
364         help='alternative URL from where to download the setuptools package')
365     parser.add_option(
366         '--insecure', dest='downloader_factory', action='store_const',
367         const=lambda: download_file_insecure, default=get_best_downloader,
368         help='Use internal, non-validating downloader'
369     )
370     options, args = parser.parse_args()
371     # positional arguments are ignored
372     return options
373
374 def main(version=DEFAULT_VERSION):
375     """Install or upgrade setuptools and EasyInstall"""
376     options = _parse_args()
377     tarball = download_setuptools(download_base=options.download_base,
378         downloader_factory=options.downloader_factory)
379     return _install(tarball, _build_install_args(options))
380
381 if __name__ == '__main__':
382     sys.exit(main())