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