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