#! /usr/bin/env python
#-----------------------------------------------------------------------------
# Copyright (c) 2013, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License with exception
# for distributing bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#-----------------------------------------------------------------------------


"""
Bootloader building script.
"""


import os
import platform
import sys

import Utils
import Options


# the following two variables are used by the target "waf dist"
VERSION=''
APPNAME=''


# these variables are mandatory ('/' are converted automatically)
top = '.'
out = 'build'


# TODO use following variable only on darwin and for 32bit loader.
# OS X 10.5 doesn't understand the load command  'LC_DYLD_INFO_ONLY'
# that is used by the OS X 10.6 linker.
# The following variable fixes 10.5 compatibility.
os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.5'


# TODO strip created binaries.


def set_options(opt):
    myplatform = Utils.detect_platform()
    
    if not myplatform.startswith('win'):
        opt.add_option('--leak-detector',
                action = 'store_true',
                help = 'Link with Boehm garbage collector to detect memory leaks.',
                default = False,
                dest = 'boehmgc')

    if myplatform.startswith('linux'):
        opt.add_option('--no-lsb',
                action = 'store_true',
                help = 'Prevent building LSB (Linux Standard Base) bootloader.',
                default = False,
                dest = 'nolsb')
        opt.add_option('--lsbcc-path',
                action = 'store',
                help = 'Path to lsbcc. By default PATH is searched for lsbcc otherwise is tried file /opt/lsb/bin/lsbcc. [Default: lsbcc]',
                default = 'lsbcc',
                dest = 'lsbcc_path')
        opt.add_option('--lsb-target-version',
                action = 'store',
                help = 'Specify LSB target version [Default: 4.0]',
                default = '4.0',
                dest = 'lsb_version')

    opt.tool_options('compiler_cc')


def configure(conf):

    def achitecture():
        return '32bit'

    conf.env.NAME = 'default'

    opt = Options.options
    myplatform = Utils.detect_platform()

    # on 64bit Mac function platform.architecture() returns 64bit even
    # for 32bit Python. This is the workaround for this.
    if myplatform == 'darwin' and sys.maxint <= 3**32:
        myarch = '32bit'
    else:
        myarch = platform.architecture()[0] # 32bit or 64bit
    conf.env.MYARCH = myarch

    # Differenciate path to bootloader with machine name if necessary.
    mymach = platform.machine()
    if mymach.startswith('arm'):
        mymach = 'arm'
    else:
        # Assume x86/x86_64 machine.
        mymach = None
    conf.env.MYMACHINE = mymach


    Utils.pprint('CYAN', '%s-%s detected' % (platform.system(), myarch))

    if myplatform == 'darwin' and myarch == '64bit':
        Utils.pprint('CYAN', 'WARNING: Building bootloader for Python 64-bit on Mac OSX')
        Utils.pprint('CYAN', 'For 32b-bit bootloader prepend the python command with:')
        Utils.pprint('CYAN', 'VERSIONER_PYTHON_PREFER_32_BIT=yes arch -i386 python')

    if myplatform.startswith('linux') and not opt.nolsb:
        Utils.pprint('CYAN', 'Building LSB bootloader.')

    ### C compiler

    if myplatform.startswith('win'):
        try:
            # mingw (gcc for Windows)
            conf.check_tool('gcc')
        except:
            try:
                # Newest detected MSVC version is used by default.
                # msvc 7.1 (Visual Studio 2003)
                # msvc 8.0 (Visual Studio 2005)
                # msvc 9.0 (Visual Studio 2008)
                conf.env['MSVC_VERSIONS'] = ['msvc 7.1', 'msvc 8.0', 'msvc 9.0', 'wsdk 7.0', 'wsdk 7.1']
                if myarch == '32bit':
                    conf.env['MSVC_TARGETS'] = ['x86', 'ia32']
                elif myarch == '64bit':
                    conf.env['MSVC_TARGETS'] = ['x64', 'x86_amd64', 'intel64', 'em64t']
                conf.check_tool('msvc')
                conf.print_all_msvc_detected()
                # Do not embed manifest file.
                # Manifest file will be added in the phase of packaging python application.
                conf.env.MSVC_MANIFEST = False
            except Exception, e:
                print e
                Utils.pprint('RED', str(e))
                Utils.pprint('RED', 'GCC (MinGW) or MSVC compiler not found.')
                exit(1)
        else:
            # When using GCC, deactivate declarations after statements
            # (not supported by Visual Studio)
            conf.env.append_value('CCFLAGS', '-Wdeclaration-after-statement')
            conf.env.append_value('CCFLAGS', '-Werror')

    else:
        conf.check_tool('compiler_cc')


    ### build LSB (Linux Standard Base) boot loader
    if myplatform.startswith('linux') and not opt.nolsb:
        try:
            # custom lsbcc path
            if opt.lsbcc_path != 'lsbcc':
                conf.env.LSBCC = conf.find_program(opt.lsbcc_path, mandatory=True)
            # default values
            else:
                conf.env.LSBCC = conf.find_program(opt.lsbcc_path)
                if not conf.env.LSBCC:
                    conf.env.LSBCC = conf.find_program('/opt/lsb/bin/lsbcc',
                            mandatory=True)
        except:
            Utils.pprint('RED', 'LSB (Linux Standard Base) tools >= 4.0 are required.')
            Utils.pprint('RED', 'Try --no-lsb option if not interested in building LSB binary.')
            exit(2)
        # lsbcc as CC compiler
        conf.env.append_value('CCFLAGS', '--lsb-cc=%s' % conf.env.CC[0])
        conf.env.append_value('LINKFLAGS', '--lsb-cc=%s' % conf.env.CC[0])
        conf.env.CC = conf.env.LSBCC
        conf.env.LINK_CC = conf.env.LSBCC
        ## check LSBCC flags
        # --lsb-besteffort - binary will work on platforms without LSB stuff
        # --lsb-besteffort - available in LSB build tools >= 4.0
        conf.check_cc(ccflags='--lsb-besteffort',
                msg='Checking for LSB build tools >= 4.0',
                errmsg='LSB >= 4.0 is required', mandatory=True)
        conf.env.append_value('CCFLAGS', '--lsb-besteffort')
        conf.env.append_value('LINKFLAGS', '--lsb-besteffort')
        # binary compatibility with a specific LSB version
        # LSB 4.0 can generate binaries compatible with 3.0, 3.1, 3.2, 4.0
        # however because of using function 'mkdtemp', loader requires
        # using target version 4.0
        lsb_target_flag = '--lsb-target-version=%s' % opt.lsb_version
        conf.env.append_value('CCFLAGS', lsb_target_flag)
        conf.env.append_value('LINKFLAGS', lsb_target_flag)


    ### Defines, Includes

    if myplatform.startswith('lin'):
        # make sure we don't use declarations after statement (break Visual Studio)
        conf.env.append_value('CCFLAGS', '-Wdeclaration-after-statement')
        conf.env.append_value('CCFLAGS', '-Werror')

    if not myplatform.startswith('win'):
        # Defines common for Unix and Unix-like platforms.
        # For details see:
        #   http://man.he.net/man7/feature_test_macros
        #
        ## Without these definitions compiling maight fail on OSX.
        conf.env.append_value('CCDEFINES', '_POSIX_C_SOURCE=200112L')
        # SUS v2 (UNIX 98) definitions.
        #   Mac OS X 10.5 is UNIX 03 compliant.
        conf.env.append_value('CCDEFINES', '_XOPEN_SOURCE=500')
        conf.env.append_value('CCDEFINES', '_REENTRANT')
        # Function 'mkdtemp' is available only if _BSD_SOURCE is defined.
        conf.env.append_value('CCDEFINES', '_BSD_SOURCE')
        

    if myplatform.startswith('win'):
        conf.env.append_value('CCDEFINES', 'WIN32')
        conf.env.append_value('CPPPATH', '../zlib')

    if myplatform.startswith('sun'):
        conf.env.append_value('CCDEFINES', 'SUNOS')

    if myplatform.startswith('aix'):
        conf.env.append_value('CCDEFINES', 'AIX')

    conf.env.append_value('CPPPATH', os.path.join('..', 'common'))


    ### Libraries

    if myplatform.startswith('win'):
        conf.check_cc(lib='user32', mandatory=True)
        conf.check_cc(lib='comctl32', mandatory=True)
        conf.check_cc(lib='kernel32', mandatory=True)
        conf.check_cc(lib='ws2_32', mandatory=True)

    else:
        conf.check_cc(lib='dl', mandatory=True)
        conf.check_cc(lib='z', mandatory=True)
        if conf.check_cc(function_name='readlink', header_name='unistd.h'):
            conf.env.append_value('CCFLAGS', '-DHAVE_READLINK')

        # This uses Boehm GC to manage memory - it replaces malloc() / free()
        # functions. Some messages are printed if memory is not deallocated.
        if opt.boehmgc:
            conf.check_cc(lib='gc', mandatory=True)
            conf.env.append_value('CCDEFINES', 'PYI_LEAK_DETECTOR')
            conf.env.append_value('CCDEFINES', 'GC_FIND_LEAK')
            conf.env.append_value('CCDEFINES', 'GC_DEBUG')
            conf.env.append_value('CCDEFINES', 'SAVE_CALL_CHAIN')

    ### ccflags

    if myplatform.startswith('win') and conf.env.CC_NAME == 'gcc':
        # Disables console - MinGW option
        conf.check_cc(ccflags='-mwindows', mandatory=True,
                msg='Checking for flags -mwindows')
        # Use Visual C++ compatible alignment
        conf.check_cc(ccflags='-mms-bitfields', mandatory=True,
                msg='Checking for flags -mms-bitfields')
        conf.env.append_value('CCFLAGS', '-mms-bitfields')

    elif myplatform.startswith('win') and conf.env.CC_NAME == 'msvc':
        if myarch == '32bit':
            conf.env.append_value('LINKFLAGS', '/MACHINE:X86')
        elif myarch == '64bit':
            conf.env.append_value('LINKFLAGS', '/MACHINE:X64')
        # Enable 64bit porting warnings and other warnings too.
        conf.env.append_value('CCFLAGS', '/W3')
        # We use SEH exceptions in winmain.c; make sure they are activated.
        conf.env.append_value('CCFLAGS', '/EHa')
    
    # Compile with 64bit gcc 32bit binaries or vice versa.
    if conf.env.CC_NAME == 'gcc':
        if myarch == '32bit' and conf.check_cc(ccflags='-m32', msg='Checking for flags -m32'):
            conf.env.append_value('CCFLAGS', '-m32')
        elif myarch == '64bit':
            conf.env.append_value('CCFLAGS', '-m64')

    # Ensure proper architecture flags on Mac OS X.
    # TODO Add support for universal binaries.
    if myplatform.startswith('darwin'):

        available_archs = {'32bit': 'i386', '64bit': 'x86_64'}
        mac_arch = available_archs[myarch]

        conf.env.append_value('CCFLAGS', '-arch')
        conf.env.append_value('CCFLAGS', mac_arch)
        conf.env.append_value('CXXFLAGS', '-arch')
        conf.env.append_value('CXXFLAGS', mac_arch)
        conf.env.append_value('LINKFLAGS', '-arch')
        conf.env.append_value('LINKFLAGS', mac_arch)

        if myarch == '32bit':
            conf.env.append_value('CCFLAGS', '-mmacosx-version-min=10.5')
        else:
            conf.env.append_value('CCFLAGS', '-mmacosx-version-min=10.6')

        

       
    # On linux link only with needed libraries.
    # -Wl,--as-needed is on some platforms detected during configure but
    # fails during build. (Mac OS X, Solaris, AIX)
    if myplatform.startswith('linux') and conf.check_cc(ccflags='-Wl,--as-needed',
            msg='Checking for flags -Wl,--as-needed'):
        conf.env.append_value('LINKFLAGS', '-Wl,--as-needed')


    ### Other stuff

    if myplatform.startswith('win'):
        # RC file - icon
        conf.check_tool('winres')

    ### DEBUG and RELEASE environments

    rel = conf.env.copy()
    dbg = conf.env.copy()
    rel.set_variant('release') # separate subfolder for building
    dbg.set_variant('debug') # separate subfolder for building
    rel.detach() # detach environment from default
    dbg.detach()

    ## setup DEBUG environment
    dbg.set_variant('debug') # separate subfolder for building
    conf.set_env_name('debug', dbg)
    conf.setenv('debug')
    # This define enables verbose console output of the bootloader.
    conf.env.append_value('CCDEFINES', ['LAUNCH_DEBUG'])
    conf.env.append_value('CCDEFINES', 'NDEBUG')
    dbgw = conf.env.copy() # WINDOWED DEBUG environment
    dbgw.set_variant('debugw') # separate subfolder for building
    dbgw.detach()

    ## setup windowed DEBUG environment
    conf.set_env_name('debugw', dbgw)
    conf.setenv('debugw')
    conf.env.append_value('CCDEFINES', 'WINDOWED')
    # disables console - mingw option
    if myplatform.startswith('win') and conf.env.CC_NAME == 'gcc':
            conf.env.append_value('LINKFLAGS', '-mwindows')
    elif myplatform.startswith('darwin'):
        #conf.env.append_value('CCFLAGS', '-I/Developer/Headers/FlatCarbon')
        # To support catching AppleEvents and running as ordinary OSX GUI app,
        # we have to link against the Carbon framework.
        # This linkage only needs to be there for the windowed bootloaders.
        conf.env.append_value('LINKFLAGS', '-framework')
        conf.env.append_value('LINKFLAGS', 'Carbon')
        # conf.env.append_value('LINKFLAGS', '-framework')
        # conf.env.append_value('LINKFLAGS', 'ApplicationServices')


    ## setup RELEASE environment
    conf.set_env_name('release', rel)
    conf.setenv('release')
    conf.env.append_value('CCDEFINES', 'NDEBUG')
    conf.env.append_value('CCFLAGS', conf.env.CCFLAGS_RELEASE)
    relw = conf.env.copy() # WINDOWED RELEASE environment
    relw.set_variant('releasew') # separate subfolder for building
    relw.detach()

    ## setup windowed RELEASE environment
    conf.set_env_name('releasew', relw)
    conf.setenv('releasew')
    conf.env.append_value('CCDEFINES', 'WINDOWED')
    # disables console
    if myplatform.startswith('win') and conf.env.CC_NAME == 'gcc':
            conf.env.append_value('LINKFLAGS', '-mwindows')
    elif myplatform.startswith('darwin'):
        # To support catching AppleEvents and running as ordinary OSX GUI app,
        # we have to link against the Carbon framework.
        # This linkage only needs to be there for the windowed bootloaders.
        conf.env.append_value('LINKFLAGS', '-framework')
        conf.env.append_value('LINKFLAGS', 'Carbon')
        # TODO Do we need to link with this framework?
        # conf.env.append_value('LINKFLAGS', '-framework')
        # conf.env.append_value('LINKFLAGS', 'ApplicationServices')


# TODO Use 'strip' command to decrease the size of compiled bootloaders.
def build(bld):
    myplatform = Utils.detect_platform()
    opt = Options.options

    # Force to run with 1 job (no parallel building).
    # There are reported build failures on multicore CPUs
    # with some msvc versions.
    # TODO revisit parallel building with switch to waf 1.7
    opt.jobs = 1

    install_path = '../../PyInstaller/bootloader/' + platform.system() + "-" + bld.env.MYARCH
    if bld.env.MYMACHINE:
        install_path += '-' + bld.env.MYMACHINE
    targets = dict(release='run', debug='run_d', releasew='runw', debugw='runw_d')

    if myplatform.startswith('win'):

        # static lib zlib

        for key in targets.keys():
            bld(
                features = 'cc cstaticlib',
                source = bld.path.ant_glob('zlib/*.c'),
                target = 'staticlib_zlib',
                env = bld.env_of_name(key),
                includes = ['zlib'],
            )

        # console

        for key in ('release', 'debug'):
            bld(
                features = 'cc cprogram',
                source = bld.path.ant_glob('windows/utils.c windows/run.rc common/*.c'),
                target = targets[key],
                install_path = install_path,
                uselib_local = 'staticlib_zlib',
                uselib = 'USER32 COMCTL32 KERNEL32 WS2_32',
                env = bld.env_of_name(key).copy(),
                includes = ['common', 'windows', 'zlib'],
            )

        # windowed

        for key in ('releasew', 'debugw'):
            bld(
                features = 'cc cprogram',
                source = bld.path.ant_glob('windows/utils.c windows/runw.rc common/*.c'), # uses different RC file (icon)
                target = targets[key],
                install_path = install_path,
                uselib_local = 'staticlib_zlib',
                uselib = 'USER32 COMCTL32 KERNEL32 WS2_32',
                env = bld.env_of_name(key).copy(),
                includes = ['common', 'windows', 'zlib'],
            )

        ## inprocsrvr console

        if bld.env.CC_NAME == 'msvc':
            linkflags_c = bld.env.CPPFLAGS_CONSOLE
            linkflags_w = bld.env.CPPFLAGS_WINDOWS
        else:
            linkflags_c = ''
            linkflags_w = ''

        for key, value in dict(release='inprocsrvr', debug='inprocsrvr_d').items():
            bld(
                features = 'cc cshlib',
                source = bld.path.ant_glob('common/pyi_*.c windows/*.c'),
                target = value,
                install_path = install_path,
                uselib_local = 'staticlib_zlib',
                uselib = 'USER32 COMCTL32 KERNEL32 WS2_32',
                env = bld.env_of_name(key).copy(),
                linkflags = linkflags_c,
                includes = ['common', 'windows', 'zlib'],
            )

        ## inprocsrvr windowed

        for key, value in dict(releasew='inprocsrvrw', debugw='inprocsrvrw_d').items():
            bld(
                features = 'cc cshlib',
                source = bld.path.ant_glob('common/pyi_*.c windows/*.c'),
                target = value,
                install_path = install_path,
                uselib_local = 'staticlib_zlib',
                uselib = 'USER32 COMCTL32 KERNEL32 WS2_32',
                env = bld.env_of_name(key).copy(),
                linkflags = linkflags_w,
                includes = ['common', 'windows', 'zlib'],
            )

    else: # linux, darwin (MacOSX)

        libs = ['dl', 'z', 'm']  # 'z' - zlib, 'm' - math,
        if opt.boehmgc:
            libs.append('gc')

        for key, value in targets.items():
            bld(
                features = 'cc cprogram',
                source = bld.path.ant_glob('linux/*.c common/*.c'),
                target = value,
                install_path = install_path,
                lib = libs,
                env = bld.env_of_name(key).copy(),
                includes = ['common', 'linux'],
            )
