blob: 191c8baf3198a0955e76ce0a604457707f7971ef [file] [log] [blame]
Allen Webb3e498aa2023-09-05 14:40:49 +00001# Copyright 2023 The ChromiumOS Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Check whether a package links libraries not in RDEPEND.
6
7If no argument is provided it will check all installed packages. It takes the
8BOARD environment variable into account.
9
10Example:
Nicholas Bishopda8cc212023-09-26 13:25:39 -040011 package_has_missing_deps.py --board=amd64-generic --match \
Allen Webb3e498aa2023-09-05 14:40:49 +000012 chromeos-base/cryptohome
13"""
14
15import argparse
16import collections
Mike Frysinger494c6182023-09-21 11:44:45 -040017import logging
Allen Webb3e498aa2023-09-05 14:40:49 +000018import os
Allen Webbe8c1da02023-09-08 18:25:22 +000019from pathlib import Path
Allen Webb3e498aa2023-09-05 14:40:49 +000020import pprint
21import sys
Mike Frysinger1e9ac7c2023-09-21 14:53:48 -040022from typing import Iterable, List, Optional, Set, Union
Allen Webb3e498aa2023-09-05 14:40:49 +000023
24from chromite.lib import build_target_lib
25from chromite.lib import chroot_lib
26from chromite.lib import commandline
27from chromite.lib import cros_build_lib
Allen Webb3e498aa2023-09-05 14:40:49 +000028from chromite.lib import portage_util
29from chromite.lib.parser import package_info
30
31
Allen Webb7cc7cd92023-09-11 16:16:33 +000032VIRTUALS = {
Allen Webb4c582aa2023-09-12 17:20:49 +000033 "virtual/acl": ("sys-apps/acl", "media-libs/img-ddk-bin"),
34 "virtual/arc-opengles": (
35 "media-libs/arc-img-ddk",
36 "media-libs/arc-mesa-img",
37 "media-libs/arc-mali-drivers",
38 "media-libs/arc-mali-drivers-bifrost",
39 "media-libs/arc-mali-drivers-bifrost-bin",
40 "media-libs/arc-mali-drivers-valhall",
41 "media-libs/arc-mali-drivers-valhall-bin",
42 "media-libs/arc-mesa",
43 "media-libs/arc-mesa-amd",
44 "media-libs/arc-mesa-freedreno",
45 "media-libs/arc-mesa-iris",
46 "media-libs/arc-mesa-virgl",
47 "x11-drivers/opengles-headers",
48 ),
49 "virtual/cros-camera-hal": (
50 "media-libs/cros-camera-hal-intel-ipu3",
51 "media-libs/cros-camera-hal-intel-ipu6",
52 "media-libs/cros-camera-hal-mtk",
53 "media-libs/cros-camera-hal-qti",
54 "media-libs/cros-camera-hal-rockchip-isp1",
55 "media-libs/cros-camera-hal-usb",
56 "media-libs/qti-7c-camera-tuning",
57 ),
58 "virtual/img-ddk": ("media-libs/img-ddk", "media-libs/img-ddk-bin"),
59 "virtual/jpeg": ("media-libs/libjpeg-turbo", "media-libs/jpeg"),
60 "virtual/krb5": ("app-crypt/mit-krb5", "app-crypt/heimdal"),
Allen Webb7cc7cd92023-09-11 16:16:33 +000061 "virtual/libcrypt": ("sys-libs/libxcrypt",),
62 "virtual/libelf": ("dev-libs/elfutils", "sys-freebsd/freebsd-lib"),
63 "virtual/libiconv": ("dev-libs/libiconv",),
64 "virtual/libintl": ("dev-libs/libintl",),
65 "virtual/libudev": (
66 "sys-apps/systemd-utils",
67 "sys-fs/udev",
68 "sys-fs/eudev",
69 "sys-apps/systemd",
70 ),
71 "virtual/libusb": ("dev-libs/libusb", "sys-freebsd/freebsd-lib"),
Allen Webb4c582aa2023-09-12 17:20:49 +000072 "virtual/opengles": (
73 "media-libs/img-ddk",
74 "media-libs/img-ddk-bin",
Ricky Liangd5f64432023-09-15 15:50:04 +080075 "media-libs/libglvnd",
Allen Webb4c582aa2023-09-12 17:20:49 +000076 "media-libs/mali-drivers-bin",
77 "media-libs/mali-drivers-bifrost",
78 "media-libs/mali-drivers-bifrost-bin",
79 "media-libs/mali-drivers-valhall",
80 "media-libs/mali-drivers-valhall-bin",
81 "media-libs/mesa",
82 "media-libs/mesa-amd",
83 "media-libs/mesa-freedreno",
84 "media-libs/mesa-iris",
85 "media-libs/mesa-llvmpipe",
86 "media-libs/mesa-panfrost",
87 "media-libs/mesa-reven",
88 "x11-drivers/opengles-headers",
89 ),
90 "virtual/vulkan-icd": (
91 "media-libs/img-ddk",
92 "media-libs/img-ddk-bin",
93 "media-libs/mali-drivers-bifrost",
94 "media-libs/mali-drivers-bifrost-bin",
95 "media-libs/mali-drivers-valhall",
96 "media-libs/mali-drivers-valhall-bin",
97 "media-libs/mesa",
98 "media-libs/mesa-freedreno",
99 "media-libs/mesa-iris",
100 "media-libs/mesa-llvmpipe",
101 "media-libs/mesa-radv",
102 "media-libs/vulkan-loader",
103 ),
Allen Webb7cc7cd92023-09-11 16:16:33 +0000104}
105
106
Allen Webb3e498aa2023-09-05 14:40:49 +0000107def env_to_libs(var: str) -> List[str]:
108 """Converts value of REQUIRES to a list of .so files.
109
110 For example:
111 "arm_32: libRSSupport.so libblasV8.so libc.so ..."
112 Becomes:
113 ["libRSSupport.so", "libblasV8.so", "libc.so", ...]
114 """
115 return [x for x in var.split() if not x.endswith(":")]
116
117
118class DotSoResolver:
119 """Provides shared library related dependency operations."""
120
121 def __init__(
122 self,
123 board: Optional[str] = None,
124 root: Union[os.PathLike, str] = "/",
125 chroot: Optional[chroot_lib.Chroot] = None,
126 ):
127 self.board = board
128 self.chroot = chroot if chroot else chroot_lib.Chroot()
129
130 self.sdk_db = portage_util.PortageDB()
Mike Frysinger2c65b082023-09-21 12:22:58 -0400131 self._sdk_db_packges = None
Allen Webb3e498aa2023-09-05 14:40:49 +0000132 self.db = self.sdk_db if root == "/" else portage_util.PortageDB(root)
Mike Frysinger2c65b082023-09-21 12:22:58 -0400133 self._db_packges = None
Allen Webb3e498aa2023-09-05 14:40:49 +0000134 self.provided_libs_cache = {}
135
Allen Webbe8c1da02023-09-08 18:25:22 +0000136 # Lazy initialize since it might not be needed.
137 self.lib_to_package_map = None
138
Mike Frysinger2c65b082023-09-21 12:22:58 -0400139 @property
140 def sdk_db_packages(self):
141 """Cache sdk_db.InstalledPackages().
142
143 We won't be modifying it, so it's safe for us to reuse the results.
144 """
145 if self._sdk_db_packges is None:
146 self._sdk_db_packges = self.sdk_db.InstalledPackages()
147 return self._sdk_db_packges
148
149 @property
150 def db_packages(self):
151 """Cache db.InstalledPackages().
152
153 We won't be modifying it, so it's safe for us to reuse the results.
154 """
155 if self._db_packges is None:
156 self._db_packges = self.db.InstalledPackages()
157 return self._db_packges
158
Mike Frysinger1e9ac7c2023-09-21 14:53:48 -0400159 def get_packages(
Allen Webb3e498aa2023-09-05 14:40:49 +0000160 self, query: str, from_sdk=False
Mike Frysinger1e9ac7c2023-09-21 14:53:48 -0400161 ) -> Iterable[portage_util.InstalledPackage]:
162 """Find matching InstalledPackage(s) for the |query|."""
Mike Frysinger2c65b082023-09-21 12:22:58 -0400163 packages = self.sdk_db_packages if from_sdk else self.db_packages
Allen Webb3e498aa2023-09-05 14:40:49 +0000164 info = package_info.parse(query)
165 for package in packages:
166 if info.package != package.package:
167 continue
168 if info.category != package.category:
169 continue
170 dep_info = package.package_info
171 if info.revision and info.revision != dep_info.revision:
172 continue
173 if info.pv and info.pv != dep_info.pv:
174 continue
Mike Frysinger494c6182023-09-21 11:44:45 -0400175 logging.debug("query: %s: matched %s", query, dep_info.cpvr)
Mike Frysinger1e9ac7c2023-09-21 14:53:48 -0400176 yield package
Allen Webb3e498aa2023-09-05 14:40:49 +0000177
178 def get_required_libs(self, package) -> Set[str]:
179 """Return a set of required .so files."""
Allen Webb8fc0bba2023-09-11 14:37:25 +0000180 requires = package.requires
181 if requires is not None:
182 return set(env_to_libs(package.requires))
183 # Fallback to needed if requires is not available.
184 aggregate = set()
185 needed = package.needed
186 if needed is not None:
187 for libs in needed.values():
188 aggregate.update(libs)
189 return aggregate
Allen Webb3e498aa2023-09-05 14:40:49 +0000190
Mike Frysinger329bd332023-09-21 14:18:18 -0400191 def get_deps(
192 self, package: portage_util.InstalledPackage
193 ) -> Set[portage_util.InstalledPackage]:
Allen Webb7cc7cd92023-09-11 16:16:33 +0000194 """Return a list of dependencies.
195
196 This expands the virtuals listed below.
197 """
Allen Webb3e498aa2023-09-05 14:40:49 +0000198 cpvr = f"{package.category}/{package.pf}"
Allen Webb7cc7cd92023-09-11 16:16:33 +0000199 expanded = []
Mike Frysinger329bd332023-09-21 14:18:18 -0400200 deps = set()
201
202 # Handling ||() nodes is difficult. Be lazy and expand all of them.
203 # We could compare against the installed db to try and find a match,
204 # but this seems easiest for now as our PortageDB API doesn't support
205 # these kind of primitives yet.
206 def _anyof_reduce(choices: List[str]) -> str:
207 """Reduce ||() nodes."""
208
209 def _flatten(eles):
210 for e in eles:
211 if isinstance(e, tuple):
212 yield from _flatten(e)
213 else:
214 yield e
215
216 citer = _flatten(choices)
217 ret = next(citer)
218 package_dependencies.extend(citer)
219 return ret
220
221 package_dependencies = []
222 package_dependencies.extend(
223 package.depend.reduce(anyof_reduce=_anyof_reduce)
224 )
225 package_dependencies.extend(
226 package.rdepend.reduce(anyof_reduce=_anyof_reduce)
227 )
228
229 for fulldep in package_dependencies:
230 # Preclean the atom. We can only handle basic forms like
231 # CATEGORY/PF, not the full dependency specification. See the
232 # ebuild(5) man page for more details.
233 dep = fulldep
234
235 # Ignore blockers.
236 if dep.startswith("!"):
237 logging.debug("%s: ignoring blocker: %s", cpvr, dep)
238 continue
239
240 # Rip off the SLOT spec.
241 dep = dep.split(":", 1)[0]
242 # Rip off any USE flag constraints.
243 dep = dep.split("[", 1)[0]
244 # Trim leading & trailing version ranges.
245 dep = dep.lstrip("<>=~").rstrip("*")
246
247 logging.debug(
248 "%s: found package dependency: %s -> %s", cpvr, fulldep, dep
249 )
250
Allen Webb7cc7cd92023-09-11 16:16:33 +0000251 info = package_info.parse(dep)
252 if not info:
253 continue
254
255 cp = info.cp
256 if cp in VIRTUALS:
257 expanded += VIRTUALS[cp]
258 continue
259
Mike Frysinger329bd332023-09-21 14:18:18 -0400260 pkgs = self.db.GetInstalledPackage(info.category, info.pvr)
261 if not pkgs:
262 pkgs = list(self.get_packages(info.atom))
263 else:
264 pkgs = [pkgs]
265
266 if pkgs:
267 deps.update(pkgs)
Mike Frysinger494c6182023-09-21 11:44:45 -0400268 else:
269 logging.warning("%s: could not find installed %s", cpvr, dep)
Allen Webb7cc7cd92023-09-11 16:16:33 +0000270
271 for dep in expanded:
Mike Frysinger329bd332023-09-21 14:18:18 -0400272 deps.update(self.get_packages(dep))
Allen Webb7cc7cd92023-09-11 16:16:33 +0000273
274 return deps
Allen Webb3e498aa2023-09-05 14:40:49 +0000275
276 def get_implicit_libs(self):
277 """Return a set of .so files that are provided by the system."""
Allen Webb611e8b12023-09-13 15:54:52 +0000278 # libstdc++ comes from the toolchain so always ignore it.
279 implicit_libs = {"libstdc++.so", "libstdc++.so.6"}
Allen Webb3e498aa2023-09-05 14:40:49 +0000280 for dep, from_sdk in (
281 ("cross-aarch64-cros-linux-gnu/glibc", True),
282 ("cross-armv7a-cros-linux-gnueabihf/glibc", True),
283 ("cross-i686-cros-linux-gnu/glibc", True),
284 ("cross-x86_64-cros-linux-gnu/glibc", True),
285 ("sys-libs/glibc", False),
286 ("sys-libs/libcxx", False),
287 ("sys-libs/llvm-libunwind", False),
288 ):
Mike Frysinger1e9ac7c2023-09-21 14:53:48 -0400289 for pkg in self.get_packages(dep, from_sdk):
290 implicit_libs.update(self.provided_libs(pkg))
Allen Webb3e498aa2023-09-05 14:40:49 +0000291 return implicit_libs
292
293 def provided_libs(self, package: portage_util.InstalledPackage) -> Set[str]:
294 """Return a set of .so files provided by |package|."""
295 cpvr = f"{package.category}/{package.pf}"
296 if cpvr in self.provided_libs_cache:
297 return self.provided_libs_cache[cpvr]
298
299 libs = set()
300 contents = package.ListContents()
301 # Keep only the .so files
302 for typ, path in contents:
303 if typ == package.DIR:
304 continue
305 filename = os.path.basename(path)
Mike Frysinger28f7b952023-09-21 11:40:01 -0400306 if filename.endswith(".so") or (
307 ".so." in filename and not filename.endswith(".debug")
308 ):
Allen Webb3e498aa2023-09-05 14:40:49 +0000309 libs.add(filename)
310 self.provided_libs_cache[cpvr] = libs
311 return libs
312
Allen Webb8fc0bba2023-09-11 14:37:25 +0000313 def cache_libs_from_build(
314 self, package: portage_util.InstalledPackage, image_dir: Path
315 ):
316 """Populate the provided_libs_cache for the package from the image dir.
317
318 When using build-info, CONTENTS might not be available yet. so provide
319 alternative using the destination directory of the ebuild.
320 """
321
322 cpvr = f"{package.category}/{package.pf}"
323 libs = set()
324 for _, _, files in os.walk(image_dir):
325 for file in files:
Mike Frysinger28f7b952023-09-21 11:40:01 -0400326 if file.endswith(".so") or (
327 ".so." in file and not file.endswith(".debug")
328 ):
Allen Webb8fc0bba2023-09-11 14:37:25 +0000329 libs.add(os.path.basename(file))
330 self.provided_libs_cache[cpvr] = libs
331
Allen Webb3e498aa2023-09-05 14:40:49 +0000332 def get_provided_from_all_deps(
333 self, package: portage_util.InstalledPackage
334 ) -> Set[str]:
335 """Return a set of .so files provided by the immediate dependencies."""
336 provided_libs = set()
Allen Webb8fc0bba2023-09-11 14:37:25 +0000337 # |package| may not actually be installed yet so manually add it to the
338 # since a package can depend on its own libs.
339 provided_libs.update(self.provided_libs(package))
Allen Webb7cc7cd92023-09-11 16:16:33 +0000340 for pkg in self.get_deps(package):
Mike Frysinger494c6182023-09-21 11:44:45 -0400341 logging.debug(
342 "%s: loading libs from dependency %s",
343 package.package_info.cpvr,
344 pkg.package_info.cpvr,
345 )
Allen Webb7cc7cd92023-09-11 16:16:33 +0000346 provided_libs.update(self.provided_libs(pkg))
Allen Webb3e498aa2023-09-05 14:40:49 +0000347 return provided_libs
348
Allen Webbe8c1da02023-09-08 18:25:22 +0000349 def lib_to_package(self, lib_filename: str = None) -> Set[str]:
350 """Return a set of packages that contain the library."""
351 if self.lib_to_package_map is None:
352 lookup = collections.defaultdict(set)
353 for pkg in self.db.InstalledPackages():
354 cpvr = f"{pkg.category}/{pkg.pf}"
355 # Packages with bundled libs for internal use and/or standaline
356 # binary packages.
357 if f"{pkg.category}/{pkg.package}" in (
358 "app-emulation/qemu",
359 "chromeos-base/aosp-frameworks-ml-nn-vts",
360 "chromeos-base/factory",
361 "chromeos-base/signingtools-bin",
362 "sys-devel/gcc-bin",
363 ):
364 continue
365 for lib in set(self.provided_libs(pkg)):
366 lookup[lib].add(cpvr)
367 self.lib_to_package_map = lookup
368 else:
369 lookup = self.lib_to_package_map
370 if not lib_filename:
371 return set()
372 try:
373 return lookup[lib_filename]
374 except KeyError:
375 return set()
Allen Webb3e498aa2023-09-05 14:40:49 +0000376
377
378def get_parser() -> commandline.ArgumentParser:
379 """Build the argument parser."""
380 parser = commandline.ArgumentParser(description=__doc__)
381
382 parser.add_argument("package", nargs="*", help="package atom")
383
384 parser.add_argument(
385 "-b",
386 "--board",
387 "--build-target",
388 default=cros_build_lib.GetDefaultBoard(),
389 help="ChromeOS board (Uses the SDK if not specified)",
390 )
391
392 parser.add_argument(
Allen Webb7d34a9a2023-09-18 09:02:15 -0500393 "--no-default-board",
394 dest="board",
395 const=None,
396 action="store_const",
397 help="Ignore the default board",
398 )
399
400 parser.add_argument(
Allen Webbe8c1da02023-09-08 18:25:22 +0000401 "-i",
402 "--build-info",
403 default=None,
404 type=Path,
405 help="Path to build-info folder post src_install",
406 )
407
408 parser.add_argument(
Allen Webb8fc0bba2023-09-11 14:37:25 +0000409 "-x",
410 "--image",
411 default=None,
412 type=Path,
413 help="Path to image folder post src_install (${D} if unspecified)",
414 )
415
416 parser.add_argument(
Allen Webb3e498aa2023-09-05 14:40:49 +0000417 "--match",
418 default=False,
419 action="store_true",
420 help="Try to match missing libraries",
421 )
422
423 parser.add_argument(
424 "-j",
425 "--jobs",
426 default=None,
427 type=int,
428 help="Number of parallel processes",
429 )
430
431 return parser
432
433
434def parse_arguments(argv: List[str]) -> argparse.Namespace:
435 """Parse and validate arguments."""
436 parser = get_parser()
437 opts = parser.parse_args(argv)
Allen Webbe8c1da02023-09-08 18:25:22 +0000438 if opts.build_info and opts.package:
Allen Webb8fc0bba2023-09-11 14:37:25 +0000439 parser.error("Do not specify a package when setting --board-info")
440 if opts.image and not opts.build_info:
441 parser.error("--image requires --board-info")
Allen Webbe8c1da02023-09-08 18:25:22 +0000442 if opts.build_info or len(opts.package) == 1:
Allen Webb3e498aa2023-09-05 14:40:49 +0000443 opts.jobs = 1
444 return opts
445
446
447def check_package(
Allen Webbe8c1da02023-09-08 18:25:22 +0000448 package: portage_util.InstalledPackage,
Allen Webb3e498aa2023-09-05 14:40:49 +0000449 implicit: Set[str],
450 resolver: DotSoResolver,
451 match: bool,
452 debug: bool,
453) -> bool:
454 """Returns false if the package has missing dependencies"""
455 if not package:
Allen Webb473464d2023-09-22 15:35:33 -0500456 print("Package not installed")
Allen Webb3e498aa2023-09-05 14:40:49 +0000457 return False
458
459 provided = resolver.get_provided_from_all_deps(package)
460 if debug:
461 print("provided")
Mike Frysinger494c6182023-09-21 11:44:45 -0400462 pprint.pprint(sorted(provided))
Allen Webb3e498aa2023-09-05 14:40:49 +0000463
464 available = provided.union(implicit)
465 required = resolver.get_required_libs(package)
466 if debug:
467 print("required")
Mike Frysinger494c6182023-09-21 11:44:45 -0400468 pprint.pprint(sorted(required))
Allen Webb3e498aa2023-09-05 14:40:49 +0000469 unsatisfied = required - available
470 if unsatisfied:
471 cpvr = package.package_info.cpvr
Allen Webb473464d2023-09-22 15:35:33 -0500472 print(
473 f"'{cpvr}': Package is linked against libraries that are not "
474 "listed as dependencies in the ebuild:"
475 )
Mike Frysinger494c6182023-09-21 11:44:45 -0400476 pprint.pprint(sorted(unsatisfied))
Allen Webb3e498aa2023-09-05 14:40:49 +0000477 if match:
478 missing = set()
479 for lib in unsatisfied:
Allen Webbe8c1da02023-09-08 18:25:22 +0000480 missing.update(resolver.lib_to_package(lib))
Allen Webb3e498aa2023-09-05 14:40:49 +0000481 if missing:
Allen Webb473464d2023-09-22 15:35:33 -0500482 print(f"'{cpvr}': needs the following added to DEPEND/RDEPEND:")
Mike Frysinger494c6182023-09-21 11:44:45 -0400483 pprint.pprint(sorted(missing))
Allen Webb3e498aa2023-09-05 14:40:49 +0000484 return False
485 return True
486
487
488def main(argv: Optional[List[str]]):
489 """Main."""
Allen Webb3353e152023-09-22 21:00:49 +0000490 commandline.RunInsideChroot()
Allen Webb3e498aa2023-09-05 14:40:49 +0000491 opts = parse_arguments(argv)
492 opts.Freeze()
493
494 board = opts.board
495 root = build_target_lib.get_default_sysroot_path(board)
496 if board:
497 os.environ["PORTAGE_CONFIGROOT"] = root
498 os.environ["SYSROOT"] = root
499 os.environ["ROOT"] = root
500
501 failed = False
502 resolver = DotSoResolver(board, root)
Allen Webb3e498aa2023-09-05 14:40:49 +0000503
504 if not opts.package:
Allen Webbe8c1da02023-09-08 18:25:22 +0000505 if opts.build_info:
Allen Webb8fc0bba2023-09-11 14:37:25 +0000506 pkg = portage_util.InstalledPackage(resolver.db, opts.build_info)
507 image_path = opts.image or os.environ.get("D")
508 if image_path:
509 resolver.cache_libs_from_build(pkg, Path(image_path))
510 packages = [pkg]
Allen Webbe8c1da02023-09-08 18:25:22 +0000511 else:
512 packages = resolver.db.InstalledPackages()
Allen Webb3e498aa2023-09-05 14:40:49 +0000513 else:
Mike Frysinger1e9ac7c2023-09-21 14:53:48 -0400514 packages = []
515 for pkg in opts.package:
516 packages.extend(resolver.get_packages(pkg))
Allen Webb3e498aa2023-09-05 14:40:49 +0000517
518 implicit = resolver.get_implicit_libs()
Allen Webb3e498aa2023-09-05 14:40:49 +0000519
Mike Frysinger329bd332023-09-21 14:18:18 -0400520 if opts.match:
521 # Pre initialize the map before starting jobs.
522 resolver.lib_to_package()
523 for package in packages:
524 if not check_package(
525 package,
526 implicit,
527 resolver,
528 opts.match,
529 opts.debug,
Allen Webb3e498aa2023-09-05 14:40:49 +0000530 ):
Mike Frysinger329bd332023-09-21 14:18:18 -0400531 failed = True
Allen Webb3e498aa2023-09-05 14:40:49 +0000532
533 if failed:
Allen Webb473464d2023-09-22 15:35:33 -0500534 print(
535 """\
536For more information about DEPEND vs. RDEPEND in ebuilds see:
537https://chromium.googlesource.com/chromiumos/docs/+/HEAD/portage/\
538ebuild_faq.md#dependency-types"""
539 )
Allen Webb3e498aa2023-09-05 14:40:49 +0000540 sys.exit(1)
541
542
543if __name__ == "__main__":
544 main(sys.argv[1:])