blob: b4e8598223881ba0b8c17669c4bca93a816bdfbc [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."""
83 return set(env_to_libs(package.requires or ""))
84
85 def get_deps(self, package):
86 """Return a list of dependencies."""
87 cpvr = f"{package.category}/{package.pf}"
88 return portage_util.GetFlattenedDepsForPackage(
89 cpvr, board=self.board, depth=1
90 )
91
92 def get_implicit_libs(self):
93 """Return a set of .so files that are provided by the system."""
94 implicit_libs = set()
95 for dep, from_sdk in (
96 ("cross-aarch64-cros-linux-gnu/glibc", True),
97 ("cross-armv7a-cros-linux-gnueabihf/glibc", True),
98 ("cross-i686-cros-linux-gnu/glibc", True),
99 ("cross-x86_64-cros-linux-gnu/glibc", True),
100 ("sys-libs/glibc", False),
101 ("sys-libs/libcxx", False),
102 ("sys-libs/llvm-libunwind", False),
103 ):
104 pkg = self.get_package(dep, from_sdk)
105 if not pkg:
106 continue
107 implicit_libs.update(self.provided_libs(pkg))
108 return implicit_libs
109
110 def provided_libs(self, package: portage_util.InstalledPackage) -> Set[str]:
111 """Return a set of .so files provided by |package|."""
112 cpvr = f"{package.category}/{package.pf}"
113 if cpvr in self.provided_libs_cache:
114 return self.provided_libs_cache[cpvr]
115
116 libs = set()
117 contents = package.ListContents()
118 # Keep only the .so files
119 for typ, path in contents:
120 if typ == package.DIR:
121 continue
122 filename = os.path.basename(path)
123 if filename.endswith(".so") or ".so." in filename:
124 libs.add(filename)
125 self.provided_libs_cache[cpvr] = libs
126 return libs
127
128 def get_provided_from_all_deps(
129 self, package: portage_util.InstalledPackage
130 ) -> Set[str]:
131 """Return a set of .so files provided by the immediate dependencies."""
132 provided_libs = set()
133 deps = self.get_deps(package)
134 for dep in deps:
135 info = package_info.parse(dep)
136 pkg = self.db.GetInstalledPackage(info.category, info.pvr)
137 if pkg:
138 provided_libs.update(self.provided_libs(pkg))
139 return provided_libs
140
Allen Webbe8c1da02023-09-08 18:25:22 +0000141 def lib_to_package(self, lib_filename: str = None) -> Set[str]:
142 """Return a set of packages that contain the library."""
143 if self.lib_to_package_map is None:
144 lookup = collections.defaultdict(set)
145 for pkg in self.db.InstalledPackages():
146 cpvr = f"{pkg.category}/{pkg.pf}"
147 # Packages with bundled libs for internal use and/or standaline
148 # binary packages.
149 if f"{pkg.category}/{pkg.package}" in (
150 "app-emulation/qemu",
151 "chromeos-base/aosp-frameworks-ml-nn-vts",
152 "chromeos-base/factory",
153 "chromeos-base/signingtools-bin",
154 "sys-devel/gcc-bin",
155 ):
156 continue
157 for lib in set(self.provided_libs(pkg)):
158 lookup[lib].add(cpvr)
159 self.lib_to_package_map = lookup
160 else:
161 lookup = self.lib_to_package_map
162 if not lib_filename:
163 return set()
164 try:
165 return lookup[lib_filename]
166 except KeyError:
167 return set()
Allen Webb3e498aa2023-09-05 14:40:49 +0000168
169
170def get_parser() -> commandline.ArgumentParser:
171 """Build the argument parser."""
172 parser = commandline.ArgumentParser(description=__doc__)
173
174 parser.add_argument("package", nargs="*", help="package atom")
175
176 parser.add_argument(
177 "-b",
178 "--board",
179 "--build-target",
180 default=cros_build_lib.GetDefaultBoard(),
181 help="ChromeOS board (Uses the SDK if not specified)",
182 )
183
184 parser.add_argument(
Allen Webbe8c1da02023-09-08 18:25:22 +0000185 "-i",
186 "--build-info",
187 default=None,
188 type=Path,
189 help="Path to build-info folder post src_install",
190 )
191
192 parser.add_argument(
Allen Webb3e498aa2023-09-05 14:40:49 +0000193 "--match",
194 default=False,
195 action="store_true",
196 help="Try to match missing libraries",
197 )
198
199 parser.add_argument(
200 "-j",
201 "--jobs",
202 default=None,
203 type=int,
204 help="Number of parallel processes",
205 )
206
207 return parser
208
209
210def parse_arguments(argv: List[str]) -> argparse.Namespace:
211 """Parse and validate arguments."""
212 parser = get_parser()
213 opts = parser.parse_args(argv)
Allen Webbe8c1da02023-09-08 18:25:22 +0000214 if opts.build_info and opts.package:
215 raise Exception("Do not specify a package when setting --board-info")
216 if opts.build_info or len(opts.package) == 1:
Allen Webb3e498aa2023-09-05 14:40:49 +0000217 opts.jobs = 1
218 return opts
219
220
221def check_package(
Allen Webbe8c1da02023-09-08 18:25:22 +0000222 package: portage_util.InstalledPackage,
Allen Webb3e498aa2023-09-05 14:40:49 +0000223 implicit: Set[str],
224 resolver: DotSoResolver,
225 match: bool,
226 debug: bool,
227) -> bool:
228 """Returns false if the package has missing dependencies"""
229 if not package:
230 print("missing package")
231 return False
232
233 provided = resolver.get_provided_from_all_deps(package)
234 if debug:
235 print("provided")
236 pprint.pprint(provided)
237
238 available = provided.union(implicit)
239 required = resolver.get_required_libs(package)
240 if debug:
241 print("required")
242 pprint.pprint(required)
243 unsatisfied = required - available
244 if unsatisfied:
245 cpvr = package.package_info.cpvr
246 print(f"'{cpvr}' missing deps for: ", end="")
247 pprint.pprint(unsatisfied)
248 if match:
249 missing = set()
250 for lib in unsatisfied:
Allen Webbe8c1da02023-09-08 18:25:22 +0000251 missing.update(resolver.lib_to_package(lib))
Allen Webb3e498aa2023-09-05 14:40:49 +0000252 if missing:
253 print(f"'{cpvr}' needs: ", end="")
254 pprint.pprint(missing)
255 return False
256 return True
257
258
259def main(argv: Optional[List[str]]):
260 """Main."""
261 opts = parse_arguments(argv)
262 opts.Freeze()
263
264 board = opts.board
265 root = build_target_lib.get_default_sysroot_path(board)
266 if board:
267 os.environ["PORTAGE_CONFIGROOT"] = root
268 os.environ["SYSROOT"] = root
269 os.environ["ROOT"] = root
270
271 failed = False
272 resolver = DotSoResolver(board, root)
Allen Webb3e498aa2023-09-05 14:40:49 +0000273
274 if not opts.package:
Allen Webbe8c1da02023-09-08 18:25:22 +0000275 if opts.build_info:
276 packages = [
277 portage_util.InstalledPackage(resolver.db, opts.build_info)
278 ]
279 else:
280 packages = resolver.db.InstalledPackages()
Allen Webb3e498aa2023-09-05 14:40:49 +0000281 else:
282 packages = [resolver.get_package(p) for p in opts.package]
283
284 implicit = resolver.get_implicit_libs()
285 if opts.debug:
286 print("implicit")
287 pprint.pprint(implicit)
288
289 if opts.jobs == 1:
290 for package in packages:
291 if not check_package(
292 package,
Allen Webb3e498aa2023-09-05 14:40:49 +0000293 implicit,
294 resolver,
295 opts.match,
296 opts.debug,
297 ):
298 failed = True
299 else:
Allen Webbe8c1da02023-09-08 18:25:22 +0000300 if opts.match:
301 # Pre initialize the map before starting jobs.
302 resolver.lib_to_package()
Allen Webb3e498aa2023-09-05 14:40:49 +0000303 for ret in parallel.RunTasksInProcessPool(
304 lambda p: check_package(
Allen Webbe8c1da02023-09-08 18:25:22 +0000305 p, implicit, resolver, opts.match, opts.debug
Allen Webb3e498aa2023-09-05 14:40:49 +0000306 ),
307 [[p] for p in packages],
308 opts.jobs,
309 ):
310 if not ret:
311 failed = True
312
313 if failed:
314 sys.exit(1)
315
316
317if __name__ == "__main__":
318 main(sys.argv[1:])