blob: b34b8e63e663a17b9cc6bb30347566f06e7da31a [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
32def env_to_libs(var: str) -> List[str]:
33 """Converts value of REQUIRES to a list of .so files.
34
35 For example:
36 "arm_32: libRSSupport.so libblasV8.so libc.so ..."
37 Becomes:
38 ["libRSSupport.so", "libblasV8.so", "libc.so", ...]
39 """
40 return [x for x in var.split() if not x.endswith(":")]
41
42
43class DotSoResolver:
44 """Provides shared library related dependency operations."""
45
46 def __init__(
47 self,
48 board: Optional[str] = None,
49 root: Union[os.PathLike, str] = "/",
50 chroot: Optional[chroot_lib.Chroot] = None,
51 ):
52 self.board = board
53 self.chroot = chroot if chroot else chroot_lib.Chroot()
54
55 self.sdk_db = portage_util.PortageDB()
56 self.db = self.sdk_db if root == "/" else portage_util.PortageDB(root)
57 self.provided_libs_cache = {}
58
Allen Webbe8c1da02023-09-08 18:25:22 +000059 # Lazy initialize since it might not be needed.
60 self.lib_to_package_map = None
61
Allen Webb3e498aa2023-09-05 14:40:49 +000062 def get_package(
63 self, query: str, from_sdk=False
64 ) -> Optional[portage_util.InstalledPackage]:
65 """Try to find an InstalledPackage for the provided package string"""
66 packages = (self.sdk_db if from_sdk else self.db).InstalledPackages()
67 info = package_info.parse(query)
68 for package in packages:
69 if info.package != package.package:
70 continue
71 if info.category != package.category:
72 continue
73 dep_info = package.package_info
74 if info.revision and info.revision != dep_info.revision:
75 continue
76 if info.pv and info.pv != dep_info.pv:
77 continue
78 return package
79 return None
80
81 def get_required_libs(self, package) -> Set[str]:
82 """Return a set of required .so files."""
Allen Webb8fc0bba2023-09-11 14:37:25 +000083 requires = package.requires
84 if requires is not None:
85 return set(env_to_libs(package.requires))
86 # Fallback to needed if requires is not available.
87 aggregate = set()
88 needed = package.needed
89 if needed is not None:
90 for libs in needed.values():
91 aggregate.update(libs)
92 return aggregate
Allen Webb3e498aa2023-09-05 14:40:49 +000093
94 def get_deps(self, package):
95 """Return a list of dependencies."""
96 cpvr = f"{package.category}/{package.pf}"
97 return portage_util.GetFlattenedDepsForPackage(
98 cpvr, board=self.board, depth=1
99 )
100
101 def get_implicit_libs(self):
102 """Return a set of .so files that are provided by the system."""
103 implicit_libs = set()
104 for dep, from_sdk in (
105 ("cross-aarch64-cros-linux-gnu/glibc", True),
106 ("cross-armv7a-cros-linux-gnueabihf/glibc", True),
107 ("cross-i686-cros-linux-gnu/glibc", True),
108 ("cross-x86_64-cros-linux-gnu/glibc", True),
109 ("sys-libs/glibc", False),
110 ("sys-libs/libcxx", False),
111 ("sys-libs/llvm-libunwind", False),
112 ):
113 pkg = self.get_package(dep, from_sdk)
114 if not pkg:
115 continue
116 implicit_libs.update(self.provided_libs(pkg))
117 return implicit_libs
118
119 def provided_libs(self, package: portage_util.InstalledPackage) -> Set[str]:
120 """Return a set of .so files provided by |package|."""
121 cpvr = f"{package.category}/{package.pf}"
122 if cpvr in self.provided_libs_cache:
123 return self.provided_libs_cache[cpvr]
124
125 libs = set()
126 contents = package.ListContents()
127 # Keep only the .so files
128 for typ, path in contents:
129 if typ == package.DIR:
130 continue
131 filename = os.path.basename(path)
132 if filename.endswith(".so") or ".so." in filename:
133 libs.add(filename)
134 self.provided_libs_cache[cpvr] = libs
135 return libs
136
Allen Webb8fc0bba2023-09-11 14:37:25 +0000137 def cache_libs_from_build(
138 self, package: portage_util.InstalledPackage, image_dir: Path
139 ):
140 """Populate the provided_libs_cache for the package from the image dir.
141
142 When using build-info, CONTENTS might not be available yet. so provide
143 alternative using the destination directory of the ebuild.
144 """
145
146 cpvr = f"{package.category}/{package.pf}"
147 libs = set()
148 for _, _, files in os.walk(image_dir):
149 for file in files:
150 if file.endswith(".so") or ".so." in file:
151 libs.add(os.path.basename(file))
152 self.provided_libs_cache[cpvr] = libs
153
Allen Webb3e498aa2023-09-05 14:40:49 +0000154 def get_provided_from_all_deps(
155 self, package: portage_util.InstalledPackage
156 ) -> Set[str]:
157 """Return a set of .so files provided by the immediate dependencies."""
158 provided_libs = set()
Allen Webb8fc0bba2023-09-11 14:37:25 +0000159 # |package| may not actually be installed yet so manually add it to the
160 # since a package can depend on its own libs.
161 provided_libs.update(self.provided_libs(package))
Allen Webb3e498aa2023-09-05 14:40:49 +0000162 deps = self.get_deps(package)
163 for dep in deps:
164 info = package_info.parse(dep)
165 pkg = self.db.GetInstalledPackage(info.category, info.pvr)
166 if pkg:
167 provided_libs.update(self.provided_libs(pkg))
168 return provided_libs
169
Allen Webbe8c1da02023-09-08 18:25:22 +0000170 def lib_to_package(self, lib_filename: str = None) -> Set[str]:
171 """Return a set of packages that contain the library."""
172 if self.lib_to_package_map is None:
173 lookup = collections.defaultdict(set)
174 for pkg in self.db.InstalledPackages():
175 cpvr = f"{pkg.category}/{pkg.pf}"
176 # Packages with bundled libs for internal use and/or standaline
177 # binary packages.
178 if f"{pkg.category}/{pkg.package}" in (
179 "app-emulation/qemu",
180 "chromeos-base/aosp-frameworks-ml-nn-vts",
181 "chromeos-base/factory",
182 "chromeos-base/signingtools-bin",
183 "sys-devel/gcc-bin",
184 ):
185 continue
186 for lib in set(self.provided_libs(pkg)):
187 lookup[lib].add(cpvr)
188 self.lib_to_package_map = lookup
189 else:
190 lookup = self.lib_to_package_map
191 if not lib_filename:
192 return set()
193 try:
194 return lookup[lib_filename]
195 except KeyError:
196 return set()
Allen Webb3e498aa2023-09-05 14:40:49 +0000197
198
199def get_parser() -> commandline.ArgumentParser:
200 """Build the argument parser."""
201 parser = commandline.ArgumentParser(description=__doc__)
202
203 parser.add_argument("package", nargs="*", help="package atom")
204
205 parser.add_argument(
206 "-b",
207 "--board",
208 "--build-target",
209 default=cros_build_lib.GetDefaultBoard(),
210 help="ChromeOS board (Uses the SDK if not specified)",
211 )
212
213 parser.add_argument(
Allen Webbe8c1da02023-09-08 18:25:22 +0000214 "-i",
215 "--build-info",
216 default=None,
217 type=Path,
218 help="Path to build-info folder post src_install",
219 )
220
221 parser.add_argument(
Allen Webb8fc0bba2023-09-11 14:37:25 +0000222 "-x",
223 "--image",
224 default=None,
225 type=Path,
226 help="Path to image folder post src_install (${D} if unspecified)",
227 )
228
229 parser.add_argument(
Allen Webb3e498aa2023-09-05 14:40:49 +0000230 "--match",
231 default=False,
232 action="store_true",
233 help="Try to match missing libraries",
234 )
235
236 parser.add_argument(
237 "-j",
238 "--jobs",
239 default=None,
240 type=int,
241 help="Number of parallel processes",
242 )
243
244 return parser
245
246
247def parse_arguments(argv: List[str]) -> argparse.Namespace:
248 """Parse and validate arguments."""
249 parser = get_parser()
250 opts = parser.parse_args(argv)
Allen Webbe8c1da02023-09-08 18:25:22 +0000251 if opts.build_info and opts.package:
Allen Webb8fc0bba2023-09-11 14:37:25 +0000252 parser.error("Do not specify a package when setting --board-info")
253 if opts.image and not opts.build_info:
254 parser.error("--image requires --board-info")
Allen Webbe8c1da02023-09-08 18:25:22 +0000255 if opts.build_info or len(opts.package) == 1:
Allen Webb3e498aa2023-09-05 14:40:49 +0000256 opts.jobs = 1
257 return opts
258
259
260def check_package(
Allen Webbe8c1da02023-09-08 18:25:22 +0000261 package: portage_util.InstalledPackage,
Allen Webb3e498aa2023-09-05 14:40:49 +0000262 implicit: Set[str],
263 resolver: DotSoResolver,
264 match: bool,
265 debug: bool,
266) -> bool:
267 """Returns false if the package has missing dependencies"""
268 if not package:
269 print("missing package")
270 return False
271
272 provided = resolver.get_provided_from_all_deps(package)
273 if debug:
274 print("provided")
275 pprint.pprint(provided)
276
277 available = provided.union(implicit)
278 required = resolver.get_required_libs(package)
279 if debug:
280 print("required")
281 pprint.pprint(required)
282 unsatisfied = required - available
283 if unsatisfied:
284 cpvr = package.package_info.cpvr
285 print(f"'{cpvr}' missing deps for: ", end="")
286 pprint.pprint(unsatisfied)
287 if match:
288 missing = set()
289 for lib in unsatisfied:
Allen Webbe8c1da02023-09-08 18:25:22 +0000290 missing.update(resolver.lib_to_package(lib))
Allen Webb3e498aa2023-09-05 14:40:49 +0000291 if missing:
292 print(f"'{cpvr}' needs: ", end="")
293 pprint.pprint(missing)
294 return False
295 return True
296
297
298def main(argv: Optional[List[str]]):
299 """Main."""
300 opts = parse_arguments(argv)
301 opts.Freeze()
302
303 board = opts.board
304 root = build_target_lib.get_default_sysroot_path(board)
305 if board:
306 os.environ["PORTAGE_CONFIGROOT"] = root
307 os.environ["SYSROOT"] = root
308 os.environ["ROOT"] = root
309
310 failed = False
311 resolver = DotSoResolver(board, root)
Allen Webb3e498aa2023-09-05 14:40:49 +0000312
313 if not opts.package:
Allen Webbe8c1da02023-09-08 18:25:22 +0000314 if opts.build_info:
Allen Webb8fc0bba2023-09-11 14:37:25 +0000315 pkg = portage_util.InstalledPackage(resolver.db, opts.build_info)
316 image_path = opts.image or os.environ.get("D")
317 if image_path:
318 resolver.cache_libs_from_build(pkg, Path(image_path))
319 packages = [pkg]
Allen Webbe8c1da02023-09-08 18:25:22 +0000320 else:
321 packages = resolver.db.InstalledPackages()
Allen Webb3e498aa2023-09-05 14:40:49 +0000322 else:
323 packages = [resolver.get_package(p) for p in opts.package]
324
325 implicit = resolver.get_implicit_libs()
326 if opts.debug:
327 print("implicit")
328 pprint.pprint(implicit)
329
330 if opts.jobs == 1:
331 for package in packages:
332 if not check_package(
333 package,
Allen Webb3e498aa2023-09-05 14:40:49 +0000334 implicit,
335 resolver,
336 opts.match,
337 opts.debug,
338 ):
339 failed = True
340 else:
Allen Webbe8c1da02023-09-08 18:25:22 +0000341 if opts.match:
342 # Pre initialize the map before starting jobs.
343 resolver.lib_to_package()
Allen Webb3e498aa2023-09-05 14:40:49 +0000344 for ret in parallel.RunTasksInProcessPool(
345 lambda p: check_package(
Allen Webbe8c1da02023-09-08 18:25:22 +0000346 p, implicit, resolver, opts.match, opts.debug
Allen Webb3e498aa2023-09-05 14:40:49 +0000347 ),
348 [[p] for p in packages],
349 opts.jobs,
350 ):
351 if not ret:
352 failed = True
353
354 if failed:
355 sys.exit(1)
356
357
358if __name__ == "__main__":
359 main(sys.argv[1:])