blob: 2ddfa869aa590d7819e2c967867035347957a412 [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
Chris McDonald59650c32021-07-20 15:29:28 -060017import logging
Alex Klein161495b2019-09-26 16:59:46 -060018import multiprocessing
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070019import os
20import pickle
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070021import tempfile
Mike Frysingere852b072021-05-21 12:39:03 -040022import urllib.parse
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070023
24from chromite.lib import binpkg
Mike Frysinger06a51c82021-04-06 11:39:17 -040025from chromite.lib import build_target_lib
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070026from chromite.lib import cache
27from chromite.lib import commandline
Alex Klein161495b2019-09-26 16:59:46 -060028from chromite.lib import constants
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070029from chromite.lib import cros_build_lib
Chris McDonald59650c32021-07-20 15:29:28 -060030from chromite.lib import gs
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070031from chromite.lib import osutils
Gilad Arnold83233ed2015-05-08 12:12:13 -070032from chromite.lib import path_util
Mike Frysinger151f5fb2019-10-22 20:36:25 -040033from chromite.utils import outcap
Bertrand SIMONNET01394c32015-02-09 17:20:25 -080034
Chris McDonald59650c32021-07-20 15:29:28 -060035
Bertrand SIMONNET01394c32015-02-09 17:20:25 -080036if cros_build_lib.IsInsideChroot():
Mike Frysinger27e21b72018-07-12 14:20:21 -040037 # pylint: disable=import-error
Bertrand SIMONNET01394c32015-02-09 17:20:25 -080038 from portage import create_trees
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070039
Mike Frysinger6a2b0f22020-02-20 13:34:07 -050040
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070041DEBUG_SYMS_EXT = '.debug.tbz2'
42
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080043# We cache the package indexes. When the format of what we store changes,
44# bump the cache version to avoid problems.
45CACHE_VERSION = '1'
46
47
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070048class DebugSymbolsInstaller(object):
Mike Frysinger151f5fb2019-10-22 20:36:25 -040049 """Container for enviromnent objects, needed to make multiprocessing work."""
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070050
51 def __init__(self, vartree, gs_context, sysroot, stdout_to_null):
52 self._vartree = vartree
53 self._gs_context = gs_context
54 self._sysroot = sysroot
55 self._stdout_to_null = stdout_to_null
Mike Frysinger151f5fb2019-10-22 20:36:25 -040056 self._capturer = outcap.OutputCapturer()
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070057
58 def __enter__(self):
59 if self._stdout_to_null:
Mike Frysinger151f5fb2019-10-22 20:36:25 -040060 self._capturer.StartCapturing()
61
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070062 return self
63
64 def __exit__(self, _exc_type, _exc_val, _exc_tb):
65 if self._stdout_to_null:
Mike Frysinger151f5fb2019-10-22 20:36:25 -040066 self._capturer.StopCapturing()
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070067
68 def Install(self, cpv, url):
69 """Install the debug symbols for |cpv|.
70
71 This will install the debug symbols tarball in PKGDIR so that it can be
72 used later.
73
74 Args:
75 cpv: the cpv of the package to build. This assumes that the cpv is
76 installed in the sysroot.
77 url: url of the debug symbols archive. This could be a Google Storage url
78 or a local path.
79 """
80 archive = os.path.join(self._vartree.settings['PKGDIR'],
81 cpv + DEBUG_SYMS_EXT)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080082 # GsContext does not understand file:// scheme so we need to extract the
83 # path ourselves.
Mike Frysinger3dcacee2019-08-23 17:09:11 -040084 parsed_url = urllib.parse.urlsplit(url)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080085 if not parsed_url.scheme or parsed_url.scheme == 'file':
86 url = parsed_url.path
87
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070088 if not os.path.isfile(archive):
89 self._gs_context.Copy(url, archive, debug_level=logging.DEBUG)
90
Mike Frysinger2e169a92022-04-27 02:15:09 -040091 compression = cros_build_lib.CompressionDetectType(archive)
92 compressor = cros_build_lib.FindCompressor(compression)
93
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070094 with osutils.TempDir(sudo_rm=True) as tempdir:
Alex Klein161495b2019-09-26 16:59:46 -060095 cros_build_lib.sudo_run(
Mike Frysinger2e169a92022-04-27 02:15:09 -040096 ['tar', '-I', compressor, '-xf', archive, '-C', tempdir], quiet=True)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070097
98 with open(self._vartree.getpath(cpv, filename='CONTENTS'),
99 'a') as content_file:
100 # Merge the content of the temporary dir into the sysroot.
101 # pylint: disable=protected-access
102 link = self._vartree.dbapi._dblink(cpv)
103 link.mergeme(tempdir, self._sysroot, content_file, None, '', {}, None)
104
105
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700106def ShouldGetSymbols(cpv, vardb, remote_symbols):
107 """Return True if the symbols for cpv are available and are not installed.
108
109 We try to check if the symbols are installed before checking availability as
110 a GS request is more expensive than checking locally.
111
112 Args:
113 cpv: cpv of the package
114 vardb: a vartree dbapi
115 remote_symbols: a mapping from cpv to debug symbols url
116
117 Returns:
118 True if |cpv|'s debug symbols are not installed and are available
119 """
120 features, contents = vardb.aux_get(cpv, ['FEATURES', 'CONTENTS'])
121
Alex Klein161495b2019-09-26 16:59:46 -0600122 return ('separatedebug' in features and not '/usr/lib/debug/' in contents and
123 cpv in remote_symbols)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700124
125
126def RemoteSymbols(vartree, binhost_cache=None):
127 """Get the cpv to debug symbols mapping.
128
129 If several binhost contain debug symbols for the same cpv, keep only the
130 highest priority one.
131
132 Args:
133 vartree: a vartree
134 binhost_cache: a cache containing the cpv to debug symbols url for all
135 known binhosts. None if we are not caching binhosts.
136
137 Returns:
138 a dictionary mapping the cpv to a remote debug symbols gsurl.
139 """
140 symbols_mapping = {}
141 for binhost in vartree.settings['PORTAGE_BINHOST'].split():
142 if binhost:
143 symbols_mapping.update(ListBinhost(binhost, binhost_cache))
144 return symbols_mapping
145
146
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800147def GetPackageIndex(binhost, binhost_cache=None):
148 """Get the packages index for |binhost|.
149
150 If a cache is provided, use it to a cache remote packages index.
151
152 Args:
153 binhost: a portage binhost, local, google storage or http.
154 binhost_cache: a cache for the remote packages index.
155
156 Returns:
157 A PackageIndex object.
158 """
159 key = binhost.split('://')[-1]
160 key = key.rstrip('/').split('/')
161
162 if binhost_cache and binhost_cache.Lookup(key).Exists():
Mike Frysinger8e99b372019-12-05 19:05:02 -0500163 with open(binhost_cache.Lookup(key).path, 'rb') as f:
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800164 return pickle.load(f)
165
Alex Klein161495b2019-09-26 16:59:46 -0600166 pkgindex = binpkg.GrabRemotePackageIndex(binhost, quiet=True)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800167 if pkgindex and binhost_cache:
168 # Only cache remote binhosts as local binhosts can change.
169 with tempfile.NamedTemporaryFile(delete=False) as temp_file:
170 pickle.dump(pkgindex, temp_file)
171 temp_file.file.close()
172 binhost_cache.Lookup(key).Assign(temp_file.name)
173 elif pkgindex is None:
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400174 urlparts = urllib.parse.urlsplit(binhost)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800175 if urlparts.scheme not in ('file', ''):
176 # Don't fail the build on network errors. Print a warning message and
177 # continue.
Lann Martinffb95162018-08-28 12:02:54 -0600178 logging.warning('Could not get package index %s', binhost)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800179 return None
180
181 binhost = urlparts.path
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800182 if not os.path.isdir(binhost):
183 raise ValueError('unrecognized binhost format for %s.')
184 pkgindex = binpkg.GrabLocalPackageIndex(binhost)
185
186 return pkgindex
187
188
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700189def ListBinhost(binhost, binhost_cache=None):
190 """Return the cpv to debug symbols mapping for a given binhost.
191
192 List the content of the binhost to extract the cpv to debug symbols
193 mapping. If --cachebinhost is set, we cache the result to avoid the
194 cost of gsutil every time.
195
196 Args:
197 binhost: a portage binhost, local or on google storage.
198 binhost_cache: a cache containing mappings cpv to debug symbols url for a
199 given binhost (None if we don't want to cache).
200
201 Returns:
202 A cpv to debug symbols url mapping.
203 """
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700204
205 symbols = {}
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800206 pkgindex = GetPackageIndex(binhost, binhost_cache)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800207 if pkgindex is None:
208 return symbols
209
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700210 for p in pkgindex.packages:
211 if p.get('DEBUG_SYMBOLS') == 'yes':
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800212 path = p.get('PATH', p['CPV'] + '.tbz2')
213 base_url = pkgindex.header.get('URI', binhost)
214 symbols[p['CPV']] = os.path.join(base_url,
215 path.replace('.tbz2', DEBUG_SYMS_EXT))
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700216
217 return symbols
218
219
220def GetMatchingCPV(package, vardb):
221 """Return the cpv of the installed package matching |package|.
222
223 Args:
224 package: package name
225 vardb: a vartree dbapi
226
227 Returns:
228 The cpv of the installed package whose name matchex |package|.
229 """
230 matches = vardb.match(package)
231 if not matches:
232 cros_build_lib.Die('Could not find package %s' % package)
233 if len(matches) != 1:
234 cros_build_lib.Die('Ambiguous package name: %s.\n'
235 'Matching: %s' % (package, ' '.join(matches)))
236 return matches[0]
237
238
Alex Klein161495b2019-09-26 16:59:46 -0600239def GetVartree(sysroot):
240 """Get the portage vartree.
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700241
Alex Klein161495b2019-09-26 16:59:46 -0600242 This must be called in subprocesses only. The vartree is not serializable,
243 and is implemented as a singleton in portage, so it always breaks the
244 parallel call when initialized before it is called.
245 """
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700246 trees = create_trees(target_root=sysroot, config_root=sysroot)
Alex Klein161495b2019-09-26 16:59:46 -0600247 return trees[sysroot]['vartree']
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700248
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700249
Alex Klein161495b2019-09-26 16:59:46 -0600250def GetBinhostCache(options):
251 """Get and optionally clear the binhost cache."""
Gilad Arnold83233ed2015-05-08 12:12:13 -0700252 cache_dir = os.path.join(path_util.FindCacheDir(),
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800253 'cros_install_debug_syms-v' + CACHE_VERSION)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700254 if options.clearcache:
255 osutils.RmDir(cache_dir, ignore_missing=True)
256
257 binhost_cache = None
258 if options.cachebinhost:
259 binhost_cache = cache.DiskCache(cache_dir)
260
Alex Klein161495b2019-09-26 16:59:46 -0600261 return binhost_cache
Bertrand SIMONNETbef192e2014-12-17 14:09:16 -0800262
Alex Klein161495b2019-09-26 16:59:46 -0600263
264def GetInstallArgs(options, sysroot):
265 """Resolve the packages that are to have their debug symbols installed.
266
267 This function must be called in subprocesses only since it requires the
268 portage vartree.
269 See: GetVartree
270
271 Returns:
272 list[(pkg, binhost_url)]
273 """
274 vartree = GetVartree(sysroot)
275 symbols_mapping = RemoteSymbols(vartree, GetBinhostCache(options))
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700276
277 if options.all:
278 to_install = vartree.dbapi.cpv_all()
279 else:
280 to_install = [GetMatchingCPV(p, vartree.dbapi) for p in options.packages]
281
Alex Klein161495b2019-09-26 16:59:46 -0600282 to_install = [
283 p for p in to_install
284 if ShouldGetSymbols(p, vartree.dbapi, symbols_mapping)
285 ]
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700286
Alex Klein161495b2019-09-26 16:59:46 -0600287 return [(p, symbols_mapping[p]) for p in to_install]
288
289
290def ListInstallArgs(options, sysroot):
291 """List the args for the calling process."""
292 lines = ['%s %s' % arg for arg in GetInstallArgs(options, sysroot)]
293 print('\n'.join(lines))
294
295
296def GetInstallArgsList(argv):
297 """Get the install args from the --list reexec of the command."""
Alex Klein209066a2020-05-11 16:36:54 -0600298 # Insert the --list as the first argument to prevent parsing --list as a
299 # package when a package is given.
300 cmd = [argv[0]] + ['--list'] + argv[1:]
Mike Frysinger8e99b372019-12-05 19:05:02 -0500301 result = cros_build_lib.run(cmd, capture_output=True, encoding='utf-8')
Alex Klein161495b2019-09-26 16:59:46 -0600302 lines = result.output.splitlines()
303 return [line.split() for line in lines if line]
304
305
306def _InstallOne(sysroot, debug, args):
307 """Parallelizable wrapper for the DebugSymbolsInstaller.Install method."""
308 vartree = GetVartree(sysroot)
309 gs_context = gs.GSContext(boto_file=vartree.settings['BOTO_CONFIG'])
310 with DebugSymbolsInstaller(vartree, gs_context, sysroot,
311 not debug) as installer:
312 installer.Install(*args)
313
314
315def GetParser():
316 """Build the argument parser."""
317 parser = commandline.ArgumentParser(description=__doc__)
318 parser.add_argument('--board', help='Board name (required).', required=True)
319 parser.add_argument(
320 '--all',
321 action='store_true',
322 default=False,
323 help='Install the debug symbols for all installed packages')
324 parser.add_argument(
325 'packages',
326 nargs=argparse.REMAINDER,
327 help='list of packages that need the debug symbols.')
328
329 advanced = parser.add_argument_group('Advanced options')
330 advanced.add_argument(
331 '--nocachebinhost',
332 dest='cachebinhost',
333 default=True,
334 action='store_false',
335 help="Don't cache the list of files contained in binhosts. "
336 '(Default: cache)')
337 advanced.add_argument(
338 '--clearcache',
339 action='store_true',
340 default=False,
341 help='Clear the binhost cache.')
342 advanced.add_argument(
343 '-j',
344 '--jobs',
345 default=multiprocessing.cpu_count(),
346 type=int,
347 help='Number of processes to run in parallel.')
348 advanced.add_argument(
349 '--list',
350 action='store_true',
351 default=False,
352 help='List the packages whose debug symbols would be installed and '
353 'their binhost path.')
354
355 return parser
356
357
358def ParseArgs(argv):
359 """Parse and validate arguments."""
360 parser = GetParser()
361
362 options = parser.parse_args(argv)
363 if options.all and options.packages:
364 parser.error('Cannot use --all with a list of packages')
365
366 options.Freeze()
367
368 return options
369
370
371def main(argv):
372 if not cros_build_lib.IsInsideChroot():
373 raise commandline.ChrootRequiredError(argv)
374
Mike Frysinger2b1fed52022-04-27 02:15:33 -0400375 options = ParseArgs(argv)
376
Alex Klein161495b2019-09-26 16:59:46 -0600377 cmd = [os.path.join(constants.CHROMITE_BIN_DIR,
378 'cros_install_debug_syms')] + argv
Ram Chandrasekar69751282022-02-25 21:07:36 +0000379 if osutils.IsNonRootUser():
Alex Klein161495b2019-09-26 16:59:46 -0600380 cros_build_lib.sudo_run(cmd)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700381 return
382
Alex Klein161495b2019-09-26 16:59:46 -0600383 # sysroot must have a trailing / as the tree dictionary produced by
384 # create_trees in indexed with a trailing /.
Mike Frysinger06a51c82021-04-06 11:39:17 -0400385 sysroot = build_target_lib.get_default_sysroot_path(options.board) + '/'
Alex Klein161495b2019-09-26 16:59:46 -0600386
387 if options.list:
388 ListInstallArgs(options, sysroot)
389 return
390
391 args = GetInstallArgsList(cmd)
392
393 if not args:
394 logging.info('No packages found needing debug symbols.')
395 return
396
397 # Partial to simplify the arguments to parallel since the first two are the
398 # same for every call.
399 partial_install = functools.partial(_InstallOne, sysroot, options.debug)
400 pool = multiprocessing.Pool(processes=options.jobs)
401 pool.map(partial_install, args)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700402
Ralph Nathan5a582ff2015-03-20 18:18:30 -0700403 logging.debug('installation done, updating packages index file')
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700404 packages_dir = os.path.join(sysroot, 'packages')
405 packages_file = os.path.join(packages_dir, 'Packages')
406 # binpkg will set DEBUG_SYMBOLS automatically if it detects the debug symbols
407 # in the packages dir.
408 pkgindex = binpkg.GrabLocalPackageIndex(packages_dir)
409 with open(packages_file, 'w') as p:
410 pkgindex.Write(p)