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