blob: a9039b7aad4473a55e53e013438c39d15aa48333 [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
18import pprint
19import sys
20from typing import Dict, List, Optional, Set, Union
21
22from chromite.lib import build_target_lib
23from chromite.lib import chroot_lib
24from chromite.lib import commandline
25from chromite.lib import cros_build_lib
26from chromite.lib import parallel
27from chromite.lib import portage_util
28from chromite.lib.parser import package_info
29
30
31def env_to_libs(var: str) -> List[str]:
32 """Converts value of REQUIRES to a list of .so files.
33
34 For example:
35 "arm_32: libRSSupport.so libblasV8.so libc.so ..."
36 Becomes:
37 ["libRSSupport.so", "libblasV8.so", "libc.so", ...]
38 """
39 return [x for x in var.split() if not x.endswith(":")]
40
41
42class DotSoResolver:
43 """Provides shared library related dependency operations."""
44
45 def __init__(
46 self,
47 board: Optional[str] = None,
48 root: Union[os.PathLike, str] = "/",
49 chroot: Optional[chroot_lib.Chroot] = None,
50 ):
51 self.board = board
52 self.chroot = chroot if chroot else chroot_lib.Chroot()
53
54 self.sdk_db = portage_util.PortageDB()
55 self.db = self.sdk_db if root == "/" else portage_util.PortageDB(root)
56 self.provided_libs_cache = {}
57
58 def get_package(
59 self, query: str, from_sdk=False
60 ) -> Optional[portage_util.InstalledPackage]:
61 """Try to find an InstalledPackage for the provided package string"""
62 packages = (self.sdk_db if from_sdk else self.db).InstalledPackages()
63 info = package_info.parse(query)
64 for package in packages:
65 if info.package != package.package:
66 continue
67 if info.category != package.category:
68 continue
69 dep_info = package.package_info
70 if info.revision and info.revision != dep_info.revision:
71 continue
72 if info.pv and info.pv != dep_info.pv:
73 continue
74 return package
75 return None
76
77 def get_required_libs(self, package) -> Set[str]:
78 """Return a set of required .so files."""
79 return set(env_to_libs(package.requires or ""))
80
81 def get_deps(self, package):
82 """Return a list of dependencies."""
83 cpvr = f"{package.category}/{package.pf}"
84 return portage_util.GetFlattenedDepsForPackage(
85 cpvr, board=self.board, depth=1
86 )
87
88 def get_implicit_libs(self):
89 """Return a set of .so files that are provided by the system."""
90 implicit_libs = set()
91 for dep, from_sdk in (
92 ("cross-aarch64-cros-linux-gnu/glibc", True),
93 ("cross-armv7a-cros-linux-gnueabihf/glibc", True),
94 ("cross-i686-cros-linux-gnu/glibc", True),
95 ("cross-x86_64-cros-linux-gnu/glibc", True),
96 ("sys-libs/glibc", False),
97 ("sys-libs/libcxx", False),
98 ("sys-libs/llvm-libunwind", False),
99 ):
100 pkg = self.get_package(dep, from_sdk)
101 if not pkg:
102 continue
103 implicit_libs.update(self.provided_libs(pkg))
104 return implicit_libs
105
106 def provided_libs(self, package: portage_util.InstalledPackage) -> Set[str]:
107 """Return a set of .so files provided by |package|."""
108 cpvr = f"{package.category}/{package.pf}"
109 if cpvr in self.provided_libs_cache:
110 return self.provided_libs_cache[cpvr]
111
112 libs = set()
113 contents = package.ListContents()
114 # Keep only the .so files
115 for typ, path in contents:
116 if typ == package.DIR:
117 continue
118 filename = os.path.basename(path)
119 if filename.endswith(".so") or ".so." in filename:
120 libs.add(filename)
121 self.provided_libs_cache[cpvr] = libs
122 return libs
123
124 def get_provided_from_all_deps(
125 self, package: portage_util.InstalledPackage
126 ) -> Set[str]:
127 """Return a set of .so files provided by the immediate dependencies."""
128 provided_libs = set()
129 deps = self.get_deps(package)
130 for dep in deps:
131 info = package_info.parse(dep)
132 pkg = self.db.GetInstalledPackage(info.category, info.pvr)
133 if pkg:
134 provided_libs.update(self.provided_libs(pkg))
135 return provided_libs
136
137 def lib_to_package_map(self) -> Dict[str, Set[str]]:
138 """Return dict mapping libraries to packages."""
139 lookup = collections.defaultdict(set)
140 for pkg in self.db.InstalledPackages():
141 cpvr = f"{pkg.category}/{pkg.pf}"
142 # Packages with bundled libs for internal use and/or standaline
143 # binary packages.
144 if f"{pkg.category}/{pkg.package}" in (
145 "app-emulation/qemu",
146 "chromeos-base/aosp-frameworks-ml-nn-vts",
147 "chromeos-base/factory",
148 "chromeos-base/signingtools-bin",
149 "sys-devel/gcc-bin",
150 ):
151 continue
152 for lib in set(self.provided_libs(pkg)):
153 lookup[lib].add(cpvr)
154 return lookup
155
156
157def get_parser() -> commandline.ArgumentParser:
158 """Build the argument parser."""
159 parser = commandline.ArgumentParser(description=__doc__)
160
161 parser.add_argument("package", nargs="*", help="package atom")
162
163 parser.add_argument(
164 "-b",
165 "--board",
166 "--build-target",
167 default=cros_build_lib.GetDefaultBoard(),
168 help="ChromeOS board (Uses the SDK if not specified)",
169 )
170
171 parser.add_argument(
172 "--match",
173 default=False,
174 action="store_true",
175 help="Try to match missing libraries",
176 )
177
178 parser.add_argument(
179 "-j",
180 "--jobs",
181 default=None,
182 type=int,
183 help="Number of parallel processes",
184 )
185
186 return parser
187
188
189def parse_arguments(argv: List[str]) -> argparse.Namespace:
190 """Parse and validate arguments."""
191 parser = get_parser()
192 opts = parser.parse_args(argv)
193 if len(opts.package) == 1:
194 opts.jobs = 1
195 return opts
196
197
198def check_package(
199 package,
200 lib_to_package: Dict[str, List[str]],
201 implicit: Set[str],
202 resolver: DotSoResolver,
203 match: bool,
204 debug: bool,
205) -> bool:
206 """Returns false if the package has missing dependencies"""
207 if not package:
208 print("missing package")
209 return False
210
211 provided = resolver.get_provided_from_all_deps(package)
212 if debug:
213 print("provided")
214 pprint.pprint(provided)
215
216 available = provided.union(implicit)
217 required = resolver.get_required_libs(package)
218 if debug:
219 print("required")
220 pprint.pprint(required)
221 unsatisfied = required - available
222 if unsatisfied:
223 cpvr = package.package_info.cpvr
224 print(f"'{cpvr}' missing deps for: ", end="")
225 pprint.pprint(unsatisfied)
226 if match:
227 missing = set()
228 for lib in unsatisfied:
229 missing.update(lib_to_package[lib])
230 if missing:
231 print(f"'{cpvr}' needs: ", end="")
232 pprint.pprint(missing)
233 return False
234 return True
235
236
237def main(argv: Optional[List[str]]):
238 """Main."""
239 opts = parse_arguments(argv)
240 opts.Freeze()
241
242 board = opts.board
243 root = build_target_lib.get_default_sysroot_path(board)
244 if board:
245 os.environ["PORTAGE_CONFIGROOT"] = root
246 os.environ["SYSROOT"] = root
247 os.environ["ROOT"] = root
248
249 failed = False
250 resolver = DotSoResolver(board, root)
251 lib_to_package = {}
252 if opts.match:
253 lib_to_package = resolver.lib_to_package_map()
254
255 if not opts.package:
256 packages = resolver.db.InstalledPackages()
257 else:
258 packages = [resolver.get_package(p) for p in opts.package]
259
260 implicit = resolver.get_implicit_libs()
261 if opts.debug:
262 print("implicit")
263 pprint.pprint(implicit)
264
265 if opts.jobs == 1:
266 for package in packages:
267 if not check_package(
268 package,
269 lib_to_package,
270 implicit,
271 resolver,
272 opts.match,
273 opts.debug,
274 ):
275 failed = True
276 else:
277 for ret in parallel.RunTasksInProcessPool(
278 lambda p: check_package(
279 p, lib_to_package, implicit, resolver, opts.match, opts.debug
280 ),
281 [[p] for p in packages],
282 opts.jobs,
283 ):
284 if not ret:
285 failed = True
286
287 if failed:
288 sys.exit(1)
289
290
291if __name__ == "__main__":
292 main(sys.argv[1:])