#! /usr/bin/env python # encoding: utf-8 import argparse import errno import logging import os import platform import re import sys import subprocess import tempfile try: import winreg except ImportError: import _winreg as winreg try: import urllib.request as request except ImportError: import urllib as request try: import urllib.parse as parse except ImportError: import urlparse as parse class EmptyLogger(object): ''' Provides an implementation that performs no logging ''' def debug(self, *k, **kw): pass def info(self, *k, **kw): pass def warn(self, *k, **kw): pass def error(self, *k, **kw): pass def critical(self, *k, **kw): pass def setLevel(self, *k, **kw): pass urls = ( 'http://downloads.sourceforge.net/project/mingw-w64/Toolchains%20' 'targetting%20Win32/Personal%20Builds/mingw-builds/installer/' 'repository.txt', 'http://downloads.sourceforge.net/project/mingwbuilds/host-windows/' 'repository.txt' ) ''' A list of mingw-build repositories ''' def repository(urls = urls, log = EmptyLogger()): ''' Downloads and parse mingw-build repository files and parses them ''' log.info('getting mingw-builds repository') versions = {} re_sourceforge = re.compile(r'http://sourceforge.net/projects/([^/]+)/files') re_sub = r'http://downloads.sourceforge.net/project/\1' for url in urls: log.debug(' - requesting: %s', url) socket = request.urlopen(url) repo = socket.read() if not isinstance(repo, str): repo = repo.decode(); socket.close() for entry in repo.split('\n')[:-1]: value = entry.split('|') version = tuple([int(n) for n in value[0].strip().split('.')]) version = versions.setdefault(version, {}) arch = value[1].strip() if arch == 'x32': arch = 'i686' elif arch == 'x64': arch = 'x86_64' arch = version.setdefault(arch, {}) threading = arch.setdefault(value[2].strip(), {}) exceptions = threading.setdefault(value[3].strip(), {}) revision = exceptions.setdefault(int(value[4].strip()[3:]), re_sourceforge.sub(re_sub, value[5].strip())) return versions def find_in_path(file, path=None): ''' Attempts to find an executable in the path ''' if platform.system() == 'Windows': file += '.exe' if path is None: path = os.environ.get('PATH', '') if type(path) is type(''): path = path.split(os.pathsep) return list(filter(os.path.exists, map(lambda dir, file=file: os.path.join(dir, file), path))) def find_7zip(log = EmptyLogger()): ''' Attempts to find 7zip for unpacking the mingw-build archives ''' log.info('finding 7zip') path = find_in_path('7z') if not path: key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\7-Zip') path, _ = winreg.QueryValueEx(key, 'Path') path = [os.path.join(path, '7z.exe')] log.debug('found \'%s\'', path[0]) return path[0] find_7zip() def unpack(archive, location, log = EmptyLogger()): ''' Unpacks a mingw-builds archive ''' sevenzip = find_7zip(log) log.info('unpacking %s', os.path.basename(archive)) cmd = [sevenzip, 'x', archive, '-o' + location, '-y'] log.debug(' - %r', cmd) with open(os.devnull, 'w') as devnull: subprocess.check_call(cmd, stdout = devnull) def download(url, location, log = EmptyLogger()): ''' Downloads and unpacks a mingw-builds archive ''' log.info('downloading MinGW') log.debug(' - url: %s', url) log.debug(' - location: %s', location) re_content = re.compile(r'attachment;[ \t]*filename=(")?([^"]*)(")?[\r\n]*') stream = request.urlopen(url) try: content = stream.getheader('Content-Disposition') or '' except AttributeError: content = stream.headers.getheader('Content-Disposition') or '' matches = re_content.match(content) if matches: filename = matches.group(2) else: parsed = parse.urlparse(stream.geturl()) filename = os.path.basename(parsed.path) try: os.makedirs(location) except OSError as e: if e.errno == errno.EEXIST and os.path.isdir(location): pass else: raise archive = os.path.join(location, filename) with open(archive, 'wb') as out: while True: buf = stream.read(1024) if not buf: break out.write(buf) unpack(archive, location, log = log) os.remove(archive) possible = os.path.join(location, 'mingw64') if not os.path.exists(possible): possible = os.path.join(location, 'mingw32') if not os.path.exists(possible): raise ValueError('Failed to find unpacked MinGW: ' + possible) return possible def root(location = None, arch = None, version = None, threading = None, exceptions = None, revision = None, log = EmptyLogger()): ''' Returns the root folder of a specific version of the mingw-builds variant of gcc. Will download the compiler if needed ''' # Get the repository if we don't have all the information if not (arch and version and threading and exceptions and revision): versions = repository(log = log) # Determine some defaults version = version or max(versions.keys()) if not arch: arch = platform.machine().lower() if arch == 'x86': arch = 'i686' elif arch == 'amd64': arch = 'x86_64' if not threading: keys = versions[version][arch].keys() if 'posix' in keys: threading = 'posix' elif 'win32' in keys: threading = 'win32' else: threading = keys[0] if not exceptions: keys = versions[version][arch][threading].keys() if 'seh' in keys: exceptions = 'seh' elif 'sjlj' in keys: exceptions = 'sjlj' else: exceptions = keys[0] if revision == None: revision = max(versions[version][arch][threading][exceptions].keys()) if not location: location = os.path.join(tempfile.gettempdir(), 'mingw-builds') # Get the download url url = versions[version][arch][threading][exceptions][revision] # Tell the user whatzzup log.info('finding MinGW %s', '.'.join(str(v) for v in version)) log.debug(' - arch: %s', arch) log.debug(' - threading: %s', threading) log.debug(' - exceptions: %s', exceptions) log.debug(' - revision: %s', revision) log.debug(' - url: %s', url) # Store each specific revision differently slug = '{version}-{arch}-{threading}-{exceptions}-rev{revision}' slug = slug.format( version = '.'.join(str(v) for v in version), arch = arch, threading = threading, exceptions = exceptions, revision = revision ) if arch == 'x86_64': root_dir = os.path.join(location, slug, 'mingw64') elif arch == 'i686': root_dir = os.path.join(location, slug, 'mingw32') else: raise ValueError('Unknown MinGW arch: ' + arch) # Download if needed if not os.path.exists(root_dir): downloaded = download(url, os.path.join(location, slug), log = log) if downloaded != root_dir: raise ValueError('The location of mingw did not match\n%s\n%s' % (downloaded, root_dir)) return root_dir def str2ver(string): ''' Converts a version string into a tuple ''' try: version = tuple(int(v) for v in string.split('.')) if len(version) is not 3: raise ValueError() except ValueError: raise argparse.ArgumentTypeError( 'please provide a three digit version string') return version def main(): ''' Invoked when the script is run directly by the python interpreter ''' parser = argparse.ArgumentParser( description = 'Downloads a specific version of MinGW', formatter_class = argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument('--location', help = 'the location to download the compiler to', default = os.path.join(tempfile.gettempdir(), 'mingw-builds')) parser.add_argument('--arch', required = True, choices = ['i686', 'x86_64'], help = 'the target MinGW architecture string') parser.add_argument('--version', type = str2ver, help = 'the version of GCC to download') parser.add_argument('--threading', choices = ['posix', 'win32'], help = 'the threading type of the compiler') parser.add_argument('--exceptions', choices = ['sjlj', 'seh', 'dwarf'], help = 'the method to throw exceptions') parser.add_argument('--revision', type=int, help = 'the revision of the MinGW release') group = parser.add_mutually_exclusive_group() group.add_argument('-v', '--verbose', action='store_true', help='increase the script output verbosity') group.add_argument('-q', '--quiet', action='store_true', help='only print errors and warning') args = parser.parse_args() # Create the logger logger = logging.getLogger('mingw') handler = logging.StreamHandler() formatter = logging.Formatter('%(message)s') handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.INFO) if args.quiet: logger.setLevel(logging.WARN) if args.verbose: logger.setLevel(logging.DEBUG) # Get MinGW root_dir = root(location = args.location, arch = args.arch, version = args.version, threading = args.threading, exceptions = args.exceptions, revision = args.revision, log = logger) sys.stdout.write('%s\n' % os.path.join(root_dir, 'bin')) if __name__ == '__main__': try: main() except IOError as e: sys.stderr.write('IO error: %s\n' % e) sys.exit(1) except OSError as e: sys.stderr.write('OS error: %s\n' % e) sys.exit(1) except KeyboardInterrupt as e: sys.stderr.write('Killed\n') sys.exit(1)