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
« 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)
18from debian.deb822 import PkgRelation
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
64ACCEPTABLE_CLEAN_ON_REMOVAL_FOR_GLOBS_AND_EXACT_MATCHES = frozenset(
65 [
66 "./var/log",
67 ]
68)
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)
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 )
101 api.pluggable_manifest_rule(
102 OPARSER_PACKAGES,
103 "transformations",
104 list[TransformationRule],
105 _unpack_list,
106 register_value=False,
107 )
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 )
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 )
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.
149 Examples:
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`
164 The levels are defined in [man:dpkg-gensymbols.1] as:
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.
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.
176 The default check-level in `dpkg-gensymbols` is `1`.
178 [man:dpkg-gensymbols.1]: https://manpages.debian.org/dpkg-gensymbols.1
179 """),
180 ),
181 ],
182 ),
183 )
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 )
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 )
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.
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.
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
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.
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.
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.
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 )
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.
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.
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
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.
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.
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.
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 )
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]]
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]]
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
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 )
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
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
422 return service_definition.replace(**changes)
425class BinaryVersionParsedFormat(DebputyParsedContent):
426 binary_version: str
429class BuiltUsingParsedFormat(DebputyParsedContentStandardConditional):
430 """Also used for static-built-using."""
432 sources_for: str
435class ListParsedFormat(DebputyParsedContent):
436 elements: list[Any]
439class ListOfTransformationRulesFormat(DebputyParsedContent):
440 elements: list[TransformationRule]
443class ListOfDpkgMaintscriptHelperCommandFormat(DebputyParsedContent):
444 elements: list[DpkgMaintscriptHelperCommand]
447class InstallationSearchDirsParsedFormat(DebputyParsedContent):
448 installation_search_dirs: list[FileSystemExactMatchRule]
451type DpkgGensymbolsCheckLevel = typing.Literal[0, 1, 2, 3, 4]
454class DpkgGensymbolsOptionsFormat(DebputyParsedContent):
455 check_level: DpkgGensymbolsCheckLevel
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
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)
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"]
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"]
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 ]
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)
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)
525_VALID_BUILT_USING_GLOB = re.compile("[a-z*][a-z0-9.+*-]*")
526_BUILT_USING_GLOB_TO_RE = str.maketrans({".": "[.]", "+": "[+]", "*": ".*"})
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)
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))
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 )
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)
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
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]
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]
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)
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
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" ]'
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 )
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)
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
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 )
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 )
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")
787 snippet = MaintscriptSnippet(
788 definition_source.path,
789 SnippetResolver.snippet_template("".join(content_lines)),
790 )
791 package_state.maintscript_snippets["postrm"].append(snippet)