blob: 706ad559db9c753fe02d24077600875684eb7682 [file] [log] [blame]
Matt Clarkson60910b32015-03-30 14:28:27 +01001#! /usr/bin/env python
2# encoding: utf-8
3
4import argparse
5import errno
6import logging
7import os
8import platform
9import re
10import sys
11import subprocess
12import tempfile
13
14try:
15 import winreg
16except ImportError:
17 import _winreg as winreg
18try:
19 import urllib.request as request
20except ImportError:
21 import urllib as request
22try:
23 import urllib.parse as parse
24except ImportError:
25 import urlparse as parse
26
27class EmptyLogger(object):
28 '''
29 Provides an implementation that performs no logging
30 '''
31 def debug(self, *k, **kw):
32 pass
33 def info(self, *k, **kw):
34 pass
35 def warn(self, *k, **kw):
36 pass
37 def error(self, *k, **kw):
38 pass
39 def critical(self, *k, **kw):
40 pass
41 def setLevel(self, *k, **kw):
42 pass
43
44urls = (
45 'http://downloads.sourceforge.net/project/mingw-w64/Toolchains%20'
46 'targetting%20Win32/Personal%20Builds/mingw-builds/installer/'
47 'repository.txt',
48 'http://downloads.sourceforge.net/project/mingwbuilds/host-windows/'
49 'repository.txt'
50)
51'''
52A list of mingw-build repositories
53'''
54
55def repository(urls = urls, log = EmptyLogger()):
56 '''
57 Downloads and parse mingw-build repository files and parses them
58 '''
59 log.info('getting mingw-builds repository')
60 versions = {}
61 re_sourceforge = re.compile(r'http://sourceforge.net/projects/([^/]+)/files')
62 re_sub = r'http://downloads.sourceforge.net/project/\1'
63 for url in urls:
64 log.debug(' - requesting: %s', url)
65 socket = request.urlopen(url)
66 repo = socket.read()
67 if not isinstance(repo, str):
68 repo = repo.decode();
69 socket.close()
70 for entry in repo.split('\n')[:-1]:
71 value = entry.split('|')
72 version = tuple([int(n) for n in value[0].strip().split('.')])
73 version = versions.setdefault(version, {})
74 arch = value[1].strip()
75 if arch == 'x32':
76 arch = 'i686'
77 elif arch == 'x64':
78 arch = 'x86_64'
79 arch = version.setdefault(arch, {})
80 threading = arch.setdefault(value[2].strip(), {})
81 exceptions = threading.setdefault(value[3].strip(), {})
82 revision = exceptions.setdefault(int(value[4].strip()[3:]),
83 re_sourceforge.sub(re_sub, value[5].strip()))
84 return versions
85
86def find_in_path(file, path=None):
87 '''
88 Attempts to find an executable in the path
89 '''
90 if platform.system() == 'Windows':
91 file += '.exe'
92 if path is None:
93 path = os.environ.get('PATH', '')
94 if type(path) is type(''):
95 path = path.split(os.pathsep)
96 return list(filter(os.path.exists,
97 map(lambda dir, file=file: os.path.join(dir, file), path)))
98
99def find_7zip(log = EmptyLogger()):
100 '''
101 Attempts to find 7zip for unpacking the mingw-build archives
102 '''
103 log.info('finding 7zip')
104 path = find_in_path('7z')
105 if not path:
106 key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\7-Zip')
107 path, _ = winreg.QueryValueEx(key, 'Path')
108 path = [os.path.join(path, '7z.exe')]
109 log.debug('found \'%s\'', path[0])
110 return path[0]
111
112find_7zip()
113
114def unpack(archive, location, log = EmptyLogger()):
115 '''
116 Unpacks a mingw-builds archive
117 '''
118 sevenzip = find_7zip(log)
119 log.info('unpacking %s', os.path.basename(archive))
120 cmd = [sevenzip, 'x', archive, '-o' + location, '-y']
121 log.debug(' - %r', cmd)
122 with open(os.devnull, 'w') as devnull:
123 subprocess.check_call(cmd, stdout = devnull)
124
125def download(url, location, log = EmptyLogger()):
126 '''
127 Downloads and unpacks a mingw-builds archive
128 '''
129 log.info('downloading MinGW')
130 log.debug(' - url: %s', url)
131 log.debug(' - location: %s', location)
132
133 re_content = re.compile(r'attachment;[ \t]*filename=(")?([^"]*)(")?[\r\n]*')
134
135 stream = request.urlopen(url)
136 try:
137 content = stream.getheader('Content-Disposition') or ''
138 except AttributeError:
139 content = stream.headers.getheader('Content-Disposition') or ''
140 matches = re_content.match(content)
141 if matches:
142 filename = matches.group(2)
143 else:
144 parsed = parse.urlparse(stream.geturl())
145 filename = os.path.basename(parsed.path)
146
147 try:
148 os.makedirs(location)
149 except OSError as e:
150 if e.errno == errno.EEXIST and os.path.isdir(location):
151 pass
152 else:
153 raise
154
155 archive = os.path.join(location, filename)
156 with open(archive, 'wb') as out:
157 while True:
158 buf = stream.read(1024)
159 if not buf:
160 break
161 out.write(buf)
162 unpack(archive, location, log = log)
163 os.remove(archive)
164
165 possible = os.path.join(location, 'mingw64')
166 if not os.path.exists(possible):
167 possible = os.path.join(location, 'mingw32')
168 if not os.path.exists(possible):
169 raise ValueError('Failed to find unpacked MinGW: ' + possible)
170 return possible
171
172def root(location = None, arch = None, version = None, threading = None,
173 exceptions = None, revision = None, log = EmptyLogger()):
174 '''
175 Returns the root folder of a specific version of the mingw-builds variant
176 of gcc. Will download the compiler if needed
177 '''
178
179 # Get the repository if we don't have all the information
180 if not (arch and version and threading and exceptions and revision):
181 versions = repository(log = log)
182
183 # Determine some defaults
184 version = version or max(versions.keys())
185 if not arch:
186 arch = platform.machine().lower()
187 if arch == 'x86':
188 arch = 'i686'
189 elif arch == 'amd64':
190 arch = 'x86_64'
191 if not threading:
192 keys = versions[version][arch].keys()
193 if 'posix' in keys:
194 threading = 'posix'
195 elif 'win32' in keys:
196 threading = 'win32'
197 else:
198 threading = keys[0]
199 if not exceptions:
200 keys = versions[version][arch][threading].keys()
201 if 'seh' in keys:
202 exceptions = 'seh'
203 elif 'sjlj' in keys:
204 exceptions = 'sjlj'
205 else:
206 exceptions = keys[0]
207 if revision == None:
208 revision = max(versions[version][arch][threading][exceptions].keys())
209 if not location:
210 location = os.path.join(tempfile.gettempdir(), 'mingw-builds')
211
212 # Get the download url
213 url = versions[version][arch][threading][exceptions][revision]
214
215 # Tell the user whatzzup
216 log.info('finding MinGW %s', '.'.join(str(v) for v in version))
217 log.debug(' - arch: %s', arch)
218 log.debug(' - threading: %s', threading)
219 log.debug(' - exceptions: %s', exceptions)
220 log.debug(' - revision: %s', revision)
221 log.debug(' - url: %s', url)
222
223 # Store each specific revision differently
224 slug = '{version}-{arch}-{threading}-{exceptions}-rev{revision}'
225 slug = slug.format(
226 version = '.'.join(str(v) for v in version),
227 arch = arch,
228 threading = threading,
229 exceptions = exceptions,
230 revision = revision
231 )
232 if arch == 'x86_64':
233 root_dir = os.path.join(location, slug, 'mingw64')
234 elif arch == 'i686':
235 root_dir = os.path.join(location, slug, 'mingw32')
236 else:
237 raise ValueError('Unknown MinGW arch: ' + arch)
238
239 # Download if needed
240 if not os.path.exists(root_dir):
241 downloaded = download(url, os.path.join(location, slug), log = log)
242 if downloaded != root_dir:
243 raise ValueError('The location of mingw did not match\n%s\n%s'
244 % (downloaded, root_dir))
245
246 return root_dir
247
248def str2ver(string):
249 '''
250 Converts a version string into a tuple
251 '''
252 try:
253 version = tuple(int(v) for v in string.split('.'))
254 if len(version) is not 3:
255 raise ValueError()
256 except ValueError:
257 raise argparse.ArgumentTypeError(
258 'please provide a three digit version string')
259 return version
260
261def main():
262 '''
263 Invoked when the script is run directly by the python interpreter
264 '''
265 parser = argparse.ArgumentParser(
266 description = 'Downloads a specific version of MinGW',
267 formatter_class = argparse.ArgumentDefaultsHelpFormatter
268 )
269 parser.add_argument('--location',
270 help = 'the location to download the compiler to',
271 default = os.path.join(tempfile.gettempdir(), 'mingw-builds'))
272 parser.add_argument('--arch', required = True, choices = ['i686', 'x86_64'],
273 help = 'the target MinGW architecture string')
274 parser.add_argument('--version', type = str2ver,
275 help = 'the version of GCC to download')
276 parser.add_argument('--threading', choices = ['posix', 'win32'],
277 help = 'the threading type of the compiler')
278 parser.add_argument('--exceptions', choices = ['sjlj', 'seh', 'dwarf'],
279 help = 'the method to throw exceptions')
280 parser.add_argument('--revision', type=int,
281 help = 'the revision of the MinGW release')
282 group = parser.add_mutually_exclusive_group()
283 group.add_argument('-v', '--verbose', action='store_true',
284 help='increase the script output verbosity')
285 group.add_argument('-q', '--quiet', action='store_true',
286 help='only print errors and warning')
287 args = parser.parse_args()
288
289 # Create the logger
290 logger = logging.getLogger('mingw')
291 handler = logging.StreamHandler()
292 formatter = logging.Formatter('%(message)s')
293 handler.setFormatter(formatter)
294 logger.addHandler(handler)
295 logger.setLevel(logging.INFO)
296 if args.quiet:
297 logger.setLevel(logging.WARN)
298 if args.verbose:
299 logger.setLevel(logging.DEBUG)
300
301 # Get MinGW
302 root_dir = root(location = args.location, arch = args.arch,
303 version = args.version, threading = args.threading,
304 exceptions = args.exceptions, revision = args.revision,
305 log = logger)
306
307 sys.stdout.write('%s\n' % os.path.join(root_dir, 'bin'))
308
309if __name__ == '__main__':
310 try:
311 main()
312 except IOError as e:
313 sys.stderr.write('IO error: %s\n' % e)
314 sys.exit(1)
315 except OSError as e:
316 sys.stderr.write('OS error: %s\n' % e)
317 sys.exit(1)
318 except KeyboardInterrupt as e:
319 sys.stderr.write('Killed\n')
320 sys.exit(1)