Browse code

move from distutils to setuputils

setuputils is newer than distutils, and is supported on python 2.6 and
higher like we currently support. This also packages everything into
eggs for distribution. Maybe this will reduce the number of people
with bugs regarding where s3cmd's modules got installed.

Matt Domsch authored on 2015/02/04 03:48:00
Showing 2 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,356 @@
0
+#!/usr/bin/env python
1
+
2
+"""
3
+Setuptools bootstrapping installer.
4
+
5
+Run this script to install or upgrade setuptools.
6
+"""
7
+
8
+import os
9
+import shutil
10
+import sys
11
+import tempfile
12
+import zipfile
13
+import optparse
14
+import subprocess
15
+import platform
16
+import textwrap
17
+import contextlib
18
+import warnings
19
+
20
+from distutils import log
21
+
22
+try:
23
+    from urllib.request import urlopen
24
+except ImportError:
25
+    from urllib2 import urlopen
26
+
27
+try:
28
+    from site import USER_SITE
29
+except ImportError:
30
+    USER_SITE = None
31
+
32
+DEFAULT_VERSION = "12.0.5"
33
+DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
34
+
35
+
36
+def _python_cmd(*args):
37
+    """
38
+    Execute a command.
39
+
40
+    Return True if the command succeeded.
41
+    """
42
+    args = (sys.executable,) + args
43
+    return subprocess.call(args) == 0
44
+
45
+
46
+def _install(archive_filename, install_args=()):
47
+    """Install Setuptools."""
48
+    with archive_context(archive_filename):
49
+        # installing
50
+        log.warn('Installing Setuptools')
51
+        if not _python_cmd('setup.py', 'install', *install_args):
52
+            log.warn('Something went wrong during the installation.')
53
+            log.warn('See the error message above.')
54
+            # exitcode will be 2
55
+            return 2
56
+
57
+
58
+def _build_egg(egg, archive_filename, to_dir):
59
+    """Build Setuptools egg."""
60
+    with archive_context(archive_filename):
61
+        # building an egg
62
+        log.warn('Building a Setuptools egg in %s', to_dir)
63
+        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
64
+    # returning the result
65
+    log.warn(egg)
66
+    if not os.path.exists(egg):
67
+        raise IOError('Could not build the egg.')
68
+
69
+
70
+class ContextualZipFile(zipfile.ZipFile):
71
+
72
+    """Supplement ZipFile class to support context manager for Python 2.6."""
73
+
74
+    def __enter__(self):
75
+        return self
76
+
77
+    def __exit__(self, type, value, traceback):
78
+        self.close()
79
+
80
+    def __new__(cls, *args, **kwargs):
81
+        """Construct a ZipFile or ContextualZipFile as appropriate."""
82
+        if hasattr(zipfile.ZipFile, '__exit__'):
83
+            return zipfile.ZipFile(*args, **kwargs)
84
+        return super(ContextualZipFile, cls).__new__(cls)
85
+
86
+
87
+@contextlib.contextmanager
88
+def archive_context(filename):
89
+    """
90
+    Unzip filename to a temporary directory, set to the cwd.
91
+
92
+    The unzipped target is cleaned up after.
93
+    """
94
+    tmpdir = tempfile.mkdtemp()
95
+    log.warn('Extracting in %s', tmpdir)
96
+    old_wd = os.getcwd()
97
+    try:
98
+        os.chdir(tmpdir)
99
+        with ContextualZipFile(filename) as archive:
100
+            archive.extractall()
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
+        yield
107
+
108
+    finally:
109
+        os.chdir(old_wd)
110
+        shutil.rmtree(tmpdir)
111
+
112
+
113
+def _do_download(version, download_base, to_dir, download_delay):
114
+    """Download Setuptools."""
115
+    egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg'
116
+                       % (version, sys.version_info[0], sys.version_info[1]))
117
+    if not os.path.exists(egg):
118
+        archive = download_setuptools(version, download_base,
119
+                                      to_dir, download_delay)
120
+        _build_egg(egg, archive, to_dir)
121
+    sys.path.insert(0, egg)
122
+
123
+    # Remove previously-imported pkg_resources if present (see
124
+    # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
125
+    if 'pkg_resources' in sys.modules:
126
+        del sys.modules['pkg_resources']
127
+
128
+    import setuptools
129
+    setuptools.bootstrap_install_from = egg
130
+
131
+
132
+def use_setuptools(
133
+        version=DEFAULT_VERSION, download_base=DEFAULT_URL,
134
+        to_dir=os.curdir, download_delay=15):
135
+    """
136
+    *deprecated* Download, install, and import Setuptools.
137
+
138
+    Return None.
139
+    """
140
+    warnings.warn(
141
+        "`use_setuptools` is deprecated. To enforce a specific "
142
+        "version of setuptools, use `pkg_resources.require`.",
143
+        DeprecationWarning,
144
+    )
145
+    to_dir = os.path.abspath(to_dir)
146
+    rep_modules = 'pkg_resources', 'setuptools'
147
+    imported = set(sys.modules).intersection(rep_modules)
148
+    conflict_tmpl = textwrap.dedent("""
149
+        The required version of setuptools (>={version}) is not available,
150
+        and can't be installed while this script is running. Please
151
+        install a more recent version first, using
152
+        'easy_install -U setuptools'.
153
+
154
+        (Currently using {VC_err.args[0]!r})
155
+        """)
156
+    try:
157
+        import pkg_resources
158
+    except ImportError:
159
+        return _do_download(version, download_base, to_dir, download_delay)
160
+    try:
161
+        pkg_resources.require("setuptools>=" + version)
162
+        return
163
+    except pkg_resources.DistributionNotFound:
164
+        return _do_download(version, download_base, to_dir, download_delay)
165
+    except pkg_resources.VersionConflict as VC_err:
166
+        if imported:
167
+            msg = conflict_tmpl.format(VC_err=VC_err, version=version)
168
+            sys.stderr.write(msg)
169
+            sys.exit(2)
170
+
171
+        # otherwise, reload ok
172
+        del pkg_resources, sys.modules['pkg_resources']
173
+        return _do_download(version, download_base, to_dir, download_delay)
174
+
175
+
176
+def _clean_check(cmd, target):
177
+    """
178
+    Run the command to download target.
179
+
180
+    If the command fails, clean up before re-raising the error.
181
+    """
182
+    try:
183
+        subprocess.check_call(cmd)
184
+    except subprocess.CalledProcessError:
185
+        if os.access(target, os.F_OK):
186
+            os.unlink(target)
187
+        raise
188
+
189
+
190
+def download_file_powershell(url, target):
191
+    """
192
+    Download the file at url to target using Powershell.
193
+
194
+    Powershell will validate trust.
195
+    Raise an exception if the command cannot complete.
196
+    """
197
+    target = os.path.abspath(target)
198
+    ps_cmd = (
199
+        "[System.Net.WebRequest]::DefaultWebProxy.Credentials = "
200
+        "[System.Net.CredentialCache]::DefaultCredentials; "
201
+        "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)"
202
+        % vars()
203
+    )
204
+    cmd = [
205
+        'powershell',
206
+        '-Command',
207
+        ps_cmd,
208
+    ]
209
+    _clean_check(cmd, target)
210
+
211
+
212
+def has_powershell():
213
+    """Determine if Powershell is available."""
214
+    if platform.system() != 'Windows':
215
+        return False
216
+    cmd = ['powershell', '-Command', 'echo test']
217
+    with open(os.path.devnull, 'wb') as devnull:
218
+        try:
219
+            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
220
+        except Exception:
221
+            return False
222
+    return True
223
+download_file_powershell.viable = has_powershell
224
+
225
+
226
+def download_file_curl(url, target):
227
+    cmd = ['curl', url, '--silent', '--output', target]
228
+    _clean_check(cmd, target)
229
+
230
+
231
+def has_curl():
232
+    cmd = ['curl', '--version']
233
+    with open(os.path.devnull, 'wb') as devnull:
234
+        try:
235
+            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
236
+        except Exception:
237
+            return False
238
+    return True
239
+download_file_curl.viable = has_curl
240
+
241
+
242
+def download_file_wget(url, target):
243
+    cmd = ['wget', url, '--quiet', '--output-document', target]
244
+    _clean_check(cmd, target)
245
+
246
+
247
+def has_wget():
248
+    cmd = ['wget', '--version']
249
+    with open(os.path.devnull, 'wb') as devnull:
250
+        try:
251
+            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
252
+        except Exception:
253
+            return False
254
+    return True
255
+download_file_wget.viable = has_wget
256
+
257
+
258
+def download_file_insecure(url, target):
259
+    """Use Python to download the file, without connection authentication."""
260
+    src = urlopen(url)
261
+    try:
262
+        # Read all the data in one block.
263
+        data = src.read()
264
+    finally:
265
+        src.close()
266
+
267
+    # Write all the data in one block to avoid creating a partial file.
268
+    with open(target, "wb") as dst:
269
+        dst.write(data)
270
+download_file_insecure.viable = lambda: True
271
+
272
+
273
+def get_best_downloader():
274
+    downloaders = (
275
+        download_file_powershell,
276
+        download_file_curl,
277
+        download_file_wget,
278
+        download_file_insecure,
279
+    )
280
+    viable_downloaders = (dl for dl in downloaders if dl.viable())
281
+    return next(viable_downloaders, None)
282
+
283
+
284
+def download_setuptools(
285
+        version=DEFAULT_VERSION, download_base=DEFAULT_URL,
286
+        to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader):
287
+    """
288
+    Download setuptools from a specified location and return its filename.
289
+
290
+    `version` should be a valid setuptools version number that is available
291
+    as an sdist for download under the `download_base` URL (which should end
292
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
293
+    `delay` is the number of seconds to pause before an actual download
294
+    attempt.
295
+
296
+    ``downloader_factory`` should be a function taking no arguments and
297
+    returning a function for downloading a URL to a target.
298
+    """
299
+    # making sure we use the absolute path
300
+    to_dir = os.path.abspath(to_dir)
301
+    zip_name = "setuptools-%s.zip" % version
302
+    url = download_base + zip_name
303
+    saveto = os.path.join(to_dir, zip_name)
304
+    if not os.path.exists(saveto):  # Avoid repeated downloads
305
+        log.warn("Downloading %s", url)
306
+        downloader = downloader_factory()
307
+        downloader(url, saveto)
308
+    return os.path.realpath(saveto)
309
+
310
+
311
+def _build_install_args(options):
312
+    """
313
+    Build the arguments to 'python setup.py install' on the setuptools package.
314
+
315
+    Returns list of command line arguments.
316
+    """
317
+    return ['--user'] if options.user_install else []
318
+
319
+
320
+def _parse_args():
321
+    """Parse the command line for options."""
322
+    parser = optparse.OptionParser()
323
+    parser.add_option(
324
+        '--user', dest='user_install', action='store_true', default=False,
325
+        help='install in user site package (requires Python 2.6 or later)')
326
+    parser.add_option(
327
+        '--download-base', dest='download_base', metavar="URL",
328
+        default=DEFAULT_URL,
329
+        help='alternative URL from where to download the setuptools package')
330
+    parser.add_option(
331
+        '--insecure', dest='downloader_factory', action='store_const',
332
+        const=lambda: download_file_insecure, default=get_best_downloader,
333
+        help='Use internal, non-validating downloader'
334
+    )
335
+    parser.add_option(
336
+        '--version', help="Specify which version to download",
337
+        default=DEFAULT_VERSION,
338
+    )
339
+    options, args = parser.parse_args()
340
+    # positional arguments are ignored
341
+    return options
342
+
343
+
344
+def main():
345
+    """Install or upgrade setuptools and EasyInstall."""
346
+    options = _parse_args()
347
+    archive = download_setuptools(
348
+        version=options.version,
349
+        download_base=options.download_base,
350
+        downloader_factory=options.downloader_factory,
351
+    )
352
+    return _install(archive, _build_install_args(options))
353
+
354
+if __name__ == '__main__':
355
+    sys.exit(main())
... ...
@@ -1,7 +1,10 @@
1
-from distutils.core import setup
2 1
 import sys
3 2
 import os
4 3
 
4
+import ez_setup
5
+ez_setup.use_setuptools()
6
+from setuptools import setup, find_packages
7
+
5 8
 import S3.PkgInfo
6 9
 
7 10
 if float("%d.%d" % sys.version_info[:2]) < 2.6: