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