Coverage for src/debputy/plugin/debputy/binary_package_rules.py: 82%

173 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-07 12:14 +0200

1import dataclasses 

2import os 

3import textwrap 

4from typing import ( 

5 Any, 

6 List, 

7 NotRequired, 

8 Union, 

9 Literal, 

10 TypedDict, 

11 Annotated, 

12 Optional, 

13 FrozenSet, 

14 Self, 

15 cast, 

16) 

17 

18from debputy import DEBPUTY_DOC_ROOT_DIR 

19from debputy.maintscript_snippet import DpkgMaintscriptHelperCommand, MaintscriptSnippet 

20from debputy.manifest_parser.base_types import ( 

21 DebputyParsedContent, 

22 FileSystemExactMatchRule, 

23) 

24from debputy.manifest_parser.declarative_parser import ( 

25 DebputyParseHint, 

26 ParserGenerator, 

27) 

28from debputy.manifest_parser.exceptions import ManifestParseException 

29from debputy.manifest_parser.parser_data import ParserContextData 

30from debputy.manifest_parser.util import AttributePath 

31from debputy.path_matcher import MatchRule, MATCH_ANYTHING, ExactFileSystemPath 

32from debputy.plugin.api import reference_documentation 

33from debputy.plugin.api.impl import ( 

34 DebputyPluginInitializerProvider, 

35 ServiceDefinitionImpl, 

36) 

37from debputy.plugin.api.impl_types import OPARSER_PACKAGES 

38from debputy.plugin.api.spec import ( 

39 ServiceUpgradeRule, 

40 ServiceDefinition, 

41 DSD, 

42 documented_attr, 

43) 

44from debputy.transformation_rules import TransformationRule 

45from debputy.util import _error 

46 

47ACCEPTABLE_CLEAN_ON_REMOVAL_FOR_GLOBS_AND_EXACT_MATCHES = frozenset( 

48 [ 

49 "./var/log", 

50 ] 

51) 

52 

53 

54ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF = frozenset( 

55 [ 

56 "./etc", 

57 "./run", 

58 "./var/lib", 

59 "./var/cache", 

60 "./var/backups", 

61 "./var/spool", 

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

63 "./usr/lib/modules", 

64 "./lib/modules", 

65 # udev special case 

66 "./lib/udev", 

67 "./usr/lib/udev", 

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

69 "./usr/share/misc", 

70 ] 

71) 

72 

73 

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

75 api.pluggable_manifest_rule( 

76 OPARSER_PACKAGES, 

77 "binary-version", 

78 BinaryVersionParsedFormat, 

79 _parse_binary_version, 

80 source_format=str, 

81 inline_reference_documentation=reference_documentation( 

82 title="Custom binary version (`binary-version`)", 

83 description=textwrap.dedent( 

84 """\ 

85 In the *rare* case that you need a binary package to have a custom version, you can use 

86 the `binary-version:` key to describe the desired package version. An example being: 

87 

88 packages: 

89 foo: 

90 # The foo package needs a different epoch because we took it over from a different 

91 # source package with higher epoch version 

92 binary-version: '1:{{DEB_VERSION_UPSTREAM_REVISION}}' 

93 

94 Use this feature sparingly as it is generally not possible to undo as each version must be 

95 monotonously higher than the previous one. This feature translates into `-v` option for 

96 `dpkg-gencontrol`. 

97 

98 The value for the `binary-version` key is a string that defines the binary version. Generally, 

99 you will want it to contain one of the versioned related substitution variables such as 

100 `{{DEB_VERSION_UPSTREAM_REVISION}}`. Otherwise, you will have to remember to bump the version 

101 manually with each upload as versions cannot be reused and the package would not support binNMUs 

102 either. 

103 """ 

104 ), 

105 reference_documentation_url=f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md#custom-binary-version-binary-version", 

106 ), 

107 ) 

108 

109 api.pluggable_manifest_rule( 

110 OPARSER_PACKAGES, 

111 "transformations", 

112 List[TransformationRule], 

113 _unpack_list, 

114 inline_reference_documentation=reference_documentation( 

115 title="Transformations (`transformations`)", 

116 description=textwrap.dedent( 

117 """\ 

118 You can define a `transformations` under the package definition, which is a list a transformation 

119 rules. An example: 

120 

121 packages: 

122 foo: 

123 transformations: 

124 - remove: 'usr/share/doc/{{PACKAGE}}/INSTALL.md' 

125 - move: 

126 source: bar/* 

127 target: foo/ 

128 

129 

130 Transformations are ordered and are applied in the listed order. A path can be matched by multiple 

131 transformations; how that plays out depends on which transformations are applied and in which order. 

132 A quick summary: 

133 

134 - Transformations that modify the file system layout affect how path matches in later transformations. 

135 As an example, `move` and `remove` transformations affects what globs and path matches expand to in 

136 later transformation rules. 

137 

138 - For other transformations generally the latter transformation overrules the earlier one, when they 

139 overlap or conflict. 

140 """ 

141 ), 

142 reference_documentation_url=f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md#transformations-transformations", 

143 ), 

144 ) 

145 

146 api.pluggable_manifest_rule( 

147 OPARSER_PACKAGES, 

148 "conffile-management", 

149 List[DpkgMaintscriptHelperCommand], 

150 _unpack_list, 

151 ) 

152 

153 api.pluggable_manifest_rule( 

154 OPARSER_PACKAGES, 

155 "services", 

156 List[ServiceRuleParsedFormat], 

157 _process_service_rules, 

158 source_format=List[ServiceRuleSourceFormat], 

159 inline_reference_documentation=reference_documentation( 

160 title="Define how services in the package will be handled (`services`)", 

161 description=textwrap.dedent( 

162 """\ 

163 If you have non-standard requirements for certain services in the package, you can define those via 

164 the `services` attribute. The `services` attribute is a list of service rules. Example: 

165 

166 packages: 

167 foo: 

168 services: 

169 - service: "foo" 

170 enable-on-install: false 

171 - service: "bar" 

172 on-upgrade: stop-then-start 

173 """ 

174 ), 

175 attributes=[ 

176 documented_attr( 

177 "service", 

178 textwrap.dedent( 

179 f"""\ 

180 Name of the service to match. The name is usually the basename of the service file. 

181 However, aliases can also be used for relevant system managers. When aliases **and** 

182 multiple service managers are involved, then the rule will apply to all matches. 

183 For details on aliases, please see 

184 {DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md#service-managers-and-aliases. 

185 

186 - Note: For systemd, the `.service` suffix can be omitted from name, but other 

187 suffixes such as `.timer` cannot. 

188 """ 

189 ), 

190 ), 

191 documented_attr( 

192 "type_of_service", 

193 textwrap.dedent( 

194 """\ 

195 The type of service this rule applies to. To act on a `systemd` timer, you would 

196 set this to `timer` (etc.). Each service manager defines its own set of types 

197 of services. 

198 """ 

199 ), 

200 ), 

201 documented_attr( 

202 "service_scope", 

203 textwrap.dedent( 

204 """\ 

205 The scope of the service. It must be either `system` and `user`. 

206 - Note: The keyword is defined to support `user`, but `debputy` does not support `user` 

207 services at the moment (the detection logic is missing). 

208 """ 

209 ), 

210 ), 

211 documented_attr( 

212 ["service_manager", "service_managers"], 

213 textwrap.dedent( 

214 """\ 

215 Which service managers this rule is for. When omitted, all service managers with this 

216 service will be affected. This can be used to specify separate rules for the same 

217 service under different service managers. 

218 - When this attribute is explicitly given, then all the listed service managers must 

219 provide at least one service matching the definition. In contract, when it is omitted, 

220 then all service manager integrations are consulted but as long as at least one 

221 service is match from any service manager, the rule is accepted. 

222 """ 

223 ), 

224 ), 

225 documented_attr( 

226 "enable_on_install", 

227 textwrap.dedent( 

228 """\ 

229 Whether to automatically enable the service on installation. Note: This does 

230 **not** affect whether the service will be started nor how restarts during 

231 upgrades will happen. 

232 - If omitted, the plugin detecting the service decides the default. 

233 """ 

234 ), 

235 ), 

236 documented_attr( 

237 "start_on_install", 

238 textwrap.dedent( 

239 """\ 

240 Whether to automatically start the service on installation. Whether it is 

241 enabled or how upgrades are handled have separate attributes. 

242 - If omitted, the plugin detecting the service decides the default. 

243 """ 

244 ), 

245 ), 

246 documented_attr( 

247 "on_upgrade", 

248 textwrap.dedent( 

249 """\ 

250 How `debputy` should handle the service during upgrades. The default depends on the 

251 plugin detecting the service. Valid values are: 

252 

253 - `do-nothing`: During an upgrade, the package should not attempt to stop, reload or 

254 restart the service. 

255 - `reload`: During an upgrade, prefer reloading the service rather than restarting 

256 if possible. Note that the result may become `restart` instead if the service 

257 manager integration determines that `reload` is not supported. 

258 - `restart`: During an upgrade, `restart` the service post upgrade. The service 

259 will be left running during the upgrade process. 

260 - `stop-then-start`: Stop the service before the upgrade, perform the upgrade and 

261 then start the service. 

262 """ 

263 ), 

264 ), 

265 ], 

266 reference_documentation_url=f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md#service-management-services", 

267 ), 

268 ) 

269 

270 api.pluggable_manifest_rule( 

271 OPARSER_PACKAGES, 

272 "clean-after-removal", 

273 ListParsedFormat, 

274 _parse_clean_after_removal, 

275 source_format=List[Any], 

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

277 inline_reference_documentation=reference_documentation( 

278 title="Remove runtime created paths on purge or post removal (`clean-after-removal`)", 

279 description=textwrap.dedent( 

280 """\ 

281 For some packages, it is necessary to clean up some run-time created paths. Typical use cases are 

282 deleting log files, cache files, or persistent state. This can be done via the `clean-after-removal`. 

283 An example being: 

284 

285 packages: 

286 foo: 

287 clean-after-removal: 

288 - /var/log/foo/*.log 

289 - /var/log/foo/*.log.gz 

290 - path: /var/log/foo/ 

291 ignore-non-empty-dir: true 

292 - /etc/non-conffile-configuration.conf 

293 - path: /var/cache/foo 

294 recursive: true 

295 

296 The `clean-after-removal` key accepts a list, where each element is either a mapping, a string or a list 

297 of strings. When an element is a mapping, then the following key/value pairs are applicable: 

298 

299 * `path` or `paths` (required): A path match (`path`) or a list of path matches (`paths`) defining the 

300 path(s) that should be removed after clean. The path match(es) can use globs and manifest variables. 

301 Every path matched will by default be removed via `rm -f` or `rmdir` depending on whether the path 

302 provided ends with a *literal* `/`. Special-rules for matches: 

303 - Glob is interpreted by the shell, so shell (`/bin/sh`) rules apply to globs rather than 

304 `debputy`'s glob rules. As an example, `foo/*` will **not** match `foo/.hidden-file`. 

305 - `debputy` cannot evaluate whether these paths/globs will match the desired paths (or anything at 

306 all). Be sure to test the resulting package. 

307 - When a symlink is matched, it is not followed. 

308 - Directory handling depends on the `recursive` attribute and whether the pattern ends with a literal 

309 "/". 

310 - `debputy` has restrictions on the globs being used to prevent rules that could cause massive damage 

311 to the system. 

312 

313 * `recursive` (optional): When `true`, the removal rule will use `rm -fr` rather than `rm -f` or `rmdir` 

314 meaning any directory matched will be deleted along with all of its contents. 

315 

316 * `ignore-non-empty-dir` (optional): When `true`, each path must be or match a directory (and as a 

317 consequence each path must with a literal `/`). The affected directories will be deleted only if they 

318 are empty. Non-empty directories will be skipped. This option is mutually exclusive with `recursive`. 

319 

320 * `delete-on` (optional, defaults to `purge`): This attribute defines when the removal happens. It can 

321 be set to one of the following values: 

322 - `purge`: The removal happens with the package is being purged. This is the default. At a technical 

323 level, the removal occurs at `postrm purge`. 

324 - `removal`: The removal happens immediately after the package has been removed. At a technical level, 

325 the removal occurs at `postrm remove`. 

326 

327 This feature resembles the concept of `rpm`'s `%ghost` files. 

328 """ 

329 ), 

330 reference_documentation_url=f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md#remove-runtime-created-paths-on-purge-or-post-removal-clean-after-removal", 

331 ), 

332 ) 

333 

334 api.pluggable_manifest_rule( 

335 OPARSER_PACKAGES, 

336 "installation-search-dirs", 

337 InstallationSearchDirsParsedFormat, 

338 _parse_installation_search_dirs, 

339 source_format=List[FileSystemExactMatchRule], 

340 inline_reference_documentation=reference_documentation( 

341 title="Custom installation time search directories (`installation-search-dirs`)", 

342 description=textwrap.dedent( 

343 """\ 

344 For source packages that does multiple build, it can be an advantage to provide a custom list of 

345 installation-time search directories. This can be done via the `installation-search-dirs` key. A common 

346 example is building the source twice with different optimization and feature settings where the second 

347 build is for the `debian-installer` (in the form of a `udeb` package). A sample manifest snippet could 

348 look something like: 

349 

350 installations: 

351 - install: 

352 # Because of the search order (see below), `foo` installs `debian/tmp/usr/bin/tool`, 

353 # while `foo-udeb` installs `debian/tmp-udeb/usr/bin/tool` (assuming both paths are 

354 # available). Note the rule can be split into two with the same effect if that aids 

355 # readability or understanding. 

356 source: usr/bin/tool 

357 into: 

358 - foo 

359 - foo-udeb 

360 packages: 

361 foo-udeb: 

362 installation-search-dirs: 

363 - debian/tmp-udeb 

364 

365 

366 The `installation-search-dirs` key accepts a list, where each element is a path (str) relative from the 

367 source root to the directory that should be used as a search directory (absolute paths are still interpreted 

368 as relative to the source root). This list should contain all search directories that should be applicable 

369 for this package (except the source root itself, which is always appended after the provided list). If the 

370 key is omitted, then `debputy` will provide a default search order (In the `dh` integration, the default 

371 is the directory `debian/tmp`). 

372 

373 If a non-existing or non-directory path is listed, then it will be skipped (info-level note). If the path 

374 exists and is a directory, it will also be checked for "not-installed" paths. 

375 """ 

376 ), 

377 reference_documentation_url=f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md#custom-installation-time-search-directories-installation-search-dirs", 

378 ), 

379 ) 

380 

381 

382class ServiceRuleSourceFormat(TypedDict): 

383 service: str 

384 type_of_service: NotRequired[str] 

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

386 enable_on_install: NotRequired[bool] 

387 start_on_install: NotRequired[bool] 

388 on_upgrade: NotRequired[ServiceUpgradeRule] 

389 service_manager: NotRequired[ 

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

391 ] 

392 service_managers: NotRequired[List[str]] 

393 

394 

395class ServiceRuleParsedFormat(DebputyParsedContent): 

396 service: str 

397 type_of_service: NotRequired[str] 

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

399 enable_on_install: NotRequired[bool] 

400 start_on_install: NotRequired[bool] 

401 on_upgrade: NotRequired[ServiceUpgradeRule] 

402 service_managers: NotRequired[List[str]] 

403 

404 

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

406class ServiceRule: 

407 definition_source: str 

408 service: str 

409 type_of_service: str 

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

411 enable_on_install: Optional[bool] 

412 start_on_install: Optional[bool] 

413 on_upgrade: Optional[ServiceUpgradeRule] 

414 service_managers: Optional[FrozenSet[str]] 

415 

416 @classmethod 

417 def from_service_rule_parsed_format( 

418 cls, 

419 data: ServiceRuleParsedFormat, 

420 attribute_path: AttributePath, 

421 ) -> "Self": 

422 service_managers = data.get("service_managers") 

423 return cls( 

424 attribute_path.path, 

425 data["service"], 

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

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

428 data.get("enable_on_install"), 

429 data.get("start_on_install"), 

430 data.get("on_upgrade"), 

431 frozenset(service_managers) if service_managers else service_managers, 

432 ) 

433 

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

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

436 

437 def apply_to_service_definition( 

438 self, 

439 service_definition: ServiceDefinition[DSD], 

440 ) -> ServiceDefinition[DSD]: 

441 assert isinstance(service_definition, ServiceDefinitionImpl) 

442 if not service_definition.is_plugin_provided_definition: 

443 _error( 

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

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

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

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

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

449 ) 

450 changes = { 

451 "definition_source": self.definition_source, 

452 "is_plugin_provided_definition": False, 

453 } 

454 if ( 

455 self.service != service_definition.name 

456 and self.service in service_definition.names 

457 ): 

458 changes["name"] = self.service 

459 if self.enable_on_install is not None: 

460 changes["auto_start_on_install"] = self.enable_on_install 

461 if self.start_on_install is not None: 

462 changes["auto_start_on_install"] = self.start_on_install 

463 if self.on_upgrade is not None: 

464 changes["on_upgrade"] = self.on_upgrade 

465 

466 return service_definition.replace(**changes) 

467 

468 

469class BinaryVersionParsedFormat(DebputyParsedContent): 

470 binary_version: str 

471 

472 

473class ListParsedFormat(DebputyParsedContent): 

474 elements: List[Any] 

475 

476 

477class ListOfTransformationRulesFormat(DebputyParsedContent): 

478 elements: List[TransformationRule] 

479 

480 

481class ListOfDpkgMaintscriptHelperCommandFormat(DebputyParsedContent): 

482 elements: List[DpkgMaintscriptHelperCommand] 

483 

484 

485class InstallationSearchDirsParsedFormat(DebputyParsedContent): 

486 installation_search_dirs: List[FileSystemExactMatchRule] 

487 

488 

489def _parse_binary_version( 

490 _name: str, 

491 parsed_data: BinaryVersionParsedFormat, 

492 _attribute_path: AttributePath, 

493 _parser_context: ParserContextData, 

494) -> str: 

495 return parsed_data["binary_version"] 

496 

497 

498def _parse_installation_search_dirs( 

499 _name: str, 

500 parsed_data: InstallationSearchDirsParsedFormat, 

501 _attribute_path: AttributePath, 

502 _parser_context: ParserContextData, 

503) -> List[FileSystemExactMatchRule]: 

504 return parsed_data["installation_search_dirs"] 

505 

506 

507def _process_service_rules( 

508 _name: str, 

509 parsed_data: List[ServiceRuleParsedFormat], 

510 attribute_path: AttributePath, 

511 _parser_context: ParserContextData, 

512) -> List[ServiceRule]: 

513 return [ 

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

515 for i, x in enumerate(parsed_data) 

516 ] 

517 

518 

519def _unpack_list( 

520 _name: str, 

521 parsed_data: List[Any], 

522 _attribute_path: AttributePath, 

523 _parser_context: ParserContextData, 

524) -> List[Any]: 

525 return parsed_data 

526 

527 

528class CleanAfterRemovalRuleSourceFormat(TypedDict): 

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

530 paths: NotRequired[List[str]] 

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

532 recursive: NotRequired[bool] 

533 ignore_non_empty_dir: NotRequired[bool] 

534 

535 

536class CleanAfterRemovalRule(DebputyParsedContent): 

537 paths: List[str] 

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

539 recursive: NotRequired[bool] 

540 ignore_non_empty_dir: NotRequired[bool] 

541 

542 

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

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

545_CLEAN_AFTER_REMOVAL_RULE_PARSER = ParserGenerator().generate_parser( 

546 CleanAfterRemovalRule, 

547 source_content=Union[CleanAfterRemovalRuleSourceFormat, str, List[str]], 

548 inline_reference_documentation=reference_documentation( 

549 reference_documentation_url=f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md#remove-runtime-created-paths-on-purge-or-post-removal-clean-after-removal", 

550 ), 

551) 

552 

553 

554# Order between clean_on_removal and conffile_management is 

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

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

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

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

559# 

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

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

562# what this note is about) 

563def _parse_clean_after_removal( 

564 _name: str, 

565 parsed_data: ListParsedFormat, 

566 attribute_path: AttributePath, 

567 parser_context: ParserContextData, 

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

569 raw_clean_after_removal = parsed_data["elements"] 

570 package_state = parser_context.current_binary_package_state 

571 

572 for no, raw_transformation in enumerate(raw_clean_after_removal): 

573 definition_source = attribute_path[no] 

574 clean_after_removal_rules = _CLEAN_AFTER_REMOVAL_RULE_PARSER.parse_input( 

575 raw_transformation, 

576 definition_source, 

577 parser_context=parser_context, 

578 ) 

579 patterns = clean_after_removal_rules["paths"] 

580 if patterns: 580 ↛ 582line 580 didn't jump to line 582, because the condition on line 580 was never false

581 definition_source.path_hint = patterns[0] 

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

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

584 ignore_non_empty_dir = ( 

585 clean_after_removal_rules.get("ignore_non_empty_dir") or False 

586 ) 

587 if delete_on == "purge": 587 ↛ 590line 587 didn't jump to line 590, because the condition on line 587 was never false

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

589 else: 

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

591 

592 if ignore_non_empty_dir: 

593 if recurse: 593 ↛ 594line 593 didn't jump to line 594, because the condition on line 593 was never true

594 raise ManifestParseException( 

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

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

597 ) 

598 for pattern in patterns: 

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

600 raise ManifestParseException( 

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

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

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

604 ) 

605 

606 substitution = parser_context.substitution 

607 match_rules = [ 

608 MatchRule.from_path_or_glob( 

609 p, definition_source.path, substitution=substitution 

610 ) 

611 for p in patterns 

612 ] 

613 content_lines = [ 

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

615 ] 

616 for idx, match_rule in enumerate(match_rules): 

617 original_pattern = patterns[idx] 

618 if match_rule is MATCH_ANYTHING: 618 ↛ 619line 618 didn't jump to line 619, because the condition on line 618 was never true

619 raise ManifestParseException( 

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

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

622 ) 

623 is_subdir_match = False 

624 matched_directory: Optional[str] 

625 if isinstance(match_rule, ExactFileSystemPath): 

626 matched_directory = ( 

627 os.path.dirname(match_rule.path) 

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

629 else match_rule.path 

630 ) 

631 is_subdir_match = True 

632 else: 

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

634 

635 if matched_directory is None: 635 ↛ 636line 635 didn't jump to line 636, because the condition on line 635 was never true

636 raise ManifestParseException( 

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

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

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

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

641 ) 

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

643 ".", 

644 "./", 

645 "", 

646 ) 

647 acceptable_directory = False 

648 would_have_allowed_direct_match = False 

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

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

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

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

653 # acceptable path. As an example: 

654 # 

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

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

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

658 if is_subdir_match and ( 

659 matched_directory 

660 in ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF 

661 ): 

662 acceptable_directory = True 

663 break 

664 if ( 

665 matched_directory 

666 in ACCEPTABLE_CLEAN_ON_REMOVAL_FOR_GLOBS_AND_EXACT_MATCHES 

667 ): 

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

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

670 acceptable_directory = True 

671 break 

672 if ( 

673 matched_directory 

674 in ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF 

675 ): 

676 would_have_allowed_direct_match = True 

677 break 

678 matched_directory = os.path.dirname(matched_directory) 

679 is_subdir_match = True 

680 

681 if would_have_allowed_direct_match and not acceptable_directory: 

682 raise ManifestParseException( 

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

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

685 " have been permitted." 

686 ) 

687 elif not acceptable_directory: 

688 raise ManifestParseException( 

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

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

691 ) 

692 

693 try: 

694 shell_escaped_pattern = match_rule.shell_escape_pattern() 

695 except TypeError: 

696 raise ManifestParseException( 

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

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

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

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

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

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

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

704 ) 

705 

706 if ignore_non_empty_dir: 

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

708 elif recurse: 

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

710 elif original_pattern.endswith("/"): 

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

712 else: 

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

714 content_lines.append(cmd) 

715 content_lines.append("fi\n") 

716 

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

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