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