blob: f9dae90662b93e741053bb70154044f3f326f05d [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."""
Alex Klein209066a2020-05-11 16:36:54 -0600301 # Insert the --list as the first argument to prevent parsing --list as a
302 # package when a package is given.
303 cmd = [argv[0]] + ['--list'] + argv[1:]
Mike Frysinger8e99b372019-12-05 19:05:02 -0500304 result = cros_build_lib.run(cmd, capture_output=True, encoding='utf-8')
Alex Klein161495b2019-09-26 16:59:46 -0600305 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)