mac: Fix DownloadItemController unit tests on Yosemite.
[chromium-blink-merge.git] / third_party / instrumented_libraries / download_build_install.py
blob7452043590985d7a3fe0c3ea08ebfece1d13d1e8
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.
6 """Downloads, builds (with instrumentation) and installs shared libraries."""
8 import argparse
9 import os
10 import platform
11 import re
12 import shlex
13 import shutil
14 import subprocess
15 import sys
17 SCRIPT_ABSOLUTE_PATH = os.path.dirname(os.path.abspath(__file__))
19 class ScopedChangeDirectory(object):
20 """Changes current working directory and restores it back automatically."""
22 def __init__(self, path):
23 self.path = path
24 self.old_path = ''
26 def __enter__(self):
27 self.old_path = os.getcwd()
28 os.chdir(self.path)
29 return self
31 def __exit__(self, exc_type, exc_value, traceback):
32 os.chdir(self.old_path)
34 def get_package_build_dependencies(package):
35 command = 'apt-get -s build-dep %s | grep Inst | cut -d " " -f 2' % package
36 command_result = subprocess.Popen(command, stdout=subprocess.PIPE,
37 shell=True)
38 if command_result.wait():
39 raise Exception('Failed to determine build dependencies for %s' % package)
40 build_dependencies = [l.strip() for l in command_result.stdout]
41 return build_dependencies
44 def check_package_build_dependencies(package):
45 build_dependencies = get_package_build_dependencies(package)
46 if len(build_dependencies):
47 print >> sys.stderr, 'Please, install build-dependencies for %s' % package
48 print >> sys.stderr, 'One-liner for APT:'
49 print >> sys.stderr, 'sudo apt-get -y --no-remove build-dep %s' % package
50 sys.exit(1)
53 def shell_call(command, verbose=False, environment=None):
54 """ Wrapper on subprocess.Popen
56 Calls command with specific environment and verbosity using
57 subprocess.Popen
59 Args:
60 command: Command to run in shell.
61 verbose: If False, hides all stdout and stderr in case of successful build.
62 Otherwise, always prints stdout and stderr.
63 environment: Parameter 'env' for subprocess.Popen.
65 Returns:
66 None
68 Raises:
69 Exception: if return code after call is not zero.
70 """
71 child = subprocess.Popen(
72 command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
73 env=environment, shell=True)
74 stdout, stderr = child.communicate()
75 if verbose or child.returncode:
76 print stdout
77 if child.returncode:
78 raise Exception('Failed to run: %s' % command)
81 def run_shell_commands(commands, verbose=False, environment=None):
82 for command in commands:
83 shell_call(command, verbose, environment)
86 def fix_rpaths(destdir):
87 # TODO(earthdok): reimplement fix_rpaths.sh in Python.
88 shell_call("%s/fix_rpaths.sh %s/lib" % (SCRIPT_ABSOLUTE_PATH, destdir))
91 def destdir_configure_make_install(parsed_arguments, environment,
92 install_prefix):
93 configure_command = './configure %s' % parsed_arguments.extra_configure_flags
94 configure_command += ' --libdir=/lib/'
95 # Installing to a temporary directory allows us to safely clean up the .la
96 # files below.
97 destdir = '%s/debian/instrumented_build' % os.getcwd()
98 # Some makefiles use BUILDROOT or INSTALL_ROOT instead of DESTDIR.
99 make_command = 'make DESTDIR=%s BUILDROOT=%s INSTALL_ROOT=%s' % (destdir,
100 destdir,
101 destdir)
102 build_and_install_in_destdir = [
103 configure_command,
104 '%s -j%s' % (make_command, parsed_arguments.jobs),
105 # Parallel install is flaky for some packages.
106 '%s install -j1' % make_command,
107 # Kill the .la files. They contain absolute paths, and will cause build
108 # errors in dependent libraries.
109 'rm %s/lib/*.la -f' % destdir
111 run_shell_commands(build_and_install_in_destdir,
112 parsed_arguments.verbose, environment)
113 fix_rpaths(destdir)
114 shell_call(
115 # Now move the contents of the temporary destdir to their final place.
116 'cp %s/* %s/ -rdf' % (destdir, install_prefix),
117 parsed_arguments.verbose, environment)
120 def nss_make_and_copy(parsed_arguments, environment, install_prefix):
121 # NSS uses a build system that's different from configure/make/install. All
122 # flags must be passed as arguments to make.
123 make_args = []
124 # Do an optimized build.
125 make_args.append('BUILD_OPT=1')
126 # Set USE_64=1 on x86_64 systems.
127 if platform.architecture()[0] == '64bit':
128 make_args.append('USE_64=1')
129 # Passing C(XX)FLAGS overrides the defaults, and EXTRA_C(XX)FLAGS is not
130 # supported. Append our extra flags to CC/CXX.
131 make_args.append('CC="%s %s"' % (environment['CC'], environment['CFLAGS']))
132 make_args.append('CXX="%s %s"' %
133 (environment['CXX'], environment['CXXFLAGS']))
134 # We need to override ZDEFS_FLAG at least to prevent -Wl,-z,defs.
135 # Might as well use this to pass the linker flags, since ZDEF_FLAG is always
136 # added during linking on Linux.
137 make_args.append('ZDEFS_FLAG="-Wl,-z,nodefs %s"' % environment['LDFLAGS'])
138 make_args.append('NSPR_INCLUDE_DIR=/usr/include/nspr')
139 make_args.append('NSPR_LIB_DIR=%s/lib' % install_prefix)
140 make_args.append('NSS_ENABLE_ECC=1')
141 # Make sure we don't override the default flags.
142 for variable in ['CFLAGS', 'CXXFLAGS', 'LDFLAGS']:
143 del environment[variable]
144 with ScopedChangeDirectory('nss') as cd_nss:
145 # -j is not supported
146 shell_call('make %s' % ' '.join(make_args), parsed_arguments.verbose,
147 environment)
148 fix_rpaths(os.getcwd())
149 # 'make install' is not supported. Copy the DSOs manually.
150 install_dir = '%s/lib/' % install_prefix
151 for (dirpath, dirnames, filenames) in os.walk('./lib/'):
152 for filename in filenames:
153 if filename.endswith('.so'):
154 full_path = os.path.join(dirpath, filename)
155 if parsed_arguments.verbose:
156 print 'download_build_install.py: installing %s' % full_path
157 shutil.copy(full_path, install_dir)
160 def libcap2_make_install(parsed_arguments, environment, install_prefix):
161 # libcap2 doesn't come with a configure script
162 make_args = [
163 '%s="%s"' % (name, environment[name])
164 for name in['CC', 'CXX', 'CFLAGS', 'CXXFLAGS', 'LDFLAGS']]
165 shell_call('make -j%s %s' % (parsed_arguments.jobs, ' '.join(make_args)),
166 parsed_arguments.verbose, environment)
167 destdir = '%s/debian/instrumented_build' % os.getcwd()
168 install_args = [
169 'DESTDIR=%s' % destdir,
170 # Do not install in lib64/.
171 'lib=lib',
172 # Skip a step that requires sudo.
173 'RAISE_SETFCAP=no'
175 shell_call('make -j%s install %s' %
176 (parsed_arguments.jobs, ' '.join(install_args)),
177 parsed_arguments.verbose, environment)
178 fix_rpaths(destdir)
179 shell_call([
180 # Now move the contents of the temporary destdir to their final place.
181 'cp %s/* %s/ -rdf' % (destdir, install_prefix)],
182 parsed_arguments.verbose, environment)
185 def libpci3_make_install(parsed_arguments, environment, install_prefix):
186 # pciutils doesn't have a configure script
187 # This build script follows debian/rules.
189 # Find out the package version. We'll use this when creating symlinks.
190 dir_name = os.path.split(os.getcwd())[-1]
191 match = re.match('pciutils-(\d+\.\d+\.\d+)', dir_name)
192 if match is None:
193 raise Exception(
194 'Unable to guess libpci3 version from directory name: %s' % dir_name)
195 version = match.group(1)
197 # `make install' will create a "$(DESTDIR)-udeb" directory alongside destdir.
198 # We don't want that in our product dir, so we use an intermediate directory.
199 destdir = '%s/debian/pciutils' % os.getcwd()
200 make_args = [
201 '%s="%s"' % (name, environment[name])
202 for name in['CC', 'CXX', 'CFLAGS', 'CXXFLAGS', 'LDFLAGS']]
203 make_args.append('SHARED=yes')
204 # pciutils-3.2.1 (Trusty) fails to build due to unresolved libkmod symbols.
205 # The binary package has no dependencies on libkmod, so it looks like it was
206 # actually built without libkmod support.
207 make_args.append('LIBKMOD=no')
208 paths = [
209 'LIBDIR=/lib/',
210 'PREFIX=/usr',
211 'SBINDIR=/usr/bin',
212 'IDSDIR=/usr/share/misc',
214 install_args = ['DESTDIR=%s' % destdir]
215 run_shell_commands([
216 'mkdir -p %s-udeb/usr/bin' % destdir,
217 'make -j%s %s' % (parsed_arguments.jobs, ' '.join(make_args + paths)),
218 'make -j%s %s install' % (
219 parsed_arguments.jobs,
220 ' '.join(install_args + paths))],
221 parsed_arguments.verbose, environment)
222 fix_rpaths(destdir)
223 # Now move the contents of the temporary destdir to their final place.
224 run_shell_commands([
225 'cp %s/* %s/ -rd' % (destdir, install_prefix),
226 'install -m 644 lib/libpci.so* %s/lib/' % install_prefix,
227 'ln -sf libpci.so.%s %s/lib/libpci.so.3' % (version, install_prefix)],
228 parsed_arguments.verbose, environment)
231 def build_and_install(parsed_arguments, environment, install_prefix):
232 if parsed_arguments.build_method == 'destdir':
233 destdir_configure_make_install(
234 parsed_arguments, environment, install_prefix)
235 elif parsed_arguments.build_method == 'custom_nss':
236 nss_make_and_copy(parsed_arguments, environment, install_prefix)
237 elif parsed_arguments.build_method == 'custom_libcap':
238 libcap2_make_install(parsed_arguments, environment, install_prefix)
239 elif parsed_arguments.build_method == 'custom_libpci3':
240 libpci3_make_install(parsed_arguments, environment, install_prefix)
241 else:
242 raise Exception('Unrecognized build method: %s' %
243 parsed_arguments.build_method)
246 def unescape_flags(s):
247 # GYP escapes the build flags as if they are going to be inserted directly
248 # into the command line. Since we pass them via CFLAGS/LDFLAGS, we must drop
249 # the double quotes accordingly.
250 return ' '.join(shlex.split(s))
253 def build_environment(parsed_arguments, product_directory, install_prefix):
254 environment = os.environ.copy()
255 # The CC/CXX environment variables take precedence over the command line
256 # flags.
257 if 'CC' not in environment and parsed_arguments.cc:
258 environment['CC'] = parsed_arguments.cc
259 if 'CXX' not in environment and parsed_arguments.cxx:
260 environment['CXX'] = parsed_arguments.cxx
262 cflags = unescape_flags(parsed_arguments.cflags)
263 if parsed_arguments.sanitizer_blacklist:
264 cflags += ' -fsanitize-blacklist=%s/%s' % (
265 SCRIPT_ABSOLUTE_PATH,
266 parsed_arguments.sanitizer_blacklist)
267 environment['CFLAGS'] = cflags
268 environment['CXXFLAGS'] = cflags
270 ldflags = unescape_flags(parsed_arguments.ldflags)
271 # Make sure the linker searches the instrumented libraries dir for
272 # library dependencies.
273 environment['LDFLAGS'] = '%s -L%s/lib' % (ldflags, install_prefix)
275 if parsed_arguments.sanitizer_type == 'asan':
276 # Do not report leaks during the build process.
277 environment['ASAN_OPTIONS'] = '%s:detect_leaks=0' % \
278 environment.get('ASAN_OPTIONS', '')
280 # libappindicator1 needs this.
281 environment['CSC'] = '/usr/bin/mono-csc'
282 return environment
286 def download_build_install(parsed_arguments):
287 product_directory = os.path.normpath('%s/%s' % (
288 SCRIPT_ABSOLUTE_PATH,
289 parsed_arguments.product_directory))
291 install_prefix = '%s/instrumented_libraries/%s' % (
292 product_directory,
293 parsed_arguments.sanitizer_type)
295 environment = build_environment(parsed_arguments, product_directory,
296 install_prefix)
298 package_directory = '%s/%s' % (parsed_arguments.intermediate_directory,
299 parsed_arguments.package)
301 # Clobber by default, unless the developer wants to hack on the package's
302 # source code.
303 clobber = (environment.get('INSTRUMENTED_LIBRARIES_NO_CLOBBER', '') != '1')
305 download_source = True
306 if os.path.exists(package_directory):
307 if clobber:
308 shell_call('rm -rf %s' % package_directory, parsed_arguments.verbose)
309 else:
310 download_source = False
311 if download_source:
312 os.makedirs(package_directory)
314 with ScopedChangeDirectory(package_directory) as cd_package:
315 if download_source:
316 shell_call('apt-get source %s' % parsed_arguments.package,
317 parsed_arguments.verbose)
318 # There should be exactly one subdirectory after downloading a package.
319 subdirectories = [d for d in os.listdir('.') if os.path.isdir(d)]
320 if len(subdirectories) != 1:
321 raise Exception('apt-get source %s must create exactly one subdirectory.'
322 % parsed_arguments.package)
323 with ScopedChangeDirectory(subdirectories[0]):
324 # Here we are in the package directory.
325 if download_source:
326 # Patch/run_before_build steps are only done once.
327 if parsed_arguments.patch:
328 shell_call(
329 'patch -p1 -i %s/%s' %
330 (os.path.relpath(cd_package.old_path),
331 parsed_arguments.patch),
332 parsed_arguments.verbose)
333 if parsed_arguments.run_before_build:
334 shell_call(
335 '%s/%s' %
336 (os.path.relpath(cd_package.old_path),
337 parsed_arguments.run_before_build),
338 parsed_arguments.verbose)
339 try:
340 build_and_install(parsed_arguments, environment, install_prefix)
341 except Exception as exception:
342 print exception
343 print 'Failed to build package %s.' % parsed_arguments.package
344 print ('Probably, some of its dependencies are not installed: %s' %
345 ' '.join(get_package_build_dependencies(parsed_arguments.package)))
346 sys.exit(1)
348 # Touch a txt file to indicate package is installed.
349 open('%s/%s.txt' % (install_prefix, parsed_arguments.package), 'w').close()
351 # Remove downloaded package and generated temporary build files.
352 # Failed builds intentionally skip this step, in order to aid in tracking down
353 # build failures.
354 if clobber:
355 shell_call('rm -rf %s' % package_directory, parsed_arguments.verbose)
357 def main():
358 argument_parser = argparse.ArgumentParser(
359 description='Download, build and install instrumented package')
361 argument_parser.add_argument('-j', '--jobs', type=int, default=1)
362 argument_parser.add_argument('-p', '--package', required=True)
363 argument_parser.add_argument(
364 '-i', '--product-directory', default='.',
365 help='Relative path to the directory with chrome binaries')
366 argument_parser.add_argument(
367 '-m', '--intermediate-directory', default='.',
368 help='Relative path to the directory for temporary build files')
369 argument_parser.add_argument('--extra-configure-flags', default='')
370 argument_parser.add_argument('--cflags', default='')
371 argument_parser.add_argument('--ldflags', default='')
372 argument_parser.add_argument('-s', '--sanitizer-type', required=True,
373 choices=['asan', 'msan', 'tsan'])
374 argument_parser.add_argument('-v', '--verbose', action='store_true')
375 argument_parser.add_argument('--check-build-deps', action='store_true')
376 argument_parser.add_argument('--cc')
377 argument_parser.add_argument('--cxx')
378 argument_parser.add_argument('--patch', default='')
379 # This should be a shell script to run before building specific libraries.
380 # This will be run after applying the patch above.
381 argument_parser.add_argument('--run-before-build', default='')
382 argument_parser.add_argument('--build-method', default='destdir')
383 argument_parser.add_argument('--sanitizer-blacklist', default='')
385 # Ignore all empty arguments because in several cases gyp passes them to the
386 # script, but ArgumentParser treats them as positional arguments instead of
387 # ignoring (and doesn't have such options).
388 parsed_arguments = argument_parser.parse_args(
389 [arg for arg in sys.argv[1:] if len(arg) != 0])
390 # Ensure current working directory is this script directory.
391 os.chdir(SCRIPT_ABSOLUTE_PATH)
392 # Ensure all build dependencies are installed.
393 if parsed_arguments.check_build_deps:
394 check_package_build_dependencies(parsed_arguments.package)
396 download_build_install(parsed_arguments)
399 if __name__ == '__main__':
400 main()