| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/python | |
| 2 # Copyright 2013 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 """Downloads, builds (with instrumentation) and installs shared libraries.""" | |
| 7 | |
| 8 import argparse | |
| 9 import os | |
| 10 import platform | |
| 11 import re | |
| 12 import shlex | |
| 13 import shutil | |
| 14 import subprocess | |
| 15 import sys | |
| 16 | |
| 17 SCRIPT_ABSOLUTE_PATH = os.path.dirname(os.path.abspath(__file__)) | |
| 18 | |
| 19 def unescape_flags(s): | |
| 20 """Un-escapes build flags received from GYP. | |
| 21 | |
| 22 GYP escapes build flags as if they are to be inserted directly into a command | |
| 23 line, wrapping each flag in double quotes. When flags are passed via | |
| 24 CFLAGS/LDFLAGS instead, double quotes must be dropped. | |
| 25 """ | |
| 26 return ' '.join(shlex.split(s)) | |
| 27 | |
| 28 | |
| 29 def real_path(path_relative_to_script): | |
| 30 """Returns the absolute path to a file. | |
| 31 | |
| 32 GYP generates paths relative to the location of the .gyp file, which coincides | |
| 33 with the location of this script. This function converts them to absolute | |
| 34 paths. | |
| 35 """ | |
| 36 return os.path.realpath(os.path.join(SCRIPT_ABSOLUTE_PATH, | |
| 37 path_relative_to_script)) | |
| 38 | |
| 39 | |
| 40 class InstrumentedPackageBuilder(object): | |
| 41 """Checks out and builds a single instrumented package.""" | |
| 42 def __init__(self, args, clobber): | |
| 43 self._cc = args.cc | |
| 44 self._cxx = args.cxx | |
| 45 self._extra_configure_flags = args.extra_configure_flags | |
| 46 self._jobs = args.jobs | |
| 47 self._libdir = args.libdir | |
| 48 self._package = args.package | |
| 49 self._patch = real_path(args.patch) if args.patch else None | |
| 50 self._run_before_build = \ | |
| 51 real_path(args.run_before_build) if args.run_before_build else None | |
| 52 self._sanitizer = args.sanitizer | |
| 53 self._verbose = args.verbose | |
| 54 self._clobber = clobber | |
| 55 self._working_dir = os.path.join( | |
| 56 real_path(args.intermediate_dir), self._package, '') | |
| 57 | |
| 58 product_dir = real_path(args.product_dir) | |
| 59 self._destdir = os.path.join( | |
| 60 product_dir, 'instrumented_libraries', self._sanitizer) | |
| 61 self._source_archives_dir = os.path.join( | |
| 62 product_dir, 'instrumented_libraries', 'sources', self._package) | |
| 63 | |
| 64 self._cflags = unescape_flags(args.cflags) | |
| 65 if args.sanitizer_blacklist: | |
| 66 blacklist_file = real_path(args.sanitizer_blacklist) | |
| 67 self._cflags += ' -fsanitize-blacklist=%s' % blacklist_file | |
| 68 | |
| 69 self._ldflags = unescape_flags(args.ldflags) | |
| 70 | |
| 71 self.init_build_env() | |
| 72 | |
| 73 # Initialized later. | |
| 74 self._source_dir = None | |
| 75 self._source_archives = None | |
| 76 | |
| 77 def init_build_env(self): | |
| 78 self._build_env = os.environ.copy() | |
| 79 | |
| 80 self._build_env['CC'] = self._cc | |
| 81 self._build_env['CXX'] = self._cxx | |
| 82 | |
| 83 self._build_env['CFLAGS'] = self._cflags | |
| 84 self._build_env['CXXFLAGS'] = self._cflags | |
| 85 self._build_env['LDFLAGS'] = self._ldflags | |
| 86 | |
| 87 if self._sanitizer == 'asan': | |
| 88 # Do not report leaks during the build process. | |
| 89 self._build_env['ASAN_OPTIONS'] = \ | |
| 90 '%s:detect_leaks=0' % self._build_env.get('ASAN_OPTIONS', '') | |
| 91 | |
| 92 # libappindicator1 needs this. | |
| 93 self._build_env['CSC'] = '/usr/bin/mono-csc' | |
| 94 | |
| 95 def shell_call(self, command, env=None, cwd=None): | |
| 96 """Wrapper around subprocess.Popen(). | |
| 97 | |
| 98 Calls command with specific environment and verbosity using | |
| 99 subprocess.Popen(). | |
| 100 """ | |
| 101 child = subprocess.Popen( | |
| 102 command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, | |
| 103 env=env, shell=True, cwd=cwd) | |
| 104 stdout, stderr = child.communicate() | |
| 105 if self._verbose or child.returncode: | |
| 106 print stdout | |
| 107 if child.returncode: | |
| 108 raise Exception('Failed to run: %s' % command) | |
| 109 | |
| 110 def maybe_download_source(self): | |
| 111 """Checks out the source code (if needed). | |
| 112 | |
| 113 Checks out the source code for the package, if required (i.e. unless running | |
| 114 in no-clobber mode). Initializes self._source_dir and self._source_archives. | |
| 115 """ | |
| 116 get_fresh_source = self._clobber or not os.path.exists(self._working_dir) | |
| 117 if get_fresh_source: | |
| 118 self.shell_call('rm -rf %s' % self._working_dir) | |
| 119 os.makedirs(self._working_dir) | |
| 120 self.shell_call('apt-get source %s' % self._package, | |
| 121 cwd=self._working_dir) | |
| 122 | |
| 123 (dirpath, dirnames, filenames) = os.walk(self._working_dir).next() | |
| 124 | |
| 125 if len(dirnames) != 1: | |
| 126 raise Exception( | |
| 127 '`apt-get source %s\' must create exactly one subdirectory.' | |
| 128 % self._package) | |
| 129 self._source_dir = os.path.join(dirpath, dirnames[0], '') | |
| 130 | |
| 131 if len(filenames) == 0: | |
| 132 raise Exception('Can\'t find source archives after `apt-get source %s\'.' | |
| 133 % self._package) | |
| 134 self._source_archives = \ | |
| 135 [os.path.join(dirpath, filename) for filename in filenames] | |
| 136 | |
| 137 return get_fresh_source | |
| 138 | |
| 139 def patch_source(self): | |
| 140 if self._patch: | |
| 141 self.shell_call('patch -p1 -i %s' % self._patch, cwd=self._source_dir) | |
| 142 if self._run_before_build: | |
| 143 self.shell_call(self._run_before_build, cwd=self._source_dir) | |
| 144 | |
| 145 def copy_source_archives(self): | |
| 146 """Copies the downloaded source archives to the output dir. | |
| 147 | |
| 148 For license compliance purposes, every Chromium build that includes | |
| 149 instrumented libraries must include their full source code. | |
| 150 """ | |
| 151 self.shell_call('rm -rf %s' % self._source_archives_dir) | |
| 152 os.makedirs(self._source_archives_dir) | |
| 153 for filename in self._source_archives: | |
| 154 shutil.copy(filename, self._source_archives_dir) | |
| 155 if self._patch: | |
| 156 shutil.copy(self._patch, self._source_archives_dir) | |
| 157 | |
| 158 def download_build_install(self): | |
| 159 got_fresh_source = self.maybe_download_source() | |
| 160 if got_fresh_source: | |
| 161 self.patch_source() | |
| 162 self.copy_source_archives() | |
| 163 | |
| 164 self.shell_call('mkdir -p %s' % self.dest_libdir()) | |
| 165 | |
| 166 try: | |
| 167 self.build_and_install() | |
| 168 except Exception as exception: | |
| 169 print 'ERROR: Failed to build package %s. Have you run ' \ | |
| 170 'src/third_party/instrumented_libraries/install-build-deps.sh?' % \ | |
| 171 self._package | |
| 172 print | |
| 173 raise | |
| 174 | |
| 175 # Touch a text file to indicate package is installed. | |
| 176 stamp_file = os.path.join(self._destdir, '%s.txt' % self._package) | |
| 177 open(stamp_file, 'w').close() | |
| 178 | |
| 179 # Remove downloaded package and generated temporary build files. Failed | |
| 180 # builds intentionally skip this step to help debug build failures. | |
| 181 if self._clobber: | |
| 182 self.shell_call('rm -rf %s' % self._working_dir) | |
| 183 | |
| 184 def fix_rpaths(self, directory): | |
| 185 # TODO(earthdok): reimplement fix_rpaths.sh in Python. | |
| 186 script = real_path('fix_rpaths.sh') | |
| 187 self.shell_call("%s %s" % (script, directory)) | |
| 188 | |
| 189 def temp_dir(self): | |
| 190 """Returns the directory which will be passed to `make install'.""" | |
| 191 return os.path.join(self._source_dir, 'debian', 'instrumented_build') | |
| 192 | |
| 193 def temp_libdir(self): | |
| 194 """Returns the directory under temp_dir() containing the DSOs.""" | |
| 195 return os.path.join(self.temp_dir(), self._libdir) | |
| 196 | |
| 197 def dest_libdir(self): | |
| 198 """Returns the final location of the DSOs.""" | |
| 199 return os.path.join(self._destdir, self._libdir) | |
| 200 | |
| 201 def make(self, args, jobs=None, env=None, cwd=None): | |
| 202 """Invokes `make'. | |
| 203 | |
| 204 Invokes `make' with the specified args, using self._build_env and | |
| 205 self._source_dir by default. | |
| 206 """ | |
| 207 if jobs is None: | |
| 208 jobs = self._jobs | |
| 209 if cwd is None: | |
| 210 cwd = self._source_dir | |
| 211 if env is None: | |
| 212 env = self._build_env | |
| 213 cmd = ['make', '-j%s' % jobs] + args | |
| 214 self.shell_call(' '.join(cmd), env=env, cwd=cwd) | |
| 215 | |
| 216 def make_install(self, args, **kwargs): | |
| 217 """Invokes `make install'.""" | |
| 218 self.make(['install'] + args, **kwargs) | |
| 219 | |
| 220 def build_and_install(self): | |
| 221 """Builds and installs the DSOs. | |
| 222 | |
| 223 Builds the package with ./configure + make, installs it to a temporary | |
| 224 location, then moves the relevant files to their permanent location. | |
| 225 """ | |
| 226 configure_cmd = './configure --libdir=/%s/ %s' % ( | |
| 227 self._libdir, self._extra_configure_flags) | |
| 228 self.shell_call(configure_cmd, env=self._build_env, cwd=self._source_dir) | |
| 229 | |
| 230 # Some makefiles use BUILDROOT or INSTALL_ROOT instead of DESTDIR. | |
| 231 args = ['DESTDIR', 'BUILDROOT', 'INSTALL_ROOT'] | |
| 232 make_args = ['%s=%s' % (name, self.temp_dir()) for name in args] | |
| 233 self.make(make_args) | |
| 234 | |
| 235 # Some packages don't support parallel install. Use -j1 always. | |
| 236 self.make_install(make_args, jobs=1) | |
| 237 | |
| 238 # .la files are not needed, nuke them. | |
| 239 self.shell_call('rm %s/*.la -f' % self.temp_libdir()) | |
| 240 | |
| 241 self.fix_rpaths(self.temp_libdir()) | |
| 242 | |
| 243 # Now move the contents of the temporary destdir to their final place. | |
| 244 # We only care for the contents of LIBDIR. | |
| 245 self.shell_call('cp %s/* %s/ -rdf' % (self.temp_libdir(), | |
| 246 self.dest_libdir())) | |
| 247 | |
| 248 | |
| 249 class LibcapBuilder(InstrumentedPackageBuilder): | |
| 250 def build_and_install(self): | |
| 251 # libcap2 doesn't have a configure script | |
| 252 build_args = ['CC', 'CXX', 'CFLAGS', 'CXXFLAGS', 'LDFLAGS'] | |
| 253 make_args = [ | |
| 254 '%s="%s"' % (name, self._build_env[name]) for name in build_args | |
| 255 ] | |
| 256 self.make(make_args) | |
| 257 | |
| 258 install_args = [ | |
| 259 'DESTDIR=%s' % self.temp_dir(), | |
| 260 'lib=%s' % self._libdir, | |
| 261 # Skip a step that requires sudo. | |
| 262 'RAISE_SETFCAP=no' | |
| 263 ] | |
| 264 self.make_install(install_args) | |
| 265 | |
| 266 self.fix_rpaths(self.temp_libdir()) | |
| 267 | |
| 268 # Now move the contents of the temporary destdir to their final place. | |
| 269 # We only care for the contents of LIBDIR. | |
| 270 self.shell_call('cp %s/* %s/ -rdf' % (self.temp_libdir(), | |
| 271 self.dest_libdir())) | |
| 272 | |
| 273 | |
| 274 class Libpci3Builder(InstrumentedPackageBuilder): | |
| 275 def package_version(self): | |
| 276 """Guesses libpci3 version from source directory name.""" | |
| 277 dir_name = os.path.split(os.path.normpath(self._source_dir))[-1] | |
| 278 match = re.match('pciutils-(\d+\.\d+\.\d+)', dir_name) | |
| 279 if match is None: | |
| 280 raise Exception( | |
| 281 'Unable to guess libpci3 version from directory name: %s' % dir_name) | |
| 282 return match.group(1) | |
| 283 | |
| 284 def temp_libdir(self): | |
| 285 # DSOs have to be picked up from <source_dir>/lib, since `make install' | |
| 286 # doesn't actualy install them anywhere. | |
| 287 return os.path.join(self._source_dir, 'lib') | |
| 288 | |
| 289 def build_and_install(self): | |
| 290 # pciutils doesn't have a configure script | |
| 291 # This build process follows debian/rules. | |
| 292 self.shell_call('mkdir -p %s-udeb/usr/bin' % self.temp_dir()) | |
| 293 | |
| 294 build_args = ['CC', 'CXX', 'CFLAGS', 'CXXFLAGS', 'LDFLAGS'] | |
| 295 make_args = [ | |
| 296 '%s="%s"' % (name, self._build_env[name]) for name in build_args | |
| 297 ] | |
| 298 make_args += [ | |
| 299 'LIBDIR=/%s/' % self._libdir, | |
| 300 'PREFIX=/usr', | |
| 301 'SBINDIR=/usr/bin', | |
| 302 'IDSDIR=/usr/share/misc', | |
| 303 'SHARED=yes', | |
| 304 # pciutils-3.2.1 (Trusty) fails to build due to unresolved libkmod | |
| 305 # symbols. The binary package has no dependencies on libkmod, so it | |
| 306 # looks like it was actually built without libkmod support. | |
| 307 'LIBKMOD=no', | |
| 308 ] | |
| 309 self.make(make_args) | |
| 310 | |
| 311 # `make install' is not needed. | |
| 312 self.fix_rpaths(self.temp_libdir()) | |
| 313 | |
| 314 # Now install the DSOs to their final place. | |
| 315 self.shell_call( | |
| 316 'install -m 644 %s/libpci.so* %s' % (self.temp_libdir(), | |
| 317 self.dest_libdir())) | |
| 318 self.shell_call( | |
| 319 'ln -sf libpci.so.%s %s/libpci.so.3' % (self.package_version(), | |
| 320 self.dest_libdir())) | |
| 321 | |
| 322 | |
| 323 class NSSBuilder(InstrumentedPackageBuilder): | |
| 324 def build_and_install(self): | |
| 325 # NSS uses a build system that's different from configure/make/install. All | |
| 326 # flags must be passed as arguments to make. | |
| 327 make_args = [ | |
| 328 # Do an optimized build. | |
| 329 'BUILD_OPT=1', | |
| 330 # CFLAGS/CXXFLAGS should not be used, as doing so overrides the flags in | |
| 331 # the makefile completely. The only way to append our flags is to tack | |
| 332 # them onto CC/CXX. | |
| 333 'CC="%s %s"' % (self._build_env['CC'], self._build_env['CFLAGS']), | |
| 334 'CXX="%s %s"' % (self._build_env['CXX'], self._build_env['CXXFLAGS']), | |
| 335 # We need to override ZDEFS_FLAG at least to avoid -Wl,-z,defs, which | |
| 336 # is not compatible with sanitizers. We also need some way to pass | |
| 337 # LDFLAGS without overriding the defaults. Conveniently, ZDEF_FLAG is | |
| 338 # always appended to link flags when building NSS on Linux, so we can | |
| 339 # just add our LDFLAGS here. | |
| 340 'ZDEFS_FLAG="-Wl,-z,nodefs %s"' % self._build_env['LDFLAGS'], | |
| 341 'NSPR_INCLUDE_DIR=/usr/include/nspr', | |
| 342 'NSPR_LIB_DIR=%s' % self.dest_libdir(), | |
| 343 'NSS_ENABLE_ECC=1' | |
| 344 ] | |
| 345 if platform.architecture()[0] == '64bit': | |
| 346 make_args.append('USE_64=1') | |
| 347 | |
| 348 # Make sure we don't override the default flags in the makefile. | |
| 349 for variable in ['CFLAGS', 'CXXFLAGS', 'LDFLAGS']: | |
| 350 del self._build_env[variable] | |
| 351 | |
| 352 # Hardcoded paths. | |
| 353 temp_dir = os.path.join(self._source_dir, 'nss') | |
| 354 temp_libdir = os.path.join(temp_dir, 'lib') | |
| 355 | |
| 356 # Parallel build is not supported. Also, the build happens in | |
| 357 # <source_dir>/nss. | |
| 358 self.make(make_args, jobs=1, cwd=temp_dir) | |
| 359 | |
| 360 self.fix_rpaths(temp_libdir) | |
| 361 | |
| 362 # 'make install' is not supported. Copy the DSOs manually. | |
| 363 for (dirpath, dirnames, filenames) in os.walk(temp_libdir): | |
| 364 for filename in filenames: | |
| 365 if filename.endswith('.so'): | |
| 366 full_path = os.path.join(dirpath, filename) | |
| 367 if self._verbose: | |
| 368 print 'download_build_install.py: installing %s' % full_path | |
| 369 shutil.copy(full_path, self.dest_libdir()) | |
| 370 | |
| 371 | |
| 372 def main(): | |
| 373 parser = argparse.ArgumentParser( | |
| 374 description='Download, build and install an instrumented package.') | |
| 375 | |
| 376 parser.add_argument('-j', '--jobs', type=int, default=1) | |
| 377 parser.add_argument('-p', '--package', required=True) | |
| 378 parser.add_argument( | |
| 379 '-i', '--product-dir', default='.', | |
| 380 help='Relative path to the directory with chrome binaries') | |
| 381 parser.add_argument( | |
| 382 '-m', '--intermediate-dir', default='.', | |
| 383 help='Relative path to the directory for temporary build files') | |
| 384 parser.add_argument('--extra-configure-flags', default='') | |
| 385 parser.add_argument('--cflags', default='') | |
| 386 parser.add_argument('--ldflags', default='') | |
| 387 parser.add_argument('-s', '--sanitizer', required=True, | |
| 388 choices=['asan', 'msan', 'tsan']) | |
| 389 parser.add_argument('-v', '--verbose', action='store_true') | |
| 390 parser.add_argument('--cc') | |
| 391 parser.add_argument('--cxx') | |
| 392 parser.add_argument('--patch', default='') | |
| 393 # This should be a shell script to run before building specific libraries. | |
| 394 # This will be run after applying the patch above. | |
| 395 parser.add_argument('--run-before-build', default='') | |
| 396 parser.add_argument('--build-method', default='destdir') | |
| 397 parser.add_argument('--sanitizer-blacklist', default='') | |
| 398 # The LIBDIR argument to configure/make. | |
| 399 parser.add_argument('--libdir', default='lib') | |
| 400 | |
| 401 # Ignore all empty arguments because in several cases gyp passes them to the | |
| 402 # script, but ArgumentParser treats them as positional arguments instead of | |
| 403 # ignoring (and doesn't have such options). | |
| 404 args = parser.parse_args([arg for arg in sys.argv[1:] if len(arg) != 0]) | |
| 405 | |
| 406 # Clobber by default, unless the developer wants to hack on the package's | |
| 407 # source code. | |
| 408 clobber = \ | |
| 409 (os.environ.get('INSTRUMENTED_LIBRARIES_NO_CLOBBER', '') != '1') | |
| 410 | |
| 411 if args.build_method == 'destdir': | |
| 412 builder = InstrumentedPackageBuilder(args, clobber) | |
| 413 elif args.build_method == 'custom_nss': | |
| 414 builder = NSSBuilder(args, clobber) | |
| 415 elif args.build_method == 'custom_libcap': | |
| 416 builder = LibcapBuilder(args, clobber) | |
| 417 elif args.build_method == 'custom_libpci3': | |
| 418 builder = Libpci3Builder(args, clobber) | |
| 419 else: | |
| 420 raise Exception('Unrecognized build method: %s' % args.build_method) | |
| 421 | |
| 422 builder.download_build_install() | |
| 423 | |
| 424 if __name__ == '__main__': | |
| 425 main() | |
| OLD | NEW |