mingw.py 10.1 KB
#!/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)