Coverage for src/debputy/analysis/debian_dir.py: 30%
345 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
1import dataclasses
2import json
3import os
4import stat
5import subprocess
6from typing import (
7 AbstractSet,
8 List,
9 Tuple,
10 Optional,
11 Dict,
12 Any,
13 Union,
14 TypedDict,
15 NotRequired,
16)
17from collections.abc import Mapping, Iterable, Sequence, Iterator, Container
19from debputy.analysis import REFERENCE_DATA_TABLE
20from debputy.analysis.analysis_util import flatten_ppfs
21from debputy.dh.dh_assistant import (
22 resolve_active_and_inactive_dh_commands,
23 read_dh_addon_sequences,
24 extract_dh_compat_level,
25)
26from debputy.integration_detection import determine_debputy_integration_mode
27from debputy.packager_provided_files import (
28 PackagerProvidedFile,
29 detect_all_packager_provided_files,
30)
31from debputy.packages import BinaryPackage, SourcePackage
32from debputy.plugin.api import (
33 VirtualPath,
34 packager_provided_file_reference_documentation,
35)
36from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
37from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin
38from debputy.plugin.api.impl_types import (
39 PluginProvidedKnownPackagingFile,
40 DebputyPluginMetadata,
41 KnownPackagingFileInfo,
42 DHCompatibilityBasedRule,
43 PackagerProvidedFileClassSpec,
44 expand_known_packaging_config_features,
45)
46from debputy.plugin.api.spec import DebputyIntegrationMode
47from debputy.util import (
48 assume_not_none,
49 escape_shell,
50 _trace_log,
51 _is_trace_log_enabled,
52 render_command,
53)
55PackagingFileInfo = TypedDict(
56 "PackagingFileInfo",
57 {
58 "path": str,
59 "binary-package": NotRequired[str],
60 "install-path": NotRequired[str],
61 "install-pattern": NotRequired[str],
62 "file-categories": NotRequired[list[str]],
63 "config-features": NotRequired[list[str]],
64 "pkgfile-is-active-in-build": NotRequired[bool],
65 "pkgfile-stem": NotRequired[str],
66 "pkgfile-explicit-package-name": NotRequired[bool],
67 "pkgfile-name-segment": NotRequired[str],
68 "pkgfile-architecture-restriction": NotRequired[str],
69 "likely-typo-of": NotRequired[str],
70 "likely-generated-from": NotRequired[list[str]],
71 "related-tools": NotRequired[list[str]],
72 "documentation-uris": NotRequired[list[str]],
73 "debputy-cmd-templates": NotRequired[list[list[str]]],
74 "generates": NotRequired[str],
75 "generated-from": NotRequired[str],
76 },
77)
80def _perl_json_bool(base: Any | None) -> Any | None:
81 if base is None: 81 ↛ 82line 81 didn't jump to line 82 because the condition on line 81 was never true
82 return None
84 if isinstance(base, bool): 84 ↛ 91line 84 didn't jump to line 91 because the condition on line 84 was always true
85 return base
87 # Account for different ways that Perl will or could return a JSON bool,
88 # when not using `JSON::PP::{true,false}`.
89 #
90 # https://salsa.debian.org/debian/debputy/-/issues/119#note_627057
91 if isinstance(base, int):
92 if base == 0:
93 return False
94 return True if base == 1 else base
95 if isinstance(base, str):
96 return False if base == "" else base
97 return base
100def scan_debian_dir(
101 feature_set: PluginProvidedFeatureSet,
102 source_package: SourcePackage,
103 binary_packages: Mapping[str, BinaryPackage],
104 debian_dir: VirtualPath,
105 *,
106 uses_dh_sequencer: bool = True,
107 dh_sequences: AbstractSet[str] | None = None,
108) -> tuple[list[PackagingFileInfo], list[str], int, object | None]:
109 known_packaging_files = feature_set.known_packaging_files
110 debputy_plugin_metadata = plugin_metadata_for_debputys_own_plugin()
111 reference_data_set_names = [
112 "config-features",
113 "file-categories",
114 ]
115 for n in reference_data_set_names:
116 assert n in REFERENCE_DATA_TABLE
118 annotated: list[PackagingFileInfo] = []
119 seen_paths: dict[str, PackagingFileInfo] = {}
121 if dh_sequences is None:
122 r = read_dh_addon_sequences(debian_dir)
123 if r is not None:
124 bd_sequences, dr_sequences, uses_dh_sequencer = r
125 dh_sequences = bd_sequences | dr_sequences
126 else:
127 dh_sequences = set()
128 uses_dh_sequencer = False
129 debputy_integration_mode = determine_debputy_integration_mode(
130 source_package.fields,
131 dh_sequences,
132 )
133 is_debputy_package = debputy_integration_mode is not None
134 dh_compat_level, dh_assistant_exit_code = extract_dh_compat_level()
135 dh_issues = []
137 static_packaging_files = {
138 kpf.detection_value: kpf
139 for kpf in known_packaging_files.values()
140 if kpf.detection_method == "path"
141 }
142 dh_pkgfile_docs = {
143 kpf.detection_value: kpf
144 for kpf in known_packaging_files.values()
145 if kpf.detection_method == "dh.pkgfile"
146 }
147 if is_debputy_package:
148 all_debputy_ppfs = list(
149 flatten_ppfs(
150 detect_all_packager_provided_files(
151 feature_set,
152 debian_dir,
153 binary_packages,
154 allow_fuzzy_matches=True,
155 detect_typos=True,
156 ignore_paths=static_packaging_files,
157 )
158 )
159 )
160 else:
161 all_debputy_ppfs = []
163 if dh_compat_level is not None:
164 (
165 all_dh_ppfs,
166 _,
167 dh_issues,
168 dh_assistant_exit_code,
169 ) = resolve_debhelper_config_files(
170 debian_dir,
171 binary_packages,
172 debputy_plugin_metadata,
173 feature_set,
174 dh_sequences,
175 dh_compat_level,
176 uses_dh_sequencer,
177 debputy_integration_mode=debputy_integration_mode,
178 ignore_paths=static_packaging_files,
179 )
181 else:
182 all_dh_ppfs = []
184 for ppf in all_debputy_ppfs:
185 key = ppf.path.path
186 ref_doc = ppf.definition.reference_documentation
187 documentation_uris = (
188 ref_doc.format_documentation_uris if ref_doc is not None else None
189 )
190 details: PackagingFileInfo = {
191 "path": key,
192 "binary-package": ppf.package_name,
193 "pkgfile-stem": ppf.definition.stem,
194 "pkgfile-explicit-package-name": ppf.uses_explicit_package_name,
195 "pkgfile-is-active-in-build": ppf.definition.has_active_command,
196 "debputy-cmd-templates": [
197 ["debputy", "plugin", "show", "p-p-f", ppf.definition.stem]
198 ],
199 }
200 if ppf.fuzzy_match and key.endswith(".in"):
201 _merge_list(details, "file-categories", ["generic-template"])
202 details["generates"] = key[:-3]
203 elif assume_not_none(ppf.path.parent_dir).get(ppf.path.name + ".in"):
204 _merge_list(details, "file-categories", ["generated"])
205 details["generated-from"] = key + ".in"
206 name_segment = ppf.name_segment
207 arch_restriction = ppf.architecture_restriction
208 if name_segment is not None:
209 details["pkgfile-name-segment"] = name_segment
210 if arch_restriction:
211 details["pkgfile-architecture-restriction"] = arch_restriction
212 seen_paths[key] = details
213 annotated.append(details)
214 static_details = static_packaging_files.get(key)
215 if static_details is not None:
216 # debhelper compat rules does not apply to debputy files
217 _add_known_packaging_data(details, static_details, None)
218 if documentation_uris:
219 details["documentation-uris"] = list(documentation_uris)
221 _merge_ppfs(annotated, seen_paths, all_dh_ppfs, dh_pkgfile_docs, dh_compat_level)
223 for virtual_path in _scan_debian_dir(debian_dir):
224 key = virtual_path.path
225 if key in seen_paths or _skip_path(virtual_path):
226 continue
228 static_match = static_packaging_files.get(virtual_path.path)
229 if static_match is not None:
230 details: PackagingFileInfo = {
231 "path": key,
232 }
233 annotated.append(details)
234 if assume_not_none(virtual_path.parent_dir).get(virtual_path.name + ".in"):
235 details["generated-from"] = key + ".in"
236 _merge_list(details, "file-categories", ["generated"])
237 _add_known_packaging_data(details, static_match, dh_compat_level)
239 return annotated, reference_data_set_names, dh_assistant_exit_code, dh_issues
242def _skip_path(virtual_path: VirtualPath) -> bool:
243 if virtual_path.is_symlink:
244 try:
245 st = os.stat(virtual_path.fs_path)
246 except FileNotFoundError:
247 return True
248 else:
249 if not stat.S_ISREG(st.st_mode):
250 return True
251 elif not virtual_path.is_file:
252 return True
253 return False
256def _fake_PPFClassSpec(
257 debputy_plugin_metadata: DebputyPluginMetadata,
258 stem: str,
259 doc_uris: Sequence[str] | None,
260 install_pattern: str | None,
261 *,
262 default_priority: int | None = None,
263 packageless_is_fallback_for_all_packages: bool = False,
264 post_formatting_rewrite: str | None = None,
265 bug_950723: bool = False,
266 has_active_command: bool = False,
267) -> PackagerProvidedFileClassSpec:
268 if install_pattern is None: 268 ↛ 270line 268 didn't jump to line 270 because the condition on line 268 was always true
269 install_pattern = "not-a-real-ppf"
270 if post_formatting_rewrite is not None: 270 ↛ 271line 270 didn't jump to line 271 because the condition on line 270 was never true
271 formatting_hook = _POST_FORMATTING_REWRITE[post_formatting_rewrite]
272 else:
273 formatting_hook = None
274 return PackagerProvidedFileClassSpec(
275 debputy_plugin_metadata,
276 stem,
277 install_pattern,
278 allow_architecture_segment=True,
279 allow_name_segment=True,
280 default_priority=default_priority,
281 default_mode=0o644,
282 post_formatting_rewrite=formatting_hook,
283 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages,
284 reservation_only=False,
285 formatting_callback=None,
286 bug_950723=bug_950723,
287 has_active_command=has_active_command,
288 reference_documentation=packager_provided_file_reference_documentation(
289 format_documentation_uris=doc_uris,
290 ),
291 )
294def _relevant_dh_compat_rules(
295 compat_level: int | None,
296 info: KnownPackagingFileInfo,
297) -> Iterable[DHCompatibilityBasedRule]:
298 if compat_level is None:
299 return
300 dh_compat_rules = info.get("dh_compat_rules")
301 if not dh_compat_rules:
302 return
303 for dh_compat_rule in dh_compat_rules:
304 rule_compat_level = dh_compat_rule.get("starting_with_compat_level")
305 if rule_compat_level is not None and compat_level < rule_compat_level:
306 continue
307 yield dh_compat_rule
310def _kpf_install_pattern(
311 compat_level: int | None,
312 ppkpf: PluginProvidedKnownPackagingFile,
313) -> str | None:
314 for compat_rule in _relevant_dh_compat_rules(compat_level, ppkpf.info):
315 install_pattern = compat_rule.get("install_pattern")
316 if install_pattern is not None:
317 return install_pattern
318 return ppkpf.info.get("install_pattern")
321def resolve_debhelper_config_files(
322 debian_dir: VirtualPath,
323 binary_packages: Mapping[str, BinaryPackage],
324 debputy_plugin_metadata: DebputyPluginMetadata,
325 plugin_feature_set: PluginProvidedFeatureSet,
326 dh_rules_addons: AbstractSet[str],
327 dh_compat_level: int,
328 saw_dh: bool,
329 ignore_paths: Container[str] = frozenset(),
330 *,
331 debputy_integration_mode: DebputyIntegrationMode | None = None,
332 cwd: str | None = None,
333) -> tuple[list[PackagerProvidedFile], list[str], object | None, int]:
335 dh_ppf_docs = {
336 kpf.detection_value: kpf
337 for kpf in plugin_feature_set.known_packaging_files.values()
338 if kpf.detection_method == "dh.pkgfile"
339 }
341 dh_ppfs = {}
342 commands, exit_code = _relevant_dh_commands(dh_rules_addons, cwd=cwd)
344 cmd = ["dh_assistant", "list-guessed-dh-config-files"]
345 if dh_rules_addons:
346 addons = ",".join(dh_rules_addons)
347 cmd.append(f"--with={addons}")
348 try:
349 output = subprocess.check_output(
350 cmd,
351 stderr=subprocess.DEVNULL,
352 cwd=cwd,
353 )
354 except (subprocess.CalledProcessError, FileNotFoundError) as e:
355 config_files = []
356 issues = None
357 missing_introspection: list[str] = []
358 if isinstance(e, subprocess.CalledProcessError):
359 exit_code = e.returncode
360 else:
361 exit_code = 127
362 if _is_trace_log_enabled():
363 _trace_log(
364 f"Command {render_command(*cmd, cwd=cwd)} failed with {exit_code} ({cwd=})"
365 )
366 else:
367 result = json.loads(output)
368 config_files = result.get("config-files", [])
369 missing_introspection: list[str] = result.get("commands-not-introspectable", [])
370 issues = result.get("issues")
371 if _is_trace_log_enabled(): 371 ↛ 372line 371 didn't jump to line 372 because the condition on line 371 was never true
372 _trace_log(
373 f"Command {render_command(*cmd, cwd=cwd)} returned successfully: {output}"
374 )
375 dh_commands = resolve_active_and_inactive_dh_commands(dh_rules_addons)
376 for config_file in config_files:
377 if not isinstance(config_file, dict): 377 ↛ 378line 377 didn't jump to line 378 because the condition on line 377 was never true
378 continue
379 if config_file.get("file-type") != "pkgfile":
380 continue
381 stem = config_file.get("pkgfile")
382 if stem is None: 382 ↛ 383line 382 didn't jump to line 383 because the condition on line 382 was never true
383 continue
384 internal = config_file.get("internal")
385 if isinstance(internal, dict):
386 bug_950723 = internal.get("bug#950723", False) is True
387 else:
388 bug_950723 = False
389 commands = config_file.get("commands")
390 documentation_uris = []
391 related_tools = []
392 seen_commands = set()
393 seen_docs = set()
394 ppkpf = dh_ppf_docs.get(stem)
396 if ppkpf: 396 ↛ 397line 396 didn't jump to line 397 because the condition on line 396 was never true
397 dh_cmds = ppkpf.info.get("debhelper_commands")
398 doc_uris = ppkpf.info.get("documentation_uris")
399 default_priority = ppkpf.info.get("default_priority")
400 if doc_uris is not None:
401 seen_docs.update(doc_uris)
402 documentation_uris.extend(doc_uris)
403 if dh_cmds is not None:
404 seen_commands.update(dh_cmds)
405 related_tools.extend(dh_cmds)
406 install_pattern = _kpf_install_pattern(dh_compat_level, ppkpf)
407 post_formatting_rewrite = ppkpf.info.get("post_formatting_rewrite")
408 packageless_is_fallback_for_all_packages = ppkpf.info.get(
409 "packageless_is_fallback_for_all_packages",
410 False,
411 )
412 # If it is a debhelper PPF, then `has_active_command` is false by default.
413 has_active_command = ppkpf.info.get("has_active_command", False)
414 else:
415 install_pattern = None
416 default_priority = None
417 post_formatting_rewrite = None
418 packageless_is_fallback_for_all_packages = False
419 has_active_command = False
420 for command in commands:
421 if isinstance(command, dict): 421 ↛ 420line 421 didn't jump to line 420 because the condition on line 421 was always true
422 command_name = command.get("command")
423 if isinstance(command_name, str) and command_name: 423 ↛ 432line 423 didn't jump to line 432 because the condition on line 423 was always true
424 if command_name not in seen_commands: 424 ↛ 427line 424 didn't jump to line 427 because the condition on line 424 was always true
425 related_tools.append(command_name)
426 seen_commands.add(command_name)
427 manpage = f"man:{command_name}(1)"
428 if manpage not in seen_docs: 428 ↛ 433line 428 didn't jump to line 433 because the condition on line 428 was always true
429 documentation_uris.append(manpage)
430 seen_docs.add(manpage)
431 else:
432 continue
433 is_active = _perl_json_bool(command.get("is-active", True))
434 if is_active is None and command_name in dh_commands.active_commands: 434 ↛ 435line 434 didn't jump to line 435 because the condition on line 434 was never true
435 is_active = True
436 if not isinstance(is_active, bool): 436 ↛ 437line 436 didn't jump to line 437 because the condition on line 436 was never true
437 continue
438 if is_active:
439 has_active_command = True
441 if debputy_integration_mode == "full": 441 ↛ 443line 441 didn't jump to line 443 because the condition on line 441 was never true
442 # dh commands are never active in full integration mode.
443 has_active_command = False
444 elif not saw_dh: 444 ↛ 448line 444 didn't jump to line 448 because the condition on line 444 was always true
445 # If we did not see `dh`, we assume classic `debhelper` where we have no way of knowing
446 # which commands are active.
447 has_active_command = True
448 dh_ppfs[stem] = _fake_PPFClassSpec(
449 debputy_plugin_metadata,
450 stem,
451 documentation_uris,
452 install_pattern,
453 default_priority=default_priority,
454 post_formatting_rewrite=post_formatting_rewrite,
455 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages,
456 bug_950723=bug_950723,
457 has_active_command=has_active_command,
458 )
459 for ppkpf in dh_ppf_docs.values(): 459 ↛ 460line 459 didn't jump to line 460 because the loop on line 459 never started
460 stem = ppkpf.detection_value
461 if stem in dh_ppfs:
462 continue
464 default_priority = ppkpf.info.get("default_priority")
465 install_pattern = _kpf_install_pattern(dh_compat_level, ppkpf)
466 post_formatting_rewrite = ppkpf.info.get("post_formatting_rewrite")
467 packageless_is_fallback_for_all_packages = ppkpf.info.get(
468 "packageless_is_fallback_for_all_packages",
469 False,
470 )
471 has_active_command = (
472 ppkpf.info.get("has_active_command", False) if saw_dh else False
473 )
474 if not has_active_command:
475 dh_cmds = ppkpf.info.get("debhelper_commands")
476 if dh_cmds:
477 has_active_command = any(
478 c in dh_commands.active_commands for c in dh_cmds
479 )
480 dh_ppfs[stem] = _fake_PPFClassSpec(
481 debputy_plugin_metadata,
482 stem,
483 ppkpf.info.get("documentation_uris"),
484 install_pattern,
485 default_priority=default_priority,
486 post_formatting_rewrite=post_formatting_rewrite,
487 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages,
488 has_active_command=has_active_command,
489 )
490 dh_ppf_feature_set = _fake_plugin_feature_set(plugin_feature_set, dh_ppfs)
491 all_dh_ppfs = list(
492 flatten_ppfs(
493 detect_all_packager_provided_files(
494 dh_ppf_feature_set,
495 debian_dir,
496 binary_packages,
497 allow_fuzzy_matches=True,
498 detect_typos=True,
499 ignore_paths=ignore_paths,
500 )
501 )
502 )
503 return all_dh_ppfs, missing_introspection, issues, exit_code
506def _fake_plugin_feature_set(
507 plugin_feature_set: PluginProvidedFeatureSet,
508 fake_defs: Mapping[str, PackagerProvidedFileClassSpec],
509) -> PluginProvidedFeatureSet:
510 return dataclasses.replace(plugin_feature_set, packager_provided_files=fake_defs)
513def _merge_list(
514 existing_table: dict[str, Any],
515 key: str,
516 new_data: Sequence[str] | None,
517) -> None:
518 if not new_data:
519 return
520 existing_values = existing_table.get(key, [])
521 if isinstance(existing_values, tuple):
522 existing_values = list(existing_values)
523 assert isinstance(existing_values, list)
524 seen = set(existing_values)
525 existing_values.extend(x for x in new_data if x not in seen)
526 existing_table[key] = existing_values
529def _merge_ppfs(
530 identified: list[PackagingFileInfo],
531 seen_paths: dict[str, PackagingFileInfo],
532 ppfs: list[PackagerProvidedFile],
533 context: Mapping[str, PluginProvidedKnownPackagingFile],
534 dh_compat_level: int | None,
535) -> None:
536 for ppf in ppfs:
537 key = ppf.path.path
538 ref_doc = ppf.definition.reference_documentation
539 documentation_uris = (
540 ref_doc.format_documentation_uris if ref_doc is not None else None
541 )
542 if not ppf.definition.installed_as_format.startswith("not-a-real-ppf"):
543 try:
544 parts = ppf.compute_dest()
545 except RuntimeError:
546 dest = None
547 else:
548 dest = "/".join(parts).lstrip(".")
549 else:
550 dest = None
551 orig_details = seen_paths.get(key)
552 if orig_details is None:
553 details: PackagingFileInfo = {
554 "path": key,
555 "pkgfile-stem": ppf.definition.stem,
556 "pkgfile-is-active-in-build": ppf.definition.has_active_command,
557 "pkgfile-explicit-package-name": ppf.uses_explicit_package_name,
558 "binary-package": ppf.package_name,
559 }
560 if ppf.expected_path is not None:
561 details["likely-typo-of"] = ppf.expected_path
562 identified.append(details)
563 else:
564 details = orig_details
565 # We do not merge the "is typo" field; if the original
566 for k, v in [
567 ("pkgfile-stem", ppf.definition.stem),
568 ("pkgfile-explicit-package-name", ppf.definition.has_active_command),
569 ("binary-package", ppf.package_name),
570 ]:
571 if k not in details:
572 details[k] = v
573 if ppf.definition.has_active_command and details.get(
574 "pkgfile-is-active-in-build", False
575 ):
576 details["pkgfile-is-active-in-build"] = True
577 if ppf.expected_path is None and "likely-typo-of" in details:
578 del details["likely-typo-of"]
580 name_segment = ppf.name_segment
581 arch_restriction = ppf.architecture_restriction
582 if name_segment is not None and "pkgfile-name-segment" not in details:
583 details["pkgfile-name-segment"] = name_segment
584 if (
585 arch_restriction is not None
586 and "pkgfile-architecture-restriction" not in details
587 ):
588 details["pkgfile-architecture-restriction"] = arch_restriction
589 if ppf.fuzzy_match and key.endswith(".in"):
590 _merge_list(details, "file-categories", ["generic-template"])
591 details["generates"] = key[:-3]
592 elif assume_not_none(ppf.path.parent_dir).get(ppf.path.name + ".in"):
593 _merge_list(details, "file-categories", ["generated"])
594 details["generated-from"] = key + ".in"
595 if dest is not None and "install-path" not in details:
596 details["install-path"] = dest
598 extra_details = context.get(ppf.definition.stem)
599 if extra_details is not None:
600 _add_known_packaging_data(details, extra_details, dh_compat_level)
602 _merge_list(details, "documentation-uris", documentation_uris)
605def _relevant_dh_commands(
606 dh_rules_addons: Iterable[str],
607 cwd: str | None = None,
608) -> tuple[list[str], int]:
609 cmd = ["dh_assistant", "list-commands", "--output-format=json"]
610 if dh_rules_addons:
611 addons = ",".join(dh_rules_addons)
612 cmd.append(f"--with={addons}")
613 try:
614 output = subprocess.check_output(
615 cmd,
616 stderr=subprocess.DEVNULL,
617 cwd=cwd,
618 )
619 except (FileNotFoundError, subprocess.CalledProcessError) as e:
620 exit_code = 127
621 if isinstance(e, subprocess.CalledProcessError):
622 exit_code = e.returncode
623 if _is_trace_log_enabled():
624 _trace_log(
625 f"Command {render_command(*cmd, cwd=cwd)} failed with {exit_code} ({cwd=})"
626 )
627 return [], exit_code
628 else:
629 data = json.loads(output)
630 commands_json = data.get("commands")
631 commands = []
632 if _is_trace_log_enabled(): 632 ↛ 633line 632 didn't jump to line 633 because the condition on line 632 was never true
633 _trace_log(
634 f"Command {render_command(*cmd, cwd=cwd)} returned successfully: {output}"
635 )
636 for command in commands_json:
637 if isinstance(command, dict): 637 ↛ 636line 637 didn't jump to line 636 because the condition on line 637 was always true
638 command_name = command.get("command")
639 if isinstance(command_name, str) and command_name: 639 ↛ 636line 639 didn't jump to line 636 because the condition on line 639 was always true
640 commands.append(command_name)
641 return commands, 0
644def _add_known_packaging_data(
645 details: PackagingFileInfo,
646 plugin_data: PluginProvidedKnownPackagingFile,
647 dh_compat_level: int | None,
648):
649 install_pattern = _kpf_install_pattern(
650 dh_compat_level,
651 plugin_data,
652 )
653 config_features = plugin_data.info.get("config_features")
654 if config_features:
655 config_features = expand_known_packaging_config_features(
656 dh_compat_level or 0,
657 config_features,
658 )
659 _merge_list(details, "config-features", config_features)
661 if dh_compat_level is not None:
662 extra_config_features = []
663 for dh_compat_rule in _relevant_dh_compat_rules(
664 dh_compat_level, plugin_data.info
665 ):
666 cf = dh_compat_rule.get("add_config_features")
667 if cf:
668 extra_config_features.extend(cf)
669 if extra_config_features:
670 extra_config_features = expand_known_packaging_config_features(
671 dh_compat_level,
672 extra_config_features,
673 )
674 _merge_list(details, "config-features", extra_config_features)
675 if "install-pattern" not in details and install_pattern is not None:
676 details["install-pattern"] = install_pattern
677 for mk, ok in [
678 ("file_categories", "file-categories"),
679 ("documentation_uris", "documentation-uris"),
680 ("debputy_cmd_templates", "debputy-cmd-templates"),
681 ]:
682 value = plugin_data.info.get(mk)
683 if value and ok == "debputy-cmd-templates":
684 value = [escape_shell(*c) for c in value]
685 _merge_list(details, ok, value)
688def _scan_debian_dir(debian_dir: VirtualPath) -> Iterator[VirtualPath]:
689 for p in debian_dir.iterdir:
690 yield p
691 if p.is_dir and p.path in ("debian/source", "debian/tests"):
692 yield from p.iterdir
695_POST_FORMATTING_REWRITE = {
696 "period-to-underscore": lambda n: n.replace(".", "_"),
697}