1#! /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 is 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) 321