Coverage for src/debputy/plugins/debputy/metadata_detectors.py: 91%
298 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-04 09:01 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-04 09:01 +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(f"""\
107 if [ -x "$(command -v systemd-tmpfiles)" ]; then
108 systemd-tmpfiles ${{DPKG_ROOT:+--root="$DPKG_ROOT"}} --create {tmpfiles_escaped} || true
109 fi
110 """)
112 after_removal_snippet = textwrap.dedent("""\
113 if [ -x "$(command -v systemd-tmpfiles)" ]; then
114 systemd-tmpfiles ${{DPKG_ROOT:+--root="$DPKG_ROOT"}} --remove - >/dev/null <<TMPFILES_EOF || true
115 {TMPFILES_CONTENT}
116 TMPFILES_EOF
117 fi
118 """).format(
119 TMPFILES_CONTENT=tmpfiles_content,
120 )
121 purge_snippet = textwrap.dedent("""\
122 if [ -x "$(command -v systemd-tmpfiles)" ]; then
123 if systemd-tmpfiles --help 2>/dev/null | grep -qe "--purge"; then
124 systemd-tmpfiles ${{DPKG_ROOT:+--root="$DPKG_ROOT"}} --purge - >/dev/null <<TMPFILES_EOF || true
125 {TMPFILES_CONTENT}
126 TMPFILES_EOF
127 fi
128 fi
129 """).format(
130 TMPFILES_CONTENT=tmpfiles_content,
131 )
133 ctrl.maintscript.on_configure(postinst_snippet)
134 # We use "heredocs" and have to disable indent.
135 ctrl.maintscript.on_removed(after_removal_snippet, indent=False)
136 ctrl.maintscript.on_purge(purge_snippet, indent=False)
139def _all_sysusers_conf(fs_root: VirtualPath) -> Iterable[VirtualPath]:
140 sysusers_dir = fs_root.lookup(SYSTEMD_SYSUSERS_DIR)
141 if not sysusers_dir:
142 return
143 for child in sysusers_dir.iterdir():
144 if not child.name.endswith(".conf"):
145 continue
146 yield child
149def detect_systemd_sysusers(
150 fs_root: VirtualPath,
151 ctrl: BinaryCtrlAccessor,
152 _unused: PackageProcessingContext,
153) -> None:
154 sysusers_confs = [p.name for p in _all_sysusers_conf(fs_root)]
155 if not sysusers_confs:
156 return
158 sysusers_escaped = ctrl.maintscript.escape_shell_words(*sysusers_confs)
160 snippet = textwrap.dedent(f"""\
161 systemd-sysusers ${{DPKG_ROOT:+--root="$DPKG_ROOT"}} {sysusers_escaped}
162 """)
164 ctrl.substvars.add_dependency(
165 "misc:Depends", "systemd | systemd-standalone-sysusers | systemd-sysusers"
166 )
167 ctrl.maintscript.on_configure(snippet)
170def detect_commands(
171 fs_root: VirtualPath,
172 ctrl: BinaryCtrlAccessor,
173 context: PackageProcessingContext,
174) -> None:
175 if context.binary_package.is_udeb:
176 return
177 for path_name in SYSTEM_DEFAULT_PATH_DIRS:
178 path_dir = fs_root.lookup(path_name)
179 if path_dir is None or not path_dir.is_dir:
180 continue
181 for child in path_dir.iterdir():
182 if not (child.is_file or child.is_symlink):
183 continue
184 ctrl.substvars.add_dependency("misc:Commands", child.name)
187def detect_icons(
188 fs_root: VirtualPath,
189 ctrl: BinaryCtrlAccessor,
190 _unused: PackageProcessingContext,
191) -> None:
192 icons_root_dir = fs_root.lookup("./usr/share/icons")
193 if not icons_root_dir:
194 return
195 icon_dirs = []
196 for subdir in icons_root_dir.iterdir():
197 if subdir.name in ("gnome", "hicolor"):
198 # dh_icons skips this for some reason.
199 continue
200 for p in subdir.all_paths():
201 if p.is_file and p.name.endswith((".png", ".svg", ".xpm", ".icon")):
202 icon_dirs.append(subdir.absolute)
203 break
204 if not icon_dirs:
205 return
207 icon_dir_list_escaped = ctrl.maintscript.escape_shell_words(*icon_dirs)
209 postinst_snippet = textwrap.dedent(f"""\
210 if command -v update-icon-caches >/dev/null; then
211 update-icon-caches {icon_dir_list_escaped}
212 fi
213 """)
215 postrm_snippet = textwrap.dedent(f"""\
216 if command -v update-icon-caches >/dev/null; then
217 update-icon-caches {icon_dir_list_escaped}
218 fi
219 """)
221 ctrl.maintscript.on_configure(postinst_snippet)
222 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
225def detect_gsettings_dependencies(
226 fs_root: VirtualPath,
227 ctrl: BinaryCtrlAccessor,
228 _unused: PackageProcessingContext,
229) -> None:
230 gsettings_schema_dir = fs_root.lookup(GSETTINGS_SCHEMA_DIR)
231 if not gsettings_schema_dir:
232 return
234 for path in gsettings_schema_dir.all_paths():
235 if path.is_file and path.name.endswith((".xml", ".override")):
236 ctrl.substvars.add_dependency(
237 "misc:Depends", "dconf-gsettings-backend | gsettings-backend"
238 )
239 break
242def detect_kernel_modules(
243 fs_root: VirtualPath,
244 ctrl: BinaryCtrlAccessor,
245 _unused: PackageProcessingContext,
246) -> None:
247 for prefix in [".", "./usr"]:
248 module_root_dir = fs_root.lookup(f"{prefix}/lib/modules")
250 if not module_root_dir:
251 continue
253 module_version_dirs = []
255 for module_version_dir in module_root_dir.iterdir():
256 if not module_version_dir.is_dir:
257 continue
259 for fs_path in module_version_dir.all_paths():
260 if fs_path.name.endswith(KERNEL_MODULE_EXTENSIONS):
261 module_version_dirs.append(module_version_dir.name)
262 break
264 for module_version in module_version_dirs:
265 module_version_escaped = ctrl.maintscript.escape_shell_words(module_version)
266 postinst_snippet = textwrap.dedent(f"""\
267 if [ -e /boot/System.map-{module_version_escaped} ]; then
268 depmod -a -F /boot/System.map-{module_version_escaped} {module_version_escaped} || true
269 fi
270 """)
272 postrm_snippet = textwrap.dedent(f"""\
273 if [ -e /boot/System.map-{module_version_escaped} ]; then
274 depmod -a -F /boot/System.map-{module_version_escaped} {module_version_escaped} || true
275 fi
276 """)
278 ctrl.maintscript.on_configure(postinst_snippet)
279 # TODO: This should probably be on removal. However, this is what debhelper did and we should
280 # do the same until we are sure (not that it matters a lot).
281 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
284def detect_xfonts(
285 fs_root: VirtualPath,
286 ctrl: BinaryCtrlAccessor,
287 context: PackageProcessingContext,
288) -> None:
289 xfonts_root_dir = fs_root.lookup("./usr/share/fonts/X11/")
290 if not xfonts_root_dir:
291 return
293 cmds = []
294 cmds_postinst = []
295 cmds_postrm = []
296 escape_shell_words = ctrl.maintscript.escape_shell_words
297 package_name = context.binary_package.name
299 for xfonts_dir in xfonts_root_dir.iterdir():
300 xfonts_dirname = xfonts_dir.name
301 if not xfonts_dir.is_dir or xfonts_dirname.startswith("."):
302 continue
303 if fs_root.lookup(f"./etc/X11/xfonts/{xfonts_dirname}/{package_name}.scale"):
304 cmds.append(escape_shell_words("update-fonts-scale", xfonts_dirname))
305 cmds.append(
306 escape_shell_words("update-fonts-dir", "--x11r7-layout", xfonts_dirname)
307 )
308 alias_file = fs_root.lookup(
309 f"./etc/X11/xfonts/{xfonts_dirname}/{package_name}.alias"
310 )
311 if alias_file:
312 cmds_postinst.append(
313 escape_shell_words(
314 "update-fonts-alias",
315 "--include",
316 alias_file.absolute,
317 xfonts_dirname,
318 )
319 )
320 cmds_postrm.append(
321 escape_shell_words(
322 "update-fonts-alias",
323 "--exclude",
324 alias_file.absolute,
325 xfonts_dirname,
326 )
327 )
329 if not cmds:
330 return
332 postinst_snippet = textwrap.dedent(f"""\
333 if command -v update-fonts-dir >/dev/null; then
334 {';'.join(itertools.chain(cmds, cmds_postinst))}
335 fi
336 """)
338 postrm_snippet = textwrap.dedent(f"""\
339 if [ -x "`command -v update-fonts-dir`" ]; then
340 {';'.join(itertools.chain(cmds, cmds_postrm))}
341 fi
342 """)
344 ctrl.maintscript.unconditionally_in_script("postinst", postinst_snippet)
345 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
346 ctrl.substvars.add_dependency("misc:Depends", "xfonts-utils")
349# debputy does not support python2, so we do not list python / python2.
350_PYTHON_PUBLIC_DIST_DIR_NAMES = re.compile(r"(?:pypy|python)3(?:[.]\d+)?")
353def _public_python_dist_dirs(fs_root: VirtualPath) -> Iterator[VirtualPath]:
354 usr_lib = fs_root.lookup("./usr/lib")
355 root_dirs = []
356 if usr_lib:
357 root_dirs.append(usr_lib)
359 dbg_root = fs_root.lookup("./usr/lib/debug/usr/lib")
360 if dbg_root: 360 ↛ 361line 360 didn't jump to line 361 because the condition on line 360 was never true
361 root_dirs.append(dbg_root)
363 for root_dir in root_dirs:
364 python_dirs = (
365 path
366 for path in root_dir.iterdir()
367 if path.is_dir and _PYTHON_PUBLIC_DIST_DIR_NAMES.match(path.name)
368 )
369 for python_dir in python_dirs:
370 dist_packages = python_dir.get("dist-packages")
371 if not dist_packages:
372 continue
373 yield dist_packages
376def _has_py_file_in_dir(d: VirtualPath) -> bool:
377 return any(f.is_file and f.name.endswith(".py") for f in d.all_paths())
380def detect_pycompile_files(
381 fs_root: VirtualPath,
382 ctrl: BinaryCtrlAccessor,
383 context: PackageProcessingContext,
384) -> None:
385 package = context.binary_package.name
386 # TODO: Support configurable list of private dirs
387 private_search_dirs = [
388 fs_root.lookup(os.path.join(d, package))
389 for d in [
390 "./usr/share",
391 "./usr/share/games",
392 "./usr/lib",
393 f"./usr/lib/{context.binary_package.deb_multiarch}",
394 "./usr/lib/games",
395 ]
396 ]
397 private_search_dirs_with_py_files = [
398 p for p in private_search_dirs if p is not None and _has_py_file_in_dir(p)
399 ]
400 public_search_dirs_has_py_files = any(
401 p is not None and _has_py_file_in_dir(p)
402 for p in _public_python_dist_dirs(fs_root)
403 )
405 if not public_search_dirs_has_py_files and not private_search_dirs_with_py_files:
406 return
408 # The dh_python3 helper also supports -V and -X. We do not use them. They can be
409 # replaced by bcep support instead, which is how we will be supporting this kind
410 # of configuration down the line.
411 ctrl.maintscript.unconditionally_in_script(
412 "prerm",
413 textwrap.dedent(f"""\
414 if command -v py3clean >/dev/null 2>&1; then
415 py3clean -p {package}
416 else
417 dpkg -L {package} | sed -En -e '/^(.*)\\/(.+)\\.py$/s,,rm "\\1/__pycache__/\\2".*,e'
418 find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir
419 fi
420 """),
421 )
422 if public_search_dirs_has_py_files:
423 ctrl.maintscript.on_configure(textwrap.dedent(f"""\
424 if command -v py3compile >/dev/null 2>&1; then
425 py3compile -p {package}
426 fi
427 if command -v pypy3compile >/dev/null 2>&1; then
428 pypy3compile -p {package} || true
429 fi
430 """))
431 for private_dir in private_search_dirs_with_py_files:
432 escaped_dir = ctrl.maintscript.escape_shell_words(private_dir.absolute)
433 ctrl.maintscript.on_configure(textwrap.dedent(f"""\
434 if command -v py3compile >/dev/null 2>&1; then
435 py3compile -p {package} {escaped_dir}
436 fi
437 if command -v pypy3compile >/dev/null 2>&1; then
438 pypy3compile -p {package} {escaped_dir} || true
439 fi
440 """))
443def translate_capabilities(
444 fs_root: VirtualPath,
445 ctrl: BinaryCtrlAccessor,
446 _context: PackageProcessingContext,
447) -> None:
448 caps = []
449 maintscript = ctrl.maintscript
450 for p in fs_root.all_paths():
451 if not p.is_file:
452 continue
453 metadata_ref = p.metadata(DebputyCapability)
454 capability = metadata_ref.value
455 if capability is None:
456 continue
458 abs_path = maintscript.escape_shell_words(p.absolute)
460 cap_script = "".join(
461 [
462 " # Triggered by: {DEFINITION_SOURCE}\n"
463 " _TPATH=$(dpkg-divert --truename {ABS_PATH})\n",
464 ' if setcap {CAP} "{DPKG_ROOT_UNQUOTED}${{_TPATH}}"; then\n',
465 ' chmod {MODE} "{DPKG_ROOT_UNQUOTED}${{_TPATH}}"\n',
466 ' echo "Successfully applied capabilities {CAP} on ${{_TPATH}}"\n',
467 " else\n",
468 # We do not reset the mode here; generally a re-install or upgrade would re-store both mode,
469 # and remove the capabilities.
470 ' echo "The setcap failed to processes {CAP} on ${{_TPATH}}; falling back to no capability support" >&2\n',
471 " fi\n",
472 ]
473 ).format(
474 CAP=maintscript.escape_shell_words(capability.capabilities).replace(
475 "\\+", "+"
476 ),
477 DPKG_ROOT_UNQUOTED=DPKG_ROOT_UNQUOTED,
478 ABS_PATH=abs_path,
479 MODE=maintscript.escape_shell_words(str(capability.capability_mode)),
480 DEFINITION_SOURCE=capability.definition_source.replace("\n", "\\n"),
481 )
482 assert cap_script.endswith("\n")
483 caps.append(cap_script)
485 if not caps:
486 return
488 maintscript.on_configure(
489 textwrap.dedent("""\
490 if command -v setcap > /dev/null; then
491 {SET_CAP_COMMANDS}
492 unset _TPATH
493 else
494 echo "The setcap utility is not installed available; falling back to no capability support" >&2
495 fi
496 """).format(
497 SET_CAP_COMMANDS="".join(caps).rstrip("\n"),
498 )
499 )
502def pam_auth_update(
503 fs_root: VirtualPath,
504 ctrl: BinaryCtrlAccessor,
505 _context: PackageProcessingContext,
506) -> None:
507 pam_configs_dir = fs_root.lookup("/usr/share/pam-configs")
508 if not pam_configs_dir:
509 return
510 pam_configs = [f.name for f in pam_configs_dir.iterdir() if f.is_file]
511 if not pam_configs: 511 ↛ 512line 511 didn't jump to line 512 because the condition on line 511 was never true
512 return
513 maintscript = ctrl.maintscript
514 maintscript.on_configure(
515 'pam-auth-update ${DPKG_ROOT:+--root "$DPKG_ROOT"} --package\n'
516 )
517 maintscript.on_before_removal(
518 textwrap.dedent(
519 f"""\
520 if [ "${{DPKG_MAINTSCRIPT_PACKAGE_REFCOUNT:-1}}" = 1 ]; then
521 pam-auth-update --package --remove {maintscript.escape_shell_words(*pam_configs)}
522 fi
523 """,
524 )
525 )
528def auto_depends_arch_any_solink(
529 fs_foot: VirtualPath,
530 ctrl: BinaryCtrlAccessor,
531 context: PackageProcessingContext,
532) -> None:
533 package = context.binary_package
534 if package.is_arch_all:
535 return
536 libbasedir = fs_foot.lookup("usr/lib")
537 if not libbasedir:
538 return
539 libmadir = libbasedir.get(package.deb_multiarch)
540 if libmadir: 540 ↛ 543line 540 didn't jump to line 543 because the condition on line 540 was always true
541 libdirs = [libmadir, libbasedir]
542 else:
543 libdirs = [libbasedir]
544 targets = []
545 for libdir in libdirs:
546 for path in libdir.iterdir():
547 if not path.is_symlink or not path.name.endswith(".so"):
548 continue
549 target = path.readlink()
550 resolved = assume_not_none(path.parent_dir).lookup(target)
551 if resolved is not None: 551 ↛ 552line 551 didn't jump to line 552 because the condition on line 551 was never true
552 continue
553 targets.append((libdir.path, target))
555 roots = [
556 (pkg, root)
557 for pkg, root in context.accessible_package_roots()
558 if not pkg.is_udeb
559 ]
561 if not roots:
562 return
564 for libdir_path, target in targets:
565 final_path = os.path.join(libdir_path, target)
566 matches = []
567 for opkg, ofs_root in roots:
568 m = ofs_root.lookup(final_path)
569 if not m: 569 ↛ 570line 569 didn't jump to line 570 because the condition on line 569 was never true
570 continue
571 matches.append(opkg)
572 if not matches or len(matches) > 1:
573 if matches: 573 ↛ 580line 573 didn't jump to line 580 because the condition on line 573 was always true
574 all_matches = ", ".join(p.name for p in matches)
575 _warn(
576 f"auto-depends-solink: The {final_path} was found in multiple packages ({all_matches}):"
577 f" Not generating a dependency."
578 )
579 else:
580 _warn(
581 f"auto-depends-solink: The {final_path} was NOT found in any accessible package:"
582 " Not generating a dependency. This detection only works when both packages are arch:any"
583 " and they have the same build-profiles."
584 )
585 continue
586 pkg_dep = matches[0]
587 # The debputy API should not allow this constraint to fail
588 assert pkg_dep.is_arch_all == package.is_arch_all
589 # If both packages are arch:all or both are arch:any, we can generate a tight dependency
590 relation = f"{pkg_dep.name} (= ${{binary:Version}})"
591 ctrl.substvars.add_dependency("misc:Depends", relation)
594def _built_using_enabled_matches(
595 context: PackageProcessingContext,
596) -> Iterator["_BuiltUsingTodo"]:
597 """Helper implementing pass 1 of detect_built_using."""
598 pkg = context.binary_package
599 host = pkg.resolved_architecture
600 table = context.dpkg_arch_query_table
601 profiles = context.deb_options_and_profiles.deb_build_profiles
602 condition_context = context.condition_context(pkg)
603 for cls, field in (
604 (BuiltUsing, "Built-Using"),
605 (StaticBuiltUsing, "Static-Built-Using"),
606 ):
607 for deps, cond, attr in context.manifest_configuration(pkg, cls) or ():
608 for is_first, relation in deps:
609 name = relation["name"]
610 if not PkgRelation.holds_on_arch(relation, host, table):
611 _debug_log(
612 f"{attr}: {name} in Build-Depends is disabled by host architecture {host}."
613 )
614 elif not PkgRelation.holds_with_profiles(relation, profiles):
615 _debug_log(
616 f"{attr}: {name} in Build-Depends is disabled by profiles {' '.join(profiles)}."
617 )
618 elif cond is not None and not cond.evaluate(condition_context):
619 _debug_log(f"{attr}: {name} is disabled by its manifest condition.")
620 else:
621 yield _BuiltUsingTodo(field, name, is_first, attr)
624class _BuiltUsingTodo(typing.NamedTuple):
625 """Data transmitted between the two passes of detect_built_using."""
627 field: str
628 dep: str
629 first_option: bool # This relation was the first in its alternative.
630 attribute_path: AttributePath
633def _split_dpkg_output(dpkg_output: str) -> Iterable[tuple[str, str]]:
634 for line in dpkg_output.splitlines(keepends=False):
635 status, package_name, source_and_version = line.split("\x1f")
636 if status[1] == "i": 636 ↛ 634line 636 didn't jump to line 634 because the condition on line 636 was always true
637 yield package_name, source_and_version
640def _sources_for(
641 deps: Iterable[str],
642) -> Mapping[str, str]:
643 """Map installed packages among deps to a "source (= version)"
644 relation, excluding unknown or not installed packages.
646 >>> r = _sources_for(("dpkg", "dpkg", "dpkg-dev", "gcc", "dummy"))
647 >>> r["dpkg"] # doctest: +ELLIPSIS
648 'dpkg (= ...)'
649 >>> r["dpkg-dev"] # doctest: +ELLIPSIS
650 'dpkg (= ...)'
651 >>> r["gcc"] # doctest: +ELLIPSIS
652 'gcc-defaults (= ...)'
653 >>> "dummy" in r
654 False
655 """
656 cp = subprocess.run(
657 args=(
658 "dpkg-query",
659 "-Wf${db:Status-Abbrev}\x1f${Package}\x1f${source:Package} (= ${source:Version})\n",
660 *deps,
661 ),
662 capture_output=True,
663 check=False,
664 text=True,
665 )
666 # 0: OK 1: unknown package 2: other
667 if cp.returncode not in (0, 1): 667 ↛ 668line 667 didn't jump to line 668 because the condition on line 667 was never true
668 _error(f"dpkg-query -W failed (code: {cp.returncode}): {cp.stderr.rstrip()}")
669 # For the example above, stdout is:
670 # "ii \x1fdpkg\x1fdpkg (= 1.22.21)\n"
671 # "ii \x1fdpkg-dev\x1fdpkg (= 1.22.21)\n"
672 # "ii \x1fgcc\x1fgcc-defaults (= 1.220)\n"
673 # ^: the package is (i)nstalled
674 return dict(_split_dpkg_output(cp.stdout))
677def detect_built_using(
678 _fs_root: VirtualPath,
679 ctrl: BinaryCtrlAccessor,
680 context: PackageProcessingContext,
681) -> None:
682 """For efficiency on patterns like librust-*-dev, a first pass
683 constructs a todo list, then at most one dpkg-query subprocess is
684 spawn per binary package and the process actually happens.
685 """
687 all_todos = tuple(_built_using_enabled_matches(context))
688 if all_todos:
689 _built_using_process_matches(ctrl, all_todos)
692def _built_using_process_matches(
693 ctrl: BinaryCtrlAccessor,
694 all_todos: Iterable[_BuiltUsingTodo],
695) -> None:
696 """Helper implementing pass 2 of detect_built_using."""
697 relevant_sources = _sources_for(sorted(t.dep for t in all_todos))
698 already_warned = set()
699 for field, dep, first_option, attribute_path in all_todos:
700 if dep in relevant_sources: 700 ↛ 707line 700 didn't jump to line 707 because the condition on line 700 was always true
701 ctrl.substvars.add_dependency(
702 f"debputy:{field}",
703 relevant_sources[dep],
704 )
705 # With `Build-Depends: a | b`, in usual configurations,
706 # `a` is installed but `b` might not be.
707 elif first_option and dep not in already_warned:
708 _warn(
709 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."
710 )
711 already_warned.add(dep)
712 else:
713 _debug_log(
714 f"{attribute_path.path}: {dep} is not installed and therefore not included in the built-using fields."
715 )