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
« 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)
17from debian.deb822 import PkgRelation
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
58ACCEPTABLE_CLEAN_ON_REMOVAL_FOR_GLOBS_AND_EXACT_MATCHES = frozenset(
59 [
60 "./var/log",
61 ]
62)
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)
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 )
95 api.pluggable_manifest_rule(
96 OPARSER_PACKAGES,
97 "transformations",
98 list[TransformationRule],
99 _unpack_list,
100 register_value=False,
101 )
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 )
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 )
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 )
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 )
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.
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.
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
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.
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.
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.
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 )
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.
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.
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
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.
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.
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.
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 )
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]]
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]]
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
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 )
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
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
363 return service_definition.replace(**changes)
366class BinaryVersionParsedFormat(DebputyParsedContent):
367 binary_version: str
370class BuiltUsingParsedFormat(DebputyParsedContentStandardConditional):
371 """Also used for static-built-using."""
373 sources_for: str
376class ListParsedFormat(DebputyParsedContent):
377 elements: list[Any]
380class ListOfTransformationRulesFormat(DebputyParsedContent):
381 elements: list[TransformationRule]
384class ListOfDpkgMaintscriptHelperCommandFormat(DebputyParsedContent):
385 elements: list[DpkgMaintscriptHelperCommand]
388class InstallationSearchDirsParsedFormat(DebputyParsedContent):
389 installation_search_dirs: list[FileSystemExactMatchRule]
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"]
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"]
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 ]
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)
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)
442_VALID_BUILT_USING_GLOB = re.compile("[a-z*][a-z0-9.+*-]*")
443_BUILT_USING_GLOB_TO_RE = str.maketrans({".": "[.]", "+": "[+]", "*": ".*"})
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)
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))
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 )
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)
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
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]
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]
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)
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
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" ]'
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 )
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)
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
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 )
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 )
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")
704 snippet = MaintscriptSnippet(definition_source.path, "".join(content_lines))
705 package_state.maintscript_snippets["postrm"].append(snippet)