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