blob: 3b9722b48c83b2327ec42870ad7ffbe319453699 [file] [log] [blame]
#!/usr/bin/python
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Downloads, builds (with instrumentation) and installs shared libraries."""
import argparse
import ast
import fcntl
import os
import platform
import re
import shlex
import shutil
import subprocess
import sys
SCRIPT_ABSOLUTE_PATH = os.path.dirname(os.path.abspath(__file__))
def unescape_flags(s):
"""Un-escapes build flags received from GYP.
GYP escapes build flags as if they are to be inserted directly into a command
line, wrapping each flag in double quotes. When flags are passed via
CFLAGS/LDFLAGS instead, double quotes must be dropped.
"""
if not s:
return ''
try:
return ' '.join(ast.literal_eval(s))
except (SyntaxError, ValueError):
return ' '.join(shlex.split(s))
def real_path(path_relative_to_gyp):
"""Returns the absolute path to a file.
GYP generates paths relative to the location of the .gyp file, which is one
level above the location of this script. This function converts them to
absolute paths.
"""
return os.path.realpath(os.path.join(SCRIPT_ABSOLUTE_PATH, '..',
path_relative_to_gyp))
class InstrumentedPackageBuilder(object):
"""Checks out and builds a single instrumented package."""
def __init__(self, args, clobber):
self._cc = args.cc
self._cxx = args.cxx
self._extra_configure_flags = unescape_flags(args.extra_configure_flags)
self._libdir = args.libdir
self._package = args.package
self._patch = real_path(args.patch) if args.patch else None
self._pre_build = \
real_path(args.pre_build) if args.pre_build else None
self._sanitizer = args.sanitizer
self._verbose = args.verbose
self._clobber = clobber
self._working_dir = os.path.join(
real_path(args.intermediate_dir), self._package, '')
product_dir = real_path(args.product_dir)
self._destdir = os.path.join(
product_dir, 'instrumented_libraries', self._sanitizer)
self._source_archives_dir = os.path.join(
product_dir, 'instrumented_libraries', 'sources', self._package)
self._cflags = unescape_flags(args.cflags)
if args.sanitizer_blacklist:
blacklist_file = real_path(args.sanitizer_blacklist)
self._cflags += ' -fsanitize-blacklist=%s' % blacklist_file
self._ldflags = unescape_flags(args.ldflags)
self.init_build_env()
# Initialized later.
self._source_dir = None
self._source_archives = None
def init_build_env(self):
self._build_env = os.environ.copy()
self._build_env['CC'] = self._cc
self._build_env['CXX'] = self._cxx
self._build_env['CFLAGS'] = self._cflags
self._build_env['CXXFLAGS'] = self._cflags
self._build_env['LDFLAGS'] = self._ldflags
if self._sanitizer == 'asan':
# Do not report leaks during the build process.
self._build_env['ASAN_OPTIONS'] = \
'%s:detect_leaks=0' % self._build_env.get('ASAN_OPTIONS', '')
# libappindicator1 needs this.
self._build_env['CSC'] = '/usr/bin/mono-csc'
def shell_call(self, command, env=None, cwd=None, ignore_ret_code=False):
"""Wrapper around subprocess.Popen().
Calls command with specific environment and verbosity using
subprocess.Popen().
"""
child = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
env=env, shell=True, cwd=cwd)
stdout, stderr = child.communicate()
if ignore_ret_code:
if self._verbose:
print stdout
return
if self._verbose or child.returncode:
print stdout
if child.returncode:
raise Exception('Failed to run: %s' % command)
def maybe_download_source(self):
"""Checks out the source code (if needed).
Checks out the source code for the package, if required (i.e. unless running
in no-clobber mode). Initializes self._source_dir and self._source_archives.
"""
get_fresh_source = self._clobber or not os.path.exists(self._working_dir)
if get_fresh_source:
shutil.rmtree(self._working_dir, ignore_errors=True)
os.makedirs(self._working_dir)
# Download one source package at a time, otherwise, there will
# be connection errors in gnutls_handshake().
lock = open('apt-source-lock', 'w')
fcntl.flock(lock, fcntl.LOCK_EX)
self.shell_call('apt-get source %s' % self._package,
cwd=self._working_dir)
fcntl.flock(lock, fcntl.LOCK_UN)
(dirpath, dirnames, filenames) = os.walk(self._working_dir).next()
if len(dirnames) != 1:
raise Exception(
'`apt-get source %s\' must create exactly one subdirectory.'
% self._package)
self._source_dir = os.path.join(dirpath, dirnames[0], '')
if len(filenames) == 0:
raise Exception('Can\'t find source archives after `apt-get source %s\'.'
% self._package)
self._source_archives = \
[os.path.join(dirpath, filename) for filename in filenames]
return get_fresh_source
def patch_source(self):
if self._patch:
self.shell_call('patch -p1 -i %s' % self._patch, cwd=self._source_dir)
if self._pre_build:
self.shell_call(self._pre_build, cwd=self._source_dir)
def copy_source_archives(self):
"""Copies the downloaded source archives to the output dir.
For license compliance purposes, every Chromium build that includes
instrumented libraries must include their full source code.
"""
shutil.rmtree(self._source_archives_dir, ignore_errors=True)
os.makedirs(self._source_archives_dir)
for filename in self._source_archives:
shutil.copy(filename, self._source_archives_dir)
if self._patch:
shutil.copy(self._patch, self._source_archives_dir)
def download_build_install(self):
got_fresh_source = self.maybe_download_source()
if got_fresh_source:
self.patch_source()
self.copy_source_archives()
if not os.path.exists(self.dest_libdir()):
os.makedirs(self.dest_libdir())
try:
self.build_and_install()
except Exception as exception:
print 'ERROR: Failed to build package %s. Have you run ' \
'src/third_party/instrumented_libraries/scripts/' \
'install-build-deps.sh?' % \
self._package
print
raise
# Touch a text file to indicate package is installed.
stamp_file = os.path.join(self._destdir, '%s.txt' % self._package)
open(stamp_file, 'w').close()
# Remove downloaded package and generated temporary build files. Failed
# builds intentionally skip this step to help debug build failures.
if self._clobber:
self.shell_call('rm -rf %s' % self._working_dir)
def fix_rpaths(self, directory):
# TODO(eugenis): reimplement fix_rpaths.sh in Python.
script = real_path('scripts/fix_rpaths.sh')
self.shell_call("%s %s" % (script, directory))
def temp_dir(self):
"""Returns the directory which will be passed to `make install'."""
return os.path.join(self._source_dir, 'debian', 'instrumented_build')
def temp_libdir(self):
"""Returns the directory under temp_dir() containing the DSOs."""
return os.path.join(self.temp_dir(), self._libdir)
def dest_libdir(self):
"""Returns the final location of the DSOs."""
return os.path.join(self._destdir, self._libdir)
def cleanup_after_install(self):
"""Removes unneeded files in self.temp_libdir()."""
# .la files are not needed, nuke them.
# In case --no-static is not supported, nuke any static libraries we built.
self.shell_call(
'find %s -name *.la -or -name *.a | xargs rm -f' % self.temp_libdir())
# .pc files are not needed.
self.shell_call('rm %s/pkgconfig -rf' % self.temp_libdir())
def make(self, args, env=None, cwd=None, ignore_ret_code=False):
"""Invokes `make'.
Invokes `make' with the specified args, using self._build_env and
self._source_dir by default.
"""
if cwd is None:
cwd = self._source_dir
if env is None:
env = self._build_env
cmd = ['make'] + args
self.shell_call(' '.join(cmd), env=env, cwd=cwd,
ignore_ret_code=ignore_ret_code)
def make_install(self, args, **kwargs):
"""Invokes `make install'."""
self.make(['install'] + args, **kwargs)
def build_and_install(self):
"""Builds and installs the DSOs.
Builds the package with ./configure + make, installs it to a temporary
location, then moves the relevant files to their permanent location.
"""
configure_cmd = './configure --libdir=/%s/ %s' % (
self._libdir, self._extra_configure_flags)
self.shell_call(configure_cmd, env=self._build_env, cwd=self._source_dir)
# Some makefiles use BUILDROOT or INSTALL_ROOT instead of DESTDIR.
args = ['DESTDIR', 'BUILDROOT', 'INSTALL_ROOT']
make_args = ['%s=%s' % (name, self.temp_dir()) for name in args]
self.make(make_args)
self.make_install(make_args)
self.cleanup_after_install()
self.fix_rpaths(self.temp_libdir())
# Now move the contents of the temporary destdir to their final place.
# We only care for the contents of LIBDIR.
self.shell_call('cp %s/* %s/ -rdf' % (self.temp_libdir(),
self.dest_libdir()))
class LibcapBuilder(InstrumentedPackageBuilder):
def build_and_install(self):
# libcap2 doesn't have a configure script
build_args = ['CC', 'CXX', 'CFLAGS', 'CXXFLAGS', 'LDFLAGS']
make_args = [
'%s="%s"' % (name, self._build_env[name]) for name in build_args
]
self.make(make_args)
install_args = [
'DESTDIR=%s' % self.temp_dir(),
'lib=%s' % self._libdir,
# Skip a step that requires sudo.
'RAISE_SETFCAP=no'
]
self.make_install(install_args)
self.cleanup_after_install()
self.fix_rpaths(self.temp_libdir())
# Now move the contents of the temporary destdir to their final place.
# We only care for the contents of LIBDIR.
self.shell_call('cp %s/* %s/ -rdf' % (self.temp_libdir(),
self.dest_libdir()))
class Libpci3Builder(InstrumentedPackageBuilder):
def package_version(self):
"""Guesses libpci3 version from source directory name."""
dir_name = os.path.split(os.path.normpath(self._source_dir))[-1]
match = re.match('pciutils-(\d+\.\d+\.\d+)', dir_name)
if match is None:
raise Exception(
'Unable to guess libpci3 version from directory name: %s' % dir_name)
return match.group(1)
def temp_libdir(self):
# DSOs have to be picked up from <source_dir>/lib, since `make install'
# doesn't actualy install them anywhere.
return os.path.join(self._source_dir, 'lib')
def build_and_install(self):
# pciutils doesn't have a configure script
# This build process follows debian/rules.
self.shell_call('mkdir -p %s-udeb/usr/bin' % self.temp_dir())
build_args = ['CC', 'CXX', 'CFLAGS', 'CXXFLAGS', 'LDFLAGS']
make_args = [
'%s="%s"' % (name, self._build_env[name]) for name in build_args
]
make_args += [
'LIBDIR=/%s/' % self._libdir,
'PREFIX=/usr',
'SBINDIR=/usr/bin',
'IDSDIR=/usr/share/misc',
'SHARED=yes',
# pciutils-3.2.1 (Trusty) fails to build due to unresolved libkmod
# symbols. The binary package has no dependencies on libkmod, so it
# looks like it was actually built without libkmod support.
'LIBKMOD=no',
]
self.make(make_args)
# `make install' is not needed.
self.fix_rpaths(self.temp_libdir())
# Now install the DSOs to their final place.
self.shell_call(
'install -m 644 %s/libpci.so* %s' % (self.temp_libdir(),
self.dest_libdir()))
self.shell_call(
'ln -sf libpci.so.%s %s/libpci.so.3' % (self.package_version(),
self.dest_libdir()))
class NSSBuilder(InstrumentedPackageBuilder):
def build_and_install(self):
# NSS uses a build system that's different from configure/make/install. All
# flags must be passed as arguments to make.
make_args = [
# Do an optimized build.
'BUILD_OPT=1',
# CFLAGS/CXXFLAGS should not be used, as doing so overrides the flags in
# the makefile completely. The only way to append our flags is to tack
# them onto CC/CXX.
'CC="%s %s"' % (self._build_env['CC'], self._build_env['CFLAGS']),
'CXX="%s %s"' % (self._build_env['CXX'], self._build_env['CXXFLAGS']),
# We need to override ZDEFS_FLAG at least to avoid -Wl,-z,defs, which
# is not compatible with sanitizers. We also need some way to pass
# LDFLAGS without overriding the defaults. Conveniently, ZDEF_FLAG is
# always appended to link flags when building NSS on Linux, so we can
# just add our LDFLAGS here.
'ZDEFS_FLAG="-Wl,-z,nodefs %s"' % self._build_env['LDFLAGS'],
'NSPR_INCLUDE_DIR=/usr/include/nspr',
'NSPR_LIB_DIR=%s' % self.dest_libdir(),
'NSS_ENABLE_ECC=1'
]
if platform.architecture()[0] == '64bit':
make_args.append('USE_64=1')
# Make sure we don't override the default flags in the makefile.
for variable in ['CFLAGS', 'CXXFLAGS', 'LDFLAGS']:
del self._build_env[variable]
# Hardcoded paths.
temp_dir = os.path.join(self._source_dir, 'nss')
temp_libdir = os.path.join(temp_dir, 'lib')
# The build happens in <source_dir>/nss. Building fails after all
# the required DSOs have been built, so ignore the error.
self.make(make_args, cwd=temp_dir, ignore_ret_code=True)
self.fix_rpaths(temp_libdir)
# 'make install' is not supported. Copy the DSOs manually.
for (dirpath, dirnames, filenames) in os.walk(temp_libdir):
for filename in filenames:
if filename.endswith('.so'):
full_path = os.path.join(dirpath, filename)
if self._verbose:
print 'download_build_install.py: installing %s' % full_path
shutil.copy(full_path, self.dest_libdir())
class StubBuilder(InstrumentedPackageBuilder):
def download_build_install(self):
self._touch(os.path.join(self._destdir, '%s.txt' % self._package))
self.shell_call('mkdir -p %s' % self.dest_libdir())
self._touch(os.path.join(self.dest_libdir(), '%s.so.0' % self._package))
def _touch(self, path):
with open(path, 'w'):
pass
def main():
parser = argparse.ArgumentParser(
description='Download, build and install an instrumented package.')
parser.add_argument('-p', '--package', required=True)
parser.add_argument(
'-i', '--product-dir', default='.',
help='Relative path to the directory with chrome binaries')
parser.add_argument(
'-m', '--intermediate-dir', default='.',
help='Relative path to the directory for temporary build files')
parser.add_argument('--extra-configure-flags', default='')
parser.add_argument('--cflags', default='')
parser.add_argument('--ldflags', default='')
parser.add_argument('-s', '--sanitizer', required=True,
choices=['asan', 'msan', 'tsan'])
parser.add_argument('-v', '--verbose', action='store_true')
parser.add_argument('--cc')
parser.add_argument('--cxx')
parser.add_argument('--patch', default='')
# This should be a shell script to run before building specific libraries.
# This will be run after applying the patch above.
parser.add_argument('--pre-build', default='')
parser.add_argument('--build-method', default='destdir')
parser.add_argument('--sanitizer-blacklist', default='')
# The LIBDIR argument to configure/make.
parser.add_argument('--libdir', default='lib')
# Ignore all empty arguments because in several cases gyp passes them to the
# script, but ArgumentParser treats them as positional arguments instead of
# ignoring (and doesn't have such options).
args = parser.parse_args([arg for arg in sys.argv[1:] if len(arg) != 0])
# Clobber by default, unless the developer wants to hack on the package's
# source code.
clobber = \
(os.environ.get('INSTRUMENTED_LIBRARIES_NO_CLOBBER', '') != '1')
if args.build_method == 'destdir':
builder = InstrumentedPackageBuilder(args, clobber)
elif args.build_method == 'custom_nss':
builder = NSSBuilder(args, clobber)
elif args.build_method == 'custom_libcap':
builder = LibcapBuilder(args, clobber)
elif args.build_method == 'custom_libpci3':
builder = Libpci3Builder(args, clobber)
elif args.build_method == 'stub':
builder = StubBuilder(args, clobber)
else:
raise Exception('Unrecognized build method: %s' % args.build_method)
builder.download_build_install()
if __name__ == '__main__':
main()