blob: 24a42ab367d06a24b475001402bc411ef17ce38d [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
Mike Frysinger6a2b0f22020-02-20 13:34:07 -050023import sys
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070024import 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
Mike Frysingerd8d25cd2021-04-06 11:39:17 -040029from chromite.lib import build_target_lib
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070030from chromite.lib import cache
31from chromite.lib import commandline
Alex Klein161495b2019-09-26 16:59:46 -060032from chromite.lib import constants
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070033from chromite.lib import cros_build_lib
Ralph Nathan91874ca2015-03-19 13:29:41 -070034from chromite.lib import cros_logging as logging
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070035from chromite.lib import osutils
Gilad Arnold83233ed2015-05-08 12:12:13 -070036from chromite.lib import path_util
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070037from chromite.lib import gs
Mike Frysinger151f5fb2019-10-22 20:36:25 -040038from chromite.utils import outcap
Bertrand SIMONNET01394c32015-02-09 17:20:25 -080039
40if cros_build_lib.IsInsideChroot():
Mike Frysinger27e21b72018-07-12 14:20:21 -040041 # pylint: disable=import-error
Bertrand SIMONNET01394c32015-02-09 17:20:25 -080042 from portage import create_trees
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070043
Mike Frysinger6a2b0f22020-02-20 13:34:07 -050044
45assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
46
47
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070048DEBUG_SYMS_EXT = '.debug.tbz2'
49
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080050# We cache the package indexes. When the format of what we store changes,
51# bump the cache version to avoid problems.
52CACHE_VERSION = '1'
53
54
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070055class DebugSymbolsInstaller(object):
Mike Frysinger151f5fb2019-10-22 20:36:25 -040056 """Container for enviromnent objects, needed to make multiprocessing work."""
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070057
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
Mike Frysinger151f5fb2019-10-22 20:36:25 -040063 self._capturer = outcap.OutputCapturer()
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070064
65 def __enter__(self):
66 if self._stdout_to_null:
Mike Frysinger151f5fb2019-10-22 20:36:25 -040067 self._capturer.StartCapturing()
68
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070069 return self
70
71 def __exit__(self, _exc_type, _exc_val, _exc_tb):
72 if self._stdout_to_null:
Mike Frysinger151f5fb2019-10-22 20:36:25 -040073 self._capturer.StopCapturing()
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070074
75 def Install(self, cpv, url):
76 """Install the debug symbols for |cpv|.
77
78 This will install the debug symbols tarball in PKGDIR so that it can be
79 used later.
80
81 Args:
82 cpv: the cpv of the package to build. This assumes that the cpv is
83 installed in the sysroot.
84 url: url of the debug symbols archive. This could be a Google Storage url
85 or a local path.
86 """
87 archive = os.path.join(self._vartree.settings['PKGDIR'],
88 cpv + DEBUG_SYMS_EXT)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080089 # GsContext does not understand file:// scheme so we need to extract the
90 # path ourselves.
Mike Frysinger3dcacee2019-08-23 17:09:11 -040091 parsed_url = urllib.parse.urlsplit(url)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080092 if not parsed_url.scheme or parsed_url.scheme == 'file':
93 url = parsed_url.path
94
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070095 if not os.path.isfile(archive):
96 self._gs_context.Copy(url, archive, debug_level=logging.DEBUG)
97
98 with osutils.TempDir(sudo_rm=True) as tempdir:
Alex Klein161495b2019-09-26 16:59:46 -060099 cros_build_lib.sudo_run(
100 ['tar', '-I', 'bzip2 -q', '-xf', archive, '-C', tempdir], quiet=True)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700101
102 with open(self._vartree.getpath(cpv, filename='CONTENTS'),
103 'a') as content_file:
104 # Merge the content of the temporary dir into the sysroot.
105 # pylint: disable=protected-access
106 link = self._vartree.dbapi._dblink(cpv)
107 link.mergeme(tempdir, self._sysroot, content_file, None, '', {}, None)
108
109
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700110def ShouldGetSymbols(cpv, vardb, remote_symbols):
111 """Return True if the symbols for cpv are available and are not installed.
112
113 We try to check if the symbols are installed before checking availability as
114 a GS request is more expensive than checking locally.
115
116 Args:
117 cpv: cpv of the package
118 vardb: a vartree dbapi
119 remote_symbols: a mapping from cpv to debug symbols url
120
121 Returns:
122 True if |cpv|'s debug symbols are not installed and are available
123 """
124 features, contents = vardb.aux_get(cpv, ['FEATURES', 'CONTENTS'])
125
Alex Klein161495b2019-09-26 16:59:46 -0600126 return ('separatedebug' in features and not '/usr/lib/debug/' in contents and
127 cpv in remote_symbols)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700128
129
130def RemoteSymbols(vartree, binhost_cache=None):
131 """Get the cpv to debug symbols mapping.
132
133 If several binhost contain debug symbols for the same cpv, keep only the
134 highest priority one.
135
136 Args:
137 vartree: a vartree
138 binhost_cache: a cache containing the cpv to debug symbols url for all
139 known binhosts. None if we are not caching binhosts.
140
141 Returns:
142 a dictionary mapping the cpv to a remote debug symbols gsurl.
143 """
144 symbols_mapping = {}
145 for binhost in vartree.settings['PORTAGE_BINHOST'].split():
146 if binhost:
147 symbols_mapping.update(ListBinhost(binhost, binhost_cache))
148 return symbols_mapping
149
150
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800151def GetPackageIndex(binhost, binhost_cache=None):
152 """Get the packages index for |binhost|.
153
154 If a cache is provided, use it to a cache remote packages index.
155
156 Args:
157 binhost: a portage binhost, local, google storage or http.
158 binhost_cache: a cache for the remote packages index.
159
160 Returns:
161 A PackageIndex object.
162 """
163 key = binhost.split('://')[-1]
164 key = key.rstrip('/').split('/')
165
166 if binhost_cache and binhost_cache.Lookup(key).Exists():
Mike Frysinger8e99b372019-12-05 19:05:02 -0500167 with open(binhost_cache.Lookup(key).path, 'rb') as f:
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800168 return pickle.load(f)
169
Alex Klein161495b2019-09-26 16:59:46 -0600170 pkgindex = binpkg.GrabRemotePackageIndex(binhost, quiet=True)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800171 if pkgindex and binhost_cache:
172 # Only cache remote binhosts as local binhosts can change.
173 with tempfile.NamedTemporaryFile(delete=False) as temp_file:
174 pickle.dump(pkgindex, temp_file)
175 temp_file.file.close()
176 binhost_cache.Lookup(key).Assign(temp_file.name)
177 elif pkgindex is None:
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400178 urlparts = urllib.parse.urlsplit(binhost)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800179 if urlparts.scheme not in ('file', ''):
180 # Don't fail the build on network errors. Print a warning message and
181 # continue.
Lann Martinffb95162018-08-28 12:02:54 -0600182 logging.warning('Could not get package index %s', binhost)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800183 return None
184
185 binhost = urlparts.path
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800186 if not os.path.isdir(binhost):
187 raise ValueError('unrecognized binhost format for %s.')
188 pkgindex = binpkg.GrabLocalPackageIndex(binhost)
189
190 return pkgindex
191
192
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700193def ListBinhost(binhost, binhost_cache=None):
194 """Return the cpv to debug symbols mapping for a given binhost.
195
196 List the content of the binhost to extract the cpv to debug symbols
197 mapping. If --cachebinhost is set, we cache the result to avoid the
198 cost of gsutil every time.
199
200 Args:
201 binhost: a portage binhost, local or on google storage.
202 binhost_cache: a cache containing mappings cpv to debug symbols url for a
203 given binhost (None if we don't want to cache).
204
205 Returns:
206 A cpv to debug symbols url mapping.
207 """
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700208
209 symbols = {}
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800210 pkgindex = GetPackageIndex(binhost, binhost_cache)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800211 if pkgindex is None:
212 return symbols
213
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700214 for p in pkgindex.packages:
215 if p.get('DEBUG_SYMBOLS') == 'yes':
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800216 path = p.get('PATH', p['CPV'] + '.tbz2')
217 base_url = pkgindex.header.get('URI', binhost)
218 symbols[p['CPV']] = os.path.join(base_url,
219 path.replace('.tbz2', DEBUG_SYMS_EXT))
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700220
221 return symbols
222
223
224def GetMatchingCPV(package, vardb):
225 """Return the cpv of the installed package matching |package|.
226
227 Args:
228 package: package name
229 vardb: a vartree dbapi
230
231 Returns:
232 The cpv of the installed package whose name matchex |package|.
233 """
234 matches = vardb.match(package)
235 if not matches:
236 cros_build_lib.Die('Could not find package %s' % package)
237 if len(matches) != 1:
238 cros_build_lib.Die('Ambiguous package name: %s.\n'
239 'Matching: %s' % (package, ' '.join(matches)))
240 return matches[0]
241
242
Alex Klein161495b2019-09-26 16:59:46 -0600243def GetVartree(sysroot):
244 """Get the portage vartree.
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700245
Alex Klein161495b2019-09-26 16:59:46 -0600246 This must be called in subprocesses only. The vartree is not serializable,
247 and is implemented as a singleton in portage, so it always breaks the
248 parallel call when initialized before it is called.
249 """
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700250 trees = create_trees(target_root=sysroot, config_root=sysroot)
Alex Klein161495b2019-09-26 16:59:46 -0600251 return trees[sysroot]['vartree']
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700252
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700253
Alex Klein161495b2019-09-26 16:59:46 -0600254def GetBinhostCache(options):
255 """Get and optionally clear the binhost cache."""
Gilad Arnold83233ed2015-05-08 12:12:13 -0700256 cache_dir = os.path.join(path_util.FindCacheDir(),
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800257 'cros_install_debug_syms-v' + CACHE_VERSION)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700258 if options.clearcache:
259 osutils.RmDir(cache_dir, ignore_missing=True)
260
261 binhost_cache = None
262 if options.cachebinhost:
263 binhost_cache = cache.DiskCache(cache_dir)
264
Alex Klein161495b2019-09-26 16:59:46 -0600265 return binhost_cache
Bertrand SIMONNETbef192e2014-12-17 14:09:16 -0800266
Alex Klein161495b2019-09-26 16:59:46 -0600267
268def GetInstallArgs(options, sysroot):
269 """Resolve the packages that are to have their debug symbols installed.
270
271 This function must be called in subprocesses only since it requires the
272 portage vartree.
273 See: GetVartree
274
275 Returns:
276 list[(pkg, binhost_url)]
277 """
278 vartree = GetVartree(sysroot)
279 symbols_mapping = RemoteSymbols(vartree, GetBinhostCache(options))
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700280
281 if options.all:
282 to_install = vartree.dbapi.cpv_all()
283 else:
284 to_install = [GetMatchingCPV(p, vartree.dbapi) for p in options.packages]
285
Alex Klein161495b2019-09-26 16:59:46 -0600286 to_install = [
287 p for p in to_install
288 if ShouldGetSymbols(p, vartree.dbapi, symbols_mapping)
289 ]
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700290
Alex Klein161495b2019-09-26 16:59:46 -0600291 return [(p, symbols_mapping[p]) for p in to_install]
292
293
294def ListInstallArgs(options, sysroot):
295 """List the args for the calling process."""
296 lines = ['%s %s' % arg for arg in GetInstallArgs(options, sysroot)]
297 print('\n'.join(lines))
298
299
300def GetInstallArgsList(argv):
301 """Get the install args from the --list reexec of the command."""
Alex Klein209066a2020-05-11 16:36:54 -0600302 # Insert the --list as the first argument to prevent parsing --list as a
303 # package when a package is given.
304 cmd = [argv[0]] + ['--list'] + argv[1:]
Mike Frysinger8e99b372019-12-05 19:05:02 -0500305 result = cros_build_lib.run(cmd, capture_output=True, encoding='utf-8')
Alex Klein161495b2019-09-26 16:59:46 -0600306 lines = result.output.splitlines()
307 return [line.split() for line in lines if line]
308
309
310def _InstallOne(sysroot, debug, args):
311 """Parallelizable wrapper for the DebugSymbolsInstaller.Install method."""
312 vartree = GetVartree(sysroot)
313 gs_context = gs.GSContext(boto_file=vartree.settings['BOTO_CONFIG'])
314 with DebugSymbolsInstaller(vartree, gs_context, sysroot,
315 not debug) as installer:
316 installer.Install(*args)
317
318
319def GetParser():
320 """Build the argument parser."""
321 parser = commandline.ArgumentParser(description=__doc__)
322 parser.add_argument('--board', help='Board name (required).', required=True)
323 parser.add_argument(
324 '--all',
325 action='store_true',
326 default=False,
327 help='Install the debug symbols for all installed packages')
328 parser.add_argument(
329 'packages',
330 nargs=argparse.REMAINDER,
331 help='list of packages that need the debug symbols.')
332
333 advanced = parser.add_argument_group('Advanced options')
334 advanced.add_argument(
335 '--nocachebinhost',
336 dest='cachebinhost',
337 default=True,
338 action='store_false',
339 help="Don't cache the list of files contained in binhosts. "
340 '(Default: cache)')
341 advanced.add_argument(
342 '--clearcache',
343 action='store_true',
344 default=False,
345 help='Clear the binhost cache.')
346 advanced.add_argument(
347 '-j',
348 '--jobs',
349 default=multiprocessing.cpu_count(),
350 type=int,
351 help='Number of processes to run in parallel.')
352 advanced.add_argument(
353 '--list',
354 action='store_true',
355 default=False,
356 help='List the packages whose debug symbols would be installed and '
357 'their binhost path.')
358
359 return parser
360
361
362def ParseArgs(argv):
363 """Parse and validate arguments."""
364 parser = GetParser()
365
366 options = parser.parse_args(argv)
367 if options.all and options.packages:
368 parser.error('Cannot use --all with a list of packages')
369
370 options.Freeze()
371
372 return options
373
374
375def main(argv):
376 if not cros_build_lib.IsInsideChroot():
377 raise commandline.ChrootRequiredError(argv)
378
379 cmd = [os.path.join(constants.CHROMITE_BIN_DIR,
380 'cros_install_debug_syms')] + argv
381 if os.geteuid() != 0:
382 cros_build_lib.sudo_run(cmd)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700383 return
384
Alex Klein161495b2019-09-26 16:59:46 -0600385 options = ParseArgs(argv)
Allen Webb89e50af2018-12-21 10:28:34 -0800386
Alex Klein161495b2019-09-26 16:59:46 -0600387 # sysroot must have a trailing / as the tree dictionary produced by
388 # create_trees in indexed with a trailing /.
Mike Frysingerd8d25cd2021-04-06 11:39:17 -0400389 sysroot = build_target_lib.get_default_sysroot_path(options.board) + '/'
Alex Klein161495b2019-09-26 16:59:46 -0600390
391 if options.list:
392 ListInstallArgs(options, sysroot)
393 return
394
395 args = GetInstallArgsList(cmd)
396
397 if not args:
398 logging.info('No packages found needing debug symbols.')
399 return
400
401 # Partial to simplify the arguments to parallel since the first two are the
402 # same for every call.
403 partial_install = functools.partial(_InstallOne, sysroot, options.debug)
404 pool = multiprocessing.Pool(processes=options.jobs)
405 pool.map(partial_install, args)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700406
Ralph Nathan5a582ff2015-03-20 18:18:30 -0700407 logging.debug('installation done, updating packages index file')
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700408 packages_dir = os.path.join(sysroot, 'packages')
409 packages_file = os.path.join(packages_dir, 'Packages')
410 # binpkg will set DEBUG_SYMBOLS automatically if it detects the debug symbols
411 # in the packages dir.
412 pkgindex = binpkg.GrabLocalPackageIndex(packages_dir)
413 with open(packages_file, 'w') as p:
414 pkgindex.Write(p)