Coverage for src/debputy/plugins/debputy/private_api.py: 83%
540 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-04-19 20:37 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-04-19 20:37 +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:
175 try:
176 libcap = ctypes.cdll.LoadLibrary(cap_library_path)
177 has_libcap = True
178 except OSError:
179 pass
181 if libcap is None:
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 File system owner reference that is part of the passwd base data (such as "root").
492 The group can be provided in either of the following three forms:
494 * A name (recommended), such as "root"
495 * The UID in the form of an integer (that is, no quoting), such as 0 (for "root")
496 * The name and the UID separated by colon such as "root:0" (for "root").
498 Note in the last case, the `debputy` will validate that the name and the UID match.
500 Some owners (such as "nobody") are deliberately disallowed.
501 """),
502 examples=[
503 type_mapping_example("root"),
504 type_mapping_example(0),
505 type_mapping_example("root:0"),
506 type_mapping_example("bin"),
507 ],
508 ),
509 )
510 api.register_mapped_type(
511 TypeMapping(
512 StaticFileSystemGroup,
513 Union[int, str],
514 lambda v, ap, _: StaticFileSystemGroup.from_manifest_value(v, ap),
515 ),
516 reference_documentation=type_mapping_reference_documentation(
517 description=textwrap.dedent("""\
518 File system group reference that is part of the passwd base data (such as "root").
520 The group can be provided in either of the following three forms:
522 * A name (recommended), such as "root"
523 * The GID in the form of an integer (that is, no quoting), such as 0 (for "root")
524 * The name and the GID separated by colon such as "root:0" (for "root").
526 Note in the last case, the `debputy` will validate that the name and the GID match.
528 Some owners (such as "nobody") are deliberately disallowed.
529 """),
530 examples=[
531 type_mapping_example("root"),
532 type_mapping_example(0),
533 type_mapping_example("root:0"),
534 type_mapping_example("tty"),
535 ],
536 ),
537 )
539 api.register_mapped_type(
540 TypeMapping(
541 BinaryPackage,
542 str,
543 type_mapper_str2package,
544 ),
545 reference_documentation=type_mapping_reference_documentation(
546 description="Name of a package in debian/control",
547 ),
548 )
550 api.register_mapped_type(
551 TypeMapping(
552 PackageSelector,
553 str,
554 PackageSelector.parse,
555 ),
556 reference_documentation=type_mapping_reference_documentation(
557 description=textwrap.dedent("""\
558 Match a package or set of a packages from debian/control
560 The simplest package selector is the name of a binary package from `debian/control`.
561 However, selections can also match multiple packages based on a given criteria, such
562 as `arch:all`/`arch:any` (matches packages where the `Architecture` field is set to
563 `all` or is not set to `all` respectively) or `package-type:deb` / `package-type:udeb`
564 (matches packages where `Package-Type` is set to `deb` or is set to `udeb`
565 respectively).
566 """),
567 ),
568 )
570 api.register_mapped_type(
571 TypeMapping(
572 FileSystemMode,
573 str,
574 lambda v, ap, _: FileSystemMode.parse_filesystem_mode(v, ap),
575 ),
576 reference_documentation=type_mapping_reference_documentation(
577 description="A file system mode either in the form of an octal mode or a symbolic mode.",
578 examples=[
579 type_mapping_example("a+x"),
580 type_mapping_example("u=rwX,go=rX"),
581 type_mapping_example("0755"),
582 ],
583 ),
584 )
585 api.register_mapped_type(
586 TypeMapping(
587 OctalMode,
588 str,
589 lambda v, ap, _: OctalMode.parse_filesystem_mode(v, ap),
590 ),
591 reference_documentation=type_mapping_reference_documentation(
592 description="A file system mode using the octal mode representation. Must always be a provided as a string (that is, quoted).",
593 examples=[
594 type_mapping_example("0644"),
595 type_mapping_example("0755"),
596 ],
597 ),
598 )
599 api.register_mapped_type(
600 TypeMapping(
601 BuildEnvironmentDefinition,
602 str,
603 lambda v, ap, pc: assume_not_none(pc).resolve_build_environment(v, ap),
604 ),
605 reference_documentation=type_mapping_reference_documentation(
606 description="Reference to a build environment defined in `build-environments`",
607 ),
608 )
611def register_service_managers(
612 api: DebputyPluginInitializerProvider,
613) -> None:
614 api.service_provider(
615 "systemd",
616 detect_systemd_service_files,
617 generate_snippets_for_systemd_units,
618 )
619 api.service_provider(
620 "sysvinit",
621 detect_sysv_init_service_files,
622 generate_snippets_for_init_scripts,
623 )
626def register_automatic_discard_rules(
627 api: DebputyPluginInitializerProvider,
628) -> None:
629 api.automatic_discard_rule(
630 "python-cache-files",
631 _debputy_discard_pyc_files,
632 rule_reference_documentation="Discards any *.pyc, *.pyo files and any __pycache__ directories",
633 examples=automatic_discard_rule_example(
634 (".../foo.py", False),
635 ".../__pycache__/",
636 ".../__pycache__/...",
637 ".../foo.pyc",
638 ".../foo.pyo",
639 ),
640 )
641 api.automatic_discard_rule(
642 "la-files",
643 _debputy_prune_la_files,
644 rule_reference_documentation="Discards any file with the extension .la beneath the directory /usr/lib",
645 examples=automatic_discard_rule_example(
646 "usr/lib/libfoo.la",
647 ("usr/lib/libfoo.so.1.0.0", False),
648 ),
649 )
650 api.automatic_discard_rule(
651 "backup-files",
652 _debputy_prune_backup_files,
653 rule_reference_documentation="Discards common back up files such as foo~, foo.bak or foo.orig",
654 examples=(
655 automatic_discard_rule_example(
656 ".../foo~",
657 ".../foo.orig",
658 ".../foo.rej",
659 ".../DEADJOE",
660 ".../.foo.sw.",
661 ),
662 ),
663 )
664 api.automatic_discard_rule(
665 "version-control-paths",
666 _debputy_prune_vcs_paths,
667 rule_reference_documentation="Discards common version control paths such as .git, .gitignore, CVS, etc.",
668 examples=automatic_discard_rule_example(
669 ("tools/foo", False),
670 ".../CVS/",
671 ".../CVS/...",
672 ".../.gitignore",
673 ".../.gitattributes",
674 ".../.git/",
675 ".../.git/...",
676 ),
677 )
678 api.automatic_discard_rule(
679 "gnu-info-dir-file",
680 _debputy_prune_info_dir_file,
681 rule_reference_documentation="Discards the /usr/share/info/dir file (causes package file conflicts)",
682 examples=automatic_discard_rule_example(
683 "usr/share/info/dir",
684 ("usr/share/info/foo.info", False),
685 ("usr/share/info/dir.info", False),
686 ("usr/share/random/case/dir", False),
687 ),
688 )
689 api.automatic_discard_rule(
690 "debian-dir",
691 _debputy_prune_binary_debian_dir,
692 rule_reference_documentation="(Implementation detail) Discards any DEBIAN directory to avoid it from appearing"
693 " literally in the file listing",
694 examples=(
695 automatic_discard_rule_example(
696 "DEBIAN/",
697 "DEBIAN/control",
698 ("usr/bin/foo", False),
699 ("usr/share/DEBIAN/foo", False),
700 ),
701 ),
702 )
703 api.automatic_discard_rule(
704 "doxygen-cruft-files",
705 _debputy_prune_doxygen_cruft,
706 rule_reference_documentation="Discards cruft files generated by doxygen",
707 examples=automatic_discard_rule_example(
708 ("usr/share/doc/foo/api/doxygen.css", False),
709 ("usr/share/doc/foo/api/doxygen.svg", False),
710 ("usr/share/doc/foo/api/index.html", False),
711 "usr/share/doc/foo/api/.../cruft.map",
712 "usr/share/doc/foo/api/.../cruft.md5",
713 ),
714 )
717def register_processing_steps(api: DebputyPluginInitializerProvider) -> None:
718 api.package_processor("manpages", process_manpages)
719 api.package_processor("clean-la-files", clean_la_files)
720 # strip-non-determinism makes assumptions about the PackageProcessingContext implementation
721 api.package_processor(
722 "strip-nondeterminism",
723 cast("Any", strip_non_determinism),
724 depends_on_processor=["manpages"],
725 )
726 api.package_processor(
727 "compression",
728 apply_compression,
729 depends_on_processor=["manpages", "strip-nondeterminism"],
730 )
733def register_variables_via_private_api(api: DebputyPluginInitializerProvider) -> None:
734 api.manifest_variable_provider(
735 load_source_variables,
736 {
737 "DEB_SOURCE": "Name of the source package (`dpkg-parsechangelog -SSource`)",
738 "DEB_VERSION": "Version from the top most changelog entry (`dpkg-parsechangelog -SVersion`)",
739 "DEB_VERSION_EPOCH_UPSTREAM": "Version from the top most changelog entry *without* the Debian revision",
740 "DEB_VERSION_UPSTREAM_REVISION": "Version from the top most changelog entry *without* the epoch",
741 "DEB_VERSION_UPSTREAM": "Upstream version from the top most changelog entry (that is, *without* epoch and Debian revision)",
742 "SOURCE_DATE_EPOCH": textwrap.dedent("""\
743 Timestamp from the top most changelog entry (`dpkg-parsechangelog -STimestamp`)
744 Please see <https://reproducible-builds.org/docs/source-date-epoch/> for the full definition of
745 this variable.
746 """),
747 "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": None,
748 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": None,
749 },
750 )
753def document_builtin_variables(api: DebputyPluginInitializerProvider) -> None:
754 api.document_builtin_variable(
755 "PACKAGE",
756 "Name of the binary package (only available in binary context)",
757 is_context_specific=True,
758 )
760 arch_types = _DOCUMENTED_DPKG_ARCH_TYPES
762 for arch_type, (arch_type_tag, arch_type_doc) in arch_types.items():
763 for arch_var, arch_var_doc in _DOCUMENTED_DPKG_ARCH_VARS.items():
764 full_var = f"DEB_{arch_type}_{arch_var}"
765 documentation = textwrap.dedent(f"""\
766 {arch_var_doc} ({arch_type_tag})
767 This variable describes machine information used when the package is compiled and assembled.
768 * Machine type: {arch_type_doc}
769 * Value description: {arch_var_doc}
771 The value is the output of: `dpkg-architecture -q{full_var}`
772 """)
773 api.document_builtin_variable(
774 full_var,
775 documentation,
776 is_for_special_case=arch_type != "HOST",
777 )
780def _format_docbase_filename(
781 path_format: str,
782 format_param: PPFFormatParam,
783 docbase_file: VirtualPath,
784) -> str:
785 with docbase_file.open() as fd:
786 content = Deb822(fd)
787 proper_name = content["Document"]
788 if proper_name is not None: 788 ↛ 791line 788 didn't jump to line 791 because the condition on line 788 was always true
789 format_param["name"] = proper_name
790 else:
791 _warn(
792 f"The docbase file {docbase_file.fs_path} is missing the Document field"
793 )
794 return path_format.format(**format_param)
797def register_special_ppfs(api: DebputyPluginInitializerProvider) -> None:
798 api.packager_provided_file(
799 "doc-base",
800 "/usr/share/doc-base/{owning_package}.{name}",
801 format_callback=_format_docbase_filename,
802 )
804 api.packager_provided_file(
805 "shlibs",
806 "DEBIAN/shlibs",
807 allow_name_segment=False,
808 reservation_only=True,
809 reference_documentation=packager_provided_file_reference_documentation(
810 format_documentation_uris=["man:deb-shlibs(5)"],
811 ),
812 )
813 api.packager_provided_file(
814 "symbols",
815 "DEBIAN/symbols",
816 allow_name_segment=False,
817 allow_architecture_segment=True,
818 reservation_only=True,
819 reference_documentation=packager_provided_file_reference_documentation(
820 format_documentation_uris=["man:deb-symbols(5)"],
821 ),
822 )
823 api.packager_provided_file(
824 "conffiles",
825 "DEBIAN/conffiles",
826 allow_name_segment=False,
827 allow_architecture_segment=True,
828 reservation_only=True,
829 )
830 api.packager_provided_file(
831 "templates",
832 "DEBIAN/templates",
833 allow_name_segment=False,
834 allow_architecture_segment=False,
835 reservation_only=True,
836 )
837 api.packager_provided_file(
838 "alternatives",
839 "DEBIAN/alternatives",
840 allow_name_segment=False,
841 allow_architecture_segment=True,
842 reservation_only=True,
843 )
846def register_install_rules(api: DebputyPluginInitializerProvider) -> None:
847 api.pluggable_manifest_rule(
848 InstallRule,
849 MK_INSTALLATIONS_INSTALL,
850 ParsedInstallRule,
851 _install_rule_handler,
852 source_format=_with_alt_form(ParsedInstallRuleSourceFormat),
853 inline_reference_documentation=reference_documentation(
854 title="Generic install (`install`)",
855 description=textwrap.dedent("""\
856 The generic `install` rule can be used to install arbitrary paths into packages
857 and is *similar* to how `dh_install` from debhelper works. It is a two "primary" uses.
859 1) The classic "install into directory" similar to the standard `dh_install`
860 2) The "install as" similar to `dh-exec`'s `foo => bar` feature.
862 The `install` rule installs a path exactly once into each package it acts on. In
863 the rare case that you want to install the same source *multiple* times into the
864 *same* packages, please have a look at `{MULTI_DEST_INSTALL}`.
865 """.format(MULTI_DEST_INSTALL=MK_INSTALLATIONS_MULTI_DEST_INSTALL)),
866 non_mapping_description=textwrap.dedent("""\
867 When the input is a string or a list of string, then that value is used as shorthand
868 for `source` or `sources` (respectively). This form can only be used when `into` is
869 not required.
870 """),
871 attributes=[
872 documented_attr(
873 ["source", "sources"],
874 textwrap.dedent("""\
875 A path match (`source`) or a list of path matches (`sources`) defining the
876 source path(s) to be installed. The path match(es) can use globs. Each match
877 is tried against default search directories.
878 - When a symlink is matched, then the symlink (not its target) is installed
879 as-is. When a directory is matched, then the directory is installed along
880 with all the contents that have not already been installed somewhere.
881 """),
882 ),
883 documented_attr(
884 "dest_dir",
885 textwrap.dedent("""\
886 A path defining the destination *directory*. The value *cannot* use globs, but can
887 use substitution. If neither `as` nor `dest-dir` is given, then `dest-dir` defaults
888 to the directory name of the `source`.
889 """),
890 ),
891 documented_attr(
892 "into",
893 textwrap.dedent("""\
894 Either a package name or a list of package names for which these paths should be
895 installed. This key is conditional on whether there are multiple binary packages listed
896 in `debian/control`. When there is only one binary package, then that binary is the
897 default for `into`. Otherwise, the key is required.
898 """),
899 ),
900 documented_attr(
901 "install_as",
902 textwrap.dedent("""\
903 A path defining the path to install the source as. This is a full path. This option
904 is mutually exclusive with `dest-dir` and `sources` (but not `source`). When `as` is
905 given, then `source` must match exactly one "not yet matched" path.
906 """),
907 ),
908 *docs_from(DebputyParsedContentStandardConditional),
909 ],
910 reference_documentation_url=manifest_format_doc("generic-install-install"),
911 ),
912 )
913 api.pluggable_manifest_rule(
914 InstallRule,
915 [
916 MK_INSTALLATIONS_INSTALL_DOCS,
917 "install-doc",
918 ],
919 ParsedInstallRule,
920 _install_docs_rule_handler,
921 source_format=_with_alt_form(ParsedInstallDocRuleSourceFormat),
922 inline_reference_documentation=reference_documentation(
923 title="Install documentation (`install-docs`)",
924 description=textwrap.dedent("""\
925 This install rule resemble that of `dh_installdocs`. It is a shorthand over the generic
926 `install` rule with the following key features:
928 1) The default `dest-dir` is to use the package's documentation directory (usually something
929 like `/usr/share/doc/{{PACKAGE}}`, though it respects the "main documentation package"
930 recommendation from Debian Policy). The `dest-dir` or `as` can be set in case the
931 documentation in question goes into another directory or with a concrete path. In this
932 case, it is still "better" than `install` due to the remaining benefits.
933 2) The rule comes with pre-defined conditional logic for skipping the rule under
934 `DEB_BUILD_OPTIONS=nodoc`, so you do not have to write that conditional yourself.
935 3) The `into` parameter can be omitted as long as there is a exactly one non-`udeb`
936 package listed in `debian/control`.
938 With these two things in mind, it behaves just like the `install` rule.
940 Note: It is often worth considering to use a more specialized version of the `install-docs`
941 rule when one such is available. If you are looking to install an example or a man page,
942 consider whether `install-examples` or `install-man` might be a better fit for your
943 use-case.
944 """),
945 non_mapping_description=textwrap.dedent("""\
946 When the input is a string or a list of string, then that value is used as shorthand
947 for `source` or `sources` (respectively). This form can only be used when `into` is
948 not required.
949 """),
950 attributes=[
951 documented_attr(
952 ["source", "sources"],
953 textwrap.dedent("""\
954 A path match (`source`) or a list of path matches (`sources`) defining the
955 source path(s) to be installed. The path match(es) can use globs. Each match
956 is tried against default search directories.
957 - When a symlink is matched, then the symlink (not its target) is installed
958 as-is. When a directory is matched, then the directory is installed along
959 with all the contents that have not already been installed somewhere.
961 - **CAVEAT**: Specifying `source: examples` where `examples` resolves to a
962 directory for `install-examples` will give you an `examples/examples`
963 directory in the package, which is rarely what you want. Often, you
964 can solve this by using `examples/*` instead. Similar for `install-docs`
965 and a `doc` or `docs` directory.
966 """),
967 ),
968 documented_attr(
969 "dest_dir",
970 textwrap.dedent("""\
971 A path defining the destination *directory*. The value *cannot* use globs, but can
972 use substitution. If neither `as` nor `dest-dir` is given, then `dest-dir` defaults
973 to the relevant package documentation directory (a la `/usr/share/doc/{{PACKAGE}}`).
974 """),
975 ),
976 documented_attr(
977 "into",
978 textwrap.dedent("""\
979 Either a package name or a list of package names for which these paths should be
980 installed as documentation. This key is conditional on whether there are multiple
981 (non-`udeb`) binary packages listed in `debian/control`. When there is only one
982 (non-`udeb`) binary package, then that binary is the default for `into`. Otherwise,
983 the key is required.
984 """),
985 ),
986 documented_attr(
987 "install_as",
988 textwrap.dedent("""\
989 A path defining the path to install the source as. This is a full path. This option
990 is mutually exclusive with `dest-dir` and `sources` (but not `source`). When `as` is
991 given, then `source` must match exactly one "not yet matched" path.
992 """),
993 ),
994 documented_attr(
995 "when",
996 textwrap.dedent("""\
997 A condition as defined in [Conditional rules](${MANIFEST_FORMAT_DOC}#conditional-rules).
998 This condition will be combined with the built-in condition provided by these rules
999 (rather than replacing it).
1000 """),
1001 ),
1002 ],
1003 reference_documentation_url=manifest_format_doc(
1004 "install-documentation-install-docs"
1005 ),
1006 ),
1007 )
1008 api.pluggable_manifest_rule(
1009 InstallRule,
1010 [
1011 MK_INSTALLATIONS_INSTALL_EXAMPLES,
1012 "install-example",
1013 ],
1014 ParsedInstallExamplesRule,
1015 _install_examples_rule_handler,
1016 source_format=_with_alt_form(ParsedInstallExamplesRuleSourceFormat),
1017 inline_reference_documentation=reference_documentation(
1018 title="Install examples (`install-examples`)",
1019 description=textwrap.dedent("""\
1020 This install rule resemble that of `dh_installexamples`. It is a shorthand over the generic `
1021 install` rule with the following key features:
1023 1) It pre-defines the `dest-dir` that respects the "main documentation package" recommendation from
1024 Debian Policy. The `install-examples` will use the `examples` subdir for the package documentation
1025 dir.
1026 2) The rule comes with pre-defined conditional logic for skipping the rule under
1027 `DEB_BUILD_OPTIONS=nodoc`, so you do not have to write that conditional yourself.
1028 3) The `into` parameter can be omitted as long as there is a exactly one non-`udeb`
1029 package listed in `debian/control`.
1031 With these two things in mind, it behaves just like the `install` rule.
1032 """),
1033 non_mapping_description=textwrap.dedent("""\
1034 When the input is a string or a list of string, then that value is used as shorthand
1035 for `source` or `sources` (respectively). This form can only be used when `into` is
1036 not required.
1037 """),
1038 attributes=[
1039 documented_attr(
1040 ["source", "sources"],
1041 textwrap.dedent("""\
1042 A path match (`source`) or a list of path matches (`sources`) defining the
1043 source path(s) to be installed. The path match(es) can use globs. Each match
1044 is tried against default search directories.
1045 - When a symlink is matched, then the symlink (not its target) is installed
1046 as-is. When a directory is matched, then the directory is installed along
1047 with all the contents that have not already been installed somewhere.
1049 - **CAVEAT**: Specifying `source: examples` where `examples` resolves to a
1050 directory for `install-examples` will give you an `examples/examples`
1051 directory in the package, which is rarely what you want. Often, you
1052 can solve this by using `examples/*` instead. Similar for `install-docs`
1053 and a `doc` or `docs` directory.
1054 """),
1055 ),
1056 documented_attr(
1057 "into",
1058 textwrap.dedent("""\
1059 Either a package name or a list of package names for which these paths should be
1060 installed as examples. This key is conditional on whether there are (non-`udeb`)
1061 multiple binary packages listed in `debian/control`. When there is only one
1062 (non-`udeb`) binary package, then that binary is the default for `into`.
1063 Otherwise, the key is required.
1064 """),
1065 ),
1066 documented_attr(
1067 "when",
1068 textwrap.dedent("""\
1069 A condition as defined in [Conditional rules](${MANIFEST_FORMAT_DOC}#conditional-rules).
1070 This condition will be combined with the built-in condition provided by these rules
1071 (rather than replacing it).
1072 """),
1073 ),
1074 ],
1075 reference_documentation_url=manifest_format_doc(
1076 "install-examples-install-examples"
1077 ),
1078 ),
1079 )
1080 api.pluggable_manifest_rule(
1081 InstallRule,
1082 MK_INSTALLATIONS_INSTALL_MAN,
1083 ParsedInstallManpageRule,
1084 _install_man_rule_handler,
1085 source_format=_with_alt_form(ParsedInstallManpageRuleSourceFormat),
1086 inline_reference_documentation=reference_documentation(
1087 title="Install man pages (`install-man`)",
1088 description=textwrap.dedent("""\
1089 Install rule for installing man pages similar to `dh_installman`. It is a shorthand
1090 over the generic `install` rule with the following key features:
1092 1) The rule can only match files (notably, symlinks cannot be matched by this rule).
1093 2) The `dest-dir` is computed per source file based on the man page's section and
1094 language.
1095 3) The `into` parameter can be omitted as long as there is a exactly one non-`udeb`
1096 package listed in `debian/control`.
1097 4) The rule comes with man page specific attributes such as `language` and `section`
1098 for when the auto-detection is insufficient.
1099 5) The rule comes with pre-defined conditional logic for skipping the rule under
1100 `DEB_BUILD_OPTIONS=nodoc`, so you do not have to write that conditional yourself.
1102 With these things in mind, the rule behaves similar to the `install` rule.
1103 """),
1104 non_mapping_description=textwrap.dedent("""\
1105 When the input is a string or a list of string, then that value is used as shorthand
1106 for `source` or `sources` (respectively). This form can only be used when `into` is
1107 not required.
1108 """),
1109 attributes=[
1110 documented_attr(
1111 ["source", "sources"],
1112 textwrap.dedent("""\
1113 A path match (`source`) or a list of path matches (`sources`) defining the
1114 source path(s) to be installed. The path match(es) can use globs. Each match
1115 is tried against default search directories.
1116 - When a symlink is matched, then the symlink (not its target) is installed
1117 as-is. When a directory is matched, then the directory is installed along
1118 with all the contents that have not already been installed somewhere.
1119 """),
1120 ),
1121 documented_attr(
1122 "into",
1123 textwrap.dedent("""\
1124 Either a package name or a list of package names for which these paths should be
1125 installed as man pages. This key is conditional on whether there are multiple (non-`udeb`)
1126 binary packages listed in `debian/control`. When there is only one (non-`udeb`) binary
1127 package, then that binary is the default for `into`. Otherwise, the key is required.
1128 """),
1129 ),
1130 documented_attr(
1131 "section",
1132 textwrap.dedent("""\
1133 If provided, it must be an integer between 1 and 9 (both inclusive), defining the
1134 section the man pages belong overriding any auto-detection that `debputy` would
1135 have performed.
1136 """),
1137 ),
1138 documented_attr(
1139 "language",
1140 textwrap.dedent("""\
1141 If provided, it must be either a 2 letter language code (such as `de`), a 5 letter
1142 language + dialect code (such as `pt_BR`), or one of the special keywords `C`,
1143 `derive-from-path`, or `derive-from-basename`. The default is `derive-from-path`.
1144 - When `language` is `C`, then the man pages are assumed to be "untranslated".
1145 - When `language` is a language code (with or without dialect), then all man pages
1146 matched will be assumed to be translated to that concrete language / dialect.
1147 - When `language` is `derive-from-path`, then `debputy` attempts to derive the
1148 language from the path (`man/<language>/man<section>`). This matches the
1149 default of `dh_installman`. When no language can be found for a given source,
1150 `debputy` behaves like language was `C`.
1151 - When `language` is `derive-from-basename`, then `debputy` attempts to derive
1152 the language from the basename (`foo.<language>.1`) similar to `dh_installman`
1153 previous default. When no language can be found for a given source, `debputy`
1154 behaves like language was `C`. Note this is prone to false positives where
1155 `.pl`, `.so` or similar two-letter extensions gets mistaken for a language code
1156 (`.pl` can both be "Polish" or "Perl Script", `.so` can both be "Somali" and
1157 "Shared Object" documentation). In this configuration, such extensions are
1158 always assumed to be a language.
1159 """),
1160 ),
1161 *docs_from(DebputyParsedContentStandardConditional),
1162 ],
1163 reference_documentation_url=manifest_format_doc(
1164 "install-manpages-install-man"
1165 ),
1166 ),
1167 )
1168 api.pluggable_manifest_rule(
1169 InstallRule,
1170 MK_INSTALLATIONS_DISCARD,
1171 ParsedInstallDiscardRule,
1172 _install_discard_rule_handler,
1173 source_format=_with_alt_form(ParsedInstallDiscardRuleSourceFormat),
1174 inline_reference_documentation=reference_documentation(
1175 title="Discard (or exclude) upstream provided paths (`discard`)",
1176 description=textwrap.dedent("""\
1177 When installing paths from `debian/tmp` into packages, it might be useful to ignore
1178 some paths that you never need installed. This can be done with the `discard` rule.
1180 Once a path is discarded, it cannot be matched by any other install rules. A path
1181 that is discarded, is considered handled when `debputy` checks for paths you might
1182 have forgotten to install. The `discard` feature therefore *also* replaces the
1183 `debian/not-installed` file used by `debhelper` and `cdbs`.
1184 """),
1185 non_mapping_description=textwrap.dedent("""\
1186 When the input is a string or a list of string, then that value is used as shorthand
1187 for `path` or `paths` (respectively).
1188 """),
1189 attributes=[
1190 documented_attr(
1191 ["path", "paths"],
1192 textwrap.dedent("""\
1193 A path match (`path`) or a list of path matches (`paths`) defining the source
1194 path(s) that should not be installed anywhere. The path match(es) can use globs.
1195 - When a symlink is matched, then the symlink (not its target) is discarded as-is.
1196 When a directory is matched, then the directory is discarded along with all the
1197 contents that have not already been installed somewhere.
1198 """),
1199 ),
1200 documented_attr(
1201 ["search_dir", "search_dirs"],
1202 textwrap.dedent("""\
1203 A path (`search-dir`) or a list to paths (`search-dirs`) that defines
1204 which search directories apply to. This attribute is primarily useful
1205 for source packages that uses "per package search dirs", and you want
1206 to restrict a discard rule to a subset of the relevant search dirs.
1207 Note all listed search directories must be either an explicit search
1208 requested by the packager or a search directory that `debputy`
1209 provided automatically (such as `debian/tmp`). Listing other paths
1210 will make `debputy` report an error.
1211 - Note that the `path` or `paths` must match at least one entry in
1212 any of the search directories unless *none* of the search directories
1213 exist (or the condition in `required-when` evaluates to false). When
1214 none of the search directories exist, the discard rule is silently
1215 skipped. This special-case enables you to have discard rules only
1216 applicable to certain builds that are only performed conditionally.
1217 """),
1218 ),
1219 documented_attr(
1220 "required_when",
1221 textwrap.dedent("""\
1222 A condition as defined in [Conditional rules](#conditional-rules). The discard
1223 rule is always applied. When the conditional is present and evaluates to false,
1224 the discard rule can silently match nothing.When the condition is absent, *or*
1225 it evaluates to true, then each pattern provided must match at least one path.
1226 """),
1227 ),
1228 ],
1229 reference_documentation_url=manifest_format_doc(
1230 "discard-or-exclude-upstream-provided-paths-discard"
1231 ),
1232 ),
1233 )
1234 api.pluggable_manifest_rule(
1235 InstallRule,
1236 MK_INSTALLATIONS_MULTI_DEST_INSTALL,
1237 ParsedMultiDestInstallRule,
1238 _multi_dest_install_rule_handler,
1239 source_format=ParsedMultiDestInstallRuleSourceFormat,
1240 inline_reference_documentation=reference_documentation(
1241 title=f"Multi destination install (`{MK_INSTALLATIONS_MULTI_DEST_INSTALL}`)",
1242 description=textwrap.dedent("""\
1243 The `${RULE_NAME}` is a variant of the generic `install` rule that installs sources
1244 into multiple destination paths. This is needed for the rare case where you want a
1245 path to be installed *twice* (or more) into the *same* package. The rule is a two
1246 "primary" uses.
1248 1) The classic "install into directory" similar to the standard `dh_install`,
1249 except you list 2+ destination directories.
1250 2) The "install as" similar to `dh-exec`'s `foo => bar` feature, except you list
1251 2+ `as` names.
1252 """),
1253 attributes=[
1254 documented_attr(
1255 ["source", "sources"],
1256 textwrap.dedent("""\
1257 A path match (`source`) or a list of path matches (`sources`) defining the
1258 source path(s) to be installed. The path match(es) can use globs. Each match
1259 is tried against default search directories.
1260 - When a symlink is matched, then the symlink (not its target) is installed
1261 as-is. When a directory is matched, then the directory is installed along
1262 with all the contents that have not already been installed somewhere.
1263 """),
1264 ),
1265 documented_attr(
1266 "dest_dirs",
1267 textwrap.dedent("""\
1268 A list of paths defining the destination *directories*. The value *cannot* use
1269 globs, but can use substitution. It is mutually exclusive with `as` but must be
1270 provided if `as` is not provided. The attribute must contain at least two paths
1271 (if you do not have two paths, you want `install`).
1272 """),
1273 ),
1274 documented_attr(
1275 "into",
1276 textwrap.dedent("""\
1277 Either a package name or a list of package names for which these paths should be
1278 installed. This key is conditional on whether there are multiple binary packages listed
1279 in `debian/control`. When there is only one binary package, then that binary is the
1280 default for `into`. Otherwise, the key is required.
1281 """),
1282 ),
1283 documented_attr(
1284 "install_as",
1285 textwrap.dedent("""\
1286 A list of paths, which defines all the places the source will be installed.
1287 Each path must be a full path without globs (but can use substitution).
1288 This option is mutually exclusive with `dest-dirs` and `sources` (but not
1289 `source`). When `as` is given, then `source` must match exactly one
1290 "not yet matched" path. The attribute must contain at least two paths
1291 (if you do not have two paths, you want `install`).
1292 """),
1293 ),
1294 *docs_from(DebputyParsedContentStandardConditional),
1295 ],
1296 reference_documentation_url=manifest_format_doc("generic-install-install"),
1297 ),
1298 )
1301def register_transformation_rules(api: DebputyPluginInitializerProvider) -> None:
1302 api.pluggable_manifest_rule(
1303 TransformationRule,
1304 "move",
1305 TransformationMoveRuleSpec,
1306 _transformation_move_handler,
1307 inline_reference_documentation=reference_documentation(
1308 title="Move transformation rule (`move`)",
1309 description=textwrap.dedent("""\
1310 The move transformation rule is mostly only useful for single binary source packages,
1311 where everything from upstream's build system is installed automatically into the package.
1312 In those case, you might find yourself with some files that need to be renamed to match
1313 Debian specific requirements.
1315 This can be done with the `move` transformation rule, which is a rough emulation of the
1316 `mv` command line tool.
1317 """),
1318 attributes=[
1319 documented_attr(
1320 "source",
1321 textwrap.dedent("""\
1322 A path match defining the source path(s) to be renamed. The value can use globs
1323 and substitutions.
1324 """),
1325 ),
1326 documented_attr(
1327 "target",
1328 textwrap.dedent("""\
1329 A path defining the target path. The value *cannot* use globs, but can use
1330 substitution. If the target ends with a literal `/` (prior to substitution),
1331 the target will *always* be a directory.
1332 """),
1333 ),
1334 *docs_from(DebputyParsedContentStandardConditional),
1335 ],
1336 reference_documentation_url=manifest_format_doc(
1337 "move-transformation-rule-move"
1338 ),
1339 ),
1340 )
1341 api.pluggable_manifest_rule(
1342 TransformationRule,
1343 "remove",
1344 TransformationRemoveRuleSpec,
1345 _transformation_remove_handler,
1346 source_format=_with_alt_form(TransformationRemoveRuleInputFormat),
1347 inline_reference_documentation=reference_documentation(
1348 title="Remove transformation rule (`remove`)",
1349 description=textwrap.dedent("""\
1350 The remove transformation rule is mostly only useful for single binary source packages,
1351 where everything from upstream's build system is installed automatically into the package.
1352 In those case, you might find yourself with some files that are _not_ relevant for the
1353 Debian package (but would be relevant for other distros or for non-distro local builds).
1354 Common examples include `INSTALL` files or `LICENSE` files (when they are just a subset
1355 of `debian/copyright`).
1357 In the manifest, you can ask `debputy` to remove paths from the debian package by using
1358 the `remove` transformation rule.
1360 Note that `remove` removes paths from future glob matches and transformation rules.
1361 """),
1362 non_mapping_description=textwrap.dedent("""\
1363 When the input is a string or a list of string, then that value is used as shorthand
1364 for `path` or `paths` (respectively).
1365 """),
1366 attributes=[
1367 documented_attr(
1368 ["path", "paths"],
1369 textwrap.dedent("""\
1370 A path match (`path`) or a list of path matches (`paths`) defining the
1371 path(s) inside the package that should be removed. The path match(es)
1372 can use globs.
1373 - When a symlink is matched, then the symlink (not its target) is removed
1374 as-is. When a directory is matched, then the directory is removed
1375 along with all the contents.
1376 """),
1377 ),
1378 documented_attr(
1379 "keep_empty_parent_dirs",
1380 textwrap.dedent("""\
1381 A boolean determining whether to prune parent directories that become
1382 empty as a consequence of this rule. When provided and `true`, this
1383 rule will leave empty directories behind. Otherwise, if this rule
1384 causes a directory to become empty that directory will be removed.
1385 """),
1386 ),
1387 documented_attr(
1388 "when",
1389 textwrap.dedent("""\
1390 A condition as defined in [Conditional rules](${MANIFEST_FORMAT_DOC}#conditional-rules).
1391 This condition will be combined with the built-in condition provided by these rules
1392 (rather than replacing it).
1393 """),
1394 ),
1395 ],
1396 reference_documentation_url=manifest_format_doc(
1397 "remove-transformation-rule-remove"
1398 ),
1399 ),
1400 )
1401 api.pluggable_manifest_rule(
1402 TransformationRule,
1403 "create-symlink",
1404 CreateSymlinkRule,
1405 _transformation_create_symlink,
1406 inline_reference_documentation=reference_documentation(
1407 title="Create symlinks transformation rule (`create-symlink`)",
1408 description=textwrap.dedent("""\
1409 Often, the upstream build system will provide the symlinks for you. However,
1410 in some cases, it is useful for the packager to define distribution specific
1411 symlinks. This can be done via the `create-symlink` transformation rule.
1412 """),
1413 attributes=[
1414 documented_attr(
1415 "path",
1416 textwrap.dedent("""\
1417 The path that should be a symlink. The path may contain substitution
1418 variables such as `{{DEB_HOST_MULTIARCH}}` but _cannot_ use globs.
1419 Parent directories are implicitly created as necessary.
1420 * Note that if `path` already exists, the behavior of this
1421 transformation depends on the value of `replacement-rule`.
1422 """),
1423 ),
1424 documented_attr(
1425 "target",
1426 textwrap.dedent("""\
1427 Where the symlink should point to. The target may contain substitution
1428 variables such as `{{DEB_HOST_MULTIARCH}}` but _cannot_ use globs.
1429 The link target is _not_ required to exist inside the package.
1430 * The `debputy` tool will normalize the target according to the rules
1431 of the Debian Policy. Use absolute or relative target at your own
1432 preference.
1433 """),
1434 ),
1435 documented_attr(
1436 "replacement_rule",
1437 textwrap.dedent("""\
1438 This attribute defines how to handle if `path` already exists. It can
1439 be set to one of the following values:
1440 - `error-if-exists`: When `path` already exists, `debputy` will
1441 stop with an error. This is similar to `ln -s` semantics.
1442 - `error-if-directory`: When `path` already exists, **and** it is
1443 a directory, `debputy` will stop with an error. Otherwise,
1444 remove the `path` first and then create the symlink. This is
1445 similar to `ln -sf` semantics.
1446 - `abort-on-non-empty-directory` (default): When `path` already
1447 exists, then it will be removed provided it is a non-directory
1448 **or** an *empty* directory and the symlink will then be
1449 created. If the path is a *non-empty* directory, `debputy`
1450 will stop with an error.
1451 - `discard-existing`: When `path` already exists, it will be
1452 removed. If the `path` is a directory, all its contents will
1453 be removed recursively along with the directory. Finally,
1454 the symlink is created. This is similar to having an explicit
1455 `remove` rule just prior to the `create-symlink` that is
1456 conditional on `path` existing (plus the condition defined in
1457 `when` if any).
1459 Keep in mind, that `replacement-rule` only applies if `path` exists.
1460 If the symlink cannot be created, because a part of `path` exist and
1461 is *not* a directory, then `create-symlink` will fail regardless of
1462 the value in `replacement-rule`.
1463 """),
1464 ),
1465 *docs_from(DebputyParsedContentStandardConditional),
1466 ],
1467 reference_documentation_url=manifest_format_doc(
1468 "create-symlinks-transformation-rule-create-symlink"
1469 ),
1470 ),
1471 )
1472 api.pluggable_manifest_rule(
1473 TransformationRule,
1474 "path-metadata",
1475 PathManifestRule,
1476 _transformation_path_metadata,
1477 source_format=PathManifestSourceDictFormat,
1478 inline_reference_documentation=reference_documentation(
1479 title="Change path owner/group or mode (`path-metadata`)",
1480 description=textwrap.dedent("""\
1481 The `debputy` command normalizes the path metadata (such as ownership and mode) similar
1482 to `dh_fixperms`. For most packages, the default is what you want. However, in some
1483 cases, the package has a special case or two that `debputy` does not cover. In that
1484 case, you can tell `debputy` to use the metadata you want by using the `path-metadata`
1485 transformation.
1487 Common use-cases include setuid/setgid binaries (such `usr/bin/sudo`) or/and static
1488 ownership (such as /usr/bin/write).
1489 """),
1490 attributes=[
1491 documented_attr(
1492 ["path", "paths"],
1493 textwrap.dedent("""\
1494 A path match (`path`) or a list of path matches (`paths`) defining the path(s)
1495 inside the package that should be affected. The path match(es) can use globs
1496 and substitution variables. Special-rules for matches:
1497 - Symlinks are never followed and will never be matched by this rule.
1498 - Directory handling depends on the `recursive` attribute.
1499 """),
1500 ),
1501 documented_attr(
1502 "owner",
1503 textwrap.dedent("""\
1504 Denotes the owner of the paths matched by `path` or `paths`. When omitted,
1505 no change of owner is done.
1506 """),
1507 ),
1508 documented_attr(
1509 "group",
1510 textwrap.dedent("""\
1511 Denotes the group of the paths matched by `path` or `paths`. When omitted,
1512 no change of group is done.
1513 """),
1514 ),
1515 documented_attr(
1516 "mode",
1517 textwrap.dedent("""\
1518 Denotes the mode of the paths matched by `path` or `paths`. When omitted,
1519 no change in mode is done. Note that numeric mode must always be given as
1520 a string (i.e., with quotes). Symbolic mode can be used as well. If
1521 symbolic mode uses a relative definition (e.g., `o-rx`), then it is
1522 relative to the matched path's current mode.
1523 """),
1524 ),
1525 documented_attr(
1526 "capabilities",
1527 textwrap.dedent("""\
1528 Denotes a Linux capability that should be applied to the path. When provided,
1529 `debputy` will cause the capability to be applied to all *files* denoted by
1530 the `path`/`paths` attribute on install (via `postinst configure`) provided
1531 that `setcap` is installed on the system when the `postinst configure` is
1532 run.
1533 - If any non-file paths are matched, the `capabilities` will *not* be applied
1534 to those paths.
1536 """),
1537 ),
1538 documented_attr(
1539 "capability_mode",
1540 textwrap.dedent("""\
1541 Denotes the mode to apply to the path *if* the Linux capability denoted in
1542 `capabilities` was successfully applied. If omitted, it defaults to `a-s` as
1543 generally capabilities are used to avoid "setuid"/"setgid" binaries. The
1544 `capability-mode` is relative to the *final* path mode (the mode of the path
1545 in the produced `.deb`). The `capability-mode` attribute cannot be used if
1546 `capabilities` is omitted.
1547 """),
1548 ),
1549 documented_attr(
1550 "recursive",
1551 textwrap.dedent("""\
1552 When a directory is matched, then the metadata changes are applied to the
1553 directory itself. When `recursive` is `true`, then the transformation is
1554 *also* applied to all paths beneath the directory. The default value for
1555 this attribute is `false`.
1556 """),
1557 ),
1558 *docs_from(DebputyParsedContentStandardConditional),
1559 ],
1560 reference_documentation_url=manifest_format_doc(
1561 "change-path-ownergroup-or-mode-path-metadata"
1562 ),
1563 ),
1564 )
1565 api.pluggable_manifest_rule(
1566 TransformationRule,
1567 "create-directories",
1568 EnsureDirectoryRule,
1569 _transformation_mkdirs,
1570 source_format=_with_alt_form(EnsureDirectorySourceFormat),
1571 inline_reference_documentation=reference_documentation(
1572 title="Create directories transformation rule (`create-directories`)",
1573 description=textwrap.dedent("""\
1574 NOTE: This transformation is only really needed if you need to create an empty
1575 directory somewhere in your package as an integration point. All `debputy`
1576 transformations will create directories as required.
1578 In most cases, upstream build systems and `debputy` will create all the relevant
1579 directories. However, in some rare cases you may want to explicitly define a path
1580 to be a directory. Maybe to silence a linter that is warning you about a directory
1581 being empty, or maybe you need an empty directory that nothing else is creating for
1582 you. This can be done via the `create-directories` transformation rule.
1584 Unless you have a specific need for the mapping form, you are recommended to use the
1585 shorthand form of just listing the directories you want created.
1586 """),
1587 non_mapping_description=textwrap.dedent("""\
1588 When the input is a string or a list of string, then that value is used as shorthand
1589 for `path` or `paths` (respectively).
1590 """),
1591 attributes=[
1592 documented_attr(
1593 ["path", "paths"],
1594 textwrap.dedent("""\
1595 A path (`path`) or a list of path (`paths`) defining the path(s) inside the
1596 package that should be created as directories. The path(es) _cannot_ use globs
1597 but can use substitution variables. Parent directories are implicitly created
1598 (with owner `root:root` and mode `0755` - only explicitly listed directories
1599 are affected by the owner/mode options)
1600 """),
1601 ),
1602 documented_attr(
1603 "owner",
1604 textwrap.dedent("""\
1605 Denotes the owner of the directory (but _not_ what is inside the directory).
1606 Default is "root".
1607 """),
1608 ),
1609 documented_attr(
1610 "group",
1611 textwrap.dedent("""\
1612 Denotes the group of the directory (but _not_ what is inside the directory).
1613 Default is "root".
1614 """),
1615 ),
1616 documented_attr(
1617 "mode",
1618 textwrap.dedent("""\
1619 Denotes the mode of the directory (but _not_ what is inside the directory).
1620 Note that numeric mode must always be given as a string (i.e., with quotes).
1621 Symbolic mode can be used as well. If symbolic mode uses a relative
1622 definition (e.g., `o-rx`), then it is relative to the directory's current mode
1623 (if it already exists) or `0755` if the directory is created by this
1624 transformation. The default is "0755".
1625 """),
1626 ),
1627 *docs_from(DebputyParsedContentStandardConditional),
1628 ],
1629 reference_documentation_url=manifest_format_doc(
1630 "create-directories-transformation-rule-directories"
1631 ),
1632 ),
1633 )
1636def register_manifest_condition_rules(api: DebputyPluginInitializerProvider) -> None:
1637 api.provide_manifest_keyword(
1638 ManifestCondition,
1639 "cross-compiling",
1640 lambda *_: ManifestCondition.is_cross_building(),
1641 )
1642 api.provide_manifest_keyword(
1643 ManifestCondition,
1644 "can-execute-compiled-binaries",
1645 lambda *_: ManifestCondition.can_execute_compiled_binaries(),
1646 )
1647 api.provide_manifest_keyword(
1648 ManifestCondition,
1649 "run-build-time-tests",
1650 lambda *_: ManifestCondition.run_build_time_tests(),
1651 )
1653 api.pluggable_manifest_rule(
1654 ManifestCondition,
1655 "not",
1656 MCNot,
1657 _mc_not,
1658 source_format=ManifestCondition,
1659 )
1660 api.pluggable_manifest_rule(
1661 ManifestCondition,
1662 ["any-of", "all-of"],
1663 MCAnyOfAllOf,
1664 _mc_any_of,
1665 source_format=list[ManifestCondition],
1666 )
1667 api.pluggable_manifest_rule(
1668 ManifestCondition,
1669 "arch-matches",
1670 MCArchMatches,
1671 _mc_arch_matches,
1672 source_format=str,
1673 inline_reference_documentation=reference_documentation(
1674 title="Architecture match condition `arch-matches`",
1675 description=textwrap.dedent("""\
1676 Sometimes, a rule needs to be conditional on the architecture.
1677 This can be done by using the `arch-matches` rule. In 99.99%
1678 of the cases, `arch-matches` will be form you are looking for
1679 and practically behaves like a comparison against
1680 `dpkg-architecture -qDEB_HOST_ARCH`.
1682 For the cross-compiling specialists or curious people: The
1683 `arch-matches` rule behaves like a `package-context-arch-matches`
1684 in the context of a binary package and like
1685 `source-context-arch-matches` otherwise. The details of those
1686 are covered in their own keywords.
1687 """),
1688 non_mapping_description=textwrap.dedent("""\
1689 The value must be a string in the form of a space separated list
1690 architecture names or architecture wildcards (same syntax as the
1691 architecture restriction in Build-Depends in debian/control except
1692 there is no enclosing `[]` brackets). The names/wildcards can
1693 optionally be prefixed by `!` to negate them. However, either
1694 *all* names / wildcards must have negation or *none* of them may
1695 have it.
1696 """),
1697 reference_documentation_url=manifest_format_doc(
1698 "architecture-match-condition-arch-matches-mapping"
1699 ),
1700 ),
1701 )
1703 context_arch_doc = reference_documentation(
1704 title="Explicit source or binary package context architecture match condition"
1705 " `source-context-arch-matches`, `package-context-arch-matches` (mapping)",
1706 description=textwrap.dedent("""\
1707 **These are special-case conditions**. Unless you know that you have a very special-case,
1708 you should probably use `arch-matches` instead. These conditions are aimed at people with
1709 corner-case special architecture needs. It also assumes the reader is familiar with the
1710 `arch-matches` condition.
1712 To understand these rules, here is a quick primer on `debputy`'s concept of "source context"
1713 vs "(binary) package context" architecture. For a native build, these two contexts are the
1714 same except that in the package context an `Architecture: all` package always resolve to
1715 `all` rather than `DEB_HOST_ARCH`. As a consequence, `debputy` forbids `arch-matches` and
1716 `package-context-arch-matches` in the context of an `Architecture: all` package as a warning
1717 to the packager that condition does not make sense.
1719 In the very rare case that you need an architecture condition for an `Architecture: all` package,
1720 you can use `source-context-arch-matches`. However, this means your `Architecture: all` package
1721 is not reproducible between different build hosts (which has known to be relevant for some
1722 very special cases).
1724 Additionally, for the 0.0001% case you are building a cross-compiling compiler (that is,
1725 `DEB_HOST_ARCH != DEB_TARGET_ARCH` and you are working with `gcc` or similar) `debputy` can be
1726 instructed (opt-in) to use `DEB_TARGET_ARCH` rather than `DEB_HOST_ARCH` for certain packages when
1727 evaluating an architecture condition in context of a binary package. This can be useful if the
1728 compiler produces supporting libraries that need to be built for the `DEB_TARGET_ARCH` rather than
1729 the `DEB_HOST_ARCH`. This is where `arch-matches` or `package-context-arch-matches` can differ
1730 subtly from `source-context-arch-matches` in how they evaluate the condition. This opt-in currently
1731 relies on setting `X-DH-Build-For-Type: target` for each of the relevant packages in
1732 `debian/control`. However, unless you are a cross-compiling specialist, you will probably never
1733 need to care about nor use any of this.
1735 Accordingly, the possible conditions are:
1737 * `arch-matches`: This is the form recommended to laymen and as the default use-case. This
1738 conditional acts `package-context-arch-matches` if the condition is used in the context
1739 of a binary package. Otherwise, it acts as `source-context-arch-matches`.
1741 * `source-context-arch-matches`: With this conditional, the provided architecture constraint is compared
1742 against the build time provided host architecture (`dpkg-architecture -qDEB_HOST_ARCH`). This can
1743 be useful when an `Architecture: all` package needs an architecture condition for some reason.
1745 * `package-context-arch-matches`: With this conditional, the provided architecture constraint is compared
1746 against the package's resolved architecture. This condition can only be used in the context of a binary
1747 package (usually, under `packages.<name>.`). If the package is an `Architecture: all` package, the
1748 condition will fail with an error as the condition always have the same outcome. For all other
1749 packages, the package's resolved architecture is the same as the build time provided host architecture
1750 (`dpkg-architecture -qDEB_HOST_ARCH`).
1752 - However, as noted above there is a special case for when compiling a cross-compiling compiler, where
1753 this behaves subtly different from `source-context-arch-matches`.
1755 All conditions are used the same way as `arch-matches`. Simply replace `arch-matches` with the other
1756 condition. See the `arch-matches` description for an example.
1757 """),
1758 non_mapping_description=textwrap.dedent("""\
1759 The value must be a string in the form of a space separated list
1760 architecture names or architecture wildcards (same syntax as the
1761 architecture restriction in Build-Depends in debian/control except
1762 there is no enclosing `[]` brackets). The names/wildcards can
1763 optionally be prefixed by `!` to negate them. However, either
1764 *all* names / wildcards must have negation or *none* of them may
1765 have it.
1766 """),
1767 )
1769 api.pluggable_manifest_rule(
1770 ManifestCondition,
1771 "source-context-arch-matches",
1772 MCArchMatches,
1773 _mc_source_context_arch_matches,
1774 source_format=str,
1775 inline_reference_documentation=context_arch_doc,
1776 )
1777 api.pluggable_manifest_rule(
1778 ManifestCondition,
1779 "package-context-arch-matches",
1780 MCArchMatches,
1781 _mc_arch_matches,
1782 source_format=str,
1783 inline_reference_documentation=context_arch_doc,
1784 )
1785 api.pluggable_manifest_rule(
1786 ManifestCondition,
1787 "build-profiles-matches",
1788 MCBuildProfileMatches,
1789 _mc_build_profile_matches,
1790 source_format=str,
1791 )
1794def register_dpkg_conffile_rules(api: DebputyPluginInitializerProvider) -> None:
1795 api.pluggable_manifest_rule(
1796 DpkgMaintscriptHelperCommand,
1797 "remove",
1798 DpkgRemoveConffileRule,
1799 _dpkg_conffile_remove,
1800 inline_reference_documentation=None, # TODO: write and add
1801 )
1803 api.pluggable_manifest_rule(
1804 DpkgMaintscriptHelperCommand,
1805 "rename",
1806 DpkgRenameConffileRule,
1807 _dpkg_conffile_rename,
1808 inline_reference_documentation=None, # TODO: write and add
1809 )
1812class _ModeOwnerBase(DebputyParsedContentStandardConditional):
1813 mode: NotRequired[FileSystemMode]
1814 owner: NotRequired[StaticFileSystemOwner]
1815 group: NotRequired[StaticFileSystemGroup]
1818class PathManifestSourceDictFormat(_ModeOwnerBase):
1819 path: NotRequired[
1820 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("paths")]
1821 ]
1822 paths: NotRequired[list[FileSystemMatchRule]]
1823 recursive: NotRequired[bool]
1824 capabilities: NotRequired[Capability]
1825 capability_mode: NotRequired[FileSystemMode]
1828class PathManifestRule(_ModeOwnerBase):
1829 paths: list[FileSystemMatchRule]
1830 recursive: NotRequired[bool]
1831 capabilities: NotRequired[Capability]
1832 capability_mode: NotRequired[FileSystemMode]
1835class EnsureDirectorySourceFormat(_ModeOwnerBase):
1836 path: NotRequired[
1837 Annotated[FileSystemExactMatchRule, DebputyParseHint.target_attribute("paths")]
1838 ]
1839 paths: NotRequired[list[FileSystemExactMatchRule]]
1842class EnsureDirectoryRule(_ModeOwnerBase):
1843 paths: list[FileSystemExactMatchRule]
1846class CreateSymlinkRule(DebputyParsedContentStandardConditional):
1847 path: FileSystemExactMatchRule
1848 target: Annotated[SymlinkTarget, DebputyParseHint.not_path_error_hint()]
1849 replacement_rule: NotRequired[CreateSymlinkReplacementRule]
1852class TransformationMoveRuleSpec(DebputyParsedContentStandardConditional):
1853 source: FileSystemMatchRule
1854 target: FileSystemExactMatchRule
1857class TransformationRemoveRuleSpec(DebputyParsedContentStandardConditional):
1858 paths: list[FileSystemMatchRule]
1859 keep_empty_parent_dirs: NotRequired[bool]
1862class TransformationRemoveRuleInputFormat(DebputyParsedContentStandardConditional):
1863 path: NotRequired[
1864 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("paths")]
1865 ]
1866 paths: NotRequired[list[FileSystemMatchRule]]
1867 keep_empty_parent_dirs: NotRequired[bool]
1870class ParsedInstallRuleSourceFormat(DebputyParsedContentStandardConditional):
1871 sources: NotRequired[list[FileSystemMatchRule]]
1872 source: NotRequired[
1873 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
1874 ]
1875 into: NotRequired[
1876 Annotated[
1877 str | list[str],
1878 DebputyParseHint.required_when_multi_binary(),
1879 ]
1880 ]
1881 dest_dir: NotRequired[
1882 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()]
1883 ]
1884 install_as: NotRequired[
1885 Annotated[
1886 FileSystemExactMatchRule,
1887 DebputyParseHint.conflicts_with_source_attributes("sources", "dest_dir"),
1888 DebputyParseHint.manifest_attribute("as"),
1889 DebputyParseHint.not_path_error_hint(),
1890 ]
1891 ]
1894class ParsedInstallDocRuleSourceFormat(DebputyParsedContentStandardConditional):
1895 sources: NotRequired[list[FileSystemMatchRule]]
1896 source: NotRequired[
1897 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
1898 ]
1899 into: NotRequired[
1900 Annotated[
1901 str | list[str],
1902 DebputyParseHint.required_when_multi_binary(
1903 package_types=PackageTypeSelector.DEB
1904 ),
1905 ]
1906 ]
1907 dest_dir: NotRequired[
1908 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()]
1909 ]
1910 install_as: NotRequired[
1911 Annotated[
1912 FileSystemExactMatchRule,
1913 DebputyParseHint.conflicts_with_source_attributes("sources", "dest_dir"),
1914 DebputyParseHint.manifest_attribute("as"),
1915 DebputyParseHint.not_path_error_hint(),
1916 ]
1917 ]
1920class ParsedInstallRule(DebputyParsedContentStandardConditional):
1921 sources: list[FileSystemMatchRule]
1922 into: NotRequired[list[BinaryPackage]]
1923 dest_dir: NotRequired[FileSystemExactMatchRule]
1924 install_as: NotRequired[FileSystemExactMatchRule]
1927class ParsedMultiDestInstallRuleSourceFormat(DebputyParsedContentStandardConditional):
1928 sources: NotRequired[list[FileSystemMatchRule]]
1929 source: NotRequired[
1930 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
1931 ]
1932 into: NotRequired[
1933 Annotated[
1934 str | list[str],
1935 DebputyParseHint.required_when_multi_binary(),
1936 ]
1937 ]
1938 dest_dirs: NotRequired[
1939 Annotated[
1940 list[FileSystemExactMatchRule], DebputyParseHint.not_path_error_hint()
1941 ]
1942 ]
1943 install_as: NotRequired[
1944 Annotated[
1945 list[FileSystemExactMatchRule],
1946 DebputyParseHint.conflicts_with_source_attributes("sources", "dest_dirs"),
1947 DebputyParseHint.not_path_error_hint(),
1948 DebputyParseHint.manifest_attribute("as"),
1949 ]
1950 ]
1953class ParsedMultiDestInstallRule(DebputyParsedContentStandardConditional):
1954 sources: list[FileSystemMatchRule]
1955 into: NotRequired[list[BinaryPackage]]
1956 dest_dirs: NotRequired[list[FileSystemExactMatchRule]]
1957 install_as: NotRequired[list[FileSystemExactMatchRule]]
1960class ParsedInstallExamplesRule(DebputyParsedContentStandardConditional):
1961 sources: list[FileSystemMatchRule]
1962 into: NotRequired[list[BinaryPackage]]
1965class ParsedInstallExamplesRuleSourceFormat(DebputyParsedContentStandardConditional):
1966 sources: NotRequired[list[FileSystemMatchRule]]
1967 source: NotRequired[
1968 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
1969 ]
1970 into: NotRequired[
1971 Annotated[
1972 str | list[str],
1973 DebputyParseHint.required_when_multi_binary(
1974 package_types=PackageTypeSelector.DEB
1975 ),
1976 ]
1977 ]
1980class ParsedInstallManpageRule(DebputyParsedContentStandardConditional):
1981 sources: list[FileSystemMatchRule]
1982 language: NotRequired[str]
1983 section: NotRequired[int]
1984 into: NotRequired[list[BinaryPackage]]
1987class ParsedInstallManpageRuleSourceFormat(DebputyParsedContentStandardConditional):
1988 sources: NotRequired[list[FileSystemMatchRule]]
1989 source: NotRequired[
1990 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
1991 ]
1992 language: NotRequired[str]
1993 section: NotRequired[int]
1994 into: NotRequired[
1995 Annotated[
1996 str | list[str],
1997 DebputyParseHint.required_when_multi_binary(
1998 package_types=PackageTypeSelector.DEB
1999 ),
2000 ]
2001 ]
2004class ParsedInstallDiscardRuleSourceFormat(DebputyParsedContent):
2005 paths: NotRequired[list[FileSystemMatchRule]]
2006 path: NotRequired[
2007 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("paths")]
2008 ]
2009 search_dir: NotRequired[
2010 Annotated[
2011 FileSystemExactMatchRule, DebputyParseHint.target_attribute("search_dirs")
2012 ]
2013 ]
2014 search_dirs: NotRequired[list[FileSystemExactMatchRule]]
2015 required_when: NotRequired[ManifestCondition]
2018class ParsedInstallDiscardRule(DebputyParsedContent):
2019 paths: list[FileSystemMatchRule]
2020 search_dirs: NotRequired[list[FileSystemExactMatchRule]]
2021 required_when: NotRequired[ManifestCondition]
2024class DpkgConffileManagementRuleBase(DebputyParsedContent):
2025 prior_to_version: NotRequired[str]
2026 owning_package: NotRequired[str]
2029class DpkgRenameConffileRule(DpkgConffileManagementRuleBase):
2030 source: str
2031 target: str
2034class DpkgRemoveConffileRule(DpkgConffileManagementRuleBase):
2035 path: str
2038class MCAnyOfAllOf(DebputyParsedContent):
2039 conditions: list[ManifestCondition]
2042class MCNot(DebputyParsedContent):
2043 negated_condition: ManifestCondition
2046class MCArchMatches(DebputyParsedContent):
2047 arch_matches: str
2050class MCBuildProfileMatches(DebputyParsedContent):
2051 build_profile_matches: str
2054def _parse_filename(
2055 filename: str,
2056 attribute_path: AttributePath,
2057 *,
2058 allow_directories: bool = True,
2059) -> str:
2060 try:
2061 normalized_path = _normalize_path(filename, with_prefix=False)
2062 except ValueError as e:
2063 raise ManifestParseException(
2064 f'Error parsing the path "{filename}" defined in {attribute_path.path}: {e.args[0]}'
2065 ) from None
2066 if not allow_directories and filename.endswith("/"): 2066 ↛ 2067line 2066 didn't jump to line 2067 because the condition on line 2066 was never true
2067 raise ManifestParseException(
2068 f'The path "{filename}" in {attribute_path.path} ends with "/" implying it is a directory,'
2069 f" but this feature can only be used for files"
2070 )
2071 if normalized_path == ".": 2071 ↛ 2072line 2071 didn't jump to line 2072 because the condition on line 2071 was never true
2072 raise ManifestParseException(
2073 f'The path "{filename}" in {attribute_path.path} looks like the root directory,'
2074 f" but this feature does not allow the root directory here."
2075 )
2076 return normalized_path
2079def _with_alt_form(t: type[TypedDict]):
2080 return Union[
2081 t,
2082 list[str],
2083 str,
2084 ]
2087def _dpkg_conffile_rename(
2088 _name: str,
2089 parsed_data: DpkgRenameConffileRule,
2090 path: AttributePath,
2091 _context: ParserContextData,
2092) -> DpkgMaintscriptHelperCommand:
2093 source_file = parsed_data["source"]
2094 target_file = parsed_data["target"]
2095 normalized_source = _parse_filename(
2096 source_file,
2097 path["source"],
2098 allow_directories=False,
2099 )
2100 path.path_hint = source_file
2102 normalized_target = _parse_filename(
2103 target_file,
2104 path["target"],
2105 allow_directories=False,
2106 )
2107 normalized_source = "/" + normalized_source
2108 normalized_target = "/" + normalized_target
2110 if normalized_source == normalized_target: 2110 ↛ 2111line 2110 didn't jump to line 2111 because the condition on line 2110 was never true
2111 raise ManifestParseException(
2112 f"Invalid rename defined in {path.path}: The source and target path are the same!"
2113 )
2115 version, owning_package = _parse_conffile_prior_version_and_owning_package(
2116 parsed_data, path
2117 )
2118 return DpkgMaintscriptHelperCommand.mv_conffile(
2119 path,
2120 normalized_source,
2121 normalized_target,
2122 version,
2123 owning_package,
2124 )
2127def _dpkg_conffile_remove(
2128 _name: str,
2129 parsed_data: DpkgRemoveConffileRule,
2130 path: AttributePath,
2131 _context: ParserContextData,
2132) -> DpkgMaintscriptHelperCommand:
2133 source_file = parsed_data["path"]
2134 normalized_source = _parse_filename(
2135 source_file,
2136 path["path"],
2137 allow_directories=False,
2138 )
2139 path.path_hint = source_file
2141 normalized_source = "/" + normalized_source
2143 version, owning_package = _parse_conffile_prior_version_and_owning_package(
2144 parsed_data, path
2145 )
2146 return DpkgMaintscriptHelperCommand.rm_conffile(
2147 path,
2148 normalized_source,
2149 version,
2150 owning_package,
2151 )
2154def _parse_conffile_prior_version_and_owning_package(
2155 d: DpkgConffileManagementRuleBase,
2156 attribute_path: AttributePath,
2157) -> tuple[str | None, str | None]:
2158 prior_version = d.get("prior_to_version")
2159 owning_package = d.get("owning_package")
2161 if prior_version is not None and not PKGVERSION_REGEX.match(prior_version): 2161 ↛ 2162line 2161 didn't jump to line 2162 because the condition on line 2161 was never true
2162 p = attribute_path["prior_to_version"]
2163 raise ManifestParseException(
2164 f"The {MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION} parameter in {p.path} must be a"
2165 r" valid package version (i.e., match (?:\d+:)?\d[0-9A-Za-z.+:~]*(?:-[0-9A-Za-z.+:~]+)*)."
2166 )
2168 if owning_package is not None and not PKGNAME_REGEX.match(owning_package): 2168 ↛ 2169line 2168 didn't jump to line 2169 because the condition on line 2168 was never true
2169 p = attribute_path["owning_package"]
2170 raise ManifestParseException(
2171 f"The {MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE} parameter in {p.path} must be a valid"
2172 f" package name (i.e., match {PKGNAME_REGEX.pattern})."
2173 )
2175 return prior_version, owning_package
2178def _install_rule_handler(
2179 _name: str,
2180 parsed_data: ParsedInstallRule,
2181 path: AttributePath,
2182 context: ParserContextData,
2183) -> InstallRule:
2184 sources = parsed_data["sources"]
2185 install_as = parsed_data.get("install_as")
2186 into = frozenset(
2187 parsed_data.get("into")
2188 or (context.single_binary_package(path, package_attribute="into"),)
2189 )
2190 dest_dir = parsed_data.get("dest_dir")
2191 condition = parsed_data.get("when")
2192 if install_as is not None:
2193 assert len(sources) == 1
2194 assert dest_dir is None
2195 return InstallRule.install_as(
2196 sources[0],
2197 install_as.match_rule.path,
2198 into,
2199 path.path,
2200 condition,
2201 )
2202 return InstallRule.install_dest(
2203 sources,
2204 dest_dir.match_rule.path if dest_dir is not None else None,
2205 into,
2206 path.path,
2207 condition,
2208 )
2211def _multi_dest_install_rule_handler(
2212 _name: str,
2213 parsed_data: ParsedMultiDestInstallRule,
2214 path: AttributePath,
2215 context: ParserContextData,
2216) -> InstallRule:
2217 sources = parsed_data["sources"]
2218 install_as = parsed_data.get("install_as")
2219 into = frozenset(
2220 parsed_data.get("into")
2221 or (context.single_binary_package(path, package_attribute="into"),)
2222 )
2223 dest_dirs = parsed_data.get("dest_dirs")
2224 condition = parsed_data.get("when")
2225 if install_as is not None:
2226 assert len(sources) == 1
2227 assert dest_dirs is None
2228 if len(install_as) < 2: 2228 ↛ 2229line 2228 didn't jump to line 2229 because the condition on line 2228 was never true
2229 raise ManifestParseException(
2230 f"The {path['install_as'].path} attribute must contain at least two paths."
2231 )
2232 return InstallRule.install_multi_as(
2233 sources[0],
2234 [p.match_rule.path for p in install_as],
2235 into,
2236 path.path,
2237 condition,
2238 )
2239 if dest_dirs is None: 2239 ↛ 2240line 2239 didn't jump to line 2240 because the condition on line 2239 was never true
2240 raise ManifestParseException(
2241 f"Either the `as` or the `dest-dirs` key must be provided at {path.path}"
2242 )
2243 if len(dest_dirs) < 2: 2243 ↛ 2244line 2243 didn't jump to line 2244 because the condition on line 2243 was never true
2244 raise ManifestParseException(
2245 f"The {path['dest_dirs'].path} attribute must contain at least two paths."
2246 )
2247 return InstallRule.install_multi_dest(
2248 sources,
2249 [dd.match_rule.path for dd in dest_dirs],
2250 into,
2251 path.path,
2252 condition,
2253 )
2256def _install_docs_rule_handler(
2257 _name: str,
2258 parsed_data: ParsedInstallRule,
2259 path: AttributePath,
2260 context: ParserContextData,
2261) -> InstallRule:
2262 sources = parsed_data["sources"]
2263 install_as = parsed_data.get("install_as")
2264 dest_dir = parsed_data.get("dest_dir")
2265 condition = parsed_data.get("when")
2266 into = frozenset(
2267 parsed_data.get("into")
2268 or (
2269 context.single_binary_package(
2270 path,
2271 package_types=PackageTypeSelector.DEB,
2272 package_attribute="into",
2273 ),
2274 )
2275 )
2276 if install_as is not None: 2276 ↛ 2277line 2276 didn't jump to line 2277 because the condition on line 2276 was never true
2277 assert len(sources) == 1
2278 assert dest_dir is None
2279 return InstallRule.install_doc_as(
2280 sources[0],
2281 install_as.match_rule.path,
2282 into,
2283 path.path,
2284 condition,
2285 )
2286 return InstallRule.install_doc(
2287 sources,
2288 None if dest_dir is None else dest_dir.raw_match_rule,
2289 into,
2290 path.path,
2291 condition,
2292 )
2295def _install_examples_rule_handler(
2296 _name: str,
2297 parsed_data: ParsedInstallExamplesRule,
2298 path: AttributePath,
2299 context: ParserContextData,
2300) -> InstallRule:
2301 return InstallRule.install_examples(
2302 sources=parsed_data["sources"],
2303 into=frozenset(
2304 parsed_data.get("into")
2305 or (
2306 context.single_binary_package(
2307 path,
2308 package_types=PackageTypeSelector.DEB,
2309 package_attribute="into",
2310 ),
2311 )
2312 ),
2313 definition_source=path.path,
2314 condition=parsed_data.get("when"),
2315 )
2318def _install_man_rule_handler(
2319 _name: str,
2320 parsed_data: ParsedInstallManpageRule,
2321 attribute_path: AttributePath,
2322 context: ParserContextData,
2323) -> InstallRule:
2324 sources = parsed_data["sources"]
2325 language = parsed_data.get("language")
2326 section = parsed_data.get("section")
2328 if language is not None:
2329 is_lang_ok = language in (
2330 "C",
2331 "derive-from-basename",
2332 "derive-from-path",
2333 )
2335 if not is_lang_ok and len(language) == 2 and language.islower(): 2335 ↛ 2336line 2335 didn't jump to line 2336 because the condition on line 2335 was never true
2336 is_lang_ok = True
2338 if ( 2338 ↛ 2345line 2338 didn't jump to line 2345 because the condition on line 2338 was never true
2339 not is_lang_ok
2340 and len(language) == 5
2341 and language[2] == "_"
2342 and language[:2].islower()
2343 and language[3:].isupper()
2344 ):
2345 is_lang_ok = True
2347 if not is_lang_ok: 2347 ↛ 2348line 2347 didn't jump to line 2348 because the condition on line 2347 was never true
2348 raise ManifestParseException(
2349 f'The language attribute must in a 2-letter language code ("de"), a 5-letter language + dialect'
2350 f' code ("pt_BR"), "derive-from-basename", "derive-from-path", or omitted. The problematic'
2351 f' definition is {attribute_path["language"]}'
2352 )
2354 if section is not None and (section < 1 or section > 10): 2354 ↛ 2355line 2354 didn't jump to line 2355 because the condition on line 2354 was never true
2355 raise ManifestParseException(
2356 f"The section attribute must in the range [1-9] or omitted. The problematic definition is"
2357 f' {attribute_path["section"]}'
2358 )
2359 if section is None and any(s.raw_match_rule.endswith(".gz") for s in sources): 2359 ↛ 2360line 2359 didn't jump to line 2360 because the condition on line 2359 was never true
2360 raise ManifestParseException(
2361 "Sorry, compressed man pages are not supported without an explicit `section` definition at the moment."
2362 " This limitation may be removed in the future. Problematic definition from"
2363 f' {attribute_path["sources"]}'
2364 )
2365 if any(s.raw_match_rule.endswith("/") for s in sources): 2365 ↛ 2366line 2365 didn't jump to line 2366 because the condition on line 2365 was never true
2366 raise ManifestParseException(
2367 'The install-man rule can only match non-directories. Therefore, none of the sources can end with "/".'
2368 " as that implies the source is for a directory. Problematic definition from"
2369 f' {attribute_path["sources"]}'
2370 )
2371 return InstallRule.install_man(
2372 sources=sources,
2373 into=frozenset(
2374 parsed_data.get("into")
2375 or (
2376 context.single_binary_package(
2377 attribute_path,
2378 package_types=PackageTypeSelector.DEB,
2379 package_attribute="into",
2380 ),
2381 )
2382 ),
2383 section=section,
2384 language=language,
2385 definition_source=attribute_path.path,
2386 condition=parsed_data.get("when"),
2387 )
2390def _install_discard_rule_handler(
2391 _name: str,
2392 parsed_data: ParsedInstallDiscardRule,
2393 path: AttributePath,
2394 _context: ParserContextData,
2395) -> InstallRule:
2396 limit_to = parsed_data.get("search_dirs")
2397 if limit_to is not None and not limit_to: 2397 ↛ 2398line 2397 didn't jump to line 2398 because the condition on line 2397 was never true
2398 p = path["search_dirs"]
2399 raise ManifestParseException(f"The {p.path} attribute must not be empty.")
2400 condition = parsed_data.get("required_when")
2401 return InstallRule.discard_paths(
2402 parsed_data["paths"],
2403 path.path,
2404 condition,
2405 limit_to=limit_to,
2406 )
2409def _transformation_move_handler(
2410 _name: str,
2411 parsed_data: TransformationMoveRuleSpec,
2412 path: AttributePath,
2413 _context: ParserContextData,
2414) -> TransformationRule:
2415 source_match = parsed_data["source"]
2416 target_path = parsed_data["target"].match_rule.path
2417 condition = parsed_data.get("when")
2419 if ( 2419 ↛ 2423line 2419 didn't jump to line 2423 because the condition on line 2419 was never true
2420 isinstance(source_match, ExactFileSystemPath)
2421 and source_match.path == target_path
2422 ):
2423 raise ManifestParseException(
2424 f"The transformation rule {path.path} requests a move of {source_match} to"
2425 f" {target_path}, which is the same path"
2426 )
2427 return MoveTransformationRule(
2428 source_match.match_rule,
2429 target_path,
2430 target_path.endswith("/"),
2431 path,
2432 condition,
2433 )
2436def _transformation_remove_handler(
2437 _name: str,
2438 parsed_data: TransformationRemoveRuleSpec,
2439 attribute_path: AttributePath,
2440 _context: ParserContextData,
2441) -> TransformationRule:
2442 paths = parsed_data["paths"]
2443 keep_empty_parent_dirs = parsed_data.get("keep_empty_parent_dirs", False)
2445 return RemoveTransformationRule(
2446 [m.match_rule for m in paths],
2447 keep_empty_parent_dirs,
2448 attribute_path,
2449 )
2452def _transformation_create_symlink(
2453 _name: str,
2454 parsed_data: CreateSymlinkRule,
2455 attribute_path: AttributePath,
2456 _context: ParserContextData,
2457) -> TransformationRule:
2458 link_dest = parsed_data["path"].match_rule.path
2459 replacement_rule: CreateSymlinkReplacementRule = parsed_data.get(
2460 "replacement_rule",
2461 "abort-on-non-empty-directory",
2462 )
2463 try:
2464 link_target = debian_policy_normalize_symlink_target(
2465 link_dest,
2466 parsed_data["target"].symlink_target,
2467 )
2468 except ValueError as e: # pragma: no cover
2469 raise AssertionError(
2470 "Debian Policy normalization should not raise ValueError here"
2471 ) from e
2473 condition = parsed_data.get("when")
2475 return CreateSymlinkPathTransformationRule(
2476 link_target,
2477 link_dest,
2478 replacement_rule,
2479 attribute_path,
2480 condition,
2481 )
2484def _transformation_path_metadata(
2485 _name: str,
2486 parsed_data: PathManifestRule,
2487 attribute_path: AttributePath,
2488 context: ParserContextData,
2489) -> TransformationRule:
2490 match_rules = parsed_data["paths"]
2491 owner = parsed_data.get("owner")
2492 group = parsed_data.get("group")
2493 mode = parsed_data.get("mode")
2494 recursive = parsed_data.get("recursive", False)
2495 capabilities = parsed_data.get("capabilities")
2496 capability_mode = parsed_data.get("capability_mode")
2497 cap: str | None = None
2499 if capabilities is not None: 2499 ↛ 2500line 2499 didn't jump to line 2500 because the condition on line 2499 was never true
2500 check_integration_mode(
2501 attribute_path["capabilities"],
2502 context,
2503 _NOT_INTEGRATION_RRR,
2504 )
2505 if capability_mode is None:
2506 capability_mode = SymbolicMode.parse_filesystem_mode(
2507 "a-s",
2508 attribute_path["capability-mode"],
2509 )
2510 cap = capabilities.value
2511 validate_cap = check_cap_checker()
2512 validate_cap(cap, attribute_path["capabilities"].path)
2513 elif capability_mode is not None and capabilities is None: 2513 ↛ 2514line 2513 didn't jump to line 2514 because the condition on line 2513 was never true
2514 check_integration_mode(
2515 attribute_path["capability_mode"],
2516 context,
2517 _NOT_INTEGRATION_RRR,
2518 )
2519 raise ManifestParseException(
2520 "The attribute capability-mode cannot be provided without capabilities"
2521 f" in {attribute_path.path}"
2522 )
2523 if owner is None and group is None and mode is None and capabilities is None: 2523 ↛ 2524line 2523 didn't jump to line 2524 because the condition on line 2523 was never true
2524 raise ManifestParseException(
2525 "At least one of owner, group, mode, or capabilities must be provided"
2526 f" in {attribute_path.path}"
2527 )
2528 condition = parsed_data.get("when")
2530 return PathMetadataTransformationRule(
2531 [m.match_rule for m in match_rules],
2532 owner,
2533 group,
2534 mode,
2535 recursive,
2536 cap,
2537 capability_mode,
2538 attribute_path.path,
2539 condition,
2540 )
2543def _transformation_mkdirs(
2544 _name: str,
2545 parsed_data: EnsureDirectoryRule,
2546 attribute_path: AttributePath,
2547 _context: ParserContextData,
2548) -> TransformationRule:
2549 provided_paths = parsed_data["paths"]
2550 owner = parsed_data.get("owner")
2551 group = parsed_data.get("group")
2552 mode = parsed_data.get("mode")
2554 condition = parsed_data.get("when")
2556 return CreateDirectoryTransformationRule(
2557 [p.match_rule.path for p in provided_paths],
2558 owner,
2559 group,
2560 mode,
2561 attribute_path.path,
2562 condition,
2563 )
2566def _at_least_two(
2567 content: list[Any],
2568 attribute_path: AttributePath,
2569 attribute_name: str,
2570) -> None:
2571 if len(content) < 2: 2571 ↛ 2572line 2571 didn't jump to line 2572 because the condition on line 2571 was never true
2572 raise ManifestParseException(
2573 f"Must have at least two conditions in {attribute_path[attribute_name].path}"
2574 )
2577def _mc_any_of(
2578 name: str,
2579 parsed_data: MCAnyOfAllOf,
2580 attribute_path: AttributePath,
2581 _context: ParserContextData,
2582) -> ManifestCondition:
2583 conditions = parsed_data["conditions"]
2584 _at_least_two(conditions, attribute_path, "conditions")
2585 if name == "any-of": 2585 ↛ 2586line 2585 didn't jump to line 2586 because the condition on line 2585 was never true
2586 return ManifestCondition.any_of(conditions)
2587 assert name == "all-of"
2588 return ManifestCondition.all_of(conditions)
2591def _mc_not(
2592 _name: str,
2593 parsed_data: MCNot,
2594 _attribute_path: AttributePath,
2595 _context: ParserContextData,
2596) -> ManifestCondition:
2597 condition = parsed_data["negated_condition"]
2598 return condition.negated()
2601def _extract_arch_matches(
2602 parsed_data: MCArchMatches,
2603 attribute_path: AttributePath,
2604) -> list[str]:
2605 arch_matches_as_str = parsed_data["arch_matches"]
2606 # Can we check arch list for typos? If we do, it must be tight in how close matches it does.
2607 # Consider "arm" vs. "armel" (edit distance 2, but both are valid). Likewise, names often
2608 # include a bit indicator "foo", "foo32", "foo64" - all of these have an edit distance of 2
2609 # of each other.
2610 arch_matches_as_list = arch_matches_as_str.split()
2611 attr_path = attribute_path["arch_matches"]
2612 if not arch_matches_as_list: 2612 ↛ 2613line 2612 didn't jump to line 2613 because the condition on line 2612 was never true
2613 raise ManifestParseException(
2614 f"The condition at {attr_path.path} must not be empty"
2615 )
2617 if arch_matches_as_list[0].startswith("[") or arch_matches_as_list[-1].endswith( 2617 ↛ 2620line 2617 didn't jump to line 2620 because the condition on line 2617 was never true
2618 "]"
2619 ):
2620 raise ManifestParseException(
2621 f"The architecture match at {attr_path.path} must be defined without enclosing it with "
2622 '"[" or/and "]" brackets'
2623 )
2624 return arch_matches_as_list
2627def _mc_source_context_arch_matches(
2628 _name: str,
2629 parsed_data: MCArchMatches,
2630 attribute_path: AttributePath,
2631 _context: ParserContextData,
2632) -> ManifestCondition:
2633 arch_matches = _extract_arch_matches(parsed_data, attribute_path)
2634 return SourceContextArchMatchManifestCondition(arch_matches)
2637def _mc_package_context_arch_matches(
2638 name: str,
2639 parsed_data: MCArchMatches,
2640 attribute_path: AttributePath,
2641 context: ParserContextData,
2642) -> ManifestCondition:
2643 arch_matches = _extract_arch_matches(parsed_data, attribute_path)
2645 if not context.is_in_binary_package_state: 2645 ↛ 2646line 2645 didn't jump to line 2646 because the condition on line 2645 was never true
2646 raise ManifestParseException(
2647 f'The condition "{name}" at {attribute_path.path} can only be used in the context of a binary package.'
2648 )
2650 package_state = context.current_binary_package_state
2651 if package_state.binary_package.is_arch_all: 2651 ↛ 2652line 2651 didn't jump to line 2652 because the condition on line 2651 was never true
2652 result = context.dpkg_arch_query_table.architecture_is_concerned(
2653 "all", arch_matches
2654 )
2655 attr_path = attribute_path["arch_matches"]
2656 raise ManifestParseException(
2657 f"The package architecture restriction at {attr_path.path} is applied to the"
2658 f' "Architecture: all" package {package_state.binary_package.name}, which does not make sense'
2659 f" as the condition will always resolves to `{str(result).lower()}`."
2660 f" If you **really** need an architecture specific constraint for this rule, consider using"
2661 f' "source-context-arch-matches" instead. However, this is a very rare use-case!'
2662 )
2663 return BinaryPackageContextArchMatchManifestCondition(arch_matches)
2666def _mc_arch_matches(
2667 name: str,
2668 parsed_data: MCArchMatches,
2669 attribute_path: AttributePath,
2670 context: ParserContextData,
2671) -> ManifestCondition:
2672 if context.is_in_binary_package_state:
2673 return _mc_package_context_arch_matches(
2674 name, parsed_data, attribute_path, context
2675 )
2676 return _mc_source_context_arch_matches(name, parsed_data, attribute_path, context)
2679def _mc_build_profile_matches(
2680 _name: str,
2681 parsed_data: MCBuildProfileMatches,
2682 attribute_path: AttributePath,
2683 _context: ParserContextData,
2684) -> ManifestCondition:
2685 build_profile_spec = parsed_data["build_profile_matches"].strip()
2686 attr_path = attribute_path["build_profile_matches"]
2687 if not build_profile_spec: 2687 ↛ 2688line 2687 didn't jump to line 2688 because the condition on line 2687 was never true
2688 raise ManifestParseException(
2689 f"The condition at {attr_path.path} must not be empty"
2690 )
2691 try:
2692 active_profiles_match(build_profile_spec, frozenset())
2693 except ValueError as e:
2694 raise ManifestParseException(
2695 f"Could not parse the build specification at {attr_path.path}: {e.args[0]}"
2696 )
2697 return BuildProfileMatch(build_profile_spec)