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