Coverage for src/debputy/plugin/debputy/metadata_detectors.py: 96%
228 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-07 12:14 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-07 12:14 +0200
1import itertools
2import os
3import re
4import textwrap
5from typing import Iterable, Iterator
7from debputy.plugin.api import (
8 VirtualPath,
9 BinaryCtrlAccessor,
10 PackageProcessingContext,
11)
12from debputy.plugin.debputy.paths import (
13 INITRAMFS_HOOK_DIR,
14 SYSTEMD_TMPFILES_DIR,
15 GSETTINGS_SCHEMA_DIR,
16 SYSTEMD_SYSUSERS_DIR,
17)
18from debputy.plugin.debputy.types import DebputyCapability
19from debputy.util import assume_not_none, _warn
21DPKG_ROOT = '"${DPKG_ROOT}"'
22DPKG_ROOT_UNQUOTED = "${DPKG_ROOT}"
24KERNEL_MODULE_EXTENSIONS = tuple(
25 f"{ext}{comp_ext}"
26 for ext, comp_ext in itertools.product(
27 (".o", ".ko"),
28 ("", ".gz", ".bz2", ".xz"),
29 )
30)
33def detect_initramfs_hooks(
34 fs_root: VirtualPath,
35 ctrl: BinaryCtrlAccessor,
36 _unused: PackageProcessingContext,
37) -> None:
38 hook_dir = fs_root.lookup(INITRAMFS_HOOK_DIR)
39 if not hook_dir:
40 return
41 for _ in hook_dir.iterdir:
42 # Only add the trigger if the directory is non-empty. It is unlikely to matter a lot,
43 # but we do this to match debhelper.
44 break
45 else:
46 return
48 ctrl.dpkg_trigger("activate-noawait", "update-initramfs")
51def _all_tmpfiles_conf(fs_root: VirtualPath) -> Iterable[VirtualPath]:
52 seen_tmpfiles = set()
53 tmpfiles_dirs = [
54 SYSTEMD_TMPFILES_DIR,
55 "./etc/tmpfiles.d",
56 ]
57 for tmpfiles_dir_path in tmpfiles_dirs:
58 tmpfiles_dir = fs_root.lookup(tmpfiles_dir_path)
59 if not tmpfiles_dir:
60 continue
61 for path in tmpfiles_dir.iterdir:
62 if (
63 not path.is_file
64 or not path.name.endswith(".conf")
65 or path.name in seen_tmpfiles
66 ):
67 continue
68 seen_tmpfiles.add(path.name)
69 yield path
72def detect_systemd_tmpfiles(
73 fs_root: VirtualPath,
74 ctrl: BinaryCtrlAccessor,
75 _unused: PackageProcessingContext,
76) -> None:
77 tmpfiles_confs = [
78 x.name for x in sorted(_all_tmpfiles_conf(fs_root), key=lambda x: x.name)
79 ]
80 if not tmpfiles_confs:
81 return
83 tmpfiles_escaped = ctrl.maintscript.escape_shell_words(*tmpfiles_confs)
85 snippet = textwrap.dedent(
86 f"""\
87 if [ -x "$(command -v systemd-tmpfiles)" ]; then
88 systemd-tmpfiles ${{DPKG_ROOT:+--root="$DPKG_ROOT"}} --create {tmpfiles_escaped} || true
89 fi
90 """
91 )
93 ctrl.maintscript.on_configure(snippet)
96def _all_sysusers_conf(fs_root: VirtualPath) -> Iterable[VirtualPath]:
97 sysusers_dir = fs_root.lookup(SYSTEMD_SYSUSERS_DIR)
98 if not sysusers_dir:
99 return
100 for child in sysusers_dir.iterdir:
101 if not child.name.endswith(".conf"):
102 continue
103 yield child
106def detect_systemd_sysusers(
107 fs_root: VirtualPath,
108 ctrl: BinaryCtrlAccessor,
109 _unused: PackageProcessingContext,
110) -> None:
111 sysusers_confs = [p.name for p in _all_sysusers_conf(fs_root)]
112 if not sysusers_confs:
113 return
115 sysusers_escaped = ctrl.maintscript.escape_shell_words(*sysusers_confs)
117 snippet = textwrap.dedent(
118 f"""\
119 systemd-sysusers ${{DPKG_ROOT:+--root="$DPKG_ROOT"}} --create {sysusers_escaped} || true
120 """
121 )
123 ctrl.substvars.add_dependency(
124 "misc:Depends", "systemd | systemd-standalone-sysusers | systemd-sysusers"
125 )
126 ctrl.maintscript.on_configure(snippet)
129def detect_icons(
130 fs_root: VirtualPath,
131 ctrl: BinaryCtrlAccessor,
132 _unused: PackageProcessingContext,
133) -> None:
134 icons_root_dir = fs_root.lookup("./usr/share/icons")
135 if not icons_root_dir:
136 return
137 icon_dirs = []
138 for subdir in icons_root_dir.iterdir:
139 if subdir.name in ("gnome", "hicolor"):
140 # dh_icons skips this for some reason.
141 continue
142 for p in subdir.all_paths():
143 if p.is_file and p.name.endswith((".png", ".svg", ".xpm", ".icon")):
144 icon_dirs.append(subdir.absolute)
145 break
146 if not icon_dirs:
147 return
149 icon_dir_list_escaped = ctrl.maintscript.escape_shell_words(*icon_dirs)
151 postinst_snippet = textwrap.dedent(
152 f"""\
153 if command -v update-icon-caches >/dev/null; then
154 update-icon-caches {icon_dir_list_escaped}
155 fi
156 """
157 )
159 postrm_snippet = textwrap.dedent(
160 f"""\
161 if command -v update-icon-caches >/dev/null; then
162 update-icon-caches {icon_dir_list_escaped}
163 fi
164 """
165 )
167 ctrl.maintscript.on_configure(postinst_snippet)
168 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
171def detect_gsettings_dependencies(
172 fs_root: VirtualPath,
173 ctrl: BinaryCtrlAccessor,
174 _unused: PackageProcessingContext,
175) -> None:
176 gsettings_schema_dir = fs_root.lookup(GSETTINGS_SCHEMA_DIR)
177 if not gsettings_schema_dir:
178 return
180 for path in gsettings_schema_dir.all_paths():
181 if path.is_file and path.name.endswith((".xml", ".override")):
182 ctrl.substvars.add_dependency(
183 "misc:Depends", "dconf-gsettings-backend | gsettings-backend"
184 )
185 break
188def detect_kernel_modules(
189 fs_root: VirtualPath,
190 ctrl: BinaryCtrlAccessor,
191 _unused: PackageProcessingContext,
192) -> None:
193 for prefix in [".", "./usr"]:
194 module_root_dir = fs_root.lookup(f"{prefix}/lib/modules")
196 if not module_root_dir:
197 continue
199 module_version_dirs = []
201 for module_version_dir in module_root_dir.iterdir:
202 if not module_version_dir.is_dir:
203 continue
205 for fs_path in module_version_dir.all_paths():
206 if fs_path.name.endswith(KERNEL_MODULE_EXTENSIONS):
207 module_version_dirs.append(module_version_dir.name)
208 break
210 for module_version in module_version_dirs:
211 module_version_escaped = ctrl.maintscript.escape_shell_words(module_version)
212 postinst_snippet = textwrap.dedent(
213 f"""\
214 if [ -e /boot/System.map-{module_version_escaped} ]; then
215 depmod -a -F /boot/System.map-{module_version_escaped} {module_version_escaped} || true
216 fi
217 """
218 )
220 postrm_snippet = textwrap.dedent(
221 f"""\
222 if [ -e /boot/System.map-{module_version_escaped} ]; then
223 depmod -a -F /boot/System.map-{module_version_escaped} {module_version_escaped} || true
224 fi
225 """
226 )
228 ctrl.maintscript.on_configure(postinst_snippet)
229 # TODO: This should probably be on removal. However, this is what debhelper did and we should
230 # do the same until we are sure (not that it matters a lot).
231 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
234def detect_xfonts(
235 fs_root: VirtualPath,
236 ctrl: BinaryCtrlAccessor,
237 context: PackageProcessingContext,
238) -> None:
239 xfonts_root_dir = fs_root.lookup("./usr/share/fonts/X11/")
240 if not xfonts_root_dir:
241 return
243 cmds = []
244 cmds_postinst = []
245 cmds_postrm = []
246 escape_shell_words = ctrl.maintscript.escape_shell_words
247 package_name = context.binary_package.name
249 for xfonts_dir in xfonts_root_dir.iterdir:
250 xfonts_dirname = xfonts_dir.name
251 if not xfonts_dir.is_dir or xfonts_dirname.startswith("."):
252 continue
253 if fs_root.lookup(f"./etc/X11/xfonts/{xfonts_dirname}/{package_name}.scale"):
254 cmds.append(escape_shell_words("update-fonts-scale", xfonts_dirname))
255 cmds.append(
256 escape_shell_words("update-fonts-dir", "--x11r7-layout", xfonts_dirname)
257 )
258 alias_file = fs_root.lookup(
259 f"./etc/X11/xfonts/{xfonts_dirname}/{package_name}.alias"
260 )
261 if alias_file:
262 cmds_postinst.append(
263 escape_shell_words(
264 "update-fonts-alias",
265 "--include",
266 alias_file.absolute,
267 xfonts_dirname,
268 )
269 )
270 cmds_postrm.append(
271 escape_shell_words(
272 "update-fonts-alias",
273 "--exclude",
274 alias_file.absolute,
275 xfonts_dirname,
276 )
277 )
279 if not cmds:
280 return
282 postinst_snippet = textwrap.dedent(
283 f"""\
284 if command -v update-fonts-dir >/dev/null; then
285 {';'.join(itertools.chain(cmds, cmds_postinst))}
286 fi
287 """
288 )
290 postrm_snippet = textwrap.dedent(
291 f"""\
292 if [ -x "`command -v update-fonts-dir`" ]; then
293 {';'.join(itertools.chain(cmds, cmds_postrm))}
294 fi
295 """
296 )
298 ctrl.maintscript.unconditionally_in_script("postinst", postinst_snippet)
299 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
300 ctrl.substvars.add_dependency("misc:Depends", "xfonts-utils")
303# debputy does not support python2, so we do not list python / python2.
304_PYTHON_PUBLIC_DIST_DIR_NAMES = re.compile(r"(?:pypy|python)3(?:[.]\d+)?")
307def _public_python_dist_dirs(fs_root: VirtualPath) -> Iterator[VirtualPath]:
308 usr_lib = fs_root.lookup("./usr/lib")
309 root_dirs = []
310 if usr_lib:
311 root_dirs.append(usr_lib)
313 dbg_root = fs_root.lookup("./usr/lib/debug/usr/lib")
314 if dbg_root: 314 ↛ 315line 314 didn't jump to line 315, because the condition on line 314 was never true
315 root_dirs.append(dbg_root)
317 for root_dir in root_dirs:
318 python_dirs = (
319 path
320 for path in root_dir.iterdir
321 if path.is_dir and _PYTHON_PUBLIC_DIST_DIR_NAMES.match(path.name)
322 )
323 for python_dir in python_dirs:
324 dist_packages = python_dir.get("dist-packages")
325 if not dist_packages:
326 continue
327 yield dist_packages
330def _has_py_file_in_dir(d: VirtualPath) -> bool:
331 return any(f.is_file and f.name.endswith(".py") for f in d.all_paths()) 331 ↛ exitline 331 didn't finish the generator expression on line 331
334def detect_pycompile_files(
335 fs_root: VirtualPath,
336 ctrl: BinaryCtrlAccessor,
337 context: PackageProcessingContext,
338) -> None:
339 package = context.binary_package.name
340 # TODO: Support configurable list of private dirs
341 private_search_dirs = [
342 fs_root.lookup(os.path.join(d, package))
343 for d in [
344 "./usr/share",
345 "./usr/share/games",
346 "./usr/lib",
347 f"./usr/lib/{context.binary_package.deb_multiarch}",
348 "./usr/lib/games",
349 ]
350 ]
351 private_search_dirs_with_py_files = [
352 p for p in private_search_dirs if p is not None and _has_py_file_in_dir(p)
353 ]
354 public_search_dirs_has_py_files = any(
355 p is not None and _has_py_file_in_dir(p)
356 for p in _public_python_dist_dirs(fs_root)
357 )
359 if not public_search_dirs_has_py_files and not private_search_dirs_with_py_files:
360 return
362 # The dh_python3 helper also supports -V and -X. We do not use them. They can be
363 # replaced by bcep support instead, which is how we will be supporting this kind
364 # of configuration down the line.
365 ctrl.maintscript.unconditionally_in_script(
366 "prerm",
367 textwrap.dedent(
368 f"""\
369 if command -v py3clean >/dev/null 2>&1; then
370 py3clean -p {package}
371 else
372 dpkg -L {package} | sed -En -e '/^(.*)\\/(.+)\\.py$/s,,rm "\\1/__pycache__/\\2".*,e'
373 find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir
374 fi
375 """
376 ),
377 )
378 if public_search_dirs_has_py_files:
379 ctrl.maintscript.on_configure(
380 textwrap.dedent(
381 f"""\
382 if command -v py3compile >/dev/null 2>&1; then
383 py3compile -p {package}
384 fi
385 if command -v pypy3compile >/dev/null 2>&1; then
386 pypy3compile -p {package} || true
387 fi
388 """
389 )
390 )
391 for private_dir in private_search_dirs_with_py_files:
392 escaped_dir = ctrl.maintscript.escape_shell_words(private_dir.absolute)
393 ctrl.maintscript.on_configure(
394 textwrap.dedent(
395 f"""\
396 if command -v py3compile >/dev/null 2>&1; then
397 py3compile -p {package} {escaped_dir}
398 fi
399 if command -v pypy3compile >/dev/null 2>&1; then
400 pypy3compile -p {package} {escaped_dir} || true
401 fi
402 """
403 )
404 )
407def translate_capabilities(
408 fs_root: VirtualPath,
409 ctrl: BinaryCtrlAccessor,
410 _context: PackageProcessingContext,
411) -> None:
412 caps = []
413 maintscript = ctrl.maintscript
414 for p in fs_root.all_paths():
415 if not p.is_file:
416 continue
417 metadata_ref = p.metadata(DebputyCapability)
418 capability = metadata_ref.value
419 if capability is None:
420 continue
422 abs_path = maintscript.escape_shell_words(p.absolute)
424 cap_script = "".join(
425 [
426 " # Triggered by: {DEFINITION_SOURCE}\n"
427 " _TPATH=$(dpkg-divert --truename {ABS_PATH})\n",
428 ' if setcap {CAP} "{DPKG_ROOT_UNQUOTED}${{_TPATH}}"; then\n',
429 ' chmod {MODE} "{DPKG_ROOT_UNQUOTED}${{_TPATH}}"\n',
430 ' echo "Successfully applied capabilities {CAP} on ${{_TPATH}}"\n',
431 " else\n",
432 # We do not reset the mode here; generally a re-install or upgrade would re-store both mode,
433 # and remove the capabilities.
434 ' echo "The setcap failed to processes {CAP} on ${{_TPATH}}; falling back to no capability support" >&2\n',
435 " fi\n",
436 ]
437 ).format(
438 CAP=maintscript.escape_shell_words(capability.capabilities).replace(
439 "\\+", "+"
440 ),
441 DPKG_ROOT_UNQUOTED=DPKG_ROOT_UNQUOTED,
442 ABS_PATH=abs_path,
443 MODE=maintscript.escape_shell_words(str(capability.capability_mode)),
444 DEFINITION_SOURCE=capability.definition_source.replace("\n", "\\n"),
445 )
446 assert cap_script.endswith("\n")
447 caps.append(cap_script)
449 if not caps:
450 return
452 maintscript.on_configure(
453 textwrap.dedent(
454 """\
455 if command -v setcap > /dev/null; then
456 {SET_CAP_COMMANDS}
457 unset _TPATH
458 else
459 echo "The setcap utility is not installed available; falling back to no capability support" >&2
460 fi
461 """
462 ).format(
463 SET_CAP_COMMANDS="".join(caps).rstrip("\n"),
464 )
465 )
468def pam_auth_update(
469 fs_root: VirtualPath,
470 ctrl: BinaryCtrlAccessor,
471 _context: PackageProcessingContext,
472) -> None:
473 pam_configs = fs_root.lookup("/usr/share/pam-configs")
474 if not pam_configs:
475 return
476 maintscript = ctrl.maintscript
477 for pam_config in pam_configs.iterdir:
478 if not pam_config.is_file: 478 ↛ 479line 478 didn't jump to line 479, because the condition on line 478 was never true
479 continue
480 maintscript.on_configure("pam-auth-update --package\n")
481 maintscript.on_before_removal(
482 textwrap.dedent(
483 f"""\
484 if [ "${{DPKG_MAINTSCRIPT_PACKAGE_REFCOUNT:-1}}" = 1 ]; then
485 pam-auth-update --package --remove {maintscript.escape_shell_words(pam_config.name)}
486 fi
487 """
488 )
489 )
492def auto_depends_arch_any_solink(
493 fs_foot: VirtualPath,
494 ctrl: BinaryCtrlAccessor,
495 context: PackageProcessingContext,
496) -> None:
497 package = context.binary_package
498 if package.is_arch_all:
499 return
500 libbasedir = fs_foot.lookup("usr/lib")
501 if not libbasedir:
502 return
503 libmadir = libbasedir.get(package.deb_multiarch)
504 if libmadir: 504 ↛ 507line 504 didn't jump to line 507, because the condition on line 504 was never false
505 libdirs = [libmadir, libbasedir]
506 else:
507 libdirs = [libbasedir]
508 targets = []
509 for libdir in libdirs:
510 for path in libdir.iterdir:
511 if not path.is_symlink or not path.name.endswith(".so"):
512 continue
513 target = path.readlink()
514 resolved = assume_not_none(path.parent_dir).lookup(target)
515 if resolved is not None: 515 ↛ 516line 515 didn't jump to line 516, because the condition on line 515 was never true
516 continue
517 targets.append((libdir.path, target))
519 roots = list(context.accessible_package_roots())
520 if not roots:
521 return
523 for libdir, target in targets:
524 final_path = os.path.join(libdir, target)
525 matches = []
526 for opkg, ofs_root in roots:
527 m = ofs_root.lookup(final_path)
528 if not m: 528 ↛ 529line 528 didn't jump to line 529, because the condition on line 528 was never true
529 continue
530 matches.append(opkg)
531 if not matches or len(matches) > 1:
532 if matches: 532 ↛ 539line 532 didn't jump to line 539, because the condition on line 532 was never false
533 all_matches = ", ".join(p.name for p in matches)
534 _warn(
535 f"auto-depends-solink: The {final_path} was found in multiple packages ({all_matches}):"
536 f" Not generating a dependency."
537 )
538 else:
539 _warn(
540 f"auto-depends-solink: The {final_path} was NOT found in any accessible package:"
541 " Not generating a dependency. This detection only works when both packages are arch:any"
542 " and they have the same build-profiles."
543 )
544 continue
545 pkg_dep = matches[0]
546 # The debputy API should not allow this constraint to fail
547 assert pkg_dep.is_arch_all == package.is_arch_all
548 # If both packages are arch:all or both are arch:any, we can generate a tight dependency
549 relation = f"{pkg_dep.name} (= ${{binary:Version}})"
550 ctrl.substvars.add_dependency("misc:Depends", relation)