Coverage for src/debputy/plugin/api/spec.py: 88%
376 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-02-28 21:56 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-02-28 21:56 +0000
1import contextlib
2import dataclasses
3import io
4import tempfile
5import textwrap
6from typing import (
7 Literal,
8 overload,
9 TypeVar,
10 Any,
11 TYPE_CHECKING,
12 TextIO,
13 Generic,
14 ContextManager,
15 get_args,
16 final,
17 cast,
18)
19from collections.abc import Iterable, Callable, Iterator, Sequence, Container
21from debian.debian_support import DpkgArchTable
22from debian.substvars import Substvars
24from debputy import util
25from debputy._deb_options_profiles import DebBuildOptionsAndProfiles
26from debputy.exceptions import (
27 TestPathWithNonExistentFSPathError,
28 PureVirtualPathError,
29 PluginInitializationError,
30)
31from debputy.interpreter import Interpreter, extract_shebang_interpreter_from_file
32from debputy.manifest_conditions import ConditionContext
33from debputy.manifest_parser.tagging_types import DebputyDispatchableType
34from debputy.manifest_parser.util import parse_symbolic_mode
35from debputy.packages import BinaryPackage, SourcePackage
36from debputy.types import S
37from debputy.util import PackageTypeSelector
39if TYPE_CHECKING:
40 from debputy.manifest_parser.base_types import (
41 StaticFileSystemOwner,
42 StaticFileSystemGroup,
43 )
44 from debputy.plugin.api.doc_parsing import ParserRefDocumentation
45 from debputy.plugin.api.impl_types import DIPHandler
46 from debputy.plugins.debputy.to_be_api_types import BuildSystemRule
47 from debputy.plugin.api.impl import DebputyPluginInitializerProvider
50DP = TypeVar("DP", bound=DebputyDispatchableType)
53PluginInitializationEntryPoint = Callable[["DebputyPluginInitializer"], None]
54MetadataAutoDetector = Callable[
55 ["VirtualPath", "BinaryCtrlAccessor", "PackageProcessingContext"], None
56]
57PackageProcessor = Callable[["VirtualPath", None, "PackageProcessingContext"], None]
58DpkgTriggerType = Literal[
59 "activate",
60 "activate-await",
61 "activate-noawait",
62 "interest",
63 "interest-await",
64 "interest-noawait",
65]
66Maintscript = Literal["postinst", "preinst", "prerm", "postrm"]
68ServiceUpgradeRule = Literal[
69 "do-nothing",
70 "reload",
71 "restart",
72 "stop-then-start",
73]
75DSD = TypeVar("DSD")
76ServiceDetector = Callable[
77 ["VirtualPath", "ServiceRegistry[DSD]", "PackageProcessingContext"],
78 None,
79]
80ServiceIntegrator = Callable[
81 [
82 Sequence["ServiceDefinition[DSD]"],
83 "BinaryCtrlAccessor",
84 "PackageProcessingContext",
85 ],
86 None,
87]
89PMT = TypeVar("PMT")
90DebputyIntegrationMode = Literal[
91 "full",
92 "dh-sequence-zz-debputy",
93 "dh-sequence-zz-debputy-rrr",
94]
96INTEGRATION_MODE_FULL: DebputyIntegrationMode = "full"
97INTEGRATION_MODE_DH_DEBPUTY_RRR: DebputyIntegrationMode = "dh-sequence-zz-debputy-rrr"
98INTEGRATION_MODE_DH_DEBPUTY: DebputyIntegrationMode = "dh-sequence-zz-debputy"
99ALL_DEBPUTY_INTEGRATION_MODES: frozenset[DebputyIntegrationMode] = frozenset(
100 get_args(DebputyIntegrationMode)
101)
103_DEBPUTY_DISPATCH_METADATA_ATTR_NAME = "_debputy_dispatch_metadata"
106def only_integrations(
107 *integrations: DebputyIntegrationMode,
108) -> Container[DebputyIntegrationMode]:
109 return frozenset(integrations)
112def not_integrations(
113 *integrations: DebputyIntegrationMode,
114) -> Container[DebputyIntegrationMode]:
115 return ALL_DEBPUTY_INTEGRATION_MODES - frozenset(integrations)
118@dataclasses.dataclass(slots=True, frozen=True)
119class PackagerProvidedFileReferenceDocumentation:
120 description: str | None = None
121 format_documentation_uris: Sequence[str] = tuple()
123 def replace(self, **changes: Any) -> "PackagerProvidedFileReferenceDocumentation":
124 return dataclasses.replace(self, **changes)
127def packager_provided_file_reference_documentation(
128 *,
129 description: str | None = None,
130 format_documentation_uris: Sequence[str] | None = tuple(),
131) -> PackagerProvidedFileReferenceDocumentation:
132 """Provide documentation for a given packager provided file.
134 :param description: Textual description presented to the user.
135 :param format_documentation_uris: A sequence of URIs to documentation that describes
136 the format of the file. Most relevant first.
137 :return:
138 """
139 uris = tuple(format_documentation_uris) if format_documentation_uris else tuple()
140 return PackagerProvidedFileReferenceDocumentation(
141 description=description,
142 format_documentation_uris=uris,
143 )
146class PathMetadataReference(Generic[PMT]):
147 """An accessor to plugin provided metadata
149 This is a *short-lived* reference to a piece of metadata. It should *not* be stored beyond
150 the boundaries of the current plugin execution context as it can be become invalid (as an
151 example, if the path associated with this path is removed, then this reference become invalid)
152 """
154 @property
155 def is_present(self) -> bool:
156 """Determine whether the value has been set
158 If the current plugin cannot access the value, then this method unconditionally returns
159 `False` regardless of whether the value is there.
161 :return: `True` if the value has been set to a not None value (and not been deleted).
162 Otherwise, this property is `False`.
163 """
164 raise NotImplementedError
166 @property
167 def can_read(self) -> bool:
168 """Test whether it is possible to read the metadata
170 Note: That the metadata being readable does *not* imply that the metadata is present.
172 :return: True if it is possible to read the metadata. This is always True for the
173 owning plugin.
174 """
175 raise NotImplementedError
177 @property
178 def can_write(self) -> bool:
179 """Test whether it is possible to update the metadata
181 :return: True if it is possible to update the metadata.
182 """
183 raise NotImplementedError
185 @property
186 def value(self) -> PMT | None:
187 """Fetch the currently stored value if present.
189 :return: The value previously stored if any. Returns `None` if the value was never
190 stored, explicitly set to `None` or was deleted.
191 """
192 raise NotImplementedError
194 @value.setter
195 def value(self, value: PMT | None) -> None:
196 """Replace any current value with the provided value
198 This operation is only possible if the path is writable *and* the caller is from
199 the owning plugin OR the owning plugin made the reference read-write.
200 """
201 raise NotImplementedError
203 @value.deleter
204 def value(self) -> None:
205 """Delete any current value.
207 This has the same effect as setting the value to `None`. It has the same restrictions
208 as the value setter.
209 """
210 self.value = None
213@dataclasses.dataclass(slots=True)
214class PathDef:
215 path_name: str
216 mode: int | None = None
217 mtime: int | None = None
218 has_fs_path: bool | None = None
219 fs_path: str | None = None
220 link_target: str | None = None
221 content: str | None = None
222 materialized_content: str | None = None
225@dataclasses.dataclass(slots=True, frozen=True)
226class DispatchablePluggableManifestRuleMetadata(Generic[DP]):
227 """NOT PUBLIC API (used internally by part of the public API)"""
229 manifest_keywords: Sequence[str]
230 dispatched_type: type[DP]
231 unwrapped_constructor: "DIPHandler"
232 expected_debputy_integration_mode: Container[DebputyIntegrationMode] | None = None
233 online_reference_documentation: "ParserDocumentation | None" = None
234 apply_standard_attribute_documentation: bool = False
235 source_format: Any | None = None
238@dataclasses.dataclass(slots=True, frozen=True)
239class BuildSystemManifestRuleMetadata(DispatchablePluggableManifestRuleMetadata):
240 build_system_impl: type["BuildSystemRule"] | None = None
241 auto_detection_shadow_build_systems: frozenset[str] = frozenset()
244def virtual_path_def(
245 path_name: str,
246 /,
247 mode: int | None = None,
248 mtime: int | None = None,
249 fs_path: str | None = None,
250 link_target: str | None = None,
251 content: str | None = None,
252 materialized_content: str | None = None,
253) -> PathDef:
254 """Define a virtual path for use with examples or, in tests, `build_virtual_file_system`
256 :param path_name: The full path. Must start with "./". If it ends with "/", the path will be interpreted
257 as a directory (the `is_dir` attribute will be True). Otherwise, it will be a symlink or file depending
258 on whether a `link_target` is provided.
259 :param mode: The mode to use for this path. Defaults to 0644 for files and 0755 for directories. The mode
260 should be None for symlinks.
261 :param mtime: Define the last modified time for this path. If not provided, debputy will provide a default
262 if the mtime attribute is accessed.
263 :param fs_path: Define a file system path for this path. This causes `has_fs_path` to return True and the
264 `fs_path` attribute will return this value. The test is required to make this path available to the extent
265 required. Note that the virtual file system will *not* examine the provided path in any way nor attempt
266 to resolve defaults from the path.
267 :param link_target: A target for the symlink. Providing a not None value for this parameter will make the
268 path a symlink.
269 :param content: The content of the path (if opened). The path must be a file.
270 :param materialized_content: Same as `content` except `debputy` will put the contents into a physical file
271 as needed. Cannot be used with `content` or `fs_path`.
272 :return: An *opaque* object to be passed to `build_virtual_file_system`. While the exact type is provided
273 to aid with typing, the type name and its behavior is not part of the API.
274 """
276 is_dir = path_name.endswith("/")
277 is_symlink = link_target is not None
279 if is_symlink:
280 if mode is not None:
281 raise ValueError(
282 f'Please do not provide mode for symlinks. Triggered by "{path_name}"'
283 )
284 if is_dir:
285 raise ValueError(
286 "Path name looks like a directory, but a symlink target was also provided."
287 f' Please remove the trailing slash OR the symlink_target. Triggered by "{path_name}"'
288 )
290 if content and (is_dir or is_symlink): 290 ↛ 291line 290 didn't jump to line 291 because the condition on line 290 was never true
291 raise ValueError(
292 "Content was defined however, the path appears to be a directory a or a symlink"
293 f' Please remove the content, the trailing slash OR the symlink_target. Triggered by "{path_name}"'
294 )
296 if materialized_content is not None:
297 if content is not None: 297 ↛ 298line 297 didn't jump to line 298 because the condition on line 297 was never true
298 raise ValueError(
299 "The materialized_content keyword is mutually exclusive with the content keyword."
300 f' Triggered by "{path_name}"'
301 )
302 if fs_path is not None: 302 ↛ 303line 302 didn't jump to line 303 because the condition on line 302 was never true
303 raise ValueError(
304 "The materialized_content keyword is mutually exclusive with the fs_path keyword."
305 f' Triggered by "{path_name}"'
306 )
307 return PathDef(
308 path_name,
309 mode=mode,
310 mtime=mtime,
311 has_fs_path=bool(fs_path) or materialized_content is not None,
312 fs_path=fs_path,
313 link_target=link_target,
314 content=content,
315 materialized_content=materialized_content,
316 )
319class PackageProcessingContext:
320 """Context for auto-detectors of metadata and package processors (no instantiation)
322 This object holds some context related data for the metadata detector or/and package
323 processors. It may receive new attributes in the future.
324 """
326 __slots__ = ()
328 @property
329 def source_package(self) -> SourcePackage:
330 """The source package stanza from `debian/control`"""
331 raise NotImplementedError
333 @property
334 def binary_package(self) -> BinaryPackage:
335 """The binary package stanza from `debian/control`"""
336 raise NotImplementedError
338 @property
339 def binary_package_version(self) -> str:
340 """The version of the binary package
342 Note this never includes the binNMU version for arch:all packages, but it may for arch:any.
343 """
344 raise NotImplementedError
346 @property
347 def related_udeb_package(self) -> BinaryPackage | None:
348 """An udeb related to this binary package (if any)"""
349 raise NotImplementedError
351 @property
352 def related_udeb_package_version(self) -> str | None:
353 """The version of the related udeb package (if present)
355 Note this never includes the binNMU version for arch:all packages, but it may for arch:any.
356 """
357 raise NotImplementedError
359 def accessible_package_roots(self) -> Iterable[tuple[BinaryPackage, "VirtualPath"]]:
360 raise NotImplementedError
362 def manifest_configuration[T](
363 self,
364 context_package: SourcePackage | BinaryPackage,
365 value_type: type[T],
366 ) -> T | None:
367 """Request access to configuration from the manifest
369 This method will return the value associated with a pluggable manifest rule assuming
370 said configuration was provided.
373 :param context_package: The context in which the configuration will be. Generally, it will be
374 the binary package for anything under `packages:` and the source package otherwise.
375 :param value_type: The type used during registered (return type of the parser/unpack function)
376 :return: The value from the manifest if present or `None` otherwise
377 """
378 raise NotImplementedError
380 @property
381 def dpkg_arch_query_table(self) -> DpkgArchTable:
382 raise NotImplementedError
384 @property
385 def deb_options_and_profiles(self) -> DebBuildOptionsAndProfiles:
386 raise NotImplementedError
388 @property
389 def source_condition_context(self) -> ConditionContext:
390 raise NotImplementedError
392 def condition_context(
393 self, binary_package: BinaryPackage | None
394 ) -> ConditionContext:
395 raise NotImplementedError
397 @property
398 def binary_condition_context(self) -> ConditionContext:
399 return self.condition_context(self.binary_package)
402class DebputyPluginDefinition:
403 """Plugin definition entity
405 The plugin definition provides a way for the plugin code to register the features it provides via
406 accessors.
407 """
409 __slots__ = ("_generic_initializers",)
411 def __init__(self) -> None:
412 self._generic_initializers: list[
413 Callable[["DebputyPluginInitializerProvider"], None]
414 ] = []
416 @staticmethod
417 def _name2id(provided_id: str | None, name: str) -> str:
418 if provided_id is not None: 418 ↛ 419line 418 didn't jump to line 419 because the condition on line 418 was never true
419 return provided_id
420 if name.endswith("_"): 420 ↛ 421line 420 didn't jump to line 421 because the condition on line 420 was never true
421 name = name[:-1]
422 return name.replace("_", "-")
424 def metadata_or_maintscript_detector(
425 self,
426 func: MetadataAutoDetector | None = None,
427 *,
428 detector_id: str | None = None,
429 package_types: PackageTypeSelector = PackageTypeSelector.DEB,
430 ) -> Callable[[MetadataAutoDetector], MetadataAutoDetector] | MetadataAutoDetector:
431 """Provide a pre-assembly hook that can affect the metadata/maintscript of binary ("deb") packages
433 The provided hook will be run once per binary package to be assembled, and it can see all the content
434 ("data.tar") planned to be included in the deb. The hook may do any *read-only* analysis of this content
435 and provide metadata, alter substvars or inject maintscript snippets. However, the hook must *not*
436 change the content ("data.tar") part of the deb.
438 The hook will be run unconditionally for all binary packages built. When the hook does not apply to all
439 packages, it must provide its own (internal) logic for detecting whether it is relevant and reduce itself
440 to a no-op if it should not apply to the current package.
442 Hooks are run in "some implementation defined order" and should not rely on being run before or after
443 any other hook.
445 The hooks are only applied to packages defined in `debian/control`. Notably, the metadata detector will
446 not apply to auto-generated `-dbgsym` packages (as those are not listed explicitly in `debian/control`).
449 >>> plugin_definition = define_debputy_plugin()
450 >>> @plugin_definition.metadata_or_maintscript_detector
451 ... def gsettings_dependencies(
452 ... fs_root: "VirtualPath",
453 ... ctrl: "BinaryCtrlAccessor",
454 ... context: "PackageProcessingContext",
455 ... ) -> None:
456 ... gsettings_schema_dir = fs_root.lookup("/usr/share/glib-2.0/schemas")
457 ... if gsettings_schema_dir is None:
458 ... return
459 ... for path in gsettings_schema_dir.all_paths():
460 ... if path.is_file and path.name.endswith((".xml", ".override")):
461 ... ctrl.substvars.add_dependency(
462 ... "misc:Depends",
463 ... "dconf-gsettings-backend | gsettings-backend",
464 ... )
465 ... break
467 :param func: The function to be decorated/registered.
468 :param detector_id: A plugin-wide unique ID for this detector. Packagers may use this ID for disabling
469 the detector and accordingly the ID is part of the plugin's API toward the packager.
470 :param package_types: Which kind of packages this metadata detector applies to. The package type is generally
471 defined by `Package-Type` field in the binary package. The default is to only run for regular `deb` packages
472 and ignore `udeb` packages.
473 """
475 def _decorate(f: MetadataAutoDetector) -> MetadataAutoDetector:
477 final_id = self._name2id(detector_id, f.__name__)
479 def _init(api: "DebputyPluginInitializer") -> None:
480 api.metadata_or_maintscript_detector(
481 final_id,
482 f,
483 package_types=package_types,
484 )
486 self._generic_initializers.append(_init)
488 return f
490 if func:
491 return _decorate(func)
492 return _decorate
494 def manifest_variable(
495 self,
496 variable_name: str,
497 value: str,
498 *,
499 variable_reference_documentation: str | None = None,
500 ) -> None:
501 """Provide a variable that can be used in the package manifest
503 >>> plugin_definition = define_debputy_plugin()
504 >>> # Enable users to use "{{path:BASH_COMPLETION_DIR}}/foo" in their manifest.
505 >>> plugin_definition.manifest_variable(
506 ... "path:BASH_COMPLETION_DIR",
507 ... "/usr/share/bash-completion/completions",
508 ... variable_reference_documentation="Directory to install bash completions into",
509 ... )
511 :param variable_name: The variable name. It should be provided without the substution prefix/suffix
512 :param value: The constant value that the variable should resolve to. It is not possible to provide
513 dynamic / context based values at this time.
514 :param variable_reference_documentation: A short snippet of reference documentation that explains
515 the purpose of the variable. This will be shown to users in various contexts such as the hover
516 docs.
517 """
519 def _init(api: "DebputyPluginInitializer") -> None:
520 api.manifest_variable(
521 variable_name,
522 value,
523 variable_reference_documentation=variable_reference_documentation,
524 )
526 self._generic_initializers.append(_init)
528 def packager_provided_file(
529 self,
530 stem: str,
531 installed_path: str,
532 *,
533 default_mode: int = 0o0644,
534 default_priority: int | None = None,
535 allow_name_segment: bool = True,
536 allow_architecture_segment: bool = False,
537 post_formatting_rewrite: Callable[[str], str] | None = None,
538 packageless_is_fallback_for_all_packages: bool = False,
539 reservation_only: bool = False,
540 reference_documentation: None | (
541 PackagerProvidedFileReferenceDocumentation
542 ) = None,
543 ) -> None:
544 """Register a packager provided file (debian/<pkg>.foo)
546 Register a packager provided file that debputy should automatically detect and install for the
547 packager (example `debian/foo.tmpfiles` -> `debian/foo/usr/lib/tmpfiles.d/foo.conf`). A packager
548 provided file typically identified by a package prefix and a "stem" and by convention placed
549 in the `debian/` directory.
551 Where possible, please define these packager provided files via the JSON metadata rather than
552 via Python code.
554 Like debhelper, debputy also supports the `foo.bar.tmpfiles` variant where the file is to be
555 installed into the `foo` package but be named after the `bar` segment rather than the package name.
556 This feature can be controlled via the `allow_name_segment` parameter.
558 :param stem: The "stem" of the file. This would be the `tmpfiles` part of `debian/foo.tmpfiles`.
559 Note that this value must be unique across all registered packager provided files.
560 :param installed_path: A format string describing where the file should be installed. Would be
561 `/usr/lib/tmpfiles.d/{name}.conf` from the example above.
563 The caller should provide a string with one or more of the placeholders listed below (usually `{name}`
564 should be one of them). The format affect the entire path.
566 The following placeholders are supported:
567 * `{name}` - The name in the name segment (defaulting the package name if no name segment is given)
568 * `{priority}` / `{priority:02}` - The priority of the file. Only provided priorities are used (that
569 is, default_priority is not None). The latter variant ensuring that the priority takes at least
570 two characters and the `0` character is left-padded for priorities that takes less than two
571 characters.
572 * `{owning_package}` - The name of the package. Should only be used when `{name}` alone is insufficient.
573 If you do not want the "name" segment in the first place, use `allow_name_segment=False` instead.
575 The path is always interpreted as relative to the binary package root.
577 :param default_mode: The mode the installed file should have by default. Common options are 0o0644 (the default)
578 or 0o0755 (for files that must be executable).
579 :param allow_architecture_segment: If True, the file may have an optional "architecture" segment at the end
580 (`foo.tmpfiles.amd64`), which marks it architecture specific. When False, debputy will detect the
581 "architecture" segment and report the use as an error. Note the architecture segment is only allowed for
582 arch:any packages. If a file targeting an arch:all package uses an architecture specific file it will
583 always result in an error.
584 :param allow_name_segment: If True, the file may have an optional "name" segment after the package name prefix.
585 (`foo.<name-here>.tmpfiles`). When False, debputy will detect the "name" segment and report the use as an
586 error.
588 Note that when this parameter is True, then a package can provide multiple instances with this stem. When
589 False, then at most one file can be provided per package.
590 :param default_priority: Special-case option for packager files that are installed into directories that have
591 "parse ordering" or "priority". These files will generally be installed as something like `20-foo.conf`
592 where the `20-` denotes their "priority". If the plugin is registering such a file type, then it should
593 provide a default priority.
595 The following placeholders are supported:
596 * `{name}` - The name in the name segment (defaulting the package name if no name segment is given)
597 * `{priority}` - The priority of the file. Only provided priorities are used (that is, default_priority
598 is not None)
599 * `{owning_package}` - The name of the package. Should only be used when `{name}` alone is insufficient.
600 If you do not want the "name" segment in the first place, use `allow_name_segment=False` instead.
601 :param post_formatting_rewrite: An optional "name correcting" callback. It receives the formatted name and can
602 do any transformation required. The primary use-case for this is to replace "forbidden" characters. The most
603 common case for debputy itself is to replace "." with "_" for tools that refuse to work with files containing
604 "." (`lambda x: x.replace(".", "_")`). The callback operates on basename of formatted version of the
605 `installed_path` and the callback should return the basename.
606 :param packageless_is_fallback_for_all_packages: If True, the packageless variant (such as, `debian/changelog`)
607 is a fallback for every package.
608 :param reference_documentation: Reference documentation for the packager provided file. Use the
609 packager_provided_file_reference_documentation function to provide the value for this parameter.
610 :param reservation_only: When True, tell debputy that the plugin reserves this packager provided file, but that
611 debputy should not actually install it automatically. This is useful in the cases, where the plugin
612 needs to process the file before installing it. The file will be marked as provided by this plugin. This
613 enables introspection and detects conflicts if other plugins attempts to claim the file.
614 """
616 def _init(api: "DebputyPluginInitializer") -> None:
617 api.packager_provided_file(
618 stem,
619 installed_path,
620 default_mode=default_mode,
621 default_priority=default_priority,
622 allow_name_segment=allow_name_segment,
623 allow_architecture_segment=allow_architecture_segment,
624 post_formatting_rewrite=post_formatting_rewrite,
625 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages,
626 reservation_only=reservation_only,
627 reference_documentation=reference_documentation,
628 )
630 self._generic_initializers.append(_init)
632 def initialize(self, api: "DebputyPluginInitializer") -> None:
633 """Initialize the plugin from this definition
635 Most plugins will not need this function as the plugin loading will call this function
636 when relevant. However, a plugin can call this manually from a function-based initializer.
637 This is mostly useful if the function-based initializer need to set up a few features
638 that the `DebputyPluginDefinition` cannot do and the mix/match approach is not too
639 distracting for plugin maintenance.
641 :param api: The plugin initializer provided by the `debputy` plugin system
642 """
644 api_impl = cast(
645 "DebputyPluginInitializerProvider",
646 api,
647 )
648 initializers = self._generic_initializers
649 if not initializers:
650 plugin_name = api_impl.plugin_metadata.plugin_name
651 raise PluginInitializationError(
652 f"Initialization of {plugin_name}: The plugin definition was never used to register any features."
653 " If you want to conditionally register features, please use an initializer functon instead."
654 )
656 for initializer in initializers:
657 initializer(api_impl)
660def define_debputy_plugin() -> DebputyPluginDefinition:
661 return DebputyPluginDefinition()
664class DebputyPluginInitializer:
665 __slots__ = ()
667 def packager_provided_file(
668 self,
669 stem: str,
670 installed_path: str,
671 *,
672 default_mode: int = 0o0644,
673 default_priority: int | None = None,
674 allow_name_segment: bool = True,
675 allow_architecture_segment: bool = False,
676 post_formatting_rewrite: Callable[[str], str] | None = None,
677 packageless_is_fallback_for_all_packages: bool = False,
678 reservation_only: bool = False,
679 reference_documentation: None | (
680 PackagerProvidedFileReferenceDocumentation
681 ) = None,
682 ) -> None:
683 """Register a packager provided file (debian/<pkg>.foo)
685 Register a packager provided file that debputy should automatically detect and install for the
686 packager (example `debian/foo.tmpfiles` -> `debian/foo/usr/lib/tmpfiles.d/foo.conf`). A packager
687 provided file typically identified by a package prefix and a "stem" and by convention placed
688 in the `debian/` directory.
690 Like debhelper, debputy also supports the `foo.bar.tmpfiles` variant where the file is to be
691 installed into the `foo` package but be named after the `bar` segment rather than the package name.
692 This feature can be controlled via the `allow_name_segment` parameter.
694 :param stem: The "stem" of the file. This would be the `tmpfiles` part of `debian/foo.tmpfiles`.
695 Note that this value must be unique across all registered packager provided files.
696 :param installed_path: A format string describing where the file should be installed. Would be
697 `/usr/lib/tmpfiles.d/{name}.conf` from the example above.
699 The caller should provide a string with one or more of the placeholders listed below (usually `{name}`
700 should be one of them). The format affect the entire path.
702 The following placeholders are supported:
703 * `{name}` - The name in the name segment (defaulting the package name if no name segment is given)
704 * `{priority}` / `{priority:02}` - The priority of the file. Only provided priorities are used (that
705 is, default_priority is not None). The latter variant ensuring that the priority takes at least
706 two characters and the `0` character is left-padded for priorities that takes less than two
707 characters.
708 * `{owning_package}` - The name of the package. Should only be used when `{name}` alone is insufficient.
709 If you do not want the "name" segment in the first place, use `allow_name_segment=False` instead.
711 The path is always interpreted as relative to the binary package root.
713 :param default_mode: The mode the installed file should have by default. Common options are 0o0644 (the default)
714 or 0o0755 (for files that must be executable).
715 :param allow_architecture_segment: If True, the file may have an optional "architecture" segment at the end
716 (`foo.tmpfiles.amd64`), which marks it architecture specific. When False, debputy will detect the
717 "architecture" segment and report the use as an error. Note the architecture segment is only allowed for
718 arch:any packages. If a file targeting an arch:all package uses an architecture specific file it will
719 always result in an error.
720 :param allow_name_segment: If True, the file may have an optional "name" segment after the package name prefix.
721 (`foo.<name-here>.tmpfiles`). When False, debputy will detect the "name" segment and report the use as an
722 error.
723 :param default_priority: Special-case option for packager files that are installed into directories that have
724 "parse ordering" or "priority". These files will generally be installed as something like `20-foo.conf`
725 where the `20-` denotes their "priority". If the plugin is registering such a file type, then it should
726 provide a default priority.
728 The following placeholders are supported:
729 * `{name}` - The name in the name segment (defaulting the package name if no name segment is given)
730 * `{priority}` - The priority of the file. Only provided priorities are used (that is, default_priority
731 is not None)
732 * `{owning_package}` - The name of the package. Should only be used when `{name}` alone is insufficient.
733 If you do not want the "name" segment in the first place, use `allow_name_segment=False` instead.
734 :param post_formatting_rewrite: An optional "name correcting" callback. It receives the formatted name and can
735 do any transformation required. The primary use-case for this is to replace "forbidden" characters. The most
736 common case for debputy itself is to replace "." with "_" for tools that refuse to work with files containing
737 "." (`lambda x: x.replace(".", "_")`). The callback operates on basename of formatted version of the
738 `installed_path` and the callback should return the basename.
739 :param packageless_is_fallback_for_all_packages: If True, the packageless variant (such as, `debian/changelog`)
740 is a fallback for every package.
741 :param reference_documentation: Reference documentation for the packager provided file. Use the
742 packager_provided_file_reference_documentation function to provide the value for this parameter.
743 :param reservation_only: When True, tell debputy that the plugin reserves this packager provided file, but that
744 debputy should not actually install it automatically. This is useful in the cases, where the plugin
745 needs to process the file before installing it. The file will be marked as provided by this plugin. This
746 enables introspection and detects conflicts if other plugins attempts to claim the file.
747 """
748 raise NotImplementedError
750 def metadata_or_maintscript_detector(
751 self,
752 auto_detector_id: str,
753 auto_detector: MetadataAutoDetector,
754 *,
755 package_types: PackageTypeSelector = PackageTypeSelector.DEB,
756 ) -> None:
757 """Provide a pre-assembly hook that can affect the metadata/maintscript of binary ("deb") packages
759 The provided hook will be run once per binary package to be assembled, and it can see all the content
760 ("data.tar") planned to be included in the deb. The hook may do any *read-only* analysis of this content
761 and provide metadata, alter substvars or inject maintscript snippets. However, the hook must *not*
762 change the content ("data.tar") part of the deb.
764 The hook will be run unconditionally for all binary packages built. When the hook does not apply to all
765 packages, it must provide its own (internal) logic for detecting whether it is relevant and reduce itself
766 to a no-op if it should not apply to the current package.
768 Hooks are run in "some implementation defined order" and should not rely on being run before or after
769 any other hook.
771 The hooks are only applied to packages defined in `debian/control`. Notably, the metadata detector will
772 not apply to auto-generated `-dbgsym` packages (as those are not listed explicitly in `debian/control`).
774 :param auto_detector_id: A plugin-wide unique ID for this detector. Packagers may use this ID for disabling
775 the detector and accordingly the ID is part of the plugin's API toward the packager.
776 :param auto_detector: The code to be called that will be run at the metadata generation state (once for each
777 binary package).
778 :param package_types: Which kind of packages this metadata detector applies to. The package type is generally
779 defined by `Package-Type` field in the binary package. The default is to only run for regular `deb` packages
780 and ignore `udeb` packages.
781 """
782 raise NotImplementedError
784 def manifest_variable(
785 self,
786 variable_name: str,
787 value: str,
788 *,
789 variable_reference_documentation: str | None = None,
790 ) -> None:
791 """Provide a variable that can be used in the package manifest
793 >>> # Enable users to use "{{path:BASH_COMPLETION_DIR}}/foo" in their manifest.
794 >>> api.manifest_variable( # doctest: +SKIP
795 ... "path:BASH_COMPLETION_DIR",
796 ... "/usr/share/bash-completion/completions",
797 ... variable_reference_documentation="Directory to install bash completions into",
798 ... )
800 :param variable_name: The variable name.
801 :param value: The value the variable should resolve to.
802 :param variable_reference_documentation: A short snippet of reference documentation that explains
803 the purpose of the variable.
804 """
805 raise NotImplementedError
808class MaintscriptAccessor:
809 __slots__ = ()
811 def on_configure(
812 self,
813 run_snippet: str,
814 /,
815 indent: bool | None = None,
816 perform_substitution: bool = True,
817 skip_on_rollback: bool = False,
818 ) -> None:
819 """Provide a snippet to be run when the package is about to be "configured"
821 This condition is the most common "post install" condition and covers the two
822 common cases:
823 * On initial install, OR
824 * On upgrade
826 In dpkg maintscript terms, this method roughly corresponds to postinst containing
827 `if [ "$1" = configure ]; then <snippet>; fi`
829 Additionally, the condition will by default also include rollback/abort scenarios such as "above-remove",
830 which is normally what you want but most people forget about.
832 :param run_snippet: The actual shell snippet to be run in the given condition. The snippet must be idempotent.
833 The snippet may contain newlines as necessary, which will make the result more readable. Additionally, the
834 snippet may contain '{{FOO}}' substitutions by default.
835 :param skip_on_rollback: By default, this condition will also cover common rollback scenarios. This
836 is normally what you want (or benign in most cases due to the idempotence requirement for maintscripts).
837 However, you can disable the rollback cases, leaving only "On initial install OR On upgrade".
838 :param indent: If True, the provided snippet will be indented to fit the condition provided by debputy.
839 In most cases, this is safe to do and provides more readable scripts. However, it may cause issues
840 with some special shell syntax (such as "Heredocs"). When False, the snippet will *not* be re-indented.
841 You are recommended to do 4 spaces of indentation when indent is False for readability.
842 :param perform_substitution: When True, `{{FOO}}` will be substituted in the snippet. When False, no
843 substitution is provided.
844 """
845 raise NotImplementedError
847 def on_initial_install(
848 self,
849 run_snippet: str,
850 /,
851 indent: bool | None = None,
852 perform_substitution: bool = True,
853 ) -> None:
854 """Provide a snippet to be run when the package is about to be "configured" for the first time
856 The snippet will only be run on the first time the package is installed (ever or since last purge).
857 Note that "first" does not mean "exactly once" as dpkg does *not* provide such semantics. There are two
858 common cases where this can snippet can be run multiple times for the same system (and why the snippet
859 must still be idempotent):
861 1) The package is installed (1), then purged and then installed again (2). This can partly be mitigated
862 by having an `on_purge` script to do clean up.
864 2) As the package is installed, the `postinst` script terminates prematurely (Disk full, power loss, etc.).
865 The user resolves the problem and runs `dpkg --configure <pkg>`, which in turn restarts the script
866 from the beginning. This is why scripts must be idempotent in general.
868 In dpkg maintscript terms, this method roughly corresponds to postinst containing
869 `if [ "$1" = configure ] && [ -z "$2" ]; then <snippet>; fi`
871 :param run_snippet: The actual shell snippet to be run in the given condition. The snippet must be idempotent.
872 The snippet may contain newlines as necessary, which will make the result more readable. Additionally, the
873 snippet may contain '{{FOO}}' substitutions by default.
874 :param indent: If True, the provided snippet will be indented to fit the condition provided by debputy.
875 In most cases, this is safe to do and provides more readable scripts. However, it may cause issues
876 with some special shell syntax (such as "Heredocs"). When False, the snippet will *not* be re-indented.
877 You are recommended to do 4 spaces of indentation when indent is False for readability.
878 :param perform_substitution: When True, `{{FOO}}` will be substituted in the snippet. When False, no
879 substitution is provided.
880 """
881 raise NotImplementedError
883 def on_upgrade(
884 self,
885 run_snippet: str,
886 /,
887 indent: bool | None = None,
888 perform_substitution: bool = True,
889 ) -> None:
890 """Provide a snippet to be run when the package is about to be "configured" after an upgrade
892 The snippet will only be run on any upgrade (that is, it will be skipped on the initial install).
894 In dpkg maintscript terms, this method roughly corresponds to postinst containing
895 `if [ "$1" = configure ] && [ -n "$2" ]; then <snippet>; fi`
897 :param run_snippet: The actual shell snippet to be run in the given condition. The snippet must be idempotent.
898 The snippet may contain newlines as necessary, which will make the result more readable. Additionally, the
899 snippet may contain '{{FOO}}' substitutions by default.
900 :param indent: If True, the provided snippet will be indented to fit the condition provided by debputy.
901 In most cases, this is safe to do and provides more readable scripts. However, it may cause issues
902 with some special shell syntax (such as "Heredocs"). When False, the snippet will *not* be re-indented.
903 You are recommended to do 4 spaces of indentation when indent is False for readability.
904 :param perform_substitution: When True, `{{FOO}}` will be substituted in the snippet. When False, no
905 substitution is provided.
906 """
907 raise NotImplementedError
909 def on_upgrade_from(
910 self,
911 version: str,
912 run_snippet: str,
913 /,
914 indent: bool | None = None,
915 perform_substitution: bool = True,
916 ) -> None:
917 """Provide a snippet to be run when the package is about to be "configured" after an upgrade from a given version
919 The snippet will only be run on any upgrade (that is, it will be skipped on the initial install).
921 In dpkg maintscript terms, this method roughly corresponds to postinst containing
922 `if [ "$1" = configure ] && dpkg --compare-versions le-nl "$2" ; then <snippet>; fi`
924 :param version: The version to upgrade from
925 :param run_snippet: The actual shell snippet to be run in the given condition. The snippet must be idempotent.
926 The snippet may contain newlines as necessary, which will make the result more readable. Additionally, the
927 snippet may contain '{{FOO}}' substitutions by default.
928 :param indent: If True, the provided snippet will be indented to fit the condition provided by debputy.
929 In most cases, this is safe to do and provides more readable scripts. However, it may cause issues
930 with some special shell syntax (such as "Heredocs"). When False, the snippet will *not* be re-indented.
931 You are recommended to do 4 spaces of indentation when indent is False for readability.
932 :param perform_substitution: When True, `{{FOO}}` will be substituted in the snippet. When False, no
933 substitution is provided.
934 """
935 raise NotImplementedError
937 def on_before_removal(
938 self,
939 run_snippet: str,
940 /,
941 indent: bool | None = None,
942 perform_substitution: bool = True,
943 ) -> None:
944 """Provide a snippet to be run when the package is about to be removed
946 The snippet will be run before dpkg removes any files.
948 In dpkg maintscript terms, this method roughly corresponds to prerm containing
949 `if [ "$1" = remove ] ; then <snippet>; fi`
951 :param run_snippet: The actual shell snippet to be run in the given condition. The snippet must be idempotent.
952 The snippet may contain newlines as necessary, which will make the result more readable. Additionally, the
953 snippet may contain '{{FOO}}' substitutions by default.
954 :param indent: If True, the provided snippet will be indented to fit the condition provided by debputy.
955 In most cases, this is safe to do and provides more readable scripts. However, it may cause issues
956 with some special shell syntax (such as "Heredocs"). When False, the snippet will *not* be re-indented.
957 You are recommended to do 4 spaces of indentation when indent is False for readability.
958 :param perform_substitution: When True, `{{FOO}}` will be substituted in the snippet. When False, no
959 substitution is provided.
960 """
961 raise NotImplementedError
963 def on_removed(
964 self,
965 run_snippet: str,
966 /,
967 indent: bool | None = None,
968 perform_substitution: bool = True,
969 ) -> None:
970 """Provide a snippet to be run when the package has been removed
972 The snippet will be run after dpkg removes the package content from the file system.
974 **WARNING**: The snippet *cannot* rely on dependencies and must rely on `Essential: yes` packages.
976 In dpkg maintscript terms, this method roughly corresponds to postrm containing
977 `if [ "$1" = remove ] ; then <snippet>; fi`
979 :param run_snippet: The actual shell snippet to be run in the given condition. The snippet must be idempotent.
980 The snippet may contain newlines as necessary, which will make the result more readable. Additionally, the
981 snippet may contain '{{FOO}}' substitutions by default.
982 :param indent: If True, the provided snippet will be indented to fit the condition provided by debputy.
983 In most cases, this is safe to do and provides more readable scripts. However, it may cause issues
984 with some special shell syntax (such as "Heredocs"). When False, the snippet will *not* be re-indented.
985 You are recommended to do 4 spaces of indentation when indent is False for readability.
986 :param perform_substitution: When True, `{{FOO}}` will be substituted in the snippet. When False, no
987 substitution is provided.
988 """
989 raise NotImplementedError
991 def on_purge(
992 self,
993 run_snippet: str,
994 /,
995 indent: bool | None = None,
996 perform_substitution: bool = True,
997 ) -> None:
998 """Provide a snippet to be run when the package is being purged.
1000 The snippet will when the package is purged from the system.
1002 **WARNING**: The snippet *cannot* rely on dependencies and must rely on `Essential: yes` packages.
1004 In dpkg maintscript terms, this method roughly corresponds to postrm containing
1005 `if [ "$1" = purge ] ; then <snippet>; fi`
1007 :param run_snippet: The actual shell snippet to be run in the given condition. The snippet must be idempotent.
1008 The snippet may contain newlines as necessary, which will make the result more readable. Additionally, the
1009 snippet may contain '{{FOO}}' substitutions by default.
1010 :param indent: If True, the provided snippet will be indented to fit the condition provided by debputy.
1011 In most cases, this is safe to do and provides more readable scripts. However, it may cause issues
1012 with some special shell syntax (such as "Heredocs"). When False, the snippet will *not* be re-indented.
1013 You are recommended to do 4 spaces of indentation when indent is False for readability.
1014 :param perform_substitution: When True, `{{FOO}}` will be substituted in the snippet. When False, no
1015 substitution is provided.
1016 """
1017 raise NotImplementedError
1019 def unconditionally_in_script(
1020 self,
1021 maintscript: Maintscript,
1022 run_snippet: str,
1023 /,
1024 perform_substitution: bool = True,
1025 ) -> None:
1026 """Provide a snippet to be run in a given script
1028 Run a given snippet unconditionally from a given script. The snippet must contain its own conditional
1029 for when it should be run.
1031 :param maintscript: The maintscript to insert the snippet into.
1032 :param run_snippet: The actual shell snippet to be run. The snippet will be run unconditionally and should
1033 contain its own conditions as necessary. The snippet must be idempotent. The snippet may contain newlines
1034 as necessary, which will make the result more readable. Additionally, the snippet may contain '{{FOO}}'
1035 substitutions by default.
1036 :param perform_substitution: When True, `{{FOO}}` will be substituted in the snippet. When False, no
1037 substitution is provided.
1038 """
1039 raise NotImplementedError
1041 def escape_shell_words(self, *args: str) -> str:
1042 """Provide sh-shell escape of strings
1044 `assert escape_shell("foo", "fu bar", "baz") == 'foo "fu bar" baz'`
1046 This is useful for ensuring file names and other "input" are considered one parameter even when they
1047 contain spaces or shell meta-characters.
1049 :param args: The string(s) to be escaped.
1050 :return: Each argument escaped so that each argument becomes a single "word" and then all these words are
1051 joined by a single space.
1052 """
1053 return util.escape_shell(*args)
1056class BinaryCtrlAccessor:
1057 __slots__ = ()
1059 def dpkg_trigger(self, trigger_type: DpkgTriggerType, trigger_target: str) -> None:
1060 """Register a declarative dpkg level trigger
1062 The provided trigger will be added to the package's metadata (the triggers file of the control.tar).
1064 If the trigger has already been added previously, a second call with the same trigger data will be ignored.
1065 """
1066 raise NotImplementedError
1068 @property
1069 def maintscript(self) -> MaintscriptAccessor:
1070 """Attribute for manipulating maintscripts"""
1071 raise NotImplementedError
1073 @property
1074 def substvars(self) -> "FlushableSubstvars":
1075 """Attribute for manipulating dpkg substvars (deb-substvars)"""
1076 raise NotImplementedError
1079class VirtualPath:
1080 __slots__ = ()
1082 @property
1083 def name(self) -> str:
1084 """Basename of the path a.k.a. last segment of the path
1086 In a path "usr/share/doc/pkg/changelog.gz" the basename is "changelog.gz".
1088 For a directory, the basename *never* ends with a `/`.
1089 """
1090 raise NotImplementedError
1092 def iterdir(self) -> Iterable["VirtualPath"]:
1093 """Returns an iterable that iterates over all children of this path
1095 For directories, this returns an iterable of all children. For non-directories,
1096 the iterable is always empty.
1097 """
1098 raise NotImplementedError
1100 def lookup(self, path: str) -> "VirtualPath | None":
1101 """Perform a path lookup relative to this path
1103 As an example `doc_dir = fs_root.lookup('./usr/share/doc')`
1105 If the provided path starts with `/`, then the lookup is performed relative to the
1106 file system root. That is, you can assume the following to always be True:
1108 `fs_root.lookup("usr") == any_path_beneath_fs_root.lookup('/usr')`
1110 Note: This method requires the path to be attached (see `is_detached`) regardless of
1111 whether the lookup is relative or absolute.
1113 If the path traverse a symlink, the symlink will be resolved.
1115 :param path: The path to look. Can contain "." and ".." segments. If starting with `/`,
1116 look up is performed relative to the file system root, otherwise the lookup is relative
1117 to this path.
1118 :return: The path object for the desired path if it can be found. Otherwise, None.
1119 """
1120 raise NotImplementedError
1122 def all_paths(self) -> Iterable["VirtualPath"]:
1123 """Iterate over this path and all of its descendants (if any)
1125 If used on the root path, then every path in the package is returned.
1127 The iterable is ordered, so using the order in output will be produce
1128 bit-for-bit reproducible output. Additionally, a directory will always
1129 be seen before its descendants. Otherwise, the order is implementation
1130 defined.
1132 The iteration is lazy and as a side effect do account for some obvious
1133 mutation. Like if the current path is removed, then none of its children
1134 will be returned (provided mutation happens before the lazy iteration
1135 was required to resolve it). Likewise, mutation of the directory will
1136 also work (again, provided mutation happens before the lazy iteration order).
1138 :return: An ordered iterable of this path followed by its descendants.
1139 """
1140 raise NotImplementedError
1142 # The __getitem__ behaves like __getitem__ from Dict but __iter__ would ideally work like a Sequence.
1143 # However, that does not feel compatible, so lets force people to use .children instead for the Sequence
1144 # behavior to avoid surprises for now.
1145 # (Maybe it is a non-issue, but it is easier to add the API later than to remove it once we have committed
1146 # to using it)
1147 __iter__ = None
1149 def __getitem__(self, key: object) -> "VirtualPath":
1150 """Lookup a (direct) child by name
1152 Ignoring the possible `KeyError`, then the following are the same:
1153 `fs_root["usr"] == fs_root.lookup('usr')`
1155 Note that unlike `.lookup` this can only locate direct children.
1156 """
1157 raise NotImplementedError
1159 def __delitem__(self, key) -> None:
1160 """Remove a child from this node if it exists
1162 If that child is a directory, then the entire tree is removed (like `rm -fr`).
1163 """
1164 raise NotImplementedError
1166 def get(self, key: str) -> "VirtualPath | None":
1167 """Lookup a (direct) child by name
1169 The following are the same:
1170 `fs_root.get("usr") == fs_root.lookup('usr')`
1172 Note that unlike `.lookup` this can only locate direct children.
1173 """
1174 try:
1175 return self[key]
1176 except KeyError:
1177 return None
1179 def __contains__(self, item: object) -> bool:
1180 """Determine if this path includes a given child (either by object or string)
1182 Examples:
1184 if 'foo' in dir: ...
1185 """
1186 if isinstance(item, VirtualPath): 1186 ↛ 1187line 1186 didn't jump to line 1187 because the condition on line 1186 was never true
1187 return item.parent_dir is self
1188 if not isinstance(item, str): 1188 ↛ 1189line 1188 didn't jump to line 1189 because the condition on line 1188 was never true
1189 return False
1190 m = self.get(item)
1191 return m is not None
1193 @property
1194 def path(self) -> str:
1195 """Returns the "full" path for this file system entry
1197 This is the path that debputy uses to refer to this file system entry. It is always
1198 normalized. Use the `absolute` attribute for how the path looks
1199 when the package is installed. Alternatively, there is also `fs_path`, which is the
1200 path to the underlying file system object (assuming there is one). That is the one
1201 you need if you want to read the file.
1203 This is attribute is mostly useful for debugging or for looking up the path relative
1204 to the "root" of the virtual file system that debputy maintains.
1206 If the path is detached (see `is_detached`), then this method returns the path as it
1207 was known prior to being detached.
1208 """
1209 raise NotImplementedError
1211 @property
1212 def absolute(self) -> str:
1213 """Returns the absolute version of this path
1215 This is how to refer to this path when the package is installed.
1217 If the path is detached (see `is_detached`), then this method returns the last known location
1218 of installation (prior to being detached).
1220 :return: The absolute path of this file as it would be on the installed system.
1221 """
1222 p = self.path.lstrip(".")
1223 if not p.startswith("/"):
1224 return f"/{p}"
1225 return p
1227 def is_root_dir(self) -> bool:
1228 """Whether the current path is the root directory
1230 :return: True if this path is the root directory. False otherwise
1231 """
1232 # The root directory is never detachable in the current setup
1233 raise NotImplementedError
1235 @property
1236 def parent_dir(self) -> "VirtualPath | None":
1237 """The parent directory of this path
1239 Note this operation requires the path is "attached" (see `is_detached`). All paths are attached
1240 by default but unlinking paths will cause them to become detached.
1242 :return: The parent path or None for the root.
1243 """
1244 raise NotImplementedError
1246 @property
1247 def size(self) -> int:
1248 """Resolve the file size (`st_size`)
1250 :return: The size of the file in bytes
1251 """
1252 raise NotImplementedError
1254 @property
1255 def mode(self) -> int:
1256 """Determine the mode bits of this path object
1258 Note that:
1259 * like with `stat` above, this never follows symlinks.
1260 * the mode returned by this method is not always a 1:1 with the mode in the
1261 physical file system. As an optimization, `debputy` skips unnecessary writes
1262 to the underlying file system in many cases.
1265 :return: The mode bits for the path.
1266 """
1267 raise NotImplementedError
1269 @mode.setter
1270 def mode(self, new_mode: int) -> None:
1271 """Set the octal file mode of this path
1273 Note that:
1274 * this operation will fail if `path.is_read_write` returns False.
1275 * this operation is generally *not* synced to the physical file system (as
1276 an optimization).
1278 :param new_mode: The new octal mode for this path. Note that `debputy` insists
1279 that all paths have the `user read bit` and, for directories also, the
1280 `user execute bit`. The absence of these minimal mode bits causes hard to
1281 debug errors.
1282 """
1283 raise NotImplementedError
1285 @property
1286 def is_executable(self) -> bool:
1287 """Determine whether a path is considered executable
1289 Generally, this means that at least one executable bit is set. This will
1290 basically always be true for directories as directories need the execute
1291 parameter to be traversable.
1293 :return: True if the path is considered executable with its current mode
1294 """
1295 return bool(self.mode & 0o0111)
1297 def chmod(self, new_mode: int | str) -> None:
1298 """Set the file mode of this path
1300 This is similar to setting the `mode` attribute. However, this method accepts
1301 a string argument, which will be parsed as a symbolic mode (example: `u+rX,go=rX`).
1303 Note that:
1304 * this operation will fail if `path.is_read_write` returns False.
1305 * this operation is generally *not* synced to the physical file system (as
1306 an optimization).
1308 :param new_mode: The new mode for this path.
1309 Note that `debputy` insists that all paths have the `user read bit` and, for
1310 directories also, the `user execute bit`. The absence of these minimal mode
1311 bits causes hard to debug errors.
1312 """
1313 if isinstance(new_mode, str):
1314 segments = parse_symbolic_mode(new_mode, None)
1315 final_mode = self.mode
1316 is_dir = self.is_dir
1317 for segment in segments:
1318 final_mode = segment.apply(final_mode, is_dir)
1319 self.mode = final_mode
1320 else:
1321 self.mode = new_mode
1323 def chown(
1324 self,
1325 owner: "StaticFileSystemOwner | None",
1326 group: "StaticFileSystemGroup | None",
1327 ) -> None:
1328 """Change the owner/group of this path
1330 :param owner: The desired owner definition for this path. If None, then no change of owner is performed.
1331 :param group: The desired group definition for this path. If None, then no change of group is performed.
1332 """
1333 raise NotImplementedError
1335 @property
1336 def mtime(self) -> float:
1337 """Determine the mtime of this path object
1339 Note that:
1340 * like with `stat` above, this never follows symlinks.
1341 * the mtime returned has *not* been clamped against ´SOURCE_DATE_EPOCH`. Timestamp
1342 normalization is handled later by `debputy`.
1343 * the mtime returned by this method is not always a 1:1 with the mtime in the
1344 physical file system. As an optimization, `debputy` skips unnecessary writes
1345 to the underlying file system in many cases.
1347 :return: The mtime for the path.
1348 """
1349 raise NotImplementedError
1351 @mtime.setter
1352 def mtime(self, new_mtime: float) -> None:
1353 """Set the mtime of this path
1355 Note that:
1356 * this operation will fail if `path.is_read_write` returns False.
1357 * this operation is generally *not* synced to the physical file system (as
1358 an optimization).
1360 :param new_mtime: The new mtime of this path. Note that the caller does not need to
1361 account for `SOURCE_DATE_EPOCH`. Timestamp normalization is handled later.
1362 """
1363 raise NotImplementedError
1365 def readlink(self) -> str:
1366 """Determine the link target of this path assuming it is a symlink
1368 For paths where `is_symlink` is True, this already returns a link target even when
1369 `has_fs_path` is False.
1371 :return: The link target of the path or an error is this is not a symlink
1372 """
1373 raise NotImplementedError()
1375 @overload
1376 def open( 1376 ↛ exitline 1376 didn't return from function 'open' because
1377 self,
1378 *,
1379 byte_io: Literal[False] = False,
1380 buffering: int = -1,
1381 ) -> TextIO: ...
1383 @overload
1384 def open( 1384 ↛ exitline 1384 didn't return from function 'open' because
1385 self,
1386 *,
1387 byte_io: Literal[True],
1388 buffering: Literal[0] = ...,
1389 ) -> io.FileIO: ...
1391 @overload
1392 def open( 1392 ↛ exitline 1392 didn't return from function 'open' because
1393 self,
1394 *,
1395 byte_io: Literal[True],
1396 buffering: int = -1,
1397 ) -> io.BufferedReader: ...
1399 def open(self, *, byte_io=False, buffering=-1):
1400 """Open the file for reading. Usually used with a context manager
1402 By default, the file is opened in text mode (utf-8). Binary mode can be requested
1403 via the `byte_io` parameter. This operation is only valid for files (`is_file` returns
1404 `True`). Usage on symlinks and directories will raise exceptions.
1406 This method *often* requires the `fs_path` to be present. However, tests as a notable
1407 case can inject content without having the `fs_path` point to a real file. (To be clear,
1408 such tests are generally expected to ensure `has_fs_path` returns `True`).
1411 :param byte_io: If True, open the file in binary mode (like `rb` for `open`)
1412 :param buffering: Same as open(..., buffering=...) where supported. Notably during
1413 testing, the content may be purely in memory and use a BytesIO/StringIO
1414 (which does not accept that parameter, but then it is buffered in a different way)
1415 :return: The file handle.
1416 """
1418 if not self.is_file: 1418 ↛ 1419line 1418 didn't jump to line 1419 because the condition on line 1418 was never true
1419 raise TypeError(f"Cannot open {self.path} for reading: It is not a file")
1421 if byte_io:
1422 return open(self.fs_path, "rb", buffering=buffering)
1423 return open(self.fs_path, encoding="utf-8", buffering=buffering)
1425 @property
1426 def fs_path(self) -> str:
1427 """Request the underling fs_path of this path
1429 Only available when `has_fs_path` is True. Generally this should only be used for files to read
1430 the contents of the file and do some action based on the parsed result.
1432 The path should only be used for read-only purposes as debputy may assume that it is safe to have
1433 multiple paths pointing to the same file system path.
1435 Note that:
1436 * This is often *not* available for directories and symlinks.
1437 * The debputy in-memory file system overrules the physical file system. Attempting to "fix" things
1438 by using `os.chmod` or `os.unlink`'ing files, etc. will generally not do as you expect. Best case,
1439 your actions are ignored and worst case it will cause the build to fail as it violates debputy's
1440 internal invariants.
1442 :return: The path to the underlying file system object on the build system or an error if no such
1443 file exist (see `has_fs_path`).
1444 """
1445 raise NotImplementedError()
1447 @property
1448 def is_dir(self) -> bool:
1449 """Determine if this path is a directory
1451 Never follows symlinks.
1453 :return: True if this path is a directory. False otherwise.
1454 """
1455 raise NotImplementedError()
1457 @property
1458 def is_file(self) -> bool:
1459 """Determine if this path is a directory
1461 Never follows symlinks.
1463 :return: True if this path is a regular file. False otherwise.
1464 """
1465 raise NotImplementedError()
1467 @property
1468 def is_symlink(self) -> bool:
1469 """Determine if this path is a symlink
1471 :return: True if this path is a symlink. False otherwise.
1472 """
1473 raise NotImplementedError()
1475 @property
1476 def has_fs_path(self) -> bool:
1477 """Determine whether this path is backed by a file system path
1479 :return: True if this path is backed by a file system object on the build system.
1480 """
1481 raise NotImplementedError()
1483 @property
1484 def is_read_write(self) -> bool:
1485 """When true, the file system entry may be mutated
1487 Read-write rules are:
1489 +--------------------------+-------------------+------------------------+
1490 | File system | From / Inside | Read-Only / Read-Write |
1491 +--------------------------+-------------------+------------------------+
1492 | Source directory | Any context | Read-Only |
1493 | Binary staging directory | Package Processor | Read-Write |
1494 | Binary staging directory | Metadata Detector | Read-Only |
1495 +--------------------------+-------------------+------------------------+
1497 These rules apply to the virtual file system (`debputy` cannot enforce
1498 these rules in the underlying file system). The `debputy` code relies
1499 on these rules for its logic in multiple places to catch bugs and for
1500 optimizations.
1502 As an example, the reason why the file system is read-only when Metadata
1503 Detectors are run is based the contents of the file system has already
1504 been committed. New files will not be included, removals of existing
1505 files will trigger a hard error when the package is assembled, etc.
1506 To avoid people spending hours debugging why their code does not work
1507 as intended, `debputy` instead throws a hard error if you try to mutate
1508 the file system when it is read-only mode to "fail fast".
1510 :return: Whether file system mutations are permitted.
1511 """
1512 return False
1514 def mkdir(self, name: str) -> "VirtualPath":
1515 """Create a new subdirectory of the current path
1517 :param name: Basename of the new directory. The directory must not contain a path
1518 with this basename.
1519 :return: The new subdirectory
1520 """
1521 raise NotImplementedError
1523 def mkdirs(self, path: str) -> "VirtualPath":
1524 """Ensure a given path exists and is a directory.
1526 :param path: Path to the directory to create. Any parent directories will be
1527 created as needed. If the path already exists and is a directory, then it
1528 is returned. If any part of the path exists and that is not a directory,
1529 then the `mkdirs` call will raise an error.
1530 :return: The directory denoted by the given path
1531 """
1532 raise NotImplementedError
1534 def add_file(
1535 self,
1536 name: str,
1537 *,
1538 unlink_if_exists: bool = True,
1539 use_fs_path_mode: bool = False,
1540 mode: int = 0o0644,
1541 mtime: float | None = None,
1542 ) -> ContextManager["VirtualPath"]:
1543 """Add a new regular file as a child of this path
1545 This method will insert a new file into the virtual file system as a child
1546 of the current path (which must be a directory). The caller must use the
1547 return value as a context manager (see example). During the life-cycle of
1548 the managed context, the caller can fill out the contents of the file
1549 from the new path's `fs_path` attribute. The `fs_path` will exist as an
1550 empty file when the context manager is entered.
1552 Once the context manager exits, mutation of the `fs_path` is no longer permitted.
1554 >>> import subprocess
1555 >>> path = ... # doctest: +SKIP
1556 >>> with path.add_file("foo") as new_file, open(new_file.fs_path, "w") as fd: # doctest: +SKIP
1557 ... fd.writelines(["Some", "Content", "Here"])
1559 The caller can replace the provided `fs_path` entirely provided at the end result
1560 (when the context manager exits) is a regular file with no hard links.
1562 Note that this operation will fail if `path.is_read_write` returns False.
1564 :param name: Basename of the new file
1565 :param unlink_if_exists: If the name was already in use, then either an exception is thrown
1566 (when `unlink_if_exists` is False) or the path will be removed via ´unlink(recursive=False)`
1567 (when `unlink_if_exists` is True)
1568 :param use_fs_path_mode: When True, the file created will have this mode in the physical file
1569 system. When the context manager exists, `debputy` will refresh its mode to match the mode
1570 in the physical file system. This is primarily useful if the caller uses a subprocess to
1571 mutate the path and the file mode is relevant for this tool (either as input or output).
1572 When the parameter is false, the new file is guaranteed to be readable and writable for
1573 the current user. However, no other guarantees are given (not even that it matches the
1574 `mode` parameter and any changes to the mode in the physical file system will be ignored.
1575 :param mode: This is the initial file mode. Note the `use_fs_path_mode` parameter for how
1576 this interacts with the physical file system.
1577 :param mtime: If the caller has a more accurate mtime than the mtime of the generated file,
1578 then it can be provided here. Note that all mtimes will later be clamped based on
1579 `SOURCE_DATE_EPOCH`. This parameter is only for when the conceptual mtime of this path
1580 should be earlier than `SOURCE_DATE_EPOCH`.
1581 :return: A Context manager that upon entering provides a `VirtualPath` instance for the
1582 new file. The instance remains valid after the context manager exits (assuming it exits
1583 successfully), but the file denoted by `fs_path` must not be changed after the context
1584 manager exits
1585 """
1586 raise NotImplementedError
1588 def replace_fs_path_content(
1589 self,
1590 *,
1591 use_fs_path_mode: bool = False,
1592 ) -> ContextManager[str]:
1593 """Replace the contents of this file via inline manipulation
1595 Used as a context manager to provide the fs path for manipulation.
1597 Example:
1598 >>> import subprocess
1599 >>> path = ... # doctest: +SKIP
1600 >>> with path.replace_fs_path_content() as fs_path: # doctest: +SKIP
1601 ... subprocess.check_call(['strip', fs_path]) # doctest: +SKIP
1603 The provided file system path should be manipulated inline. The debputy framework may
1604 copy it first as necessary and therefore the provided fs_path may be different from
1605 `path.fs_path` prior to entering the context manager.
1607 Note that this operation will fail if `path.is_read_write` returns False.
1609 If the mutation causes the returned `fs_path` to be a non-file or a hard-linked file
1610 when the context manager exits, `debputy` will raise an error at that point. To preserve
1611 the internal invariants of `debputy`, the path will be unlinked as `debputy` cannot
1612 reliably restore the path.
1614 :param use_fs_path_mode: If True, any changes to the mode on the physical FS path will be
1615 recorded as the desired mode of the file when the contextmanager ends. The provided FS path
1616 with start with the current mode when `use_fs_path_mode` is True. Otherwise, `debputy` will
1617 ignore the mode of the file system entry and reuse its own current mode
1618 definition.
1619 :return: A Context manager that upon entering provides the path to a muable (copy) of
1620 this path's `fs_path` attribute. The file on the underlying path may be mutated however
1621 the caller wishes until the context manager exits.
1622 """
1623 raise NotImplementedError
1625 def add_symlink(self, link_name: str, link_target: str) -> "VirtualPath":
1626 """Add a new regular file as a child of this path
1628 This will create a new symlink inside the current path. If the path already exists,
1629 the existing path will be unlinked via `unlink(recursive=False)`.
1631 Note that this operation will fail if `path.is_read_write` returns False.
1633 :param link_name: The basename of the link file entry.
1634 :param link_target: The target of the link. Link target normalization will
1635 be handled by `debputy`, so the caller can use relative or absolute paths.
1636 (At the time of writing, symlink target normalization happens late)
1637 :return: The newly created symlink.
1638 """
1639 raise NotImplementedError
1641 def unlink(self, *, recursive: bool = False) -> None:
1642 """Unlink a file or a directory
1644 This operation will remove the path from the file system (causing `is_detached` to return True).
1646 When the path is a:
1648 * symlink, then the symlink itself is removed. The target (if present) is not affected.
1649 * *non-empty* directory, then the `recursive` parameter decides the outcome. An empty
1650 directory will be removed regardless of the value of `recursive`.
1652 Note that:
1653 * the root directory cannot be deleted.
1654 * this operation will fail if `path.is_read_write` returns False.
1656 :param recursive: If True, then non-empty directories will be unlinked as well removing everything inside them
1657 as well. When False, an error is raised if the path is a non-empty directory
1658 """
1659 raise NotImplementedError
1661 def interpreter(self) -> Interpreter | None:
1662 """Determine the interpreter of the file (`#!`-line details)
1664 Note: this method is only applicable for files (`is_file` is True).
1666 :return: The detected interpreter if present or None if no interpreter can be detected.
1667 """
1668 if not self.is_file:
1669 raise TypeError("Only files can have interpreters")
1670 try:
1671 with self.open(byte_io=True, buffering=4096) as fd:
1672 return extract_shebang_interpreter_from_file(fd)
1673 except (PureVirtualPathError, TestPathWithNonExistentFSPathError):
1674 return None
1676 def metadata(
1677 self,
1678 metadata_type: type[PMT],
1679 ) -> PathMetadataReference[PMT]:
1680 """Fetch the path metadata reference to access the underlying metadata
1682 Calling this method returns a reference to an arbitrary piece of metadata associated
1683 with this path. Plugins can store any arbitrary data associated with a given path.
1684 Keep in mind that the metadata is stored in memory, so keep the size in moderation.
1686 To store / update the metadata, the path must be in read-write mode. However,
1687 already stored metadata remains accessible even if the path becomes read-only.
1689 Note this method is not applicable if the path is detached
1691 :param metadata_type: Type of the metadata being stored.
1692 :return: A reference to the metadata.
1693 """
1694 raise NotImplementedError
1697class FlushableSubstvars(Substvars):
1698 __slots__ = ()
1700 @contextlib.contextmanager
1701 def flush(self) -> Iterator[str]:
1702 """Temporarily write the substvars to a file and then re-read it again
1704 >>> s = FlushableSubstvars()
1705 >>> 'Test:Var' in s
1706 False
1707 >>> with s.flush() as name, open(name, 'wt', encoding='utf-8') as fobj:
1708 ... _ = fobj.write('Test:Var=bar\\n') # "_ = " is to ignore the return value of write
1709 >>> 'Test:Var' in s
1710 True
1712 Used as a context manager to define when the file is flushed and can be
1713 accessed via the file system. If the context terminates successfully, the
1714 file is read and its content replaces the current substvars.
1716 This is mostly useful if the plugin needs to interface with a third-party
1717 tool that requires a file as interprocess communication (IPC) for sharing
1718 the substvars.
1720 The file may be truncated or completed replaced (change inode) as long as
1721 the provided path points to a regular file when the context manager
1722 terminates successfully.
1724 Note that any manipulation of the substvars via the `Substvars` API while
1725 the file is flushed will silently be discarded if the context manager completes
1726 successfully.
1727 """
1728 with tempfile.NamedTemporaryFile(mode="w+t", encoding="utf-8") as tmp:
1729 self.write_substvars(tmp)
1730 tmp.flush() # Temping to use close, but then we have to manually delete the file.
1731 yield tmp.name
1732 # Re-open; seek did not work when I last tried (if I did it work, feel free to
1733 # convert back to seek - as long as it works!)
1734 with open(tmp.name, encoding="utf-8") as fd:
1735 self.read_substvars(fd)
1737 def save(self) -> None:
1738 # Promote the debputy extension over `save()` for the plugins.
1739 if self._substvars_path is None:
1740 raise TypeError(
1741 "Please use `flush()` extension to temporarily write the substvars to the file system"
1742 )
1743 super().save()
1746class ServiceRegistry(Generic[DSD]):
1747 __slots__ = ()
1749 def register_service(
1750 self,
1751 path: VirtualPath,
1752 name: str | list[str],
1753 *,
1754 type_of_service: str = "service", # "timer", etc.
1755 service_scope: str = "system",
1756 enable_by_default: bool = True,
1757 start_by_default: bool = True,
1758 default_upgrade_rule: ServiceUpgradeRule = "restart",
1759 service_context: DSD | None = None,
1760 ) -> None:
1761 """Register a service detected in the package
1763 All the details will either be provided as-is or used as default when the plugin provided
1764 integration code is called.
1766 Two services from different service managers are considered related when:
1768 1) They are of the same type (`type_of_service`) and has the same scope (`service_scope`), AND
1769 2) Their plugin provided names has an overlap
1771 Related services can be covered by the same service definition in the manifest.
1773 :param path: The path defining this service.
1774 :param name: The name of the service. Multiple ones can be provided if the service has aliases.
1775 Note that when providing multiple names, `debputy` will use the first name in the list as the
1776 default name if it has to choose. Any alternative name provided can be used by the packager
1777 to identify this service.
1778 :param type_of_service: The type of service. By default, this is "service", but plugins can
1779 provide other types (such as "timer" for the systemd timer unit).
1780 :param service_scope: The scope for this service. By default, this is "system" meaning the
1781 service is a system-wide service. Service managers can define their own scopes such as
1782 "user" (which is used by systemd for "per-user" services).
1783 :param enable_by_default: Whether the service should be enabled by default, assuming the
1784 packager does not explicitly override this setting.
1785 :param start_by_default: Whether the service should be started by default on install, assuming
1786 the packager does not explicitly override this setting.
1787 :param default_upgrade_rule: The default value for how the service should be processed during
1788 upgrades. Options are:
1789 * `do-nothing`: The plugin should not interact with the running service (if any)
1790 (maintenance of the enabled start, start on install, etc. are still applicable)
1791 * `reload`: The plugin should attempt to reload the running service (if any).
1792 Note: In combination with `auto_start_in_install == False`, be careful to not
1793 start the service if not is not already running.
1794 * `restart`: The plugin should attempt to restart the running service (if any).
1795 Note: In combination with `auto_start_in_install == False`, be careful to not
1796 start the service if not is not already running.
1797 * `stop-then-start`: The plugin should stop the service during `prerm upgrade`
1798 and start it against in the `postinst` script.
1800 :param service_context: Any custom data that the detector want to pass along to the
1801 integrator for this service.
1802 """
1803 raise NotImplementedError
1806@dataclasses.dataclass(slots=True, frozen=True)
1807class ParserAttributeDocumentation:
1808 attributes: frozenset[str]
1809 description: str | None
1811 @property
1812 def is_hidden(self) -> bool:
1813 return False
1816@final
1817@dataclasses.dataclass(slots=True, frozen=True)
1818class StandardParserAttributeDocumentation(ParserAttributeDocumentation):
1819 sort_category: int = 0
1822def undocumented_attr(attr: str) -> ParserAttributeDocumentation:
1823 """Describe an attribute as undocumented
1825 If you for some reason do not want to document a particular attribute, you can mark it as
1826 undocumented. This is required if you are only documenting a subset of the attributes,
1827 because `debputy` assumes any omission to be a mistake.
1829 :param attr: Name of the attribute
1830 """
1831 return ParserAttributeDocumentation(
1832 frozenset({attr}),
1833 None,
1834 )
1837@dataclasses.dataclass(slots=True, frozen=True)
1838class ParserDocumentation:
1839 synopsis: str | None = None
1840 title: str | None = None
1841 description: str | None = None
1842 attribute_doc: Sequence[ParserAttributeDocumentation] | None = None
1843 alt_parser_description: str | None = None
1844 documentation_reference_url: str | None = None
1846 def replace(self, **changes: Any) -> "ParserDocumentation":
1847 return dataclasses.replace(self, **changes)
1849 @classmethod
1850 def from_ref_doc(cls, ref_doc: "ParserRefDocumentation") -> "ParserDocumentation":
1851 attr = [
1852 documented_attr(d["attr"], d["description"])
1853 for d in ref_doc.get("attributes", [])
1854 ]
1855 undoc_attr = ref_doc.get("undocumented_attributes")
1856 if undoc_attr: 1856 ↛ 1857line 1856 didn't jump to line 1857 because the condition on line 1856 was never true
1857 attr.extend(undocumented_attr(attr) for attr in undoc_attr)
1859 return reference_documentation(
1860 title=ref_doc["title"],
1861 description=ref_doc["description"],
1862 attributes=attr,
1863 non_mapping_description=ref_doc.get("non_mapping_description"),
1864 reference_documentation_url=ref_doc.get("ref_doc_url"),
1865 synopsis=ref_doc.get("synopsis"),
1866 )
1869@dataclasses.dataclass(slots=True, frozen=True)
1870class TypeMappingExample(Generic[S]):
1871 source_input: S
1874@dataclasses.dataclass(slots=True, frozen=True)
1875class TypeMappingDocumentation(Generic[S]):
1876 description: str | None = None
1877 examples: Sequence[TypeMappingExample[S]] = tuple()
1880def type_mapping_example(source_input: S) -> TypeMappingExample[S]:
1881 return TypeMappingExample(source_input)
1884def type_mapping_reference_documentation(
1885 *,
1886 description: str | None = None,
1887 examples: TypeMappingExample[S] | Iterable[TypeMappingExample[S]] = tuple(),
1888) -> TypeMappingDocumentation[S]:
1889 e = (
1890 tuple([examples])
1891 if isinstance(examples, TypeMappingExample)
1892 else tuple(examples)
1893 )
1894 return TypeMappingDocumentation(
1895 description=description,
1896 examples=e,
1897 )
1900def documented_attr(
1901 attr: str | Iterable[str],
1902 description: str,
1903) -> ParserAttributeDocumentation:
1904 """Describe an attribute or a group of attributes
1906 :param attr: A single attribute or a sequence of attributes. The attribute must be the
1907 attribute name as used in the source format version of the TypedDict.
1909 If multiple attributes are provided, they will be documented together. This is often
1910 useful if these attributes are strongly related (such as different names for the same
1911 target attribute).
1912 :param description: The description the user should see for this attribute / these
1913 attributes. This parameter can be a Python format string with variables listed in
1914 the description of `reference_documentation`.
1915 :return: An opaque representation of the documentation,
1916 """
1917 attributes = [attr] if isinstance(attr, str) else attr
1918 return ParserAttributeDocumentation(
1919 frozenset(attributes),
1920 description,
1921 )
1924def reference_documentation(
1925 title: str | None = None,
1926 description: str | None = textwrap.dedent(
1927 """\
1928 This is an automatically generated reference documentation for ${RULE_NAME}. It is generated
1929 from input provided by ${PLUGIN_NAME} via the debputy API.
1931 (If you are the provider of the ${PLUGIN_NAME} plugin, you can replace this text with
1932 your own documentation by providing the `inline_reference_documentation` when registering
1933 the manifest rule.)
1934 """
1935 ),
1936 attributes: Sequence[ParserAttributeDocumentation] | None = None,
1937 non_mapping_description: str | None = None,
1938 reference_documentation_url: str | None = None,
1939 synopsis: str | None = None,
1940) -> ParserDocumentation:
1941 """Provide inline reference documentation for the manifest snippet
1943 For parameters that mention that they are a Python format, the following template variables
1944 are available (`${FOO}`):
1946 * RULE_NAME: Name of the rule. If manifest snippet has aliases, this will be the name of
1947 the alias provided by the user.
1948 * MANIFEST_FORMAT_DOC: Path OR URL to the "MANIFEST-FORMAT" reference documentation from
1949 `debputy`. By using the MANIFEST_FORMAT_DOC variable, you ensure that you point to the
1950 file that matches the version of `debputy` itself.
1951 * PLUGIN_NAME: Name of the plugin providing this rule.
1953 :param title: The text you want the user to see as for your rule. A placeholder is provided by default.
1954 This parameter can be a Python format string with the above listed variables.
1955 :param description: The text you want the user to see as a description for the rule. An auto-generated
1956 placeholder is provided by default saying that no human written documentation was provided.
1957 This parameter can be a Python format string with the above listed variables.
1958 :param synopsis: One-line plain-text description used for describing the feature during completion.
1959 :param attributes: A sequence of attribute-related documentation. Each element of the sequence should
1960 be the result of `documented_attr` or `undocumented_attr`. The sequence must cover all source
1961 attributes exactly once.
1962 :param non_mapping_description: The text you want the user to see as the description for your rule when
1963 `debputy` describes its non-mapping format. Must not be provided for rules that do not have an
1964 (optional) non-mapping format as source format. This parameter can be a Python format string with
1965 the above listed variables.
1966 :param reference_documentation_url: A URL to the reference documentation.
1967 :return: An opaque representation of the documentation,
1968 """
1969 if title is None:
1970 title = "Auto-generated reference documentation for ${RULE_NAME}"
1971 return ParserDocumentation(
1972 synopsis,
1973 title,
1974 description,
1975 attributes,
1976 non_mapping_description,
1977 reference_documentation_url,
1978 )
1981class ServiceDefinition(Generic[DSD]):
1982 __slots__ = ()
1984 @property
1985 def name(self) -> str:
1986 """Name of the service registered by the plugin
1988 This is always a plugin provided name for this service (that is, `x.name in x.names`
1989 will always be `True`). Where possible, this will be the same as the one that the
1990 packager provided when they provided any configuration related to this service.
1991 When not possible, this will be the first name provided by the plugin (`x.names[0]`).
1993 If all the aliases are equal, then using this attribute will provide traceability
1994 between the manifest and the generated maintscript snippets. When the exact name
1995 used is important, the plugin should ignore this attribute and pick the name that
1996 is needed.
1997 """
1998 raise NotImplementedError
2000 @property
2001 def names(self) -> Sequence[str]:
2002 """All *plugin provided* names and aliases of the service
2004 This is the name/sequence of names that the plugin provided when it registered
2005 the service earlier.
2006 """
2007 raise NotImplementedError
2009 @property
2010 def path(self) -> VirtualPath:
2011 """The registered path for this service
2013 :return: The path that was associated with this service when it was registered
2014 earlier.
2015 """
2016 raise NotImplementedError
2018 @property
2019 def type_of_service(self) -> str:
2020 """Type of the service such as "service" (daemon), "timer", etc.
2022 :return: The type of service scope. It is the same value as the one as the plugin provided
2023 when registering the service (if not explicitly provided, it defaults to "service").
2024 """
2025 raise NotImplementedError
2027 @property
2028 def service_scope(self) -> str:
2029 """Service scope such as "system" or "user"
2031 :return: The service scope. It is the same value as the one as the plugin provided
2032 when registering the service (if not explicitly provided, it defaults to "system")
2033 """
2034 raise NotImplementedError
2036 @property
2037 def auto_enable_on_install(self) -> bool:
2038 """Whether the service should be auto-enabled on install
2040 :return: True if the service should be enabled automatically, false if not.
2041 """
2042 raise NotImplementedError
2044 @property
2045 def auto_start_on_install(self) -> bool:
2046 """Whether the service should be auto-started on install
2048 :return: True if the service should be started automatically, false if not.
2049 """
2050 raise NotImplementedError
2052 @property
2053 def on_upgrade(self) -> ServiceUpgradeRule:
2054 """How to handle the service during an upgrade
2056 Options are:
2057 * `do-nothing`: The plugin should not interact with the running service (if any)
2058 (maintenance of the enabled start, start on install, etc. are still applicable)
2059 * `reload`: The plugin should attempt to reload the running service (if any).
2060 Note: In combination with `auto_start_in_install == False`, be careful to not
2061 start the service if not is not already running.
2062 * `restart`: The plugin should attempt to restart the running service (if any).
2063 Note: In combination with `auto_start_in_install == False`, be careful to not
2064 start the service if not is not already running.
2065 * `stop-then-start`: The plugin should stop the service during `prerm upgrade`
2066 and start it against in the `postinst` script.
2068 Note: In all cases, the plugin should still consider what to do in
2069 `prerm remove`, which is the last point in time where the plugin can rely on the
2070 service definitions in the file systems to stop the services when the package is
2071 being uninstalled.
2073 :return: The service restart rule
2074 """
2075 raise NotImplementedError
2077 @property
2078 def definition_source(self) -> str:
2079 """Describes where this definition came from
2081 If the definition is provided by the packager, then this will reference the part
2082 of the manifest that made this definition. Otherwise, this will be a reference
2083 to the plugin providing this definition.
2085 :return: The source of this definition
2086 """
2087 raise NotImplementedError
2089 @property
2090 def is_plugin_provided_definition(self) -> bool:
2091 """Whether the definition source points to the plugin or a package provided definition
2093 :return: True if definition is 100% from the plugin. False if the definition is partially
2094 or fully from another source (usually, the packager via the manifest).
2095 """
2096 raise NotImplementedError
2098 @property
2099 def service_context(self) -> DSD | None:
2100 """Custom service context (if any) provided by the detector code of the plugin
2102 :return: If the detection code provided a custom data when registering the
2103 service, this attribute will reference that data. If nothing was provided,
2104 then this attribute will be None.
2105 """
2106 raise NotImplementedError