#!/usr/bin/env python
#
#                     The LLVM Compiler Infrastructure
#
# This file is distributed under the University of Illinois Open Source
# License. See LICENSE.TXT for details.
#
##===----------------------------------------------------------------------===##
#
# This script attempts to be a drop-in replacement for gcc.
#
##===----------------------------------------------------------------------===##

import os
import sys
import subprocess

def checkenv(name, alternate=None):
    """checkenv(var, alternate=None) - Return the given environment var,
    or alternate if it is undefined or empty."""
    v = os.getenv(name)
    if v and v.strip():
        return v.strip()
    return alternate

def checkbool(name, default=False):
    v = os.getenv(name)
    if v:
        try:
            return bool(int(v))
        except:
            pass
    return default

CCC_LOG = checkenv('CCC_LOG')
CCC_ECHO = checkbool('CCC_ECHO')
CCC_NATIVE = checkbool('CCC_NATIVE','1')
CCC_FALLBACK = checkbool('CCC_FALLBACK')
CCC_LANGUAGES = checkenv('CCC_LANGUAGES','c,c++,c-cpp-output,objective-c,objective-c++,objective-c-cpp-output,assembler-with-cpp')
if CCC_LANGUAGES:
    CCC_LANGUAGES = set([s.strip() for s in CCC_LANGUAGES.split(',')])

# We want to support use as CC or LD, so we need different defines.
CLANG = checkenv('CLANG', 'clang')
LLC = checkenv('LLC', 'llc')
AS = checkenv('AS', 'as')
CC = checkenv('CCC_CC', 'cc')
LD = checkenv('CCC_LD', 'c++')

def error(message):
    print >> sys.stderr, 'ccc: ' + message
    sys.exit(1)

def quote(arg):
    if '"' in arg or ' ' in arg:
        return repr(arg)
    return arg

def stripoutput(args):
    """stripoutput(args) -> (output_name, newargs)
    
    Remove the -o argument from the arg list and return the output
    filename and a new argument list. Assumes there will be at most
    one -o option. If no output argument is found the result is (None,
    args)."""
    for i,a in enumerate(args):
        if a.startswith('-o'):
            if a=='-o':
                if i+1<len(args):
                    return args[i+1],args[:i]+args[i+2:]
            elif a.startswith('-o='):
                opt,arg = a.split('=',1)
                return arg,args[:i]+args[i+1:]
    return None,args

def run(args):
    if CCC_ECHO:
        print ' '.join(map(quote, args))
        sys.stdout.flush()
    code = subprocess.call(args)
    if code > 255:
        code = 1
    if code:
        sys.exit(code)

def remove(path):
    """remove(path) -> bool - Attempt to remove the file at path (if any).

    The result indicates if the remove was successful. A warning is
    printed if there is an error removing the file."""
    if os.path.exists(path):
        try:
            os.remove(path)
        except:
            print >>sys.stderr, 'WARNING: Unable to remove temp "%s"'%(path,)
            return False
    return True

def preprocess(args):
    command = [CLANG,'-E']
    run(command + args)

def syntaxonly(args):
    command = [CLANG,'-fsyntax-only']
    run(command + args)
    
def compile_fallback(args):
    command = [CC,'-c']
    run(command + args)
    
def compile(args, native, save_temps=False, asm_opts=[]):
    if native:
        output,args = stripoutput(args)
        if not output:
            raise ValueError,'Expected to always have explicit -o in compile()'

        # I prefer suffixing these to changing the extension, which is
        # more likely to overwrite other things. We could of course
        # use temp files.
        bc_output = output + '.bc'
        s_output = output + '.s'
        command = [CLANG,'-emit-llvm-bc']
        try:
            run(command + args + ['-o', bc_output])
            # FIXME: What controls relocation model?
            run([LLC, '-relocation-model=pic', '-f', '-o', s_output, bc_output])
            run([AS, '-o', output, s_output] + asm_opts)
        finally:
            if not save_temps:
                remove(bc_output)
                remove(s_output)
    else:
        command = [CLANG,'-emit-llvm-bc']
        run(command + args)

def checked_compile(args, native, language, save_temps, asm_opts):
    if CCC_LANGUAGES and language and language not in CCC_LANGUAGES:
        log('fallback', args)
        print >>sys.stderr, 'NOTE: ccc: Using fallback compiler for: %s'%(' '.join(map(quote, args)),)
        compile_fallback(args)
    elif CCC_FALLBACK:
        try:
            compile(args, native, save_temps, asm_opts)
        except:
            log('fallback-on-fail', args)
            print >>sys.stderr, 'WARNING: ccc: Using fallback compiler for: %s'%(' '.join(map(quote, args)),)
            compile_fallback(args)
    else:
        compile(args, native, save_temps, asm_opts)
    
def link(args, native):
    if native:
        run([LD] + args)
    else:
        command = ['llvm-ld', '-native', '-disable-internalize']
        run(command + args)

def extension(path):
    return path.split(".")[-1]

def changeextension(path, newext):
    i = path.rfind('.')
    if i < 0:
        return path
    j = path.rfind('/', 0, i)
    if j < 0:
        return path[:i] + "." + newext
    return path[j+1:i] + "." + newext

def inferlanguage(extension):
    if extension == "c":
        return "c"
    elif extension in ["cpp", "cc"]:
        return "c++"
    elif extension == "i":
        return "c-cpp-output"
    elif extension == "m":
        return "objective-c"
    elif extension == "mm":
        return "objective-c++"
    elif extension == "mi":
        return "objective-c-cpp-output"
    elif extension == "s":
        return "assembler"
    elif extension == "S":
        return "assembler-with-cpp"
    else:
        return ""

def log(name, item):
    if CCC_LOG:
        f = open(CCC_LOG,'a')
        print >>f, (name, item)
        f.close()

def inferaction(args):
    if '-E' in args:
        return 'preprocess'
    if '-fsyntax-only' in args:
        return 'syntax-only'
    if '-c' in args:
        return 'compile'
    for arg in args:
        if arg.startswith('-print-prog-name'):
            return 'pring-prog-name'
    return 'link'

def main(args):
    log('invoke', args)

    action = inferaction(args)
    output = ''
    asm_opts = []
    compile_opts = []
    link_opts = []
    files = []
    save_temps = 0
    language = ''
    native = CCC_NATIVE

    i = 0
    while i < len(args):
        arg = args[i]

        if '=' in arg:
            argkey,argvalue = arg.split('=',1)
        else:
            argkey,argvalue = arg,None

        # Modes ccc supports
        if arg == '-save-temps':
            save_temps = 1
        if arg == '-emit-llvm' or arg == '--emit-llvm':
            native = False

        # Options with no arguments that should pass through
        if arg in ['-v', '-fobjc-gc', '-fobjc-gc-only', '-fnext-runtime',
                   '-fgnu-runtime']:
            compile_opts.append(arg)
            link_opts.append(arg)
        
        # Options with one argument that should be ignored
        if arg in ['--param', '-u']:
            i += 1

        # Preprocessor options with one argument that should be ignored
        if arg in ['-MT', '-MF']:
            i += 1

        # Prefix matches for the compile mode
        if arg[:2] in ['-D', '-I', '-U', '-F']:
            if not arg[2:]:
                arg += args[i+1]
                i += 1
            compile_opts.append(arg)
        if argkey in ('-std', '-mmacosx-version-min'):
            compile_opts.append(arg)

        # Special case debug options to only pass -g to clang. This is
        # wrong.
        if arg in ('-g', '-gdwarf-2'):
            compile_opts.append('-g')

        # Options with one argument that should pass through to compiler
        if arg in [ '-include', '-idirafter', '-iprefix',
                       '-iquote', '-isystem', '-iwithprefix',
                       '-iwithprefixbefore']:
            compile_opts.append(arg)
            compile_opts.append(args[i+1])
            i += 1

        # Options with no arguments that should pass through
        if (arg in ('-dynamiclib', '-bundle', '-headerpad_max_install_names',
                    '-nostdlib', '-static', '-dynamic', '-r') or
            arg.startswith('-Wl,')):
            link_opts.append(arg)

        # Options with one argument that should pass through
        if arg in ('-framework', '-multiply_defined', '-bundle_loader',
                   '-weak_framework',
                   '-e', '-install_name',
                   '-unexported_symbols_list', '-exported_symbols_list', 
                   '-compatibility_version', '-current_version', '-init',
                   '-seg1addr', '-dylib_file', '-Xlinker', '-undefined'):
            link_opts.append(arg)
            link_opts.append(args[i+1])
            i += 1

        # Options with one argument that should pass through to both
        if arg in ['-isysroot', '-arch']:
            compile_opts.append(arg)
            compile_opts.append(args[i+1])
            link_opts.append(arg)
            link_opts.append(args[i+1])
            asm_opts.append(arg)
            asm_opts.append(args[i+1])
            i += 1
        
        # Options with three arguments that should pass through
        if arg in ('-sectorder',):
            link_opts.extend(args[i:i+4])
            i += 3

        # Prefix matches for the link mode
        if arg[:2] in ['-l', '-L', '-F', '-R']:
            link_opts.append(arg)

        # Enable threads
        if arg == '-pthread':
          link_opts.append('-lpthread')

        # Input files
        if arg == '-filelist':
            f = open(args[i+1])
            for line in f:
                files.append(line.strip())
            f.close()
            i += 1
        if arg == '-x':
            language = args[i+1]
            compile_opts.append(arg)
            compile_opts.append(args[i+1])
            i += 1
        if arg[0] != '-':
            files.append(arg)

        # Output file
        if arg == '-o':
            output = args[i+1]
            i += 1

        i += 1
    
    if action == 'print-prog-name':
        # assume we can handle everything
        print sys.argv[0]
        return

    if not files:
        error('no input files')

    if action == 'preprocess' or save_temps:
        for i, file in enumerate(files):
            if not language:
                language = inferlanguage(extension(file))
            if save_temps and action != 'preprocess':
                # Need a temporary output file
                if language == 'c':
                    poutput = changeextension(file, "i");
                elif language == 'objective-c':
                    poutput = changeextension(file, "mi");
                else:
                    poutput = changeextension(file, "tmp." + extension(file))
                files[i] = poutput
            else:
                poutput = output
            args = []
            if language:
                args.extend(['-x', language])
            if poutput:
                args += ['-o', poutput, file] + compile_opts
            else:
                args += [file] + compile_opts
            preprocess(args)
            # Discard the explicit language after used once
            language = ''

    if action == 'syntax-only':
        for i, file in enumerate(files):
            if not language:
                language = inferlanguage(extension(file))
            args = []
            if language:
                args.extend(['-x', language])
            args += [file] + compile_opts
            syntaxonly(args)
            language = ''

    if action == 'compile' or save_temps:
        for i, file in enumerate(files):
            if not language:
                language = inferlanguage(extension(file))
            if save_temps and action != "compile":
                # Need a temporary output file
                coutput = changeextension(file, "o");
                files[i] = coutput
            elif not output:
                coutput = changeextension(file, "o")
            else:
                coutput = output
            args = []
            if language:
                args.extend(['-x', language])
            args += ['-o', coutput, file] + compile_opts
            checked_compile(args, native, language, save_temps, asm_opts)
            language = ''

    if action == 'link':
        for i, file in enumerate(files):
            if not language:
                language = inferlanguage(extension(file))
            ext = extension(file)
            if ext != "o" and ext != "a" and ext != "so":
                out = changeextension(file, "o")
                args = []
                if language:
                    args.extend(['-x', language])
                args = ['-o', out, file] + compile_opts
                checked_compile(args, native, language, save_temps, asm_opts)
                language = ''
                files[i] = out
        if not output:
            output = 'a.out'
        args = ['-o', output] + files + link_opts
        link(args, native)

if __name__ == '__main__':
    main(sys.argv[1:])
