blob: 8ff2a3c079e3de1cdc9c736ce8aac3b4263175b4 [file] [log] [blame]
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -07001# Copyright 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Install debug symbols for specified packages.
6
7Only reinstall the debug symbols if they are not already installed to save time.
8
9The debug symbols are packaged outside of the prebuilt package in a
10.debug.tbz2 archive when FEATURES=separatedebug is set (by default on
11builders). On local machines, separatedebug is not set and the debug symbols
12are part of the prebuilt package.
13"""
14
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070015import argparse
Alex Klein161495b2019-09-26 16:59:46 -060016import functools
17import multiprocessing
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070018import os
19import pickle
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070020import tempfile
Mike Frysingere852b072021-05-21 12:39:03 -040021import urllib.parse
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070022
23from chromite.lib import binpkg
Mike Frysinger06a51c82021-04-06 11:39:17 -040024from chromite.lib import build_target_lib
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070025from chromite.lib import cache
26from chromite.lib import commandline
Alex Klein161495b2019-09-26 16:59:46 -060027from chromite.lib import constants
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070028from chromite.lib import cros_build_lib
Ralph Nathan91874ca2015-03-19 13:29:41 -070029from chromite.lib import cros_logging as logging
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070030from chromite.lib import osutils
Gilad Arnold83233ed2015-05-08 12:12:13 -070031from chromite.lib import path_util
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070032from chromite.lib import gs
Mike Frysinger151f5fb2019-10-22 20:36:25 -040033from chromite.utils import outcap
Bertrand SIMONNET01394c32015-02-09 17:20:25 -080034
35if cros_build_lib.IsInsideChroot():
Mike Frysinger27e21b72018-07-12 14:20:21 -040036 # pylint: disable=import-error
Bertrand SIMONNET01394c32015-02-09 17:20:25 -080037 from portage import create_trees
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070038
Mike Frysinger6a2b0f22020-02-20 13:34:07 -050039
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070040DEBUG_SYMS_EXT = '.debug.tbz2'
41
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080042# We cache the package indexes. When the format of what we store changes,
43# bump the cache version to avoid problems.
44CACHE_VERSION = '1'
45
46
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070047class DebugSymbolsInstaller(object):
Mike Frysinger151f5fb2019-10-22 20:36:25 -040048 """Container for enviromnent objects, needed to make multiprocessing work."""
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070049
50 def __init__(self, vartree, gs_context, sysroot, stdout_to_null):
51 self._vartree = vartree
52 self._gs_context = gs_context
53 self._sysroot = sysroot
54 self._stdout_to_null = stdout_to_null
Mike Frysinger151f5fb2019-10-22 20:36:25 -040055 self._capturer = outcap.OutputCapturer()
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070056
57 def __enter__(self):
58 if self._stdout_to_null:
Mike Frysinger151f5fb2019-10-22 20:36:25 -040059 self._capturer.StartCapturing()
60
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070061 return self
62
63 def __exit__(self, _exc_type, _exc_val, _exc_tb):
64 if self._stdout_to_null:
Mike Frysinger151f5fb2019-10-22 20:36:25 -040065 self._capturer.StopCapturing()
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070066
67 def Install(self, cpv, url):
68 """Install the debug symbols for |cpv|.
69
70 This will install the debug symbols tarball in PKGDIR so that it can be
71 used later.
72
73 Args:
74 cpv: the cpv of the package to build. This assumes that the cpv is
75 installed in the sysroot.
76 url: url of the debug symbols archive. This could be a Google Storage url
77 or a local path.
78 """
79 archive = os.path.join(self._vartree.settings['PKGDIR'],
80 cpv + DEBUG_SYMS_EXT)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080081 # GsContext does not understand file:// scheme so we need to extract the
82 # path ourselves.
Mike Frysinger3dcacee2019-08-23 17:09:11 -040083 parsed_url = urllib.parse.urlsplit(url)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080084 if not parsed_url.scheme or parsed_url.scheme == 'file':
85 url = parsed_url.path
86
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070087 if not os.path.isfile(archive):
88 self._gs_context.Copy(url, archive, debug_level=logging.DEBUG)
89
90 with osutils.TempDir(sudo_rm=True) as tempdir:
Alex Klein161495b2019-09-26 16:59:46 -060091 cros_build_lib.sudo_run(
92 ['tar', '-I', 'bzip2 -q', '-xf', archive, '-C', tempdir], quiet=True)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070093
94 with open(self._vartree.getpath(cpv, filename='CONTENTS'),
95 'a') as content_file:
96 # Merge the content of the temporary dir into the sysroot.
97 # pylint: disable=protected-access
98 link = self._vartree.dbapi._dblink(cpv)
99 link.mergeme(tempdir, self._sysroot, content_file, None, '', {}, None)
100
101
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700102def ShouldGetSymbols(cpv, vardb, remote_symbols):
103 """Return True if the symbols for cpv are available and are not installed.
104
105 We try to check if the symbols are installed before checking availability as
106 a GS request is more expensive than checking locally.
107
108 Args:
109 cpv: cpv of the package
110 vardb: a vartree dbapi
111 remote_symbols: a mapping from cpv to debug symbols url
112
113 Returns:
114 True if |cpv|'s debug symbols are not installed and are available
115 """
116 features, contents = vardb.aux_get(cpv, ['FEATURES', 'CONTENTS'])
117
Alex Klein161495b2019-09-26 16:59:46 -0600118 return ('separatedebug' in features and not '/usr/lib/debug/' in contents and
119 cpv in remote_symbols)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700120
121
122def RemoteSymbols(vartree, binhost_cache=None):
123 """Get the cpv to debug symbols mapping.
124
125 If several binhost contain debug symbols for the same cpv, keep only the
126 highest priority one.
127
128 Args:
129 vartree: a vartree
130 binhost_cache: a cache containing the cpv to debug symbols url for all
131 known binhosts. None if we are not caching binhosts.
132
133 Returns:
134 a dictionary mapping the cpv to a remote debug symbols gsurl.
135 """
136 symbols_mapping = {}
137 for binhost in vartree.settings['PORTAGE_BINHOST'].split():
138 if binhost:
139 symbols_mapping.update(ListBinhost(binhost, binhost_cache))
140 return symbols_mapping
141
142
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800143def GetPackageIndex(binhost, binhost_cache=None):
144 """Get the packages index for |binhost|.
145
146 If a cache is provided, use it to a cache remote packages index.
147
148 Args:
149 binhost: a portage binhost, local, google storage or http.
150 binhost_cache: a cache for the remote packages index.
151
152 Returns:
153 A PackageIndex object.
154 """
155 key = binhost.split('://')[-1]
156 key = key.rstrip('/').split('/')
157
158 if binhost_cache and binhost_cache.Lookup(key).Exists():
Mike Frysinger8e99b372019-12-05 19:05:02 -0500159 with open(binhost_cache.Lookup(key).path, 'rb') as f:
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800160 return pickle.load(f)
161
Alex Klein161495b2019-09-26 16:59:46 -0600162 pkgindex = binpkg.GrabRemotePackageIndex(binhost, quiet=True)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800163 if pkgindex and binhost_cache:
164 # Only cache remote binhosts as local binhosts can change.
165 with tempfile.NamedTemporaryFile(delete=False) as temp_file:
166 pickle.dump(pkgindex, temp_file)
167 temp_file.file.close()
168 binhost_cache.Lookup(key).Assign(temp_file.name)
169 elif pkgindex is None:
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400170 urlparts = urllib.parse.urlsplit(binhost)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800171 if urlparts.scheme not in ('file', ''):
172 # Don't fail the build on network errors. Print a warning message and
173 # continue.
Lann Martinffb95162018-08-28 12:02:54 -0600174 logging.warning('Could not get package index %s', binhost)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800175 return None
176
177 binhost = urlparts.path
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800178 if not os.path.isdir(binhost):
179 raise ValueError('unrecognized binhost format for %s.')
180 pkgindex = binpkg.GrabLocalPackageIndex(binhost)
181
182 return pkgindex
183
184
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700185def ListBinhost(binhost, binhost_cache=None):
186 """Return the cpv to debug symbols mapping for a given binhost.
187
188 List the content of the binhost to extract the cpv to debug symbols
189 mapping. If --cachebinhost is set, we cache the result to avoid the
190 cost of gsutil every time.
191
192 Args:
193 binhost: a portage binhost, local or on google storage.
194 binhost_cache: a cache containing mappings cpv to debug symbols url for a
195 given binhost (None if we don't want to cache).
196
197 Returns:
198 A cpv to debug symbols url mapping.
199 """
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700200
201 symbols = {}
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800202 pkgindex = GetPackageIndex(binhost, binhost_cache)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800203 if pkgindex is None:
204 return symbols
205
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700206 for p in pkgindex.packages:
207 if p.get('DEBUG_SYMBOLS') == 'yes':
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800208 path = p.get('PATH', p['CPV'] + '.tbz2')
209 base_url = pkgindex.header.get('URI', binhost)
210 symbols[p['CPV']] = os.path.join(base_url,
211 path.replace('.tbz2', DEBUG_SYMS_EXT))
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700212
213 return symbols
214
215
216def GetMatchingCPV(package, vardb):
217 """Return the cpv of the installed package matching |package|.
218
219 Args:
220 package: package name
221 vardb: a vartree dbapi
222
223 Returns:
224 The cpv of the installed package whose name matchex |package|.
225 """
226 matches = vardb.match(package)
227 if not matches:
228 cros_build_lib.Die('Could not find package %s' % package)
229 if len(matches) != 1:
230 cros_build_lib.Die('Ambiguous package name: %s.\n'
231 'Matching: %s' % (package, ' '.join(matches)))
232 return matches[0]
233
234
Alex Klein161495b2019-09-26 16:59:46 -0600235def GetVartree(sysroot):
236 """Get the portage vartree.
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700237
Alex Klein161495b2019-09-26 16:59:46 -0600238 This must be called in subprocesses only. The vartree is not serializable,
239 and is implemented as a singleton in portage, so it always breaks the
240 parallel call when initialized before it is called.
241 """
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700242 trees = create_trees(target_root=sysroot, config_root=sysroot)
Alex Klein161495b2019-09-26 16:59:46 -0600243 return trees[sysroot]['vartree']
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700244
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700245
Alex Klein161495b2019-09-26 16:59:46 -0600246def GetBinhostCache(options):
247 """Get and optionally clear the binhost cache."""
Gilad Arnold83233ed2015-05-08 12:12:13 -0700248 cache_dir = os.path.join(path_util.FindCacheDir(),
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800249 'cros_install_debug_syms-v' + CACHE_VERSION)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700250 if options.clearcache:
251 osutils.RmDir(cache_dir, ignore_missing=True)
252
253 binhost_cache = None
254 if options.cachebinhost:
255 binhost_cache = cache.DiskCache(cache_dir)
256
Alex Klein161495b2019-09-26 16:59:46 -0600257 return binhost_cache
Bertrand SIMONNETbef192e2014-12-17 14:09:16 -0800258
Alex Klein161495b2019-09-26 16:59:46 -0600259
260def GetInstallArgs(options, sysroot):
261 """Resolve the packages that are to have their debug symbols installed.
262
263 This function must be called in subprocesses only since it requires the
264 portage vartree.
265 See: GetVartree
266
267 Returns:
268 list[(pkg, binhost_url)]
269 """
270 vartree = GetVartree(sysroot)
271 symbols_mapping = RemoteSymbols(vartree, GetBinhostCache(options))
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700272
273 if options.all:
274 to_install = vartree.dbapi.cpv_all()
275 else:
276 to_install = [GetMatchingCPV(p, vartree.dbapi) for p in options.packages]
277
Alex Klein161495b2019-09-26 16:59:46 -0600278 to_install = [
279 p for p in to_install
280 if ShouldGetSymbols(p, vartree.dbapi, symbols_mapping)
281 ]
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700282
Alex Klein161495b2019-09-26 16:59:46 -0600283 return [(p, symbols_mapping[p]) for p in to_install]
284
285
286def ListInstallArgs(options, sysroot):
287 """List the args for the calling process."""
288 lines = ['%s %s' % arg for arg in GetInstallArgs(options, sysroot)]
289 print('\n'.join(lines))
290
291
292def GetInstallArgsList(argv):
293 """Get the install args from the --list reexec of the command."""
Alex Klein209066a2020-05-11 16:36:54 -0600294 # Insert the --list as the first argument to prevent parsing --list as a
295 # package when a package is given.
296 cmd = [argv[0]] + ['--list'] + argv[1:]
Mike Frysinger8e99b372019-12-05 19:05:02 -0500297 result = cros_build_lib.run(cmd, capture_output=True, encoding='utf-8')
Alex Klein161495b2019-09-26 16:59:46 -0600298 lines = result.output.splitlines()
299 return [line.split() for line in lines if line]
300
301
302def _InstallOne(sysroot, debug, args):
303 """Parallelizable wrapper for the DebugSymbolsInstaller.Install method."""
304 vartree = GetVartree(sysroot)
305 gs_context = gs.GSContext(boto_file=vartree.settings['BOTO_CONFIG'])
306 with DebugSymbolsInstaller(vartree, gs_context, sysroot,
307 not debug) as installer:
308 installer.Install(*args)
309
310
311def GetParser():
312 """Build the argument parser."""
313 parser = commandline.ArgumentParser(description=__doc__)
314 parser.add_argument('--board', help='Board name (required).', required=True)
315 parser.add_argument(
316 '--all',
317 action='store_true',
318 default=False,
319 help='Install the debug symbols for all installed packages')
320 parser.add_argument(
321 'packages',
322 nargs=argparse.REMAINDER,
323 help='list of packages that need the debug symbols.')
324
325 advanced = parser.add_argument_group('Advanced options')
326 advanced.add_argument(
327 '--nocachebinhost',
328 dest='cachebinhost',
329 default=True,
330 action='store_false',
331 help="Don't cache the list of files contained in binhosts. "
332 '(Default: cache)')
333 advanced.add_argument(
334 '--clearcache',
335 action='store_true',
336 default=False,
337 help='Clear the binhost cache.')
338 advanced.add_argument(
339 '-j',
340 '--jobs',
341 default=multiprocessing.cpu_count(),
342 type=int,
343 help='Number of processes to run in parallel.')
344 advanced.add_argument(
345 '--list',
346 action='store_true',
347 default=False,
348 help='List the packages whose debug symbols would be installed and '
349 'their binhost path.')
350
351 return parser
352
353
354def ParseArgs(argv):
355 """Parse and validate arguments."""
356 parser = GetParser()
357
358 options = parser.parse_args(argv)
359 if options.all and options.packages:
360 parser.error('Cannot use --all with a list of packages')
361
362 options.Freeze()
363
364 return options
365
366
367def main(argv):
368 if not cros_build_lib.IsInsideChroot():
369 raise commandline.ChrootRequiredError(argv)
370
371 cmd = [os.path.join(constants.CHROMITE_BIN_DIR,
372 'cros_install_debug_syms')] + argv
373 if os.geteuid() != 0:
374 cros_build_lib.sudo_run(cmd)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700375 return
376
Alex Klein161495b2019-09-26 16:59:46 -0600377 options = ParseArgs(argv)
Allen Webb89e50af2018-12-21 10:28:34 -0800378
Alex Klein161495b2019-09-26 16:59:46 -0600379 # sysroot must have a trailing / as the tree dictionary produced by
380 # create_trees in indexed with a trailing /.
Mike Frysinger06a51c82021-04-06 11:39:17 -0400381 sysroot = build_target_lib.get_default_sysroot_path(options.board) + '/'
Alex Klein161495b2019-09-26 16:59:46 -0600382
383 if options.list:
384 ListInstallArgs(options, sysroot)
385 return
386
387 args = GetInstallArgsList(cmd)
388
389 if not args:
390 logging.info('No packages found needing debug symbols.')
391 return
392
393 # Partial to simplify the arguments to parallel since the first two are the
394 # same for every call.
395 partial_install = functools.partial(_InstallOne, sysroot, options.debug)
396 pool = multiprocessing.Pool(processes=options.jobs)
397 pool.map(partial_install, args)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700398
Ralph Nathan5a582ff2015-03-20 18:18:30 -0700399 logging.debug('installation done, updating packages index file')
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700400 packages_dir = os.path.join(sysroot, 'packages')
401 packages_file = os.path.join(packages_dir, 'Packages')
402 # binpkg will set DEBUG_SYMBOLS automatically if it detects the debug symbols
403 # in the packages dir.
404 pkgindex = binpkg.GrabLocalPackageIndex(packages_dir)
405 with open(packages_file, 'w') as p:
406 pkgindex.Write(p)