blob: 3eb547e7b1b2705cd4f253f072549632c9e91598 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -07002# Copyright 2014 The Chromium OS 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"""Install debug symbols for specified packages.
7
8Only reinstall the debug symbols if they are not already installed to save time.
9
10The debug symbols are packaged outside of the prebuilt package in a
11.debug.tbz2 archive when FEATURES=separatedebug is set (by default on
12builders). On local machines, separatedebug is not set and the debug symbols
13are part of the prebuilt package.
14"""
15
16from __future__ import print_function
17
18import argparse
Alex Klein161495b2019-09-26 16:59:46 -060019import functools
20import multiprocessing
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070021import os
22import pickle
23import sys
24import tempfile
Mike Frysinger3dcacee2019-08-23 17:09:11 -040025
26from six.moves import urllib
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070027
28from chromite.lib import binpkg
29from chromite.lib import cache
30from chromite.lib import commandline
Alex Klein161495b2019-09-26 16:59:46 -060031from chromite.lib import constants
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070032from chromite.lib import cros_build_lib
Ralph Nathan91874ca2015-03-19 13:29:41 -070033from chromite.lib import cros_logging as logging
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070034from chromite.lib import osutils
Gilad Arnold83233ed2015-05-08 12:12:13 -070035from chromite.lib import path_util
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070036from chromite.lib import gs
Bertrand SIMONNET01394c32015-02-09 17:20:25 -080037
38if cros_build_lib.IsInsideChroot():
Mike Frysinger27e21b72018-07-12 14:20:21 -040039 # pylint: disable=import-error
Bertrand SIMONNET01394c32015-02-09 17:20:25 -080040 from portage import create_trees
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070041
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070042DEBUG_SYMS_EXT = '.debug.tbz2'
43
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080044# We cache the package indexes. When the format of what we store changes,
45# bump the cache version to avoid problems.
46CACHE_VERSION = '1'
47
48
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070049class DebugSymbolsInstaller(object):
50 """Container for enviromnent objects, needed to make multiprocessing work.
51
52 This also redirects stdout to null when stdout_to_null=True to avoid
53 polluting the output with portage QA warnings.
54 """
55 _old_stdout = None
56 _null = None
57
58 def __init__(self, vartree, gs_context, sysroot, stdout_to_null):
59 self._vartree = vartree
60 self._gs_context = gs_context
61 self._sysroot = sysroot
62 self._stdout_to_null = stdout_to_null
63
64 def __enter__(self):
65 if self._stdout_to_null:
66 self._old_stdout = sys.stdout
67 self._null = open(os.devnull, 'w')
68 sys.stdout = self._null
69 return self
70
71 def __exit__(self, _exc_type, _exc_val, _exc_tb):
72 if self._stdout_to_null:
73 sys.stdout = self._old_stdout
74 self._null.close()
75
76 def Install(self, cpv, url):
77 """Install the debug symbols for |cpv|.
78
79 This will install the debug symbols tarball in PKGDIR so that it can be
80 used later.
81
82 Args:
83 cpv: the cpv of the package to build. This assumes that the cpv is
84 installed in the sysroot.
85 url: url of the debug symbols archive. This could be a Google Storage url
86 or a local path.
87 """
88 archive = os.path.join(self._vartree.settings['PKGDIR'],
89 cpv + DEBUG_SYMS_EXT)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080090 # GsContext does not understand file:// scheme so we need to extract the
91 # path ourselves.
Mike Frysinger3dcacee2019-08-23 17:09:11 -040092 parsed_url = urllib.parse.urlsplit(url)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080093 if not parsed_url.scheme or parsed_url.scheme == 'file':
94 url = parsed_url.path
95
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070096 if not os.path.isfile(archive):
97 self._gs_context.Copy(url, archive, debug_level=logging.DEBUG)
98
99 with osutils.TempDir(sudo_rm=True) as tempdir:
Alex Klein161495b2019-09-26 16:59:46 -0600100 cros_build_lib.sudo_run(
101 ['tar', '-I', 'bzip2 -q', '-xf', archive, '-C', tempdir], quiet=True)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700102
103 with open(self._vartree.getpath(cpv, filename='CONTENTS'),
104 'a') as content_file:
105 # Merge the content of the temporary dir into the sysroot.
106 # pylint: disable=protected-access
107 link = self._vartree.dbapi._dblink(cpv)
108 link.mergeme(tempdir, self._sysroot, content_file, None, '', {}, None)
109
110
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700111def ShouldGetSymbols(cpv, vardb, remote_symbols):
112 """Return True if the symbols for cpv are available and are not installed.
113
114 We try to check if the symbols are installed before checking availability as
115 a GS request is more expensive than checking locally.
116
117 Args:
118 cpv: cpv of the package
119 vardb: a vartree dbapi
120 remote_symbols: a mapping from cpv to debug symbols url
121
122 Returns:
123 True if |cpv|'s debug symbols are not installed and are available
124 """
125 features, contents = vardb.aux_get(cpv, ['FEATURES', 'CONTENTS'])
126
Alex Klein161495b2019-09-26 16:59:46 -0600127 return ('separatedebug' in features and not '/usr/lib/debug/' in contents and
128 cpv in remote_symbols)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700129
130
131def RemoteSymbols(vartree, binhost_cache=None):
132 """Get the cpv to debug symbols mapping.
133
134 If several binhost contain debug symbols for the same cpv, keep only the
135 highest priority one.
136
137 Args:
138 vartree: a vartree
139 binhost_cache: a cache containing the cpv to debug symbols url for all
140 known binhosts. None if we are not caching binhosts.
141
142 Returns:
143 a dictionary mapping the cpv to a remote debug symbols gsurl.
144 """
145 symbols_mapping = {}
146 for binhost in vartree.settings['PORTAGE_BINHOST'].split():
147 if binhost:
148 symbols_mapping.update(ListBinhost(binhost, binhost_cache))
149 return symbols_mapping
150
151
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800152def GetPackageIndex(binhost, binhost_cache=None):
153 """Get the packages index for |binhost|.
154
155 If a cache is provided, use it to a cache remote packages index.
156
157 Args:
158 binhost: a portage binhost, local, google storage or http.
159 binhost_cache: a cache for the remote packages index.
160
161 Returns:
162 A PackageIndex object.
163 """
164 key = binhost.split('://')[-1]
165 key = key.rstrip('/').split('/')
166
167 if binhost_cache and binhost_cache.Lookup(key).Exists():
168 with open(binhost_cache.Lookup(key).path) as f:
169 return pickle.load(f)
170
Alex Klein161495b2019-09-26 16:59:46 -0600171 pkgindex = binpkg.GrabRemotePackageIndex(binhost, quiet=True)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800172 if pkgindex and binhost_cache:
173 # Only cache remote binhosts as local binhosts can change.
174 with tempfile.NamedTemporaryFile(delete=False) as temp_file:
175 pickle.dump(pkgindex, temp_file)
176 temp_file.file.close()
177 binhost_cache.Lookup(key).Assign(temp_file.name)
178 elif pkgindex is None:
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400179 urlparts = urllib.parse.urlsplit(binhost)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800180 if urlparts.scheme not in ('file', ''):
181 # Don't fail the build on network errors. Print a warning message and
182 # continue.
Lann Martinffb95162018-08-28 12:02:54 -0600183 logging.warning('Could not get package index %s', binhost)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800184 return None
185
186 binhost = urlparts.path
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800187 if not os.path.isdir(binhost):
188 raise ValueError('unrecognized binhost format for %s.')
189 pkgindex = binpkg.GrabLocalPackageIndex(binhost)
190
191 return pkgindex
192
193
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700194def ListBinhost(binhost, binhost_cache=None):
195 """Return the cpv to debug symbols mapping for a given binhost.
196
197 List the content of the binhost to extract the cpv to debug symbols
198 mapping. If --cachebinhost is set, we cache the result to avoid the
199 cost of gsutil every time.
200
201 Args:
202 binhost: a portage binhost, local or on google storage.
203 binhost_cache: a cache containing mappings cpv to debug symbols url for a
204 given binhost (None if we don't want to cache).
205
206 Returns:
207 A cpv to debug symbols url mapping.
208 """
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700209
210 symbols = {}
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800211 pkgindex = GetPackageIndex(binhost, binhost_cache)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800212 if pkgindex is None:
213 return symbols
214
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700215 for p in pkgindex.packages:
216 if p.get('DEBUG_SYMBOLS') == 'yes':
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800217 path = p.get('PATH', p['CPV'] + '.tbz2')
218 base_url = pkgindex.header.get('URI', binhost)
219 symbols[p['CPV']] = os.path.join(base_url,
220 path.replace('.tbz2', DEBUG_SYMS_EXT))
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700221
222 return symbols
223
224
225def GetMatchingCPV(package, vardb):
226 """Return the cpv of the installed package matching |package|.
227
228 Args:
229 package: package name
230 vardb: a vartree dbapi
231
232 Returns:
233 The cpv of the installed package whose name matchex |package|.
234 """
235 matches = vardb.match(package)
236 if not matches:
237 cros_build_lib.Die('Could not find package %s' % package)
238 if len(matches) != 1:
239 cros_build_lib.Die('Ambiguous package name: %s.\n'
240 'Matching: %s' % (package, ' '.join(matches)))
241 return matches[0]
242
243
Alex Klein161495b2019-09-26 16:59:46 -0600244def GetVartree(sysroot):
245 """Get the portage vartree.
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700246
Alex Klein161495b2019-09-26 16:59:46 -0600247 This must be called in subprocesses only. The vartree is not serializable,
248 and is implemented as a singleton in portage, so it always breaks the
249 parallel call when initialized before it is called.
250 """
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700251 trees = create_trees(target_root=sysroot, config_root=sysroot)
Alex Klein161495b2019-09-26 16:59:46 -0600252 return trees[sysroot]['vartree']
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700253
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700254
Alex Klein161495b2019-09-26 16:59:46 -0600255def GetBinhostCache(options):
256 """Get and optionally clear the binhost cache."""
Gilad Arnold83233ed2015-05-08 12:12:13 -0700257 cache_dir = os.path.join(path_util.FindCacheDir(),
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800258 'cros_install_debug_syms-v' + CACHE_VERSION)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700259 if options.clearcache:
260 osutils.RmDir(cache_dir, ignore_missing=True)
261
262 binhost_cache = None
263 if options.cachebinhost:
264 binhost_cache = cache.DiskCache(cache_dir)
265
Alex Klein161495b2019-09-26 16:59:46 -0600266 return binhost_cache
Bertrand SIMONNETbef192e2014-12-17 14:09:16 -0800267
Alex Klein161495b2019-09-26 16:59:46 -0600268
269def GetInstallArgs(options, sysroot):
270 """Resolve the packages that are to have their debug symbols installed.
271
272 This function must be called in subprocesses only since it requires the
273 portage vartree.
274 See: GetVartree
275
276 Returns:
277 list[(pkg, binhost_url)]
278 """
279 vartree = GetVartree(sysroot)
280 symbols_mapping = RemoteSymbols(vartree, GetBinhostCache(options))
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700281
282 if options.all:
283 to_install = vartree.dbapi.cpv_all()
284 else:
285 to_install = [GetMatchingCPV(p, vartree.dbapi) for p in options.packages]
286
Alex Klein161495b2019-09-26 16:59:46 -0600287 to_install = [
288 p for p in to_install
289 if ShouldGetSymbols(p, vartree.dbapi, symbols_mapping)
290 ]
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700291
Alex Klein161495b2019-09-26 16:59:46 -0600292 return [(p, symbols_mapping[p]) for p in to_install]
293
294
295def ListInstallArgs(options, sysroot):
296 """List the args for the calling process."""
297 lines = ['%s %s' % arg for arg in GetInstallArgs(options, sysroot)]
298 print('\n'.join(lines))
299
300
301def GetInstallArgsList(argv):
302 """Get the install args from the --list reexec of the command."""
303 cmd = argv + ['--list']
304 result = cros_build_lib.RunCommand(cmd, capture_output=True)
305 lines = result.output.splitlines()
306 return [line.split() for line in lines if line]
307
308
309def _InstallOne(sysroot, debug, args):
310 """Parallelizable wrapper for the DebugSymbolsInstaller.Install method."""
311 vartree = GetVartree(sysroot)
312 gs_context = gs.GSContext(boto_file=vartree.settings['BOTO_CONFIG'])
313 with DebugSymbolsInstaller(vartree, gs_context, sysroot,
314 not debug) as installer:
315 installer.Install(*args)
316
317
318def GetParser():
319 """Build the argument parser."""
320 parser = commandline.ArgumentParser(description=__doc__)
321 parser.add_argument('--board', help='Board name (required).', required=True)
322 parser.add_argument(
323 '--all',
324 action='store_true',
325 default=False,
326 help='Install the debug symbols for all installed packages')
327 parser.add_argument(
328 'packages',
329 nargs=argparse.REMAINDER,
330 help='list of packages that need the debug symbols.')
331
332 advanced = parser.add_argument_group('Advanced options')
333 advanced.add_argument(
334 '--nocachebinhost',
335 dest='cachebinhost',
336 default=True,
337 action='store_false',
338 help="Don't cache the list of files contained in binhosts. "
339 '(Default: cache)')
340 advanced.add_argument(
341 '--clearcache',
342 action='store_true',
343 default=False,
344 help='Clear the binhost cache.')
345 advanced.add_argument(
346 '-j',
347 '--jobs',
348 default=multiprocessing.cpu_count(),
349 type=int,
350 help='Number of processes to run in parallel.')
351 advanced.add_argument(
352 '--list',
353 action='store_true',
354 default=False,
355 help='List the packages whose debug symbols would be installed and '
356 'their binhost path.')
357
358 return parser
359
360
361def ParseArgs(argv):
362 """Parse and validate arguments."""
363 parser = GetParser()
364
365 options = parser.parse_args(argv)
366 if options.all and options.packages:
367 parser.error('Cannot use --all with a list of packages')
368
369 options.Freeze()
370
371 return options
372
373
374def main(argv):
375 if not cros_build_lib.IsInsideChroot():
376 raise commandline.ChrootRequiredError(argv)
377
378 cmd = [os.path.join(constants.CHROMITE_BIN_DIR,
379 'cros_install_debug_syms')] + argv
380 if os.geteuid() != 0:
381 cros_build_lib.sudo_run(cmd)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700382 return
383
Alex Klein161495b2019-09-26 16:59:46 -0600384 options = ParseArgs(argv)
Allen Webb89e50af2018-12-21 10:28:34 -0800385
Alex Klein161495b2019-09-26 16:59:46 -0600386 # sysroot must have a trailing / as the tree dictionary produced by
387 # create_trees in indexed with a trailing /.
388 sysroot = cros_build_lib.GetSysroot(options.board) + '/'
389
390 if options.list:
391 ListInstallArgs(options, sysroot)
392 return
393
394 args = GetInstallArgsList(cmd)
395
396 if not args:
397 logging.info('No packages found needing debug symbols.')
398 return
399
400 # Partial to simplify the arguments to parallel since the first two are the
401 # same for every call.
402 partial_install = functools.partial(_InstallOne, sysroot, options.debug)
403 pool = multiprocessing.Pool(processes=options.jobs)
404 pool.map(partial_install, args)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700405
Ralph Nathan5a582ff2015-03-20 18:18:30 -0700406 logging.debug('installation done, updating packages index file')
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700407 packages_dir = os.path.join(sysroot, 'packages')
408 packages_file = os.path.join(packages_dir, 'Packages')
409 # binpkg will set DEBUG_SYMBOLS automatically if it detects the debug symbols
410 # in the packages dir.
411 pkgindex = binpkg.GrabLocalPackageIndex(packages_dir)
412 with open(packages_file, 'w') as p:
413 pkgindex.Write(p)