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

225 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-05-11 16:06 +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 DpkgMaintscriptHelperCommand, MaintscriptSnippet 

26from debputy.manifest_parser.base_types import ( 

27 DebputyParsedContentStandardConditional, 

28 FileSystemExactMatchRule, 

29) 

30from debputy.manifest_parser.declarative_parser import ParserGenerator 

31from debputy.manifest_parser.exceptions import ManifestParseException 

32from debputy.manifest_parser.parse_hints import DebputyParseHint 

33from debputy.manifest_parser.parser_data import ParserContextData 

34from debputy.manifest_parser.tagging_types import DebputyParsedContent 

35from debputy.manifest_parser.util import AttributePath 

36from debputy.path_matcher import MatchRule, MATCH_ANYTHING, ExactFileSystemPath 

37from debputy.plugin.api import reference_documentation 

38from debputy.plugin.api.impl import ( 

39 DebputyPluginInitializerProvider, 

40 ServiceDefinitionImpl, 

41) 

42from debputy.plugin.api.parser_tables import OPARSER_PACKAGES 

43from debputy.plugin.api.spec import ( 

44 ServiceUpgradeRule, 

45 ServiceDefinition, 

46 DSD, 

47 INTEGRATION_MODE_DH_DEBPUTY_RRR, 

48 not_integrations, 

49 documented_attr, 

50) 

51from debputy.plugins.debputy.types import ( 

52 BuiltUsingItem, 

53 BuiltUsing, 

54 StaticBuiltUsing, 

55 MatchedBuiltUsingRelation, 

56) 

57from debputy.transformation_rules import TransformationRule 

58from debputy.util import _error, manifest_format_doc 

59 

60ACCEPTABLE_CLEAN_ON_REMOVAL_FOR_GLOBS_AND_EXACT_MATCHES = frozenset( 

61 [ 

62 "./var/log", 

63 ] 

64) 

65 

66 

67ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF = frozenset( 

68 [ 

69 "./etc", 

70 "./run", 

71 "./var/lib", 

72 "./var/cache", 

73 "./var/backups", 

74 "./var/spool", 

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

76 "./usr/lib/modules", 

77 "./lib/modules", 

78 # udev special case 

79 "./lib/udev", 

80 "./usr/lib/udev", 

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

82 "./usr/share/misc", 

83 ] 

84) 

85 

86 

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

88 api.pluggable_manifest_rule( 

89 OPARSER_PACKAGES, 

90 MK_BINARY_VERSION, 

91 BinaryVersionParsedFormat, 

92 _parse_binary_version, 

93 source_format=str, 

94 register_value=False, 

95 ) 

96 

97 api.pluggable_manifest_rule( 

98 OPARSER_PACKAGES, 

99 "transformations", 

100 list[TransformationRule], 

101 _unpack_list, 

102 register_value=False, 

103 ) 

104 

105 api.pluggable_manifest_rule( 

106 OPARSER_PACKAGES, 

107 "conffile-management", 

108 list[DpkgMaintscriptHelperCommand], 

109 _unpack_list, 

110 expected_debputy_integration_mode=not_integrations( 

111 INTEGRATION_MODE_DH_DEBPUTY_RRR 

112 ), 

113 register_value=False, 

114 ) 

115 

116 api.pluggable_manifest_rule( 

117 OPARSER_PACKAGES, 

118 MK_SERVICES, 

119 list[ServiceRuleParsedFormat], 

120 _process_service_rules, 

121 source_format=list[ServiceRuleSourceFormat], 

122 expected_debputy_integration_mode=not_integrations( 

123 INTEGRATION_MODE_DH_DEBPUTY_RRR 

124 ), 

125 register_value=False, 

126 ) 

127 

128 api.pluggable_manifest_rule( 

129 OPARSER_PACKAGES, 

130 "dpkg-gensymbols", 

131 DpkgGensymbolsOptionsFormat, 

132 DpkgGensymbolsOptions.parse, 

133 expected_debputy_integration_mode=not_integrations( 

134 INTEGRATION_MODE_DH_DEBPUTY_RRR 

135 ), 

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

137 register_value=True, 

138 inline_reference_documentation=reference_documentation( 

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

140 description=textwrap.dedent( 

141 """\ 

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

143 package. 

144 

145 Examples: 

146 

147 packages: 

148 PKG: 

149 $RULE_NAME: 

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

151 check-level: 4 

152 """, 

153 ), 

154 attributes=[ 

155 documented_attr( 

156 "check_level", 

157 textwrap.dedent("""\ 

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

159 

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

161 

162 * `0`: Never fails. 

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

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

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

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

167 

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

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

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

171 

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

173 

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

175 """), 

176 ), 

177 ], 

178 ), 

179 ) 

180 

181 api.pluggable_manifest_rule( 

182 OPARSER_PACKAGES, 

183 "clean-after-removal", 

184 ListParsedFormat, 

185 _parse_clean_after_removal, 

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

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

188 source_format=list[Any], 

189 expected_debputy_integration_mode=not_integrations( 

190 INTEGRATION_MODE_DH_DEBPUTY_RRR 

191 ), 

192 register_value=False, 

193 ) 

194 

195 api.pluggable_manifest_rule( 

196 OPARSER_PACKAGES, 

197 MK_INSTALLATION_SEARCH_DIRS, 

198 InstallationSearchDirsParsedFormat, 

199 _parse_installation_search_dirs, 

200 source_format=list[FileSystemExactMatchRule], 

201 expected_debputy_integration_mode=not_integrations( 

202 INTEGRATION_MODE_DH_DEBPUTY_RRR 

203 ), 

204 register_value=False, 

205 ) 

206 

207 api.pluggable_manifest_rule( 

208 rule_type=OPARSER_PACKAGES, 

209 rule_name="built-using", 

210 parsed_format=list[BuiltUsingParsedFormat], 

211 handler=_parse_built_using, 

212 expected_debputy_integration_mode=not_integrations( 

213 "dh-sequence-zz-debputy-rrr" 

214 ), 

215 inline_reference_documentation=reference_documentation( 

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

217 description=textwrap.dedent( 

218 """\ 

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

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

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

222 arbitrary characters. 

223 

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

225 where license of dependency libraries require the 

226 exact source to be retained. Usually these libraries 

227 will be under the license terms like GNU GPL. 

228 

229 packages: 

230 PKG: 

231 $RULE_NAME: 

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

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

234 - sources-for: foo 

235 when: # foo is always installed 

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

237 

238 Either of these conditions prevents the generation: 

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

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

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

242 architecture or build profile restriction that does 

243 not match the current run. 

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

245 This should only happen inside alternatives, see below. 

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

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

248 must be installed for unrelated reasons. 

249 

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

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

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

253 

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

255 separately. This is a compromise between 

256 reproducibility on automatic builders (where the set 

257 of installed package is constant), and least surprise 

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

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

260 solution when both are installed. 

261 

262 Architecture qualifiers and version restrictions in 

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

264 co-installations require a common source and version. 

265 """, 

266 ), 

267 ), 

268 ) 

269 

270 api.pluggable_manifest_rule( 

271 rule_type=OPARSER_PACKAGES, 

272 rule_name="static-built-using", 

273 parsed_format=list[BuiltUsingParsedFormat], 

274 handler=_parse_static_built_using, 

275 expected_debputy_integration_mode=not_integrations( 

276 "dh-sequence-zz-debputy-rrr" 

277 ), 

278 inline_reference_documentation=reference_documentation( 

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

280 description=textwrap.dedent( 

281 """\ 

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

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

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

285 arbitrary characters. 

286 

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

288 where license of dependency libraries do not require the 

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

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

291 

292 packages: 

293 PKG: 

294 $RULE_NAME: 

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

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

297 - sources-for: foo 

298 when: # foo is always installed 

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

300 

301 Either of these conditions prevents the generation: 

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

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

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

305 architecture or build profile restriction that does 

306 not match the current run. 

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

308 This should only happen inside alternatives, see below. 

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

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

311 must be installed for unrelated reasons. 

312 

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

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

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

316 

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

318 separately. This is a compromise between 

319 reproducibility on automatic builders (where the set 

320 of installed package is constant), and least surprise 

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

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

323 solution when both are installed. 

324 

325 Architecture qualifiers and version restrictions in 

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

327 co-installations require a common source and version. 

328 """, 

329 ), 

330 ), 

331 ) 

332 

333 

334class ServiceRuleSourceFormat(TypedDict): 

335 service: str 

336 type_of_service: NotRequired[str] 

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

338 enable_on_install: NotRequired[bool] 

339 start_on_install: NotRequired[bool] 

340 on_upgrade: NotRequired[ServiceUpgradeRule] 

341 service_manager: NotRequired[ 

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

343 ] 

344 service_managers: NotRequired[list[str]] 

345 

346 

347class ServiceRuleParsedFormat(DebputyParsedContent): 

348 service: str 

349 type_of_service: NotRequired[str] 

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

351 enable_on_install: NotRequired[bool] 

352 start_on_install: NotRequired[bool] 

353 on_upgrade: NotRequired[ServiceUpgradeRule] 

354 service_managers: NotRequired[list[str]] 

355 

356 

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

358class ServiceRule: 

359 definition_source: str 

360 service: str 

361 type_of_service: str 

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

363 enable_on_install: bool | None 

364 start_on_install: bool | None 

365 on_upgrade: ServiceUpgradeRule | None 

366 service_managers: frozenset[str] | None 

367 

368 @classmethod 

369 def from_service_rule_parsed_format( 

370 cls, 

371 data: ServiceRuleParsedFormat, 

372 attribute_path: AttributePath, 

373 ) -> "Self": 

374 service_managers = data.get("service_managers") 

375 return cls( 

376 attribute_path.path, 

377 data["service"], 

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

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

380 data.get("enable_on_install"), 

381 data.get("start_on_install"), 

382 data.get("on_upgrade"), 

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

384 ) 

385 

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

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

388 

389 def apply_to_service_definition( 

390 self, 

391 service_definition: ServiceDefinition[DSD], 

392 ) -> ServiceDefinition[DSD]: 

393 assert isinstance(service_definition, ServiceDefinitionImpl) 

394 if not service_definition.is_plugin_provided_definition: 

395 _error( 

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

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

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

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

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

401 ) 

402 changes = { 

403 "definition_source": self.definition_source, 

404 "is_plugin_provided_definition": False, 

405 } 

406 if ( 

407 self.service != service_definition.name 

408 and self.service in service_definition.names 

409 ): 

410 changes["name"] = self.service 

411 if self.enable_on_install is not None: 

412 changes["auto_start_on_install"] = self.enable_on_install 

413 if self.start_on_install is not None: 

414 changes["auto_start_on_install"] = self.start_on_install 

415 if self.on_upgrade is not None: 

416 changes["on_upgrade"] = self.on_upgrade 

417 

418 return service_definition.replace(**changes) 

419 

420 

421class BinaryVersionParsedFormat(DebputyParsedContent): 

422 binary_version: str 

423 

424 

425class BuiltUsingParsedFormat(DebputyParsedContentStandardConditional): 

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

427 

428 sources_for: str 

429 

430 

431class ListParsedFormat(DebputyParsedContent): 

432 elements: list[Any] 

433 

434 

435class ListOfTransformationRulesFormat(DebputyParsedContent): 

436 elements: list[TransformationRule] 

437 

438 

439class ListOfDpkgMaintscriptHelperCommandFormat(DebputyParsedContent): 

440 elements: list[DpkgMaintscriptHelperCommand] 

441 

442 

443class InstallationSearchDirsParsedFormat(DebputyParsedContent): 

444 installation_search_dirs: list[FileSystemExactMatchRule] 

445 

446 

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

448 

449 

450class DpkgGensymbolsOptionsFormat(DebputyParsedContent): 

451 check_level: DpkgGensymbolsCheckLevel 

452 

453 

454@dataclasses.dataclass 

455class DpkgGensymbolsOptions: 

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

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

458 check_level: DpkgGensymbolsCheckLevel | None 

459 

460 @classmethod 

461 def parse( 

462 cls, 

463 _name: str, 

464 parsed_data: DpkgGensymbolsOptionsFormat, 

465 _attribute_path: AttributePath, 

466 _parser_context: ParserContextData, 

467 ) -> typing.Self: 

468 return cls(**parsed_data) 

469 

470 

471def _parse_binary_version( 

472 _name: str, 

473 parsed_data: BinaryVersionParsedFormat, 

474 _attribute_path: AttributePath, 

475 _parser_context: ParserContextData, 

476) -> str: 

477 return parsed_data["binary_version"] 

478 

479 

480def _parse_installation_search_dirs( 

481 _name: str, 

482 parsed_data: InstallationSearchDirsParsedFormat, 

483 _attribute_path: AttributePath, 

484 _parser_context: ParserContextData, 

485) -> list[FileSystemExactMatchRule]: 

486 return parsed_data["installation_search_dirs"] 

487 

488 

489def _process_service_rules( 

490 _name: str, 

491 parsed_data: list[ServiceRuleParsedFormat], 

492 attribute_path: AttributePath, 

493 _parser_context: ParserContextData, 

494) -> list[ServiceRule]: 

495 return [ 

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

497 for i, x in enumerate(parsed_data) 

498 ] 

499 

500 

501def _parse_built_using( 

502 _name: str, 

503 parsed_data: list[BuiltUsingParsedFormat], 

504 attribute_path: AttributePath, 

505 parser_context: ParserContextData, 

506) -> BuiltUsing: 

507 items = _built_using_handler(parsed_data, attribute_path, parser_context) 

508 return BuiltUsing(items) 

509 

510 

511def _parse_static_built_using( 

512 _name: str, 

513 parsed_data: list[BuiltUsingParsedFormat], 

514 attribute_path: AttributePath, 

515 parser_context: ParserContextData, 

516) -> StaticBuiltUsing: 

517 items = _built_using_handler(parsed_data, attribute_path, parser_context) 

518 return StaticBuiltUsing(items) 

519 

520 

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

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

523 

524 

525def _built_using_matches( 

526 regex: re.Pattern, 

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

528 parser_context: ParserContextData, 

529) -> Iterator[MatchedBuiltUsingRelation]: 

530 """Helper for _validate_built_using.""" 

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

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

533 if raw is not None: 

534 for options in PkgRelation.parse_relations(raw): 

535 for idx, relation in enumerate(options): 

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

537 yield MatchedBuiltUsingRelation(not idx, relation) 

538 

539 

540def _validate_built_using( 

541 parsed_data: BuiltUsingParsedFormat, 

542 attribute_path: AttributePath, 

543 parser_context: ParserContextData, 

544) -> BuiltUsingItem: 

545 """Helper for _built_using_handler.""" 

546 raw_glob = parsed_data["sources_for"] 

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

548 raise ManifestParseException( 

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

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

551 ) 

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

553 

554 pkg = parser_context.current_binary_package_state.binary_package 

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

556 if pkg.is_arch_all: 

557 other = "Build-Depends-Indep" 

558 else: 

559 other = "Build-Depends-Arch" 

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

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

562 raise ManifestParseException( 

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

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

565 ) 

566 return BuiltUsingItem( 

567 matched_packages, 

568 parsed_data.get("when"), 

569 attribute_path, 

570 ) 

571 

572 

573def _built_using_handler( 

574 parsed_data: list[BuiltUsingParsedFormat], 

575 attribute_path: AttributePath, 

576 parser_context: ParserContextData, 

577) -> Iterator[BuiltUsingItem]: 

578 """Helper for _parse_built_using and _parse_static_built_using.""" 

579 for idx, pd in enumerate(parsed_data): 

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

581 

582 

583def _unpack_list( 

584 _name: str, 

585 parsed_data: list[Any], 

586 _attribute_path: AttributePath, 

587 _parser_context: ParserContextData, 

588) -> list[Any]: 

589 return parsed_data 

590 

591 

592class CleanAfterRemovalRuleSourceFormat(TypedDict): 

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

594 paths: NotRequired[list[str]] 

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

596 recursive: NotRequired[bool] 

597 ignore_non_empty_dir: NotRequired[bool] 

598 

599 

600class CleanAfterRemovalRule(DebputyParsedContent): 

601 paths: list[str] 

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

603 recursive: NotRequired[bool] 

604 ignore_non_empty_dir: NotRequired[bool] 

605 

606 

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

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

609_CLEAN_AFTER_REMOVAL_RULE_PARSER = ParserGenerator().generate_parser( 

610 CleanAfterRemovalRule, 

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

612 inline_reference_documentation=reference_documentation( 

613 reference_documentation_url=manifest_format_doc( 

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

615 ), 

616 ), 

617) 

618 

619 

620# Order between clean_on_removal and conffile_management is 

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

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

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

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

625# 

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

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

628# what this note is about) 

629def _parse_clean_after_removal( 

630 _name: str, 

631 parsed_data: ListParsedFormat, 

632 attribute_path: AttributePath, 

633 parser_context: ParserContextData, 

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

635 raw_clean_after_removal = parsed_data["elements"] 

636 package_state = parser_context.current_binary_package_state 

637 

638 for no, raw_transformation in enumerate(raw_clean_after_removal): 

639 definition_source = attribute_path[no] 

640 clean_after_removal_rules = _CLEAN_AFTER_REMOVAL_RULE_PARSER.parse_input( 

641 raw_transformation, 

642 definition_source, 

643 parser_context=parser_context, 

644 ) 

645 patterns = clean_after_removal_rules["paths"] 

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

647 definition_source.path_hint = patterns[0] 

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

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

650 ignore_non_empty_dir = ( 

651 clean_after_removal_rules.get("ignore_non_empty_dir") or False 

652 ) 

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

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

655 else: 

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

657 

658 if ignore_non_empty_dir: 

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

660 raise ManifestParseException( 

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

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

663 ) 

664 for pattern in patterns: 

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

666 raise ManifestParseException( 

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

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

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

670 ) 

671 

672 substitution = parser_context.substitution 

673 match_rules = [ 

674 MatchRule.from_path_or_glob( 

675 p, definition_source.path, substitution=substitution 

676 ) 

677 for p in patterns 

678 ] 

679 content_lines = [ 

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

681 ] 

682 for idx, match_rule in enumerate(match_rules): 

683 original_pattern = patterns[idx] 

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

685 raise ManifestParseException( 

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

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

688 ) 

689 is_subdir_match = False 

690 matched_directory: str | None 

691 if isinstance(match_rule, ExactFileSystemPath): 

692 matched_directory = ( 

693 os.path.dirname(match_rule.path) 

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

695 else match_rule.path 

696 ) 

697 is_subdir_match = True 

698 else: 

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

700 

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

702 raise ManifestParseException( 

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

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

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

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

707 ) 

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

709 ".", 

710 "./", 

711 "", 

712 ) 

713 acceptable_directory = False 

714 would_have_allowed_direct_match = False 

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

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

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

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

719 # acceptable path. As an example: 

720 # 

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

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

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

724 if is_subdir_match and ( 

725 matched_directory 

726 in ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF 

727 ): 

728 acceptable_directory = True 

729 break 

730 if ( 

731 matched_directory 

732 in ACCEPTABLE_CLEAN_ON_REMOVAL_FOR_GLOBS_AND_EXACT_MATCHES 

733 ): 

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

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

736 acceptable_directory = True 

737 break 

738 if ( 

739 matched_directory 

740 in ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF 

741 ): 

742 would_have_allowed_direct_match = True 

743 break 

744 matched_directory = os.path.dirname(matched_directory) 

745 is_subdir_match = True 

746 

747 if would_have_allowed_direct_match and not acceptable_directory: 

748 raise ManifestParseException( 

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

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

751 " have been permitted." 

752 ) 

753 elif not acceptable_directory: 

754 raise ManifestParseException( 

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

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

757 ) 

758 

759 try: 

760 shell_escaped_pattern = match_rule.shell_escape_pattern() 

761 except TypeError: 

762 raise ManifestParseException( 

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

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

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

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

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

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

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

770 ) 

771 

772 if ignore_non_empty_dir: 

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

774 elif recurse: 

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

776 elif original_pattern.endswith("/"): 

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

778 else: 

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

780 content_lines.append(cmd) 

781 content_lines.append("fi\n") 

782 

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

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