Coverage for src/debputy/plugins/debputy/private_api.py: 79%
496 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-07-22 10:58 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-07-22 10:58 +0000
1import ctypes
2import ctypes.util
3import dataclasses
4import functools
5import textwrap
6import time
7import typing
8from datetime import datetime
9from typing import cast, NotRequired, Union, TypedDict, Annotated, Any
10from collections.abc import Callable
12import debian.debian_support
13from debian.changelog import Changelog
14from debian.deb822 import Deb822
16import debputy.plugin.api.spec
17from debputy._manifest_constants import (
18 MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE,
19 MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION,
20 MK_INSTALLATIONS_INSTALL_EXAMPLES,
21 MK_INSTALLATIONS_INSTALL,
22 MK_INSTALLATIONS_INSTALL_DOCS,
23 MK_INSTALLATIONS_INSTALL_MAN,
24 MK_INSTALLATIONS_DISCARD,
25 MK_INSTALLATIONS_MULTI_DEST_INSTALL,
26)
27from debputy.exceptions import DebputyManifestVariableRequiresDebianDirError
28from debputy.installations import InstallRule
29from debputy.maintscript_snippet import (
30 DpkgMaintscriptHelperCommand,
31 MaintscriptCondition,
32 MaintscriptForBinary,
33 SUPPORTED_UDEB_SCRIPTS,
34 DPKG_DEB_CONTROL_SCRIPTS,
35 ALL_CONTROL_SCRIPTS,
36)
37from debputy.manifest_conditions import (
38 ManifestCondition,
39 BinaryPackageContextArchMatchManifestCondition,
40 BuildProfileMatch,
41 SourceContextArchMatchManifestCondition,
42)
43from debputy.manifest_parser.base_types import (
44 FileSystemMode,
45 StaticFileSystemOwner,
46 StaticFileSystemGroup,
47 SymlinkTarget,
48 FileSystemExactMatchRule,
49 FileSystemMatchRule,
50 SymbolicMode,
51 OctalMode,
52 FileSystemExactNonDirMatchRule,
53 BuildEnvironmentDefinition,
54 DebputyParsedContentStandardConditional,
55)
56from debputy.manifest_parser.exceptions import ManifestParseException
57from debputy.manifest_parser.mapper_code import type_mapper_str2package, PackageSelector
58from debputy.manifest_parser.parse_hints import DebputyParseHint
59from debputy.manifest_parser.parser_data import ParserContextData
60from debputy.manifest_parser.tagging_types import (
61 DebputyParsedContent,
62 TypeMapping,
63)
64from debputy.manifest_parser.util import AttributePath, check_integration_mode
65from debputy.packages import BinaryPackage
66from debputy.path_matcher import ExactFileSystemPath
67from debputy.plugin.api import (
68 DebputyPluginInitializer,
69 documented_attr,
70 reference_documentation,
71 VirtualPath,
72 packager_provided_file_reference_documentation,
73)
74from debputy.plugin.api.impl import DebputyPluginInitializerProvider
75from debputy.plugin.api.impl_types import automatic_discard_rule_example, PPFFormatParam
76from debputy.plugin.api.spec import (
77 type_mapping_reference_documentation,
78 type_mapping_example,
79 not_integrations,
80 INTEGRATION_MODE_DH_DEBPUTY_RRR,
81)
82from debputy.plugin.api.std_docs import docs_from
83from debputy.plugins.debputy.binary_package_rules import register_binary_package_rules
84from debputy.plugins.debputy.discard_rules import (
85 _debputy_discard_pyc_files,
86 _debputy_prune_la_files,
87 _debputy_prune_doxygen_cruft,
88 _debputy_prune_binary_debian_dir,
89 _debputy_prune_info_dir_file,
90 _debputy_prune_backup_files,
91 _debputy_prune_vcs_paths,
92)
93from debputy.plugins.debputy.manifest_root_rules import register_manifest_root_rules
94from debputy.plugins.debputy.package_processors import (
95 process_manpages,
96 apply_compression,
97 clean_la_files,
98)
99from debputy.plugins.debputy.service_management import (
100 detect_systemd_service_files,
101 generate_snippets_for_systemd_units,
102 detect_sysv_init_service_files,
103 generate_snippets_for_init_scripts,
104)
105from debputy.plugins.debputy.shlib_metadata_detectors import detect_shlibdeps
106from debputy.plugins.debputy.strip_non_determinism import strip_non_determinism
107from debputy.substitution import VariableContext
108from debputy.transformation_rules import (
109 CreateSymlinkReplacementRule,
110 TransformationRule,
111 CreateDirectoryTransformationRule,
112 RemoveTransformationRule,
113 MoveTransformationRule,
114 PathMetadataTransformationRule,
115 CreateSymlinkPathTransformationRule,
116)
117from debputy.util import (
118 _normalize_path,
119 PKGNAME_REGEX,
120 PKGVERSION_REGEX,
121 debian_policy_normalize_symlink_target,
122 active_profiles_match,
123 _error,
124 _warn,
125 _info,
126 assume_not_none,
127 manifest_format_doc,
128 PackageTypeSelector,
129)
131_DOCUMENTED_DPKG_ARCH_TYPES = {
132 "HOST": (
133 "installed on",
134 "The package will be **installed** on this type of machine / system",
135 ),
136 "BUILD": (
137 "compiled on",
138 "The compilation of this package will be performed **on** this kind of machine / system",
139 ),
140 "TARGET": (
141 "cross-compiler output",
142 "When building a cross-compiler, it will produce output for this kind of machine/system",
143 ),
144}
146_DOCUMENTED_DPKG_ARCH_VARS = {
147 "ARCH": "Debian's name for the architecture",
148 "ARCH_ABI": "Debian's name for the architecture ABI",
149 "ARCH_BITS": "Number of bits in the pointer size",
150 "ARCH_CPU": "Debian's name for the CPU type",
151 "ARCH_ENDIAN": "Endianness of the architecture (little/big)",
152 "ARCH_LIBC": "Debian's name for the libc implementation",
153 "ARCH_OS": "Debian name for the OS/kernel",
154 "GNU_CPU": "GNU's name for the CPU",
155 "GNU_SYSTEM": "GNU's name for the system",
156 "GNU_TYPE": "GNU system type (GNU_CPU and GNU_SYSTEM combined)",
157 "MULTIARCH": "Multi-arch tuple",
158}
161_NOT_INTEGRATION_RRR = not_integrations(INTEGRATION_MODE_DH_DEBPUTY_RRR)
164@dataclasses.dataclass(slots=True, frozen=True)
165class Capability:
166 value: str
168 @classmethod
169 def parse(
170 cls,
171 raw_value: str,
172 _attribute_path: AttributePath,
173 _parser_context: ParserContextData | None,
174 ) -> "Capability":
175 return cls(raw_value)
178@functools.lru_cache
179def load_libcap() -> tuple[bool, str | None, Callable[[str], bool]]:
180 cap_library_path = ctypes.util.find_library("cap.so")
181 has_libcap = False
182 libcap = None
183 if cap_library_path:
184 try:
185 libcap = ctypes.cdll.LoadLibrary(cap_library_path)
186 has_libcap = True
187 except OSError:
188 pass
190 if libcap is None:
191 warned = False
193 def _is_valid_cap(cap: str) -> bool:
194 nonlocal warned
195 if not warned:
196 _info(
197 "Could not load libcap.so; will not validate capabilities. Use `apt install libcap2` to provide"
198 " checking of capabilities."
199 )
200 warned = True
201 return True
203 else:
204 # cap_t cap_from_text(const char *path_p)
205 libcap.cap_from_text.argtypes = [ctypes.c_char_p]
206 libcap.cap_from_text.restype = ctypes.c_char_p
208 libcap.cap_free.argtypes = [ctypes.c_void_p]
209 libcap.cap_free.restype = None
211 def _is_valid_cap(cap: str) -> bool:
212 cap_t = libcap.cap_from_text(cap.encode("utf-8"))
213 ok = cap_t is not None
214 libcap.cap_free(cap_t)
215 return ok
217 return has_libcap, cap_library_path, _is_valid_cap
220def check_cap_checker() -> Callable[[str, str], None]:
221 _, libcap_path, is_valid_cap = load_libcap()
223 seen_cap = set()
225 def _check_cap(cap: str, definition_source: str) -> None:
226 if cap not in seen_cap and not is_valid_cap(cap):
227 seen_cap.add(cap)
228 cap_path = f" ({libcap_path})" if libcap_path is not None else ""
229 _warn(
230 f'The capabilities "{cap}" provided in {definition_source} were not understood by'
231 f" libcap.so{cap_path}. Please verify you provided the correct capabilities."
232 f" Note: This warning can be a false-positive if you are targeting a newer libcap.so"
233 f" than the one installed on this system."
234 )
236 return _check_cap
239def load_source_variables(variable_context: VariableContext) -> dict[str, str]:
240 try:
241 changelog = variable_context.debian_dir.lookup("changelog")
242 if changelog is None:
243 raise DebputyManifestVariableRequiresDebianDirError(
244 "The changelog was not present"
245 )
246 with changelog.open() as fd:
247 dch = Changelog(fd, max_blocks=2)
248 except FileNotFoundError as e:
249 raise DebputyManifestVariableRequiresDebianDirError(
250 "The changelog was not present"
251 ) from e
252 first_entry = dch[0]
253 first_non_binnmu_entry = dch[0]
254 if first_non_binnmu_entry.other_pairs.get("binary-only", "no") == "yes":
255 first_non_binnmu_entry = dch[1]
256 assert first_non_binnmu_entry.other_pairs.get("binary-only", "no") == "no"
257 source_version = first_entry.version
258 epoch = source_version.epoch
259 upstream_version = source_version.upstream_version
260 debian_revision = source_version.debian_revision
261 epoch_upstream = upstream_version
262 upstream_debian_revision = upstream_version
263 if epoch is not None and epoch != "": 263 ↛ 265line 263 didn't jump to line 265 because the condition on line 263 was always true
264 epoch_upstream = f"{epoch}:{upstream_version}"
265 if debian_revision is not None and debian_revision != "": 265 ↛ 268line 265 didn't jump to line 268 because the condition on line 265 was always true
266 upstream_debian_revision = f"{upstream_version}-{debian_revision}"
268 package = first_entry.package
269 if package is None: 269 ↛ 270line 269 didn't jump to line 270 because the condition on line 269 was never true
270 _error("Cannot determine the source package name from debian/changelog.")
272 date = first_entry.date
273 if date is not None: 273 ↛ 282line 273 didn't jump to line 282 because the condition on line 273 was always true
274 try:
275 local_time = datetime.strptime(date, "%a, %d %b %Y %H:%M:%S %z")
276 except ValueError:
277 _error(
278 f"Invalid date in the first changelog entry: {date!r} (Expected format: 'Thu, 26 Feb 2026 00:00:00 +0000')"
279 )
280 source_date_epoch = str(int(local_time.timestamp()))
281 else:
282 _warn(
283 "The latest changelog entry does not have a (parsable) date, using current time"
284 " for SOURCE_DATE_EPOCH"
285 )
286 source_date_epoch = str(int(time.time()))
288 if first_non_binnmu_entry is not first_entry:
289 non_binnmu_date = first_non_binnmu_entry.date
290 if non_binnmu_date is not None: 290 ↛ 294line 290 didn't jump to line 294 because the condition on line 290 was always true
291 local_time = datetime.strptime(non_binnmu_date, "%a, %d %b %Y %H:%M:%S %z")
292 snd_source_date_epoch = str(int(local_time.timestamp()))
293 else:
294 _warn(
295 "The latest (non-binNMU) changelog entry does not have a (parsable) date, using current time"
296 " for SOURCE_DATE_EPOCH (for strip-nondeterminism)"
297 )
298 snd_source_date_epoch = source_date_epoch = str(int(time.time()))
299 else:
300 snd_source_date_epoch = source_date_epoch
301 return {
302 "DEB_SOURCE": package,
303 "DEB_VERSION": source_version.full_version,
304 "DEB_VERSION_EPOCH_UPSTREAM": epoch_upstream,
305 "DEB_VERSION_UPSTREAM_REVISION": upstream_debian_revision,
306 "DEB_VERSION_UPSTREAM": upstream_version,
307 "SOURCE_DATE_EPOCH": source_date_epoch,
308 "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": str(first_non_binnmu_entry.version),
309 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": snd_source_date_epoch,
310 }
313def initialize_via_private_api(public_api: DebputyPluginInitializer) -> None:
314 api = cast("DebputyPluginInitializerProvider", public_api)
316 api.metadata_or_maintscript_detector(
317 "dpkg-shlibdeps",
318 # Private because detect_shlibdeps expects private API (hench this cast)
319 cast(debputy.plugin.api.spec.MetadataAutoDetector, detect_shlibdeps),
320 package_types=PackageTypeSelector.DEB | PackageTypeSelector.UDEB,
321 )
322 register_type_mappings(api)
323 register_variables_via_private_api(api)
324 document_builtin_variables(api)
325 register_automatic_discard_rules(api)
326 register_special_ppfs(api)
327 register_install_rules(api)
328 register_transformation_rules(api)
329 register_manifest_condition_rules(api)
330 register_maintscript_conditions(api)
331 register_dpkg_conffile_rules(api)
332 register_processing_steps(api)
333 register_service_managers(api)
334 register_manifest_root_rules(api)
335 register_binary_package_rules(api)
338def register_type_mappings(api: DebputyPluginInitializerProvider) -> None:
339 api.register_mapped_type(
340 TypeMapping(Capability, str, Capability.parse),
341 reference_documentation=type_mapping_reference_documentation(
342 description=textwrap.dedent(
343 """\
344 The value is a Linux capability parsable by cap_from_text on the host system.
346 With `libcap2` installed, `debputy` will attempt to parse the value and provide
347 warnings if the value cannot be parsed by `libcap2`. However, `debputy` will
348 currently never emit hard errors for unknown capabilities.
349 """,
350 ),
351 examples=[
352 type_mapping_example("cap_chown=p"),
353 type_mapping_example("cap_chown=ep"),
354 type_mapping_example("cap_kill-pe"),
355 type_mapping_example("=ep cap_chown-e cap_kill-ep"),
356 ],
357 ),
358 )
359 api.register_mapped_type(
360 TypeMapping(
361 FileSystemMatchRule,
362 str,
363 FileSystemMatchRule.parse_path_match,
364 ).with_mapper_as_lint_validator(),
365 reference_documentation=type_mapping_reference_documentation(
366 description=textwrap.dedent(
367 """\
368 A generic file system path match with globs.
370 Manifest variable substitution will be applied and glob expansion will be performed.
372 The match will be read as one of the following cases:
374 - Exact path match if there is no globs characters like `usr/bin/debputy`
375 - A basename glob like `*.txt` or `**/foo`
376 - A generic path glob otherwise like `usr/lib/*.so*`
378 Except for basename globs, all matches are always relative to the root directory of
379 the match, which is typically the package root directory or a search directory.
381 For basename globs, any path matching that basename beneath the package root directory
382 or relevant search directories will match.
384 Please keep in mind that:
386 * glob patterns often have to be quoted as YAML interpret the glob metacharacter as
387 an anchor reference.
389 * Directories can be matched via this type. Whether the rule using this type
390 recurse into the directory depends on the usage and not this type. Related, if
391 value for this rule ends with a literal "/", then the definition can *only* match
392 directories (similar to the shell).
394 * path matches involving glob expansion are often subject to different rules than
395 path matches without them. As an example, automatic discard rules does not apply
396 to exact path matches, but they will filter out glob matches.
397 """,
398 ),
399 examples=[
400 type_mapping_example("usr/bin/debputy"),
401 type_mapping_example("*.txt"),
402 type_mapping_example("**/foo"),
403 type_mapping_example("usr/lib/*.so*"),
404 type_mapping_example("usr/share/foo/data-*/"),
405 ],
406 ),
407 )
409 api.register_mapped_type(
410 TypeMapping(
411 FileSystemExactMatchRule,
412 str,
413 FileSystemExactMatchRule.parse_path_match,
414 ).with_mapper_as_lint_validator(),
415 reference_documentation=type_mapping_reference_documentation(
416 description=textwrap.dedent(
417 """\
418 A file system match that does **not** expand globs.
420 Manifest variable substitution will be applied. However, globs will not be expanded.
421 Any glob metacharacters will be interpreted as a literal part of path.
423 Note that a directory can be matched via this type. Whether the rule using this type
424 recurse into the directory depends on the usage and is not defined by this type.
425 Related, if value for this rule ends with a literal "/", then the definition can
426 *only* match directories (similar to the shell).
427 """,
428 ),
429 examples=[
430 type_mapping_example("usr/bin/dpkg"),
431 type_mapping_example("usr/share/foo/"),
432 type_mapping_example("usr/share/foo/data.txt"),
433 ],
434 ),
435 )
437 api.register_mapped_type(
438 TypeMapping(
439 FileSystemExactNonDirMatchRule,
440 str,
441 FileSystemExactNonDirMatchRule.parse_path_match,
442 ).with_mapper_as_lint_validator(),
443 reference_documentation=type_mapping_reference_documentation(
444 description=textwrap.dedent(
445 f"""\
446 A file system match that does **not** expand globs and must not match a directory.
448 Manifest variable substitution will be applied. However, globs will not be expanded.
449 Any glob metacharacters will be interpreted as a literal part of path.
451 This is like {FileSystemExactMatchRule.__name__} except that the match will fail if the
452 provided path matches a directory. Since a directory cannot be matched, it is an error
453 for any input to end with a "/" as only directories can be matched if the path ends
454 with a "/".
455 """,
456 ),
457 examples=[
458 type_mapping_example("usr/bin/dh_debputy"),
459 type_mapping_example("usr/share/foo/data.txt"),
460 ],
461 ),
462 )
464 api.register_mapped_type(
465 TypeMapping(
466 SymlinkTarget,
467 str,
468 lambda v, ap, pc: SymlinkTarget.parse_symlink_target(
469 v, ap, assume_not_none(pc).substitution
470 ),
471 ),
472 reference_documentation=type_mapping_reference_documentation(
473 description=textwrap.dedent(
474 """\
475 A symlink target.
477 Manifest variable substitution will be applied. This is distinct from an exact file
478 system match in that a symlink target is not relative to the package root by default
479 (explicitly prefix for "/" for absolute path targets)
481 Note that `debputy` will policy normalize symlinks when assembling the deb, so
482 use of relative or absolute symlinks comes down to preference.
483 """,
484 ),
485 examples=[
486 type_mapping_example("../foo"),
487 type_mapping_example("/usr/share/doc/bar"),
488 ],
489 ),
490 )
492 api.register_mapped_type(
493 TypeMapping(
494 StaticFileSystemOwner,
495 int | str,
496 lambda v, ap, _: StaticFileSystemOwner.from_manifest_value(v, ap),
497 ).with_mapper_as_lint_validator(),
498 reference_documentation=type_mapping_reference_documentation(
499 description=textwrap.dedent("""\
500 File system owner reference that is part of the passwd base data (such as "root").
502 The group can be provided in either of the following three forms:
504 * A name (recommended), such as "root"
505 * The UID in the form of an integer (that is, no quoting), such as 0 (for "root")
506 * The name and the UID separated by colon such as "root:0" (for "root").
508 Note in the last case, the `debputy` will validate that the name and the UID match.
510 Some owners (such as "nobody") are deliberately disallowed.
511 """),
512 examples=[
513 type_mapping_example("root"),
514 type_mapping_example(0),
515 type_mapping_example("root:0"),
516 type_mapping_example("bin"),
517 ],
518 ),
519 )
520 api.register_mapped_type(
521 TypeMapping(
522 StaticFileSystemGroup,
523 int | str,
524 lambda v, ap, _: StaticFileSystemGroup.from_manifest_value(v, ap),
525 ).with_mapper_as_lint_validator(),
526 reference_documentation=type_mapping_reference_documentation(
527 description=textwrap.dedent("""\
528 File system group reference that is part of the passwd base data (such as "root").
530 The group can be provided in either of the following three forms:
532 * A name (recommended), such as "root"
533 * The GID in the form of an integer (that is, no quoting), such as 0 (for "root")
534 * The name and the GID separated by colon such as "root:0" (for "root").
536 Note in the last case, the `debputy` will validate that the name and the GID match.
538 Some owners (such as "nobody") are deliberately disallowed.
539 """),
540 examples=[
541 type_mapping_example("root"),
542 type_mapping_example(0),
543 type_mapping_example("root:0"),
544 type_mapping_example("tty"),
545 ],
546 ),
547 )
549 api.register_mapped_type(
550 TypeMapping(
551 BinaryPackage,
552 str,
553 type_mapper_str2package,
554 ),
555 reference_documentation=type_mapping_reference_documentation(
556 description="Name of a package in debian/control",
557 ),
558 )
560 api.register_mapped_type(
561 TypeMapping(
562 PackageSelector,
563 str,
564 PackageSelector.parse,
565 ),
566 reference_documentation=type_mapping_reference_documentation(
567 description=textwrap.dedent("""\
568 Match a package or set of a packages from debian/control
570 The simplest package selector is the name of a binary package from `debian/control`.
571 However, selections can also match multiple packages based on a given criteria, such
572 as `arch:all`/`arch:any` (matches packages where the `Architecture` field is set to
573 `all` or is not set to `all` respectively) or `package-type:deb` / `package-type:udeb`
574 (matches packages where `Package-Type` is set to `deb` or is set to `udeb`
575 respectively).
576 """),
577 ),
578 )
580 api.register_mapped_type(
581 TypeMapping(
582 FileSystemMode,
583 str,
584 lambda v, ap, _: FileSystemMode.parse_filesystem_mode(v, ap),
585 ).with_mapper_as_lint_validator(),
586 reference_documentation=type_mapping_reference_documentation(
587 description="A file system mode either in the form of an octal mode or a symbolic mode.",
588 examples=[
589 type_mapping_example("a+x"),
590 type_mapping_example("u=rwX,go=rX"),
591 type_mapping_example("0755"),
592 ],
593 ),
594 )
595 api.register_mapped_type(
596 TypeMapping(
597 OctalMode,
598 str,
599 lambda v, ap, _: OctalMode.parse_filesystem_mode(v, ap),
600 ).with_mapper_as_lint_validator(),
601 reference_documentation=type_mapping_reference_documentation(
602 description="A file system mode using the octal mode representation. Must always be a provided as a string (that is, quoted).",
603 examples=[
604 type_mapping_example("0644"),
605 type_mapping_example("0755"),
606 ],
607 ),
608 )
609 api.register_mapped_type(
610 TypeMapping(
611 BuildEnvironmentDefinition,
612 str,
613 lambda v, ap, pc: assume_not_none(pc).resolve_build_environment(v, ap),
614 ),
615 reference_documentation=type_mapping_reference_documentation(
616 description="Reference to a build environment defined in `build-environments`",
617 ),
618 )
620 def _parse_version(
621 unparsed_version: str,
622 attribute_path: AttributePath,
623 _pc: ParserContextData | None,
624 ) -> debian.debian_support.Version:
625 try:
626 return debian.debian_support.Version(unparsed_version)
627 except (ValueError, TypeError) as e:
628 raise ManifestParseException(
629 f"Could not parse {unparsed_version!r} at {attribute_path.path} as a Debian version string: {e}"
630 )
632 api.register_mapped_type(
633 TypeMapping(
634 debian.debian_support.Version,
635 str,
636 _parse_version,
637 ).with_mapper_as_lint_validator(),
638 reference_documentation=type_mapping_reference_documentation(
639 description="A Debian version such as `2.3-1~bpo13+1`",
640 examples=[
641 type_mapping_example("2.3-1~bpo13+1"),
642 ],
643 ),
644 )
646 def _parse_maintscript_name(
647 unparsed_name: str,
648 attribute_path: AttributePath,
649 pc: ParserContextData | None,
650 ) -> MaintscriptForBinary:
651 is_udeb = assume_not_none(
652 pc
653 ).current_binary_package_state.binary_package.is_udeb
654 supported_names = (
655 SUPPORTED_UDEB_SCRIPTS if is_udeb else DPKG_DEB_CONTROL_SCRIPTS
656 )
657 if unparsed_name in supported_names:
658 return MaintscriptForBinary(unparsed_name)
659 supported_names = ", ".join(sorted(supported_names))
660 if unparsed_name in ALL_CONTROL_SCRIPTS:
661 raise ManifestParseException(
662 f"The maintscript name {unparsed_name!r} at {attribute_path.path} is a valid name"
663 f" but not supported here. Supported values are: {supported_names} "
664 )
665 raise ManifestParseException(
666 f"Unknown (or unsupported) maintscript name {unparsed_name!r} at {attribute_path.path}."
667 f" Supported values are: {supported_names}"
668 )
670 api.register_mapped_type(
671 TypeMapping(
672 MaintscriptForBinary,
673 str,
674 _parse_maintscript_name,
675 ),
676 reference_documentation=type_mapping_reference_documentation(
677 description="Name of maintscript such as `postinst` relevant for the binary package",
678 ),
679 )
682def register_service_managers(
683 api: DebputyPluginInitializerProvider,
684) -> None:
685 api.service_provider(
686 "systemd",
687 detect_systemd_service_files,
688 generate_snippets_for_systemd_units,
689 )
690 api.service_provider(
691 "sysvinit",
692 detect_sysv_init_service_files,
693 generate_snippets_for_init_scripts,
694 )
697def register_automatic_discard_rules(
698 api: DebputyPluginInitializerProvider,
699) -> None:
700 api.automatic_discard_rule(
701 "python-cache-files",
702 _debputy_discard_pyc_files,
703 rule_reference_documentation="Discards any *.pyc, *.pyo files and any __pycache__ directories",
704 examples=automatic_discard_rule_example(
705 (".../foo.py", False),
706 ".../__pycache__/",
707 ".../__pycache__/...",
708 ".../foo.pyc",
709 ".../foo.pyo",
710 ),
711 )
712 api.automatic_discard_rule(
713 "la-files",
714 _debputy_prune_la_files,
715 rule_reference_documentation="Discards any file with the extension .la beneath the directory /usr/lib",
716 examples=automatic_discard_rule_example(
717 "usr/lib/libfoo.la",
718 ("usr/lib/libfoo.so.1.0.0", False),
719 ),
720 )
721 api.automatic_discard_rule(
722 "backup-files",
723 _debputy_prune_backup_files,
724 rule_reference_documentation="Discards common back up files such as foo~, foo.bak or foo.orig",
725 examples=(
726 automatic_discard_rule_example(
727 ".../foo~",
728 ".../foo.orig",
729 ".../foo.rej",
730 ".../DEADJOE",
731 ".../.foo.sw.",
732 ),
733 ),
734 )
735 api.automatic_discard_rule(
736 "version-control-paths",
737 _debputy_prune_vcs_paths,
738 rule_reference_documentation="Discards common version control paths such as .git, .gitignore, CVS, etc.",
739 examples=automatic_discard_rule_example(
740 ("tools/foo", False),
741 ".../CVS/",
742 ".../CVS/...",
743 ".../.gitignore",
744 ".../.gitattributes",
745 ".../.git/",
746 ".../.git/...",
747 ),
748 )
749 api.automatic_discard_rule(
750 "gnu-info-dir-file",
751 _debputy_prune_info_dir_file,
752 rule_reference_documentation="Discards the /usr/share/info/dir file (causes package file conflicts)",
753 examples=automatic_discard_rule_example(
754 "usr/share/info/dir",
755 ("usr/share/info/foo.info", False),
756 ("usr/share/info/dir.info", False),
757 ("usr/share/random/case/dir", False),
758 ),
759 )
760 api.automatic_discard_rule(
761 "debian-dir",
762 _debputy_prune_binary_debian_dir,
763 rule_reference_documentation="(Implementation detail) Discards any DEBIAN directory to avoid it from appearing"
764 " literally in the file listing",
765 examples=(
766 automatic_discard_rule_example(
767 "DEBIAN/",
768 "DEBIAN/control",
769 ("usr/bin/foo", False),
770 ("usr/share/DEBIAN/foo", False),
771 ),
772 ),
773 )
774 api.automatic_discard_rule(
775 "doxygen-cruft-files",
776 _debputy_prune_doxygen_cruft,
777 rule_reference_documentation="Discards cruft files generated by doxygen",
778 examples=automatic_discard_rule_example(
779 ("usr/share/doc/foo/api/doxygen.css", False),
780 ("usr/share/doc/foo/api/doxygen.svg", False),
781 ("usr/share/doc/foo/api/index.html", False),
782 "usr/share/doc/foo/api/.../cruft.map",
783 "usr/share/doc/foo/api/.../cruft.md5",
784 ),
785 )
788def register_processing_steps(api: DebputyPluginInitializerProvider) -> None:
789 api.package_processor("manpages", process_manpages)
790 api.package_processor("clean-la-files", clean_la_files)
791 # strip-non-determinism makes assumptions about the PackageProcessingContext implementation
792 api.package_processor(
793 "strip-nondeterminism",
794 cast("Any", strip_non_determinism),
795 depends_on_processor=["manpages"],
796 )
797 api.package_processor(
798 "compression",
799 apply_compression,
800 depends_on_processor=["manpages", "strip-nondeterminism"],
801 )
804def register_variables_via_private_api(api: DebputyPluginInitializerProvider) -> None:
805 api.manifest_variable_provider(
806 load_source_variables,
807 {
808 "DEB_SOURCE": "Name of the source package (`dpkg-parsechangelog -SSource`)",
809 "DEB_VERSION": "Version from the top most changelog entry (`dpkg-parsechangelog -SVersion`)",
810 "DEB_VERSION_EPOCH_UPSTREAM": "Version from the top most changelog entry *without* the Debian revision",
811 "DEB_VERSION_UPSTREAM_REVISION": "Version from the top most changelog entry *without* the epoch",
812 "DEB_VERSION_UPSTREAM": "Upstream version from the top most changelog entry (that is, *without* epoch and Debian revision)",
813 "SOURCE_DATE_EPOCH": textwrap.dedent("""\
814 Timestamp from the top most changelog entry (`dpkg-parsechangelog -STimestamp`)
815 Please see <https://reproducible-builds.org/docs/source-date-epoch/> for the full definition of
816 this variable.
817 """),
818 "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": None,
819 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": None,
820 },
821 )
824def document_builtin_variables(api: DebputyPluginInitializerProvider) -> None:
825 api.document_builtin_variable(
826 "PACKAGE",
827 "Name of the binary package (only available in binary context)",
828 is_context_specific=True,
829 )
831 arch_types = _DOCUMENTED_DPKG_ARCH_TYPES
833 for arch_type, (arch_type_tag, arch_type_doc) in arch_types.items():
834 for arch_var, arch_var_doc in _DOCUMENTED_DPKG_ARCH_VARS.items():
835 full_var = f"DEB_{arch_type}_{arch_var}"
836 documentation = textwrap.dedent(f"""\
837 {arch_var_doc} ({arch_type_tag})
838 This variable describes machine information used when the package is compiled and assembled.
839 * Machine type: {arch_type_doc}
840 * Value description: {arch_var_doc}
842 The value is the output of: `dpkg-architecture -q{full_var}`
843 """)
844 api.document_builtin_variable(
845 full_var,
846 documentation,
847 is_for_special_case=arch_type != "HOST",
848 )
851def _format_docbase_filename(
852 path_format: str,
853 format_param: PPFFormatParam,
854 docbase_file: VirtualPath,
855) -> str:
856 with docbase_file.open() as fd:
857 content = Deb822(fd)
858 proper_name = content["Document"]
859 if proper_name is not None: 859 ↛ 862line 859 didn't jump to line 862 because the condition on line 859 was always true
860 format_param["name"] = proper_name
861 else:
862 _warn(
863 f"The docbase file {docbase_file.fs_path} is missing the Document field"
864 )
865 return path_format.format(**format_param)
868def register_special_ppfs(api: DebputyPluginInitializerProvider) -> None:
869 api.packager_provided_file(
870 "doc-base",
871 "/usr/share/doc-base/{owning_package}.{name}",
872 format_callback=_format_docbase_filename,
873 )
875 api.packager_provided_file(
876 "shlibs",
877 "DEBIAN/shlibs",
878 allow_name_segment=False,
879 reservation_only=True,
880 reference_documentation=packager_provided_file_reference_documentation(
881 format_documentation_uris=["man:deb-shlibs(5)"],
882 ),
883 )
884 api.packager_provided_file(
885 "symbols",
886 "DEBIAN/symbols",
887 allow_name_segment=False,
888 allow_architecture_segment=True,
889 reservation_only=True,
890 reference_documentation=packager_provided_file_reference_documentation(
891 format_documentation_uris=["man:deb-symbols(5)"],
892 ),
893 )
894 api.packager_provided_file(
895 "conffiles",
896 "DEBIAN/conffiles",
897 allow_name_segment=False,
898 allow_architecture_segment=True,
899 reservation_only=True,
900 )
901 api.packager_provided_file(
902 "templates",
903 "DEBIAN/templates",
904 allow_name_segment=False,
905 allow_architecture_segment=False,
906 reservation_only=True,
907 )
908 api.packager_provided_file(
909 "alternatives",
910 "DEBIAN/alternatives",
911 allow_name_segment=False,
912 allow_architecture_segment=True,
913 reservation_only=True,
914 )
917def register_install_rules(api: DebputyPluginInitializerProvider) -> None:
918 api.pluggable_manifest_rule(
919 InstallRule,
920 MK_INSTALLATIONS_INSTALL,
921 ParsedInstallRule,
922 _install_rule_handler,
923 source_format=_with_alt_form(ParsedInstallRuleSourceFormat),
924 inline_reference_documentation=reference_documentation(
925 title="Generic install (`install`)",
926 description=textwrap.dedent("""\
927 The generic `install` rule can be used to install arbitrary paths into packages
928 and is *similar* to how `dh_install` from debhelper works. It is a two "primary" uses.
930 1) The classic "install into directory" similar to the standard `dh_install`
931 2) The "install as" similar to `dh-exec`'s `foo => bar` feature.
933 The `install` rule installs a path exactly once into each package it acts on. In
934 the rare case that you want to install the same source *multiple* times into the
935 *same* packages, please have a look at `{MULTI_DEST_INSTALL}`.
936 """.format(MULTI_DEST_INSTALL=MK_INSTALLATIONS_MULTI_DEST_INSTALL)),
937 non_mapping_description=textwrap.dedent("""\
938 When the input is a string or a list of string, then that value is used as shorthand
939 for `source` or `sources` (respectively). This form can only be used when `into` is
940 not required.
941 """),
942 attributes=[
943 documented_attr(
944 ["source", "sources"],
945 textwrap.dedent("""\
946 A path match (`source`) or a list of path matches (`sources`) defining the
947 source path(s) to be installed. The path match(es) can use globs. Each match
948 is tried against default search directories.
949 - When a symlink is matched, then the symlink (not its target) is installed
950 as-is. When a directory is matched, then the directory is installed along
951 with all the contents that have not already been installed somewhere.
952 """),
953 ),
954 documented_attr(
955 "dest_dir",
956 textwrap.dedent("""\
957 A path defining the destination *directory*. The value *cannot* use globs, but can
958 use substitution. If neither `as` nor `dest-dir` is given, then `dest-dir` defaults
959 to the directory name of the `source`.
960 """),
961 ),
962 documented_attr(
963 "into",
964 textwrap.dedent("""\
965 Either a package name or a list of package names for which these paths should be
966 installed. This key is conditional on whether there are multiple binary packages listed
967 in `debian/control`. When there is only one binary package, then that binary is the
968 default for `into`. Otherwise, the key is required.
969 """),
970 ),
971 documented_attr(
972 "install_as",
973 textwrap.dedent("""\
974 A path defining the path to install the source as. This is a full path. This option
975 is mutually exclusive with `dest-dir` and `sources` (but not `source`). When `as` is
976 given, then `source` must match exactly one "not yet matched" path.
977 """),
978 ),
979 *docs_from(DebputyParsedContentStandardConditional),
980 ],
981 reference_documentation_url=manifest_format_doc("generic-install-install"),
982 ),
983 )
984 api.pluggable_manifest_rule(
985 InstallRule,
986 [
987 MK_INSTALLATIONS_INSTALL_DOCS,
988 "install-doc",
989 ],
990 ParsedInstallRule,
991 _install_docs_rule_handler,
992 source_format=_with_alt_form(ParsedInstallDocRuleSourceFormat),
993 inline_reference_documentation=reference_documentation(
994 title="Install documentation (`install-docs`)",
995 description=textwrap.dedent("""\
996 This install rule resemble that of `dh_installdocs`. It is a shorthand over the generic
997 `install` rule with the following key features:
999 1) The default `dest-dir` is to use the package's documentation directory (usually something
1000 like `/usr/share/doc/{{PACKAGE}}`, though it respects the "main documentation package"
1001 recommendation from Debian Policy). The `dest-dir` or `as` can be set in case the
1002 documentation in question goes into another directory or with a concrete path. In this
1003 case, it is still "better" than `install` due to the remaining benefits.
1004 2) The rule comes with pre-defined conditional logic for skipping the rule under
1005 `DEB_BUILD_OPTIONS=nodoc`, so you do not have to write that conditional yourself.
1006 3) The `into` parameter can be omitted as long as there is a exactly one non-`udeb`
1007 package listed in `debian/control`.
1009 With these two things in mind, it behaves just like the `install` rule.
1011 Note: It is often worth considering to use a more specialized version of the `install-docs`
1012 rule when one such is available. If you are looking to install an example or a man page,
1013 consider whether `install-examples` or `install-man` might be a better fit for your
1014 use-case.
1015 """),
1016 non_mapping_description=textwrap.dedent("""\
1017 When the input is a string or a list of string, then that value is used as shorthand
1018 for `source` or `sources` (respectively). This form can only be used when `into` is
1019 not required.
1020 """),
1021 attributes=[
1022 documented_attr(
1023 ["source", "sources"],
1024 textwrap.dedent("""\
1025 A path match (`source`) or a list of path matches (`sources`) defining the
1026 source path(s) to be installed. The path match(es) can use globs. Each match
1027 is tried against default search directories.
1028 - When a symlink is matched, then the symlink (not its target) is installed
1029 as-is. When a directory is matched, then the directory is installed along
1030 with all the contents that have not already been installed somewhere.
1032 - **CAVEAT**: Specifying `source: examples` where `examples` resolves to a
1033 directory for `install-examples` will give you an `examples/examples`
1034 directory in the package, which is rarely what you want. Often, you
1035 can solve this by using `examples/*` instead. Similar for `install-docs`
1036 and a `doc` or `docs` directory.
1037 """),
1038 ),
1039 documented_attr(
1040 "dest_dir",
1041 textwrap.dedent("""\
1042 A path defining the destination *directory*. The value *cannot* use globs, but can
1043 use substitution. If neither `as` nor `dest-dir` is given, then `dest-dir` defaults
1044 to the relevant package documentation directory (a la `/usr/share/doc/{{PACKAGE}}`).
1045 """),
1046 ),
1047 documented_attr(
1048 "into",
1049 textwrap.dedent("""\
1050 Either a package name or a list of package names for which these paths should be
1051 installed as documentation. This key is conditional on whether there are multiple
1052 (non-`udeb`) binary packages listed in `debian/control`. When there is only one
1053 (non-`udeb`) binary package, then that binary is the default for `into`. Otherwise,
1054 the key is required.
1055 """),
1056 ),
1057 documented_attr(
1058 "install_as",
1059 textwrap.dedent("""\
1060 A path defining the path to install the source as. This is a full path. This option
1061 is mutually exclusive with `dest-dir` and `sources` (but not `source`). When `as` is
1062 given, then `source` must match exactly one "not yet matched" path.
1063 """),
1064 ),
1065 documented_attr(
1066 "when",
1067 textwrap.dedent("""\
1068 A condition as defined in [Conditional rules](${MANIFEST_FORMAT_DOC}#conditional-rules).
1069 This condition will be combined with the built-in condition provided by these rules
1070 (rather than replacing it).
1071 """),
1072 ),
1073 ],
1074 reference_documentation_url=manifest_format_doc(
1075 "install-documentation-install-docs"
1076 ),
1077 ),
1078 )
1079 api.pluggable_manifest_rule(
1080 InstallRule,
1081 [
1082 MK_INSTALLATIONS_INSTALL_EXAMPLES,
1083 "install-example",
1084 ],
1085 ParsedInstallExamplesRule,
1086 _install_examples_rule_handler,
1087 source_format=_with_alt_form(ParsedInstallExamplesRuleSourceFormat),
1088 inline_reference_documentation=reference_documentation(
1089 title="Install examples (`install-examples`)",
1090 description=textwrap.dedent("""\
1091 This install rule resemble that of `dh_installexamples`. It is a shorthand over the generic `
1092 install` rule with the following key features:
1094 1) It pre-defines the `dest-dir` that respects the "main documentation package" recommendation from
1095 Debian Policy. The `install-examples` will use the `examples` subdir for the package documentation
1096 dir.
1097 2) The rule comes with pre-defined conditional logic for skipping the rule under
1098 `DEB_BUILD_OPTIONS=nodoc`, so you do not have to write that conditional yourself.
1099 3) The `into` parameter can be omitted as long as there is a exactly one non-`udeb`
1100 package listed in `debian/control`.
1102 With these two things in mind, it behaves just like the `install` rule.
1103 """),
1104 non_mapping_description=textwrap.dedent("""\
1105 When the input is a string or a list of string, then that value is used as shorthand
1106 for `source` or `sources` (respectively). This form can only be used when `into` is
1107 not required.
1108 """),
1109 attributes=[
1110 documented_attr(
1111 ["source", "sources"],
1112 textwrap.dedent("""\
1113 A path match (`source`) or a list of path matches (`sources`) defining the
1114 source path(s) to be installed. The path match(es) can use globs. Each match
1115 is tried against default search directories.
1116 - When a symlink is matched, then the symlink (not its target) is installed
1117 as-is. When a directory is matched, then the directory is installed along
1118 with all the contents that have not already been installed somewhere.
1120 - **CAVEAT**: Specifying `source: examples` where `examples` resolves to a
1121 directory for `install-examples` will give you an `examples/examples`
1122 directory in the package, which is rarely what you want. Often, you
1123 can solve this by using `examples/*` instead. Similar for `install-docs`
1124 and a `doc` or `docs` directory.
1125 """),
1126 ),
1127 documented_attr(
1128 "into",
1129 textwrap.dedent("""\
1130 Either a package name or a list of package names for which these paths should be
1131 installed as examples. This key is conditional on whether there are (non-`udeb`)
1132 multiple binary packages listed in `debian/control`. When there is only one
1133 (non-`udeb`) binary package, then that binary is the default for `into`.
1134 Otherwise, the key is required.
1135 """),
1136 ),
1137 documented_attr(
1138 "when",
1139 textwrap.dedent("""\
1140 A condition as defined in [Conditional rules](${MANIFEST_FORMAT_DOC}#conditional-rules).
1141 This condition will be combined with the built-in condition provided by these rules
1142 (rather than replacing it).
1143 """),
1144 ),
1145 ],
1146 reference_documentation_url=manifest_format_doc(
1147 "install-examples-install-examples"
1148 ),
1149 ),
1150 )
1151 api.pluggable_manifest_rule(
1152 InstallRule,
1153 MK_INSTALLATIONS_INSTALL_MAN,
1154 ParsedInstallManpageRule,
1155 _install_man_rule_handler,
1156 source_format=_with_alt_form(ParsedInstallManpageRuleSourceFormat),
1157 inline_reference_documentation=reference_documentation(
1158 title="Install man pages (`install-man`)",
1159 description=textwrap.dedent("""\
1160 Install rule for installing man pages similar to `dh_installman`. It is a shorthand
1161 over the generic `install` rule with the following key features:
1163 1) The rule can only match files (notably, symlinks cannot be matched by this rule).
1164 2) The `dest-dir` is computed per source file based on the man page's section and
1165 language.
1166 3) The `into` parameter can be omitted as long as there is a exactly one non-`udeb`
1167 package listed in `debian/control`.
1168 4) The rule comes with man page specific attributes such as `language` and `section`
1169 for when the auto-detection is insufficient.
1170 5) The rule comes with pre-defined conditional logic for skipping the rule under
1171 `DEB_BUILD_OPTIONS=nodoc`, so you do not have to write that conditional yourself.
1173 With these things in mind, the rule behaves similar to the `install` rule.
1174 """),
1175 non_mapping_description=textwrap.dedent("""\
1176 When the input is a string or a list of string, then that value is used as shorthand
1177 for `source` or `sources` (respectively). This form can only be used when `into` is
1178 not required.
1179 """),
1180 attributes=[
1181 documented_attr(
1182 ["source", "sources"],
1183 textwrap.dedent("""\
1184 A path match (`source`) or a list of path matches (`sources`) defining the
1185 source path(s) to be installed. The path match(es) can use globs. Each match
1186 is tried against default search directories.
1187 - When a symlink is matched, then the symlink (not its target) is installed
1188 as-is. When a directory is matched, then the directory is installed along
1189 with all the contents that have not already been installed somewhere.
1190 """),
1191 ),
1192 documented_attr(
1193 "into",
1194 textwrap.dedent("""\
1195 Either a package name or a list of package names for which these paths should be
1196 installed as man pages. This key is conditional on whether there are multiple (non-`udeb`)
1197 binary packages listed in `debian/control`. When there is only one (non-`udeb`) binary
1198 package, then that binary is the default for `into`. Otherwise, the key is required.
1199 """),
1200 ),
1201 documented_attr(
1202 "section",
1203 textwrap.dedent("""\
1204 If provided, it must be an integer between 1 and 9 (both inclusive), defining the
1205 section the man pages belong overriding any auto-detection that `debputy` would
1206 have performed.
1207 """),
1208 ),
1209 documented_attr(
1210 "language",
1211 textwrap.dedent("""\
1212 If provided, it must be either a 2 letter language code (such as `de`), a 5 letter
1213 language + dialect code (such as `pt_BR`), or one of the special keywords `C`,
1214 `derive-from-path`, or `derive-from-basename`. The default is `derive-from-path`.
1215 - When `language` is `C`, then the man pages are assumed to be "untranslated".
1216 - When `language` is a language code (with or without dialect), then all man pages
1217 matched will be assumed to be translated to that concrete language / dialect.
1218 - When `language` is `derive-from-path`, then `debputy` attempts to derive the
1219 language from the path (`man/<language>/man<section>`). This matches the
1220 default of `dh_installman`. When no language can be found for a given source,
1221 `debputy` behaves like language was `C`.
1222 - When `language` is `derive-from-basename`, then `debputy` attempts to derive
1223 the language from the basename (`foo.<language>.1`) similar to `dh_installman`
1224 previous default. When no language can be found for a given source, `debputy`
1225 behaves like language was `C`. Note this is prone to false positives where
1226 `.pl`, `.so` or similar two-letter extensions gets mistaken for a language code
1227 (`.pl` can both be "Polish" or "Perl Script", `.so` can both be "Somali" and
1228 "Shared Object" documentation). In this configuration, such extensions are
1229 always assumed to be a language.
1230 """),
1231 ),
1232 *docs_from(DebputyParsedContentStandardConditional),
1233 ],
1234 reference_documentation_url=manifest_format_doc(
1235 "install-manpages-install-man"
1236 ),
1237 ),
1238 )
1239 api.pluggable_manifest_rule(
1240 InstallRule,
1241 MK_INSTALLATIONS_DISCARD,
1242 ParsedInstallDiscardRule,
1243 _install_discard_rule_handler,
1244 source_format=_with_alt_form(ParsedInstallDiscardRuleSourceFormat),
1245 inline_reference_documentation=reference_documentation(
1246 title="Discard (or exclude) upstream provided paths (`discard`)",
1247 description=textwrap.dedent("""\
1248 When installing paths from `debian/tmp` into packages, it might be useful to ignore
1249 some paths that you never need installed. This can be done with the `discard` rule.
1251 Once a path is discarded, it cannot be matched by any other install rules. A path
1252 that is discarded, is considered handled when `debputy` checks for paths you might
1253 have forgotten to install. The `discard` feature therefore *also* replaces the
1254 `debian/not-installed` file used by `debhelper` and `cdbs`.
1255 """),
1256 non_mapping_description=textwrap.dedent("""\
1257 When the input is a string or a list of string, then that value is used as shorthand
1258 for `path` or `paths` (respectively).
1259 """),
1260 attributes=[
1261 documented_attr(
1262 ["path", "paths"],
1263 textwrap.dedent("""\
1264 A path match (`path`) or a list of path matches (`paths`) defining the source
1265 path(s) that should not be installed anywhere. The path match(es) can use globs.
1266 - When a symlink is matched, then the symlink (not its target) is discarded as-is.
1267 When a directory is matched, then the directory is discarded along with all the
1268 contents that have not already been installed somewhere.
1269 """),
1270 ),
1271 documented_attr(
1272 ["search_dir", "search_dirs"],
1273 textwrap.dedent("""\
1274 A path (`search-dir`) or a list to paths (`search-dirs`) that defines
1275 which search directories apply to. This attribute is primarily useful
1276 for source packages that uses "per package search dirs", and you want
1277 to restrict a discard rule to a subset of the relevant search dirs.
1278 Note all listed search directories must be either an explicit search
1279 requested by the packager or a search directory that `debputy`
1280 provided automatically (such as `debian/tmp`). Listing other paths
1281 will make `debputy` report an error.
1282 - Note that the `path` or `paths` must match at least one entry in
1283 any of the search directories unless *none* of the search directories
1284 exist (or the condition in `required-when` evaluates to false). When
1285 none of the search directories exist, the discard rule is silently
1286 skipped. This special-case enables you to have discard rules only
1287 applicable to certain builds that are only performed conditionally.
1288 """),
1289 ),
1290 documented_attr(
1291 "required_when",
1292 textwrap.dedent("""\
1293 A condition as defined in [Conditional rules](#conditional-rules). The discard
1294 rule is always applied. When the conditional is present and evaluates to false,
1295 the discard rule can silently match nothing.When the condition is absent, *or*
1296 it evaluates to true, then each pattern provided must match at least one path.
1297 """),
1298 ),
1299 ],
1300 reference_documentation_url=manifest_format_doc(
1301 "discard-or-exclude-upstream-provided-paths-discard"
1302 ),
1303 ),
1304 )
1305 api.pluggable_manifest_rule(
1306 InstallRule,
1307 MK_INSTALLATIONS_MULTI_DEST_INSTALL,
1308 ParsedMultiDestInstallRule,
1309 _multi_dest_install_rule_handler,
1310 source_format=ParsedMultiDestInstallRuleSourceFormat,
1311 inline_reference_documentation=reference_documentation(
1312 title=f"Multi destination install (`{MK_INSTALLATIONS_MULTI_DEST_INSTALL}`)",
1313 description=textwrap.dedent("""\
1314 The `${RULE_NAME}` is a variant of the generic `install` rule that installs sources
1315 into multiple destination paths. This is needed for the rare case where you want a
1316 path to be installed *twice* (or more) into the *same* package. The rule is a two
1317 "primary" uses.
1319 1) The classic "install into directory" similar to the standard `dh_install`,
1320 except you list 2+ destination directories.
1321 2) The "install as" similar to `dh-exec`'s `foo => bar` feature, except you list
1322 2+ `as` names.
1323 """),
1324 attributes=[
1325 documented_attr(
1326 ["source", "sources"],
1327 textwrap.dedent("""\
1328 A path match (`source`) or a list of path matches (`sources`) defining the
1329 source path(s) to be installed. The path match(es) can use globs. Each match
1330 is tried against default search directories.
1331 - When a symlink is matched, then the symlink (not its target) is installed
1332 as-is. When a directory is matched, then the directory is installed along
1333 with all the contents that have not already been installed somewhere.
1334 """),
1335 ),
1336 documented_attr(
1337 "dest_dirs",
1338 textwrap.dedent("""\
1339 A list of paths defining the destination *directories*. The value *cannot* use
1340 globs, but can use substitution. It is mutually exclusive with `as` but must be
1341 provided if `as` is not provided. The attribute must contain at least two paths
1342 (if you do not have two paths, you want `install`).
1343 """),
1344 ),
1345 documented_attr(
1346 "into",
1347 textwrap.dedent("""\
1348 Either a package name or a list of package names for which these paths should be
1349 installed. This key is conditional on whether there are multiple binary packages listed
1350 in `debian/control`. When there is only one binary package, then that binary is the
1351 default for `into`. Otherwise, the key is required.
1352 """),
1353 ),
1354 documented_attr(
1355 "install_as",
1356 textwrap.dedent("""\
1357 A list of paths, which defines all the places the source will be installed.
1358 Each path must be a full path without globs (but can use substitution).
1359 This option is mutually exclusive with `dest-dirs` and `sources` (but not
1360 `source`). When `as` is given, then `source` must match exactly one
1361 "not yet matched" path. The attribute must contain at least two paths
1362 (if you do not have two paths, you want `install`).
1363 """),
1364 ),
1365 *docs_from(DebputyParsedContentStandardConditional),
1366 ],
1367 reference_documentation_url=manifest_format_doc("generic-install-install"),
1368 ),
1369 )
1372def register_transformation_rules(api: DebputyPluginInitializerProvider) -> None:
1373 api.pluggable_manifest_rule(
1374 TransformationRule,
1375 "move",
1376 TransformationMoveRuleSpec,
1377 _transformation_move_handler,
1378 inline_reference_documentation=reference_documentation(
1379 title="Move transformation rule (`move`)",
1380 description=textwrap.dedent("""\
1381 The move transformation rule is mostly only useful for single binary source packages,
1382 where everything from upstream's build system is installed automatically into the package.
1383 In those case, you might find yourself with some files that need to be renamed to match
1384 Debian specific requirements.
1386 This can be done with the `move` transformation rule, which is a rough emulation of the
1387 `mv` command line tool.
1388 """),
1389 attributes=[
1390 documented_attr(
1391 "source",
1392 textwrap.dedent("""\
1393 A path match defining the source path(s) to be renamed. The value can use globs
1394 and substitutions.
1395 """),
1396 ),
1397 documented_attr(
1398 "target",
1399 textwrap.dedent("""\
1400 A path defining the target path. The value *cannot* use globs, but can use
1401 substitution. If the target ends with a literal `/` (prior to substitution),
1402 the target will *always* be a directory.
1403 """),
1404 ),
1405 *docs_from(DebputyParsedContentStandardConditional),
1406 ],
1407 reference_documentation_url=manifest_format_doc(
1408 "move-transformation-rule-move"
1409 ),
1410 ),
1411 )
1412 api.pluggable_manifest_rule(
1413 TransformationRule,
1414 "remove",
1415 TransformationRemoveRuleSpec,
1416 _transformation_remove_handler,
1417 source_format=_with_alt_form(TransformationRemoveRuleInputFormat),
1418 inline_reference_documentation=reference_documentation(
1419 title="Remove transformation rule (`remove`)",
1420 description=textwrap.dedent("""\
1421 The remove transformation rule is mostly only useful for single binary source packages,
1422 where everything from upstream's build system is installed automatically into the package.
1423 In those case, you might find yourself with some files that are _not_ relevant for the
1424 Debian package (but would be relevant for other distros or for non-distro local builds).
1425 Common examples include `INSTALL` files or `LICENSE` files (when they are just a subset
1426 of `debian/copyright`).
1428 In the manifest, you can ask `debputy` to remove paths from the debian package by using
1429 the `remove` transformation rule.
1431 Note that `remove` removes paths from future glob matches and transformation rules.
1432 """),
1433 non_mapping_description=textwrap.dedent("""\
1434 When the input is a string or a list of string, then that value is used as shorthand
1435 for `path` or `paths` (respectively).
1436 """),
1437 attributes=[
1438 documented_attr(
1439 ["path", "paths"],
1440 textwrap.dedent("""\
1441 A path match (`path`) or a list of path matches (`paths`) defining the
1442 path(s) inside the package that should be removed. The path match(es)
1443 can use globs.
1444 - When a symlink is matched, then the symlink (not its target) is removed
1445 as-is. When a directory is matched, then the directory is removed
1446 along with all the contents.
1447 """),
1448 ),
1449 documented_attr(
1450 "keep_empty_parent_dirs",
1451 textwrap.dedent("""\
1452 A boolean determining whether to prune parent directories that become
1453 empty as a consequence of this rule. When provided and `true`, this
1454 rule will leave empty directories behind. Otherwise, if this rule
1455 causes a directory to become empty that directory will be removed.
1456 """),
1457 ),
1458 documented_attr(
1459 "when",
1460 textwrap.dedent("""\
1461 A condition as defined in [Conditional rules](${MANIFEST_FORMAT_DOC}#conditional-rules).
1462 This condition will be combined with the built-in condition provided by these rules
1463 (rather than replacing it).
1464 """),
1465 ),
1466 ],
1467 reference_documentation_url=manifest_format_doc(
1468 "remove-transformation-rule-remove"
1469 ),
1470 ),
1471 )
1472 api.pluggable_manifest_rule(
1473 TransformationRule,
1474 "create-symlink",
1475 CreateSymlinkRule,
1476 _transformation_create_symlink,
1477 inline_reference_documentation=reference_documentation(
1478 title="Create symlinks transformation rule (`create-symlink`)",
1479 description=textwrap.dedent("""\
1480 Often, the upstream build system will provide the symlinks for you. However,
1481 in some cases, it is useful for the packager to define distribution specific
1482 symlinks. This can be done via the `create-symlink` transformation rule.
1483 """),
1484 attributes=[
1485 documented_attr(
1486 "path",
1487 textwrap.dedent("""\
1488 The path that should be a symlink. The path may contain substitution
1489 variables such as `{{DEB_HOST_MULTIARCH}}` but _cannot_ use globs.
1490 Parent directories are implicitly created as necessary.
1491 * Note that if `path` already exists, the behavior of this
1492 transformation depends on the value of `replacement-rule`.
1493 """),
1494 ),
1495 documented_attr(
1496 "target",
1497 textwrap.dedent("""\
1498 Where the symlink should point to. The target may contain substitution
1499 variables such as `{{DEB_HOST_MULTIARCH}}` but _cannot_ use globs.
1500 The link target is _not_ required to exist inside the package.
1501 * The `debputy` tool will normalize the target according to the rules
1502 of the Debian Policy. Use absolute or relative target at your own
1503 preference.
1504 """),
1505 ),
1506 documented_attr(
1507 "replacement_rule",
1508 textwrap.dedent("""\
1509 This attribute defines how to handle if `path` already exists. It can
1510 be set to one of the following values:
1511 - `error-if-exists`: When `path` already exists, `debputy` will
1512 stop with an error. This is similar to `ln -s` semantics.
1513 - `error-if-directory`: When `path` already exists, **and** it is
1514 a directory, `debputy` will stop with an error. Otherwise,
1515 remove the `path` first and then create the symlink. This is
1516 similar to `ln -sf` semantics.
1517 - `abort-on-non-empty-directory` (default): When `path` already
1518 exists, then it will be removed provided it is a non-directory
1519 **or** an *empty* directory and the symlink will then be
1520 created. If the path is a *non-empty* directory, `debputy`
1521 will stop with an error.
1522 - `discard-existing`: When `path` already exists, it will be
1523 removed. If the `path` is a directory, all its contents will
1524 be removed recursively along with the directory. Finally,
1525 the symlink is created. This is similar to having an explicit
1526 `remove` rule just prior to the `create-symlink` that is
1527 conditional on `path` existing (plus the condition defined in
1528 `when` if any).
1530 Keep in mind, that `replacement-rule` only applies if `path` exists.
1531 If the symlink cannot be created, because a part of `path` exist and
1532 is *not* a directory, then `create-symlink` will fail regardless of
1533 the value in `replacement-rule`.
1534 """),
1535 ),
1536 *docs_from(DebputyParsedContentStandardConditional),
1537 ],
1538 reference_documentation_url=manifest_format_doc(
1539 "create-symlinks-transformation-rule-create-symlink"
1540 ),
1541 ),
1542 )
1543 api.pluggable_manifest_rule(
1544 TransformationRule,
1545 "path-metadata",
1546 PathManifestRule,
1547 _transformation_path_metadata,
1548 source_format=PathManifestSourceDictFormat,
1549 inline_reference_documentation=reference_documentation(
1550 title="Change path owner/group or mode (`path-metadata`)",
1551 description=textwrap.dedent("""\
1552 The `debputy` command normalizes the path metadata (such as ownership and mode) similar
1553 to `dh_fixperms`. For most packages, the default is what you want. However, in some
1554 cases, the package has a special case or two that `debputy` does not cover. In that
1555 case, you can tell `debputy` to use the metadata you want by using the `path-metadata`
1556 transformation.
1558 Common use-cases include setuid/setgid binaries (such `usr/bin/sudo`) or/and static
1559 ownership (such as /usr/bin/write).
1560 """),
1561 attributes=[
1562 documented_attr(
1563 ["path", "paths"],
1564 textwrap.dedent("""\
1565 A path match (`path`) or a list of path matches (`paths`) defining the path(s)
1566 inside the package that should be affected. The path match(es) can use globs
1567 and substitution variables. Special-rules for matches:
1568 - Symlinks are never followed and will never be matched by this rule.
1569 - Directory handling depends on the `recursive` attribute.
1570 """),
1571 ),
1572 documented_attr(
1573 "owner",
1574 textwrap.dedent("""\
1575 Denotes the owner of the paths matched by `path` or `paths`. When omitted,
1576 no change of owner is done.
1577 """),
1578 ),
1579 documented_attr(
1580 "group",
1581 textwrap.dedent("""\
1582 Denotes the group of the paths matched by `path` or `paths`. When omitted,
1583 no change of group is done.
1584 """),
1585 ),
1586 documented_attr(
1587 "mode",
1588 textwrap.dedent("""\
1589 Denotes the mode of the paths matched by `path` or `paths`. When omitted,
1590 no change in mode is done. Note that numeric mode must always be given as
1591 a string (i.e., with quotes). Symbolic mode can be used as well. If
1592 symbolic mode uses a relative definition (e.g., `o-rx`), then it is
1593 relative to the matched path's current mode.
1594 """),
1595 ),
1596 documented_attr(
1597 "capabilities",
1598 textwrap.dedent("""\
1599 Denotes a Linux capability that should be applied to the path. When provided,
1600 `debputy` will cause the capability to be applied to all *files* denoted by
1601 the `path`/`paths` attribute on install (via `postinst configure`) provided
1602 that `setcap` is installed on the system when the `postinst configure` is
1603 run.
1604 - If any non-file paths are matched, the `capabilities` will *not* be applied
1605 to those paths.
1607 """),
1608 ),
1609 documented_attr(
1610 "capability_mode",
1611 textwrap.dedent("""\
1612 Denotes the mode to apply to the path *if* the Linux capability denoted in
1613 `capabilities` was successfully applied. If omitted, it defaults to `a-s` as
1614 generally capabilities are used to avoid "setuid"/"setgid" binaries. The
1615 `capability-mode` is relative to the *final* path mode (the mode of the path
1616 in the produced `.deb`). The `capability-mode` attribute cannot be used if
1617 `capabilities` is omitted.
1618 """),
1619 ),
1620 documented_attr(
1621 "recursive",
1622 textwrap.dedent("""\
1623 When a directory is matched, then the metadata changes are applied to the
1624 directory itself. When `recursive` is `true`, then the transformation is
1625 *also* applied to all paths beneath the directory. The default value for
1626 this attribute is `false`.
1627 """),
1628 ),
1629 *docs_from(DebputyParsedContentStandardConditional),
1630 ],
1631 reference_documentation_url=manifest_format_doc(
1632 "change-path-ownergroup-or-mode-path-metadata"
1633 ),
1634 ),
1635 )
1636 api.pluggable_manifest_rule(
1637 TransformationRule,
1638 "create-directories",
1639 EnsureDirectoryRule,
1640 _transformation_mkdirs,
1641 source_format=_with_alt_form(EnsureDirectorySourceFormat),
1642 inline_reference_documentation=reference_documentation(
1643 title="Create directories transformation rule (`create-directories`)",
1644 description=textwrap.dedent("""\
1645 NOTE: This transformation is only really needed if you need to create an empty
1646 directory somewhere in your package as an integration point. All `debputy`
1647 transformations will create directories as required.
1649 In most cases, upstream build systems and `debputy` will create all the relevant
1650 directories. However, in some rare cases you may want to explicitly define a path
1651 to be a directory. Maybe to silence a linter that is warning you about a directory
1652 being empty, or maybe you need an empty directory that nothing else is creating for
1653 you. This can be done via the `create-directories` transformation rule.
1655 Unless you have a specific need for the mapping form, you are recommended to use the
1656 shorthand form of just listing the directories you want created.
1657 """),
1658 non_mapping_description=textwrap.dedent("""\
1659 When the input is a string or a list of string, then that value is used as shorthand
1660 for `path` or `paths` (respectively).
1661 """),
1662 attributes=[
1663 documented_attr(
1664 ["path", "paths"],
1665 textwrap.dedent("""\
1666 A path (`path`) or a list of path (`paths`) defining the path(s) inside the
1667 package that should be created as directories. The path(es) _cannot_ use globs
1668 but can use substitution variables. Parent directories are implicitly created
1669 (with owner `root:root` and mode `0755` - only explicitly listed directories
1670 are affected by the owner/mode options)
1671 """),
1672 ),
1673 documented_attr(
1674 "owner",
1675 textwrap.dedent("""\
1676 Denotes the owner of the directory (but _not_ what is inside the directory).
1677 Default is "root".
1678 """),
1679 ),
1680 documented_attr(
1681 "group",
1682 textwrap.dedent("""\
1683 Denotes the group of the directory (but _not_ what is inside the directory).
1684 Default is "root".
1685 """),
1686 ),
1687 documented_attr(
1688 "mode",
1689 textwrap.dedent("""\
1690 Denotes the mode of the directory (but _not_ what is inside the directory).
1691 Note that numeric mode must always be given as a string (i.e., with quotes).
1692 Symbolic mode can be used as well. If symbolic mode uses a relative
1693 definition (e.g., `o-rx`), then it is relative to the directory's current mode
1694 (if it already exists) or `0755` if the directory is created by this
1695 transformation. The default is "0755".
1696 """),
1697 ),
1698 *docs_from(DebputyParsedContentStandardConditional),
1699 ],
1700 reference_documentation_url=manifest_format_doc(
1701 "create-directories-transformation-rule-directories"
1702 ),
1703 ),
1704 )
1707def register_manifest_condition_rules(api: DebputyPluginInitializerProvider) -> None:
1708 api.provide_manifest_keyword(
1709 ManifestCondition,
1710 "cross-compiling",
1711 lambda *_: ManifestCondition.is_cross_building(),
1712 )
1713 api.provide_manifest_keyword(
1714 ManifestCondition,
1715 "can-execute-compiled-binaries",
1716 lambda *_: ManifestCondition.can_execute_compiled_binaries(),
1717 )
1718 api.provide_manifest_keyword(
1719 ManifestCondition,
1720 "run-build-time-tests",
1721 lambda *_: ManifestCondition.run_build_time_tests(),
1722 )
1724 api.pluggable_manifest_rule(
1725 ManifestCondition,
1726 "not",
1727 MCNot,
1728 _mc_not,
1729 source_format=ManifestCondition,
1730 )
1731 api.pluggable_manifest_rule(
1732 ManifestCondition,
1733 ["any-of", "all-of"],
1734 MCAnyOfAllOf,
1735 _mc_any_of,
1736 source_format=list[ManifestCondition],
1737 )
1738 api.pluggable_manifest_rule(
1739 ManifestCondition,
1740 "arch-matches",
1741 MCArchMatches,
1742 _mc_arch_matches,
1743 source_format=str,
1744 inline_reference_documentation=reference_documentation(
1745 title="Architecture match condition `arch-matches`",
1746 description=textwrap.dedent("""\
1747 Sometimes, a rule needs to be conditional on the architecture.
1748 This can be done by using the `arch-matches` rule. In 99.99%
1749 of the cases, `arch-matches` will be form you are looking for
1750 and practically behaves like a comparison against
1751 `dpkg-architecture -qDEB_HOST_ARCH`.
1753 For the cross-compiling specialists or curious people: The
1754 `arch-matches` rule behaves like a `package-context-arch-matches`
1755 in the context of a binary package and like
1756 `source-context-arch-matches` otherwise. The details of those
1757 are covered in their own keywords.
1758 """),
1759 non_mapping_description=textwrap.dedent("""\
1760 The value must be a string in the form of a space separated list
1761 architecture names or architecture wildcards (same syntax as the
1762 architecture restriction in Build-Depends in debian/control except
1763 there is no enclosing `[]` brackets). The names/wildcards can
1764 optionally be prefixed by `!` to negate them. However, either
1765 *all* names / wildcards must have negation or *none* of them may
1766 have it.
1767 """),
1768 reference_documentation_url=manifest_format_doc(
1769 "architecture-match-condition-arch-matches-mapping"
1770 ),
1771 ),
1772 )
1774 context_arch_doc = reference_documentation(
1775 title="Explicit source or binary package context architecture match condition"
1776 " `source-context-arch-matches`, `package-context-arch-matches` (mapping)",
1777 description=textwrap.dedent("""\
1778 **These are special-case conditions**. Unless you know that you have a very special-case,
1779 you should probably use `arch-matches` instead. These conditions are aimed at people with
1780 corner-case special architecture needs. It also assumes the reader is familiar with the
1781 `arch-matches` condition.
1783 To understand these rules, here is a quick primer on `debputy`'s concept of "source context"
1784 vs "(binary) package context" architecture. For a native build, these two contexts are the
1785 same except that in the package context an `Architecture: all` package always resolve to
1786 `all` rather than `DEB_HOST_ARCH`. As a consequence, `debputy` forbids `arch-matches` and
1787 `package-context-arch-matches` in the context of an `Architecture: all` package as a warning
1788 to the packager that condition does not make sense.
1790 In the very rare case that you need an architecture condition for an `Architecture: all` package,
1791 you can use `source-context-arch-matches`. However, this means your `Architecture: all` package
1792 is not reproducible between different build hosts (which has known to be relevant for some
1793 very special cases).
1795 Additionally, for the 0.0001% case you are building a cross-compiling compiler (that is,
1796 `DEB_HOST_ARCH != DEB_TARGET_ARCH` and you are working with `gcc` or similar) `debputy` can be
1797 instructed (opt-in) to use `DEB_TARGET_ARCH` rather than `DEB_HOST_ARCH` for certain packages when
1798 evaluating an architecture condition in context of a binary package. This can be useful if the
1799 compiler produces supporting libraries that need to be built for the `DEB_TARGET_ARCH` rather than
1800 the `DEB_HOST_ARCH`. This is where `arch-matches` or `package-context-arch-matches` can differ
1801 subtly from `source-context-arch-matches` in how they evaluate the condition. This opt-in currently
1802 relies on setting `X-DH-Build-For-Type: target` for each of the relevant packages in
1803 `debian/control`. However, unless you are a cross-compiling specialist, you will probably never
1804 need to care about nor use any of this.
1806 Accordingly, the possible conditions are:
1808 * `arch-matches`: This is the form recommended to laymen and as the default use-case. This
1809 conditional acts `package-context-arch-matches` if the condition is used in the context
1810 of a binary package. Otherwise, it acts as `source-context-arch-matches`.
1812 * `source-context-arch-matches`: With this conditional, the provided architecture constraint is compared
1813 against the build time provided host architecture (`dpkg-architecture -qDEB_HOST_ARCH`). This can
1814 be useful when an `Architecture: all` package needs an architecture condition for some reason.
1816 * `package-context-arch-matches`: With this conditional, the provided architecture constraint is compared
1817 against the package's resolved architecture. This condition can only be used in the context of a binary
1818 package (usually, under `packages.<name>.`). If the package is an `Architecture: all` package, the
1819 condition will fail with an error as the condition always have the same outcome. For all other
1820 packages, the package's resolved architecture is the same as the build time provided host architecture
1821 (`dpkg-architecture -qDEB_HOST_ARCH`).
1823 - However, as noted above there is a special case for when compiling a cross-compiling compiler, where
1824 this behaves subtly different from `source-context-arch-matches`.
1826 All conditions are used the same way as `arch-matches`. Simply replace `arch-matches` with the other
1827 condition. See the `arch-matches` description for an example.
1828 """),
1829 non_mapping_description=textwrap.dedent("""\
1830 The value must be a string in the form of a space separated list
1831 architecture names or architecture wildcards (same syntax as the
1832 architecture restriction in Build-Depends in debian/control except
1833 there is no enclosing `[]` brackets). The names/wildcards can
1834 optionally be prefixed by `!` to negate them. However, either
1835 *all* names / wildcards must have negation or *none* of them may
1836 have it.
1837 """),
1838 )
1840 api.pluggable_manifest_rule(
1841 ManifestCondition,
1842 "source-context-arch-matches",
1843 MCArchMatches,
1844 _mc_source_context_arch_matches,
1845 source_format=str,
1846 inline_reference_documentation=context_arch_doc,
1847 )
1848 api.pluggable_manifest_rule(
1849 ManifestCondition,
1850 "package-context-arch-matches",
1851 MCArchMatches,
1852 _mc_arch_matches,
1853 source_format=str,
1854 inline_reference_documentation=context_arch_doc,
1855 )
1856 api.pluggable_manifest_rule(
1857 ManifestCondition,
1858 "build-profiles-matches",
1859 MCBuildProfileMatches,
1860 _mc_build_profile_matches,
1861 source_format=str,
1862 )
1865def register_maintscript_conditions(api: DebputyPluginInitializerProvider) -> None:
1866 api.provide_manifest_keyword(
1867 MaintscriptCondition,
1868 "purge",
1869 lambda *_: MaintscriptCondition.on_purge(),
1870 inline_reference_documentation=reference_documentation(
1871 title="When the package is being `purged` (`$RULE_NAME`)",
1872 synopsis="When the package is being purged",
1873 description=textwrap.dedent("""\
1874 The trigger is when `postrm` is run with its first argument
1875 being `purge`.
1876 """),
1877 ),
1878 )
1879 api.provide_manifest_keyword(
1880 MaintscriptCondition,
1881 "configure",
1882 lambda *_: MaintscriptCondition.on_configure(),
1883 inline_reference_documentation=reference_documentation(
1884 title="When the package is about to end up in the `configured` state (`$RULE_NAME`)",
1885 synopsis="When the package is about to end up in the configured state",
1886 description=textwrap.dedent("""\
1887 The trigger is when `postinst` is run and the package would end up in the `configured`
1888 state on success except the package being `triggered`.
1890 This covers *more* cases than a simple `[ "$$1" = "configure" ]` as there are some
1891 cases like `abort-deconfigure` where the package is expected to be fully operational
1892 at the end of the script. In practice, it is the same logic needed in all these cases.
1893 """),
1894 ),
1895 )
1896 api.provide_manifest_keyword(
1897 MaintscriptCondition,
1898 "initial-install",
1899 lambda *_: MaintscriptCondition.on_initial_install(),
1900 inline_reference_documentation=reference_documentation(
1901 title="When the package is being installed and not upgraded (`$RULE_NAME`)",
1902 synopsis="When the package is being installed (excluding upgrades)",
1903 description=textwrap.dedent("""\
1904 The trigger is when `postinst` is run with `configure` as its first argument,
1905 and `dpkg` provides no `old-version`. This is generally the first install,
1906 but it can also happen with a `install, remove + purge, install`.
1907 """),
1908 ),
1909 )
1910 api.pluggable_manifest_rule(
1911 MaintscriptCondition,
1912 "upgrade",
1913 UpgradeFromVersion,
1914 _parse_upgrade_from_version,
1915 as_keyword_handler=lambda *_: MaintscriptCondition.on_upgrade(),
1916 register_value=False,
1917 inline_reference_documentation=reference_documentation(
1918 title="When the package is being upgraded (`$RULE_NAME`)",
1919 synopsis="When the package is being upgraded from a previous version",
1920 description=textwrap.dedent("""\
1921 The trigger is when `postinst` is run with `configure` as its first argument,
1922 and `dpkg` provides an `old-version`.
1924 Can be used as a keyword to mean upgrade from any version:
1926 ```yaml
1927 maintscript-snippets:
1928 - on: upgrade
1929 snippet: run this on every upgrade
1930 ```
1932 Alternatively, a `from-version` can be given, at which point the code is only
1933 run when upgrading from that version or earlier.
1934 ```yaml
1935 maintscript-snippets:
1936 - on:
1937 upgrade:
1938 from-version: "0.7"
1939 snippet: run this on the first upgrade from <= 0.70
1940 ```
1941 """),
1942 attributes=[
1943 documented_attr(
1944 "from_version",
1945 textwrap.dedent("""\
1946 The latest version to upgrade from that should trigger the snippet.
1947 """),
1948 )
1949 ],
1950 ),
1951 )
1952 api.provide_manifest_keyword(
1953 MaintscriptCondition,
1954 "before-upgrade",
1955 lambda *_: MaintscriptCondition.on_before_upgrade(),
1956 inline_reference_documentation=reference_documentation(
1957 title="When the package is about to be upgraded (`$RULE_NAME`)",
1958 synopsis="When the package is about to be upgraded",
1959 description=textwrap.dedent("""\
1960 The trigger is when `preinst` is run with `upgrade` as its first argument.
1961 The second argument is the `old-version` and the third argument is `new-version`.
1962 """),
1963 ),
1964 )
1965 api.provide_manifest_keyword(
1966 MaintscriptCondition,
1967 "before-removal",
1968 lambda *_: MaintscriptCondition.on_before_removal(),
1969 inline_reference_documentation=reference_documentation(
1970 title="When the package is about to be removed (`$RULE_NAME`)",
1971 synopsis="When the package is about to be removed",
1972 description=textwrap.dedent("""\
1973 The trigger is when `prerm` is run with `remove` as its first argument.
1975 This is before any files are removed from the file system. Note that
1976 the package and its dependencies might be in the "Half-installed"
1977 state if this occurs after a failed upgrade. Therefore, some
1978 functionality of the package or its dependencies might not
1979 be present.
1981 This can be used to prevent the removal in some cases by having
1982 the snippet fail.
1983 """),
1984 ),
1985 )
1986 api.provide_manifest_keyword(
1987 MaintscriptCondition,
1988 "after-removal",
1989 lambda *_: MaintscriptCondition.on_after_removal(),
1990 inline_reference_documentation=reference_documentation(
1991 title="When the package has been removed (`$RULE_NAME`)",
1992 synopsis="When the package has been removed",
1993 description=textwrap.dedent("""\
1994 The trigger is when `postrm` is run with `remove` as its first argument.
1996 Most files will have been removed from the system and the package is now
1997 gone when the snippet is run. This can be used to clean up things that
1998 cannot be handled by the `clean-after-removal` feature.
2000 Note the snippet must only rely on `Essential: yes` packages as those
2001 are the only packages guaranteed to be present and *functional* at this
2002 time. If the snippet still needs to call commands from non-essential
2003 packages, then it must assume the command may fail in ways that cannot
2004 be detected ahead of time (the command existing is *not* a reliable
2005 indicator of it being able to run), and the snippet must then implement
2006 a reasonable fallback instead rather than failing the script entirely.
2008 The `conffiles` and other `purge`-only removed files (if any) might still
2009 remain.
2010 """),
2011 ),
2012 )
2013 api.pluggable_manifest_rule(
2014 MaintscriptCondition,
2015 "unconditionally-in-script",
2016 UnconditionallyInScript,
2017 _parse_unconditionally_in_script,
2018 source_format=str,
2019 register_value=False,
2020 inline_reference_documentation=reference_documentation(
2021 title="Unconditionally run something in a script (`$RULE_NAME`)",
2022 synopsis="Unconditionally run something in a script (or bring-your-own condition)",
2023 description=textwrap.dedent("""\
2024 Unconditionally insert a snippet in a given maintscript.
2026 This can be useful when the code needs to go into a given script
2027 and none of the other conditions apply. Remember to apply relevant
2028 guards or conditions in the snippet.
2030 Example:
2032 ```yaml
2033 maintscript-snippets:
2034 - on:
2035 unconditionally-in-script: postinst
2036 snippet: "run this unconditionally in the postinst script"
2037 ```
2038 """),
2039 ),
2040 )
2043def register_dpkg_conffile_rules(api: DebputyPluginInitializerProvider) -> None:
2044 api.pluggable_manifest_rule(
2045 DpkgMaintscriptHelperCommand,
2046 "remove",
2047 DpkgRemoveConffileRule,
2048 _dpkg_conffile_remove,
2049 inline_reference_documentation=None, # TODO: write and add
2050 )
2052 api.pluggable_manifest_rule(
2053 DpkgMaintscriptHelperCommand,
2054 "rename",
2055 DpkgRenameConffileRule,
2056 _dpkg_conffile_rename,
2057 inline_reference_documentation=None, # TODO: write and add
2058 )
2061class _ModeOwnerBase(DebputyParsedContentStandardConditional):
2062 mode: NotRequired[FileSystemMode]
2063 owner: NotRequired[StaticFileSystemOwner]
2064 group: NotRequired[StaticFileSystemGroup]
2067class PathManifestSourceDictFormat(_ModeOwnerBase):
2068 path: NotRequired[
2069 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("paths")]
2070 ]
2071 paths: NotRequired[list[FileSystemMatchRule]]
2072 recursive: NotRequired[bool]
2073 capabilities: NotRequired[Capability]
2074 capability_mode: NotRequired[FileSystemMode]
2077class PathManifestRule(_ModeOwnerBase):
2078 paths: list[FileSystemMatchRule]
2079 recursive: NotRequired[bool]
2080 capabilities: NotRequired[Capability]
2081 capability_mode: NotRequired[FileSystemMode]
2084class EnsureDirectorySourceFormat(_ModeOwnerBase):
2085 path: NotRequired[
2086 Annotated[FileSystemExactMatchRule, DebputyParseHint.target_attribute("paths")]
2087 ]
2088 paths: NotRequired[list[FileSystemExactMatchRule]]
2091class EnsureDirectoryRule(_ModeOwnerBase):
2092 paths: list[FileSystemExactMatchRule]
2095class CreateSymlinkRule(DebputyParsedContentStandardConditional):
2096 path: FileSystemExactMatchRule
2097 target: Annotated[SymlinkTarget, DebputyParseHint.not_path_error_hint()]
2098 replacement_rule: NotRequired[CreateSymlinkReplacementRule]
2101class TransformationMoveRuleSpec(DebputyParsedContentStandardConditional):
2102 source: FileSystemMatchRule
2103 target: FileSystemExactMatchRule
2106class TransformationRemoveRuleSpec(DebputyParsedContentStandardConditional):
2107 paths: list[FileSystemMatchRule]
2108 keep_empty_parent_dirs: NotRequired[bool]
2111class TransformationRemoveRuleInputFormat(DebputyParsedContentStandardConditional):
2112 path: NotRequired[
2113 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("paths")]
2114 ]
2115 paths: NotRequired[list[FileSystemMatchRule]]
2116 keep_empty_parent_dirs: NotRequired[bool]
2119class ParsedInstallRuleSourceFormat(DebputyParsedContentStandardConditional):
2120 sources: NotRequired[list[FileSystemMatchRule]]
2121 source: NotRequired[
2122 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
2123 ]
2124 into: NotRequired[
2125 Annotated[
2126 str | list[str],
2127 DebputyParseHint.required_when_multi_binary(),
2128 ]
2129 ]
2130 dest_dir: NotRequired[
2131 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()]
2132 ]
2133 install_as: NotRequired[
2134 Annotated[
2135 FileSystemExactMatchRule,
2136 DebputyParseHint.conflicts_with_source_attributes("sources", "dest_dir"),
2137 DebputyParseHint.manifest_attribute("as"),
2138 DebputyParseHint.not_path_error_hint(),
2139 ]
2140 ]
2143class ParsedInstallDocRuleSourceFormat(DebputyParsedContentStandardConditional):
2144 sources: NotRequired[list[FileSystemMatchRule]]
2145 source: NotRequired[
2146 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
2147 ]
2148 into: NotRequired[
2149 Annotated[
2150 str | list[str],
2151 DebputyParseHint.required_when_multi_binary(
2152 package_types=PackageTypeSelector.DEB
2153 ),
2154 ]
2155 ]
2156 dest_dir: NotRequired[
2157 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()]
2158 ]
2159 install_as: NotRequired[
2160 Annotated[
2161 FileSystemExactMatchRule,
2162 DebputyParseHint.conflicts_with_source_attributes("sources", "dest_dir"),
2163 DebputyParseHint.manifest_attribute("as"),
2164 DebputyParseHint.not_path_error_hint(),
2165 ]
2166 ]
2169class ParsedInstallRule(DebputyParsedContentStandardConditional):
2170 sources: list[FileSystemMatchRule]
2171 into: NotRequired[list[BinaryPackage]]
2172 dest_dir: NotRequired[FileSystemExactMatchRule]
2173 install_as: NotRequired[FileSystemExactMatchRule]
2176class ParsedMultiDestInstallRuleSourceFormat(DebputyParsedContentStandardConditional):
2177 sources: NotRequired[list[FileSystemMatchRule]]
2178 source: NotRequired[
2179 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
2180 ]
2181 into: NotRequired[
2182 Annotated[
2183 str | list[str],
2184 DebputyParseHint.required_when_multi_binary(),
2185 ]
2186 ]
2187 dest_dirs: NotRequired[
2188 Annotated[
2189 list[FileSystemExactMatchRule], DebputyParseHint.not_path_error_hint()
2190 ]
2191 ]
2192 install_as: NotRequired[
2193 Annotated[
2194 list[FileSystemExactMatchRule],
2195 DebputyParseHint.conflicts_with_source_attributes("sources", "dest_dirs"),
2196 DebputyParseHint.not_path_error_hint(),
2197 DebputyParseHint.manifest_attribute("as"),
2198 ]
2199 ]
2202class ParsedMultiDestInstallRule(DebputyParsedContentStandardConditional):
2203 sources: list[FileSystemMatchRule]
2204 into: NotRequired[list[BinaryPackage]]
2205 dest_dirs: NotRequired[list[FileSystemExactMatchRule]]
2206 install_as: NotRequired[list[FileSystemExactMatchRule]]
2209class ParsedInstallExamplesRule(DebputyParsedContentStandardConditional):
2210 sources: list[FileSystemMatchRule]
2211 into: NotRequired[list[BinaryPackage]]
2214class ParsedInstallExamplesRuleSourceFormat(DebputyParsedContentStandardConditional):
2215 sources: NotRequired[list[FileSystemMatchRule]]
2216 source: NotRequired[
2217 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
2218 ]
2219 into: NotRequired[
2220 Annotated[
2221 str | list[str],
2222 DebputyParseHint.required_when_multi_binary(
2223 package_types=PackageTypeSelector.DEB
2224 ),
2225 ]
2226 ]
2229class ParsedInstallManpageRule(DebputyParsedContentStandardConditional):
2230 sources: list[FileSystemMatchRule]
2231 language: NotRequired[str]
2232 section: NotRequired[int]
2233 into: NotRequired[list[BinaryPackage]]
2236class ParsedInstallManpageRuleSourceFormat(DebputyParsedContentStandardConditional):
2237 sources: NotRequired[list[FileSystemMatchRule]]
2238 source: NotRequired[
2239 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
2240 ]
2241 language: NotRequired[str]
2242 section: NotRequired[int]
2243 into: NotRequired[
2244 Annotated[
2245 str | list[str],
2246 DebputyParseHint.required_when_multi_binary(
2247 package_types=PackageTypeSelector.DEB
2248 ),
2249 ]
2250 ]
2253class ParsedInstallDiscardRuleSourceFormat(DebputyParsedContent):
2254 paths: NotRequired[list[FileSystemMatchRule]]
2255 path: NotRequired[
2256 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("paths")]
2257 ]
2258 search_dir: NotRequired[
2259 Annotated[
2260 FileSystemExactMatchRule, DebputyParseHint.target_attribute("search_dirs")
2261 ]
2262 ]
2263 search_dirs: NotRequired[list[FileSystemExactMatchRule]]
2264 required_when: NotRequired[ManifestCondition]
2267class ParsedInstallDiscardRule(DebputyParsedContent):
2268 paths: list[FileSystemMatchRule]
2269 search_dirs: NotRequired[list[FileSystemExactMatchRule]]
2270 required_when: NotRequired[ManifestCondition]
2273class DpkgConffileManagementRuleBase(DebputyParsedContent):
2274 prior_to_version: NotRequired[str]
2275 owning_package: NotRequired[str]
2278class DpkgRenameConffileRule(DpkgConffileManagementRuleBase):
2279 source: str
2280 target: str
2283class DpkgRemoveConffileRule(DpkgConffileManagementRuleBase):
2284 path: str
2287class MCAnyOfAllOf(DebputyParsedContent):
2288 conditions: list[ManifestCondition]
2291class MCNot(DebputyParsedContent):
2292 negated_condition: ManifestCondition
2295class MCArchMatches(DebputyParsedContent):
2296 arch_matches: str
2299class MCBuildProfileMatches(DebputyParsedContent):
2300 build_profile_matches: str
2303class UnconditionallyInScript(typing.TypedDict):
2304 script_name: MaintscriptForBinary
2307class UpgradeFromVersion(typing.TypedDict):
2308 from_version: debian.debian_support.Version
2311def _parse_filename(
2312 filename: str,
2313 attribute_path: AttributePath,
2314 *,
2315 allow_directories: bool = True,
2316) -> str:
2317 try:
2318 normalized_path = _normalize_path(filename, with_prefix=False)
2319 except ValueError as e:
2320 raise ManifestParseException(
2321 f'Error parsing the path "{filename}" defined in {attribute_path.path}: {e.args[0]}'
2322 ) from None
2323 if not allow_directories and filename.endswith("/"): 2323 ↛ 2324line 2323 didn't jump to line 2324 because the condition on line 2323 was never true
2324 raise ManifestParseException(
2325 f'The path "{filename}" in {attribute_path.path} ends with "/" implying it is a directory,'
2326 f" but this feature can only be used for files"
2327 )
2328 if normalized_path == ".": 2328 ↛ 2329line 2328 didn't jump to line 2329 because the condition on line 2328 was never true
2329 raise ManifestParseException(
2330 f'The path "{filename}" in {attribute_path.path} looks like the root directory,'
2331 f" but this feature does not allow the root directory here."
2332 )
2333 return normalized_path
2336def _with_alt_form(t: type[TypedDict]):
2337 return Union[
2338 t,
2339 list[str],
2340 str,
2341 ]
2344def _dpkg_conffile_rename(
2345 _name: str,
2346 parsed_data: DpkgRenameConffileRule,
2347 path: AttributePath,
2348 _context: ParserContextData,
2349) -> DpkgMaintscriptHelperCommand:
2350 source_file = parsed_data["source"]
2351 target_file = parsed_data["target"]
2352 normalized_source = _parse_filename(
2353 source_file,
2354 path["source"],
2355 allow_directories=False,
2356 )
2357 path.path_hint = source_file
2359 normalized_target = _parse_filename(
2360 target_file,
2361 path["target"],
2362 allow_directories=False,
2363 )
2364 normalized_source = "/" + normalized_source
2365 normalized_target = "/" + normalized_target
2367 if normalized_source == normalized_target: 2367 ↛ 2368line 2367 didn't jump to line 2368 because the condition on line 2367 was never true
2368 raise ManifestParseException(
2369 f"Invalid rename defined in {path.path}: The source and target path are the same!"
2370 )
2372 version, owning_package = _parse_conffile_prior_version_and_owning_package(
2373 parsed_data, path
2374 )
2375 return DpkgMaintscriptHelperCommand.mv_conffile(
2376 path,
2377 normalized_source,
2378 normalized_target,
2379 version,
2380 owning_package,
2381 )
2384def _dpkg_conffile_remove(
2385 _name: str,
2386 parsed_data: DpkgRemoveConffileRule,
2387 path: AttributePath,
2388 _context: ParserContextData,
2389) -> DpkgMaintscriptHelperCommand:
2390 source_file = parsed_data["path"]
2391 normalized_source = _parse_filename(
2392 source_file,
2393 path["path"],
2394 allow_directories=False,
2395 )
2396 path.path_hint = source_file
2398 normalized_source = "/" + normalized_source
2400 version, owning_package = _parse_conffile_prior_version_and_owning_package(
2401 parsed_data, path
2402 )
2403 return DpkgMaintscriptHelperCommand.rm_conffile(
2404 path,
2405 normalized_source,
2406 version,
2407 owning_package,
2408 )
2411def _parse_conffile_prior_version_and_owning_package(
2412 d: DpkgConffileManagementRuleBase,
2413 attribute_path: AttributePath,
2414) -> tuple[str | None, str | None]:
2415 prior_version = d.get("prior_to_version")
2416 owning_package = d.get("owning_package")
2418 if prior_version is not None and not PKGVERSION_REGEX.match(prior_version): 2418 ↛ 2419line 2418 didn't jump to line 2419 because the condition on line 2418 was never true
2419 p = attribute_path["prior_to_version"]
2420 raise ManifestParseException(
2421 f"The {MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION} parameter in {p.path} must be a"
2422 r" valid package version (i.e., match (?:\d+:)?\d[0-9A-Za-z.+:~]*(?:-[0-9A-Za-z.+:~]+)*)."
2423 )
2425 if owning_package is not None and not PKGNAME_REGEX.match(owning_package): 2425 ↛ 2426line 2425 didn't jump to line 2426 because the condition on line 2425 was never true
2426 p = attribute_path["owning_package"]
2427 raise ManifestParseException(
2428 f"The {MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE} parameter in {p.path} must be a valid"
2429 f" package name (i.e., match {PKGNAME_REGEX.pattern})."
2430 )
2432 return prior_version, owning_package
2435def _install_rule_handler(
2436 _name: str,
2437 parsed_data: ParsedInstallRule,
2438 path: AttributePath,
2439 context: ParserContextData,
2440) -> InstallRule:
2441 sources = parsed_data["sources"]
2442 install_as = parsed_data.get("install_as")
2443 into = frozenset(
2444 parsed_data.get("into")
2445 or (context.single_binary_package(path, package_attribute="into"),)
2446 )
2447 dest_dir = parsed_data.get("dest_dir")
2448 condition = parsed_data.get("when")
2449 if install_as is not None:
2450 assert len(sources) == 1
2451 assert dest_dir is None
2452 return InstallRule.install_as(
2453 sources[0],
2454 install_as.match_rule.path,
2455 into,
2456 path.path,
2457 condition,
2458 )
2459 return InstallRule.install_dest(
2460 sources,
2461 dest_dir.match_rule.path if dest_dir is not None else None,
2462 into,
2463 path.path,
2464 condition,
2465 )
2468def _multi_dest_install_rule_handler(
2469 _name: str,
2470 parsed_data: ParsedMultiDestInstallRule,
2471 path: AttributePath,
2472 context: ParserContextData,
2473) -> InstallRule:
2474 sources = parsed_data["sources"]
2475 install_as = parsed_data.get("install_as")
2476 into = frozenset(
2477 parsed_data.get("into")
2478 or (context.single_binary_package(path, package_attribute="into"),)
2479 )
2480 dest_dirs = parsed_data.get("dest_dirs")
2481 condition = parsed_data.get("when")
2482 if install_as is not None:
2483 assert len(sources) == 1
2484 assert dest_dirs is None
2485 if len(install_as) < 2: 2485 ↛ 2486line 2485 didn't jump to line 2486 because the condition on line 2485 was never true
2486 raise ManifestParseException(
2487 f"The {path['install_as'].path} attribute must contain at least two paths."
2488 )
2489 return InstallRule.install_multi_as(
2490 sources[0],
2491 [p.match_rule.path for p in install_as],
2492 into,
2493 path.path,
2494 condition,
2495 )
2496 if dest_dirs is None: 2496 ↛ 2497line 2496 didn't jump to line 2497 because the condition on line 2496 was never true
2497 raise ManifestParseException(
2498 f"Either the `as` or the `dest-dirs` key must be provided at {path.path}"
2499 )
2500 if len(dest_dirs) < 2: 2500 ↛ 2501line 2500 didn't jump to line 2501 because the condition on line 2500 was never true
2501 raise ManifestParseException(
2502 f"The {path['dest_dirs'].path} attribute must contain at least two paths."
2503 )
2504 return InstallRule.install_multi_dest(
2505 sources,
2506 [dd.match_rule.path for dd in dest_dirs],
2507 into,
2508 path.path,
2509 condition,
2510 )
2513def _install_docs_rule_handler(
2514 _name: str,
2515 parsed_data: ParsedInstallRule,
2516 path: AttributePath,
2517 context: ParserContextData,
2518) -> InstallRule:
2519 sources = parsed_data["sources"]
2520 install_as = parsed_data.get("install_as")
2521 dest_dir = parsed_data.get("dest_dir")
2522 condition = parsed_data.get("when")
2523 into = frozenset(
2524 parsed_data.get("into")
2525 or (
2526 context.single_binary_package(
2527 path,
2528 package_types=PackageTypeSelector.DEB,
2529 package_attribute="into",
2530 ),
2531 )
2532 )
2533 if install_as is not None: 2533 ↛ 2534line 2533 didn't jump to line 2534 because the condition on line 2533 was never true
2534 assert len(sources) == 1
2535 assert dest_dir is None
2536 return InstallRule.install_doc_as(
2537 sources[0],
2538 install_as.match_rule.path,
2539 into,
2540 path.path,
2541 condition,
2542 )
2543 return InstallRule.install_doc(
2544 sources,
2545 None if dest_dir is None else dest_dir.raw_match_rule,
2546 into,
2547 path.path,
2548 condition,
2549 )
2552def _install_examples_rule_handler(
2553 _name: str,
2554 parsed_data: ParsedInstallExamplesRule,
2555 path: AttributePath,
2556 context: ParserContextData,
2557) -> InstallRule:
2558 return InstallRule.install_examples(
2559 sources=parsed_data["sources"],
2560 into=frozenset(
2561 parsed_data.get("into")
2562 or (
2563 context.single_binary_package(
2564 path,
2565 package_types=PackageTypeSelector.DEB,
2566 package_attribute="into",
2567 ),
2568 )
2569 ),
2570 definition_source=path.path,
2571 condition=parsed_data.get("when"),
2572 )
2575def _install_man_rule_handler(
2576 _name: str,
2577 parsed_data: ParsedInstallManpageRule,
2578 attribute_path: AttributePath,
2579 context: ParserContextData,
2580) -> InstallRule:
2581 sources = parsed_data["sources"]
2582 language = parsed_data.get("language")
2583 section = parsed_data.get("section")
2585 if language is not None:
2586 is_lang_ok = language in (
2587 "C",
2588 "derive-from-basename",
2589 "derive-from-path",
2590 )
2592 if not is_lang_ok and len(language) == 2 and language.islower(): 2592 ↛ 2593line 2592 didn't jump to line 2593 because the condition on line 2592 was never true
2593 is_lang_ok = True
2595 if ( 2595 ↛ 2602line 2595 didn't jump to line 2602 because the condition on line 2595 was never true
2596 not is_lang_ok
2597 and len(language) == 5
2598 and language[2] == "_"
2599 and language[:2].islower()
2600 and language[3:].isupper()
2601 ):
2602 is_lang_ok = True
2604 if not is_lang_ok: 2604 ↛ 2605line 2604 didn't jump to line 2605 because the condition on line 2604 was never true
2605 raise ManifestParseException(
2606 f'The language attribute must in a 2-letter language code ("de"), a 5-letter language + dialect'
2607 f' code ("pt_BR"), "derive-from-basename", "derive-from-path", or omitted. The problematic'
2608 f' definition is {attribute_path["language"]}'
2609 )
2611 if section is not None and (section < 1 or section > 10): 2611 ↛ 2612line 2611 didn't jump to line 2612 because the condition on line 2611 was never true
2612 raise ManifestParseException(
2613 f"The section attribute must in the range [1-9] or omitted. The problematic definition is"
2614 f' {attribute_path["section"]}'
2615 )
2616 if section is None and any(s.raw_match_rule.endswith(".gz") for s in sources): 2616 ↛ 2617line 2616 didn't jump to line 2617 because the condition on line 2616 was never true
2617 raise ManifestParseException(
2618 "Sorry, compressed man pages are not supported without an explicit `section` definition at the moment."
2619 " This limitation may be removed in the future. Problematic definition from"
2620 f' {attribute_path["sources"]}'
2621 )
2622 if any(s.raw_match_rule.endswith("/") for s in sources): 2622 ↛ 2623line 2622 didn't jump to line 2623 because the condition on line 2622 was never true
2623 raise ManifestParseException(
2624 'The install-man rule can only match non-directories. Therefore, none of the sources can end with "/".'
2625 " as that implies the source is for a directory. Problematic definition from"
2626 f' {attribute_path["sources"]}'
2627 )
2628 return InstallRule.install_man(
2629 sources=sources,
2630 into=frozenset(
2631 parsed_data.get("into")
2632 or (
2633 context.single_binary_package(
2634 attribute_path,
2635 package_types=PackageTypeSelector.DEB,
2636 package_attribute="into",
2637 ),
2638 )
2639 ),
2640 section=section,
2641 language=language,
2642 definition_source=attribute_path.path,
2643 condition=parsed_data.get("when"),
2644 )
2647def _install_discard_rule_handler(
2648 _name: str,
2649 parsed_data: ParsedInstallDiscardRule,
2650 path: AttributePath,
2651 _context: ParserContextData,
2652) -> InstallRule:
2653 limit_to = parsed_data.get("search_dirs")
2654 if limit_to is not None and not limit_to: 2654 ↛ 2655line 2654 didn't jump to line 2655 because the condition on line 2654 was never true
2655 p = path["search_dirs"]
2656 raise ManifestParseException(f"The {p.path} attribute must not be empty.")
2657 condition = parsed_data.get("required_when")
2658 return InstallRule.discard_paths(
2659 parsed_data["paths"],
2660 path.path,
2661 condition,
2662 limit_to=limit_to,
2663 )
2666def _transformation_move_handler(
2667 _name: str,
2668 parsed_data: TransformationMoveRuleSpec,
2669 path: AttributePath,
2670 _context: ParserContextData,
2671) -> TransformationRule:
2672 source_match = parsed_data["source"]
2673 target_path = parsed_data["target"].match_rule.path
2674 condition = parsed_data.get("when")
2676 if ( 2676 ↛ 2680line 2676 didn't jump to line 2680 because the condition on line 2676 was never true
2677 isinstance(source_match, ExactFileSystemPath)
2678 and source_match.path == target_path
2679 ):
2680 raise ManifestParseException(
2681 f"The transformation rule {path.path} requests a move of {source_match} to"
2682 f" {target_path}, which is the same path"
2683 )
2684 return MoveTransformationRule(
2685 source_match.match_rule,
2686 target_path,
2687 target_path.endswith("/"),
2688 path,
2689 condition,
2690 )
2693def _transformation_remove_handler(
2694 _name: str,
2695 parsed_data: TransformationRemoveRuleSpec,
2696 attribute_path: AttributePath,
2697 _context: ParserContextData,
2698) -> TransformationRule:
2699 paths = parsed_data["paths"]
2700 keep_empty_parent_dirs = parsed_data.get("keep_empty_parent_dirs", False)
2702 return RemoveTransformationRule(
2703 [m.match_rule for m in paths],
2704 keep_empty_parent_dirs,
2705 attribute_path,
2706 )
2709def _transformation_create_symlink(
2710 _name: str,
2711 parsed_data: CreateSymlinkRule,
2712 attribute_path: AttributePath,
2713 _context: ParserContextData,
2714) -> TransformationRule:
2715 link_dest = parsed_data["path"].match_rule.path
2716 replacement_rule: CreateSymlinkReplacementRule = parsed_data.get(
2717 "replacement_rule",
2718 "abort-on-non-empty-directory",
2719 )
2720 try:
2721 link_target = debian_policy_normalize_symlink_target(
2722 link_dest,
2723 parsed_data["target"].symlink_target,
2724 )
2725 except ValueError as e: # pragma: no cover
2726 raise AssertionError(
2727 "Debian Policy normalization should not raise ValueError here"
2728 ) from e
2730 condition = parsed_data.get("when")
2732 return CreateSymlinkPathTransformationRule(
2733 link_target,
2734 link_dest,
2735 replacement_rule,
2736 attribute_path,
2737 condition,
2738 )
2741def _transformation_path_metadata(
2742 _name: str,
2743 parsed_data: PathManifestRule,
2744 attribute_path: AttributePath,
2745 context: ParserContextData,
2746) -> TransformationRule:
2747 match_rules = parsed_data["paths"]
2748 owner = parsed_data.get("owner")
2749 group = parsed_data.get("group")
2750 mode = parsed_data.get("mode")
2751 recursive = parsed_data.get("recursive", False)
2752 capabilities = parsed_data.get("capabilities")
2753 capability_mode = parsed_data.get("capability_mode")
2754 cap: str | None = None
2756 if capabilities is not None: 2756 ↛ 2757line 2756 didn't jump to line 2757 because the condition on line 2756 was never true
2757 check_integration_mode(
2758 attribute_path["capabilities"],
2759 context,
2760 _NOT_INTEGRATION_RRR,
2761 )
2762 if capability_mode is None:
2763 capability_mode = SymbolicMode.parse_filesystem_mode(
2764 "a-s",
2765 attribute_path["capability-mode"],
2766 )
2767 cap = capabilities.value
2768 validate_cap = check_cap_checker()
2769 validate_cap(cap, attribute_path["capabilities"].path)
2770 elif capability_mode is not None and capabilities is None: 2770 ↛ 2771line 2770 didn't jump to line 2771 because the condition on line 2770 was never true
2771 check_integration_mode(
2772 attribute_path["capability_mode"],
2773 context,
2774 _NOT_INTEGRATION_RRR,
2775 )
2776 raise ManifestParseException(
2777 "The attribute capability-mode cannot be provided without capabilities"
2778 f" in {attribute_path.path}"
2779 )
2780 if owner is None and group is None and mode is None and capabilities is None: 2780 ↛ 2781line 2780 didn't jump to line 2781 because the condition on line 2780 was never true
2781 raise ManifestParseException(
2782 "At least one of owner, group, mode, or capabilities must be provided"
2783 f" in {attribute_path.path}"
2784 )
2785 condition = parsed_data.get("when")
2787 return PathMetadataTransformationRule(
2788 [m.match_rule for m in match_rules],
2789 owner,
2790 group,
2791 mode,
2792 recursive,
2793 cap,
2794 capability_mode,
2795 attribute_path.path,
2796 condition,
2797 )
2800def _transformation_mkdirs(
2801 _name: str,
2802 parsed_data: EnsureDirectoryRule,
2803 attribute_path: AttributePath,
2804 _context: ParserContextData,
2805) -> TransformationRule:
2806 provided_paths = parsed_data["paths"]
2807 owner = parsed_data.get("owner")
2808 group = parsed_data.get("group")
2809 mode = parsed_data.get("mode")
2811 condition = parsed_data.get("when")
2813 return CreateDirectoryTransformationRule(
2814 [p.match_rule.path for p in provided_paths],
2815 owner,
2816 group,
2817 mode,
2818 attribute_path.path,
2819 condition,
2820 )
2823def _at_least_two(
2824 content: list[Any],
2825 attribute_path: AttributePath,
2826 attribute_name: str,
2827) -> None:
2828 if len(content) < 2: 2828 ↛ 2829line 2828 didn't jump to line 2829 because the condition on line 2828 was never true
2829 raise ManifestParseException(
2830 f"Must have at least two conditions in {attribute_path[attribute_name].path}"
2831 )
2834def _mc_any_of(
2835 name: str,
2836 parsed_data: MCAnyOfAllOf,
2837 attribute_path: AttributePath,
2838 _context: ParserContextData,
2839) -> ManifestCondition:
2840 conditions = parsed_data["conditions"]
2841 _at_least_two(conditions, attribute_path, "conditions")
2842 if name == "any-of": 2842 ↛ 2843line 2842 didn't jump to line 2843 because the condition on line 2842 was never true
2843 return ManifestCondition.any_of(conditions)
2844 assert name == "all-of"
2845 return ManifestCondition.all_of(conditions)
2848def _mc_not(
2849 _name: str,
2850 parsed_data: MCNot,
2851 _attribute_path: AttributePath,
2852 _context: ParserContextData,
2853) -> ManifestCondition:
2854 condition = parsed_data["negated_condition"]
2855 return condition.negated()
2858def _extract_arch_matches(
2859 parsed_data: MCArchMatches,
2860 attribute_path: AttributePath,
2861) -> list[str]:
2862 arch_matches_as_str = parsed_data["arch_matches"]
2863 # Can we check arch list for typos? If we do, it must be tight in how close matches it does.
2864 # Consider "arm" vs. "armel" (edit distance 2, but both are valid). Likewise, names often
2865 # include a bit indicator "foo", "foo32", "foo64" - all of these have an edit distance of 2
2866 # of each other.
2867 arch_matches_as_list = arch_matches_as_str.split()
2868 attr_path = attribute_path["arch_matches"]
2869 if not arch_matches_as_list: 2869 ↛ 2870line 2869 didn't jump to line 2870 because the condition on line 2869 was never true
2870 raise ManifestParseException(
2871 f"The condition at {attr_path.path} must not be empty"
2872 )
2874 if arch_matches_as_list[0].startswith("[") or arch_matches_as_list[-1].endswith( 2874 ↛ 2877line 2874 didn't jump to line 2877 because the condition on line 2874 was never true
2875 "]"
2876 ):
2877 raise ManifestParseException(
2878 f"The architecture match at {attr_path.path} must be defined without enclosing it with "
2879 '"[" or/and "]" brackets'
2880 )
2881 return arch_matches_as_list
2884def _mc_source_context_arch_matches(
2885 _name: str,
2886 parsed_data: MCArchMatches,
2887 attribute_path: AttributePath,
2888 _context: ParserContextData,
2889) -> ManifestCondition:
2890 arch_matches = _extract_arch_matches(parsed_data, attribute_path)
2891 return SourceContextArchMatchManifestCondition(arch_matches)
2894def _mc_package_context_arch_matches(
2895 name: str,
2896 parsed_data: MCArchMatches,
2897 attribute_path: AttributePath,
2898 context: ParserContextData,
2899) -> ManifestCondition:
2900 arch_matches = _extract_arch_matches(parsed_data, attribute_path)
2902 if not context.is_in_binary_package_state: 2902 ↛ 2903line 2902 didn't jump to line 2903 because the condition on line 2902 was never true
2903 raise ManifestParseException(
2904 f'The condition "{name}" at {attribute_path.path} can only be used in the context of a binary package.'
2905 )
2907 package_state = context.current_binary_package_state
2908 if package_state.binary_package.is_arch_all: 2908 ↛ 2909line 2908 didn't jump to line 2909 because the condition on line 2908 was never true
2909 result = context.dpkg_arch_query_table.architecture_is_concerned(
2910 "all", arch_matches
2911 )
2912 attr_path = attribute_path["arch_matches"]
2913 raise ManifestParseException(
2914 f"The package architecture restriction at {attr_path.path} is applied to the"
2915 f' "Architecture: all" package {package_state.binary_package.name}, which does not make sense'
2916 f" as the condition will always resolves to `{str(result).lower()}`."
2917 f" If you **really** need an architecture specific constraint for this rule, consider using"
2918 f' "source-context-arch-matches" instead. However, this is a very rare use-case!'
2919 )
2920 return BinaryPackageContextArchMatchManifestCondition(arch_matches)
2923def _mc_arch_matches(
2924 name: str,
2925 parsed_data: MCArchMatches,
2926 attribute_path: AttributePath,
2927 context: ParserContextData,
2928) -> ManifestCondition:
2929 if context.is_in_binary_package_state:
2930 return _mc_package_context_arch_matches(
2931 name, parsed_data, attribute_path, context
2932 )
2933 return _mc_source_context_arch_matches(name, parsed_data, attribute_path, context)
2936def _mc_build_profile_matches(
2937 _name: str,
2938 parsed_data: MCBuildProfileMatches,
2939 attribute_path: AttributePath,
2940 _context: ParserContextData,
2941) -> ManifestCondition:
2942 build_profile_spec = parsed_data["build_profile_matches"].strip()
2943 attr_path = attribute_path["build_profile_matches"]
2944 if not build_profile_spec: 2944 ↛ 2945line 2944 didn't jump to line 2945 because the condition on line 2944 was never true
2945 raise ManifestParseException(
2946 f"The condition at {attr_path.path} must not be empty"
2947 )
2948 try:
2949 active_profiles_match(build_profile_spec, frozenset())
2950 except ValueError as e:
2951 raise ManifestParseException(
2952 f"Could not parse the build specification at {attr_path.path}: {e.args[0]}"
2953 )
2954 return BuildProfileMatch(build_profile_spec)
2957def _parse_unconditionally_in_script(
2958 _name: str,
2959 parsed_data: UnconditionallyInScript,
2960 _path: AttributePath,
2961 _context: ParserContextData,
2962) -> MaintscriptCondition:
2963 script_name = parsed_data["script_name"]
2964 return MaintscriptCondition.on_unconditionally_in_script(script_name.name)
2967def _parse_upgrade_from_version(
2968 _name: str,
2969 parsed_data: UpgradeFromVersion,
2970 _path: AttributePath,
2971 _context: ParserContextData,
2972) -> MaintscriptCondition:
2973 from_version = parsed_data["from_version"]
2974 return MaintscriptCondition.on_upgrade_from(from_version)