# Python module containing general build functions
# for OpenVPN on Windows

import os, re, shutil, stat

autogen = "Automatically generated by OpenVPN Windows build system"

def get_config():
    kv = {}
    parse_version_m4(kv, home_fn('version.m4'))
    parse_settings_in(kv, mod_fn('settings.in'))

    # config fixups
    kv['DDKVER'] = os.path.basename(kv['DDK_PATH'])
    kv['DDKVER_MAJOR'] = re.match(r'^(\d+)\.', kv['DDKVER']).groups()[0]

    if 'VERSION_SUFFIX' in kv:
        kv['PRODUCT_VERSION'] += kv['VERSION_SUFFIX']

    return kv

def get_build_params():
    kv = {}
    parse_build_params(kv,mod_fn('settings.in'))

    return kv

def mod_fn(fn, src=__file__, real=True):
    p = os.path.join(os.path.dirname(src), os.path.normpath(fn))
    if real:
        p = os.path.realpath(p)
    return p

def home_fn(fn, real=True):
    return mod_fn(os.path.join('..', fn), real=real)

def cd_home():
    os.chdir(os.path.join(os.path.dirname(__file__), '..'))

def cd_service_win32():
    os.chdir(os.path.join(os.path.dirname(__file__), '../service-win32'))

def system(cmd):
    print "RUN:", cmd
    os.system(cmd)

def run_in_vs_shell(cmd):
    """Make sure environment variables are setup before running command"""
    os.environ['PATH'] += ";%s\\VC" % (os.path.normpath(config['MSVC']),)
    system('cmd /c "vcvarsall.bat x86 && %s"' % (cmd,))

def parse_version_m4(kv, version_m4):
    '''Parse define lines in version.m4'''
    r = re.compile(r'^define\((\w+),\[(.*)\]\)$')
    f = open(version_m4)
    for line in f:
        line = line.rstrip()
        m = re.match(r, line)

        if m:
            g = m.groups()

            # If we encounter PRODUCT_TAP_WIN32_MIN_MAJOR or
            # PRODUCT_TAP_WIN32_MIN_MAJOR then we need to generate extra
            # variables, PRODUCT_TAP_MAJOR_VER and PRODUCT_TAP_MINOR_VER with 
            # the same contents. This is necessary because tap-win32/tapdrv.c 
            # build depends on those.
            if g[0] == 'PRODUCT_TAP_WIN32_MIN_MAJOR':
                kv['PRODUCT_TAP_MAJOR_VER'] = g[1]
            elif g[0] == 'PRODUCT_TAP_WIN32_MIN_MINOR':
                kv['PRODUCT_TAP_MINOR_VER'] = g[1]

            # Add the variable to build configuration
            kv[g[0]] = g[1]
    f.close()

def parse_settings_in(kv, settings_in):
    r = re.compile(r'^!define\s+(\w+)(?:\s+"?(.*?)"?)?$')
    f = open(settings_in)
    for line in f:
        line = line.rstrip()
        m = re.match(r, line)
        if m:
            g = m.groups()
            kv[g[0]] = g[1] or ''
    f.close()

def parse_build_params(kv, settings_in):
    r = re.compile(r'^!define\s+(ENABLE_\w+)\s+(\w+)')

    f = open(settings_in)

    for line in f:
        line = line.rstrip()

        # Check if this is a #define line starts with ENABLE_
        m = re.match(r, line)

        if m:
                g = m.groups()
                kv[g[0]] = g[1] or ''
    f.close()

def dict_def(dict, newdefs):
    ret = dict.copy()
    ret.update(newdefs)
    return ret

def build_autodefs(kv, autodefs_in, autodefs_out):
    preprocess(kv,
               in_fn=autodefs_in,
               out_fn=autodefs_out,
               quote_begin='@',
               quote_end='@',
               head_comment='/* %s */\n\n' % autogen)

def build_config_h(kv):
    """Generate static win/config.h to config.h to mimic autotools behavior"""
    preprocess(kv,
               in_fn=mod_fn('config.h.in'),
               out_fn=home_fn('config.h'),
               quote_begin='@',
               quote_end='@',
               head_comment='/* %s */\n\n' % autogen)

def build_configure_h(kv, configure_h_out, head_comment):
    """Generate a configure.h dynamically"""
    fout = open(configure_h_out, 'w')

    # These two variables are required to view build parameters during runtime 
    configure_defines='#define CONFIGURE_DEFINES \"'
    configure_call='#define CONFIGURE_CALL \" config_all.py \"'

    # Initialize the list of enabled features
    features = ''

    # Write the header
    fout.write(head_comment)

    dict = get_build_params()

    for key, value in dict.iteritems():
        # Add enabled features
	features = features + "#define " + key + " " + value + "\n"

	# Add each enabled feature to CONFIGURE_DEFINES list
        configure_defines = configure_defines + " " + key + "=" + value + ","

    configure_defines = configure_defines + "\"" + "\n"

    fout.write(features)
    fout.write(configure_defines)
    fout.write(configure_call)


    fout.close()

def build_version_m4_vars(version_m4_vars_out, head_comment):
    """Generate a temporary file containing variables from version.m4 in 
win/settings.in format. This done to allow importing them in win/openvpn.nsi"""

    fout = open(version_m4_vars_out, 'w')
    fout.write(head_comment)

    kv = {}
    parse_version_m4(kv, home_fn('version.m4'))

    for key, value in kv.iteritems():
         line = "!define " + key + "\t" + "\"" + value + "\"" + "\n"
         fout.write(line)

    fout.close()

def preprocess(kv, in_fn, out_fn, quote_begin=None, quote_end=None, if_prefix=None, head_comment=None):
    def repfn(m):
        var, = m.groups()
        return kv.get(var, '')

    re_macro = re_ifdef = None

    if quote_begin and quote_end:
        re_macro = re.compile(r'%s(\w+)%s' % (re.escape(quote_begin), re.escape(quote_end)))

    if if_prefix:
        re_ifdef = re.compile(r'^\s*%sifdef\s+(\w+)\b' % (re.escape(if_prefix),))
        re_else = re.compile(r'^\s*%selse\b' % (re.escape(if_prefix),))
        re_endif = re.compile(r'^\s*%sendif\b' % (re.escape(if_prefix),))

    if_stack = []
    fin = open(in_fn)
    fout = open(out_fn, 'w')
    if head_comment:
        fout.write(head_comment)
    for line in fin:
        if re_ifdef:
            m = re.match(re_ifdef, line)
            if m:
                var, = m.groups()
                if_stack.append(int(var in kv))
                continue
            elif re.match(re_else, line):
                if_stack[-1] ^= 1
                continue
            elif re.match(re_endif, line):
                if_stack.pop()
                continue
        if not if_stack or min(if_stack):
            if re_macro:
                line = re.sub(re_macro, repfn, line)
            fout.write(line)
    assert not if_stack
    fin.close()
    fout.close()

def print_key_values(kv):
    for k, v in sorted(kv.items()):
        print "%s%s%s" % (k, ' '*(32-len(k)), repr(v))

def get_sources(makefile_am):
    """Parse ../Makefile.am to obtain a list of .h and .c files"""
    c = set()
    h = set()
    f = open(makefile_am)
    state = False
    for line in f:
        line = line.rstrip()
        if line == 'openvpn_SOURCES = \\':
            state = True
        elif not line:
            state = False
        elif state:
            for sf in line.split():
                if sf.endswith('.c'):
                    c.add(sf[:-2])
                elif sf.endswith('.h'):
                    h.add(sf[:-2])
                elif sf == '\\':
                    pass
                else:
                    print >>sys.stderr, "Unrecognized filename:", sf
    f.close()
    return [ sorted(list(s)) for s in (c, h) ]

def output_mak_list(title, srclist, ext):
    ret = "%s =" % (title,)
    for x in srclist:
        ret += " \\\n\t%s.%s" % (x, ext)
    ret += '\n\n'
    return ret

def make_headers_objs(makefile_am):
    """Generate HEADER and OBJS entries dynamically from ../Makefile.am"""
    c, h = get_sources(makefile_am)
    ret = output_mak_list('HEADERS', h, 'h')
    ret += output_mak_list('OBJS', c, 'obj')
    return ret

def choose_arch(arch_name):
    if arch_name == 'x64':
        return (True,)
    elif arch_name == 'x86':
        return (False,)
    elif arch_name == 'all':
        return (True, False)
    else:
        raise ValueError("architecture ('%s') must be x86, x64, or all" % (arch_name,))

def rm_rf(dir):
    print "REMOVE", dir
    shutil.rmtree(dir, ignore_errors=True)

def mkdir(dir):
    print "MKDIR", dir
    os.mkdir(dir)

def cp_a(src, dest, dest_is_dir=True):
    if dest_is_dir:
        dest = os.path.join(dest, os.path.basename(src))
    print "COPY_DIR %s %s" % (src, dest)
    shutil.copytree(src, dest)

def cp(src, dest, dest_is_dir=True):
    if dest_is_dir:
        dest = os.path.join(dest, os.path.basename(src))
    print "COPY %s %s" % (src, dest)
    shutil.copyfile(src, dest)

def rename(src, dest):
    print "RENAME %s %s" % (src, dest)
    shutil.move(src, dest)

def rm_rf(path):
    try:
        shutil.rmtree(path, onerror=onerror)
    except:
        pass

def onerror(func, path, exc_info):
    """
    Error handler for ``shutil.rmtree``.

    If the error is due to an access error (read only file)
    it attempts to add write permission and then retries.

    If the error is for another reason it re-raises the error.

    Usage : ``shutil.rmtree(path, onerror=onerror)``
    """
    if not os.access(path, os.W_OK):
        # Is the error an access error ?
        os.chmod(path, stat.S_IWUSR)
        func(path)
    else:
        raise

def mkdir_silent(dir):
    try:
        os.mkdir(dir)
    except:
        pass

config = get_config()
