Coverage for src/debputy/plugins/debputy/metadata_detectors.py: 91%
299 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-08-23 18:25 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-08-23 18:25 +0000
1import collections
2import itertools
3import os
4import re
5import subprocess
6import textwrap
7import typing
8from collections.abc import Iterable, Iterator, Mapping
10from debian.deb822 import PkgRelation
11from debputy.manifest_parser.util import AttributePath
12from debputy.packaging.alternatives import SYSTEM_DEFAULT_PATH_DIRS
13from debputy.plugin.api import (
14 VirtualPath,
15 BinaryCtrlAccessor,
16 PackageProcessingContext,
17)
18from debputy.plugins.debputy.paths import (
19 INITRAMFS_HOOK_DIR,
20 SYSTEMD_TMPFILES_DIR,
21 GSETTINGS_SCHEMA_DIR,
22 SYSTEMD_SYSUSERS_DIR,
23)
24from debputy.plugins.debputy.types import (
25 DebputyCapability,
26 BuiltUsing,
27 StaticBuiltUsing,
28)
29from debputy.util import assume_not_none, _debug_log, _error, _warn
31DPKG_ROOT = '"${DPKG_ROOT}"'
32DPKG_ROOT_UNQUOTED = "${DPKG_ROOT}"
34KERNEL_MODULE_EXTENSIONS = tuple(
35 f"{ext}{comp_ext}"
36 for ext, comp_ext in itertools.product(
37 (".o", ".ko"),
38 ("", ".gz", ".bz2", ".xz"),
39 )
40)
43def detect_initramfs_hooks(
44 fs_root: VirtualPath,
45 ctrl: BinaryCtrlAccessor,
46 _unused: PackageProcessingContext,
47) -> None:
48 hook_dir = fs_root.lookup(INITRAMFS_HOOK_DIR)
49 if not hook_dir:
50 return
51 for _ in hook_dir.iterdir():
52 # Only add the trigger if the directory is non-empty. It is unlikely to matter a lot,
53 # but we do this to match debhelper.
54 break
55 else:
56 return
58 ctrl.dpkg_trigger("activate-noawait", "update-initramfs")
61def _all_tmpfiles_conf(fs_root: VirtualPath) -> Iterable[VirtualPath]:
62 seen_tmpfiles = set()
63 tmpfiles_dirs = [
64 SYSTEMD_TMPFILES_DIR,
65 "./etc/tmpfiles.d",
66 ]
67 for tmpfiles_dir_path in tmpfiles_dirs:
68 tmpfiles_dir = fs_root.lookup(tmpfiles_dir_path)
69 if not tmpfiles_dir:
70 continue
71 for path in tmpfiles_dir.iterdir():
72 if (
73 not path.is_file
74 or not path.name.endswith(".conf")
75 or path.name in seen_tmpfiles
76 ):
77 continue
78 seen_tmpfiles.add(path.name)
79 yield path
82def _read_all_files(paths: collections.abc.Iterable[VirtualPath]) -> str:
83 contents = []
85 for p in paths:
86 with p.open() as fd:
87 contents.append(fd.read())
89 return "\n".join(contents).rstrip()
92def detect_systemd_tmpfiles(
93 fs_root: VirtualPath,
94 ctrl: BinaryCtrlAccessor,
95 _unused: PackageProcessingContext,
96) -> None:
97 tmpfiles_confs = sorted(_all_tmpfiles_conf(fs_root), key=lambda x: x.name)
98 if not tmpfiles_confs:
99 return
101 tmpfiles_escaped = ctrl.maintscript.escape_shell_words(
102 *(t.name for t in tmpfiles_confs)
103 )
104 tmpfiles_content = _read_all_files(tmpfiles_confs)
106 postinst_snippet = textwrap.dedent(
107 f"systemd-tmpfiles ${ DPKG_ROOT:+--root='$DPKG_ROOT'} --create {tmpfiles_escaped} || true"
108 )
110 after_removal_snippet = textwrap.dedent("""\
111 systemd-tmpfiles ${{DPKG_ROOT:+--root="$DPKG_ROOT"}} --remove - >/dev/null <<TMPFILES_EOF || true
112 {TMPFILES_CONTENT}
113 TMPFILES_EOF
114 """).format(
115 TMPFILES_CONTENT=tmpfiles_content,
116 )
117 purge_snippet = textwrap.dedent("""\
118 if systemd-tmpfiles --help | grep -qe "--purge"; then
119 systemd-tmpfiles ${{DPKG_ROOT:+--root="$DPKG_ROOT"}} --purge - >/dev/null <<TMPFILES_EOF || true
120 {TMPFILES_CONTENT}
121 TMPFILES_EOF
122 fi
123 """).format(
124 TMPFILES_CONTENT=tmpfiles_content,
125 )
127 ctrl.maintscript.on_configure(postinst_snippet)
128 # We use "heredocs" and have to disable indent.
129 ctrl.maintscript.on_removed(after_removal_snippet, indent=False)
130 ctrl.maintscript.on_purge(purge_snippet, indent=False)
131 ctrl.substvars.add_dependency(
132 "misc:Depends",
133 "systemd | systemd-standalone-tmpfiles | systemd-tmpfiles",
134 )
137def _all_sysusers_conf(fs_root: VirtualPath) -> Iterable[VirtualPath]:
138 sysusers_dir = fs_root.lookup(SYSTEMD_SYSUSERS_DIR)
139 if not sysusers_dir:
140 return
141 for child in sysusers_dir.iterdir():
142 if not child.name.endswith(".conf"):
143 continue
144 yield child
147def detect_systemd_sysusers(
148 fs_root: VirtualPath,
149 ctrl: BinaryCtrlAccessor,
150 _unused: PackageProcessingContext,
151) -> None:
152 sysusers_confs = [p.name for p in _all_sysusers_conf(fs_root)]
153 if not sysusers_confs:
154 return
156 sysusers_escaped = ctrl.maintscript.escape_shell_words(*sysusers_confs)
158 snippet = textwrap.dedent(f"""\
159 systemd-sysusers ${ DPKG_ROOT:+--root="$DPKG_ROOT"} {sysusers_escaped}
160 """)
162 ctrl.substvars.add_dependency(
163 "misc:Depends", "systemd | systemd-standalone-sysusers | systemd-sysusers"
164 )
165 ctrl.maintscript.on_configure(snippet)
168def detect_commands(
169 fs_root: VirtualPath,
170 ctrl: BinaryCtrlAccessor,
171 context: PackageProcessingContext,
172) -> None:
173 if context.binary_package.is_udeb:
174 return
175 for path_name in SYSTEM_DEFAULT_PATH_DIRS:
176 path_dir = fs_root.lookup(path_name)
177 if path_dir is None or not path_dir.is_dir:
178 continue
179 for child in path_dir.iterdir():
180 if not (child.is_file or child.is_symlink):
181 continue
182 ctrl.substvars.add_dependency("misc:Commands", child.name)
185def detect_icons(
186 fs_root: VirtualPath,
187 ctrl: BinaryCtrlAccessor,
188 _unused: PackageProcessingContext,
189) -> None:
190 icons_root_dir = fs_root.lookup("./usr/share/icons")
191 if not icons_root_dir:
192 return
193 icon_dirs = []
194 for subdir in icons_root_dir.iterdir():
195 if subdir.name in ("gnome", "hicolor"):
196 # dh_icons skips this for some reason.
197 continue
198 for p in subdir.all_paths():
199 if p.is_file and p.name.endswith((".png", ".svg", ".xpm", ".icon")):
200 icon_dirs.append(subdir.absolute)
201 break
202 if not icon_dirs:
203 return
205 icon_dir_list_escaped = ctrl.maintscript.escape_shell_words(*icon_dirs)
207 postinst_snippet = textwrap.dedent(f"""\
208 if command -v update-icon-caches >/dev/null; then
209 update-icon-caches {icon_dir_list_escaped}
210 fi
211 """)
213 postrm_snippet = textwrap.dedent(f"""\
214 if command -v update-icon-caches >/dev/null; then
215 update-icon-caches {icon_dir_list_escaped}
216 fi
217 """)
219 ctrl.maintscript.on_configure(postinst_snippet)
220 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
223def detect_gsettings_dependencies(
224 fs_root: VirtualPath,
225 ctrl: BinaryCtrlAccessor,
226 _unused: PackageProcessingContext,
227) -> None:
228 gsettings_schema_dir = fs_root.lookup(GSETTINGS_SCHEMA_DIR)
229 if not gsettings_schema_dir:
230 return
232 for path in gsettings_schema_dir.all_paths():
233 if path.is_file and path.name.endswith((".xml", ".override")):
234 ctrl.substvars.add_dependency(
235 "misc:Depends", "dconf-gsettings-backend | gsettings-backend"
236 )
237 break
240def detect_kernel_modules(
241 fs_root: VirtualPath,
242 ctrl: BinaryCtrlAccessor,
243 _unused: PackageProcessingContext,
244) -> None:
245 for prefix in [".", "./usr"]:
246 module_root_dir = fs_root.lookup(f"{prefix}/lib/modules")
248 if not module_root_dir:
249 continue
251 module_version_dirs = []
253 for module_version_dir in module_root_dir.iterdir():
254 if not module_version_dir.is_dir:
255 continue
257 for fs_path in module_version_dir.all_paths():
258 if fs_path.name.endswith(KERNEL_MODULE_EXTENSIONS):
259 module_version_dirs.append(module_version_dir.name)
260 break
262 for module_version in module_version_dirs:
263 module_version_escaped = ctrl.maintscript.escape_shell_words(module_version)
264 postinst_snippet = textwrap.dedent(f"""\
265 if [ -e /boot/System.map-{module_version_escaped} ]; then
266 depmod -a -F /boot/System.map-{module_version_escaped} {module_version_escaped} || true
267 fi
268 """)
270 postrm_snippet = textwrap.dedent(f"""\
271 if [ -e /boot/System.map-{module_version_escaped} ]; then
272 depmod -a -F /boot/System.map-{module_version_escaped} {module_version_escaped} || true
273 fi
274 """)
276 ctrl.maintscript.on_configure(postinst_snippet)
277 # TODO: This should probably be on removal. However, this is what debhelper did and we should
278 # do the same until we are sure (not that it matters a lot).
279 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
282def detect_xfonts(
283 fs_root: VirtualPath,
284 ctrl: BinaryCtrlAccessor,
285 context: PackageProcessingContext,
286) -> None:
287 xfonts_root_dir = fs_root.lookup("./usr/share/fonts/X11/")
288 if not xfonts_root_dir:
289 return
291 cmds = []
292 cmds_postinst = []
293 cmds_postrm = []
294 escape_shell_words = ctrl.maintscript.escape_shell_words
295 package_name = context.binary_package.name
297 for xfonts_dir in xfonts_root_dir.iterdir():
298 xfonts_dirname = xfonts_dir.name
299 if not xfonts_dir.is_dir or xfonts_dirname.startswith("."):
300 continue
301 if fs_root.lookup(f"./etc/X11/xfonts/{xfonts_dirname}/{package_name}.scale"):
302 cmds.append(escape_shell_words("update-fonts-scale", xfonts_dirname))
303 cmds.append(
304 escape_shell_words("update-fonts-dir", "--x11r7-layout", xfonts_dirname)
305 )
306 alias_file = fs_root.lookup(
307 f"./etc/X11/xfonts/{xfonts_dirname}/{package_name}.alias"
308 )
309 if alias_file:
310 cmds_postinst.append(
311 escape_shell_words(
312 "update-fonts-alias",
313 "--include",
314 alias_file.absolute,
315 xfonts_dirname,
316 )
317 )
318 cmds_postrm.append(
319 escape_shell_words(
320 "update-fonts-alias",
321 "--exclude",
322 alias_file.absolute,
323 xfonts_dirname,
324 )
325 )
327 if not cmds:
328 return
330 postinst_snippet = textwrap.dedent(f"""\
331 if command -v update-fonts-dir >/dev/null; then
332 {';'.join(itertools.chain(cmds, cmds_postinst))}
333 fi
334 """)
336 postrm_snippet = textwrap.dedent(f"""\
337 if [ -x "`command -v update-fonts-dir`" ]; then
338 {';'.join(itertools.chain(cmds, cmds_postrm))}
339 fi
340 """)
342 ctrl.maintscript.unconditionally_in_script("postinst", postinst_snippet)
343 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
344 ctrl.substvars.add_dependency("misc:Depends", "xfonts-utils")
347# debputy does not support python2, so we do not list python / python2.
348_PYTHON_PUBLIC_DIST_DIR_NAMES = re.compile(r"(?:pypy|python)3(?:[.]\d+)?")
351def _public_python_dist_dirs(fs_root: VirtualPath) -> Iterator[VirtualPath]:
352 usr_lib = fs_root.lookup("./usr/lib")
353 root_dirs = []
354 if usr_lib:
355 root_dirs.append(usr_lib)
357 dbg_root = fs_root.lookup("./usr/lib/debug/usr/lib")
358 if dbg_root: 358 ↛ 359line 358 didn't jump to line 359 because the condition on line 358 was never true
359 root_dirs.append(dbg_root)
361 for root_dir in root_dirs:
362 python_dirs = (
363 path
364 for path in root_dir.iterdir()
365 if path.is_dir and _PYTHON_PUBLIC_DIST_DIR_NAMES.match(path.name)
366 )
367 for python_dir in python_dirs:
368 dist_packages = python_dir.get("dist-packages")
369 if not dist_packages:
370 continue
371 yield dist_packages
374def _has_py_file_in_dir(d: VirtualPath) -> bool:
375 return any(f.is_file and f.name.endswith(".py") for f in d.all_paths())
378def detect_pycompile_files(
379 fs_root: VirtualPath,
380 ctrl: BinaryCtrlAccessor,
381 context: PackageProcessingContext,
382) -> None:
383 package = context.binary_package.name
384 # TODO: Support configurable list of private dirs
385 private_search_dirs = [
386 fs_root.lookup(os.path.join(d, package))
387 for d in [
388 "./usr/share",
389 "./usr/share/games",
390 "./usr/lib",
391 f"./usr/lib/{context.binary_package.deb_multiarch}",
392 "./usr/lib/games",
393 ]
394 ]
395 private_search_dirs_with_py_files = [
396 p for p in private_search_dirs if p is not None and _has_py_file_in_dir(p)
397 ]
398 public_search_dirs_has_py_files = any(
399 p is not None and _has_py_file_in_dir(p)
400 for p in _public_python_dist_dirs(fs_root)
401 )
403 if not public_search_dirs_has_py_files and not private_search_dirs_with_py_files:
404 return
406 # The dh_python3 helper also supports -V and -X. We do not use them. They can be
407 # replaced by bcep support instead, which is how we will be supporting this kind
408 # of configuration down the line.
409 ctrl.maintscript.unconditionally_in_script(
410 "prerm",
411 textwrap.dedent(f"""\
412 if command -v py3clean >/dev/null 2>&1; then
413 py3clean -p {package}
414 else
415 dpkg -L {package} | sed -En -e '/^(.*)\\/(.+)\\.py$/s,,rm "\\1/__pycache__/\\2".*,e'
416 find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir
417 fi
418 """),
419 )
420 if public_search_dirs_has_py_files:
421 ctrl.maintscript.on_configure(textwrap.dedent(f"""\
422 if command -v py3compile >/dev/null 2>&1; then
423 py3compile -p {package}
424 fi
425 if command -v pypy3compile >/dev/null 2>&1; then
426 pypy3compile -p {package} || true
427 fi
428 """))
429 for private_dir in private_search_dirs_with_py_files:
430 escaped_dir = ctrl.maintscript.escape_shell_words(private_dir.absolute)
431 ctrl.maintscript.on_configure(textwrap.dedent(f"""\
432 if command -v py3compile >/dev/null 2>&1; then
433 py3compile -p {package} {escaped_dir}
434 fi
435 if command -v pypy3compile >/dev/null 2>&1; then
436 pypy3compile -p {package} {escaped_dir} || true
437 fi
438 """))
441def translate_capabilities(
442 fs_root: VirtualPath,
443 ctrl: BinaryCtrlAccessor,
444 _context: PackageProcessingContext,
445) -> None:
446 caps = []
447 maintscript = ctrl.maintscript
448 for p in fs_root.all_paths():
449 if not p.is_file:
450 continue
451 metadata_ref = p.metadata(DebputyCapability)
452 capability = metadata_ref.value
453 if capability is None:
454 continue
456 abs_path = maintscript.escape_shell_words(p.absolute)
458 cap_script = "".join(
459 [
460 " # Triggered by: {DEFINITION_SOURCE}\n"
461 " _TPATH=$(dpkg-divert --truename {ABS_PATH})\n",
462 ' if setcap {CAP} "{DPKG_ROOT_UNQUOTED}${{_TPATH}}"; then\n',
463 ' chmod {MODE} "{DPKG_ROOT_UNQUOTED}${{_TPATH}}"\n',
464 ' echo "Successfully applied capabilities {CAP} on ${{_TPATH}}"\n',
465 " else\n",
466 # We do not reset the mode here; generally a re-install or upgrade would re-store both mode,
467 # and remove the capabilities.
468 ' echo "The setcap failed to processes {CAP} on ${{_TPATH}}; falling back to no capability support" >&2\n',
469 " fi\n",
470 ]
471 ).format(
472 CAP=maintscript.escape_shell_words(capability.capabilities).replace(
473 "\\+", "+"
474 ),
475 DPKG_ROOT_UNQUOTED=DPKG_ROOT_UNQUOTED,
476 ABS_PATH=abs_path,
477 MODE=maintscript.escape_shell_words(str(capability.capability_mode)),
478 DEFINITION_SOURCE=capability.definition_source.replace("\n", "\\n"),
479 )
480 assert cap_script.endswith("\n")
481 caps.append(cap_script)
483 if not caps:
484 return
486 maintscript.on_configure(
487 textwrap.dedent("""\
488 if command -v setcap > /dev/null; then
489 {SET_CAP_COMMANDS}
490 unset _TPATH
491 else
492 echo "The setcap utility is not installed available; falling back to no capability support" >&2
493 fi
494 """).format(
495 SET_CAP_COMMANDS="".join(caps).rstrip("\n"),
496 )
497 )
500def pam_auth_update(
501 fs_root: VirtualPath,
502 ctrl: BinaryCtrlAccessor,
503 _context: PackageProcessingContext,
504) -> None:
505 pam_configs_dir = fs_root.lookup("/usr/share/pam-configs")
506 if not pam_configs_dir:
507 return
508 pam_configs = [f.name for f in pam_configs_dir.iterdir() if f.is_file]
509 if not pam_configs: 509 ↛ 510line 509 didn't jump to line 510 because the condition on line 509 was never true
510 return
511 maintscript = ctrl.maintscript
512 maintscript.on_configure(
513 'pam-auth-update ${DPKG_ROOT:+--root "$DPKG_ROOT"} --package\n'
514 )
515 maintscript.on_before_removal(
516 textwrap.dedent(
517 f"""\
518 if [ "${ DPKG_MAINTSCRIPT_PACKAGE_REFCOUNT:-1} " = 1 ]; then
519 pam-auth-update --package --remove {maintscript.escape_shell_words(*pam_configs)}
520 fi
521 """,
522 )
523 )
526def auto_depends_arch_any_solink(
527 fs_foot: VirtualPath,
528 ctrl: BinaryCtrlAccessor,
529 context: PackageProcessingContext,
530) -> None:
531 package = context.binary_package
532 if package.is_arch_all:
533 return
534 libbasedir = fs_foot.lookup("usr/lib")
535 if not libbasedir:
536 return
537 libmadir = libbasedir.get(package.deb_multiarch)
538 if libmadir: 538 ↛ 541line 538 didn't jump to line 541 because the condition on line 538 was always true
539 libdirs = [libmadir, libbasedir]
540 else:
541 libdirs = [libbasedir]
542 targets = []
543 for libdir in libdirs:
544 for path in libdir.iterdir():
545 if not path.is_symlink or not path.name.endswith(".so"):
546 continue
547 target = path.readlink()
548 resolved = assume_not_none(path.parent_dir).lookup(target)
549 if resolved is not None: 549 ↛ 550line 549 didn't jump to line 550 because the condition on line 549 was never true
550 continue
551 targets.append((libdir.path, target))
553 roots = [
554 (pkg, root)
555 for pkg, root in context.accessible_package_roots()
556 if not pkg.is_udeb
557 ]
559 if not roots:
560 return
562 for libdir_path, target in targets:
563 final_path = os.path.join(libdir_path, target)
564 matches = []
565 for opkg, ofs_root in roots:
566 m = ofs_root.lookup(final_path)
567 if not m: 567 ↛ 568line 567 didn't jump to line 568 because the condition on line 567 was never true
568 continue
569 matches.append(opkg)
570 if not matches or len(matches) > 1:
571 if matches: 571 ↛ 578line 571 didn't jump to line 578 because the condition on line 571 was always true
572 all_matches = ", ".join(p.name for p in matches)
573 _warn(
574 f"auto-depends-solink: The {final_path} was found in multiple packages ({all_matches}):"
575 f" Not generating a dependency."
576 )
577 else:
578 _warn(
579 f"auto-depends-solink: The {final_path} was NOT found in any accessible package:"
580 " Not generating a dependency. This detection only works when both packages are arch:any"
581 " and they have the same build-profiles."
582 )
583 continue
584 pkg_dep = matches[0]
585 # The debputy API should not allow this constraint to fail
586 assert pkg_dep.is_arch_all == package.is_arch_all
587 # If both packages are arch:all or both are arch:any, we can generate a tight dependency
588 relation = f"{pkg_dep.name} (= ${ binary:Version} )"
589 ctrl.substvars.add_dependency("misc:Depends", relation)
592def _built_using_enabled_matches(
593 context: PackageProcessingContext,
594) -> Iterator["_BuiltUsingTodo"]:
595 """Helper implementing pass 1 of detect_built_using."""
596 pkg = context.binary_package
597 host = pkg.resolved_architecture
598 table = context.dpkg_arch_query_table
599 profiles = context.deb_options_and_profiles.deb_build_profiles
600 condition_context = context.condition_context(pkg)
601 for cls, field in (
602 (BuiltUsing, "Built-Using"),
603 (StaticBuiltUsing, "Static-Built-Using"),
604 ):
605 for deps, cond, attr in context.manifest_configuration(pkg, cls) or ():
606 for is_first, relation in deps:
607 name = relation["name"]
608 if not PkgRelation.holds_on_arch(relation, host, table):
609 _debug_log(
610 f"{attr}: {name} in Build-Depends is disabled by host architecture {host}."
611 )
612 elif not PkgRelation.holds_with_profiles(relation, profiles):
613 _debug_log(
614 f"{attr}: {name} in Build-Depends is disabled by profiles {' '.join(profiles)}."
615 )
616 elif cond is not None and not cond.evaluate(condition_context):
617 _debug_log(f"{attr}: {name} is disabled by its manifest condition.")
618 else:
619 yield _BuiltUsingTodo(field, name, is_first, attr)
622class _BuiltUsingTodo(typing.NamedTuple):
623 """Data transmitted between the two passes of detect_built_using."""
625 field: str
626 dep: str
627 first_option: bool # This relation was the first in its alternative.
628 attribute_path: AttributePath
631def _split_dpkg_output(dpkg_output: str) -> Iterable[tuple[str, str]]:
632 for line in dpkg_output.splitlines(keepends=False):
633 status, package_name, source_and_version = line.split("\x1f")
634 if status[1] == "i": 634 ↛ 632line 634 didn't jump to line 632 because the condition on line 634 was always true
635 yield package_name, source_and_version
638def _sources_for(
639 deps: Iterable[str],
640) -> Mapping[str, str]:
641 """Map installed packages among deps to a "source (= version)"
642 relation, excluding unknown or not installed packages.
644 >>> r = _sources_for(("dpkg", "dpkg", "dpkg-dev", "gcc", "dummy"))
645 >>> r["dpkg"] # doctest: +ELLIPSIS
646 'dpkg (= ...)'
647 >>> r["dpkg-dev"] # doctest: +ELLIPSIS
648 'dpkg (= ...)'
649 >>> r["gcc"] # doctest: +ELLIPSIS
650 'gcc-defaults (= ...)'
651 >>> "dummy" in r
652 False
653 """
654 cp = subprocess.run(
655 args=(
656 "dpkg-query",
657 "-Wf${db:Status-Abbrev}\x1f${Package}\x1f${source:Package} (= ${source:Version})\n",
658 *deps,
659 ),
660 capture_output=True,
661 check=False,
662 text=True,
663 )
664 # 0: OK 1: unknown package 2: other
665 if cp.returncode not in (0, 1): 665 ↛ 666line 665 didn't jump to line 666 because the condition on line 665 was never true
666 _error(f"dpkg-query -W failed (code: {cp.returncode}): {cp.stderr.rstrip()}")
667 # For the example above, stdout is:
668 # "ii \x1fdpkg\x1fdpkg (= 1.22.21)\n"
669 # "ii \x1fdpkg-dev\x1fdpkg (= 1.22.21)\n"
670 # "ii \x1fgcc\x1fgcc-defaults (= 1.220)\n"
671 # ^: the package is (i)nstalled
672 return dict(_split_dpkg_output(cp.stdout))
675def detect_built_using(
676 _fs_root: VirtualPath,
677 ctrl: BinaryCtrlAccessor,
678 context: PackageProcessingContext,
679) -> None:
680 """For efficiency on patterns like librust-*-dev, a first pass
681 constructs a todo list, then at most one dpkg-query subprocess is
682 spawn per binary package and the process actually happens.
683 """
685 all_todos = tuple(_built_using_enabled_matches(context))
686 if all_todos:
687 _built_using_process_matches(ctrl, all_todos)
690def _built_using_process_matches(
691 ctrl: BinaryCtrlAccessor,
692 all_todos: Iterable[_BuiltUsingTodo],
693) -> None:
694 """Helper implementing pass 2 of detect_built_using."""
695 relevant_sources = _sources_for(sorted(t.dep for t in all_todos))
696 already_warned = set()
697 for field, dep, first_option, attribute_path in all_todos:
698 if dep in relevant_sources: 698 ↛ 705line 698 didn't jump to line 705 because the condition on line 698 was always true
699 ctrl.substvars.add_dependency(
700 f"debputy:{field}",
701 relevant_sources[dep],
702 )
703 # With `Build-Depends: a | b`, in usual configurations,
704 # `a` is installed but `b` might not be.
705 elif first_option and dep not in already_warned:
706 _warn(
707 f"{attribute_path.path}: {dep} is not installed despite being the first or only option in a Build-Depends clause. It will be omitted from the built-using fields."
708 )
709 already_warned.add(dep)
710 else:
711 _debug_log(
712 f"{attribute_path.path}: {dep} is not installed and therefore not included in the built-using fields."
713 )