| #!/usr/bin/python |
| |
| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # This script is a meta-driver for the toolchain. It transforms the command |
| # line to allow the following: |
| # 1. This script ensures that '--sysroot' is passed to whatever it is wrapping. |
| # |
| # 2. It adds hardened flags to gcc invocation. The hardened flags are: |
| # -fstack-protector-strong |
| # -fPIE |
| # -pie |
| # -D_FORTIFY_SOURCE=2 |
| # |
| # It can disable -fPIE -pie by checking if -nopie is passed to gcc. In this |
| # case it removes -nopie as it is a non-standard flag. |
| # |
| # 3. Enable clang diagnostics with -clang-syntax option |
| # |
| # 4. Add new -print-cmdline option to print the command line before executon |
| # |
| # 5. Enable clang codegen. |
| # This is currently implemented as two loops on the list of arguments. The |
| # first loop # identifies hardening flags, as well as determining if clang |
| # invocation is specified. The second loop build command line for clang |
| # invocation as well adjusting gcc command line. |
| # |
| # This implementation ensure compile time of default path remains mostly |
| # the same. |
| # |
| # There is a similar hardening wrapper that wraps ld and adds -z now -z relro |
| # to the link command line (see ldwrapper). |
| # |
| # To use: |
| # mv <tool> <tool>.real |
| # ln -s <path_to_sysroot_wrapper> <tool> |
| |
| from __future__ import print_function |
| |
| import errno |
| import os |
| import re |
| import sys |
| |
| |
| # Full hardening. Some/all of these may be discarded depending on |
| # other flags. |
| # Temporarily disable function splitting because of chromium:434751. |
| flags_to_add = set(['-fstack-protector-strong', '-fPIE', '-pie', |
| '-D_FORTIFY_SOURCE=2', '-frecord-gcc-switches', |
| '-fno-reorder-blocks-and-partition']) |
| x86_disable_flags = set(['-mno-movbe']) |
| |
| # Only FORTIFY_SOURCE hardening flag is applicable for clang. |
| clang_flags = set(['-Qunused-arguments', '-D_FORTIFY_SOURCE=2', '-fPIE']) |
| |
| # If -clang-syntax is present or the command line uses clang instead of gcc. |
| invoke_clang = False |
| |
| # If -print-cmdline is present. |
| print_cmdline = False |
| |
| # If ccache should be used automatically. |
| use_ccache = True # @CCACHE_DEFAULT@ Keep this comment for code. |
| |
| fstack = set(['-D__KERNEL__', '-fno-stack-protector', '-nodefaultlibs', |
| '-nostdlib']) |
| fPIE = set(['-D__KERNEL__', '-fPIC', '-fPIE', '-fno-PIC', '-fno-PIE', |
| '-fno-pic', '-fno-pie', '-fpic', '-fpie', '-nopie', |
| '-nostartfiles', '-nostdlib', '-pie', '-static']) |
| pie = set(['-D__KERNEL__', '-A', '-fno-PIC', '-fno-PIE', '-fno-pic', '-fno-pie', |
| '-nopie', '-nostartfiles', '-nostdlib', '-pie', '-r', '--shared', |
| '-shared', '-static']) |
| sse = set(['-msse3', '-mssse3', '-msse4.1', '-msse4.2', '-msse4', '-msse4a']) |
| wrapper_only_options = set(['-clang-syntax', '-print-cmdline', |
| '-nopie', '-noccache']) |
| |
| myargs = sys.argv[1:] |
| |
| if fstack.intersection(myargs): |
| flags_to_add.remove('-fstack-protector-strong') |
| flags_to_add.add('-fno-stack-protector') |
| if fPIE.intersection(myargs): |
| flags_to_add.remove('-fPIE') |
| clang_flags.remove('-fPIE') |
| if pie.intersection(myargs): |
| flags_to_add.remove('-pie') |
| print_cmdline = '-print-cmdline' in myargs |
| clang_cmdline = list(clang_flags) |
| clang_codegen = sys.argv[0].split('-')[-1] in ('clang', 'clang++') |
| |
| # We will start to enable ASAN for cros-workon packages. |
| # For ASAN, if a library is built with ASAN, then all the binaries that use |
| # this library should at lease link against ASAN. |
| # Our solution is to add '-fsanitize=address' to LDFLAGS for asan bot. |
| # This causes another problem. Packages that are built with gcc.real have the |
| # '-fsanitize=address' option in the link time. We use clang for all asan |
| # built. This is a conflict. Our solution is if we see a gcc command with |
| # '-fsanitize=address', we first try to run it without '-fsanitize=address'. |
| # If it is successful, then we are fine. otherwise, we check the output of the |
| # command, if it contains some thing like undefinited symbol "asan_init", we |
| # invoke clang and run again. If there is an error in the gcc command, that is |
| # not related to ASAN, we just exit. |
| |
| ASAN_FLAG = '-fsanitize=address' |
| # some package will transfer '-fsanitize=address' to '-Wl,-fsanitize=address'. |
| myargs = [ASAN_FLAG if ASAN_FLAG in x else x for x in myargs] |
| link_with_asan = not clang_codegen and ASAN_FLAG in myargs |
| clang_codegen |= link_with_asan |
| invoke_clang = '-clang-syntax' in myargs or clang_codegen |
| |
| if '-noccache' in myargs or clang_codegen and not link_with_asan: |
| # TODO make clang work with ccache. |
| # Clang does not work with ccache well. At lease it fails at |
| # package adhd. |
| |
| # Only explicitly disable so we can set defaults up top. |
| use_ccache = False |
| cmdline = [x for x in myargs if x not in wrapper_only_options] |
| |
| if re.match(r'i.86|x86_64', os.path.basename(sys.argv[0])): |
| cmdline.extend(x86_disable_flags) |
| |
| if not invoke_clang: |
| gcc_cmdline = cmdline |
| else: |
| import subprocess |
| # Gcc flags to remove from the clang command line. |
| # TODO: Once clang supports gcc compatibility mode, remove |
| # these checks. |
| # |
| # Use of -Qunused-arguments allows this set to be small, just those |
| # that clang still warns about. |
| clang_unsupported = set(['-pass-exit-codes', '-Ofast', '-Wclobbered', |
| '-Wunsafe-loop-optimizations', '-Wlogical-op', |
| '-Wmissing-parameter-type', '-Woverride-init', |
| '-Wold-style-declaration', '-Wno-psabi', |
| '-Wno-unused-local-typedefs', |
| '-mno-movbe',]) |
| clang_unsupported_prefixes = ('-Wstrict-aliasing=') |
| |
| # Clang may use different options for the same or similar functionality. |
| gcc_to_clang = { |
| '-Wno-error=unused-but-set-variable': '-Wno-error=unused-variable', |
| '-Wno-error=maybe-uninitialized': '-Wno-error=uninitialized', |
| '-Wno-unused-but-set-variable': '-Wno-unused-variable', |
| '-Wunused-but-set-variable': '-Wunused-variable', |
| '-fstack-protector-strong': '-fstack-protector-all', |
| '-fvisibility=internal': '-fvisibility=hidden', |
| '-Wno-error=cpp': '-Wno-#warnings', |
| } |
| |
| # If these options are specified, do not run clang, even if -clang-syntax is |
| # specified. |
| # This is mainly for utilities that depend on compiler output. |
| skip_clang_prefixes = ('-print-', '-dump', '@') |
| skip_clang_set = set(['-', '-E', '-M']) |
| |
| # Reset gcc cmdline too. Only change is to remove -Xclang-only |
| # options if specified. |
| gcc_cmdline = [] |
| |
| skip_clang = False |
| for flag in cmdline: |
| if (not clang_codegen and |
| (flag.startswith(skip_clang_prefixes) or |
| flag in skip_clang_set or |
| flag.endswith('.S'))): |
| skip_clang = True |
| elif not (flag in clang_unsupported or |
| flag.startswith(clang_unsupported_prefixes)): |
| # Strip off -Xclang-only= if present. |
| if flag.startswith('-Xclang-only='): |
| opt = flag.partition('=')[2] |
| clang_cmdline.append(opt) |
| # No need to add to gcc_cmdline. |
| continue |
| elif flag in gcc_to_clang.keys(): |
| clang_cmdline.append(gcc_to_clang[flag]) |
| else: |
| clang_cmdline.append(flag) |
| gcc_cmdline.append(flag) |
| |
| |
| def get_proc_cmdline(pid): |
| with open('/proc/%i/cmdline' % pid) as fp: |
| return fp.read().replace('\0', ' ') |
| return None |
| |
| |
| def get_proc_status(pid, item): |
| with open('/proc/%i/status' % pid) as fp: |
| for line in fp: |
| m = re.match(r'%s:\s*(.*)' % re.escape(item), line) |
| if m: |
| return m.group(1) |
| return None |
| |
| |
| def log_parent_process_tree(log, ppid): |
| depth = 0 |
| |
| while ppid > 1: |
| cmd = get_proc_cmdline(ppid) |
| log.warning(' %*s {%5i}: %s' % (depth, '', ppid, cmd)) |
| |
| ppid = get_proc_status(ppid, 'PPid') |
| if not ppid: |
| break |
| ppid = int(ppid) |
| depth += 2 |
| |
| |
| # clang doesn't provide its own viable libstdc++ implementation. |
| # Instead, it uses gcc's. So we need to find and include path and |
| # the lib path from gcc. There is no gcc-config for cros-compiler, |
| # so we have to do it in this way. |
| # |
| # TODO: make ebuild calculate the values and write it to the wrapper to |
| # avoid calling gcc everytime. |
| def get_gcc_include(): |
| """Get the libstdc++ path.""" |
| lang_parameter = 'c' |
| if sys.argv[0].endswith('++'): |
| lang_parameter += '++' |
| origin_cmd = sys.argv[0] |
| if 'clang++' in origin_cmd: |
| origin_cmd = origin_cmd.replace('clang++', 'g++') |
| if 'clang' in origin_cmd: |
| origin_cmd = origin_cmd.replace('clang', 'gcc') |
| real_cmd = '%s.real' % origin_cmd |
| |
| command = [real_cmd, '-v', '-E', '-x', lang_parameter, '-'] |
| p = subprocess.Popen(command, stdin=subprocess.PIPE, |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| out, err = p.communicate(input='') |
| gcc_include = [] |
| include_start = False |
| for line in err.splitlines(): |
| if '#include <...> search starts here:' in line: |
| include_start = True |
| continue |
| if 'End of search list' in line: |
| include_start = False |
| if include_start: |
| if not line.endswith('include'): |
| gcc_include.append('-I' + line.strip()) |
| if 'LIBRARY_PATH' in line: |
| lib_path = line.split(':')[0].split('=')[1] |
| gcc_include.append('-B' + lib_path) |
| return gcc_include |
| |
| |
| def get_cmd_path(cmd): |
| """Return the canonicalized dir for the cmd found via $PATH.""" |
| |
| for path in os.environ['PATH'].split(':'): |
| cmd_path = os.path.join(path, cmd) |
| if os.path.exists(cmd_path): |
| cmd_full = os.path.realpath(cmd_path) |
| return os.path.dirname(cmd_full) |
| return None |
| |
| |
| def link_asan(): |
| """link process when '-fsanitize=address' appears in a gcc command.""" |
| orig_gcc_cmdline = [x for x in gcc_cmdline if ASAN_FLAG not in x] |
| gcc_execargs = (execargs + [real_gcc] + |
| list(flags_to_add) + orig_gcc_cmdline) |
| |
| p = subprocess.Popen(gcc_execargs, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| out, err = p.communicate() |
| errorcode = p.returncode |
| found_clang_undef = False |
| if (out and '__asan_' in out or |
| err and '__asan_' in err): |
| found_clang_undef = True |
| |
| if errorcode != 0 and found_clang_undef: |
| if print_cmdline: |
| print('%s %s\n' % (clang_comp, ' '.join(clang_cmdline))) |
| clang_execargs = [clang_comp] + clang_cmdline |
| sys.stdout.flush() |
| os.execv(clang_comp, clang_execargs) |
| else: |
| if print_cmdline: |
| print('[%s] %s' % (argv0, ' '.join(gcc_execargs))) |
| if out: |
| print(out) |
| if err: |
| print(err, file=sys.stderr) |
| sys.exit(errorcode) |
| |
| |
| def get_gomacc_command(): |
| """Return the gomacc command if it is found in $GOMACC_PATH.""" |
| gomacc = os.environ.get('GOMACC_PATH') |
| if gomacc and os.path.isfile(gomacc): |
| return gomacc |
| return None |
| |
| |
| def syntax_check_with_clang(clang_comp, clang_cmdline): |
| """Execute clang for syntax checking.""" |
| command = [clang_comp] + clang_cmdline |
| gomacc = get_gomacc_command() |
| if gomacc: |
| command.insert(0, gomacc) |
| if print_cmdline: |
| print('%s\n' % ' '.join(command)) |
| p = subprocess.Popen(command) |
| p.wait() |
| if p.returncode != 0: |
| sys.exit(p.returncode) |
| |
| |
| sysroot = os.environ.get('SYSROOT', '') |
| if sysroot: |
| clang_cmdline.insert(0, '--sysroot=%s' % sysroot) |
| gcc_cmdline.insert(0, '--sysroot=%s' % sysroot) |
| else: |
| import logging |
| import logging.handlers |
| import traceback |
| |
| log_file = '/tmp/sysroot_wrapper.error' |
| |
| log = logging.getLogger('sysroot_wrapper') |
| log.setLevel(logging.DEBUG) |
| |
| handler = logging.handlers.RotatingFileHandler(log_file, maxBytes=0x20000000, |
| backupCount=1) |
| formatter = logging.Formatter('%(asctime)s %(message)s') |
| handler.setFormatter(formatter) |
| log.addHandler(handler) |
| |
| log.warning('Invocation with missing SYSROOT: %s' % ' '.join(sys.argv)) |
| try: |
| log_parent_process_tree(log, os.getppid()) |
| except IOError: |
| log.error('%s' % traceback.format_exc()) |
| |
| try: |
| # The logging module does not support setting permissions. |
| os.chmod(log_file, 0666) |
| except OSError: |
| pass |
| |
| if invoke_clang and not skip_clang: |
| clang_comp = os.environ.get('CLANG', '/usr/bin/clang') |
| |
| # Check for clang or clang++. |
| if sys.argv[0].endswith('++'): |
| clang_comp += '++' |
| |
| # Specify the target for clang. |
| gcc_comp = os.path.basename(sys.argv[0]) |
| arch = '-'.join(gcc_comp.split('-')[0:-1]) |
| linker = arch + '-ld' |
| linker_path = get_cmd_path(linker) |
| clang_cmdline += ['-B' + linker_path] |
| if re.match(r'i.86', arch): |
| # We can not set -target for x86 because our target is i686-pc-linux-gnu. |
| # If the target is set, it will search for libclang_rt.asan-i686.a |
| # when linking against ASAN. However, this file does not exist. |
| # The libclang_rt.asan-i386.a is there, but we can not set target to |
| # i386-pc-linux-gnu, because the i386-pc-linux-gnu-ld does not exist. |
| clang_cmdline += ['-m32'] |
| else: |
| clang_cmdline += ['-target', arch] |
| |
| if clang_codegen and sys.argv[0].endswith('++'): |
| clang_cmdline += get_gcc_include() |
| elif not clang_codegen: |
| clang_cmdline.append('-fsyntax-only') |
| |
| if not clang_codegen: |
| syntax_check_with_clang(clang_comp, clang_cmdline) |
| |
| execargs = [] |
| real_gcc = '%s.real' % sys.argv[0] |
| gomacc = get_gomacc_command() |
| if gomacc: |
| argv0 = gomacc |
| execargs += [gomacc] |
| elif use_ccache: |
| # Portage likes to set this for us when it has FEATURES=-ccache. |
| # The other vars we need to setup manually because of tools like |
| # scons that scrubs the env before we get executed. |
| os.environ.pop('CCACHE_DISABLE', None) |
| |
| # We should be able to share the objects across compilers as |
| # the pre-processed output will differ. This allows boards |
| # that share compiler flags (like x86 boards) to share caches. |
| ccache_dir = '/var/cache/distfiles/ccache' |
| os.environ['CCACHE_DIR'] = ccache_dir |
| |
| # If RESTRICT=sandbox is enabled, then sandbox won't be setup, |
| # and the env vars won't be available for appending. |
| if 'SANDBOX_WRITE' in os.environ: |
| os.environ['SANDBOX_WRITE'] += ':%s' % ccache_dir |
| |
| # We need to get ccache to make relative paths from within the |
| # sysroot. This lets us share cached files across boards (if |
| # all other things are equal of course like CFLAGS) as well as |
| # across versions. A quick test is something like: |
| # $ export CFLAGS='-O2 -g -pipe' CXXFLAGS='-O2 -g -pipe' |
| # $ BOARD=x86-alex |
| # $ cros_workon-$BOARD stop cros-disks |
| # $ emerge-$BOARD cros-disks |
| # $ cros_workon-$BOARD start cros-disks |
| # $ emerge-$BOARD cros-disks |
| # $ BOARD=amd64-generic |
| # $ cros_workon-$BOARD stop cros-disks |
| # $ emerge-$BOARD cros-disks |
| # $ cros_workon-$BOARD start cros-disks |
| # $ emerge-$BOARD cros-disks |
| # All of those will get cache hits (ignoring the first one |
| # which will seed the cache) due to this setting. |
| if sysroot: |
| os.environ['CCACHE_BASEDIR'] = sysroot |
| |
| # Minor speed up as we don't care about this in general. |
| # os.environ['CCACHE_NOSTATS'] = 'no' |
| # Useful for debugging. |
| # os.environ['CCACHE_LOG'] = '/dev/stderr' |
| |
| # The gcc ebuild takes care of nuking the cache in the whenever it revbumps |
| # in a way that matters, so we should be able to disable ccache's check. |
| # We've found in practice though that sometimes that doesn't happen. Since |
| # the default check is cheap (it's a stat() in mtime mode), keep it enabled. |
| # os.environ['CCACHE_COMPILERCHECK'] = 'none' |
| |
| # Make sure we keep the cached files group writable. |
| os.environ['CCACHE_UMASK'] = '002' |
| |
| argv0 = '/usr/bin/ccache' |
| execargs += ['ccache'] |
| else: |
| argv0 = real_gcc |
| |
| if link_with_asan: |
| link_asan() |
| |
| if clang_codegen: |
| execargs += [clang_comp] + clang_cmdline |
| argv0 = clang_comp |
| else: |
| execargs += [real_gcc] + list(flags_to_add) + gcc_cmdline |
| |
| if print_cmdline: |
| print('[%s] %s' % (argv0, ' '.join(execargs))) |
| |
| sys.stdout.flush() |
| try: |
| os.execv(argv0, execargs) |
| except OSError as e: |
| if use_ccache and e.errno == errno.ENOENT: |
| print('error: make sure you install ccache\n', file=sys.stderr) |
| print('error: os.execv(%s, %s) failed' % (argv0, execargs), file=sys.stderr) |
| raise |