Coverage for src/debputy/plugins/debputy/binary_package_rules.py: 85%

214 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-01-16 17:20 +0000

1import dataclasses 

2import os 

3import re 

4import textwrap 

5from typing import ( 

6 Any, 

7 Iterator, 

8 NotRequired, 

9 Union, 

10 Literal, 

11 TypedDict, 

12 Annotated, 

13 Self, 

14 cast, 

15) 

16 

17from debian.deb822 import PkgRelation 

18 

19from debputy._manifest_constants import ( 

20 MK_INSTALLATION_SEARCH_DIRS, 

21 MK_BINARY_VERSION, 

22 MK_SERVICES, 

23) 

24from debputy.maintscript_snippet import DpkgMaintscriptHelperCommand, MaintscriptSnippet 

25from debputy.manifest_parser.base_types import ( 

26 DebputyParsedContentStandardConditional, 

27 FileSystemExactMatchRule, 

28) 

29from debputy.manifest_parser.declarative_parser import ParserGenerator 

30from debputy.manifest_parser.exceptions import ManifestParseException 

31from debputy.manifest_parser.parse_hints import DebputyParseHint 

32from debputy.manifest_parser.parser_data import ParserContextData 

33from debputy.manifest_parser.tagging_types import DebputyParsedContent 

34from debputy.manifest_parser.util import AttributePath 

35from debputy.path_matcher import MatchRule, MATCH_ANYTHING, ExactFileSystemPath 

36from debputy.plugin.api import reference_documentation 

37from debputy.plugin.api.impl import ( 

38 DebputyPluginInitializerProvider, 

39 ServiceDefinitionImpl, 

40) 

41from debputy.plugin.api.parser_tables import OPARSER_PACKAGES 

42from debputy.plugin.api.spec import ( 

43 ServiceUpgradeRule, 

44 ServiceDefinition, 

45 DSD, 

46 INTEGRATION_MODE_DH_DEBPUTY_RRR, 

47 not_integrations, 

48) 

49from debputy.plugins.debputy.types import ( 

50 BuiltUsingItem, 

51 BuiltUsing, 

52 StaticBuiltUsing, 

53 MatchedBuiltUsingRelation, 

54) 

55from debputy.transformation_rules import TransformationRule 

56from debputy.util import _error, manifest_format_doc 

57 

58ACCEPTABLE_CLEAN_ON_REMOVAL_FOR_GLOBS_AND_EXACT_MATCHES = frozenset( 

59 [ 

60 "./var/log", 

61 ] 

62) 

63 

64 

65ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF = frozenset( 

66 [ 

67 "./etc", 

68 "./run", 

69 "./var/lib", 

70 "./var/cache", 

71 "./var/backups", 

72 "./var/spool", 

73 # linux-image uses these paths with some `rm -f` 

74 "./usr/lib/modules", 

75 "./lib/modules", 

76 # udev special case 

77 "./lib/udev", 

78 "./usr/lib/udev", 

79 # pciutils deletes /usr/share/misc/pci.ids.<ext> 

80 "./usr/share/misc", 

81 ] 

82) 

83 

84 

85def register_binary_package_rules(api: DebputyPluginInitializerProvider) -> None: 

86 api.pluggable_manifest_rule( 

87 OPARSER_PACKAGES, 

88 MK_BINARY_VERSION, 

89 BinaryVersionParsedFormat, 

90 _parse_binary_version, 

91 source_format=str, 

92 register_value=False, 

93 ) 

94 

95 api.pluggable_manifest_rule( 

96 OPARSER_PACKAGES, 

97 "transformations", 

98 list[TransformationRule], 

99 _unpack_list, 

100 register_value=False, 

101 ) 

102 

103 api.pluggable_manifest_rule( 

104 OPARSER_PACKAGES, 

105 "conffile-management", 

106 list[DpkgMaintscriptHelperCommand], 

107 _unpack_list, 

108 expected_debputy_integration_mode=not_integrations( 

109 INTEGRATION_MODE_DH_DEBPUTY_RRR 

110 ), 

111 register_value=False, 

112 ) 

113 

114 api.pluggable_manifest_rule( 

115 OPARSER_PACKAGES, 

116 MK_SERVICES, 

117 list[ServiceRuleParsedFormat], 

118 _process_service_rules, 

119 source_format=list[ServiceRuleSourceFormat], 

120 expected_debputy_integration_mode=not_integrations( 

121 INTEGRATION_MODE_DH_DEBPUTY_RRR 

122 ), 

123 register_value=False, 

124 ) 

125 

126 api.pluggable_manifest_rule( 

127 OPARSER_PACKAGES, 

128 "clean-after-removal", 

129 ListParsedFormat, 

130 _parse_clean_after_removal, 

131 # FIXME: debputy won't see the attributes for this one :'( 

132 # (update `debputy_docs.yaml` when fixed) 

133 source_format=list[Any], 

134 expected_debputy_integration_mode=not_integrations( 

135 INTEGRATION_MODE_DH_DEBPUTY_RRR 

136 ), 

137 register_value=False, 

138 ) 

139 

140 api.pluggable_manifest_rule( 

141 OPARSER_PACKAGES, 

142 MK_INSTALLATION_SEARCH_DIRS, 

143 InstallationSearchDirsParsedFormat, 

144 _parse_installation_search_dirs, 

145 source_format=list[FileSystemExactMatchRule], 

146 expected_debputy_integration_mode=not_integrations( 

147 INTEGRATION_MODE_DH_DEBPUTY_RRR 

148 ), 

149 register_value=False, 

150 ) 

151 

152 api.pluggable_manifest_rule( 

153 rule_type=OPARSER_PACKAGES, 

154 rule_name="built-using", 

155 parsed_format=list[BuiltUsingParsedFormat], 

156 handler=_parse_built_using, 

157 expected_debputy_integration_mode=not_integrations( 

158 "dh-sequence-zz-debputy-rrr" 

159 ), 

160 inline_reference_documentation=reference_documentation( 

161 title="Built-Using dependency relations (`$RULE_NAME`)", 

162 description=textwrap.dedent( 

163 """\ 

164 Generate a `Built-Using` dependency relation on the 

165 build dependencies selected by the `sources-for`, which 

166 may contain a `*` wildcard matching any number of 

167 arbitrary characters. 

168 

169 The `built-using` should be used for static linking 

170 where license of dependency libraries require the 

171 exact source to be retained. Usually these libraries 

172 will be under the license terms like GNU GPL. 

173 

174 packages: 

175 PKG: 

176 $RULE_NAME: 

177 - sources-for: foo-*-source # foo-3.1.0-source 

178 - sources-for: librust-*-dev # several matches 

179 - sources-for: foo 

180 when: # foo is always installed 

181 arch-matches: amd64 # but only used on amd64 

182 

183 Either of these conditions prevents the generation: 

184 * PKG is not part of the current build because of its 

185 `Architecture` or `Build-Profiles` fields. 

186 * The match in `Build-Depends` carries an 

187 architecture or build profile restriction that does 

188 not match the current run. 

189 * The match in `Build-Depends` is not installed. 

190 This should only happen inside alternatives, see below. 

191 * The manifest item carries a `when:` condition that 

192 evaluates to false. This may be useful when the match 

193 must be installed for unrelated reasons. 

194 

195 Matches are searched in the `Build-Depends` field of 

196 the source package, and either `Build-Depends-Indep` 

197 or `Build-Depends-Arch` depending on PKG. 

198 

199 In alternatives like `a | b`, each option may match 

200 separately. This is a compromise between 

201 reproducibility on automatic builders (where the set 

202 of installed package is constant), and least surprise 

203 during local builds (where `b` may be installed 

204 alone). There seems to be no one-size fits all 

205 solution when both are installed. 

206 

207 Architecture qualifiers and version restrictions in 

208 `Build-Depends` are ignored. The only allowed 

209 co-installations require a common source and version. 

210 """, 

211 ), 

212 ), 

213 ) 

214 

215 api.pluggable_manifest_rule( 

216 rule_type=OPARSER_PACKAGES, 

217 rule_name="static-built-using", 

218 parsed_format=list[BuiltUsingParsedFormat], 

219 handler=_parse_static_built_using, 

220 expected_debputy_integration_mode=not_integrations( 

221 "dh-sequence-zz-debputy-rrr" 

222 ), 

223 inline_reference_documentation=reference_documentation( 

224 title="Static-Built-Using dependency relations (`$RULE_NAME`)", 

225 description=textwrap.dedent( 

226 """\ 

227 Generate a `Static-Built-Using` dependency relation on the 

228 build dependencies selected by the `sources-for`, which 

229 may contain a `*` wildcard matching any number of 

230 arbitrary characters. 

231 

232 The `static-built-using` should be used for static linking 

233 where license of dependency libraries do not require the 

234 exact source to be retained. This is usually libraries under 

235 permissive libraries like Apache-2.0 or MIT/X11/Expat. 

236 

237 packages: 

238 PKG: 

239 $RULE_NAME: 

240 - sources-for: foo-*-source # foo-3.1.0-source 

241 - sources-for: librust-*-dev # several matches 

242 - sources-for: foo 

243 when: # foo is always installed 

244 arch-matches: amd64 # but only used on amd64 

245 

246 Either of these conditions prevents the generation: 

247 * PKG is not part of the current build because of its 

248 `Architecture` or `Build-Profiles` fields. 

249 * The match in `Build-Depends` carries an 

250 architecture or build profile restriction that does 

251 not match the current run. 

252 * The match in `Build-Depends` is not installed. 

253 This should only happen inside alternatives, see below. 

254 * The manifest item carries a `when:` condition that 

255 evaluates to false. This may be useful when the match 

256 must be installed for unrelated reasons. 

257 

258 Matches are searched in the `Build-Depends` field of 

259 the source package, and either `Build-Depends-Indep` 

260 or `Build-Depends-Arch` depending on PKG. 

261 

262 In alternatives like `a | b`, each option may match 

263 separately. This is a compromise between 

264 reproducibility on automatic builders (where the set 

265 of installed package is constant), and least surprise 

266 during local builds (where `b` may be installed 

267 alone). There seems to be no one-size fits all 

268 solution when both are installed. 

269 

270 Architecture qualifiers and version restrictions in 

271 `Build-Depends` are ignored. The only allowed 

272 co-installations require a common source and version. 

273 """, 

274 ), 

275 ), 

276 ) 

277 

278 

279class ServiceRuleSourceFormat(TypedDict): 

280 service: str 

281 type_of_service: NotRequired[str] 

282 service_scope: NotRequired[Literal["system", "user"]] 

283 enable_on_install: NotRequired[bool] 

284 start_on_install: NotRequired[bool] 

285 on_upgrade: NotRequired[ServiceUpgradeRule] 

286 service_manager: NotRequired[ 

287 Annotated[str, DebputyParseHint.target_attribute("service_managers")] 

288 ] 

289 service_managers: NotRequired[list[str]] 

290 

291 

292class ServiceRuleParsedFormat(DebputyParsedContent): 

293 service: str 

294 type_of_service: NotRequired[str] 

295 service_scope: NotRequired[Literal["system", "user"]] 

296 enable_on_install: NotRequired[bool] 

297 start_on_install: NotRequired[bool] 

298 on_upgrade: NotRequired[ServiceUpgradeRule] 

299 service_managers: NotRequired[list[str]] 

300 

301 

302@dataclasses.dataclass(slots=True, frozen=True) 

303class ServiceRule: 

304 definition_source: str 

305 service: str 

306 type_of_service: str 

307 service_scope: Literal["system", "user"] 

308 enable_on_install: bool | None 

309 start_on_install: bool | None 

310 on_upgrade: ServiceUpgradeRule | None 

311 service_managers: frozenset[str] | None 

312 

313 @classmethod 

314 def from_service_rule_parsed_format( 

315 cls, 

316 data: ServiceRuleParsedFormat, 

317 attribute_path: AttributePath, 

318 ) -> "Self": 

319 service_managers = data.get("service_managers") 

320 return cls( 

321 attribute_path.path, 

322 data["service"], 

323 data.get("type_of_service", "service"), 

324 cast("Literal['system', 'user']", data.get("service_scope", "system")), 

325 data.get("enable_on_install"), 

326 data.get("start_on_install"), 

327 data.get("on_upgrade"), 

328 frozenset(service_managers) if service_managers else service_managers, 

329 ) 

330 

331 def applies_to_service_manager(self, service_manager: str) -> bool: 

332 return self.service_managers is None or service_manager in self.service_managers 

333 

334 def apply_to_service_definition( 

335 self, 

336 service_definition: ServiceDefinition[DSD], 

337 ) -> ServiceDefinition[DSD]: 

338 assert isinstance(service_definition, ServiceDefinitionImpl) 

339 if not service_definition.is_plugin_provided_definition: 

340 _error( 

341 f"Conflicting definitions related to {self.service} (type: {self.type_of_service}," 

342 f" scope: {self.service_scope}). First definition at {service_definition.definition_source}," 

343 f" the second at {self.definition_source}). If they are for different service managers," 

344 " you can often avoid this problem by explicitly defining which service managers are applicable" 

345 ' to each rule via the "service-managers" keyword.' 

346 ) 

347 changes = { 

348 "definition_source": self.definition_source, 

349 "is_plugin_provided_definition": False, 

350 } 

351 if ( 

352 self.service != service_definition.name 

353 and self.service in service_definition.names 

354 ): 

355 changes["name"] = self.service 

356 if self.enable_on_install is not None: 

357 changes["auto_start_on_install"] = self.enable_on_install 

358 if self.start_on_install is not None: 

359 changes["auto_start_on_install"] = self.start_on_install 

360 if self.on_upgrade is not None: 

361 changes["on_upgrade"] = self.on_upgrade 

362 

363 return service_definition.replace(**changes) 

364 

365 

366class BinaryVersionParsedFormat(DebputyParsedContent): 

367 binary_version: str 

368 

369 

370class BuiltUsingParsedFormat(DebputyParsedContentStandardConditional): 

371 """Also used for static-built-using.""" 

372 

373 sources_for: str 

374 

375 

376class ListParsedFormat(DebputyParsedContent): 

377 elements: list[Any] 

378 

379 

380class ListOfTransformationRulesFormat(DebputyParsedContent): 

381 elements: list[TransformationRule] 

382 

383 

384class ListOfDpkgMaintscriptHelperCommandFormat(DebputyParsedContent): 

385 elements: list[DpkgMaintscriptHelperCommand] 

386 

387 

388class InstallationSearchDirsParsedFormat(DebputyParsedContent): 

389 installation_search_dirs: list[FileSystemExactMatchRule] 

390 

391 

392def _parse_binary_version( 

393 _name: str, 

394 parsed_data: BinaryVersionParsedFormat, 

395 _attribute_path: AttributePath, 

396 _parser_context: ParserContextData, 

397) -> str: 

398 return parsed_data["binary_version"] 

399 

400 

401def _parse_installation_search_dirs( 

402 _name: str, 

403 parsed_data: InstallationSearchDirsParsedFormat, 

404 _attribute_path: AttributePath, 

405 _parser_context: ParserContextData, 

406) -> list[FileSystemExactMatchRule]: 

407 return parsed_data["installation_search_dirs"] 

408 

409 

410def _process_service_rules( 

411 _name: str, 

412 parsed_data: list[ServiceRuleParsedFormat], 

413 attribute_path: AttributePath, 

414 _parser_context: ParserContextData, 

415) -> list[ServiceRule]: 

416 return [ 

417 ServiceRule.from_service_rule_parsed_format(x, attribute_path[i]) 

418 for i, x in enumerate(parsed_data) 

419 ] 

420 

421 

422def _parse_built_using( 

423 _name: str, 

424 parsed_data: list[BuiltUsingParsedFormat], 

425 attribute_path: AttributePath, 

426 parser_context: ParserContextData, 

427) -> BuiltUsing: 

428 items = _built_using_handler(parsed_data, attribute_path, parser_context) 

429 return BuiltUsing(items) 

430 

431 

432def _parse_static_built_using( 

433 _name: str, 

434 parsed_data: list[BuiltUsingParsedFormat], 

435 attribute_path: AttributePath, 

436 parser_context: ParserContextData, 

437) -> StaticBuiltUsing: 

438 items = _built_using_handler(parsed_data, attribute_path, parser_context) 

439 return StaticBuiltUsing(items) 

440 

441 

442_VALID_BUILT_USING_GLOB = re.compile("[a-z*][a-z0-9.+*-]*") 

443_BUILT_USING_GLOB_TO_RE = str.maketrans({".": "[.]", "+": "[+]", "*": ".*"}) 

444 

445 

446def _built_using_matches( 

447 regex: re.Pattern, 

448 other: Literal["Build-Depends-Arch", "Build-Depends-Indep"], 

449 parser_context: ParserContextData, 

450) -> Iterator[MatchedBuiltUsingRelation]: 

451 """Helper for _validate_built_using.""" 

452 for bd_field in ("Build-Depends", other): 

453 raw = parser_context.source_package.fields.get(bd_field) 

454 if raw is not None: 

455 for options in PkgRelation.parse_relations(raw): 

456 for idx, relation in enumerate(options): 

457 if regex.fullmatch(relation["name"]) is not None: 

458 yield MatchedBuiltUsingRelation(not idx, relation) 

459 

460 

461def _validate_built_using( 

462 parsed_data: BuiltUsingParsedFormat, 

463 attribute_path: AttributePath, 

464 parser_context: ParserContextData, 

465) -> BuiltUsingItem: 

466 """Helper for _built_using_handler.""" 

467 raw_glob = parsed_data["sources_for"] 

468 if _VALID_BUILT_USING_GLOB.fullmatch(raw_glob) is None: 468 ↛ 469line 468 didn't jump to line 469 because the condition on line 468 was never true

469 raise ManifestParseException( 

470 f"The glob {raw_glob!r} defined at {attribute_path["sources_for"].path} contained invalid characters." 

471 f" It must only characters valid in a package name plus the `*` character" 

472 ) 

473 regex = re.compile(raw_glob.translate(_BUILT_USING_GLOB_TO_RE)) 

474 

475 pkg = parser_context.current_binary_package_state.binary_package 

476 other: Literal["Build-Depends-Arch", "Build-Depends-Indep"] 

477 if pkg.is_arch_all: 

478 other = "Build-Depends-Indep" 

479 else: 

480 other = "Build-Depends-Arch" 

481 matched_packages = tuple(_built_using_matches(regex, other, parser_context)) 

482 if not matched_packages: 482 ↛ 483line 482 didn't jump to line 483 because the condition on line 482 was never true

483 raise ManifestParseException( 

484 f"The glob {raw_glob!r} defined at {attribute_path["sources_for"].path} matches no clause of Build-Depends or {other}." 

485 " Either a Build-dependency is missing or the glob fails to match the intended build-dependency, or the glob superfluous and can be removed." 

486 ) 

487 return BuiltUsingItem( 

488 matched_packages, 

489 parsed_data.get("when"), 

490 attribute_path, 

491 ) 

492 

493 

494def _built_using_handler( 

495 parsed_data: list[BuiltUsingParsedFormat], 

496 attribute_path: AttributePath, 

497 parser_context: ParserContextData, 

498) -> Iterator[BuiltUsingItem]: 

499 """Helper for _parse_built_using and _parse_static_built_using.""" 

500 for idx, pd in enumerate(parsed_data): 

501 yield _validate_built_using(pd, attribute_path[idx], parser_context) 

502 

503 

504def _unpack_list( 

505 _name: str, 

506 parsed_data: list[Any], 

507 _attribute_path: AttributePath, 

508 _parser_context: ParserContextData, 

509) -> list[Any]: 

510 return parsed_data 

511 

512 

513class CleanAfterRemovalRuleSourceFormat(TypedDict): 

514 path: NotRequired[Annotated[str, DebputyParseHint.target_attribute("paths")]] 

515 paths: NotRequired[list[str]] 

516 delete_on: NotRequired[Literal["purge", "removal"]] 

517 recursive: NotRequired[bool] 

518 ignore_non_empty_dir: NotRequired[bool] 

519 

520 

521class CleanAfterRemovalRule(DebputyParsedContent): 

522 paths: list[str] 

523 delete_on: NotRequired[Literal["purge", "removal"]] 

524 recursive: NotRequired[bool] 

525 ignore_non_empty_dir: NotRequired[bool] 

526 

527 

528# FIXME: Not optimal that we are doing an initialization of ParserGenerator here. But the rule is not depending on any 

529# complex types that is registered by plugins, so it will work for now. 

530_CLEAN_AFTER_REMOVAL_RULE_PARSER = ParserGenerator().generate_parser( 

531 CleanAfterRemovalRule, 

532 source_content=Union[CleanAfterRemovalRuleSourceFormat, str, list[str]], 

533 inline_reference_documentation=reference_documentation( 

534 reference_documentation_url=manifest_format_doc( 

535 "remove-runtime-created-paths-on-purge-or-post-removal-clean-after-removal" 

536 ), 

537 ), 

538) 

539 

540 

541# Order between clean_on_removal and conffile_management is 

542# important. We want the dpkg conffile management rules to happen before the 

543# clean clean_on_removal rules. Since the latter only affects `postrm` 

544# and the order is reversed for `postrm` scripts (among other), we need do 

545# clean_on_removal first to account for the reversing of order. 

546# 

547# FIXME: All of this is currently not really possible todo, but it should be. 

548# (I think it is the correct order by "mistake" rather than by "design", which is 

549# what this note is about) 

550def _parse_clean_after_removal( 

551 _name: str, 

552 parsed_data: ListParsedFormat, 

553 attribute_path: AttributePath, 

554 parser_context: ParserContextData, 

555) -> None: # TODO: Return and pass to a maintscript helper 

556 raw_clean_after_removal = parsed_data["elements"] 

557 package_state = parser_context.current_binary_package_state 

558 

559 for no, raw_transformation in enumerate(raw_clean_after_removal): 

560 definition_source = attribute_path[no] 

561 clean_after_removal_rules = _CLEAN_AFTER_REMOVAL_RULE_PARSER.parse_input( 

562 raw_transformation, 

563 definition_source, 

564 parser_context=parser_context, 

565 ) 

566 patterns = clean_after_removal_rules["paths"] 

567 if patterns: 567 ↛ 569line 567 didn't jump to line 569 because the condition on line 567 was always true

568 definition_source.path_hint = patterns[0] 

569 delete_on = clean_after_removal_rules.get("delete_on") or "purge" 

570 recurse = clean_after_removal_rules.get("recursive") or False 

571 ignore_non_empty_dir = ( 

572 clean_after_removal_rules.get("ignore_non_empty_dir") or False 

573 ) 

574 if delete_on == "purge": 574 ↛ 577line 574 didn't jump to line 577 because the condition on line 574 was always true

575 condition = '[ "$1" = "purge" ]' 

576 else: 

577 condition = '[ "$1" = "remove" ]' 

578 

579 if ignore_non_empty_dir: 

580 if recurse: 580 ↛ 581line 580 didn't jump to line 581 because the condition on line 580 was never true

581 raise ManifestParseException( 

582 'The "recursive" and "ignore-non-empty-dir" options are mutually exclusive.' 

583 f" Both were enabled at the same time in at {definition_source.path}" 

584 ) 

585 for pattern in patterns: 

586 if not pattern.endswith("/"): 586 ↛ 587line 586 didn't jump to line 587 because the condition on line 586 was never true

587 raise ManifestParseException( 

588 'When ignore-non-empty-dir is True, then all patterns must end with a literal "/"' 

589 f' to ensure they only apply to directories. The pattern "{pattern}" at' 

590 f" {definition_source.path} did not." 

591 ) 

592 

593 substitution = parser_context.substitution 

594 match_rules = [ 

595 MatchRule.from_path_or_glob( 

596 p, definition_source.path, substitution=substitution 

597 ) 

598 for p in patterns 

599 ] 

600 content_lines = [ 

601 f"if {condition}; then\n", 

602 ] 

603 for idx, match_rule in enumerate(match_rules): 

604 original_pattern = patterns[idx] 

605 if match_rule is MATCH_ANYTHING: 605 ↛ 606line 605 didn't jump to line 606 because the condition on line 605 was never true

606 raise ManifestParseException( 

607 f'Using "{original_pattern}" in a clean rule would trash the system.' 

608 f" Please restrict this pattern at {definition_source.path} considerably." 

609 ) 

610 is_subdir_match = False 

611 matched_directory: str | None 

612 if isinstance(match_rule, ExactFileSystemPath): 

613 matched_directory = ( 

614 os.path.dirname(match_rule.path) 

615 if match_rule.path not in ("/", ".", "./") 

616 else match_rule.path 

617 ) 

618 is_subdir_match = True 

619 else: 

620 matched_directory = getattr(match_rule, "directory", None) 

621 

622 if matched_directory is None: 622 ↛ 623line 622 didn't jump to line 623 because the condition on line 622 was never true

623 raise ManifestParseException( 

624 f'The pattern "{original_pattern}" defined at {definition_source.path} is not' 

625 f" trivially anchored in a specific directory. Cowardly refusing to use it" 

626 f" in a clean rule as it may trash the system if the pattern is overreaching." 

627 f" Please avoid glob characters in the top level directories." 

628 ) 

629 assert matched_directory.startswith("./") or matched_directory in ( 

630 ".", 

631 "./", 

632 "", 

633 ) 

634 acceptable_directory = False 

635 would_have_allowed_direct_match = False 

636 while matched_directory not in (".", "./", ""): 

637 # Our acceptable paths set includes "/var/lib" or "/etc". We require that the 

638 # pattern is either an exact match, in which case it may match directly inside 

639 # the acceptable directory OR it is a pattern against a subdirectory of the 

640 # acceptable path. As an example: 

641 # 

642 # /etc/inputrc <-- OK, exact match 

643 # /etc/foo/* <-- OK, subdir match 

644 # /etc/* <-- ERROR, glob directly in the accepted directory. 

645 if is_subdir_match and ( 

646 matched_directory 

647 in ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF 

648 ): 

649 acceptable_directory = True 

650 break 

651 if ( 

652 matched_directory 

653 in ACCEPTABLE_CLEAN_ON_REMOVAL_FOR_GLOBS_AND_EXACT_MATCHES 

654 ): 

655 # Special-case: In some directories (such as /var/log), we allow globs directly. 

656 # Notably, X11's log files are /var/log/Xorg.*.log 

657 acceptable_directory = True 

658 break 

659 if ( 

660 matched_directory 

661 in ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF 

662 ): 

663 would_have_allowed_direct_match = True 

664 break 

665 matched_directory = os.path.dirname(matched_directory) 

666 is_subdir_match = True 

667 

668 if would_have_allowed_direct_match and not acceptable_directory: 

669 raise ManifestParseException( 

670 f'The pattern "{original_pattern}" defined at {definition_source.path} seems to' 

671 " be overreaching. If it had been a path (and not use a glob), the rule would" 

672 " have been permitted." 

673 ) 

674 elif not acceptable_directory: 

675 raise ManifestParseException( 

676 f'The pattern or path "{original_pattern}" defined at {definition_source.path} seems to' 

677 f' be overreaching or not limited to the set of "known acceptable" directories.' 

678 ) 

679 

680 try: 

681 shell_escaped_pattern = match_rule.shell_escape_pattern() 

682 except TypeError: 

683 raise ManifestParseException( 

684 f'Sorry, the pattern "{original_pattern}" defined at {definition_source.path}' 

685 f" is unfortunately not supported by `debputy` for clean-after-removal rules." 

686 f" If you can rewrite the rule to something like `/var/log/foo/*.log` or" 

687 f' similar "trivial" patterns. You may have to rewrite the pattern the rule ' 

688 f" into multiple patterns to achieve this. This restriction is to enable " 

689 f' `debputy` to ensure the pattern is correctly executed plus catch "obvious' 

690 f' system trashing" patterns. Apologies for the inconvenience.' 

691 ) 

692 

693 if ignore_non_empty_dir: 

694 cmd = f' rmdir --ignore-fail-on-non-empty "${ DPKG_ROOT} "{shell_escaped_pattern}\n' 

695 elif recurse: 

696 cmd = f' rm -fr "${ DPKG_ROOT} "{shell_escaped_pattern}\n' 

697 elif original_pattern.endswith("/"): 

698 cmd = f' rmdir "${ DPKG_ROOT} "{shell_escaped_pattern}\n' 

699 else: 

700 cmd = f' rm -f "${ DPKG_ROOT} "{shell_escaped_pattern}\n' 

701 content_lines.append(cmd) 

702 content_lines.append("fi\n") 

703 

704 snippet = MaintscriptSnippet(definition_source.path, "".join(content_lines)) 

705 package_state.maintscript_snippets["postrm"].append(snippet)