diff options
author | Ben West <bewest@gmail.com> | 2014-05-24 16:44:18 -0700 |
---|---|---|
committer | Ben West <bewest@gmail.com> | 2014-05-24 16:44:18 -0700 |
commit | 233f3e5ed307df775f53ccc66e5823dd0d46b75f (patch) | |
tree | 3c5ec358844de71b7c6435d405ede4b328302daf /ez_setup.py | |
parent | 5d92368e5b10fb2d01ae25a2730e15cd05a84da6 (diff) |
Make remotely installable, via pip/distutils et al
Diffstat (limited to 'ez_setup.py')
-rw-r--r-- | ez_setup.py | 357 |
1 files changed, 357 insertions, 0 deletions
diff --git a/ez_setup.py b/ez_setup.py new file mode 100644 index 0000000..837ef3f --- /dev/null +++ b/ez_setup.py | |||
@@ -0,0 +1,357 @@ | |||
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.0" | ||
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 download_file_powershell(url, target): | ||
155 | """ | ||
156 | Download the file at url to target using Powershell (which will validate | ||
157 | trust). Raise an exception if the command cannot complete. | ||
158 | """ | ||
159 | target = os.path.abspath(target) | ||
160 | cmd = [ | ||
161 | 'powershell', | ||
162 | '-Command', | ||
163 | "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(), | ||
164 | ] | ||
165 | subprocess.check_call(cmd) | ||
166 | |||
167 | def has_powershell(): | ||
168 | if platform.system() != 'Windows': | ||
169 | return False | ||
170 | cmd = ['powershell', '-Command', 'echo test'] | ||
171 | devnull = open(os.path.devnull, 'wb') | ||
172 | try: | ||
173 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) | ||
174 | except: | ||
175 | return False | ||
176 | finally: | ||
177 | devnull.close() | ||
178 | return True | ||
179 | |||
180 | download_file_powershell.viable = has_powershell | ||
181 | |||
182 | def download_file_curl(url, target): | ||
183 | cmd = ['curl', url, '--silent', '--output', target] | ||
184 | subprocess.check_call(cmd) | ||
185 | |||
186 | def has_curl(): | ||
187 | cmd = ['curl', '--version'] | ||
188 | devnull = open(os.path.devnull, 'wb') | ||
189 | try: | ||
190 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) | ||
191 | except: | ||
192 | return False | ||
193 | finally: | ||
194 | devnull.close() | ||
195 | return True | ||
196 | |||
197 | download_file_curl.viable = has_curl | ||
198 | |||
199 | def download_file_wget(url, target): | ||
200 | cmd = ['wget', url, '--quiet', '--output-document', target] | ||
201 | subprocess.check_call(cmd) | ||
202 | |||
203 | def has_wget(): | ||
204 | cmd = ['wget', '--version'] | ||
205 | devnull = open(os.path.devnull, 'wb') | ||
206 | try: | ||
207 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) | ||
208 | except: | ||
209 | return False | ||
210 | finally: | ||
211 | devnull.close() | ||
212 | return True | ||
213 | |||
214 | download_file_wget.viable = has_wget | ||
215 | |||
216 | def download_file_insecure(url, target): | ||
217 | """ | ||
218 | Use Python to download the file, even though it cannot authenticate the | ||
219 | connection. | ||
220 | """ | ||
221 | try: | ||
222 | from urllib.request import urlopen | ||
223 | except ImportError: | ||
224 | from urllib2 import urlopen | ||
225 | src = dst = None | ||
226 | try: | ||
227 | src = urlopen(url) | ||
228 | # Read/write all in one block, so we don't create a corrupt file | ||
229 | # if the download is interrupted. | ||
230 | data = src.read() | ||
231 | dst = open(target, "wb") | ||
232 | dst.write(data) | ||
233 | finally: | ||
234 | if src: | ||
235 | src.close() | ||
236 | if dst: | ||
237 | dst.close() | ||
238 | |||
239 | download_file_insecure.viable = lambda: True | ||
240 | |||
241 | def get_best_downloader(): | ||
242 | downloaders = [ | ||
243 | download_file_powershell, | ||
244 | download_file_curl, | ||
245 | download_file_wget, | ||
246 | download_file_insecure, | ||
247 | ] | ||
248 | |||
249 | for dl in downloaders: | ||
250 | if dl.viable(): | ||
251 | return dl | ||
252 | |||
253 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, | ||
254 | to_dir=os.curdir, delay=15): | ||
255 | """Download setuptools from a specified location and return its filename | ||
256 | |||
257 | `version` should be a valid setuptools version number that is available | ||
258 | as an egg for download under the `download_base` URL (which should end | ||
259 | with a '/'). `to_dir` is the directory where the egg will be downloaded. | ||
260 | `delay` is the number of seconds to pause before an actual download | ||
261 | attempt. | ||
262 | """ | ||
263 | # making sure we use the absolute path | ||
264 | to_dir = os.path.abspath(to_dir) | ||
265 | tgz_name = "setuptools-%s.tar.gz" % version | ||
266 | url = download_base + tgz_name | ||
267 | saveto = os.path.join(to_dir, tgz_name) | ||
268 | if not os.path.exists(saveto): # Avoid repeated downloads | ||
269 | log.warn("Downloading %s", url) | ||
270 | downloader = get_best_downloader() | ||
271 | downloader(url, saveto) | ||
272 | return os.path.realpath(saveto) | ||
273 | |||
274 | |||
275 | def _extractall(self, path=".", members=None): | ||
276 | """Extract all members from the archive to the current working | ||
277 | directory and set owner, modification time and permissions on | ||
278 | directories afterwards. `path' specifies a different directory | ||
279 | to extract to. `members' is optional and must be a subset of the | ||
280 | list returned by getmembers(). | ||
281 | """ | ||
282 | import copy | ||
283 | import operator | ||
284 | from tarfile import ExtractError | ||
285 | directories = [] | ||
286 | |||
287 | if members is None: | ||
288 | members = self | ||
289 | |||
290 | for tarinfo in members: | ||
291 | if tarinfo.isdir(): | ||
292 | # Extract directories with a safe mode. | ||
293 | directories.append(tarinfo) | ||
294 | tarinfo = copy.copy(tarinfo) | ||
295 | tarinfo.mode = 448 # decimal for oct 0700 | ||
296 | self.extract(tarinfo, path) | ||
297 | |||
298 | # Reverse sort directories. | ||
299 | if sys.version_info < (2, 4): | ||
300 | def sorter(dir1, dir2): | ||
301 | return cmp(dir1.name, dir2.name) | ||
302 | directories.sort(sorter) | ||
303 | directories.reverse() | ||
304 | else: | ||
305 | directories.sort(key=operator.attrgetter('name'), reverse=True) | ||
306 | |||
307 | # Set correct owner, mtime and filemode on directories. | ||
308 | for tarinfo in directories: | ||
309 | dirpath = os.path.join(path, tarinfo.name) | ||
310 | try: | ||
311 | self.chown(tarinfo, dirpath) | ||
312 | self.utime(tarinfo, dirpath) | ||
313 | self.chmod(tarinfo, dirpath) | ||
314 | except ExtractError: | ||
315 | e = sys.exc_info()[1] | ||
316 | if self.errorlevel > 1: | ||
317 | raise | ||
318 | else: | ||
319 | self._dbg(1, "tarfile: %s" % e) | ||
320 | |||
321 | |||
322 | def _build_install_args(options): | ||
323 | """ | ||
324 | Build the arguments to 'python setup.py install' on the setuptools package | ||
325 | """ | ||
326 | install_args = [] | ||
327 | if options.user_install: | ||
328 | if sys.version_info < (2, 6): | ||
329 | log.warn("--user requires Python 2.6 or later") | ||
330 | raise SystemExit(1) | ||
331 | install_args.append('--user') | ||
332 | return install_args | ||
333 | |||
334 | def _parse_args(): | ||
335 | """ | ||
336 | Parse the command line for options | ||
337 | """ | ||
338 | parser = optparse.OptionParser() | ||
339 | parser.add_option( | ||
340 | '--user', dest='user_install', action='store_true', default=False, | ||
341 | help='install in user site package (requires Python 2.6 or later)') | ||
342 | parser.add_option( | ||
343 | '--download-base', dest='download_base', metavar="URL", | ||
344 | default=DEFAULT_URL, | ||
345 | help='alternative URL from where to download the setuptools package') | ||
346 | options, args = parser.parse_args() | ||
347 | # positional arguments are ignored | ||
348 | return options | ||
349 | |||
350 | def main(version=DEFAULT_VERSION): | ||
351 | """Install or upgrade setuptools and EasyInstall""" | ||
352 | options = _parse_args() | ||
353 | tarball = download_setuptools(download_base=options.download_base) | ||
354 | return _install(tarball, _build_install_args(options)) | ||
355 | |||
356 | if __name__ == '__main__': | ||
357 | sys.exit(main()) | ||