summaryrefslogtreecommitdiffstats
path: root/third_party/instrumented_libraries/download_build_install.py
blob: 73ba2d08d94666076d9e947a4778e3d7e9730f82 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
#!/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 os
import platform
import re
import shlex
import shutil
import subprocess
import sys

SCRIPT_ABSOLUTE_PATH = os.path.dirname(os.path.abspath(__file__))

class ScopedChangeDirectory(object):
  """Changes current working directory and restores it back automatically."""

  def __init__(self, path):
    self.path = path
    self.old_path = ''

  def __enter__(self):
    self.old_path = os.getcwd()
    os.chdir(self.path)
    return self

  def __exit__(self, exc_type, exc_value, traceback):
    os.chdir(self.old_path)

def get_package_build_dependencies(package):
  command = 'apt-get -s build-dep %s | grep Inst | cut -d " " -f 2' % package
  command_result = subprocess.Popen(command, stdout=subprocess.PIPE,
                                    shell=True)
  if command_result.wait():
    raise Exception('Failed to determine build dependencies for %s' % package)
  build_dependencies = [l.strip() for l in command_result.stdout]
  return build_dependencies


def check_package_build_dependencies(package):
  build_dependencies = get_package_build_dependencies(package)
  if len(build_dependencies):
    print >> sys.stderr, 'Please, install build-dependencies for %s' % package
    print >> sys.stderr, 'One-liner for APT:'
    print >> sys.stderr, 'sudo apt-get -y --no-remove build-dep %s' % package
    sys.exit(1)


def shell_call(command, verbose=False, environment=None):
  """ Wrapper on subprocess.Popen

  Calls command with specific environment and verbosity using
  subprocess.Popen

  Args:
    command: Command to run in shell.
    verbose: If False, hides all stdout and stderr in case of successful build.
        Otherwise, always prints stdout and stderr.
    environment: Parameter 'env' for subprocess.Popen.

  Returns:
    None

  Raises:
    Exception: if return code after call is not zero.
  """
  child = subprocess.Popen(
      command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
      env=environment, shell=True)
  stdout, stderr = child.communicate()
  if verbose or child.returncode:
    print stdout
  if child.returncode:
    raise Exception('Failed to run: %s' % command)


def run_shell_commands(commands, verbose=False, environment=None):
  for command in commands:
    shell_call(command, verbose, environment)


def fix_rpaths(destdir):
  # TODO(earthdok): reimplement fix_rpaths.sh in Python.
  shell_call("%s/fix_rpaths.sh %s/lib" % (SCRIPT_ABSOLUTE_PATH, destdir))


def destdir_configure_make_install(parsed_arguments, environment,
                                   install_prefix):
  configure_command = './configure %s' % parsed_arguments.extra_configure_flags
  configure_command += ' --libdir=/lib/'
  # Installing to a temporary directory allows us to safely clean up the .la
  # files below.
  destdir = '%s/debian/instrumented_build' % os.getcwd()
  # Some makefiles use BUILDROOT instead of DESTDIR.
  make_command = 'make DESTDIR=%s BUILDROOT=%s' % (destdir, destdir)
  build_and_install_in_destdir = [
    configure_command,
    '%s -j%s' % (make_command, parsed_arguments.jobs),
    # Parallel install is flaky for some packages.
    '%s install -j1' % make_command,
    # Kill the .la files. They contain absolute paths, and will cause build
    # errors in dependent libraries.
    'rm %s/lib/*.la -f' % destdir
  ]
  run_shell_commands(build_and_install_in_destdir,
                     parsed_arguments.verbose, environment)
  fix_rpaths(destdir)
  shell_call(
      # Now move the contents of the temporary destdir to their final place.
      'cp %s/* %s/ -rdf' % (destdir, install_prefix),
      parsed_arguments.verbose, environment)


def nss_make_and_copy(parsed_arguments, environment, install_prefix):
  # 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.
  make_args.append('BUILD_OPT=1')
  # Set USE_64=1 on x86_64 systems.
  if platform.architecture()[0] == '64bit':
    make_args.append('USE_64=1')
  # Passing C(XX)FLAGS overrides the defaults, and EXTRA_C(XX)FLAGS is not
  # supported. Append our extra flags to CC/CXX.
  make_args.append('CC="%s %s"' % (environment['CC'], environment['CFLAGS']))
  make_args.append('CXX="%s %s"' %
                   (environment['CXX'], environment['CXXFLAGS']))
  # We need to override ZDEFS_FLAG at least to prevent -Wl,-z,defs.
  # Might as well use this to pass the linker flags, since ZDEF_FLAG is always
  # added during linking on Linux.
  make_args.append('ZDEFS_FLAG="-Wl,-z,nodefs %s"' % environment['LDFLAGS'])
  make_args.append('NSPR_INCLUDE_DIR=/usr/include/nspr')
  make_args.append('NSPR_LIB_DIR=%s/lib' % install_prefix)
  make_args.append('NSS_ENABLE_ECC=1')
  # Make sure we don't override the default flags.
  for variable in ['CFLAGS', 'CXXFLAGS', 'LDFLAGS']:
    del environment[variable]
  with ScopedChangeDirectory('nss') as cd_nss:
    # -j is not supported
    shell_call('make %s' % ' '.join(make_args), parsed_arguments.verbose,
               environment)
    fix_rpaths(os.getcwd())
    # 'make install' is not supported. Copy the DSOs manually.
    install_dir = '%s/lib/' % install_prefix
    for (dirpath, dirnames, filenames) in os.walk('./lib/'):
      for filename in filenames:
        if filename.endswith('.so'):
          full_path = os.path.join(dirpath, filename)
          if parsed_arguments.verbose:
            print 'download_build_install.py: installing %s' % full_path
          shutil.copy(full_path, install_dir)


def libcap2_make_install(parsed_arguments, environment, install_prefix):
  # libcap2 doesn't come with a configure script
  make_args = [
      '%s="%s"' % (name, environment[name])
      for name in['CC', 'CXX', 'CFLAGS', 'CXXFLAGS', 'LDFLAGS']]
  shell_call('make -j%s %s' % (parsed_arguments.jobs, ' '.join(make_args)),
             parsed_arguments.verbose, environment)
  destdir = '%s/debian/instrumented_build' % os.getcwd()
  install_args = [
      'DESTDIR=%s' % destdir,
      # Do not install in lib64/.
      'lib=lib',
      # Skip a step that requires sudo.
      'RAISE_SETFCAP=no'
  ]
  shell_call('make -j%s install %s' %
             (parsed_arguments.jobs, ' '.join(install_args)),
             parsed_arguments.verbose, environment)
  fix_rpaths(destdir)
  shell_call([
      # Now move the contents of the temporary destdir to their final place.
      'cp %s/* %s/ -rdf' % (destdir, install_prefix)],
                     parsed_arguments.verbose, environment)


def libpci3_make_install(parsed_arguments, environment, install_prefix):
  # pciutils doesn't have a configure script
  # This build script follows debian/rules.

  # Find out the package version. We'll use this when creating symlinks.
  dir_name = os.path.split(os.getcwd())[-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)
  version = match.group(1)

  # `make install' will create a "$(DESTDIR)-udeb" directory alongside destdir.
  # We don't want that in our product dir, so we use an intermediate directory.
  destdir = '%s/debian/pciutils' % os.getcwd()
  make_args = [
      '%s="%s"' % (name, environment[name])
      for name in['CC', 'CXX', 'CFLAGS', 'CXXFLAGS', 'LDFLAGS']]
  make_args.append('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.
  make_args.append('LIBKMOD=no')
  paths = [
      'LIBDIR=/lib/',
      'PREFIX=/usr',
      'SBINDIR=/usr/bin',
      'IDSDIR=/usr/share/misc',
  ]
  install_args = ['DESTDIR=%s' % destdir]
  run_shell_commands([
      'mkdir -p %s-udeb/usr/bin' % destdir,
      'make -j%s %s' % (parsed_arguments.jobs, ' '.join(make_args + paths)),
      'make -j%s %s install' % (
          parsed_arguments.jobs,
          ' '.join(install_args + paths))],
                     parsed_arguments.verbose, environment)
  fix_rpaths(destdir)
  # Now move the contents of the temporary destdir to their final place.
  run_shell_commands([
      'cp %s/* %s/ -rd' % (destdir, install_prefix),
      'install -m 644 lib/libpci.so* %s/lib/' % install_prefix,
      'ln -sf libpci.so.%s %s/lib/libpci.so.3' % (version, install_prefix)],
                     parsed_arguments.verbose, environment)


def build_and_install(parsed_arguments, environment, install_prefix):
  if parsed_arguments.build_method == 'destdir':
    destdir_configure_make_install(
        parsed_arguments, environment, install_prefix)
  elif parsed_arguments.build_method == 'custom_nss':
    nss_make_and_copy(parsed_arguments, environment, install_prefix)
  elif parsed_arguments.build_method == 'custom_libcap':
    libcap2_make_install(parsed_arguments, environment, install_prefix)
  elif parsed_arguments.build_method == 'custom_libpci3':
    libpci3_make_install(parsed_arguments, environment, install_prefix)
  else:
    raise Exception('Unrecognized build method: %s' %
                    parsed_arguments.build_method)


def unescape_flags(s):
  # GYP escapes the build flags as if they are going to be inserted directly
  # into the command line. Since we pass them via CFLAGS/LDFLAGS, we must drop
  # the double quotes accordingly. 
  return ' '.join(shlex.split(s))


def build_environment(parsed_arguments, product_directory, install_prefix):
  environment = os.environ.copy()
  # The CC/CXX environment variables take precedence over the command line
  # flags.
  if 'CC' not in environment and parsed_arguments.cc:
    environment['CC'] = parsed_arguments.cc
  if 'CXX' not in environment and parsed_arguments.cxx:
    environment['CXX'] = parsed_arguments.cxx

  cflags = unescape_flags(parsed_arguments.cflags)
  if parsed_arguments.sanitizer_blacklist:
    cflags += ' -fsanitize-blacklist=%s/%s' % (
        SCRIPT_ABSOLUTE_PATH,
        parsed_arguments.sanitizer_blacklist)
  environment['CFLAGS'] = cflags
  environment['CXXFLAGS'] = cflags

  ldflags = unescape_flags(parsed_arguments.ldflags)
  # Make sure the linker searches the instrumented libraries dir for
  # library dependencies.
  environment['LDFLAGS'] = '%s -L%s/lib' % (ldflags, install_prefix)

  if parsed_arguments.sanitizer_type == 'asan':
    # Do not report leaks during the build process.
    environment['ASAN_OPTIONS'] = '%s:detect_leaks=0' % \
        environment.get('ASAN_OPTIONS', '')

  # libappindicator1 needs this.
  environment['CSC'] = '/usr/bin/mono-csc'
  return environment



def download_build_install(parsed_arguments):
  product_directory = os.path.normpath('%s/%s' % (
      SCRIPT_ABSOLUTE_PATH,
      parsed_arguments.product_directory))

  install_prefix = '%s/instrumented_libraries/%s' % (
      product_directory,
      parsed_arguments.sanitizer_type)

  environment = build_environment(parsed_arguments, product_directory,
                                  install_prefix)

  package_directory = '%s/%s' % (parsed_arguments.intermediate_directory,
                                 parsed_arguments.package)

  # Clobber by default, unless the developer wants to hack on the package's
  # source code.
  clobber = (environment.get('INSTRUMENTED_LIBRARIES_NO_CLOBBER', '') != '1')

  download_source = True
  if os.path.exists(package_directory):
    if clobber:
      shell_call('rm -rf %s' % package_directory, parsed_arguments.verbose)
    else:
      download_source = False
  if download_source:
    os.makedirs(package_directory)

  with ScopedChangeDirectory(package_directory) as cd_package:
    if download_source:
      shell_call('apt-get source %s' % parsed_arguments.package,
                 parsed_arguments.verbose)
    # There should be exactly one subdirectory after downloading a package.
    subdirectories = [d for d in os.listdir('.') if os.path.isdir(d)]
    if len(subdirectories) != 1:
      raise Exception('apt-get source %s must create exactly one subdirectory.'
         % parsed_arguments.package)
    with ScopedChangeDirectory(subdirectories[0]):
      # Here we are in the package directory.
      if download_source:
        # Patch/run_before_build steps are only done once.
        if parsed_arguments.patch:
          shell_call(
              'patch -p1 -i %s/%s' %
              (os.path.relpath(cd_package.old_path),
               parsed_arguments.patch),
              parsed_arguments.verbose)
        if parsed_arguments.run_before_build:
          shell_call(
              '%s/%s' %
              (os.path.relpath(cd_package.old_path),
               parsed_arguments.run_before_build),
              parsed_arguments.verbose)
      try:
        build_and_install(parsed_arguments, environment, install_prefix)
      except Exception as exception:
        print exception
        print 'Failed to build package %s.' % parsed_arguments.package
        print ('Probably, some of its dependencies are not installed: %s' %
               ' '.join(get_package_build_dependencies(parsed_arguments.package)))
        sys.exit(1)

  # Touch a txt file to indicate package is installed.
  open('%s/%s.txt' % (install_prefix, parsed_arguments.package), 'w').close()

  # Remove downloaded package and generated temporary build files.
  # Failed builds intentionally skip this step, in order to aid in tracking down
  # build failures.
  if clobber:
    shell_call('rm -rf %s' % package_directory, parsed_arguments.verbose)

def main():
  argument_parser = argparse.ArgumentParser(
      description='Download, build and install instrumented package')

  argument_parser.add_argument('-j', '--jobs', type=int, default=1)
  argument_parser.add_argument('-p', '--package', required=True)
  argument_parser.add_argument(
      '-i', '--product-directory', default='.',
      help='Relative path to the directory with chrome binaries')
  argument_parser.add_argument(
      '-m', '--intermediate-directory', default='.',
      help='Relative path to the directory for temporary build files')
  argument_parser.add_argument('--extra-configure-flags', default='')
  argument_parser.add_argument('--cflags', default='')
  argument_parser.add_argument('--ldflags', default='')
  argument_parser.add_argument('-s', '--sanitizer-type', required=True,
                               choices=['asan', 'msan', 'tsan'])
  argument_parser.add_argument('-v', '--verbose', action='store_true')
  argument_parser.add_argument('--check-build-deps', action='store_true')
  argument_parser.add_argument('--cc')
  argument_parser.add_argument('--cxx')
  argument_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.
  argument_parser.add_argument('--run-before-build', default='')
  argument_parser.add_argument('--build-method', default='destdir')
  argument_parser.add_argument('--sanitizer-blacklist', default='')

  # 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).
  parsed_arguments = argument_parser.parse_args(
      [arg for arg in sys.argv[1:] if len(arg) != 0])
  # Ensure current working directory is this script directory.
  os.chdir(SCRIPT_ABSOLUTE_PATH)
  # Ensure all build dependencies are installed.
  if parsed_arguments.check_build_deps:
    check_package_build_dependencies(parsed_arguments.package)

  download_build_install(parsed_arguments)


if __name__ == '__main__':
  main()