Skip to content

Commit 94d290b

Browse files
author
Peter Amstutz
committed
Add ez_setup.py to bootstrap recent setuptools.
1 parent aeda57e commit 94d290b

File tree

2 files changed

+337
-1
lines changed

2 files changed

+337
-1
lines changed

ez_setup.py

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

setup.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
import os
44
import sys
5-
import setuptools.command.egg_info as egg_info_cmd
65
import shutil
76

7+
import ez_setup
8+
ez_setup.use_setuptools(version="18.2")
9+
10+
import setuptools.command.egg_info as egg_info_cmd
11+
812
from setuptools import setup, find_packages
913

1014
SETUP_DIR = os.path.dirname(__file__)

0 commit comments

Comments
 (0)