Coverage for src/debputy/analysis/debian_dir.py: 30%

343 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-09-07 09:27 +0000

1import dataclasses 

2import json 

3import os 

4import stat 

5import subprocess 

6from typing import ( 

7 AbstractSet, 

8 List, 

9 Mapping, 

10 Iterable, 

11 Tuple, 

12 Optional, 

13 Sequence, 

14 Dict, 

15 Any, 

16 Union, 

17 Iterator, 

18 TypedDict, 

19 NotRequired, 

20 Container, 

21) 

22 

23from debputy.analysis import REFERENCE_DATA_TABLE 

24from debputy.analysis.analysis_util import flatten_ppfs 

25from debputy.dh.dh_assistant import ( 

26 resolve_active_and_inactive_dh_commands, 

27 read_dh_addon_sequences, 

28 extract_dh_compat_level, 

29) 

30from debputy.integration_detection import determine_debputy_integration_mode 

31from debputy.packager_provided_files import ( 

32 PackagerProvidedFile, 

33 detect_all_packager_provided_files, 

34) 

35from debputy.packages import BinaryPackage, SourcePackage 

36from debputy.plugin.api import ( 

37 VirtualPath, 

38 packager_provided_file_reference_documentation, 

39) 

40from debputy.plugin.api.feature_set import PluginProvidedFeatureSet 

41from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin 

42from debputy.plugin.api.impl_types import ( 

43 PluginProvidedKnownPackagingFile, 

44 DebputyPluginMetadata, 

45 KnownPackagingFileInfo, 

46 DHCompatibilityBasedRule, 

47 PackagerProvidedFileClassSpec, 

48 expand_known_packaging_config_features, 

49) 

50from debputy.plugin.api.spec import DebputyIntegrationMode 

51from debputy.util import ( 

52 assume_not_none, 

53 escape_shell, 

54 _trace_log, 

55 _is_trace_log_enabled, 

56 render_command, 

57) 

58 

59PackagingFileInfo = TypedDict( 

60 "PackagingFileInfo", 

61 { 

62 "path": str, 

63 "binary-package": NotRequired[str], 

64 "install-path": NotRequired[str], 

65 "install-pattern": NotRequired[str], 

66 "file-categories": NotRequired[List[str]], 

67 "config-features": NotRequired[List[str]], 

68 "pkgfile-is-active-in-build": NotRequired[bool], 

69 "pkgfile-stem": NotRequired[str], 

70 "pkgfile-explicit-package-name": NotRequired[bool], 

71 "pkgfile-name-segment": NotRequired[str], 

72 "pkgfile-architecture-restriction": NotRequired[str], 

73 "likely-typo-of": NotRequired[str], 

74 "likely-generated-from": NotRequired[List[str]], 

75 "related-tools": NotRequired[List[str]], 

76 "documentation-uris": NotRequired[List[str]], 

77 "debputy-cmd-templates": NotRequired[List[List[str]]], 

78 "generates": NotRequired[str], 

79 "generated-from": NotRequired[str], 

80 }, 

81) 

82 

83 

84def _perl_json_bool(base: Optional[Any]) -> Optional[Any]: 

85 if base is None: 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true

86 return None 

87 

88 if isinstance(base, bool): 88 ↛ 95line 88 didn't jump to line 95 because the condition on line 88 was always true

89 return base 

90 

91 # Account for different ways that Perl will or could return a JSON bool, 

92 # when not using `JSON::PP::{true,false}`. 

93 # 

94 # https://salsa.debian.org/debian/debputy/-/issues/119#note_627057 

95 if isinstance(base, int): 

96 if base == 0: 

97 return False 

98 return True if base == 1 else base 

99 if isinstance(base, str): 

100 return False if base == "" else base 

101 return base 

102 

103 

104def scan_debian_dir( 

105 feature_set: PluginProvidedFeatureSet, 

106 source_package: SourcePackage, 

107 binary_packages: Mapping[str, BinaryPackage], 

108 debian_dir: VirtualPath, 

109 *, 

110 uses_dh_sequencer: bool = True, 

111 dh_sequences: Optional[AbstractSet[str]] = None, 

112) -> Tuple[List[PackagingFileInfo], List[str], int, Optional[object]]: 

113 known_packaging_files = feature_set.known_packaging_files 

114 debputy_plugin_metadata = plugin_metadata_for_debputys_own_plugin() 

115 reference_data_set_names = [ 

116 "config-features", 

117 "file-categories", 

118 ] 

119 for n in reference_data_set_names: 

120 assert n in REFERENCE_DATA_TABLE 

121 

122 annotated: List[PackagingFileInfo] = [] 

123 seen_paths: Dict[str, PackagingFileInfo] = {} 

124 

125 if dh_sequences is None: 

126 r = read_dh_addon_sequences(debian_dir) 

127 if r is not None: 

128 bd_sequences, dr_sequences, uses_dh_sequencer = r 

129 dh_sequences = bd_sequences | dr_sequences 

130 else: 

131 dh_sequences = set() 

132 uses_dh_sequencer = False 

133 debputy_integration_mode = determine_debputy_integration_mode( 

134 source_package.fields, 

135 dh_sequences, 

136 ) 

137 is_debputy_package = debputy_integration_mode is not None 

138 dh_compat_level, dh_assistant_exit_code = extract_dh_compat_level() 

139 dh_issues = [] 

140 

141 static_packaging_files = { 

142 kpf.detection_value: kpf 

143 for kpf in known_packaging_files.values() 

144 if kpf.detection_method == "path" 

145 } 

146 dh_pkgfile_docs = { 

147 kpf.detection_value: kpf 

148 for kpf in known_packaging_files.values() 

149 if kpf.detection_method == "dh.pkgfile" 

150 } 

151 if is_debputy_package: 

152 all_debputy_ppfs = list( 

153 flatten_ppfs( 

154 detect_all_packager_provided_files( 

155 feature_set, 

156 debian_dir, 

157 binary_packages, 

158 allow_fuzzy_matches=True, 

159 detect_typos=True, 

160 ignore_paths=static_packaging_files, 

161 ) 

162 ) 

163 ) 

164 else: 

165 all_debputy_ppfs = [] 

166 

167 if dh_compat_level is not None: 

168 ( 

169 all_dh_ppfs, 

170 _, 

171 dh_issues, 

172 dh_assistant_exit_code, 

173 ) = resolve_debhelper_config_files( 

174 debian_dir, 

175 binary_packages, 

176 debputy_plugin_metadata, 

177 feature_set, 

178 dh_sequences, 

179 dh_compat_level, 

180 uses_dh_sequencer, 

181 debputy_integration_mode=debputy_integration_mode, 

182 ignore_paths=static_packaging_files, 

183 ) 

184 

185 else: 

186 all_dh_ppfs = [] 

187 

188 for ppf in all_debputy_ppfs: 

189 key = ppf.path.path 

190 ref_doc = ppf.definition.reference_documentation 

191 documentation_uris = ( 

192 ref_doc.format_documentation_uris if ref_doc is not None else None 

193 ) 

194 details: PackagingFileInfo = { 

195 "path": key, 

196 "binary-package": ppf.package_name, 

197 "pkgfile-stem": ppf.definition.stem, 

198 "pkgfile-explicit-package-name": ppf.uses_explicit_package_name, 

199 "pkgfile-is-active-in-build": ppf.definition.has_active_command, 

200 "debputy-cmd-templates": [ 

201 ["debputy", "plugin", "show", "p-p-f", ppf.definition.stem] 

202 ], 

203 } 

204 if ppf.fuzzy_match and key.endswith(".in"): 

205 _merge_list(details, "file-categories", ["generic-template"]) 

206 details["generates"] = key[:-3] 

207 elif assume_not_none(ppf.path.parent_dir).get(ppf.path.name + ".in"): 

208 _merge_list(details, "file-categories", ["generated"]) 

209 details["generated-from"] = key + ".in" 

210 name_segment = ppf.name_segment 

211 arch_restriction = ppf.architecture_restriction 

212 if name_segment is not None: 

213 details["pkgfile-name-segment"] = name_segment 

214 if arch_restriction: 

215 details["pkgfile-architecture-restriction"] = arch_restriction 

216 seen_paths[key] = details 

217 annotated.append(details) 

218 static_details = static_packaging_files.get(key) 

219 if static_details is not None: 

220 # debhelper compat rules does not apply to debputy files 

221 _add_known_packaging_data(details, static_details, None) 

222 if documentation_uris: 

223 details["documentation-uris"] = list(documentation_uris) 

224 

225 _merge_ppfs(annotated, seen_paths, all_dh_ppfs, dh_pkgfile_docs, dh_compat_level) 

226 

227 for virtual_path in _scan_debian_dir(debian_dir): 

228 key = virtual_path.path 

229 if key in seen_paths or _skip_path(virtual_path): 

230 continue 

231 

232 static_match = static_packaging_files.get(virtual_path.path) 

233 if static_match is not None: 

234 details: PackagingFileInfo = { 

235 "path": key, 

236 } 

237 annotated.append(details) 

238 if assume_not_none(virtual_path.parent_dir).get(virtual_path.name + ".in"): 

239 details["generated-from"] = key + ".in" 

240 _merge_list(details, "file-categories", ["generated"]) 

241 _add_known_packaging_data(details, static_match, dh_compat_level) 

242 

243 return annotated, reference_data_set_names, dh_assistant_exit_code, dh_issues 

244 

245 

246def _skip_path(virtual_path: VirtualPath) -> bool: 

247 if virtual_path.is_symlink: 

248 try: 

249 st = os.stat(virtual_path.fs_path) 

250 except FileNotFoundError: 

251 return True 

252 else: 

253 if not stat.S_ISREG(st.st_mode): 

254 return True 

255 elif not virtual_path.is_file: 

256 return True 

257 return False 

258 

259 

260def _fake_PPFClassSpec( 

261 debputy_plugin_metadata: DebputyPluginMetadata, 

262 stem: str, 

263 doc_uris: Optional[Sequence[str]], 

264 install_pattern: Optional[str], 

265 *, 

266 default_priority: Optional[int] = None, 

267 packageless_is_fallback_for_all_packages: bool = False, 

268 post_formatting_rewrite: Optional[str] = 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 reservation_only=False, 

289 formatting_callback=None, 

290 bug_950723=bug_950723, 

291 has_active_command=has_active_command, 

292 reference_documentation=packager_provided_file_reference_documentation( 

293 format_documentation_uris=doc_uris, 

294 ), 

295 ) 

296 

297 

298def _relevant_dh_compat_rules( 

299 compat_level: Optional[int], 

300 info: KnownPackagingFileInfo, 

301) -> Iterable[DHCompatibilityBasedRule]: 

302 if compat_level is None: 

303 return 

304 dh_compat_rules = info.get("dh_compat_rules") 

305 if not dh_compat_rules: 

306 return 

307 for dh_compat_rule in dh_compat_rules: 

308 rule_compat_level = dh_compat_rule.get("starting_with_compat_level") 

309 if rule_compat_level is not None and compat_level < rule_compat_level: 

310 continue 

311 yield dh_compat_rule 

312 

313 

314def _kpf_install_pattern( 

315 compat_level: Optional[int], 

316 ppkpf: PluginProvidedKnownPackagingFile, 

317) -> Optional[str]: 

318 for compat_rule in _relevant_dh_compat_rules(compat_level, ppkpf.info): 

319 install_pattern = compat_rule.get("install_pattern") 

320 if install_pattern is not None: 

321 return install_pattern 

322 return ppkpf.info.get("install_pattern") 

323 

324 

325def resolve_debhelper_config_files( 

326 debian_dir: VirtualPath, 

327 binary_packages: Mapping[str, BinaryPackage], 

328 debputy_plugin_metadata: DebputyPluginMetadata, 

329 plugin_feature_set: PluginProvidedFeatureSet, 

330 dh_rules_addons: AbstractSet[str], 

331 dh_compat_level: int, 

332 saw_dh: bool, 

333 ignore_paths: Container[str] = frozenset(), 

334 *, 

335 debputy_integration_mode: Optional[DebputyIntegrationMode] = None, 

336 cwd: Optional[str] = None, 

337) -> Tuple[List[PackagerProvidedFile], List[str], Optional[object], int]: 

338 

339 dh_ppf_docs = { 

340 kpf.detection_value: kpf 

341 for kpf in plugin_feature_set.known_packaging_files.values() 

342 if kpf.detection_method == "dh.pkgfile" 

343 } 

344 

345 dh_ppfs = {} 

346 commands, exit_code = _relevant_dh_commands(dh_rules_addons, cwd=cwd) 

347 

348 cmd = ["dh_assistant", "list-guessed-dh-config-files"] 

349 if dh_rules_addons: 

350 addons = ",".join(dh_rules_addons) 

351 cmd.append(f"--with={addons}") 

352 try: 

353 output = subprocess.check_output( 

354 cmd, 

355 stderr=subprocess.DEVNULL, 

356 cwd=cwd, 

357 ) 

358 except (subprocess.CalledProcessError, FileNotFoundError) as e: 

359 config_files = [] 

360 issues = None 

361 if isinstance(e, subprocess.CalledProcessError): 

362 exit_code = e.returncode 

363 else: 

364 exit_code = 127 

365 if _is_trace_log_enabled(): 

366 _trace_log( 

367 f"Command {render_command(*cmd, cwd=cwd)} failed with {exit_code} ({cwd=})" 

368 ) 

369 else: 

370 result = json.loads(output) 

371 config_files: List[Union[Mapping[str, Any], object]] = result.get( 

372 "config-files", [] 

373 ) 

374 missing_introspection: Optional[List[str]] = result.get( 

375 "commands-not-introspectable", [] 

376 ) 

377 issues = result.get("issues") 

378 if _is_trace_log_enabled(): 378 ↛ 379line 378 didn't jump to line 379 because the condition on line 378 was never true

379 _trace_log( 

380 f"Command {render_command(*cmd, cwd=cwd)} returned successfully: {output}" 

381 ) 

382 dh_commands = resolve_active_and_inactive_dh_commands(dh_rules_addons) 

383 for config_file in config_files: 

384 if not isinstance(config_file, dict): 384 ↛ 385line 384 didn't jump to line 385 because the condition on line 384 was never true

385 continue 

386 if config_file.get("file-type") != "pkgfile": 

387 continue 

388 stem = config_file.get("pkgfile") 

389 if stem is None: 389 ↛ 390line 389 didn't jump to line 390 because the condition on line 389 was never true

390 continue 

391 internal = config_file.get("internal") 

392 if isinstance(internal, dict): 

393 bug_950723 = internal.get("bug#950723", False) is True 

394 else: 

395 bug_950723 = False 

396 commands = config_file.get("commands") 

397 documentation_uris = [] 

398 related_tools = [] 

399 seen_commands = set() 

400 seen_docs = set() 

401 ppkpf = dh_ppf_docs.get(stem) 

402 

403 if ppkpf: 403 ↛ 404line 403 didn't jump to line 404 because the condition on line 403 was never true

404 dh_cmds = ppkpf.info.get("debhelper_commands") 

405 doc_uris = ppkpf.info.get("documentation_uris") 

406 default_priority = ppkpf.info.get("default_priority") 

407 if doc_uris is not None: 

408 seen_docs.update(doc_uris) 

409 documentation_uris.extend(doc_uris) 

410 if dh_cmds is not None: 

411 seen_commands.update(dh_cmds) 

412 related_tools.extend(dh_cmds) 

413 install_pattern = _kpf_install_pattern(dh_compat_level, ppkpf) 

414 post_formatting_rewrite = ppkpf.info.get("post_formatting_rewrite") 

415 packageless_is_fallback_for_all_packages = ppkpf.info.get( 

416 "packageless_is_fallback_for_all_packages", 

417 False, 

418 ) 

419 # If it is a debhelper PPF, then `has_active_command` is false by default. 

420 has_active_command = ppkpf.info.get("has_active_command", False) 

421 else: 

422 install_pattern = None 

423 default_priority = None 

424 post_formatting_rewrite = None 

425 packageless_is_fallback_for_all_packages = False 

426 has_active_command = False 

427 for command in commands: 

428 if isinstance(command, dict): 428 ↛ 427line 428 didn't jump to line 427 because the condition on line 428 was always true

429 command_name = command.get("command") 

430 if isinstance(command_name, str) and command_name: 430 ↛ 439line 430 didn't jump to line 439 because the condition on line 430 was always true

431 if command_name not in seen_commands: 431 ↛ 434line 431 didn't jump to line 434 because the condition on line 431 was always true

432 related_tools.append(command_name) 

433 seen_commands.add(command_name) 

434 manpage = f"man:{command_name}(1)" 

435 if manpage not in seen_docs: 435 ↛ 440line 435 didn't jump to line 440 because the condition on line 435 was always true

436 documentation_uris.append(manpage) 

437 seen_docs.add(manpage) 

438 else: 

439 continue 

440 is_active = _perl_json_bool(command.get("is-active", True)) 

441 if is_active is None and command_name in dh_commands.active_commands: 441 ↛ 442line 441 didn't jump to line 442 because the condition on line 441 was never true

442 is_active = True 

443 if not isinstance(is_active, bool): 443 ↛ 444line 443 didn't jump to line 444 because the condition on line 443 was never true

444 continue 

445 if is_active: 

446 has_active_command = True 

447 

448 if debputy_integration_mode == "full": 448 ↛ 450line 448 didn't jump to line 450 because the condition on line 448 was never true

449 # dh commands are never active in full integration mode. 

450 has_active_command = False 

451 elif not saw_dh: 451 ↛ 455line 451 didn't jump to line 455 because the condition on line 451 was always true

452 # If we did not see `dh`, we assume classic `debhelper` where we have no way of knowing 

453 # which commands are active. 

454 has_active_command = True 

455 dh_ppfs[stem] = _fake_PPFClassSpec( 

456 debputy_plugin_metadata, 

457 stem, 

458 documentation_uris, 

459 install_pattern, 

460 default_priority=default_priority, 

461 post_formatting_rewrite=post_formatting_rewrite, 

462 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages, 

463 bug_950723=bug_950723, 

464 has_active_command=has_active_command, 

465 ) 

466 for ppkpf in dh_ppf_docs.values(): 466 ↛ 467line 466 didn't jump to line 467 because the loop on line 466 never started

467 stem = ppkpf.detection_value 

468 if stem in dh_ppfs: 

469 continue 

470 

471 default_priority = ppkpf.info.get("default_priority") 

472 install_pattern = _kpf_install_pattern(dh_compat_level, ppkpf) 

473 post_formatting_rewrite = ppkpf.info.get("post_formatting_rewrite") 

474 packageless_is_fallback_for_all_packages = ppkpf.info.get( 

475 "packageless_is_fallback_for_all_packages", 

476 False, 

477 ) 

478 has_active_command = ( 

479 ppkpf.info.get("has_active_command", False) if saw_dh else False 

480 ) 

481 if not has_active_command: 

482 dh_cmds = ppkpf.info.get("debhelper_commands") 

483 if dh_cmds: 

484 has_active_command = any( 

485 c in dh_commands.active_commands for c in dh_cmds 

486 ) 

487 dh_ppfs[stem] = _fake_PPFClassSpec( 

488 debputy_plugin_metadata, 

489 stem, 

490 ppkpf.info.get("documentation_uris"), 

491 install_pattern, 

492 default_priority=default_priority, 

493 post_formatting_rewrite=post_formatting_rewrite, 

494 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages, 

495 has_active_command=has_active_command, 

496 ) 

497 dh_ppf_feature_set = _fake_plugin_feature_set(plugin_feature_set, dh_ppfs) 

498 all_dh_ppfs = list( 

499 flatten_ppfs( 

500 detect_all_packager_provided_files( 

501 dh_ppf_feature_set, 

502 debian_dir, 

503 binary_packages, 

504 allow_fuzzy_matches=True, 

505 detect_typos=True, 

506 ignore_paths=ignore_paths, 

507 ) 

508 ) 

509 ) 

510 return all_dh_ppfs, missing_introspection, issues, exit_code 

511 

512 

513def _fake_plugin_feature_set( 

514 plugin_feature_set: PluginProvidedFeatureSet, 

515 fake_defs: Mapping[str, PackagerProvidedFileClassSpec], 

516) -> PluginProvidedFeatureSet: 

517 return dataclasses.replace(plugin_feature_set, packager_provided_files=fake_defs) 

518 

519 

520def _merge_list( 

521 existing_table: Dict[str, Any], 

522 key: str, 

523 new_data: Optional[Sequence[str]], 

524) -> None: 

525 if not new_data: 

526 return 

527 existing_values = existing_table.get(key, []) 

528 if isinstance(existing_values, tuple): 

529 existing_values = list(existing_values) 

530 assert isinstance(existing_values, list) 

531 seen = set(existing_values) 

532 existing_values.extend(x for x in new_data if x not in seen) 

533 existing_table[key] = existing_values 

534 

535 

536def _merge_ppfs( 

537 identified: List[PackagingFileInfo], 

538 seen_paths: Dict[str, PackagingFileInfo], 

539 ppfs: List[PackagerProvidedFile], 

540 context: Mapping[str, PluginProvidedKnownPackagingFile], 

541 dh_compat_level: Optional[int], 

542) -> None: 

543 for ppf in ppfs: 

544 key = ppf.path.path 

545 ref_doc = ppf.definition.reference_documentation 

546 documentation_uris = ( 

547 ref_doc.format_documentation_uris if ref_doc is not None else None 

548 ) 

549 if not ppf.definition.installed_as_format.startswith("not-a-real-ppf"): 

550 try: 

551 parts = ppf.compute_dest() 

552 except RuntimeError: 

553 dest = None 

554 else: 

555 dest = "/".join(parts).lstrip(".") 

556 else: 

557 dest = None 

558 orig_details = seen_paths.get(key) 

559 if orig_details is None: 

560 details: PackagingFileInfo = { 

561 "path": key, 

562 "pkgfile-stem": ppf.definition.stem, 

563 "pkgfile-is-active-in-build": ppf.definition.has_active_command, 

564 "pkgfile-explicit-package-name": ppf.uses_explicit_package_name, 

565 "binary-package": ppf.package_name, 

566 } 

567 if ppf.expected_path is not None: 

568 details["likely-typo-of"] = ppf.expected_path 

569 identified.append(details) 

570 else: 

571 details = orig_details 

572 # We do not merge the "is typo" field; if the original 

573 for k, v in [ 

574 ("pkgfile-stem", ppf.definition.stem), 

575 ("pkgfile-explicit-package-name", ppf.definition.has_active_command), 

576 ("binary-package", ppf.package_name), 

577 ]: 

578 if k not in details: 

579 details[k] = v 

580 if ppf.definition.has_active_command and details.get( 

581 "pkgfile-is-active-in-build", False 

582 ): 

583 details["pkgfile-is-active-in-build"] = True 

584 if ppf.expected_path is None and "likely-typo-of" in details: 

585 del details["likely-typo-of"] 

586 

587 name_segment = ppf.name_segment 

588 arch_restriction = ppf.architecture_restriction 

589 if name_segment is not None and "pkgfile-name-segment" not in details: 

590 details["pkgfile-name-segment"] = name_segment 

591 if ( 

592 arch_restriction is not None 

593 and "pkgfile-architecture-restriction" not in details 

594 ): 

595 details["pkgfile-architecture-restriction"] = arch_restriction 

596 if ppf.fuzzy_match and key.endswith(".in"): 

597 _merge_list(details, "file-categories", ["generic-template"]) 

598 details["generates"] = key[:-3] 

599 elif assume_not_none(ppf.path.parent_dir).get(ppf.path.name + ".in"): 

600 _merge_list(details, "file-categories", ["generated"]) 

601 details["generated-from"] = key + ".in" 

602 if dest is not None and "install-path" not in details: 

603 details["install-path"] = dest 

604 

605 extra_details = context.get(ppf.definition.stem) 

606 if extra_details is not None: 

607 _add_known_packaging_data(details, extra_details, dh_compat_level) 

608 

609 _merge_list(details, "documentation-uris", documentation_uris) 

610 

611 

612def _relevant_dh_commands( 

613 dh_rules_addons: Iterable[str], 

614 cwd: Optional[str] = None, 

615) -> Tuple[List[str], int]: 

616 cmd = ["dh_assistant", "list-commands", "--output-format=json"] 

617 if dh_rules_addons: 

618 addons = ",".join(dh_rules_addons) 

619 cmd.append(f"--with={addons}") 

620 try: 

621 output = subprocess.check_output( 

622 cmd, 

623 stderr=subprocess.DEVNULL, 

624 cwd=cwd, 

625 ) 

626 except (FileNotFoundError, subprocess.CalledProcessError) as e: 

627 exit_code = 127 

628 if isinstance(e, subprocess.CalledProcessError): 

629 exit_code = e.returncode 

630 if _is_trace_log_enabled(): 

631 _trace_log( 

632 f"Command {render_command(*cmd, cwd=cwd)} failed with {exit_code} ({cwd=})" 

633 ) 

634 return [], exit_code 

635 else: 

636 data = json.loads(output) 

637 commands_json = data.get("commands") 

638 commands = [] 

639 if _is_trace_log_enabled(): 639 ↛ 640line 639 didn't jump to line 640 because the condition on line 639 was never true

640 _trace_log( 

641 f"Command {render_command(*cmd, cwd=cwd)} returned successfully: {output}" 

642 ) 

643 for command in commands_json: 

644 if isinstance(command, dict): 644 ↛ 643line 644 didn't jump to line 643 because the condition on line 644 was always true

645 command_name = command.get("command") 

646 if isinstance(command_name, str) and command_name: 646 ↛ 643line 646 didn't jump to line 643 because the condition on line 646 was always true

647 commands.append(command_name) 

648 return commands, 0 

649 

650 

651def _add_known_packaging_data( 

652 details: PackagingFileInfo, 

653 plugin_data: PluginProvidedKnownPackagingFile, 

654 dh_compat_level: Optional[int], 

655): 

656 install_pattern = _kpf_install_pattern( 

657 dh_compat_level, 

658 plugin_data, 

659 ) 

660 config_features = plugin_data.info.get("config_features") 

661 if config_features: 

662 config_features = expand_known_packaging_config_features( 

663 dh_compat_level or 0, 

664 config_features, 

665 ) 

666 _merge_list(details, "config-features", config_features) 

667 

668 if dh_compat_level is not None: 

669 extra_config_features = [] 

670 for dh_compat_rule in _relevant_dh_compat_rules( 

671 dh_compat_level, plugin_data.info 

672 ): 

673 cf = dh_compat_rule.get("add_config_features") 

674 if cf: 

675 extra_config_features.extend(cf) 

676 if extra_config_features: 

677 extra_config_features = expand_known_packaging_config_features( 

678 dh_compat_level, 

679 extra_config_features, 

680 ) 

681 _merge_list(details, "config-features", extra_config_features) 

682 if "install-pattern" not in details and install_pattern is not None: 

683 details["install-pattern"] = install_pattern 

684 for mk, ok in [ 

685 ("file_categories", "file-categories"), 

686 ("documentation_uris", "documentation-uris"), 

687 ("debputy_cmd_templates", "debputy-cmd-templates"), 

688 ]: 

689 value = plugin_data.info.get(mk) 

690 if value and ok == "debputy-cmd-templates": 

691 value = [escape_shell(*c) for c in value] 

692 _merge_list(details, ok, value) 

693 

694 

695def _scan_debian_dir(debian_dir: VirtualPath) -> Iterator[VirtualPath]: 

696 for p in debian_dir.iterdir: 

697 yield p 

698 if p.is_dir and p.path in ("debian/source", "debian/tests"): 

699 yield from p.iterdir 

700 

701 

702_POST_FORMATTING_REWRITE = { 

703 "period-to-underscore": lambda n: n.replace(".", "_"), 

704}