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
« 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)
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
47ACCEPTABLE_CLEAN_ON_REMOVAL_FOR_GLOBS_AND_EXACT_MATCHES = frozenset(
48 [
49 "./var/log",
50 ]
51)
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)
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:
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}}'
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`.
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 )
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:
121 packages:
122 foo:
123 transformations:
124 - remove: 'usr/share/doc/{{PACKAGE}}/INSTALL.md'
125 - move:
126 source: bar/*
127 target: foo/
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:
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.
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 )
146 api.pluggable_manifest_rule(
147 OPARSER_PACKAGES,
148 "conffile-management",
149 List[DpkgMaintscriptHelperCommand],
150 _unpack_list,
151 )
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:
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.
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:
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 )
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:
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
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:
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.
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.
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`.
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`.
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 )
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:
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
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`).
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 )
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]]
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]]
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]]
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 )
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
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
466 return service_definition.replace(**changes)
469class BinaryVersionParsedFormat(DebputyParsedContent):
470 binary_version: str
473class ListParsedFormat(DebputyParsedContent):
474 elements: List[Any]
477class ListOfTransformationRulesFormat(DebputyParsedContent):
478 elements: List[TransformationRule]
481class ListOfDpkgMaintscriptHelperCommandFormat(DebputyParsedContent):
482 elements: List[DpkgMaintscriptHelperCommand]
485class InstallationSearchDirsParsedFormat(DebputyParsedContent):
486 installation_search_dirs: List[FileSystemExactMatchRule]
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"]
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"]
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 ]
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
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]
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]
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)
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
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" ]'
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 )
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)
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
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 )
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 )
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")
717 snippet = MaintscriptSnippet(definition_source.path, "".join(content_lines))
718 package_state.maintscript_snippets["postrm"].append(snippet)