#!/usr/bin/env python

## @file
#
# Automation of instructions from:
#   http://mingw-w64.svn.sourceforge.net/viewvc/mingw-w64/trunk/mingw-w64-doc/
#     howto-build/mingw-w64-howto-build.txt?revision=216&view=markup
#
# Copyright (c) 2008 - 2010, Intel Corporation. All rights reserved.<BR>
# This program and the accompanying materials
# are licensed and made available under the terms and conditions of the BSD License
# which accompanies this distribution.    The full text of the license may be found at
# http://opensource.org/licenses/bsd-license.php
#
# THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
# WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
#


from optparse import OptionParser
import os
import shutil
import subprocess
import sys
import tarfile
import urllib
import urlparse
try:
    from hashlib import md5
except Exception:
    from md5 import md5

if sys.version_info < (2, 5):
    #
    # This script (and edk2 BaseTools) require Python 2.5 or newer
    #
    print 'Python version 2.5 or later is required.'
    sys.exit(-1)

#
# Version and Copyright
#
VersionNumber = "0.01"
__version__ = "%prog Version " + VersionNumber
__copyright__ = "Copyright (c) 2008 - 2010, Intel Corporation.  All rights reserved."

class Config:
    """class Config

    Stores the configuration options for the rest of the script.

    Handles the command line options, and allows the code within
    the script to easily interact with the 'config' requested by
    the user.
    """

    def __init__(self):
        self.base_dir = os.getcwd()
        (self.options, self.args) = self.CheckOptions()
        self.__init_dirs__()

    def CheckOptions(self):
        Parser = \
            OptionParser(
                description=__copyright__,
                version=__version__,
                prog="mingw-gcc-build",
                usage="%prog [options] [target]"
                )
        Parser.add_option(
            "--arch",
            action = "store", type = "string",
            default = '',
            dest = "arch",
            help = "Processor architecture to build gcc for."
            )
        Parser.add_option(
            "--src-dir",
            action = "store", type = "string", dest = "src_dir",
            default = os.path.join(self.base_dir, 'src'),
            help = "Directory to download/extract binutils/gcc sources"
            )
        Parser.add_option(
            "--build-dir",
            action = "store", type = "string", dest = "build_dir",
            default = os.path.join(self.base_dir, 'build'),
            help = "Directory to download/extract binutils/gcc sources"
            )
        Parser.add_option(
            "--prefix",
            action = "store", type = "string", dest = "prefix",
            default = os.path.join(self.base_dir, 'install'),
            help = "Prefix to install binutils/gcc into"
            )
        Parser.add_option(
            "--skip-binutils",
            action = "store_true", dest = "skip_binutils",
            default = False,
            help = "Will skip building binutils"
            )
        Parser.add_option(
            "--skip-gcc",
            action = "store_true", dest = "skip_gcc",
            default = False,
            help = "Will skip building GCC"
            )
        Parser.add_option(
            "--symlinks",
            action = "store", type = "string", dest = "symlinks",
            default = os.path.join(self.base_dir, 'symlinks'),
            help = "Directory to create binutils/gcc symbolic links into."
            )
        Parser.add_option(
            "-v", "--verbose",
            action="store_true",
            type=None, help="Print verbose messages"
            )

        (Opt, Args) = Parser.parse_args()

        self.arch = Opt.arch.lower()
        allowedArchs = ('ia32', 'x64', 'ipf')
        if self.arch not in allowedArchs:
            Parser.error(
                'Please use --arch to specify one of: %s' %
                    ', '.join(allowedArchs)
                )
        self.target_arch = {'ia32': 'i686', 'x64': 'x86_64', 'ipf': 'ia64'}[self.arch]
        self.target_sys = {'ia32': 'pc', 'x64': 'pc', 'ipf': 'pc'}[self.arch]
        self.target_bin = {'ia32': 'mingw32', 'x64': 'mingw32', 'ipf': 'elf'}[self.arch]
        self.target_combo = '-'.join((self.target_arch, self.target_sys, self.target_bin))

        return (Opt, Args)

    def __init_dirs__(self):
        self.src_dir = os.path.realpath(os.path.expanduser(self.options.src_dir))
        self.build_dir = os.path.realpath(os.path.expanduser(self.options.build_dir))
        self.prefix = os.path.realpath(os.path.expanduser(self.options.prefix))
        self.symlinks = os.path.realpath(os.path.expanduser(self.options.symlinks))

    def IsConfigOk(self):

        building = []
        if not self.options.skip_binutils:
            building.append('binutils')
        if not self.options.skip_gcc:
            building.append('gcc')
        if len(building) == 0:
            print "Nothing will be built!"
            print
            print "Please try using --help and then change the configuration."
            return False

        print "Current directory:"
        print "   ", self.base_dir
        print "Sources download/extraction:", self.Relative(self.src_dir)
        print "Build directory            :", self.Relative(self.build_dir)
        print "Prefix (install) directory :", self.Relative(self.prefix)
        print "Create symlinks directory  :", self.Relative(self.symlinks)
        print "Building                   :", ', '.join(building)
        print
        answer = raw_input("Is this configuration ok? (default = no): ")
        if (answer.lower() not in ('y', 'yes')):
            print
            print "Please try using --help and then change the configuration."
            return False

        if self.arch.lower() == 'ipf':
            print
            print 'Please note that the IPF compiler built by this script has'
            print 'not yet been validated!'
            print
            answer = raw_input("Are you sure you want to build it? (default = no): ")
            if (answer.lower() not in ('y', 'yes')):
                print
                print "Please try using --help and then change the configuration."
                return False

        print
        return True

    def Relative(self, path):
        if path.startswith(self.base_dir):
            return '.' + path[len(self.base_dir):]
        return path

    def MakeDirs(self):
        for path in (self.src_dir, self.build_dir,self.prefix, self.symlinks):
            if not os.path.exists(path):
                os.makedirs(path)

class SourceFiles:
    """class SourceFiles

    Handles the downloading of source files used by the script.
    """

    def __init__(self, config):
        self.config = config
        self.source_files = self.source_files[config.arch]

        if config.options.skip_binutils:
            del self.source_files['binutils']

        if config.options.skip_gcc:
            del self.source_files['gcc']
            del self.source_files['mingw_hdr']

    source_files_common = {
        'binutils': {
            'url': 'http://www.kernel.org/pub/linux/devel/binutils/' + \
                   'binutils-$version.tar.bz2',
            'version': '2.20.51.0.5',
            'md5': '6d2de7cdf7a8389e70b124e3d73b4d37',
            },
        }

    source_files_x64 = {
        'gcc': {
            'url': 'http://ftpmirror.gnu.org/gcc/' + \
                   'gcc-$version/gcc-$version.tar.bz2',
            'version': '4.3.0',
            'md5': '197ed8468b38db1d3481c3111691d85b',
            },
        }

    source_files_ia32 = {
        'gcc': source_files_x64['gcc'],
        }

    source_files_ipf = source_files_x64.copy()
    source_files_ipf['gcc']['configure-params'] = (
        '--with-gnu-as', '--with-gnu-ld', '--with-newlib',
        '--verbose', '--disable-libssp', '--disable-nls',
        '--enable-languages=c,c++'
        )

    source_files = {
        'ia32': [source_files_common, source_files_ia32],
        'x64': [source_files_common, source_files_x64],
        'ipf': [source_files_common, source_files_ipf],
        }

    for arch in source_files:
        mergedSourceFiles = {}
        for source_files_dict in source_files[arch]:
            mergedSourceFiles.update(source_files_dict)
        for downloadItem in mergedSourceFiles:
            fdata = mergedSourceFiles[downloadItem]
            fdata['filename'] = fdata['url'].split('/')[-1]
            if 'extract-dir' not in fdata:
                for ext in ('.tar.gz', '.tar.bz2', '.zip'):
                    if fdata['filename'].endswith(ext):
                        fdata['extract-dir'] = fdata['filename'][:-len(ext)]
                        break
            replaceables = ('extract-dir', 'filename', 'url')
            for replaceItem in fdata:
                if replaceItem in replaceables: continue
                if type(fdata[replaceItem]) != str: continue
                for replaceable in replaceables:
                    if type(fdata[replaceable]) != str: continue
                    if replaceable in fdata:
                        fdata[replaceable] = \
                            fdata[replaceable].replace(
                                '$' + replaceItem,
                                fdata[replaceItem]
                                )
        source_files[arch] = mergedSourceFiles
    #print 'source_files:', source_files

    def GetAll(self):

        def progress(received, blockSize, fileSize):
            if fileSize < 0: return
            wDots = (100 * received * blockSize) / fileSize / 10
            if wDots > self.dots:
                for i in range(wDots - self.dots):
                    print '.',
                    sys.stdout.flush()
                    self.dots += 1

        maxRetries = 1
        for (fname, fdata) in self.source_files.items():
            for retries in range(maxRetries):
                try:
                    self.dots = 0
                    local_file = os.path.join(self.config.src_dir, fdata['filename'])
                    url = fdata['url']
                    print 'Downloading %s:' % fname, url
                    if retries > 0:
                        print '(retry)',
                    sys.stdout.flush()

                    completed = False
                    if os.path.exists(local_file):
                        md5_pass = self.checkHash(fdata)
                        if md5_pass:
                            print '[md5 match]',
                        else:
                            print '[md5 mismatch]',
                        sys.stdout.flush()
                        completed = md5_pass

                    if not completed:
                        urllib.urlretrieve(url, local_file, progress)

                    #
                    # BUGBUG: Suggest proxy to user if download fails.
                    #
                    # export http_proxy=http://proxyservername.mycompany.com:911
                    # export ftp_proxy=http://proxyservername.mycompany.com:911

                    if not completed and os.path.exists(local_file):
                        md5_pass = self.checkHash(fdata)
                        if md5_pass:
                            print '[md5 match]',
                        else:
                            print '[md5 mismatch]',
                        sys.stdout.flush()
                        completed = md5_pass

                    if completed:
                        print '[done]'
                        break
                    else:
                        print '[failed]'
                        print '  Tried to retrieve', url
                        print '  to', local_file
                        print 'Possible fixes:'
                        print '* If you are behind a web-proxy, try setting the',
                        print 'http_proxy environment variable'
                        print '* You can try to download this file separately',
                        print 'and rerun this script'
                        raise Exception()
                
                except KeyboardInterrupt:
                    print '[KeyboardInterrupt]'
                    return False

                except Exception, e:
                    print e

            if not completed: return False

        return True

    def checkHash(self, fdata):
        local_file = os.path.join(self.config.src_dir, fdata['filename'])
        expect_md5 = fdata['md5']
        data = open(local_file).read()
        md5sum = md5()
        md5sum.update(data)
        return md5sum.hexdigest().lower() == expect_md5.lower()

    def GetModules(self):
        return self.source_files.keys()

    def GetFilenameOf(self, module):
        return self.source_files[module]['filename']

    def GetMd5Of(self, module):
        return self.source_files[module]['md5']

    def GetExtractDirOf(self, module):
        return self.source_files[module]['extract-dir']

    def GetAdditionalParameters(self, module, step):
        key = step + '-params'
        if key in self.source_files[module]:
            return self.source_files[module][key]
        else:
            return tuple()

class Extracter:
    """class Extracter

    Handles the extraction of the source files from their downloaded
    archive files.
    """

    def __init__(self, source_files, config):
        self.source_files = source_files
        self.config = config

    def Extract(self, module):
        src = self.config.src_dir
        extractDst = os.path.join(src, self.config.arch)
        local_file = os.path.join(src, self.source_files.GetFilenameOf(module))
        moduleMd5 = self.source_files.GetMd5Of(module)
        extracted = os.path.join(extractDst, os.path.split(local_file)[1] + '.extracted')
        if not os.path.exists(extractDst):
            os.mkdir(extractDst)

        extractedMd5 = None
        if os.path.exists(extracted):
            extractedMd5 = open(extracted).read()

        if extractedMd5 != moduleMd5:
            print 'Extracting %s:' % self.config.Relative(local_file)
            tar = tarfile.open(local_file)
            tar.extractall(extractDst)
            open(extracted, 'w').write(moduleMd5)
        else:
            pass
            #print 'Previously extracted', self.config.Relative(local_file)

    def ExtractAll(self):
        for module in self.source_files.GetModules():
            self.Extract(module)

class Builder:
    """class Builder

    Builds and installs the GCC tool suite.
    """

    def __init__(self, source_files, config):
        self.source_files = source_files
        self.config = config

    def Build(self):
        if not self.config.options.skip_binutils:
            self.BuildModule('binutils')
        if not self.config.options.skip_gcc:
            self.BuildModule('gcc')
            self.MakeSymLinks()

    def IsBuildStepComplete(self, step):
        return \
            os.path.exists(
                os.path.join(
                    self.config.build_dir, self.config.arch, step + '.completed'
                    )
                )

    def MarkBuildStepComplete(self, step):
        open(
            os.path.join(
                self.config.build_dir, self.config.arch, step + '.completed'
                ),
            "w"
            ).close()


    def BuildModule(self, module):
        base_dir = os.getcwd()
        build_dir = os.path.join(self.config.build_dir, self.config.arch, module)
        module_dir = self.source_files.GetExtractDirOf(module)
        module_dir = os.path.realpath(os.path.join('src', self.config.arch, module_dir))
        configure = os.path.join(module_dir, 'configure')
        prefix = self.config.prefix
        if not os.path.exists(build_dir):
            os.makedirs(build_dir)
        os.chdir(build_dir)

        cmd = (
            configure,
            '--target=%s' % self.config.target_combo,
            '--prefix=' + prefix,
            '--with-sysroot=' + prefix,
            '--disable-werror',
            )
        if os.path.exists('/opt/local/include/gmp.h'):
            cmd += ('--with-gmp=/opt/local',)
        if module == 'gcc': cmd += ('--oldincludedir=/opt/local/include',)
        cmd += self.source_files.GetAdditionalParameters(module, 'configure')
        self.RunCommand(cmd, module, 'config', skipable=True)

        cmd = ('make',)
        if module == 'gcc':
            cmd += ('all-gcc',)
        self.RunCommand(cmd, module, 'build')

        cmd = ('make',)
        if module == 'gcc':
            cmd += ('install-gcc',)
        else:
            cmd += ('install',)
        self.RunCommand(cmd, module, 'install')

        os.chdir(base_dir)

        print '%s module is now built and installed' % module

    def RunCommand(self, cmd, module, stage, skipable=False):
        if skipable:
            if self.IsBuildStepComplete('%s.%s' % (module, stage)):
                return

        popen = lambda cmd: \
            subprocess.Popen(
                cmd,
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT
                )

        print '%s [%s] ...' % (module, stage),
        sys.stdout.flush()
        p = popen(cmd)
        output = p.stdout.read()
        p.wait()
        if p.returncode != 0:
            print '[failed!]'
            logFile = os.path.join(self.config.build_dir, 'log.txt')
            f = open(logFile, "w")
            f.write(output)
            f.close()
            raise Exception, 'Failed to %s %s\n' % (stage, module) + \
                'See output log at %s' % self.config.Relative(logFile)
        else:
            print '[done]'

        if skipable:
            self.MarkBuildStepComplete('%s.%s' % (module, stage))

    def MakeSymLinks(self):
        links_dir = os.path.join(self.config.symlinks, self.config.arch)
        if not os.path.exists(links_dir):
            os.makedirs(links_dir)
        startPrinted = False
        for link in ('ar', 'ld', 'gcc'):
            src = os.path.join(
                self.config.prefix, 'bin', self.config.target_combo + '-' + link
                )
            linkdst = os.path.join(links_dir, link)
            if not os.path.lexists(linkdst):
                if not startPrinted:
                    print 'Making symlinks in %s:' % self.config.Relative(links_dir),
                    startPrinted = True
                print link,
                os.symlink(src, linkdst)

        if startPrinted:
            print '[done]'

class App:
    """class App

    The main body of the application.
    """

    def __init__(self):
        config = Config()

        if not config.IsConfigOk():
            return

        config.MakeDirs()

        sources = SourceFiles(config)
        result = sources.GetAll()
        if result:
            print 'All files have been downloaded & verified'
        else:
            print 'An error occured while downloading a file'
            return

        Extracter(sources, config).ExtractAll()

        Builder(sources, config).Build()

App()