blob: beb84421f26f4a001ea5764c41420ff567722caa [file] [log] [blame]
#!/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