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

225 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-06-16 19:34 +0000

1import dataclasses 

2import os 

3import re 

4import textwrap 

5import typing 

6from typing import ( 

7 Any, 

8 Iterator, 

9 NotRequired, 

10 Union, 

11 Literal, 

12 TypedDict, 

13 Annotated, 

14 Self, 

15 cast, 

16) 

17 

18from debian.deb822 import PkgRelation 

19 

20from debputy._manifest_constants import ( 

21 MK_INSTALLATION_SEARCH_DIRS, 

22 MK_BINARY_VERSION, 

23 MK_SERVICES, 

24) 

25from debputy.maintscript_snippet import ( 

26 DpkgMaintscriptHelperCommand, 

27 MaintscriptSnippet, 

28 SnippetResolver, 

29) 

30from debputy.manifest_parser.base_types import ( 

31 DebputyParsedContentStandardConditional, 

32 FileSystemExactMatchRule, 

33) 

34from debputy.manifest_parser.declarative_parser import ParserGenerator 

35from debputy.manifest_parser.exceptions import ManifestParseException 

36from debputy.manifest_parser.parse_hints import DebputyParseHint 

37from debputy.manifest_parser.parser_data import ParserContextData 

38from debputy.manifest_parser.tagging_types import DebputyParsedContent 

39from debputy.manifest_parser.util import AttributePath 

40from debputy.path_matcher import MatchRule, MATCH_ANYTHING, ExactFileSystemPath 

41from debputy.plugin.api import reference_documentation 

42from debputy.plugin.api.impl import ( 

43 DebputyPluginInitializerProvider, 

44 ServiceDefinitionImpl, 

45) 

46from debputy.plugin.api.parser_tables import OPARSER_PACKAGES 

47from debputy.plugin.api.spec import ( 

48 ServiceUpgradeRule, 

49 ServiceDefinition, 

50 DSD, 

51 INTEGRATION_MODE_DH_DEBPUTY_RRR, 

52 not_integrations, 

53 documented_attr, 

54) 

55from debputy.plugins.debputy.types import ( 

56 BuiltUsingItem, 

57 BuiltUsing, 

58 StaticBuiltUsing, 

59 MatchedBuiltUsingRelation, 

60) 

61from debputy.transformation_rules import TransformationRule 

62from debputy.util import _error, manifest_format_doc 

63 

64ACCEPTABLE_CLEAN_ON_REMOVAL_FOR_GLOBS_AND_EXACT_MATCHES = frozenset( 

65 [ 

66 "./var/log", 

67 ] 

68) 

69 

70 

71ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF = frozenset( 

72 [ 

73 "./etc", 

74 "./run", 

75 "./var/lib", 

76 "./var/cache", 

77 "./var/backups", 

78 "./var/spool", 

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

80 "./usr/lib/modules", 

81 "./lib/modules", 

82 # udev special case 

83 "./lib/udev", 

84 "./usr/lib/udev", 

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

86 "./usr/share/misc", 

87 ] 

88) 

89 

90 

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

92 api.pluggable_manifest_rule( 

93 OPARSER_PACKAGES, 

94 MK_BINARY_VERSION, 

95 BinaryVersionParsedFormat, 

96 _parse_binary_version, 

97 source_format=str, 

98 register_value=False, 

99 ) 

100 

101 api.pluggable_manifest_rule( 

102 OPARSER_PACKAGES, 

103 "transformations", 

104 list[TransformationRule], 

105 _unpack_list, 

106 register_value=False, 

107 ) 

108 

109 api.pluggable_manifest_rule( 

110 OPARSER_PACKAGES, 

111 "conffile-management", 

112 list[DpkgMaintscriptHelperCommand], 

113 _unpack_list, 

114 expected_debputy_integration_mode=not_integrations( 

115 INTEGRATION_MODE_DH_DEBPUTY_RRR 

116 ), 

117 register_value=False, 

118 ) 

119 

120 api.pluggable_manifest_rule( 

121 OPARSER_PACKAGES, 

122 MK_SERVICES, 

123 list[ServiceRuleParsedFormat], 

124 _process_service_rules, 

125 source_format=list[ServiceRuleSourceFormat], 

126 expected_debputy_integration_mode=not_integrations( 

127 INTEGRATION_MODE_DH_DEBPUTY_RRR 

128 ), 

129 register_value=False, 

130 ) 

131 

132 api.pluggable_manifest_rule( 

133 OPARSER_PACKAGES, 

134 "dpkg-gensymbols", 

135 DpkgGensymbolsOptionsFormat, 

136 DpkgGensymbolsOptions.parse, 

137 expected_debputy_integration_mode=not_integrations( 

138 INTEGRATION_MODE_DH_DEBPUTY_RRR 

139 ), 

140 # Pulled by makeshlibs.py, so registration is needed. 

141 register_value=True, 

142 inline_reference_documentation=reference_documentation( 

143 title="Configure `dpkg-gensmybols` options (`$RULE_NAME`)", 

144 description=textwrap.dedent( 

145 """\ 

146 Configure the `dpkg-gensymbols` for the given binary 

147 package. 

148 

149 Examples: 

150 

151 packages: 

152 PKG: 

153 $RULE_NAME: 

154 # Pass `-c4` to `dpkg-gensymbols` 

155 check-level: 4 

156 """, 

157 ), 

158 attributes=[ 

159 documented_attr( 

160 "check_level", 

161 textwrap.dedent("""\ 

162 Configure the check-level (`-c`) for `dpkg-gensymbols` 

163 

164 The levels are defined in [man:dpkg-gensymbols.1] as: 

165 

166 * `0`: Never fails. 

167 * `1`: Fails if some symbols have disappeared. 

168 * `2`: Fails if some new symbols have been introduced. 

169 * `3`: Fails if some libraries have disappeared. 

170 * `4`: Fails if some libraries have been introduced. 

171 

172 The higher levels include all the checks from lower levels. 

173 As an example, using a value of `3` would include all the 

174 checks from `3`, `2`, and `1` at the same time. 

175 

176 The default check-level in `dpkg-gensymbols` is `1`. 

177 

178 [man:dpkg-gensymbols.1]: https://manpages.debian.org/dpkg-gensymbols.1 

179 """), 

180 ), 

181 ], 

182 ), 

183 ) 

184 

185 api.pluggable_manifest_rule( 

186 OPARSER_PACKAGES, 

187 "clean-after-removal", 

188 ListParsedFormat, 

189 _parse_clean_after_removal, 

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

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

192 source_format=list[Any], 

193 expected_debputy_integration_mode=not_integrations( 

194 INTEGRATION_MODE_DH_DEBPUTY_RRR 

195 ), 

196 register_value=False, 

197 ) 

198 

199 api.pluggable_manifest_rule( 

200 OPARSER_PACKAGES, 

201 MK_INSTALLATION_SEARCH_DIRS, 

202 InstallationSearchDirsParsedFormat, 

203 _parse_installation_search_dirs, 

204 source_format=list[FileSystemExactMatchRule], 

205 expected_debputy_integration_mode=not_integrations( 

206 INTEGRATION_MODE_DH_DEBPUTY_RRR 

207 ), 

208 register_value=False, 

209 ) 

210 

211 api.pluggable_manifest_rule( 

212 rule_type=OPARSER_PACKAGES, 

213 rule_name="built-using", 

214 parsed_format=list[BuiltUsingParsedFormat], 

215 handler=_parse_built_using, 

216 expected_debputy_integration_mode=not_integrations( 

217 "dh-sequence-zz-debputy-rrr" 

218 ), 

219 inline_reference_documentation=reference_documentation( 

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

221 description=textwrap.dedent( 

222 """\ 

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

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

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

226 arbitrary characters. 

227 

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

229 where license of dependency libraries require the 

230 exact source to be retained. Usually these libraries 

231 will be under the license terms like GNU GPL. 

232 

233 packages: 

234 PKG: 

235 $RULE_NAME: 

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

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

238 - sources-for: foo 

239 when: # foo is always installed 

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

241 

242 Either of these conditions prevents the generation: 

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

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

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

246 architecture or build profile restriction that does 

247 not match the current run. 

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

249 This should only happen inside alternatives, see below. 

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

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

252 must be installed for unrelated reasons. 

253 

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

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

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

257 

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

259 separately. This is a compromise between 

260 reproducibility on automatic builders (where the set 

261 of installed package is constant), and least surprise 

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

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

264 solution when both are installed. 

265 

266 Architecture qualifiers and version restrictions in 

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

268 co-installations require a common source and version. 

269 """, 

270 ), 

271 ), 

272 ) 

273 

274 api.pluggable_manifest_rule( 

275 rule_type=OPARSER_PACKAGES, 

276 rule_name="static-built-using", 

277 parsed_format=list[BuiltUsingParsedFormat], 

278 handler=_parse_static_built_using, 

279 expected_debputy_integration_mode=not_integrations( 

280 "dh-sequence-zz-debputy-rrr" 

281 ), 

282 inline_reference_documentation=reference_documentation( 

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

284 description=textwrap.dedent( 

285 """\ 

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

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

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

289 arbitrary characters. 

290 

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

292 where license of dependency libraries do not require the 

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

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

295 

296 packages: 

297 PKG: 

298 $RULE_NAME: 

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

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

301 - sources-for: foo 

302 when: # foo is always installed 

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

304 

305 Either of these conditions prevents the generation: 

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

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

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

309 architecture or build profile restriction that does 

310 not match the current run. 

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

312 This should only happen inside alternatives, see below. 

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

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

315 must be installed for unrelated reasons. 

316 

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

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

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

320 

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

322 separately. This is a compromise between 

323 reproducibility on automatic builders (where the set 

324 of installed package is constant), and least surprise 

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

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

327 solution when both are installed. 

328 

329 Architecture qualifiers and version restrictions in 

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

331 co-installations require a common source and version. 

332 """, 

333 ), 

334 ), 

335 ) 

336 

337 

338class ServiceRuleSourceFormat(TypedDict): 

339 service: str 

340 type_of_service: NotRequired[str] 

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

342 enable_on_install: NotRequired[bool] 

343 start_on_install: NotRequired[bool] 

344 on_upgrade: NotRequired[ServiceUpgradeRule] 

345 service_manager: NotRequired[ 

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

347 ] 

348 service_managers: NotRequired[list[str]] 

349 

350 

351class ServiceRuleParsedFormat(DebputyParsedContent): 

352 service: str 

353 type_of_service: NotRequired[str] 

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

355 enable_on_install: NotRequired[bool] 

356 start_on_install: NotRequired[bool] 

357 on_upgrade: NotRequired[ServiceUpgradeRule] 

358 service_managers: NotRequired[list[str]] 

359 

360 

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

362class ServiceRule: 

363 definition_source: str 

364 service: str 

365 type_of_service: str 

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

367 enable_on_install: bool | None 

368 start_on_install: bool | None 

369 on_upgrade: ServiceUpgradeRule | None 

370 service_managers: frozenset[str] | None 

371 

372 @classmethod 

373 def from_service_rule_parsed_format( 

374 cls, 

375 data: ServiceRuleParsedFormat, 

376 attribute_path: AttributePath, 

377 ) -> "Self": 

378 service_managers = data.get("service_managers") 

379 return cls( 

380 attribute_path.path, 

381 data["service"], 

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

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

384 data.get("enable_on_install"), 

385 data.get("start_on_install"), 

386 data.get("on_upgrade"), 

387 None if service_managers is None else frozenset(service_managers), 

388 ) 

389 

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

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

392 

393 def apply_to_service_definition( 

394 self, 

395 service_definition: ServiceDefinition[DSD], 

396 ) -> ServiceDefinition[DSD]: 

397 assert isinstance(service_definition, ServiceDefinitionImpl) 

398 if not service_definition.is_plugin_provided_definition: 

399 _error( 

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

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

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

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

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

405 ) 

406 changes = { 

407 "definition_source": self.definition_source, 

408 "is_plugin_provided_definition": False, 

409 } 

410 if ( 

411 self.service != service_definition.name 

412 and self.service in service_definition.names 

413 ): 

414 changes["name"] = self.service 

415 if self.enable_on_install is not None: 

416 changes["auto_start_on_install"] = self.enable_on_install 

417 if self.start_on_install is not None: 

418 changes["auto_start_on_install"] = self.start_on_install 

419 if self.on_upgrade is not None: 

420 changes["on_upgrade"] = self.on_upgrade 

421 

422 return service_definition.replace(**changes) 

423 

424 

425class BinaryVersionParsedFormat(DebputyParsedContent): 

426 binary_version: str 

427 

428 

429class BuiltUsingParsedFormat(DebputyParsedContentStandardConditional): 

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

431 

432 sources_for: str 

433 

434 

435class ListParsedFormat(DebputyParsedContent): 

436 elements: list[Any] 

437 

438 

439class ListOfTransformationRulesFormat(DebputyParsedContent): 

440 elements: list[TransformationRule] 

441 

442 

443class ListOfDpkgMaintscriptHelperCommandFormat(DebputyParsedContent): 

444 elements: list[DpkgMaintscriptHelperCommand] 

445 

446 

447class InstallationSearchDirsParsedFormat(DebputyParsedContent): 

448 installation_search_dirs: list[FileSystemExactMatchRule] 

449 

450 

451type DpkgGensymbolsCheckLevel = typing.Literal[0, 1, 2, 3, 4] 

452 

453 

454class DpkgGensymbolsOptionsFormat(DebputyParsedContent): 

455 check_level: DpkgGensymbolsCheckLevel 

456 

457 

458@dataclasses.dataclass 

459class DpkgGensymbolsOptions: 

460 # We leave `check_level` optional at this stage to ensure consuming code is 

461 # ready for it being optional later even though it is required now. 

462 check_level: DpkgGensymbolsCheckLevel | None 

463 

464 @classmethod 

465 def parse( 

466 cls, 

467 _name: str, 

468 parsed_data: DpkgGensymbolsOptionsFormat, 

469 _attribute_path: AttributePath, 

470 _parser_context: ParserContextData, 

471 ) -> typing.Self: 

472 return cls(**parsed_data) 

473 

474 

475def _parse_binary_version( 

476 _name: str, 

477 parsed_data: BinaryVersionParsedFormat, 

478 _attribute_path: AttributePath, 

479 _parser_context: ParserContextData, 

480) -> str: 

481 return parsed_data["binary_version"] 

482 

483 

484def _parse_installation_search_dirs( 

485 _name: str, 

486 parsed_data: InstallationSearchDirsParsedFormat, 

487 _attribute_path: AttributePath, 

488 _parser_context: ParserContextData, 

489) -> list[FileSystemExactMatchRule]: 

490 return parsed_data["installation_search_dirs"] 

491 

492 

493def _process_service_rules( 

494 _name: str, 

495 parsed_data: list[ServiceRuleParsedFormat], 

496 attribute_path: AttributePath, 

497 _parser_context: ParserContextData, 

498) -> list[ServiceRule]: 

499 return [ 

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

501 for i, x in enumerate(parsed_data) 

502 ] 

503 

504 

505def _parse_built_using( 

506 _name: str, 

507 parsed_data: list[BuiltUsingParsedFormat], 

508 attribute_path: AttributePath, 

509 parser_context: ParserContextData, 

510) -> BuiltUsing: 

511 items = _built_using_handler(parsed_data, attribute_path, parser_context) 

512 return BuiltUsing(items) 

513 

514 

515def _parse_static_built_using( 

516 _name: str, 

517 parsed_data: list[BuiltUsingParsedFormat], 

518 attribute_path: AttributePath, 

519 parser_context: ParserContextData, 

520) -> StaticBuiltUsing: 

521 items = _built_using_handler(parsed_data, attribute_path, parser_context) 

522 return StaticBuiltUsing(items) 

523 

524 

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

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

527 

528 

529def _built_using_matches( 

530 regex: re.Pattern, 

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

532 parser_context: ParserContextData, 

533) -> Iterator[MatchedBuiltUsingRelation]: 

534 """Helper for _validate_built_using.""" 

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

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

537 if raw is not None: 

538 for options in PkgRelation.parse_relations(raw): 

539 for idx, relation in enumerate(options): 

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

541 yield MatchedBuiltUsingRelation(not idx, relation) 

542 

543 

544def _validate_built_using( 

545 parsed_data: BuiltUsingParsedFormat, 

546 attribute_path: AttributePath, 

547 parser_context: ParserContextData, 

548) -> BuiltUsingItem: 

549 """Helper for _built_using_handler.""" 

550 raw_glob = parsed_data["sources_for"] 

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

552 raise ManifestParseException( 

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

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

555 ) 

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

557 

558 pkg = parser_context.current_binary_package_state.binary_package 

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

560 if pkg.is_arch_all: 

561 other = "Build-Depends-Indep" 

562 else: 

563 other = "Build-Depends-Arch" 

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

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

566 raise ManifestParseException( 

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

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

569 ) 

570 return BuiltUsingItem( 

571 matched_packages, 

572 parsed_data.get("when"), 

573 attribute_path, 

574 ) 

575 

576 

577def _built_using_handler( 

578 parsed_data: list[BuiltUsingParsedFormat], 

579 attribute_path: AttributePath, 

580 parser_context: ParserContextData, 

581) -> Iterator[BuiltUsingItem]: 

582 """Helper for _parse_built_using and _parse_static_built_using.""" 

583 for idx, pd in enumerate(parsed_data): 

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

585 

586 

587def _unpack_list( 

588 _name: str, 

589 parsed_data: list[Any], 

590 _attribute_path: AttributePath, 

591 _parser_context: ParserContextData, 

592) -> list[Any]: 

593 return parsed_data 

594 

595 

596class CleanAfterRemovalRuleSourceFormat(TypedDict): 

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

598 paths: NotRequired[list[str]] 

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

600 recursive: NotRequired[bool] 

601 ignore_non_empty_dir: NotRequired[bool] 

602 

603 

604class CleanAfterRemovalRule(DebputyParsedContent): 

605 paths: list[str] 

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

607 recursive: NotRequired[bool] 

608 ignore_non_empty_dir: NotRequired[bool] 

609 

610 

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

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

613_CLEAN_AFTER_REMOVAL_RULE_PARSER = ParserGenerator().generate_parser( 

614 CleanAfterRemovalRule, 

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

616 inline_reference_documentation=reference_documentation( 

617 reference_documentation_url=manifest_format_doc( 

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

619 ), 

620 ), 

621) 

622 

623 

624# Order between clean_on_removal and conffile_management is 

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

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

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

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

629# 

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

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

632# what this note is about) 

633def _parse_clean_after_removal( 

634 _name: str, 

635 parsed_data: ListParsedFormat, 

636 attribute_path: AttributePath, 

637 parser_context: ParserContextData, 

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

639 raw_clean_after_removal = parsed_data["elements"] 

640 package_state = parser_context.current_binary_package_state 

641 

642 for no, raw_transformation in enumerate(raw_clean_after_removal): 

643 definition_source = attribute_path[no] 

644 clean_after_removal_rules = _CLEAN_AFTER_REMOVAL_RULE_PARSER.parse_input( 

645 raw_transformation, 

646 definition_source, 

647 parser_context=parser_context, 

648 ) 

649 patterns = clean_after_removal_rules["paths"] 

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

651 definition_source.path_hint = patterns[0] 

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

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

654 ignore_non_empty_dir = ( 

655 clean_after_removal_rules.get("ignore_non_empty_dir") or False 

656 ) 

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

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

659 else: 

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

661 

662 if ignore_non_empty_dir: 

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

664 raise ManifestParseException( 

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

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

667 ) 

668 for pattern in patterns: 

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

670 raise ManifestParseException( 

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

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

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

674 ) 

675 

676 substitution = parser_context.substitution 

677 match_rules = [ 

678 MatchRule.from_path_or_glob( 

679 p, definition_source.path, substitution=substitution 

680 ) 

681 for p in patterns 

682 ] 

683 content_lines = [ 

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

685 ] 

686 for idx, match_rule in enumerate(match_rules): 

687 original_pattern = patterns[idx] 

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

689 raise ManifestParseException( 

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

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

692 ) 

693 is_subdir_match = False 

694 matched_directory: str | None 

695 if isinstance(match_rule, ExactFileSystemPath): 

696 matched_directory = ( 

697 os.path.dirname(match_rule.path) 

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

699 else match_rule.path 

700 ) 

701 is_subdir_match = True 

702 else: 

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

704 

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

706 raise ManifestParseException( 

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

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

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

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

711 ) 

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

713 ".", 

714 "./", 

715 "", 

716 ) 

717 acceptable_directory = False 

718 would_have_allowed_direct_match = False 

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

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

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

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

723 # acceptable path. As an example: 

724 # 

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

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

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

728 if is_subdir_match and ( 

729 matched_directory 

730 in ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF 

731 ): 

732 acceptable_directory = True 

733 break 

734 if ( 

735 matched_directory 

736 in ACCEPTABLE_CLEAN_ON_REMOVAL_FOR_GLOBS_AND_EXACT_MATCHES 

737 ): 

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

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

740 acceptable_directory = True 

741 break 

742 if ( 

743 matched_directory 

744 in ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF 

745 ): 

746 would_have_allowed_direct_match = True 

747 break 

748 matched_directory = os.path.dirname(matched_directory) 

749 is_subdir_match = True 

750 

751 if would_have_allowed_direct_match and not acceptable_directory: 

752 raise ManifestParseException( 

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

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

755 " have been permitted." 

756 ) 

757 elif not acceptable_directory: 

758 raise ManifestParseException( 

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

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

761 ) 

762 

763 try: 

764 shell_escaped_pattern = match_rule.shell_escape_pattern() 

765 except TypeError: 

766 raise ManifestParseException( 

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

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

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

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

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

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

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

774 ) 

775 

776 if ignore_non_empty_dir: 

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

778 elif recurse: 

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

780 elif original_pattern.endswith("/"): 

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

782 else: 

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

784 content_lines.append(cmd) 

785 content_lines.append("fi\n") 

786 

787 snippet = MaintscriptSnippet( 

788 definition_source.path, 

789 SnippetResolver.snippet_template("".join(content_lines)), 

790 ) 

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