Coverage for src/debputy/plugin/debputy/metadata_detectors.py: 96%
228 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-01-27 13:59 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-01-27 13:59 +0000
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 \
88 if [ -x "$(command -v systemd-tmpfiles)" ]; then
89 systemd-tmpfiles ${ DPKG_ROOT:+--root="$DPKG_ROOT"} --create {tmpfiles_escaped} || true
90 fi
91 """
92 )
94 ctrl.maintscript.on_configure(snippet)
97def _all_sysusers_conf(fs_root: VirtualPath) -> Iterable[VirtualPath]:
98 sysusers_dir = fs_root.lookup(SYSTEMD_SYSUSERS_DIR)
99 if not sysusers_dir:
100 return
101 for child in sysusers_dir.iterdir:
102 if not child.name.endswith(".conf"):
103 continue
104 yield child
107def detect_systemd_sysusers(
108 fs_root: VirtualPath,
109 ctrl: BinaryCtrlAccessor,
110 _unused: PackageProcessingContext,
111) -> None:
112 sysusers_confs = [p.name for p in _all_sysusers_conf(fs_root)]
113 if not sysusers_confs:
114 return
116 sysusers_escaped = ctrl.maintscript.escape_shell_words(*sysusers_confs)
118 snippet = textwrap.dedent(
119 f"""\
120 \
121 systemd-sysusers ${ DPKG_ROOT:+--root="$DPKG_ROOT"} --create {sysusers_escaped} || true
122 """
123 )
125 ctrl.substvars.add_dependency(
126 "misc:Depends", "systemd | systemd-standalone-sysusers | systemd-sysusers"
127 )
128 ctrl.maintscript.on_configure(snippet)
131def detect_icons(
132 fs_root: VirtualPath,
133 ctrl: BinaryCtrlAccessor,
134 _unused: PackageProcessingContext,
135) -> None:
136 icons_root_dir = fs_root.lookup("./usr/share/icons")
137 if not icons_root_dir:
138 return
139 icon_dirs = []
140 for subdir in icons_root_dir.iterdir:
141 if subdir.name in ("gnome", "hicolor"):
142 # dh_icons skips this for some reason.
143 continue
144 for p in subdir.all_paths():
145 if p.is_file and p.name.endswith((".png", ".svg", ".xpm", ".icon")):
146 icon_dirs.append(subdir.absolute)
147 break
148 if not icon_dirs:
149 return
151 icon_dir_list_escaped = ctrl.maintscript.escape_shell_words(*icon_dirs)
153 postinst_snippet = textwrap.dedent(
154 f"""\
155 \
156 if command -v update-icon-caches >/dev/null; then
157 update-icon-caches {icon_dir_list_escaped}
158 fi
159 """
160 )
162 postrm_snippet = textwrap.dedent(
163 f"""\
164 \
165 if command -v update-icon-caches >/dev/null; then
166 update-icon-caches {icon_dir_list_escaped}
167 fi
168 """
169 )
171 ctrl.maintscript.on_configure(postinst_snippet)
172 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
175def detect_gsettings_dependencies(
176 fs_root: VirtualPath,
177 ctrl: BinaryCtrlAccessor,
178 _unused: PackageProcessingContext,
179) -> None:
180 gsettings_schema_dir = fs_root.lookup(GSETTINGS_SCHEMA_DIR)
181 if not gsettings_schema_dir:
182 return
184 for path in gsettings_schema_dir.all_paths():
185 if path.is_file and path.name.endswith((".xml", ".override")):
186 ctrl.substvars.add_dependency(
187 "misc:Depends", "dconf-gsettings-backend | gsettings-backend"
188 )
189 break
192def detect_kernel_modules(
193 fs_root: VirtualPath,
194 ctrl: BinaryCtrlAccessor,
195 _unused: PackageProcessingContext,
196) -> None:
197 for prefix in [".", "./usr"]:
198 module_root_dir = fs_root.lookup(f"{prefix}/lib/modules")
200 if not module_root_dir:
201 continue
203 module_version_dirs = []
205 for module_version_dir in module_root_dir.iterdir:
206 if not module_version_dir.is_dir:
207 continue
209 for fs_path in module_version_dir.all_paths():
210 if fs_path.name.endswith(KERNEL_MODULE_EXTENSIONS):
211 module_version_dirs.append(module_version_dir.name)
212 break
214 for module_version in module_version_dirs:
215 module_version_escaped = ctrl.maintscript.escape_shell_words(module_version)
216 postinst_snippet = textwrap.dedent(
217 f"""\
218 \
219 if [ -e /boot/System.map-{module_version_escaped} ]; then
220 depmod -a -F /boot/System.map-{module_version_escaped} {module_version_escaped} || true
221 fi
222 """
223 )
225 postrm_snippet = textwrap.dedent(
226 f"""\
227 \
228 if [ -e /boot/System.map-{module_version_escaped} ]; then
229 depmod -a -F /boot/System.map-{module_version_escaped} {module_version_escaped} || true
230 fi
231 """
232 )
234 ctrl.maintscript.on_configure(postinst_snippet)
235 # TODO: This should probably be on removal. However, this is what debhelper did and we should
236 # do the same until we are sure (not that it matters a lot).
237 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
240def detect_xfonts(
241 fs_root: VirtualPath,
242 ctrl: BinaryCtrlAccessor,
243 context: PackageProcessingContext,
244) -> None:
245 xfonts_root_dir = fs_root.lookup("./usr/share/fonts/X11/")
246 if not xfonts_root_dir:
247 return
249 cmds = []
250 cmds_postinst = []
251 cmds_postrm = []
252 escape_shell_words = ctrl.maintscript.escape_shell_words
253 package_name = context.binary_package.name
255 for xfonts_dir in xfonts_root_dir.iterdir:
256 xfonts_dirname = xfonts_dir.name
257 if not xfonts_dir.is_dir or xfonts_dirname.startswith("."):
258 continue
259 if fs_root.lookup(f"./etc/X11/xfonts/{xfonts_dirname}/{package_name}.scale"):
260 cmds.append(escape_shell_words("update-fonts-scale", xfonts_dirname))
261 cmds.append(
262 escape_shell_words("update-fonts-dir", "--x11r7-layout", xfonts_dirname)
263 )
264 alias_file = fs_root.lookup(
265 f"./etc/X11/xfonts/{xfonts_dirname}/{package_name}.alias"
266 )
267 if alias_file:
268 cmds_postinst.append(
269 escape_shell_words(
270 "update-fonts-alias",
271 "--include",
272 alias_file.absolute,
273 xfonts_dirname,
274 )
275 )
276 cmds_postrm.append(
277 escape_shell_words(
278 "update-fonts-alias",
279 "--exclude",
280 alias_file.absolute,
281 xfonts_dirname,
282 )
283 )
285 if not cmds:
286 return
288 postinst_snippet = textwrap.dedent(
289 f"""\
290 \
291 if command -v update-fonts-dir >/dev/null; then
292 {';'.join(itertools.chain(cmds, cmds_postinst))}
293 fi
294 """
295 )
297 postrm_snippet = textwrap.dedent(
298 f"""\
299 \
300 if [ -x "`command -v update-fonts-dir`" ]; then
301 {';'.join(itertools.chain(cmds, cmds_postrm))}
302 fi
303 """
304 )
306 ctrl.maintscript.unconditionally_in_script("postinst", postinst_snippet)
307 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
308 ctrl.substvars.add_dependency("misc:Depends", "xfonts-utils")
311# debputy does not support python2, so we do not list python / python2.
312_PYTHON_PUBLIC_DIST_DIR_NAMES = re.compile(r"(?:pypy|python)3(?:[.]\d+)?")
314 314 ↛ 315line 314 didn't jump to line 315 because the condition on line 314 was never true
315def _public_python_dist_dirs(fs_root: VirtualPath) -> Iterator[VirtualPath]:
316 usr_lib = fs_root.lookup("./usr/lib")
317 root_dirs = []
318 if usr_lib:
319 root_dirs.append(usr_lib)
321 dbg_root = fs_root.lookup("./usr/lib/debug/usr/lib")
322 if dbg_root:
323 root_dirs.append(dbg_root)
325 for root_dir in root_dirs:
326 python_dirs = (
327 path
328 for path in root_dir.iterdir
329 if path.is_dir and _PYTHON_PUBLIC_DIST_DIR_NAMES.match(path.name)
330 )
331 for python_dir in python_dirs: 331 ↛ exitline 331 didn't finish the generator expression on line 331
332 dist_packages = python_dir.get("dist-packages")
333 if not dist_packages:
334 continue
335 yield dist_packages
338def _has_py_file_in_dir(d: VirtualPath) -> bool:
339 return any(f.is_file and f.name.endswith(".py") for f in d.all_paths())
342def detect_pycompile_files(
343 fs_root: VirtualPath,
344 ctrl: BinaryCtrlAccessor,
345 context: PackageProcessingContext,
346) -> None:
347 package = context.binary_package.name
348 # TODO: Support configurable list of private dirs
349 private_search_dirs = [
350 fs_root.lookup(os.path.join(d, package))
351 for d in [
352 "./usr/share",
353 "./usr/share/games",
354 "./usr/lib",
355 f"./usr/lib/{context.binary_package.deb_multiarch}",
356 "./usr/lib/games",
357 ]
358 ]
359 private_search_dirs_with_py_files = [
360 p for p in private_search_dirs if p is not None and _has_py_file_in_dir(p)
361 ]
362 public_search_dirs_has_py_files = any(
363 p is not None and _has_py_file_in_dir(p)
364 for p in _public_python_dist_dirs(fs_root)
365 )
367 if not public_search_dirs_has_py_files and not private_search_dirs_with_py_files:
368 return
370 # The dh_python3 helper also supports -V and -X. We do not use them. They can be
371 # replaced by bcep support instead, which is how we will be supporting this kind
372 # of configuration down the line.
373 ctrl.maintscript.unconditionally_in_script(
374 "prerm",
375 textwrap.dedent(
376 f"""\
377 \
378 if command -v py3clean >/dev/null 2>&1; then
379 py3clean -p {package}
380 else
381 dpkg -L {package} | sed -En -e '/^(.*)\\/(.+)\\.py$/s,,rm "\\1/__pycache__/\\2".*,e'
382 find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir
383 fi
384 """
385 ),
386 )
387 if public_search_dirs_has_py_files:
388 ctrl.maintscript.on_configure(
389 textwrap.dedent(
390 f"""\
391 \
392 if command -v py3compile >/dev/null 2>&1; then
393 py3compile -p {package}
394 fi
395 if command -v pypy3compile >/dev/null 2>&1; then
396 pypy3compile -p {package} || true
397 fi
398 """
399 )
400 )
401 for private_dir in private_search_dirs_with_py_files:
402 escaped_dir = ctrl.maintscript.escape_shell_words(private_dir.absolute)
403 ctrl.maintscript.on_configure(
404 textwrap.dedent(
405 f"""\
406 \
407 if command -v py3compile >/dev/null 2>&1; then
408 py3compile -p {package} {escaped_dir}
409 fi
410 if command -v pypy3compile >/dev/null 2>&1; then
411 pypy3compile -p {package} {escaped_dir} || true
412 fi
413 """
414 )
415 )
418def translate_capabilities(
419 fs_root: VirtualPath,
420 ctrl: BinaryCtrlAccessor,
421 _context: PackageProcessingContext,
422) -> None:
423 caps = []
424 maintscript = ctrl.maintscript
425 for p in fs_root.all_paths():
426 if not p.is_file:
427 continue
428 metadata_ref = p.metadata(DebputyCapability)
429 capability = metadata_ref.value
430 if capability is None:
431 continue
433 abs_path = maintscript.escape_shell_words(p.absolute)
435 cap_script = "".join(
436 [
437 " # Triggered by: {DEFINITION_SOURCE}\n"
438 " _TPATH=$(dpkg-divert --truename {ABS_PATH})\n",
439 ' if setcap {CAP} "{DPKG_ROOT_UNQUOTED}${{_TPATH}}"; then\n',
440 ' chmod {MODE} "{DPKG_ROOT_UNQUOTED}${{_TPATH}}"\n',
441 ' echo "Successfully applied capabilities {CAP} on ${{_TPATH}}"\n',
442 " else\n",
443 # We do not reset the mode here; generally a re-install or upgrade would re-store both mode,
444 # and remove the capabilities.
445 ' echo "The setcap failed to processes {CAP} on ${{_TPATH}}; falling back to no capability support" >&2\n',
446 " fi\n",
447 ]
448 ).format(
449 CAP=maintscript.escape_shell_words(capability.capabilities).replace(
450 "\\+", "+"
451 ),
452 DPKG_ROOT_UNQUOTED=DPKG_ROOT_UNQUOTED,
453 ABS_PATH=abs_path,
454 MODE=maintscript.escape_shell_words(str(capability.capability_mode)),
455 DEFINITION_SOURCE=capability.definition_source.replace("\n", "\\n"),
456 )
457 assert cap_script.endswith("\n")
458 caps.append(cap_script)
460 if not caps:
461 return
463 maintscript.on_configure(
464 textwrap.dedent(
465 """\
466 if command -v setcap > /dev/null; then
467 {SET_CAP_COMMANDS}
468 unset _TPATH
469 else
470 echo "The setcap utility is not installed available; falling back to no capability support" >&2
471 fi
472 """
473 ).format(
474 SET_CAP_COMMANDS="".join(caps).rstrip("\n"),
475 )
476 )
478 478 ↛ 479line 478 didn't jump to line 479 because the condition on line 478 was never true
479def pam_auth_update(
480 fs_root: VirtualPath,
481 ctrl: BinaryCtrlAccessor,
482 _context: PackageProcessingContext,
483) -> None:
484 pam_configs = fs_root.lookup("/usr/share/pam-configs")
485 if not pam_configs:
486 return
487 maintscript = ctrl.maintscript
488 for pam_config in pam_configs.iterdir:
489 if not pam_config.is_file:
490 continue
491 maintscript.on_configure("pam-auth-update --package\n")
492 maintscript.on_before_removal(
493 textwrap.dedent(
494 f"""\
495 \
496 if [ "${ DPKG_MAINTSCRIPT_PACKAGE_REFCOUNT:-1} " = 1 ]; then
497 pam-auth-update --package --remove {maintscript.escape_shell_words(pam_config.name)}
498 fi
499 """
500 )
501 )
504def auto_depends_arch_any_solink( 504 ↛ 507line 504 didn't jump to line 507 because the condition on line 504 was always true
505 fs_foot: VirtualPath,
506 ctrl: BinaryCtrlAccessor,
507 context: PackageProcessingContext,
508) -> None:
509 package = context.binary_package
510 if package.is_arch_all:
511 return
512 libbasedir = fs_foot.lookup("usr/lib")
513 if not libbasedir:
514 return
515 libmadir = libbasedir.get(package.deb_multiarch) 515 ↛ 516line 515 didn't jump to line 516 because the condition on line 515 was never true
516 if libmadir:
517 libdirs = [libmadir, libbasedir]
518 else:
519 libdirs = [libbasedir]
520 targets = []
521 for libdir in libdirs:
522 for path in libdir.iterdir:
523 if not path.is_symlink or not path.name.endswith(".so"):
524 continue
525 target = path.readlink()
526 resolved = assume_not_none(path.parent_dir).lookup(target)
527 if resolved is not None:
528 continue 528 ↛ 529line 528 didn't jump to line 529 because the condition on line 528 was never true
529 targets.append((libdir.path, target))
531 roots = list(context.accessible_package_roots())
532 if not roots: 532 ↛ 539line 532 didn't jump to line 539 because the condition on line 532 was always true
533 return
535 for libdir_path, target in targets:
536 final_path = os.path.join(libdir_path, target)
537 matches = []
538 for opkg, ofs_root in roots:
539 m = ofs_root.lookup(final_path)
540 if not m:
541 continue
542 matches.append(opkg)
543 if not matches or len(matches) > 1:
544 if matches:
545 all_matches = ", ".join(p.name for p in matches)
546 _warn(
547 f"auto-depends-solink: The {final_path} was found in multiple packages ({all_matches}):"
548 f" Not generating a dependency."
549 )
550 else:
551 _warn(
552 f"auto-depends-solink: The {final_path} was NOT found in any accessible package:"
553 " Not generating a dependency. This detection only works when both packages are arch:any"
554 " and they have the same build-profiles."
555 )
556 continue
557 pkg_dep = matches[0]
558 # The debputy API should not allow this constraint to fail
559 assert pkg_dep.is_arch_all == package.is_arch_all
560 # If both packages are arch:all or both are arch:any, we can generate a tight dependency
561 relation = f"{pkg_dep.name} (= ${ binary:Version} )"
562 ctrl.substvars.add_dependency("misc:Depends", relation)