1#!/usr/bin/env python
2#
3# Copyright (C) 2017 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18import errno
19import fcntl
20import os
21import shlex
22import subprocess
23import sys
24import time
25
26BISECT_STAGE = os.environ.get('BISECT_STAGE')
27# We do not need bisect functionality with Goma and clang.
28# Goma server does not have bisect_driver, so we only import
29# bisect_driver when needed. See http://b/34862041
30# We should be careful when doing imports because of Goma.
31if BISECT_STAGE:
32    import bisect_driver
33
34DEFAULT_BISECT_DIR = os.path.expanduser('~/ANDROID_BISECT')
35BISECT_DIR = os.environ.get('BISECT_DIR') or DEFAULT_BISECT_DIR
36STDERR_REDIRECT_KEY = 'ANDROID_LLVM_STDERR_REDIRECT'
37PREBUILT_COMPILER_PATH_KEY = 'ANDROID_LLVM_PREBUILT_COMPILER_PATH'
38DISABLED_WARNINGS_KEY = 'ANDROID_LLVM_FALLBACK_DISABLED_WARNINGS'
39
40
41def ProcessArgFile(arg_file):
42    args = []
43    # Read in entire file at once and parse as if in shell
44    with open(arg_file, 'rb') as f:
45        args.extend(shlex.split(f.read()))
46    return args
47
48
49def write_log(path, command, log):
50    with open(path, 'a+') as f:
51        while True:
52            try:
53                fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
54                break
55            except IOError as e:
56                if e.errno == errno.EAGAIN or e.errno == errno.EACCES:
57                    time.sleep(0.5)
58        f.write('==================COMMAND:====================\n')
59        f.write(' '.join(command) + '\n\n')
60        f.write(log)
61        f.write('==============================================\n\n')
62
63
64class CompilerWrapper():
65
66    def __init__(self, argv):
67        self.argv0_current = argv[0]
68        self.args = argv[1:]
69        self.execargs = []
70        self.real_compiler = None
71        self.argv0 = None
72        self.append_flags = []
73        self.prepend_flags = []
74        self.custom_flags = {'--gomacc-path': None}
75
76    def set_real_compiler(self):
77        """Find the real compiler with the absolute path."""
78        compiler_path = os.path.dirname(self.argv0_current)
79        if os.path.islink(__file__):
80            compiler = os.path.basename(os.readlink(__file__))
81        else:
82            compiler = os.path.basename(os.path.abspath(__file__))
83        self.real_compiler = os.path.join(compiler_path, compiler + '.real')
84        self.argv0 = self.real_compiler
85
86    def process_gomacc_command(self):
87        """Return the gomacc command if '--gomacc-path' is set."""
88        gomacc = self.custom_flags['--gomacc-path']
89        if gomacc and os.path.isfile(gomacc):
90            self.argv0 = gomacc
91            self.execargs += [gomacc]
92
93    def parse_custom_flags(self):
94        i = 0
95        args = []
96        while i < len(self.args):
97            if self.args[i] in self.custom_flags:
98                if i >= len(self.args) - 1:
99                    sys.exit('The value of {} is not set.'.format(self.args[i]))
100                self.custom_flags[self.args[i]] = self.args[i + 1]
101                i = i + 2
102            else:
103                args.append(self.args[i])
104                i = i + 1
105        self.args = args
106
107    def add_flags(self):
108        self.args = self.prepend_flags + self.args + self.append_flags
109
110    def prepare_compiler_args(self, enable_fallback=False):
111        self.set_real_compiler()
112        self.parse_custom_flags()
113        # Goma should not be enabled for new prebuilt.
114        if not enable_fallback:
115            self.process_gomacc_command()
116        self.add_flags()
117        self.execargs += [self.real_compiler] + self.args
118
119    def exec_clang_with_fallback(self):
120        # We only want to pass extra flags to clang and clang++.
121        if os.path.basename(__file__) in ['clang', 'clang++']:
122            # We may introduce some new warnings after rebasing and we need to
123            # disable them before we fix those warnings.
124            disabled_warnings_env = os.environ.get(DISABLED_WARNINGS_KEY, '')
125            disabled_warnings = disabled_warnings_env.split(' ')
126            self.execargs += ['-fno-color-diagnostics'] + disabled_warnings
127
128        p = subprocess.Popen(self.execargs, stderr=subprocess.PIPE)
129        (_, err) = p.communicate()
130        sys.stderr.write(err)
131        if p.returncode != 0:
132            redirect_path = os.environ[STDERR_REDIRECT_KEY]
133            write_log(redirect_path, self.execargs, err)
134            fallback_arg0 = os.path.join(os.environ[PREBUILT_COMPILER_PATH_KEY],
135                                         os.path.basename(__file__))
136            os.execv(fallback_arg0, [fallback_arg0] + self.execargs[1:])
137
138    def invoke_compiler(self):
139        enable_fallback = PREBUILT_COMPILER_PATH_KEY in os.environ
140        self.prepare_compiler_args(enable_fallback)
141        if enable_fallback:
142            self.exec_clang_with_fallback()
143        else:
144            os.execv(self.argv0, self.execargs)
145
146    def bisect(self):
147        self.prepare_compiler_args()
148        # Handle @file argument syntax with compiler
149        idx = 0
150        # The length of self.execargs can be changed during the @file argument
151        # expansion, so we need to use while loop instead of for loop.
152        while idx < len(self.execargs):
153            if self.execargs[idx][0] == '@':
154                args_in_file = ProcessArgFile(self.execargs[idx][1:])
155                self.execargs = self.execargs[0:idx] + args_in_file +\
156                        self.execargs[idx + 1:]
157                # Skip update of idx, since we want to recursively expand
158                # response files.
159            else:
160                idx = idx + 1
161        bisect_driver.bisect_driver(BISECT_STAGE, BISECT_DIR, self.execargs)
162
163
164def main(argv):
165    cw = CompilerWrapper(argv)
166    if BISECT_STAGE and BISECT_STAGE in bisect_driver.VALID_MODES\
167            and '-o' in argv:
168        cw.bisect()
169    else:
170        cw.invoke_compiler()
171
172
173if __name__ == '__main__':
174    main(sys.argv)
175