blob: 710475b85088eb8498d36778c0c0e450459c025b [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
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
Mike Frysinger151f5fb2019-10-22 20:36:25 -040037from chromite.utils import outcap
Bertrand SIMONNET01394c32015-02-09 17:20:25 -080038
39if cros_build_lib.IsInsideChroot():
Mike Frysinger27e21b72018-07-12 14:20:21 -040040 # pylint: disable=import-error
Bertrand SIMONNET01394c32015-02-09 17:20:25 -080041 from portage import create_trees
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070042
Mike Frysinger6a2b0f22020-02-20 13:34:07 -050043
44assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
45
46
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070047DEBUG_SYMS_EXT = '.debug.tbz2'
48
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080049# We cache the package indexes. When the format of what we store changes,
50# bump the cache version to avoid problems.
51CACHE_VERSION = '1'
52
53
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070054class DebugSymbolsInstaller(object):
Mike Frysinger151f5fb2019-10-22 20:36:25 -040055 """Container for enviromnent objects, needed to make multiprocessing work."""
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070056
57 def __init__(self, vartree, gs_context, sysroot, stdout_to_null):
58 self._vartree = vartree
59 self._gs_context = gs_context
60 self._sysroot = sysroot
61 self._stdout_to_null = stdout_to_null
Mike Frysinger151f5fb2019-10-22 20:36:25 -040062 self._capturer = outcap.OutputCapturer()
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070063
64 def __enter__(self):
65 if self._stdout_to_null:
Mike Frysinger151f5fb2019-10-22 20:36:25 -040066 self._capturer.StartCapturing()
67
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070068 return self
69
70 def __exit__(self, _exc_type, _exc_val, _exc_tb):
71 if self._stdout_to_null:
Mike Frysinger151f5fb2019-10-22 20:36:25 -040072 self._capturer.StopCapturing()
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070073
74 def Install(self, cpv, url):
75 """Install the debug symbols for |cpv|.
76
77 This will install the debug symbols tarball in PKGDIR so that it can be
78 used later.
79
80 Args:
81 cpv: the cpv of the package to build. This assumes that the cpv is
82 installed in the sysroot.
83 url: url of the debug symbols archive. This could be a Google Storage url
84 or a local path.
85 """
86 archive = os.path.join(self._vartree.settings['PKGDIR'],
87 cpv + DEBUG_SYMS_EXT)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080088 # GsContext does not understand file:// scheme so we need to extract the
89 # path ourselves.
Mike Frysinger3dcacee2019-08-23 17:09:11 -040090 parsed_url = urllib.parse.urlsplit(url)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -080091 if not parsed_url.scheme or parsed_url.scheme == 'file':
92 url = parsed_url.path
93
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -070094 if not os.path.isfile(archive):
95 self._gs_context.Copy(url, archive, debug_level=logging.DEBUG)
96
97 with osutils.TempDir(sudo_rm=True) as tempdir:
Alex Klein161495b2019-09-26 16:59:46 -060098 cros_build_lib.sudo_run(
99 ['tar', '-I', 'bzip2 -q', '-xf', archive, '-C', tempdir], quiet=True)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700100
101 with open(self._vartree.getpath(cpv, filename='CONTENTS'),
102 'a') as content_file:
103 # Merge the content of the temporary dir into the sysroot.
104 # pylint: disable=protected-access
105 link = self._vartree.dbapi._dblink(cpv)
106 link.mergeme(tempdir, self._sysroot, content_file, None, '', {}, None)
107
108
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700109def ShouldGetSymbols(cpv, vardb, remote_symbols):
110 """Return True if the symbols for cpv are available and are not installed.
111
112 We try to check if the symbols are installed before checking availability as
113 a GS request is more expensive than checking locally.
114
115 Args:
116 cpv: cpv of the package
117 vardb: a vartree dbapi
118 remote_symbols: a mapping from cpv to debug symbols url
119
120 Returns:
121 True if |cpv|'s debug symbols are not installed and are available
122 """
123 features, contents = vardb.aux_get(cpv, ['FEATURES', 'CONTENTS'])
124
Alex Klein161495b2019-09-26 16:59:46 -0600125 return ('separatedebug' in features and not '/usr/lib/debug/' in contents and
126 cpv in remote_symbols)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700127
128
129def RemoteSymbols(vartree, binhost_cache=None):
130 """Get the cpv to debug symbols mapping.
131
132 If several binhost contain debug symbols for the same cpv, keep only the
133 highest priority one.
134
135 Args:
136 vartree: a vartree
137 binhost_cache: a cache containing the cpv to debug symbols url for all
138 known binhosts. None if we are not caching binhosts.
139
140 Returns:
141 a dictionary mapping the cpv to a remote debug symbols gsurl.
142 """
143 symbols_mapping = {}
144 for binhost in vartree.settings['PORTAGE_BINHOST'].split():
145 if binhost:
146 symbols_mapping.update(ListBinhost(binhost, binhost_cache))
147 return symbols_mapping
148
149
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800150def GetPackageIndex(binhost, binhost_cache=None):
151 """Get the packages index for |binhost|.
152
153 If a cache is provided, use it to a cache remote packages index.
154
155 Args:
156 binhost: a portage binhost, local, google storage or http.
157 binhost_cache: a cache for the remote packages index.
158
159 Returns:
160 A PackageIndex object.
161 """
162 key = binhost.split('://')[-1]
163 key = key.rstrip('/').split('/')
164
165 if binhost_cache and binhost_cache.Lookup(key).Exists():
Mike Frysinger8e99b372019-12-05 19:05:02 -0500166 with open(binhost_cache.Lookup(key).path, 'rb') as f:
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800167 return pickle.load(f)
168
Alex Klein161495b2019-09-26 16:59:46 -0600169 pkgindex = binpkg.GrabRemotePackageIndex(binhost, quiet=True)
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800170 if pkgindex and binhost_cache:
171 # Only cache remote binhosts as local binhosts can change.
172 with tempfile.NamedTemporaryFile(delete=False) as temp_file:
173 pickle.dump(pkgindex, temp_file)
174 temp_file.file.close()
175 binhost_cache.Lookup(key).Assign(temp_file.name)
176 elif pkgindex is None:
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400177 urlparts = urllib.parse.urlsplit(binhost)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800178 if urlparts.scheme not in ('file', ''):
179 # Don't fail the build on network errors. Print a warning message and
180 # continue.
Lann Martinffb95162018-08-28 12:02:54 -0600181 logging.warning('Could not get package index %s', binhost)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800182 return None
183
184 binhost = urlparts.path
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800185 if not os.path.isdir(binhost):
186 raise ValueError('unrecognized binhost format for %s.')
187 pkgindex = binpkg.GrabLocalPackageIndex(binhost)
188
189 return pkgindex
190
191
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700192def ListBinhost(binhost, binhost_cache=None):
193 """Return the cpv to debug symbols mapping for a given binhost.
194
195 List the content of the binhost to extract the cpv to debug symbols
196 mapping. If --cachebinhost is set, we cache the result to avoid the
197 cost of gsutil every time.
198
199 Args:
200 binhost: a portage binhost, local or on google storage.
201 binhost_cache: a cache containing mappings cpv to debug symbols url for a
202 given binhost (None if we don't want to cache).
203
204 Returns:
205 A cpv to debug symbols url mapping.
206 """
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700207
208 symbols = {}
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800209 pkgindex = GetPackageIndex(binhost, binhost_cache)
Bertrand SIMONNETdd66ab52015-01-08 14:48:51 -0800210 if pkgindex is None:
211 return symbols
212
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700213 for p in pkgindex.packages:
214 if p.get('DEBUG_SYMBOLS') == 'yes':
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800215 path = p.get('PATH', p['CPV'] + '.tbz2')
216 base_url = pkgindex.header.get('URI', binhost)
217 symbols[p['CPV']] = os.path.join(base_url,
218 path.replace('.tbz2', DEBUG_SYMS_EXT))
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700219
220 return symbols
221
222
223def GetMatchingCPV(package, vardb):
224 """Return the cpv of the installed package matching |package|.
225
226 Args:
227 package: package name
228 vardb: a vartree dbapi
229
230 Returns:
231 The cpv of the installed package whose name matchex |package|.
232 """
233 matches = vardb.match(package)
234 if not matches:
235 cros_build_lib.Die('Could not find package %s' % package)
236 if len(matches) != 1:
237 cros_build_lib.Die('Ambiguous package name: %s.\n'
238 'Matching: %s' % (package, ' '.join(matches)))
239 return matches[0]
240
241
Alex Klein161495b2019-09-26 16:59:46 -0600242def GetVartree(sysroot):
243 """Get the portage vartree.
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700244
Alex Klein161495b2019-09-26 16:59:46 -0600245 This must be called in subprocesses only. The vartree is not serializable,
246 and is implemented as a singleton in portage, so it always breaks the
247 parallel call when initialized before it is called.
248 """
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700249 trees = create_trees(target_root=sysroot, config_root=sysroot)
Alex Klein161495b2019-09-26 16:59:46 -0600250 return trees[sysroot]['vartree']
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700251
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700252
Alex Klein161495b2019-09-26 16:59:46 -0600253def GetBinhostCache(options):
254 """Get and optionally clear the binhost cache."""
Gilad Arnold83233ed2015-05-08 12:12:13 -0700255 cache_dir = os.path.join(path_util.FindCacheDir(),
Bertrand SIMONNET1e146e52014-12-11 14:11:56 -0800256 'cros_install_debug_syms-v' + CACHE_VERSION)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700257 if options.clearcache:
258 osutils.RmDir(cache_dir, ignore_missing=True)
259
260 binhost_cache = None
261 if options.cachebinhost:
262 binhost_cache = cache.DiskCache(cache_dir)
263
Alex Klein161495b2019-09-26 16:59:46 -0600264 return binhost_cache
Bertrand SIMONNETbef192e2014-12-17 14:09:16 -0800265
Alex Klein161495b2019-09-26 16:59:46 -0600266
267def GetInstallArgs(options, sysroot):
268 """Resolve the packages that are to have their debug symbols installed.
269
270 This function must be called in subprocesses only since it requires the
271 portage vartree.
272 See: GetVartree
273
274 Returns:
275 list[(pkg, binhost_url)]
276 """
277 vartree = GetVartree(sysroot)
278 symbols_mapping = RemoteSymbols(vartree, GetBinhostCache(options))
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700279
280 if options.all:
281 to_install = vartree.dbapi.cpv_all()
282 else:
283 to_install = [GetMatchingCPV(p, vartree.dbapi) for p in options.packages]
284
Alex Klein161495b2019-09-26 16:59:46 -0600285 to_install = [
286 p for p in to_install
287 if ShouldGetSymbols(p, vartree.dbapi, symbols_mapping)
288 ]
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700289
Alex Klein161495b2019-09-26 16:59:46 -0600290 return [(p, symbols_mapping[p]) for p in to_install]
291
292
293def ListInstallArgs(options, sysroot):
294 """List the args for the calling process."""
295 lines = ['%s %s' % arg for arg in GetInstallArgs(options, sysroot)]
296 print('\n'.join(lines))
297
298
299def GetInstallArgsList(argv):
300 """Get the install args from the --list reexec of the command."""
301 cmd = argv + ['--list']
Mike Frysinger8e99b372019-12-05 19:05:02 -0500302 result = cros_build_lib.run(cmd, capture_output=True, encoding='utf-8')
Alex Klein161495b2019-09-26 16:59:46 -0600303 lines = result.output.splitlines()
304 return [line.split() for line in lines if line]
305
306
307def _InstallOne(sysroot, debug, args):
308 """Parallelizable wrapper for the DebugSymbolsInstaller.Install method."""
309 vartree = GetVartree(sysroot)
310 gs_context = gs.GSContext(boto_file=vartree.settings['BOTO_CONFIG'])
311 with DebugSymbolsInstaller(vartree, gs_context, sysroot,
312 not debug) as installer:
313 installer.Install(*args)
314
315
316def GetParser():
317 """Build the argument parser."""
318 parser = commandline.ArgumentParser(description=__doc__)
319 parser.add_argument('--board', help='Board name (required).', required=True)
320 parser.add_argument(
321 '--all',
322 action='store_true',
323 default=False,
324 help='Install the debug symbols for all installed packages')
325 parser.add_argument(
326 'packages',
327 nargs=argparse.REMAINDER,
328 help='list of packages that need the debug symbols.')
329
330 advanced = parser.add_argument_group('Advanced options')
331 advanced.add_argument(
332 '--nocachebinhost',
333 dest='cachebinhost',
334 default=True,
335 action='store_false',
336 help="Don't cache the list of files contained in binhosts. "
337 '(Default: cache)')
338 advanced.add_argument(
339 '--clearcache',
340 action='store_true',
341 default=False,
342 help='Clear the binhost cache.')
343 advanced.add_argument(
344 '-j',
345 '--jobs',
346 default=multiprocessing.cpu_count(),
347 type=int,
348 help='Number of processes to run in parallel.')
349 advanced.add_argument(
350 '--list',
351 action='store_true',
352 default=False,
353 help='List the packages whose debug symbols would be installed and '
354 'their binhost path.')
355
356 return parser
357
358
359def ParseArgs(argv):
360 """Parse and validate arguments."""
361 parser = GetParser()
362
363 options = parser.parse_args(argv)
364 if options.all and options.packages:
365 parser.error('Cannot use --all with a list of packages')
366
367 options.Freeze()
368
369 return options
370
371
372def main(argv):
373 if not cros_build_lib.IsInsideChroot():
374 raise commandline.ChrootRequiredError(argv)
375
376 cmd = [os.path.join(constants.CHROMITE_BIN_DIR,
377 'cros_install_debug_syms')] + argv
378 if os.geteuid() != 0:
379 cros_build_lib.sudo_run(cmd)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700380 return
381
Alex Klein161495b2019-09-26 16:59:46 -0600382 options = ParseArgs(argv)
Allen Webb89e50af2018-12-21 10:28:34 -0800383
Alex Klein161495b2019-09-26 16:59:46 -0600384 # sysroot must have a trailing / as the tree dictionary produced by
385 # create_trees in indexed with a trailing /.
386 sysroot = cros_build_lib.GetSysroot(options.board) + '/'
387
388 if options.list:
389 ListInstallArgs(options, sysroot)
390 return
391
392 args = GetInstallArgsList(cmd)
393
394 if not args:
395 logging.info('No packages found needing debug symbols.')
396 return
397
398 # Partial to simplify the arguments to parallel since the first two are the
399 # same for every call.
400 partial_install = functools.partial(_InstallOne, sysroot, options.debug)
401 pool = multiprocessing.Pool(processes=options.jobs)
402 pool.map(partial_install, args)
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700403
Ralph Nathan5a582ff2015-03-20 18:18:30 -0700404 logging.debug('installation done, updating packages index file')
Bertrand SIMONNET6b6a1312014-10-29 18:37:51 -0700405 packages_dir = os.path.join(sysroot, 'packages')
406 packages_file = os.path.join(packages_dir, 'Packages')
407 # binpkg will set DEBUG_SYMBOLS automatically if it detects the debug symbols
408 # in the packages dir.
409 pkgindex = binpkg.GrabLocalPackageIndex(packages_dir)
410 with open(packages_file, 'w') as p:
411 pkgindex.Write(p)