blob: c80fbf0e8c70ce6f46349b2243366457b77c5b80 [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:
11 package_hash_missing_deps.py --board=amd64-generic --match \
12 chromeos-base/cryptohome
13"""
14
15import argparse
16import collections
17import os
Allen Webbe8c1da02023-09-08 18:25:22 +000018from pathlib import Path
Allen Webb3e498aa2023-09-05 14:40:49 +000019import pprint
20import sys
Allen Webbe8c1da02023-09-08 18:25:22 +000021from typing import List, Optional, Set, Union
Allen Webb3e498aa2023-09-05 14:40:49 +000022
23from chromite.lib import build_target_lib
24from chromite.lib import chroot_lib
25from chromite.lib import commandline
26from chromite.lib import cros_build_lib
27from chromite.lib import parallel
28from chromite.lib import portage_util
29from chromite.lib.parser import package_info
30
31
Allen Webb7cc7cd92023-09-11 16:16:33 +000032VIRTUALS = {
33 "virtual/libcrypt": ("sys-libs/libxcrypt",),
34 "virtual/libelf": ("dev-libs/elfutils", "sys-freebsd/freebsd-lib"),
35 "virtual/libiconv": ("dev-libs/libiconv",),
36 "virtual/libintl": ("dev-libs/libintl",),
37 "virtual/libudev": (
38 "sys-apps/systemd-utils",
39 "sys-fs/udev",
40 "sys-fs/eudev",
41 "sys-apps/systemd",
42 ),
43 "virtual/libusb": ("dev-libs/libusb", "sys-freebsd/freebsd-lib"),
44}
45
46
Allen Webb3e498aa2023-09-05 14:40:49 +000047def env_to_libs(var: str) -> List[str]:
48 """Converts value of REQUIRES to a list of .so files.
49
50 For example:
51 "arm_32: libRSSupport.so libblasV8.so libc.so ..."
52 Becomes:
53 ["libRSSupport.so", "libblasV8.so", "libc.so", ...]
54 """
55 return [x for x in var.split() if not x.endswith(":")]
56
57
58class DotSoResolver:
59 """Provides shared library related dependency operations."""
60
61 def __init__(
62 self,
63 board: Optional[str] = None,
64 root: Union[os.PathLike, str] = "/",
65 chroot: Optional[chroot_lib.Chroot] = None,
66 ):
67 self.board = board
68 self.chroot = chroot if chroot else chroot_lib.Chroot()
69
70 self.sdk_db = portage_util.PortageDB()
71 self.db = self.sdk_db if root == "/" else portage_util.PortageDB(root)
72 self.provided_libs_cache = {}
73
Allen Webbe8c1da02023-09-08 18:25:22 +000074 # Lazy initialize since it might not be needed.
75 self.lib_to_package_map = None
76
Allen Webb3e498aa2023-09-05 14:40:49 +000077 def get_package(
78 self, query: str, from_sdk=False
79 ) -> Optional[portage_util.InstalledPackage]:
80 """Try to find an InstalledPackage for the provided package string"""
81 packages = (self.sdk_db if from_sdk else self.db).InstalledPackages()
82 info = package_info.parse(query)
83 for package in packages:
84 if info.package != package.package:
85 continue
86 if info.category != package.category:
87 continue
88 dep_info = package.package_info
89 if info.revision and info.revision != dep_info.revision:
90 continue
91 if info.pv and info.pv != dep_info.pv:
92 continue
93 return package
94 return None
95
96 def get_required_libs(self, package) -> Set[str]:
97 """Return a set of required .so files."""
Allen Webb8fc0bba2023-09-11 14:37:25 +000098 requires = package.requires
99 if requires is not None:
100 return set(env_to_libs(package.requires))
101 # Fallback to needed if requires is not available.
102 aggregate = set()
103 needed = package.needed
104 if needed is not None:
105 for libs in needed.values():
106 aggregate.update(libs)
107 return aggregate
Allen Webb3e498aa2023-09-05 14:40:49 +0000108
Allen Webb7cc7cd92023-09-11 16:16:33 +0000109 def get_deps(self, package) -> List[portage_util.InstalledPackage]:
110 """Return a list of dependencies.
111
112 This expands the virtuals listed below.
113 """
Allen Webb3e498aa2023-09-05 14:40:49 +0000114 cpvr = f"{package.category}/{package.pf}"
Allen Webb7cc7cd92023-09-11 16:16:33 +0000115 expanded = []
116 deps = []
117 for dep in portage_util.GetFlattenedDepsForPackage(
Allen Webb3e498aa2023-09-05 14:40:49 +0000118 cpvr, board=self.board, depth=1
Allen Webb7cc7cd92023-09-11 16:16:33 +0000119 ):
120 info = package_info.parse(dep)
121 if not info:
122 continue
123
124 cp = info.cp
125 if cp in VIRTUALS:
126 expanded += VIRTUALS[cp]
127 continue
128
129 pkg = self.db.GetInstalledPackage(info.category, info.pvr)
130 if pkg:
131 deps.append(pkg)
132
133 for dep in expanded:
134 pkg = self.get_package(dep)
135 if pkg:
136 deps.append(pkg)
137
138 return deps
Allen Webb3e498aa2023-09-05 14:40:49 +0000139
140 def get_implicit_libs(self):
141 """Return a set of .so files that are provided by the system."""
142 implicit_libs = set()
143 for dep, from_sdk in (
144 ("cross-aarch64-cros-linux-gnu/glibc", True),
145 ("cross-armv7a-cros-linux-gnueabihf/glibc", True),
146 ("cross-i686-cros-linux-gnu/glibc", True),
147 ("cross-x86_64-cros-linux-gnu/glibc", True),
148 ("sys-libs/glibc", False),
149 ("sys-libs/libcxx", False),
150 ("sys-libs/llvm-libunwind", False),
151 ):
152 pkg = self.get_package(dep, from_sdk)
153 if not pkg:
154 continue
155 implicit_libs.update(self.provided_libs(pkg))
156 return implicit_libs
157
158 def provided_libs(self, package: portage_util.InstalledPackage) -> Set[str]:
159 """Return a set of .so files provided by |package|."""
160 cpvr = f"{package.category}/{package.pf}"
161 if cpvr in self.provided_libs_cache:
162 return self.provided_libs_cache[cpvr]
163
164 libs = set()
165 contents = package.ListContents()
166 # Keep only the .so files
167 for typ, path in contents:
168 if typ == package.DIR:
169 continue
170 filename = os.path.basename(path)
171 if filename.endswith(".so") or ".so." in filename:
172 libs.add(filename)
173 self.provided_libs_cache[cpvr] = libs
174 return libs
175
Allen Webb8fc0bba2023-09-11 14:37:25 +0000176 def cache_libs_from_build(
177 self, package: portage_util.InstalledPackage, image_dir: Path
178 ):
179 """Populate the provided_libs_cache for the package from the image dir.
180
181 When using build-info, CONTENTS might not be available yet. so provide
182 alternative using the destination directory of the ebuild.
183 """
184
185 cpvr = f"{package.category}/{package.pf}"
186 libs = set()
187 for _, _, files in os.walk(image_dir):
188 for file in files:
189 if file.endswith(".so") or ".so." in file:
190 libs.add(os.path.basename(file))
191 self.provided_libs_cache[cpvr] = libs
192
Allen Webb3e498aa2023-09-05 14:40:49 +0000193 def get_provided_from_all_deps(
194 self, package: portage_util.InstalledPackage
195 ) -> Set[str]:
196 """Return a set of .so files provided by the immediate dependencies."""
197 provided_libs = set()
Allen Webb8fc0bba2023-09-11 14:37:25 +0000198 # |package| may not actually be installed yet so manually add it to the
199 # since a package can depend on its own libs.
200 provided_libs.update(self.provided_libs(package))
Allen Webb7cc7cd92023-09-11 16:16:33 +0000201 for pkg in self.get_deps(package):
202 provided_libs.update(self.provided_libs(pkg))
Allen Webb3e498aa2023-09-05 14:40:49 +0000203 return provided_libs
204
Allen Webbe8c1da02023-09-08 18:25:22 +0000205 def lib_to_package(self, lib_filename: str = None) -> Set[str]:
206 """Return a set of packages that contain the library."""
207 if self.lib_to_package_map is None:
208 lookup = collections.defaultdict(set)
209 for pkg in self.db.InstalledPackages():
210 cpvr = f"{pkg.category}/{pkg.pf}"
211 # Packages with bundled libs for internal use and/or standaline
212 # binary packages.
213 if f"{pkg.category}/{pkg.package}" in (
214 "app-emulation/qemu",
215 "chromeos-base/aosp-frameworks-ml-nn-vts",
216 "chromeos-base/factory",
217 "chromeos-base/signingtools-bin",
218 "sys-devel/gcc-bin",
219 ):
220 continue
221 for lib in set(self.provided_libs(pkg)):
222 lookup[lib].add(cpvr)
223 self.lib_to_package_map = lookup
224 else:
225 lookup = self.lib_to_package_map
226 if not lib_filename:
227 return set()
228 try:
229 return lookup[lib_filename]
230 except KeyError:
231 return set()
Allen Webb3e498aa2023-09-05 14:40:49 +0000232
233
234def get_parser() -> commandline.ArgumentParser:
235 """Build the argument parser."""
236 parser = commandline.ArgumentParser(description=__doc__)
237
238 parser.add_argument("package", nargs="*", help="package atom")
239
240 parser.add_argument(
241 "-b",
242 "--board",
243 "--build-target",
244 default=cros_build_lib.GetDefaultBoard(),
245 help="ChromeOS board (Uses the SDK if not specified)",
246 )
247
248 parser.add_argument(
Allen Webbe8c1da02023-09-08 18:25:22 +0000249 "-i",
250 "--build-info",
251 default=None,
252 type=Path,
253 help="Path to build-info folder post src_install",
254 )
255
256 parser.add_argument(
Allen Webb8fc0bba2023-09-11 14:37:25 +0000257 "-x",
258 "--image",
259 default=None,
260 type=Path,
261 help="Path to image folder post src_install (${D} if unspecified)",
262 )
263
264 parser.add_argument(
Allen Webb3e498aa2023-09-05 14:40:49 +0000265 "--match",
266 default=False,
267 action="store_true",
268 help="Try to match missing libraries",
269 )
270
271 parser.add_argument(
272 "-j",
273 "--jobs",
274 default=None,
275 type=int,
276 help="Number of parallel processes",
277 )
278
279 return parser
280
281
282def parse_arguments(argv: List[str]) -> argparse.Namespace:
283 """Parse and validate arguments."""
284 parser = get_parser()
285 opts = parser.parse_args(argv)
Allen Webbe8c1da02023-09-08 18:25:22 +0000286 if opts.build_info and opts.package:
Allen Webb8fc0bba2023-09-11 14:37:25 +0000287 parser.error("Do not specify a package when setting --board-info")
288 if opts.image and not opts.build_info:
289 parser.error("--image requires --board-info")
Allen Webbe8c1da02023-09-08 18:25:22 +0000290 if opts.build_info or len(opts.package) == 1:
Allen Webb3e498aa2023-09-05 14:40:49 +0000291 opts.jobs = 1
292 return opts
293
294
295def check_package(
Allen Webbe8c1da02023-09-08 18:25:22 +0000296 package: portage_util.InstalledPackage,
Allen Webb3e498aa2023-09-05 14:40:49 +0000297 implicit: Set[str],
298 resolver: DotSoResolver,
299 match: bool,
300 debug: bool,
301) -> bool:
302 """Returns false if the package has missing dependencies"""
303 if not package:
304 print("missing package")
305 return False
306
307 provided = resolver.get_provided_from_all_deps(package)
308 if debug:
309 print("provided")
310 pprint.pprint(provided)
311
312 available = provided.union(implicit)
313 required = resolver.get_required_libs(package)
314 if debug:
315 print("required")
316 pprint.pprint(required)
317 unsatisfied = required - available
318 if unsatisfied:
319 cpvr = package.package_info.cpvr
320 print(f"'{cpvr}' missing deps for: ", end="")
321 pprint.pprint(unsatisfied)
322 if match:
323 missing = set()
324 for lib in unsatisfied:
Allen Webbe8c1da02023-09-08 18:25:22 +0000325 missing.update(resolver.lib_to_package(lib))
Allen Webb3e498aa2023-09-05 14:40:49 +0000326 if missing:
327 print(f"'{cpvr}' needs: ", end="")
328 pprint.pprint(missing)
329 return False
330 return True
331
332
333def main(argv: Optional[List[str]]):
334 """Main."""
335 opts = parse_arguments(argv)
336 opts.Freeze()
337
338 board = opts.board
339 root = build_target_lib.get_default_sysroot_path(board)
340 if board:
341 os.environ["PORTAGE_CONFIGROOT"] = root
342 os.environ["SYSROOT"] = root
343 os.environ["ROOT"] = root
344
345 failed = False
346 resolver = DotSoResolver(board, root)
Allen Webb3e498aa2023-09-05 14:40:49 +0000347
348 if not opts.package:
Allen Webbe8c1da02023-09-08 18:25:22 +0000349 if opts.build_info:
Allen Webb8fc0bba2023-09-11 14:37:25 +0000350 pkg = portage_util.InstalledPackage(resolver.db, opts.build_info)
351 image_path = opts.image or os.environ.get("D")
352 if image_path:
353 resolver.cache_libs_from_build(pkg, Path(image_path))
354 packages = [pkg]
Allen Webbe8c1da02023-09-08 18:25:22 +0000355 else:
356 packages = resolver.db.InstalledPackages()
Allen Webb3e498aa2023-09-05 14:40:49 +0000357 else:
358 packages = [resolver.get_package(p) for p in opts.package]
359
360 implicit = resolver.get_implicit_libs()
361 if opts.debug:
362 print("implicit")
363 pprint.pprint(implicit)
364
365 if opts.jobs == 1:
366 for package in packages:
367 if not check_package(
368 package,
Allen Webb3e498aa2023-09-05 14:40:49 +0000369 implicit,
370 resolver,
371 opts.match,
372 opts.debug,
373 ):
374 failed = True
375 else:
Allen Webbe8c1da02023-09-08 18:25:22 +0000376 if opts.match:
377 # Pre initialize the map before starting jobs.
378 resolver.lib_to_package()
Allen Webb3e498aa2023-09-05 14:40:49 +0000379 for ret in parallel.RunTasksInProcessPool(
380 lambda p: check_package(
Allen Webbe8c1da02023-09-08 18:25:22 +0000381 p, implicit, resolver, opts.match, opts.debug
Allen Webb3e498aa2023-09-05 14:40:49 +0000382 ),
383 [[p] for p in packages],
384 opts.jobs,
385 ):
386 if not ret:
387 failed = True
388
389 if failed:
390 sys.exit(1)
391
392
393if __name__ == "__main__":
394 main(sys.argv[1:])