ez_setup.py 8.61 KB
Newer Older
1
#!python
2
"""Bootstrap setuptools installation
3 4 5 6

If you want to use setuptools in your package's setup.py, just include this
file in the same directory with it, and add this to the top of your setup.py::

7
    from ez_setup import use_setuptools
8 9 10 11 12 13 14 15
    use_setuptools()

If you want to require a specific version of setuptools, set a download
mirror, or use an alternate download directory, you can do so by supplying
the appropriate options to ``use_setuptools()``.

This file can also be run as a script to install or upgrade setuptools.
"""
16
import os
17
import shutil
18
import sys
agronholm's avatar
agronholm committed
19 20
import tempfile
import tarfile
21
import optparse
22
import subprocess
23

agronholm's avatar
agronholm committed
24 25
from distutils import log

26 27 28 29 30
try:
    from site import USER_SITE
except ImportError:
    USER_SITE = None

31
DEFAULT_VERSION = "0.9.7"
32
DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
agronholm's avatar
agronholm committed
33

34 35 36
def _python_cmd(*args):
    args = (sys.executable,) + args
    return subprocess.call(args) == 0
37

38
def _install(tarball, install_args=()):
39 40
    # extracting the tarball
    tmpdir = tempfile.mkdtemp()
41
    log.warn('Extracting in %s', tmpdir)
42 43 44 45
    old_wd = os.getcwd()
    try:
        os.chdir(tmpdir)
        tar = tarfile.open(tarball)
46
        _extractall(tar)
47 48 49 50 51
        tar.close()

        # going in the directory
        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
        os.chdir(subdir)
52
        log.warn('Now working in %s', subdir)
53 54

        # installing
55
        log.warn('Installing Setuptools')
56
        if not _python_cmd('setup.py', 'install', *install_args):
57 58
            log.warn('Something went wrong during the installation.')
            log.warn('See the error message above.')
59 60
            # exitcode will be 2
            return 2
61 62
    finally:
        os.chdir(old_wd)
63
        shutil.rmtree(tmpdir)
64

agronholm's avatar
agronholm committed
65

66
def _build_egg(egg, tarball, to_dir):
67 68
    # extracting the tarball
    tmpdir = tempfile.mkdtemp()
69
    log.warn('Extracting in %s', tmpdir)
70 71 72 73
    old_wd = os.getcwd()
    try:
        os.chdir(tmpdir)
        tar = tarfile.open(tarball)
74
        _extractall(tar)
75 76 77 78 79
        tar.close()

        # going in the directory
        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
        os.chdir(subdir)
80
        log.warn('Now working in %s', subdir)
81 82

        # building an egg
83
        log.warn('Building a Setuptools egg in %s', to_dir)
84
        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
85 86 87

    finally:
        os.chdir(old_wd)
88
        shutil.rmtree(tmpdir)
89 90 91 92
    # returning the result
    log.warn(egg)
    if not os.path.exists(egg):
        raise IOError('Could not build the egg.')
93

agronholm's avatar
agronholm committed
94

95
def _do_download(version, download_base, to_dir, download_delay):
96
    egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg'
97
                       % (version, sys.version_info[0], sys.version_info[1]))
98 99 100
    if not os.path.exists(egg):
        tarball = download_setuptools(version, download_base,
                                      to_dir, download_delay)
101
        _build_egg(egg, tarball, to_dir)
102
    sys.path.insert(0, egg)
103 104 105

    # Remove previously-imported pkg_resources if present (see
    # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
106 107
    if 'pkg_resources' in sys.modules:
        del sys.modules['pkg_resources']
108

109 110 111
    import setuptools
    setuptools.bootstrap_install_from = egg

agronholm's avatar
agronholm committed
112 113

def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
114
                   to_dir=os.curdir, download_delay=15):
115 116
    # making sure we use the absolute path
    to_dir = os.path.abspath(to_dir)
agronholm's avatar
agronholm committed
117 118
    was_imported = 'pkg_resources' in sys.modules or \
        'setuptools' in sys.modules
119
    try:
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
        import pkg_resources
    except ImportError:
        return _do_download(version, download_base, to_dir, download_delay)
    try:
        pkg_resources.require("setuptools>=" + version)
        return
    except pkg_resources.VersionConflict:
        e = sys.exc_info()[1]
        if was_imported:
            sys.stderr.write(
            "The required version of setuptools (>=%s) is not available,\n"
            "and can't be installed while this script is running. Please\n"
            "install a more recent version first, using\n"
            "'easy_install -U setuptools'."
            "\n\n(Currently using %r)\n" % (version, e.args[0]))
            sys.exit(2)
        else:
            del pkg_resources, sys.modules['pkg_resources']    # reload ok
138 139
            return _do_download(version, download_base, to_dir,
                                download_delay)
140 141 142
    except pkg_resources.DistributionNotFound:
        return _do_download(version, download_base, to_dir,
                            download_delay)
agronholm's avatar
agronholm committed
143

Marcin Kuzminski's avatar
Marcin Kuzminski committed
144

agronholm's avatar
agronholm committed
145 146
def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
                        to_dir=os.curdir, delay=15):
147
    """Download setuptools from a specified location and return its filename
148

149
    `version` should be a valid setuptools version number that is available
150 151
    as an egg for download under the `download_base` URL (which should end
    with a '/'). `to_dir` is the directory where the egg will be downloaded.
agronholm's avatar
agronholm committed
152 153
    `delay` is the number of seconds to pause before an actual download
    attempt.
154
    """
155 156
    # making sure we use the absolute path
    to_dir = os.path.abspath(to_dir)
157 158 159 160
    try:
        from urllib.request import urlopen
    except ImportError:
        from urllib2 import urlopen
161
    tgz_name = "setuptools-%s.tar.gz" % version
tarek's avatar
tarek committed
162 163
    url = download_base + tgz_name
    saveto = os.path.join(to_dir, tgz_name)
164
    src = dst = None
165 166 167
    if not os.path.exists(saveto):  # Avoid repeated downloads
        try:
            log.warn("Downloading %s", url)
168
            src = urlopen(url)
169 170
            # Read/write all in one block, so we don't create a corrupt file
            # if the download is interrupted.
tarek's avatar
tarek committed
171
            data = src.read()
tarek's avatar
tarek committed
172 173
            dst = open(saveto, "wb")
            dst.write(data)
174
        finally:
tarek's avatar
tarek committed
175 176 177 178
            if src:
                src.close()
            if dst:
                dst.close()
179 180
    return os.path.realpath(saveto)

Marcin Kuzminski's avatar
Marcin Kuzminski committed
181

182
def _extractall(self, path=".", members=None):
agronholm's avatar
agronholm committed
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
    """Extract all members from the archive to the current working
       directory and set owner, modification time and permissions on
       directories afterwards. `path' specifies a different directory
       to extract to. `members' is optional and must be a subset of the
       list returned by getmembers().
    """
    import copy
    import operator
    from tarfile import ExtractError
    directories = []

    if members is None:
        members = self

    for tarinfo in members:
        if tarinfo.isdir():
            # Extract directories with a safe mode.
            directories.append(tarinfo)
            tarinfo = copy.copy(tarinfo)
Marcin Kuzminski's avatar
Marcin Kuzminski committed
202
            tarinfo.mode = 448  # decimal for oct 0700
agronholm's avatar
agronholm committed
203 204 205
        self.extract(tarinfo, path)

    # Reverse sort directories.
206 207 208 209 210 211 212
    if sys.version_info < (2, 4):
        def sorter(dir1, dir2):
            return cmp(dir1.name, dir2.name)
        directories.sort(sorter)
        directories.reverse()
    else:
        directories.sort(key=operator.attrgetter('name'), reverse=True)
agronholm's avatar
agronholm committed
213 214 215 216 217 218 219 220

    # Set correct owner, mtime and filemode on directories.
    for tarinfo in directories:
        dirpath = os.path.join(path, tarinfo.name)
        try:
            self.chown(tarinfo, dirpath)
            self.utime(tarinfo, dirpath)
            self.chmod(tarinfo, dirpath)
221 222
        except ExtractError:
            e = sys.exc_info()[1]
agronholm's avatar
agronholm committed
223 224 225 226 227
            if self.errorlevel > 1:
                raise
            else:
                self._dbg(1, "tarfile: %s" % e)

Marcin Kuzminski's avatar
Marcin Kuzminski committed
228

229 230
def _build_install_args(options):
    """
231
    Build the arguments to 'python setup.py install' on the setuptools package
232
    """
233
    install_args = []
234
    if options.user_install:
235 236 237
        if sys.version_info < (2, 6):
            log.warn("--user requires Python 2.6 or later")
            raise SystemExit(1)
238
        install_args.append('--user')
239
    return install_args
tarek's avatar
tarek committed
240

241 242 243 244
def _parse_args():
    """
    Parse the command line for options
    """
245 246 247 248
    parser = optparse.OptionParser()
    parser.add_option(
        '--user', dest='user_install', action='store_true', default=False,
        help='install in user site package (requires Python 2.6 or later)')
249 250 251
    parser.add_option(
        '--download-base', dest='download_base', metavar="URL",
        default=DEFAULT_URL,
252
        help='alternative URL from where to download the setuptools package')
253
    options, args = parser.parse_args()
254 255
    # positional arguments are ignored
    return options
256

257 258 259 260
def main(version=DEFAULT_VERSION):
    """Install or upgrade setuptools and EasyInstall"""
    options = _parse_args()
    tarball = download_setuptools(download_base=options.download_base)
261
    return _install(tarball, _build_install_args(options))
agronholm's avatar
agronholm committed
262

263
if __name__ == '__main__':
264
    sys.exit(main())