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