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 |