summaryrefslogtreecommitdiffstats
path: root/src/debputy/plugin/debputy/private_api.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/plugin/debputy/private_api.py')
-rw-r--r--src/debputy/plugin/debputy/private_api.py2931
1 files changed, 2931 insertions, 0 deletions
diff --git a/src/debputy/plugin/debputy/private_api.py b/src/debputy/plugin/debputy/private_api.py
new file mode 100644
index 0000000..2db2b56
--- /dev/null
+++ b/src/debputy/plugin/debputy/private_api.py
@@ -0,0 +1,2931 @@
+import ctypes
+import ctypes.util
+import functools
+import itertools
+import textwrap
+import time
+from datetime import datetime
+from typing import (
+ cast,
+ NotRequired,
+ Optional,
+ Tuple,
+ Union,
+ Type,
+ TypedDict,
+ List,
+ Annotated,
+ Any,
+ Dict,
+ Callable,
+)
+
+from debian.changelog import Changelog
+from debian.deb822 import Deb822
+
+from debputy import DEBPUTY_DOC_ROOT_DIR
+from debputy._manifest_constants import (
+ MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE,
+ MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION,
+ MK_INSTALLATIONS_INSTALL_EXAMPLES,
+ MK_INSTALLATIONS_INSTALL,
+ MK_INSTALLATIONS_INSTALL_DOCS,
+ MK_INSTALLATIONS_INSTALL_MAN,
+ MK_INSTALLATIONS_DISCARD,
+ MK_INSTALLATIONS_MULTI_DEST_INSTALL,
+)
+from debputy.exceptions import DebputyManifestVariableRequiresDebianDirError
+from debputy.installations import InstallRule
+from debputy.maintscript_snippet import DpkgMaintscriptHelperCommand
+from debputy.manifest_conditions import (
+ ManifestCondition,
+ BinaryPackageContextArchMatchManifestCondition,
+ BuildProfileMatch,
+ SourceContextArchMatchManifestCondition,
+)
+from debputy.manifest_parser.base_types import (
+ DebputyParsedContent,
+ DebputyParsedContentStandardConditional,
+ FileSystemMode,
+ StaticFileSystemOwner,
+ StaticFileSystemGroup,
+ SymlinkTarget,
+ FileSystemExactMatchRule,
+ FileSystemMatchRule,
+ SymbolicMode,
+ TypeMapping,
+ OctalMode,
+ FileSystemExactNonDirMatchRule,
+)
+from debputy.manifest_parser.declarative_parser import DebputyParseHint
+from debputy.manifest_parser.exceptions import ManifestParseException
+from debputy.manifest_parser.mapper_code import type_mapper_str2package
+from debputy.manifest_parser.parser_data import ParserContextData
+from debputy.manifest_parser.util import AttributePath
+from debputy.packages import BinaryPackage
+from debputy.path_matcher import ExactFileSystemPath
+from debputy.plugin.api import (
+ DebputyPluginInitializer,
+ documented_attr,
+ reference_documentation,
+ VirtualPath,
+ packager_provided_file_reference_documentation,
+)
+from debputy.plugin.api.impl import DebputyPluginInitializerProvider
+from debputy.plugin.api.impl_types import automatic_discard_rule_example, PPFFormatParam
+from debputy.plugin.api.spec import (
+ type_mapping_reference_documentation,
+ type_mapping_example,
+)
+from debputy.plugin.debputy.binary_package_rules import register_binary_package_rules
+from debputy.plugin.debputy.discard_rules import (
+ _debputy_discard_pyc_files,
+ _debputy_prune_la_files,
+ _debputy_prune_doxygen_cruft,
+ _debputy_prune_binary_debian_dir,
+ _debputy_prune_info_dir_file,
+ _debputy_prune_backup_files,
+ _debputy_prune_vcs_paths,
+)
+from debputy.plugin.debputy.manifest_root_rules import register_manifest_root_rules
+from debputy.plugin.debputy.package_processors import (
+ process_manpages,
+ apply_compression,
+ clean_la_files,
+)
+from debputy.plugin.debputy.service_management import (
+ detect_systemd_service_files,
+ generate_snippets_for_systemd_units,
+ detect_sysv_init_service_files,
+ generate_snippets_for_init_scripts,
+)
+from debputy.plugin.debputy.shlib_metadata_detectors import detect_shlibdeps
+from debputy.plugin.debputy.strip_non_determinism import strip_non_determinism
+from debputy.substitution import VariableContext
+from debputy.transformation_rules import (
+ CreateSymlinkReplacementRule,
+ TransformationRule,
+ CreateDirectoryTransformationRule,
+ RemoveTransformationRule,
+ MoveTransformationRule,
+ PathMetadataTransformationRule,
+ CreateSymlinkPathTransformationRule,
+)
+from debputy.util import (
+ _normalize_path,
+ PKGNAME_REGEX,
+ PKGVERSION_REGEX,
+ debian_policy_normalize_symlink_target,
+ active_profiles_match,
+ _error,
+ _warn,
+ _info,
+ assume_not_none,
+)
+
+_DOCUMENTED_DPKG_ARCH_TYPES = {
+ "HOST": (
+ "installed on",
+ "The package will be **installed** on this type of machine / system",
+ ),
+ "BUILD": (
+ "compiled on",
+ "The compilation of this package will be performed **on** this kind of machine / system",
+ ),
+ "TARGET": (
+ "cross-compiler output",
+ "When building a cross-compiler, it will produce output for this kind of machine/system",
+ ),
+}
+
+_DOCUMENTED_DPKG_ARCH_VARS = {
+ "ARCH": "Debian's name for the architecture",
+ "ARCH_ABI": "Debian's name for the architecture ABI",
+ "ARCH_BITS": "Number of bits in the pointer size",
+ "ARCH_CPU": "Debian's name for the CPU type",
+ "ARCH_ENDIAN": "Endianness of the architecture (little/big)",
+ "ARCH_LIBC": "Debian's name for the libc implementation",
+ "ARCH_OS": "Debian name for the OS/kernel",
+ "GNU_CPU": "GNU's name for the CPU",
+ "GNU_SYSTEM": "GNU's name for the system",
+ "GNU_TYPE": "GNU system type (GNU_CPU and GNU_SYSTEM combined)",
+ "MULTIARCH": "Multi-arch tuple",
+}
+
+
+def _manifest_format_doc(anchor: str) -> str:
+ return f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md#{anchor}"
+
+
+@functools.lru_cache
+def load_libcap() -> Tuple[bool, Optional[str], Callable[[str], bool]]:
+ cap_library_path = ctypes.util.find_library("cap.so")
+ has_libcap = False
+ libcap = None
+ if cap_library_path:
+ try:
+ libcap = ctypes.cdll.LoadLibrary(cap_library_path)
+ has_libcap = True
+ except OSError:
+ pass
+
+ if libcap is None:
+ warned = False
+
+ def _is_valid_cap(cap: str) -> bool:
+ nonlocal warned
+ if not warned:
+ _info(
+ "Could not load libcap.so; will not validate capabilities. Use `apt install libcap2` to provide"
+ " checking of capabilities."
+ )
+ warned = True
+ return True
+
+ else:
+ # cap_t cap_from_text(const char *path_p)
+ libcap.cap_from_text.argtypes = [ctypes.c_char_p]
+ libcap.cap_from_text.restype = ctypes.c_char_p
+
+ libcap.cap_free.argtypes = [ctypes.c_void_p]
+ libcap.cap_free.restype = None
+
+ def _is_valid_cap(cap: str) -> bool:
+ cap_t = libcap.cap_from_text(cap.encode("utf-8"))
+ ok = cap_t is not None
+ libcap.cap_free(cap_t)
+ return ok
+
+ return has_libcap, cap_library_path, _is_valid_cap
+
+
+def check_cap_checker() -> Callable[[str, str], None]:
+ _, libcap_path, is_valid_cap = load_libcap()
+
+ seen_cap = set()
+
+ def _check_cap(cap: str, definition_source: str) -> None:
+ if cap not in seen_cap and not is_valid_cap(cap):
+ seen_cap.add(cap)
+ cap_path = f" ({libcap_path})" if libcap_path is not None else ""
+ _warn(
+ f'The capabilities "{cap}" provided in {definition_source} were not understood by'
+ f" libcap.so{cap_path}. Please verify you provided the correct capabilities."
+ f" Note: This warning can be a false-positive if you are targeting a newer libcap.so"
+ f" than the one installed on this system."
+ )
+
+ return _check_cap
+
+
+def load_source_variables(variable_context: VariableContext) -> Dict[str, str]:
+ try:
+ changelog = variable_context.debian_dir.lookup("changelog")
+ if changelog is None:
+ raise DebputyManifestVariableRequiresDebianDirError(
+ "The changelog was not present"
+ )
+ with changelog.open() as fd:
+ dch = Changelog(fd, max_blocks=2)
+ except FileNotFoundError as e:
+ raise DebputyManifestVariableRequiresDebianDirError(
+ "The changelog was not present"
+ ) from e
+ first_entry = dch[0]
+ first_non_binnmu_entry = dch[0]
+ if first_non_binnmu_entry.other_pairs.get("binary-only", "no") == "yes":
+ first_non_binnmu_entry = dch[1]
+ assert first_non_binnmu_entry.other_pairs.get("binary-only", "no") == "no"
+ source_version = first_entry.version
+ epoch = source_version.epoch
+ upstream_version = source_version.upstream_version
+ debian_revision = source_version.debian_revision
+ epoch_upstream = upstream_version
+ upstream_debian_revision = upstream_version
+ if epoch is not None and epoch != "":
+ epoch_upstream = f"{epoch}:{upstream_version}"
+ if debian_revision is not None and debian_revision != "":
+ upstream_debian_revision = f"{upstream_version}-{debian_revision}"
+
+ package = first_entry.package
+ if package is None:
+ _error("Cannot determine the source package name from debian/changelog.")
+
+ date = first_entry.date
+ if date is not None:
+ local_time = datetime.strptime(date, "%a, %d %b %Y %H:%M:%S %z")
+ source_date_epoch = str(int(local_time.timestamp()))
+ else:
+ _warn(
+ "The latest changelog entry does not have a (parsable) date, using current time"
+ " for SOURCE_DATE_EPOCH"
+ )
+ source_date_epoch = str(int(time.time()))
+
+ if first_non_binnmu_entry is not first_entry:
+ non_binnmu_date = first_non_binnmu_entry.date
+ if non_binnmu_date is not None:
+ local_time = datetime.strptime(non_binnmu_date, "%a, %d %b %Y %H:%M:%S %z")
+ snd_source_date_epoch = str(int(local_time.timestamp()))
+ else:
+ _warn(
+ "The latest (non-binNMU) changelog entry does not have a (parsable) date, using current time"
+ " for SOURCE_DATE_EPOCH (for strip-nondeterminism)"
+ )
+ snd_source_date_epoch = source_date_epoch = str(int(time.time()))
+ else:
+ snd_source_date_epoch = source_date_epoch
+ return {
+ "DEB_SOURCE": package,
+ "DEB_VERSION": source_version.full_version,
+ "DEB_VERSION_EPOCH_UPSTREAM": epoch_upstream,
+ "DEB_VERSION_UPSTREAM_REVISION": upstream_debian_revision,
+ "DEB_VERSION_UPSTREAM": upstream_version,
+ "SOURCE_DATE_EPOCH": source_date_epoch,
+ "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": str(first_non_binnmu_entry.version),
+ "_DEBPUTY_SND_SOURCE_DATE_EPOCH": snd_source_date_epoch,
+ }
+
+
+def initialize_via_private_api(public_api: DebputyPluginInitializer) -> None:
+ api = cast("DebputyPluginInitializerProvider", public_api)
+
+ api.metadata_or_maintscript_detector(
+ "dpkg-shlibdeps",
+ # Private because detect_shlibdeps expects private API (hench this cast)
+ cast("MetadataAutoDetector", detect_shlibdeps),
+ package_type={"deb", "udeb"},
+ )
+ register_type_mappings(api)
+ register_variables_via_private_api(api)
+ document_builtin_variables(api)
+ register_automatic_discard_rules(api)
+ register_special_ppfs(api)
+ register_install_rules(api)
+ register_transformation_rules(api)
+ register_manifest_condition_rules(api)
+ register_dpkg_conffile_rules(api)
+ register_processing_steps(api)
+ register_service_managers(api)
+ register_manifest_root_rules(api)
+ register_binary_package_rules(api)
+
+
+def register_type_mappings(api: DebputyPluginInitializerProvider) -> None:
+ api.register_mapped_type(
+ TypeMapping(
+ FileSystemMatchRule,
+ str,
+ FileSystemMatchRule.parse_path_match,
+ ),
+ reference_documentation=type_mapping_reference_documentation(
+ description=textwrap.dedent(
+ """\
+ A generic file system path match with globs.
+
+ Manifest variable substitution will be applied and glob expansion will be performed.
+
+ The match will be read as one of the following cases:
+
+ - Exact path match if there is no globs characters like `usr/bin/debputy`
+ - A basename glob like `*.txt` or `**/foo`
+ - A generic path glob otherwise like `usr/lib/*.so*`
+
+ Except for basename globs, all matches are always relative to the root directory of
+ the match, which is typically the package root directory or a search directory.
+
+ For basename globs, any path matching that basename beneath the package root directory
+ or relevant search directories will match.
+
+ Please keep in mind that:
+
+ * glob patterns often have to be quoted as YAML interpret the glob metacharacter as
+ an anchor reference.
+
+ * Directories can be matched via this type. Whether the rule using this type
+ recurse into the directory depends on the usage and not this type. Related, if
+ value for this rule ends with a literal "/", then the definition can *only* match
+ directories (similar to the shell).
+
+ * path matches involving glob expansion are often subject to different rules than
+ path matches without them. As an example, automatic discard rules does not apply
+ to exact path matches, but they will filter out glob matches.
+ """,
+ ),
+ examples=[
+ type_mapping_example("usr/bin/debputy"),
+ type_mapping_example("*.txt"),
+ type_mapping_example("**/foo"),
+ type_mapping_example("usr/lib/*.so*"),
+ type_mapping_example("usr/share/foo/data-*/"),
+ ],
+ ),
+ )
+
+ api.register_mapped_type(
+ TypeMapping(
+ FileSystemExactMatchRule,
+ str,
+ FileSystemExactMatchRule.parse_path_match,
+ ),
+ reference_documentation=type_mapping_reference_documentation(
+ description=textwrap.dedent(
+ """\
+ A file system match that does **not** expand globs.
+
+ Manifest variable substitution will be applied. However, globs will not be expanded.
+ Any glob metacharacters will be interpreted as a literal part of path.
+
+ Note that a directory can be matched via this type. Whether the rule using this type
+ recurse into the directory depends on the usage and is not defined by this type.
+ Related, if value for this rule ends with a literal "/", then the definition can
+ *only* match directories (similar to the shell).
+ """,
+ ),
+ examples=[
+ type_mapping_example("usr/bin/dpkg"),
+ type_mapping_example("usr/share/foo/"),
+ type_mapping_example("usr/share/foo/data.txt"),
+ ],
+ ),
+ )
+
+ api.register_mapped_type(
+ TypeMapping(
+ FileSystemExactNonDirMatchRule,
+ str,
+ FileSystemExactNonDirMatchRule.parse_path_match,
+ ),
+ reference_documentation=type_mapping_reference_documentation(
+ description=textwrap.dedent(
+ f"""\
+ A file system match that does **not** expand globs and must not match a directory.
+
+ Manifest variable substitution will be applied. However, globs will not be expanded.
+ Any glob metacharacters will be interpreted as a literal part of path.
+
+ This is like {FileSystemExactMatchRule.__name__} except that the match will fail if the
+ provided path matches a directory. Since a directory cannot be matched, it is an error
+ for any input to end with a "/" as only directories can be matched if the path ends
+ with a "/".
+ """,
+ ),
+ examples=[
+ type_mapping_example("usr/bin/dh_debputy"),
+ type_mapping_example("usr/share/foo/data.txt"),
+ ],
+ ),
+ )
+
+ api.register_mapped_type(
+ TypeMapping(
+ SymlinkTarget,
+ str,
+ lambda v, ap, pc: SymlinkTarget.parse_symlink_target(
+ v, ap, assume_not_none(pc).substitution
+ ),
+ ),
+ reference_documentation=type_mapping_reference_documentation(
+ description=textwrap.dedent(
+ """\
+ A symlink target.
+
+ Manifest variable substitution will be applied. This is distinct from an exact file
+ system match in that a symlink target is not relative to the package root by default
+ (explicitly prefix for "/" for absolute path targets)
+
+ Note that `debputy` will policy normalize symlinks when assembling the deb, so
+ use of relative or absolute symlinks comes down to preference.
+ """,
+ ),
+ examples=[
+ type_mapping_example("../foo"),
+ type_mapping_example("/usr/share/doc/bar"),
+ ],
+ ),
+ )
+
+ api.register_mapped_type(
+ TypeMapping(
+ StaticFileSystemOwner,
+ Union[int, str],
+ lambda v, ap, _: StaticFileSystemOwner.from_manifest_value(v, ap),
+ ),
+ reference_documentation=type_mapping_reference_documentation(
+ description=textwrap.dedent(
+ """\
+ File system owner reference that is part of the passwd base data (such as "root").
+
+ The group can be provided in either of the following three forms:
+
+ * A name (recommended), such as "root"
+ * The UID in the form of an integer (that is, no quoting), such as 0 (for "root")
+ * The name and the UID separated by colon such as "root:0" (for "root").
+
+ Note in the last case, the `debputy` will validate that the name and the UID match.
+
+ Some owners (such as "nobody") are deliberately disallowed.
+ """
+ ),
+ examples=[
+ type_mapping_example("root"),
+ type_mapping_example(0),
+ type_mapping_example("root:0"),
+ type_mapping_example("bin"),
+ ],
+ ),
+ )
+ api.register_mapped_type(
+ TypeMapping(
+ StaticFileSystemGroup,
+ Union[int, str],
+ lambda v, ap, _: StaticFileSystemGroup.from_manifest_value(v, ap),
+ ),
+ reference_documentation=type_mapping_reference_documentation(
+ description=textwrap.dedent(
+ """\
+ File system group reference that is part of the passwd base data (such as "root").
+
+ The group can be provided in either of the following three forms:
+
+ * A name (recommended), such as "root"
+ * The GID in the form of an integer (that is, no quoting), such as 0 (for "root")
+ * The name and the GID separated by colon such as "root:0" (for "root").
+
+ Note in the last case, the `debputy` will validate that the name and the GID match.
+
+ Some owners (such as "nobody") are deliberately disallowed.
+ """
+ ),
+ examples=[
+ type_mapping_example("root"),
+ type_mapping_example(0),
+ type_mapping_example("root:0"),
+ type_mapping_example("tty"),
+ ],
+ ),
+ )
+
+ api.register_mapped_type(
+ TypeMapping(
+ BinaryPackage,
+ str,
+ type_mapper_str2package,
+ ),
+ reference_documentation=type_mapping_reference_documentation(
+ description="Name of a package in debian/control",
+ ),
+ )
+
+ api.register_mapped_type(
+ TypeMapping(
+ FileSystemMode,
+ str,
+ lambda v, ap, _: FileSystemMode.parse_filesystem_mode(v, ap),
+ ),
+ reference_documentation=type_mapping_reference_documentation(
+ description="Either an octal mode or symbolic mode",
+ examples=[
+ type_mapping_example("a+x"),
+ type_mapping_example("u=rwX,go=rX"),
+ type_mapping_example("0755"),
+ ],
+ ),
+ )
+ api.register_mapped_type(
+ TypeMapping(
+ OctalMode,
+ str,
+ lambda v, ap, _: OctalMode.parse_filesystem_mode(v, ap),
+ ),
+ reference_documentation=type_mapping_reference_documentation(
+ description="An octal mode. Must always be a string.",
+ examples=[
+ type_mapping_example("0644"),
+ type_mapping_example("0755"),
+ ],
+ ),
+ )
+
+
+def register_service_managers(
+ api: DebputyPluginInitializerProvider,
+) -> None:
+ api.service_provider(
+ "systemd",
+ detect_systemd_service_files,
+ generate_snippets_for_systemd_units,
+ )
+ api.service_provider(
+ "sysvinit",
+ detect_sysv_init_service_files,
+ generate_snippets_for_init_scripts,
+ )
+
+
+def register_automatic_discard_rules(
+ api: DebputyPluginInitializerProvider,
+) -> None:
+ api.automatic_discard_rule(
+ "python-cache-files",
+ _debputy_discard_pyc_files,
+ rule_reference_documentation="Discards any *.pyc, *.pyo files and any __pycache__ directories",
+ examples=automatic_discard_rule_example(
+ (".../foo.py", False),
+ ".../__pycache__/",
+ ".../__pycache__/...",
+ ".../foo.pyc",
+ ".../foo.pyo",
+ ),
+ )
+ api.automatic_discard_rule(
+ "la-files",
+ _debputy_prune_la_files,
+ rule_reference_documentation="Discards any file with the extension .la beneath the directory /usr/lib",
+ examples=automatic_discard_rule_example(
+ "usr/lib/libfoo.la",
+ ("usr/lib/libfoo.so.1.0.0", False),
+ ),
+ )
+ api.automatic_discard_rule(
+ "backup-files",
+ _debputy_prune_backup_files,
+ rule_reference_documentation="Discards common back up files such as foo~, foo.bak or foo.orig",
+ examples=(
+ automatic_discard_rule_example(
+ ".../foo~",
+ ".../foo.orig",
+ ".../foo.rej",
+ ".../DEADJOE",
+ ".../.foo.sw.",
+ ),
+ ),
+ )
+ api.automatic_discard_rule(
+ "version-control-paths",
+ _debputy_prune_vcs_paths,
+ rule_reference_documentation="Discards common version control paths such as .git, .gitignore, CVS, etc.",
+ examples=automatic_discard_rule_example(
+ ("tools/foo", False),
+ ".../CVS/",
+ ".../CVS/...",
+ ".../.gitignore",
+ ".../.gitattributes",
+ ".../.git/",
+ ".../.git/...",
+ ),
+ )
+ api.automatic_discard_rule(
+ "gnu-info-dir-file",
+ _debputy_prune_info_dir_file,
+ rule_reference_documentation="Discards the /usr/share/info/dir file (causes package file conflicts)",
+ examples=automatic_discard_rule_example(
+ "usr/share/info/dir",
+ ("usr/share/info/foo.info", False),
+ ("usr/share/info/dir.info", False),
+ ("usr/share/random/case/dir", False),
+ ),
+ )
+ api.automatic_discard_rule(
+ "debian-dir",
+ _debputy_prune_binary_debian_dir,
+ rule_reference_documentation="(Implementation detail) Discards any DEBIAN directory to avoid it from appearing"
+ " literally in the file listing",
+ examples=(
+ automatic_discard_rule_example(
+ "DEBIAN/",
+ "DEBIAN/control",
+ ("usr/bin/foo", False),
+ ("usr/share/DEBIAN/foo", False),
+ ),
+ ),
+ )
+ api.automatic_discard_rule(
+ "doxygen-cruft-files",
+ _debputy_prune_doxygen_cruft,
+ rule_reference_documentation="Discards cruft files generated by doxygen",
+ examples=automatic_discard_rule_example(
+ ("usr/share/doc/foo/api/doxygen.css", False),
+ ("usr/share/doc/foo/api/doxygen.svg", False),
+ ("usr/share/doc/foo/api/index.html", False),
+ "usr/share/doc/foo/api/.../cruft.map",
+ "usr/share/doc/foo/api/.../cruft.md5",
+ ),
+ )
+
+
+def register_processing_steps(api: DebputyPluginInitializerProvider) -> None:
+ api.package_processor("manpages", process_manpages)
+ api.package_processor("clean-la-files", clean_la_files)
+ # strip-non-determinism makes assumptions about the PackageProcessingContext implementation
+ api.package_processor(
+ "strip-nondeterminism",
+ cast("Any", strip_non_determinism),
+ depends_on_processor=["manpages"],
+ )
+ api.package_processor(
+ "compression",
+ apply_compression,
+ depends_on_processor=["manpages", "strip-nondeterminism"],
+ )
+
+
+def register_variables_via_private_api(api: DebputyPluginInitializerProvider) -> None:
+ api.manifest_variable_provider(
+ load_source_variables,
+ {
+ "DEB_SOURCE": "Name of the source package (`dpkg-parsechangelog -SSource`)",
+ "DEB_VERSION": "Version from the top most changelog entry (`dpkg-parsechangelog -SVersion`)",
+ "DEB_VERSION_EPOCH_UPSTREAM": "Version from the top most changelog entry *without* the Debian revision",
+ "DEB_VERSION_UPSTREAM_REVISION": "Version from the top most changelog entry *without* the epoch",
+ "DEB_VERSION_UPSTREAM": "Upstream version from the top most changelog entry (that is, *without* epoch and Debian revision)",
+ "SOURCE_DATE_EPOCH": textwrap.dedent(
+ """\
+ Timestamp from the top most changelog entry (`dpkg-parsechangelog -STimestamp`)
+ Please see https://reproducible-builds.org/docs/source-date-epoch/ for the full definition of
+ this variable.
+ """
+ ),
+ "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": None,
+ "_DEBPUTY_SND_SOURCE_DATE_EPOCH": None,
+ },
+ )
+
+
+def document_builtin_variables(api: DebputyPluginInitializerProvider) -> None:
+ api.document_builtin_variable(
+ "PACKAGE",
+ "Name of the binary package (only available in binary context)",
+ is_context_specific=True,
+ )
+
+ arch_types = _DOCUMENTED_DPKG_ARCH_TYPES
+
+ for arch_type, (arch_type_tag, arch_type_doc) in arch_types.items():
+ for arch_var, arch_var_doc in _DOCUMENTED_DPKG_ARCH_VARS.items():
+ full_var = f"DEB_{arch_type}_{arch_var}"
+ documentation = textwrap.dedent(
+ f"""\
+ {arch_var_doc} ({arch_type_tag})
+ This variable describes machine information used when the package is compiled and assembled.
+ * Machine type: {arch_type_doc}
+ * Value description: {arch_var_doc}
+
+ The value is the output of: `dpkg-architecture -q{full_var}`
+ """
+ )
+ api.document_builtin_variable(
+ full_var,
+ documentation,
+ is_for_special_case=arch_type != "HOST",
+ )
+
+
+def _format_docbase_filename(
+ path_format: str,
+ format_param: PPFFormatParam,
+ docbase_file: VirtualPath,
+) -> str:
+ with docbase_file.open() as fd:
+ content = Deb822(fd)
+ proper_name = content["Document"]
+ if proper_name is not None:
+ format_param["name"] = proper_name
+ else:
+ _warn(
+ f"The docbase file {docbase_file.fs_path} is missing the Document field"
+ )
+ return path_format.format(**format_param)
+
+
+def register_special_ppfs(api: DebputyPluginInitializerProvider) -> None:
+ api.packager_provided_file(
+ "doc-base",
+ "/usr/share/doc-base/{owning_package}.{name}",
+ format_callback=_format_docbase_filename,
+ )
+
+ api.packager_provided_file(
+ "shlibs",
+ "DEBIAN/shlibs",
+ allow_name_segment=False,
+ reservation_only=True,
+ reference_documentation=packager_provided_file_reference_documentation(
+ format_documentation_uris=["man:deb-shlibs(5)"],
+ ),
+ )
+ api.packager_provided_file(
+ "symbols",
+ "DEBIAN/symbols",
+ allow_name_segment=False,
+ allow_architecture_segment=True,
+ reservation_only=True,
+ reference_documentation=packager_provided_file_reference_documentation(
+ format_documentation_uris=["man:deb-symbols(5)"],
+ ),
+ )
+ api.packager_provided_file(
+ "templates",
+ "DEBIAN/templates",
+ allow_name_segment=False,
+ allow_architecture_segment=False,
+ reservation_only=True,
+ )
+ api.packager_provided_file(
+ "alternatives",
+ "DEBIAN/alternatives",
+ allow_name_segment=False,
+ allow_architecture_segment=True,
+ reservation_only=True,
+ )
+
+
+def register_install_rules(api: DebputyPluginInitializerProvider) -> None:
+ api.plugable_manifest_rule(
+ InstallRule,
+ MK_INSTALLATIONS_INSTALL,
+ ParsedInstallRule,
+ _install_rule_handler,
+ source_format=_with_alt_form(ParsedInstallRuleSourceFormat),
+ inline_reference_documentation=reference_documentation(
+ title="Generic install (`install`)",
+ description=textwrap.dedent(
+ """\
+ The generic `install` rule can be used to install arbitrary paths into packages
+ and is *similar* to how `dh_install` from debhelper works. It is a two "primary" uses.
+
+ 1) The classic "install into directory" similar to the standard `dh_install`
+ 2) The "install as" similar to `dh-exec`'s `foo => bar` feature.
+
+ The `install` rule installs a path exactly once into each package it acts on. In
+ the rare case that you want to install the same source *multiple* times into the
+ *same* packages, please have a look at `{MULTI_DEST_INSTALL}`.
+ """.format(
+ MULTI_DEST_INSTALL=MK_INSTALLATIONS_MULTI_DEST_INSTALL
+ )
+ ),
+ non_mapping_description=textwrap.dedent(
+ """\
+ When the input is a string or a list of string, then that value is used as shorthand
+ for `source` or `sources` (respectively). This form can only be used when `into` is
+ not required.
+ """
+ ),
+ attributes=[
+ documented_attr(
+ ["source", "sources"],
+ textwrap.dedent(
+ """\
+ A path match (`source`) or a list of path matches (`sources`) defining the
+ source path(s) to be installed. The path match(es) can use globs. Each match
+ is tried against default search directories.
+ - When a symlink is matched, then the symlink (not its target) is installed
+ as-is. When a directory is matched, then the directory is installed along
+ with all the contents that have not already been installed somewhere.
+ """
+ ),
+ ),
+ documented_attr(
+ "dest_dir",
+ textwrap.dedent(
+ """\
+ A path defining the destination *directory*. The value *cannot* use globs, but can
+ use substitution. If neither `as` nor `dest-dir` is given, then `dest-dir` defaults
+ to the directory name of the `source`.
+ """
+ ),
+ ),
+ documented_attr(
+ "into",
+ textwrap.dedent(
+ """\
+ Either a package name or a list of package names for which these paths should be
+ installed. This key is conditional on whether there are multiple binary packages listed
+ in `debian/control`. When there is only one binary package, then that binary is the
+ default for `into`. Otherwise, the key is required.
+ """
+ ),
+ ),
+ documented_attr(
+ "install_as",
+ textwrap.dedent(
+ """\
+ A path defining the path to install the source as. This is a full path. This option
+ is mutually exclusive with `dest-dir` and `sources` (but not `source`). When `as` is
+ given, then `source` must match exactly one "not yet matched" path.
+ """
+ ),
+ ),
+ documented_attr(
+ "when",
+ textwrap.dedent(
+ """\
+ A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules).
+ """
+ ),
+ ),
+ ],
+ reference_documentation_url=_manifest_format_doc("generic-install-install"),
+ ),
+ )
+ api.plugable_manifest_rule(
+ InstallRule,
+ [
+ MK_INSTALLATIONS_INSTALL_DOCS,
+ "install-doc",
+ ],
+ ParsedInstallRule,
+ _install_docs_rule_handler,
+ source_format=_with_alt_form(ParsedInstallDocRuleSourceFormat),
+ inline_reference_documentation=reference_documentation(
+ title="Install documentation (`install-docs`)",
+ description=textwrap.dedent(
+ """\
+ This install rule resemble that of `dh_installdocs`. It is a shorthand over the generic
+ `install` rule with the following key features:
+
+ 1) The default `dest-dir` is to use the package's documentation directory (usually something
+ like `/usr/share/doc/{{PACKAGE}}`, though it respects the "main documentation package"
+ recommendation from Debian Policy). The `dest-dir` or `as` can be set in case the
+ documentation in question goes into another directory or with a concrete path. In this
+ case, it is still "better" than `install` due to the remaining benefits.
+ 2) The rule comes with pre-defined conditional logic for skipping the rule under
+ `DEB_BUILD_OPTIONS=nodoc`, so you do not have to write that conditional yourself.
+ 3) The `into` parameter can be omitted as long as there is a exactly one non-`udeb`
+ package listed in `debian/control`.
+
+ With these two things in mind, it behaves just like the `install` rule.
+
+ Note: It is often worth considering to use a more specialized version of the `install-docs`
+ rule when one such is available. If you are looking to install an example or a manpage,
+ consider whether `install-examples` or `install-man` might be a better fit for your
+ use-case.
+ """
+ ),
+ non_mapping_description=textwrap.dedent(
+ """\
+ When the input is a string or a list of string, then that value is used as shorthand
+ for `source` or `sources` (respectively). This form can only be used when `into` is
+ not required.
+ """
+ ),
+ attributes=[
+ documented_attr(
+ ["source", "sources"],
+ textwrap.dedent(
+ """\
+ A path match (`source`) or a list of path matches (`sources`) defining the
+ source path(s) to be installed. The path match(es) can use globs. Each match
+ is tried against default search directories.
+ - When a symlink is matched, then the symlink (not its target) is installed
+ as-is. When a directory is matched, then the directory is installed along
+ with all the contents that have not already been installed somewhere.
+
+ - **CAVEAT**: Specifying `source: examples` where `examples` resolves to a
+ directory for `install-examples` will give you an `examples/examples`
+ directory in the package, which is rarely what you want. Often, you
+ can solve this by using `examples/*` instead. Similar for `install-docs`
+ and a `doc` or `docs` directory.
+ """
+ ),
+ ),
+ documented_attr(
+ "dest_dir",
+ textwrap.dedent(
+ """\
+ A path defining the destination *directory*. The value *cannot* use globs, but can
+ use substitution. If neither `as` nor `dest-dir` is given, then `dest-dir` defaults
+ to the relevant package documentation directory (a la `/usr/share/doc/{{PACKAGE}}`).
+ """
+ ),
+ ),
+ documented_attr(
+ "into",
+ textwrap.dedent(
+ """\
+ Either a package name or a list of package names for which these paths should be
+ installed as documentation. This key is conditional on whether there are multiple
+ (non-`udeb`) binary packages listed in `debian/control`. When there is only one
+ (non-`udeb`) binary package, then that binary is the default for `into`. Otherwise,
+ the key is required.
+ """
+ ),
+ ),
+ documented_attr(
+ "install_as",
+ textwrap.dedent(
+ """\
+ A path defining the path to install the source as. This is a full path. This option
+ is mutually exclusive with `dest-dir` and `sources` (but not `source`). When `as` is
+ given, then `source` must match exactly one "not yet matched" path.
+ """
+ ),
+ ),
+ documented_attr(
+ "when",
+ textwrap.dedent(
+ """\
+ A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules).
+ This condition will be combined with the built-in condition provided by these rules
+ (rather than replacing it).
+ """
+ ),
+ ),
+ ],
+ reference_documentation_url=_manifest_format_doc(
+ "install-documentation-install-docs"
+ ),
+ ),
+ )
+ api.plugable_manifest_rule(
+ InstallRule,
+ [
+ MK_INSTALLATIONS_INSTALL_EXAMPLES,
+ "install-example",
+ ],
+ ParsedInstallExamplesRule,
+ _install_examples_rule_handler,
+ source_format=_with_alt_form(ParsedInstallExamplesRuleSourceFormat),
+ inline_reference_documentation=reference_documentation(
+ title="Install examples (`install-examples`)",
+ description=textwrap.dedent(
+ """\
+ This install rule resemble that of `dh_installexamples`. It is a shorthand over the generic `
+ install` rule with the following key features:
+
+ 1) It pre-defines the `dest-dir` that respects the "main documentation package" recommendation from
+ Debian Policy. The `install-examples` will use the `examples` subdir for the package documentation
+ dir.
+ 2) The rule comes with pre-defined conditional logic for skipping the rule under
+ `DEB_BUILD_OPTIONS=nodoc`, so you do not have to write that conditional yourself.
+ 3) The `into` parameter can be omitted as long as there is a exactly one non-`udeb`
+ package listed in `debian/control`.
+
+ With these two things in mind, it behaves just like the `install` rule.
+ """
+ ),
+ non_mapping_description=textwrap.dedent(
+ """\
+ When the input is a string or a list of string, then that value is used as shorthand
+ for `source` or `sources` (respectively). This form can only be used when `into` is
+ not required.
+ """
+ ),
+ attributes=[
+ documented_attr(
+ ["source", "sources"],
+ textwrap.dedent(
+ """\
+ A path match (`source`) or a list of path matches (`sources`) defining the
+ source path(s) to be installed. The path match(es) can use globs. Each match
+ is tried against default search directories.
+ - When a symlink is matched, then the symlink (not its target) is installed
+ as-is. When a directory is matched, then the directory is installed along
+ with all the contents that have not already been installed somewhere.
+
+ - **CAVEAT**: Specifying `source: examples` where `examples` resolves to a
+ directory for `install-examples` will give you an `examples/examples`
+ directory in the package, which is rarely what you want. Often, you
+ can solve this by using `examples/*` instead. Similar for `install-docs`
+ and a `doc` or `docs` directory.
+ """
+ ),
+ ),
+ documented_attr(
+ "into",
+ textwrap.dedent(
+ """\
+ Either a package name or a list of package names for which these paths should be
+ installed as examples. This key is conditional on whether there are (non-`udeb`)
+ multiple binary packages listed in `debian/control`. When there is only one
+ (non-`udeb`) binary package, then that binary is the default for `into`.
+ Otherwise, the key is required.
+ """
+ ),
+ ),
+ documented_attr(
+ "when",
+ textwrap.dedent(
+ """\
+ A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules).
+ This condition will be combined with the built-in condition provided by these rules
+ (rather than replacing it).
+ """
+ ),
+ ),
+ ],
+ reference_documentation_url=_manifest_format_doc(
+ "install-examples-install-examples"
+ ),
+ ),
+ )
+ api.plugable_manifest_rule(
+ InstallRule,
+ MK_INSTALLATIONS_INSTALL_MAN,
+ ParsedInstallManpageRule,
+ _install_man_rule_handler,
+ source_format=_with_alt_form(ParsedInstallManpageRuleSourceFormat),
+ inline_reference_documentation=reference_documentation(
+ title="Install manpages (`install-man`)",
+ description=textwrap.dedent(
+ """\
+ Install rule for installing manpages similar to `dh_installman`. It is a shorthand
+ over the generic `install` rule with the following key features:
+
+ 1) The rule can only match files (notably, symlinks cannot be matched by this rule).
+ 2) The `dest-dir` is computed per source file based on the manpage's section and
+ language.
+ 3) The `into` parameter can be omitted as long as there is a exactly one non-`udeb`
+ package listed in `debian/control`.
+ 4) The rule comes with manpage specific attributes such as `language` and `section`
+ for when the auto-detection is insufficient.
+ 5) The rule comes with pre-defined conditional logic for skipping the rule under
+ `DEB_BUILD_OPTIONS=nodoc`, so you do not have to write that conditional yourself.
+
+ With these things in mind, the rule behaves similar to the `install` rule.
+ """
+ ),
+ non_mapping_description=textwrap.dedent(
+ """\
+ When the input is a string or a list of string, then that value is used as shorthand
+ for `source` or `sources` (respectively). This form can only be used when `into` is
+ not required.
+ """
+ ),
+ attributes=[
+ documented_attr(
+ ["source", "sources"],
+ textwrap.dedent(
+ """\
+ A path match (`source`) or a list of path matches (`sources`) defining the
+ source path(s) to be installed. The path match(es) can use globs. Each match
+ is tried against default search directories.
+ - When a symlink is matched, then the symlink (not its target) is installed
+ as-is. When a directory is matched, then the directory is installed along
+ with all the contents that have not already been installed somewhere.
+ """
+ ),
+ ),
+ documented_attr(
+ "into",
+ textwrap.dedent(
+ """\
+ Either a package name or a list of package names for which these paths should be
+ installed as manpages. This key is conditional on whether there are multiple (non-`udeb`)
+ binary packages listed in `debian/control`. When there is only one (non-`udeb`) binary
+ package, then that binary is the default for `into`. Otherwise, the key is required.
+ """
+ ),
+ ),
+ documented_attr(
+ "section",
+ textwrap.dedent(
+ """\
+ If provided, it must be an integer between 1 and 9 (both inclusive), defining the
+ section the manpages belong overriding any auto-detection that `debputy` would
+ have performed.
+ """
+ ),
+ ),
+ documented_attr(
+ "language",
+ textwrap.dedent(
+ """\
+ If provided, it must be either a 2 letter language code (such as `de`), a 5 letter
+ language + dialect code (such as `pt_BR`), or one of the special keywords `C`,
+ `derive-from-path`, or `derive-from-basename`. The default is `derive-from-path`.
+ - When `language` is `C`, then the manpages are assumed to be "untranslated".
+ - When `language` is a language code (with or without dialect), then all manpages
+ matched will be assumed to be translated to that concrete language / dialect.
+ - When `language` is `derive-from-path`, then `debputy` attempts to derive the
+ language from the path (`man/<language>/man<section>`). This matches the
+ default of `dh_installman`. When no language can be found for a given source,
+ `debputy` behaves like language was `C`.
+ - When `language` is `derive-from-basename`, then `debputy` attempts to derive
+ the language from the basename (`foo.<language>.1`) similar to `dh_installman`
+ previous default. When no language can be found for a given source, `debputy`
+ behaves like language was `C`. Note this is prone to false positives where
+ `.pl`, `.so` or similar two-letter extensions gets mistaken for a language code
+ (`.pl` can both be "Polish" or "Perl Script", `.so` can both be "Somali" and
+ "Shared Object" documentation). In this configuration, such extensions are
+ always assumed to be a language.
+ """
+ ),
+ ),
+ documented_attr(
+ "when",
+ textwrap.dedent(
+ """\
+ A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules).
+ """
+ ),
+ ),
+ ],
+ reference_documentation_url=_manifest_format_doc(
+ "install-manpages-install-man"
+ ),
+ ),
+ )
+ api.plugable_manifest_rule(
+ InstallRule,
+ MK_INSTALLATIONS_DISCARD,
+ ParsedInstallDiscardRule,
+ _install_discard_rule_handler,
+ source_format=_with_alt_form(ParsedInstallDiscardRuleSourceFormat),
+ inline_reference_documentation=reference_documentation(
+ title="Discard (or exclude) upstream provided paths (`discard`)",
+ description=textwrap.dedent(
+ """\
+ When installing paths from `debian/tmp` into packages, it might be useful to ignore
+ some paths that you never need installed. This can be done with the `discard` rule.
+
+ Once a path is discarded, it cannot be matched by any other install rules. A path
+ that is discarded, is considered handled when `debputy` checks for paths you might
+ have forgotten to install. The `discard` feature is therefore *also* replaces the
+ `debian/not-installed` file used by `debhelper` and `cdbs`.
+ """
+ ),
+ non_mapping_description=textwrap.dedent(
+ """\
+ When the input is a string or a list of string, then that value is used as shorthand
+ for `path` or `paths` (respectively).
+ """
+ ),
+ attributes=[
+ documented_attr(
+ ["path", "paths"],
+ textwrap.dedent(
+ """\
+ A path match (`path`) or a list of path matches (`paths`) defining the source
+ path(s) that should not be installed anywhere. The path match(es) can use globs.
+ - When a symlink is matched, then the symlink (not its target) is discarded as-is.
+ When a directory is matched, then the directory is discarded along with all the
+ contents that have not already been installed somewhere.
+ """
+ ),
+ ),
+ documented_attr(
+ ["search_dir", "search_dirs"],
+ textwrap.dedent(
+ """\
+ A path (`search-dir`) or a list to paths (`search-dirs`) that defines
+ which search directories apply to. This attribute is primarily useful
+ for source packages that uses "per package search dirs", and you want
+ to restrict a discard rule to a subset of the relevant search dirs.
+ Note all listed search directories must be either an explicit search
+ requested by the packager or a search directory that `debputy`
+ provided automatically (such as `debian/tmp`). Listing other paths
+ will make `debputy` report an error.
+ - Note that the `path` or `paths` must match at least one entry in
+ any of the search directories unless *none* of the search directories
+ exist (or the condition in `required-when` evaluates to false). When
+ none of the search directories exist, the discard rule is silently
+ skipped. This special-case enables you to have discard rules only
+ applicable to certain builds that are only performed conditionally.
+ """
+ ),
+ ),
+ documented_attr(
+ "required_when",
+ textwrap.dedent(
+ """\
+ A condition as defined in [Conditional rules](#conditional-rules). The discard
+ rule is always applied. When the conditional is present and evaluates to false,
+ the discard rule can silently match nothing.When the condition is absent, *or*
+ it evaluates to true, then each pattern provided must match at least one path.
+ """
+ ),
+ ),
+ ],
+ reference_documentation_url=_manifest_format_doc(
+ "discard-or-exclude-upstream-provided-paths-discard"
+ ),
+ ),
+ )
+ api.plugable_manifest_rule(
+ InstallRule,
+ MK_INSTALLATIONS_MULTI_DEST_INSTALL,
+ ParsedMultiDestInstallRule,
+ _multi_dest_install_rule_handler,
+ source_format=ParsedMultiDestInstallRuleSourceFormat,
+ inline_reference_documentation=reference_documentation(
+ title=f"Multi destination install (`{MK_INSTALLATIONS_MULTI_DEST_INSTALL}`)",
+ description=textwrap.dedent(
+ """\
+ The `{RULE_NAME}` is a variant of the generic `install` rule that installs sources
+ into multiple destination paths. This is needed for the rare case where you want a
+ path to be installed *twice* (or more) into the *same* package. The rule is a two
+ "primary" uses.
+
+ 1) The classic "install into directory" similar to the standard `dh_install`,
+ except you list 2+ destination directories.
+ 2) The "install as" similar to `dh-exec`'s `foo => bar` feature, except you list
+ 2+ `as` names.
+ """.format(
+ RULE_NAME=MK_INSTALLATIONS_MULTI_DEST_INSTALL
+ )
+ ),
+ attributes=[
+ documented_attr(
+ ["source", "sources"],
+ textwrap.dedent(
+ """\
+ A path match (`source`) or a list of path matches (`sources`) defining the
+ source path(s) to be installed. The path match(es) can use globs. Each match
+ is tried against default search directories.
+ - When a symlink is matched, then the symlink (not its target) is installed
+ as-is. When a directory is matched, then the directory is installed along
+ with all the contents that have not already been installed somewhere.
+ """
+ ),
+ ),
+ documented_attr(
+ "dest_dirs",
+ textwrap.dedent(
+ """\
+ A list of paths defining the destination *directories*. The value *cannot* use
+ globs, but can use substitution. It is mutually exclusive with `as` but must be
+ provided if `as` is not provided. The attribute must contain at least two paths
+ (if you do not have two paths, you want `install`).
+ """
+ ),
+ ),
+ documented_attr(
+ "into",
+ textwrap.dedent(
+ """\
+ Either a package name or a list of package names for which these paths should be
+ installed. This key is conditional on whether there are multiple binary packages listed
+ in `debian/control`. When there is only one binary package, then that binary is the
+ default for `into`. Otherwise, the key is required.
+ """
+ ),
+ ),
+ documented_attr(
+ "install_as",
+ textwrap.dedent(
+ """\
+ A list of paths, which defines all the places the source will be installed.
+ Each path must be a full path without globs (but can use substitution).
+ This option is mutually exclusive with `dest-dirs` and `sources` (but not
+ `source`). When `as` is given, then `source` must match exactly one
+ "not yet matched" path. The attribute must contain at least two paths
+ (if you do not have two paths, you want `install`).
+ """
+ ),
+ ),
+ documented_attr(
+ "when",
+ textwrap.dedent(
+ """\
+ A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules).
+ """
+ ),
+ ),
+ ],
+ reference_documentation_url=_manifest_format_doc("generic-install-install"),
+ ),
+ )
+
+
+def register_transformation_rules(api: DebputyPluginInitializerProvider) -> None:
+ api.plugable_manifest_rule(
+ TransformationRule,
+ "move",
+ TransformationMoveRuleSpec,
+ _transformation_move_handler,
+ inline_reference_documentation=reference_documentation(
+ title="Move transformation rule (`move`)",
+ description=textwrap.dedent(
+ """\
+ The move transformation rule is mostly only useful for single binary source packages,
+ where everything from upstream's build system is installed automatically into the package.
+ In those case, you might find yourself with some files that need to be renamed to match
+ Debian specific requirements.
+
+ This can be done with the `move` transformation rule, which is a rough emulation of the
+ `mv` command line tool.
+ """
+ ),
+ attributes=[
+ documented_attr(
+ "source",
+ textwrap.dedent(
+ """\
+ A path match defining the source path(s) to be renamed. The value can use globs
+ and substitutions.
+ """
+ ),
+ ),
+ documented_attr(
+ "target",
+ textwrap.dedent(
+ """\
+ A path defining the target path. The value *cannot* use globs, but can use
+ substitution. If the target ends with a literal `/` (prior to substitution),
+ the target will *always* be a directory.
+ """
+ ),
+ ),
+ documented_attr(
+ "when",
+ textwrap.dedent(
+ """\
+ A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules).
+ """
+ ),
+ ),
+ ],
+ reference_documentation_url=_manifest_format_doc(
+ "move-transformation-rule-move"
+ ),
+ ),
+ )
+ api.plugable_manifest_rule(
+ TransformationRule,
+ "remove",
+ TransformationRemoveRuleSpec,
+ _transformation_remove_handler,
+ source_format=_with_alt_form(TransformationRemoveRuleInputFormat),
+ inline_reference_documentation=reference_documentation(
+ title="Remove transformation rule (`remove`)",
+ description=textwrap.dedent(
+ """\
+ The remove transformation rule is mostly only useful for single binary source packages,
+ where everything from upstream's build system is installed automatically into the package.
+ In those case, you might find yourself with some files that are _not_ relevant for the
+ Debian package (but would be relevant for other distros or for non-distro local builds).
+ Common examples include `INSTALL` files or `LICENSE` files (when they are just a subset
+ of `debian/copyright`).
+
+ In the manifest, you can ask `debputy` to remove paths from the debian package by using
+ the `remove` transformation rule.
+
+ Note that `remove` removes paths from future glob matches and transformation rules.
+ """
+ ),
+ non_mapping_description=textwrap.dedent(
+ """\
+ When the input is a string or a list of string, then that value is used as shorthand
+ for `path` or `paths` (respectively).
+ """
+ ),
+ attributes=[
+ documented_attr(
+ ["path", "paths"],
+ textwrap.dedent(
+ """\
+ A path match (`path`) or a list of path matches (`paths`) defining the
+ path(s) inside the package that should be removed. The path match(es)
+ can use globs.
+ - When a symlink is matched, then the symlink (not its target) is removed
+ as-is. When a directory is matched, then the directory is removed
+ along with all the contents.
+ """
+ ),
+ ),
+ documented_attr(
+ "keep_empty_parent_dirs",
+ textwrap.dedent(
+ """\
+ A boolean determining whether to prune parent directories that become
+ empty as a consequence of this rule. When provided and `true`, this
+ rule will leave empty directories behind. Otherwise, if this rule
+ causes a directory to become empty that directory will be removed.
+ """
+ ),
+ ),
+ documented_attr(
+ "when",
+ textwrap.dedent(
+ """\
+ A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules).
+ This condition will be combined with the built-in condition provided by these rules
+ (rather than replacing it).
+ """
+ ),
+ ),
+ ],
+ reference_documentation_url=_manifest_format_doc(
+ "remove-transformation-rule-remove"
+ ),
+ ),
+ )
+ api.plugable_manifest_rule(
+ TransformationRule,
+ "create-symlink",
+ CreateSymlinkRule,
+ _transformation_create_symlink,
+ inline_reference_documentation=reference_documentation(
+ title="Create symlinks transformation rule (`create-symlink`)",
+ description=textwrap.dedent(
+ """\
+ Often, the upstream build system will provide the symlinks for you. However,
+ in some cases, it is useful for the packager to define distribution specific
+ symlinks. This can be done via the `create-symlink` transformation rule.
+ """
+ ),
+ attributes=[
+ documented_attr(
+ "path",
+ textwrap.dedent(
+ """\
+ The path that should be a symlink. The path may contain substitution
+ variables such as `{{DEB_HOST_MULTIARCH}}` but _cannot_ use globs.
+ Parent directories are implicitly created as necessary.
+ * Note that if `path` already exists, the behaviour of this
+ transformation depends on the value of `replacement-rule`.
+ """
+ ),
+ ),
+ documented_attr(
+ "target",
+ textwrap.dedent(
+ """\
+ Where the symlink should point to. The target may contain substitution
+ variables such as `{{DEB_HOST_MULTIARCH}}` but _cannot_ use globs.
+ The link target is _not_ required to exist inside the package.
+ * The `debputy` tool will normalize the target according to the rules
+ of the Debian Policy. Use absolute or relative target at your own
+ preference.
+ """
+ ),
+ ),
+ documented_attr(
+ "replacement_rule",
+ textwrap.dedent(
+ """\
+ This attribute defines how to handle if `path` already exists. It can
+ be set to one of the following values:
+ - `error-if-exists`: When `path` already exists, `debputy` will
+ stop with an error. This is similar to `ln -s` semantics.
+ - `error-if-directory`: When `path` already exists, **and** it is
+ a directory, `debputy` will stop with an error. Otherwise,
+ remove the `path` first and then create the symlink. This is
+ similar to `ln -sf` semantics.
+ - `abort-on-non-empty-directory` (default): When `path` already
+ exists, then it will be removed provided it is a non-directory
+ **or** an *empty* directory and the symlink will then be
+ created. If the path is a *non-empty* directory, `debputy`
+ will stop with an error.
+ - `discard-existing`: When `path` already exists, it will be
+ removed. If the `path` is a directory, all its contents will
+ be removed recursively along with the directory. Finally,
+ the symlink is created. This is similar to having an explicit
+ `remove` rule just prior to the `create-symlink` that is
+ conditional on `path` existing (plus the condition defined in
+ `when` if any).
+
+ Keep in mind, that `replacement-rule` only applies if `path` exists.
+ If the symlink cannot be created, because a part of `path` exist and
+ is *not* a directory, then `create-symlink` will fail regardless of
+ the value in `replacement-rule`.
+ """
+ ),
+ ),
+ documented_attr(
+ "when",
+ textwrap.dedent(
+ """\
+ A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules).
+ """
+ ),
+ ),
+ ],
+ reference_documentation_url=_manifest_format_doc(
+ "create-symlinks-transformation-rule-create-symlink"
+ ),
+ ),
+ )
+ api.plugable_manifest_rule(
+ TransformationRule,
+ "path-metadata",
+ PathManifestRule,
+ _transformation_path_metadata,
+ source_format=PathManifestSourceDictFormat,
+ inline_reference_documentation=reference_documentation(
+ title="Change path owner/group or mode (`path-metadata`)",
+ description=textwrap.dedent(
+ """\
+ The `debputy` command normalizes the path metadata (such as ownership and mode) similar
+ to `dh_fixperms`. For most packages, the default is what you want. However, in some
+ cases, the package has a special case or two that `debputy` does not cover. In that
+ case, you can tell `debputy` to use the metadata you want by using the `path-metadata`
+ transformation.
+
+ Common use-cases include setuid/setgid binaries (such `usr/bin/sudo`) or/and static
+ ownership (such as /usr/bin/write).
+ """
+ ),
+ attributes=[
+ documented_attr(
+ ["path", "paths"],
+ textwrap.dedent(
+ """\
+ A path match (`path`) or a list of path matches (`paths`) defining the path(s)
+ inside the package that should be affected. The path match(es) can use globs
+ and substitution variables. Special-rules for matches:
+ - Symlinks are never followed and will never be matched by this rule.
+ - Directory handling depends on the `recursive` attribute.
+ """
+ ),
+ ),
+ documented_attr(
+ "owner",
+ textwrap.dedent(
+ """\
+ Denotes the owner of the paths matched by `path` or `paths`. When omitted,
+ no change of owner is done.
+ """
+ ),
+ ),
+ documented_attr(
+ "group",
+ textwrap.dedent(
+ """\
+ Denotes the group of the paths matched by `path` or `paths`. When omitted,
+ no change of group is done.
+ """
+ ),
+ ),
+ documented_attr(
+ "mode",
+ textwrap.dedent(
+ """\
+ Denotes the mode of the paths matched by `path` or `paths`. When omitted,
+ no change in mode is done. Note that numeric mode must always be given as
+ a string (i.e., with quotes). Symbolic mode can be used as well. If
+ symbolic mode uses a relative definition (e.g., `o-rx`), then it is
+ relative to the matched path's current mode.
+ """
+ ),
+ ),
+ documented_attr(
+ "capabilities",
+ textwrap.dedent(
+ """\
+ Denotes a Linux capability that should be applied to the path. When provided,
+ `debputy` will cause the capability to be applied to all *files* denoted by
+ the `path`/`paths` attribute on install (via `postinst configure`) provided
+ that `setcap` is installed on the system when the `postinst configure` is
+ run.
+ - If any non-file paths are matched, the `capabilities` will *not* be applied
+ to those paths.
+
+ """
+ ),
+ ),
+ documented_attr(
+ "capability_mode",
+ textwrap.dedent(
+ """\
+ Denotes the mode to apply to the path *if* the Linux capability denoted in
+ `capabilities` was successfully applied. If omitted, it defaults to `a-s` as
+ generally capabilities are used to avoid "setuid"/"setgid" binaries. The
+ `capability-mode` is relative to the *final* path mode (the mode of the path
+ in the produced `.deb`). The `capability-mode` attribute cannot be used if
+ `capabilities` is omitted.
+ """
+ ),
+ ),
+ documented_attr(
+ "recursive",
+ textwrap.dedent(
+ """\
+ When a directory is matched, then the metadata changes are applied to the
+ directory itself. When `recursive` is `true`, then the transformation is
+ *also* applied to all paths beneath the directory. The default value for
+ this attribute is `false`.
+ """
+ ),
+ ),
+ documented_attr(
+ "when",
+ textwrap.dedent(
+ """\
+ A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules).
+ """
+ ),
+ ),
+ ],
+ reference_documentation_url=_manifest_format_doc(
+ "change-path-ownergroup-or-mode-path-metadata"
+ ),
+ ),
+ )
+ api.plugable_manifest_rule(
+ TransformationRule,
+ "create-directories",
+ EnsureDirectoryRule,
+ _transformation_mkdirs,
+ source_format=_with_alt_form(EnsureDirectorySourceFormat),
+ inline_reference_documentation=reference_documentation(
+ title="Create directories transformation rule (`create-directories`)",
+ description=textwrap.dedent(
+ """\
+ NOTE: This transformation is only really needed if you need to create an empty
+ directory somewhere in your package as an integration point. All `debputy`
+ transformations will create directories as required.
+
+ In most cases, upstream build systems and `debputy` will create all the relevant
+ directories. However, in some rare cases you may want to explicitly define a path
+ to be a directory. Maybe to silence a linter that is warning you about a directory
+ being empty, or maybe you need an empty directory that nothing else is creating for
+ you. This can be done via the `create-directories` transformation rule.
+
+ Unless you have a specific need for the mapping form, you are recommended to use the
+ shorthand form of just listing the directories you want created.
+ """
+ ),
+ non_mapping_description=textwrap.dedent(
+ """\
+ When the input is a string or a list of string, then that value is used as shorthand
+ for `path` or `paths` (respectively).
+ """
+ ),
+ attributes=[
+ documented_attr(
+ ["path", "paths"],
+ textwrap.dedent(
+ """\
+ A path (`path`) or a list of path (`paths`) defining the path(s) inside the
+ package that should be created as directories. The path(es) _cannot_ use globs
+ but can use substitution variables. Parent directories are implicitly created
+ (with owner `root:root` and mode `0755` - only explicitly listed directories
+ are affected by the owner/mode options)
+ """
+ ),
+ ),
+ documented_attr(
+ "owner",
+ textwrap.dedent(
+ """\
+ Denotes the owner of the directory (but _not_ what is inside the directory).
+ Default is "root".
+ """
+ ),
+ ),
+ documented_attr(
+ "group",
+ textwrap.dedent(
+ """\
+ Denotes the group of the directory (but _not_ what is inside the directory).
+ Default is "root".
+ """
+ ),
+ ),
+ documented_attr(
+ "mode",
+ textwrap.dedent(
+ """\
+ Denotes the mode of the directory (but _not_ what is inside the directory).
+ Note that numeric mode must always be given as a string (i.e., with quotes).
+ Symbolic mode can be used as well. If symbolic mode uses a relative
+ definition (e.g., `o-rx`), then it is relative to the directory's current mode
+ (if it already exists) or `0755` if the directory is created by this
+ transformation. The default is "0755".
+ """
+ ),
+ ),
+ documented_attr(
+ "when",
+ textwrap.dedent(
+ """\
+ A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules).
+ """
+ ),
+ ),
+ ],
+ reference_documentation_url=_manifest_format_doc(
+ "create-directories-transformation-rule-directories"
+ ),
+ ),
+ )
+
+
+def register_manifest_condition_rules(api: DebputyPluginInitializerProvider) -> None:
+ api.provide_manifest_keyword(
+ ManifestCondition,
+ "cross-compiling",
+ lambda *_: ManifestCondition.is_cross_building(),
+ inline_reference_documentation=reference_documentation(
+ title="Cross-Compiling condition `cross-compiling`",
+ description=textwrap.dedent(
+ """\
+ The `cross-compiling` condition is used to determine if the current build is
+ performing a cross build (i.e., `DEB_BUILD_GNU_TYPE` != `DEB_HOST_GNU_TYPE`).
+ Often this has consequences for what is possible to do.
+
+ Note if you specifically want to know:
+
+ * whether build-time tests should be run, then please use the
+ `run-build-time-tests` condition.
+ * whether compiled binaries can be run as if it was a native binary, please
+ use the `can-execute-compiled-binaries` condition instead. That condition
+ accounts for cross-building in its evaluation.
+ """
+ ),
+ reference_documentation_url=_manifest_format_doc(
+ "cross-compiling-condition-cross-compiling-string"
+ ),
+ ),
+ )
+ api.provide_manifest_keyword(
+ ManifestCondition,
+ "can-execute-compiled-binaries",
+ lambda *_: ManifestCondition.can_execute_compiled_binaries(),
+ inline_reference_documentation=reference_documentation(
+ title="Can run produced binaries `can-execute-compiled-binaries`",
+ description=textwrap.dedent(
+ """\
+ The `can-execute-compiled-binaries` condition is used to assert the build
+ can assume that all compiled binaries can be run as-if they were native
+ binaries. For native builds, this condition always evaluates to `true`.
+ For cross builds, the condition is generally evaluates to `false`. However,
+ there are special-cases where binaries can be run during cross-building.
+ Accordingly, this condition is subtly different from the `cross-compiling`
+ condition.
+
+ Note this condition should *not* be used when you know the binary has been
+ built for the build architecture (`DEB_BUILD_ARCH`) or for determining
+ whether build-time tests should be run (for build-time tests, please use
+ the `run-build-time-tests` condition instead). Some upstream build systems
+ are advanced enough to distinguish building a final product vs. building
+ a helper tool that needs to run during build. The latter will often be
+ compiled by a separate compiler (often using `$(CC_FOR_BUILD)`,
+ `cc_for_build` or similar variable names in upstream build systems for
+ that compiler).
+ """
+ ),
+ reference_documentation_url=_manifest_format_doc(
+ "can-run-produced-binaries-can-execute-compiled-binaries-string"
+ ),
+ ),
+ )
+ api.provide_manifest_keyword(
+ ManifestCondition,
+ "run-build-time-tests",
+ lambda *_: ManifestCondition.run_build_time_tests(),
+ inline_reference_documentation=reference_documentation(
+ title="Whether build time tests should be run `run-build-time-tests`",
+ description=textwrap.dedent(
+ """\
+ The `run-build-time-tests` condition is used to determine whether (build
+ time) tests should be run for this build. This condition roughly
+ translates into whether `nocheck` is present in `DEB_BUILD_OPTIONS`.
+
+ In general, the manifest *should not* prevent build time tests from being
+ run during cross-builds.
+ """
+ ),
+ reference_documentation_url=_manifest_format_doc(
+ "whether-build-time-tests-should-be-run-run-build-time-tests-string"
+ ),
+ ),
+ )
+
+ api.plugable_manifest_rule(
+ ManifestCondition,
+ "not",
+ MCNot,
+ _mc_not,
+ inline_reference_documentation=reference_documentation(
+ title="Negated condition `not` (mapping)",
+ description=textwrap.dedent(
+ """\
+ It is possible to negate a condition via the `not` condition.
+
+ As an example:
+
+ packages:
+ util-linux:
+ transformations:
+ - create-symlink
+ path: sbin/getty
+ target: /sbin/agetty
+ when:
+ # On Hurd, the package "hurd" ships "sbin/getty".
+ # This example happens to also be alternative to `arch-marches: '!hurd-any`
+ not:
+ arch-matches: 'hurd-any'
+
+ The `not` condition is specified as a mapping, where the key is `not` and the
+ value is a nested condition.
+ """
+ ),
+ attributes=[
+ documented_attr(
+ "negated_condition",
+ textwrap.dedent(
+ """\
+ The condition to be negated.
+ """
+ ),
+ ),
+ ],
+ reference_documentation_url=_manifest_format_doc(
+ "whether-build-time-tests-should-be-run-run-build-time-tests-string"
+ ),
+ ),
+ )
+ api.plugable_manifest_rule(
+ ManifestCondition,
+ ["any-of", "all-of"],
+ MCAnyOfAllOf,
+ _mc_any_of,
+ source_format=List[ManifestCondition],
+ inline_reference_documentation=reference_documentation(
+ title="All or any of a list of conditions `all-of`/`any-of`",
+ description=textwrap.dedent(
+ """\
+ It is possible to aggregate conditions using the `all-of` or `any-of`
+ condition. This provide `X and Y` and `X or Y` semantics (respectively).
+ """
+ ),
+ reference_documentation_url=_manifest_format_doc(
+ "all-or-any-of-a-list-of-conditions-all-ofany-of-list"
+ ),
+ ),
+ )
+ api.plugable_manifest_rule(
+ ManifestCondition,
+ "arch-matches",
+ MCArchMatches,
+ _mc_arch_matches,
+ source_format=str,
+ inline_reference_documentation=reference_documentation(
+ title="Architecture match condition `arch-matches`",
+ description=textwrap.dedent(
+ """\
+ Sometimes, a rule needs to be conditional on the architecture.
+ This can be done by using the `arch-matches` rule. In 99.99%
+ of the cases, `arch-matches` will be form you are looking for
+ and practically behaves like a comparison against
+ `dpkg-architecture -qDEB_HOST_ARCH`.
+
+ For the cross-compiling specialists or curious people: The
+ `arch-matches` rule behaves like a `package-context-arch-matches`
+ in the context of a binary package and like
+ `source-context-arch-matches` otherwise. The details of those
+ are covered in their own keywords.
+ """
+ ),
+ non_mapping_description=textwrap.dedent(
+ """\
+ The value must be a string in the form of a space separated list
+ architecture names or architecture wildcards (same syntax as the
+ architecture restriction in Build-Depends in debian/control except
+ there is no enclosing `[]` brackets). The names/wildcards can
+ optionally be prefixed by `!` to negate them. However, either
+ *all* names / wildcards must have negation or *none* of them may
+ have it.
+ """
+ ),
+ reference_documentation_url=_manifest_format_doc(
+ "architecture-match-condition-arch-matches-mapping"
+ ),
+ ),
+ )
+
+ context_arch_doc = reference_documentation(
+ title="Explicit source or binary package context architecture match condition"
+ " `source-context-arch-matches`, `package-context-arch-matches` (mapping)",
+ description=textwrap.dedent(
+ """\
+ **These are special-case conditions**. Unless you know that you have a very special-case,
+ you should probably use `arch-matches` instead. These conditions are aimed at people with
+ corner-case special architecture needs. It also assumes the reader is familiar with the
+ `arch-matches` condition.
+
+ To understand these rules, here is a quick primer on `debputy`'s concept of "source context"
+ vs "(binary) package context" architecture. For a native build, these two contexts are the
+ same except that in the package context an `Architecture: all` package always resolve to
+ `all` rather than `DEB_HOST_ARCH`. As a consequence, `debputy` forbids `arch-matches` and
+ `package-context-arch-matches` in the context of an `Architecture: all` package as a warning
+ to the packager that condition does not make sense.
+
+ In the very rare case that you need an architecture condition for an `Architecture: all` package,
+ you can use `source-context-arch-matches`. However, this means your `Architecture: all` package
+ is not reproducible between different build hosts (which has known to be relevant for some
+ very special cases).
+
+ Additionally, for the 0.0001% case you are building a cross-compiling compiler (that is,
+ `DEB_HOST_ARCH != DEB_TARGET_ARCH` and you are working with `gcc` or similar) `debputy` can be
+ instructed (opt-in) to use `DEB_TARGET_ARCH` rather than `DEB_HOST_ARCH` for certain packages when
+ evaluating an architecture condition in context of a binary package. This can be useful if the
+ compiler produces supporting libraries that need to be built for the `DEB_TARGET_ARCH` rather than
+ the `DEB_HOST_ARCH`. This is where `arch-matches` or `package-context-arch-matches` can differ
+ subtly from `source-context-arch-matches` in how they evaluate the condition. This opt-in currently
+ relies on setting `X-DH-Build-For-Type: target` for each of the relevant packages in
+ `debian/control`. However, unless you are a cross-compiling specialist, you will probably never
+ need to care about nor use any of this.
+
+ Accordingly, the possible conditions are:
+
+ * `arch-matches`: This is the form recommended to laymen and as the default use-case. This
+ conditional acts `package-context-arch-matches` if the condition is used in the context
+ of a binary package. Otherwise, it acts as `source-context-arch-matches`.
+
+ * `source-context-arch-matches`: With this conditional, the provided architecture constraint is compared
+ against the build time provided host architecture (`dpkg-architecture -qDEB_HOST_ARCH`). This can
+ be useful when an `Architecture: all` package needs an architecture condition for some reason.
+
+ * `package-context-arch-matches`: With this conditional, the provided architecture constraint is compared
+ against the package's resolved architecture. This condition can only be used in the context of a binary
+ package (usually, under `packages.<name>.`). If the package is an `Architecture: all` package, the
+ condition will fail with an error as the condition always have the same outcome. For all other
+ packages, the package's resolved architecture is the same as the build time provided host architecture
+ (`dpkg-architecture -qDEB_HOST_ARCH`).
+
+ - However, as noted above there is a special case for when compiling a cross-compiling compiler, where
+ this behaves subtly different from `source-context-arch-matches`.
+
+ All conditions are used the same way as `arch-matches`. Simply replace `arch-matches` with the other
+ condition. See the `arch-matches` description for an example.
+ """
+ ),
+ non_mapping_description=textwrap.dedent(
+ """\
+ The value must be a string in the form of a space separated list
+ architecture names or architecture wildcards (same syntax as the
+ architecture restriction in Build-Depends in debian/control except
+ there is no enclosing `[]` brackets). The names/wildcards can
+ optionally be prefixed by `!` to negate them. However, either
+ *all* names / wildcards must have negation or *none* of them may
+ have it.
+ """
+ ),
+ )
+
+ api.plugable_manifest_rule(
+ ManifestCondition,
+ "source-context-arch-matches",
+ MCArchMatches,
+ _mc_source_context_arch_matches,
+ source_format=str,
+ inline_reference_documentation=context_arch_doc,
+ )
+ api.plugable_manifest_rule(
+ ManifestCondition,
+ "package-context-arch-matches",
+ MCArchMatches,
+ _mc_arch_matches,
+ source_format=str,
+ inline_reference_documentation=context_arch_doc,
+ )
+ api.plugable_manifest_rule(
+ ManifestCondition,
+ "build-profiles-matches",
+ MCBuildProfileMatches,
+ _mc_build_profile_matches,
+ source_format=str,
+ inline_reference_documentation=reference_documentation(
+ title="Active build profile match condition `build-profiles-matches`",
+ description=textwrap.dedent(
+ """\
+ The `build-profiles-matches` condition is used to assert whether the
+ active build profiles (`DEB_BUILD_PROFILES` / `dpkg-buildpackage -P`)
+ matches a given build profile restriction.
+ """
+ ),
+ non_mapping_description=textwrap.dedent(
+ """\
+ The value is a string using the same syntax as the `Build-Profiles`
+ field from `debian/control` (i.e., a space separated list of
+ `<[!]profile ...>` groups).
+ """
+ ),
+ reference_documentation_url=_manifest_format_doc(
+ "active-build-profile-match-condition-build-profiles-matches-mapping"
+ ),
+ ),
+ )
+
+
+def register_dpkg_conffile_rules(api: DebputyPluginInitializerProvider) -> None:
+ api.plugable_manifest_rule(
+ DpkgMaintscriptHelperCommand,
+ "remove",
+ DpkgRemoveConffileRule,
+ _dpkg_conffile_remove,
+ inline_reference_documentation=None, # TODO: write and add
+ )
+
+ api.plugable_manifest_rule(
+ DpkgMaintscriptHelperCommand,
+ "rename",
+ DpkgRenameConffileRule,
+ _dpkg_conffile_rename,
+ inline_reference_documentation=None, # TODO: write and add
+ )
+
+
+class _ModeOwnerBase(DebputyParsedContentStandardConditional):
+ mode: NotRequired[FileSystemMode]
+ owner: NotRequired[StaticFileSystemOwner]
+ group: NotRequired[StaticFileSystemGroup]
+
+
+class PathManifestSourceDictFormat(_ModeOwnerBase):
+ path: NotRequired[
+ Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("paths")]
+ ]
+ paths: NotRequired[List[FileSystemMatchRule]]
+ recursive: NotRequired[bool]
+ capabilities: NotRequired[str]
+ capability_mode: NotRequired[FileSystemMode]
+
+
+class PathManifestRule(_ModeOwnerBase):
+ paths: List[FileSystemMatchRule]
+ recursive: NotRequired[bool]
+ capabilities: NotRequired[str]
+ capability_mode: NotRequired[FileSystemMode]
+
+
+class EnsureDirectorySourceFormat(_ModeOwnerBase):
+ path: NotRequired[
+ Annotated[FileSystemExactMatchRule, DebputyParseHint.target_attribute("paths")]
+ ]
+ paths: NotRequired[List[FileSystemExactMatchRule]]
+
+
+class EnsureDirectoryRule(_ModeOwnerBase):
+ paths: List[FileSystemExactMatchRule]
+
+
+class CreateSymlinkRule(DebputyParsedContentStandardConditional):
+ path: FileSystemExactMatchRule
+ target: Annotated[SymlinkTarget, DebputyParseHint.not_path_error_hint()]
+ replacement_rule: NotRequired[CreateSymlinkReplacementRule]
+
+
+class TransformationMoveRuleSpec(DebputyParsedContentStandardConditional):
+ source: FileSystemMatchRule
+ target: FileSystemExactMatchRule
+
+
+class TransformationRemoveRuleSpec(DebputyParsedContentStandardConditional):
+ paths: List[FileSystemMatchRule]
+ keep_empty_parent_dirs: NotRequired[bool]
+
+
+class TransformationRemoveRuleInputFormat(DebputyParsedContentStandardConditional):
+ path: NotRequired[
+ Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("paths")]
+ ]
+ paths: NotRequired[List[FileSystemMatchRule]]
+ keep_empty_parent_dirs: NotRequired[bool]
+
+
+class ParsedInstallRuleSourceFormat(DebputyParsedContentStandardConditional):
+ sources: NotRequired[List[FileSystemMatchRule]]
+ source: NotRequired[
+ Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
+ ]
+ into: NotRequired[
+ Annotated[
+ Union[str, List[str]],
+ DebputyParseHint.required_when_multi_binary(),
+ ]
+ ]
+ dest_dir: NotRequired[
+ Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()]
+ ]
+ install_as: NotRequired[
+ Annotated[
+ FileSystemExactMatchRule,
+ DebputyParseHint.conflicts_with_source_attributes("sources", "dest_dir"),
+ DebputyParseHint.manifest_attribute("as"),
+ DebputyParseHint.not_path_error_hint(),
+ ]
+ ]
+
+
+class ParsedInstallDocRuleSourceFormat(DebputyParsedContentStandardConditional):
+ sources: NotRequired[List[FileSystemMatchRule]]
+ source: NotRequired[
+ Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
+ ]
+ into: NotRequired[
+ Annotated[
+ Union[str, List[str]],
+ DebputyParseHint.required_when_multi_binary(package_type="deb"),
+ ]
+ ]
+ dest_dir: NotRequired[
+ Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()]
+ ]
+ install_as: NotRequired[
+ Annotated[
+ FileSystemExactMatchRule,
+ DebputyParseHint.conflicts_with_source_attributes("sources", "dest_dir"),
+ DebputyParseHint.manifest_attribute("as"),
+ DebputyParseHint.not_path_error_hint(),
+ ]
+ ]
+
+
+class ParsedInstallRule(DebputyParsedContentStandardConditional):
+ sources: List[FileSystemMatchRule]
+ into: NotRequired[List[BinaryPackage]]
+ dest_dir: NotRequired[FileSystemExactMatchRule]
+ install_as: NotRequired[FileSystemExactMatchRule]
+
+
+class ParsedMultiDestInstallRuleSourceFormat(DebputyParsedContentStandardConditional):
+ sources: NotRequired[List[FileSystemMatchRule]]
+ source: NotRequired[
+ Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
+ ]
+ into: NotRequired[
+ Annotated[
+ Union[str, List[str]],
+ DebputyParseHint.required_when_multi_binary(),
+ ]
+ ]
+ dest_dirs: NotRequired[
+ Annotated[
+ List[FileSystemExactMatchRule], DebputyParseHint.not_path_error_hint()
+ ]
+ ]
+ install_as: NotRequired[
+ Annotated[
+ List[FileSystemExactMatchRule],
+ DebputyParseHint.conflicts_with_source_attributes("sources", "dest_dirs"),
+ DebputyParseHint.not_path_error_hint(),
+ DebputyParseHint.manifest_attribute("as"),
+ ]
+ ]
+
+
+class ParsedMultiDestInstallRule(DebputyParsedContentStandardConditional):
+ sources: List[FileSystemMatchRule]
+ into: NotRequired[List[BinaryPackage]]
+ dest_dirs: NotRequired[List[FileSystemExactMatchRule]]
+ install_as: NotRequired[List[FileSystemExactMatchRule]]
+
+
+class ParsedInstallExamplesRule(DebputyParsedContentStandardConditional):
+ sources: List[FileSystemMatchRule]
+ into: NotRequired[List[BinaryPackage]]
+
+
+class ParsedInstallExamplesRuleSourceFormat(DebputyParsedContentStandardConditional):
+ sources: NotRequired[List[FileSystemMatchRule]]
+ source: NotRequired[
+ Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
+ ]
+ into: NotRequired[
+ Annotated[
+ Union[str, List[str]],
+ DebputyParseHint.required_when_multi_binary(package_type="deb"),
+ ]
+ ]
+
+
+class ParsedInstallManpageRule(DebputyParsedContentStandardConditional):
+ sources: List[FileSystemMatchRule]
+ language: NotRequired[str]
+ section: NotRequired[int]
+ into: NotRequired[List[BinaryPackage]]
+
+
+class ParsedInstallManpageRuleSourceFormat(DebputyParsedContentStandardConditional):
+ sources: NotRequired[List[FileSystemMatchRule]]
+ source: NotRequired[
+ Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")]
+ ]
+ language: NotRequired[str]
+ section: NotRequired[int]
+ into: NotRequired[
+ Annotated[
+ Union[str, List[str]],
+ DebputyParseHint.required_when_multi_binary(package_type="deb"),
+ ]
+ ]
+
+
+class ParsedInstallDiscardRuleSourceFormat(DebputyParsedContent):
+ paths: NotRequired[List[FileSystemMatchRule]]
+ path: NotRequired[
+ Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("paths")]
+ ]
+ search_dir: NotRequired[
+ Annotated[
+ FileSystemExactMatchRule, DebputyParseHint.target_attribute("search_dirs")
+ ]
+ ]
+ search_dirs: NotRequired[List[FileSystemExactMatchRule]]
+ required_when: NotRequired[ManifestCondition]
+
+
+class ParsedInstallDiscardRule(DebputyParsedContent):
+ paths: List[FileSystemMatchRule]
+ search_dirs: NotRequired[List[FileSystemExactMatchRule]]
+ required_when: NotRequired[ManifestCondition]
+
+
+class DpkgConffileManagementRuleBase(DebputyParsedContent):
+ prior_to_version: NotRequired[str]
+ owning_package: NotRequired[str]
+
+
+class DpkgRenameConffileRule(DpkgConffileManagementRuleBase):
+ source: str
+ target: str
+
+
+class DpkgRemoveConffileRule(DpkgConffileManagementRuleBase):
+ path: str
+
+
+class MCAnyOfAllOf(DebputyParsedContent):
+ conditions: List[ManifestCondition]
+
+
+class MCNot(DebputyParsedContent):
+ negated_condition: Annotated[
+ ManifestCondition, DebputyParseHint.manifest_attribute("not")
+ ]
+
+
+class MCArchMatches(DebputyParsedContent):
+ arch_matches: str
+
+
+class MCBuildProfileMatches(DebputyParsedContent):
+ build_profile_matches: str
+
+
+def _parse_filename(
+ filename: str,
+ attribute_path: AttributePath,
+ *,
+ allow_directories: bool = True,
+) -> str:
+ try:
+ normalized_path = _normalize_path(filename, with_prefix=False)
+ except ValueError as e:
+ raise ManifestParseException(
+ f'Error parsing the path "{filename}" defined in {attribute_path.path}: {e.args[0]}'
+ ) from None
+ if not allow_directories and filename.endswith("/"):
+ raise ManifestParseException(
+ f'The path "{filename}" in {attribute_path.path} ends with "/" implying it is a directory,'
+ f" but this feature can only be used for files"
+ )
+ if normalized_path == ".":
+ raise ManifestParseException(
+ f'The path "{filename}" in {attribute_path.path} looks like the root directory,'
+ f" but this feature does not allow the root directory here."
+ )
+ return normalized_path
+
+
+def _with_alt_form(t: Type[TypedDict]):
+ return Union[
+ t,
+ List[str],
+ str,
+ ]
+
+
+def _dpkg_conffile_rename(
+ _name: str,
+ parsed_data: DpkgRenameConffileRule,
+ path: AttributePath,
+ _context: ParserContextData,
+) -> DpkgMaintscriptHelperCommand:
+ source_file = parsed_data["source"]
+ target_file = parsed_data["target"]
+ normalized_source = _parse_filename(
+ source_file,
+ path["source"],
+ allow_directories=False,
+ )
+ path.path_hint = source_file
+
+ normalized_target = _parse_filename(
+ target_file,
+ path["target"],
+ allow_directories=False,
+ )
+ normalized_source = "/" + normalized_source
+ normalized_target = "/" + normalized_target
+
+ if normalized_source == normalized_target:
+ raise ManifestParseException(
+ f"Invalid rename defined in {path.path}: The source and target path are the same!"
+ )
+
+ version, owning_package = _parse_conffile_prior_version_and_owning_package(
+ parsed_data, path
+ )
+ return DpkgMaintscriptHelperCommand.mv_conffile(
+ path,
+ normalized_source,
+ normalized_target,
+ version,
+ owning_package,
+ )
+
+
+def _dpkg_conffile_remove(
+ _name: str,
+ parsed_data: DpkgRemoveConffileRule,
+ path: AttributePath,
+ _context: ParserContextData,
+) -> DpkgMaintscriptHelperCommand:
+ source_file = parsed_data["path"]
+ normalized_source = _parse_filename(
+ source_file,
+ path["path"],
+ allow_directories=False,
+ )
+ path.path_hint = source_file
+
+ normalized_source = "/" + normalized_source
+
+ version, owning_package = _parse_conffile_prior_version_and_owning_package(
+ parsed_data, path
+ )
+ return DpkgMaintscriptHelperCommand.rm_conffile(
+ path,
+ normalized_source,
+ version,
+ owning_package,
+ )
+
+
+def _parse_conffile_prior_version_and_owning_package(
+ d: DpkgConffileManagementRuleBase,
+ attribute_path: AttributePath,
+) -> Tuple[Optional[str], Optional[str]]:
+ prior_version = d.get("prior_to_version")
+ owning_package = d.get("owning_package")
+
+ if prior_version is not None and not PKGVERSION_REGEX.match(prior_version):
+ p = attribute_path["prior_to_version"]
+ raise ManifestParseException(
+ f"The {MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION} parameter in {p.path} must be a"
+ r" valid package version (i.e., match (?:\d+:)?\d[0-9A-Za-z.+:~]*(?:-[0-9A-Za-z.+:~]+)*)."
+ )
+
+ if owning_package is not None and not PKGNAME_REGEX.match(owning_package):
+ p = attribute_path["owning_package"]
+ raise ManifestParseException(
+ f"The {MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE} parameter in {p.path} must be a valid"
+ f" package name (i.e., match {PKGNAME_REGEX.pattern})."
+ )
+
+ return prior_version, owning_package
+
+
+def _install_rule_handler(
+ _name: str,
+ parsed_data: ParsedInstallRule,
+ path: AttributePath,
+ context: ParserContextData,
+) -> InstallRule:
+ sources = parsed_data["sources"]
+ install_as = parsed_data.get("install_as")
+ into = parsed_data.get("into")
+ dest_dir = parsed_data.get("dest_dir")
+ condition = parsed_data.get("when")
+ if not into:
+ into = [context.single_binary_package(path, package_attribute="into")]
+ into = frozenset(into)
+ if install_as is not None:
+ assert len(sources) == 1
+ assert dest_dir is None
+ return InstallRule.install_as(
+ sources[0],
+ install_as.match_rule.path,
+ into,
+ path.path,
+ condition,
+ )
+ return InstallRule.install_dest(
+ sources,
+ dest_dir.match_rule.path if dest_dir is not None else None,
+ into,
+ path.path,
+ condition,
+ )
+
+
+def _multi_dest_install_rule_handler(
+ _name: str,
+ parsed_data: ParsedMultiDestInstallRule,
+ path: AttributePath,
+ context: ParserContextData,
+) -> InstallRule:
+ sources = parsed_data["sources"]
+ install_as = parsed_data.get("install_as")
+ into = parsed_data.get("into")
+ dest_dirs = parsed_data.get("dest_dirs")
+ condition = parsed_data.get("when")
+ if not into:
+ into = [context.single_binary_package(path, package_attribute="into")]
+ into = frozenset(into)
+ if install_as is not None:
+ assert len(sources) == 1
+ assert dest_dirs is None
+ if len(install_as) < 2:
+ raise ManifestParseException(
+ f"The {path['install_as'].path} attribute must contain at least two paths."
+ )
+ return InstallRule.install_multi_as(
+ sources[0],
+ [p.match_rule.path for p in install_as],
+ into,
+ path.path,
+ condition,
+ )
+ if dest_dirs is None:
+ raise ManifestParseException(
+ f"Either the `as` or the `dest-dirs` key must be provided at {path.path}"
+ )
+ if len(dest_dirs) < 2:
+ raise ManifestParseException(
+ f"The {path['dest_dirs'].path} attribute must contain at least two paths."
+ )
+ return InstallRule.install_multi_dest(
+ sources,
+ [dd.match_rule.path for dd in dest_dirs],
+ into,
+ path.path,
+ condition,
+ )
+
+
+def _install_docs_rule_handler(
+ _name: str,
+ parsed_data: ParsedInstallRule,
+ path: AttributePath,
+ context: ParserContextData,
+) -> InstallRule:
+ sources = parsed_data["sources"]
+ install_as = parsed_data.get("install_as")
+ into = parsed_data.get("into")
+ dest_dir = parsed_data.get("dest_dir")
+ condition = parsed_data.get("when")
+ if not into:
+ into = [
+ context.single_binary_package(
+ path, package_type="deb", package_attribute="into"
+ )
+ ]
+ into = frozenset(into)
+ if install_as is not None:
+ assert len(sources) == 1
+ assert dest_dir is None
+ return InstallRule.install_doc_as(
+ sources[0],
+ install_as.match_rule.path,
+ into,
+ path.path,
+ condition,
+ )
+ return InstallRule.install_doc(
+ sources,
+ dest_dir,
+ into,
+ path.path,
+ condition,
+ )
+
+
+def _install_examples_rule_handler(
+ _name: str,
+ parsed_data: ParsedInstallExamplesRule,
+ path: AttributePath,
+ context: ParserContextData,
+) -> InstallRule:
+ sources = parsed_data["sources"]
+ into = parsed_data.get("into")
+ if not into:
+ into = [
+ context.single_binary_package(
+ path, package_type="deb", package_attribute="into"
+ )
+ ]
+ condition = parsed_data.get("when")
+ into = frozenset(into)
+ return InstallRule.install_examples(
+ sources,
+ into,
+ path.path,
+ condition,
+ )
+
+
+def _install_man_rule_handler(
+ _name: str,
+ parsed_data: ParsedInstallManpageRule,
+ attribute_path: AttributePath,
+ context: ParserContextData,
+) -> InstallRule:
+ sources = parsed_data["sources"]
+ language = parsed_data.get("language")
+ section = parsed_data.get("section")
+
+ if language is not None:
+ is_lang_ok = language in (
+ "C",
+ "derive-from-basename",
+ "derive-from-path",
+ )
+
+ if not is_lang_ok and len(language) == 2 and language.islower():
+ is_lang_ok = True
+
+ if (
+ not is_lang_ok
+ and len(language) == 5
+ and language[2] == "_"
+ and language[:2].islower()
+ and language[3:].isupper()
+ ):
+ is_lang_ok = True
+
+ if not is_lang_ok:
+ raise ManifestParseException(
+ f'The language attribute must in a 2-letter language code ("de"), a 5-letter language + dialect'
+ f' code ("pt_BR"), "derive-from-basename", "derive-from-path", or omitted. The problematic'
+ f' definition is {attribute_path["language"]}'
+ )
+
+ if section is not None and (section < 1 or section > 10):
+ raise ManifestParseException(
+ f"The section attribute must in the range [1-9] or omitted. The problematic definition is"
+ f' {attribute_path["section"]}'
+ )
+ if section is None and any(s.raw_match_rule.endswith(".gz") for s in sources):
+ raise ManifestParseException(
+ "Sorry, compressed manpages are not supported without an explicit `section` definition at the moment."
+ " This limitation may be removed in the future. Problematic definition from"
+ f' {attribute_path["sources"]}'
+ )
+ if any(s.raw_match_rule.endswith("/") for s in sources):
+ raise ManifestParseException(
+ 'The install-man rule can only match non-directories. Therefore, none of the sources can end with "/".'
+ " as that implies the source is for a directory. Problematic definition from"
+ f' {attribute_path["sources"]}'
+ )
+ into = parsed_data.get("into")
+ if not into:
+ into = [
+ context.single_binary_package(
+ attribute_path, package_type="deb", package_attribute="into"
+ )
+ ]
+ condition = parsed_data.get("when")
+ into = frozenset(into)
+ return InstallRule.install_man(
+ sources,
+ into,
+ section,
+ language,
+ attribute_path.path,
+ condition,
+ )
+
+
+def _install_discard_rule_handler(
+ _name: str,
+ parsed_data: ParsedInstallDiscardRule,
+ path: AttributePath,
+ _context: ParserContextData,
+) -> InstallRule:
+ limit_to = parsed_data.get("search_dirs")
+ if limit_to is not None and not limit_to:
+ p = path["search_dirs"]
+ raise ManifestParseException(f"The {p.path} attribute must not be empty.")
+ condition = parsed_data.get("required_when")
+ return InstallRule.discard_paths(
+ parsed_data["paths"],
+ path.path,
+ condition,
+ limit_to=limit_to,
+ )
+
+
+def _transformation_move_handler(
+ _name: str,
+ parsed_data: TransformationMoveRuleSpec,
+ path: AttributePath,
+ _context: ParserContextData,
+) -> TransformationRule:
+ source_match = parsed_data["source"]
+ target_path = parsed_data["target"].match_rule.path
+ condition = parsed_data.get("when")
+
+ if (
+ isinstance(source_match, ExactFileSystemPath)
+ and source_match.path == target_path
+ ):
+ raise ManifestParseException(
+ f"The transformation rule {path.path} requests a move of {source_match} to"
+ f" {target_path}, which is the same path"
+ )
+ return MoveTransformationRule(
+ source_match.match_rule,
+ target_path,
+ target_path.endswith("/"),
+ path,
+ condition,
+ )
+
+
+def _transformation_remove_handler(
+ _name: str,
+ parsed_data: TransformationRemoveRuleSpec,
+ attribute_path: AttributePath,
+ _context: ParserContextData,
+) -> TransformationRule:
+ paths = parsed_data["paths"]
+ keep_empty_parent_dirs = parsed_data.get("keep_empty_parent_dirs", False)
+
+ return RemoveTransformationRule(
+ [m.match_rule for m in paths],
+ keep_empty_parent_dirs,
+ attribute_path,
+ )
+
+
+def _transformation_create_symlink(
+ _name: str,
+ parsed_data: CreateSymlinkRule,
+ attribute_path: AttributePath,
+ _context: ParserContextData,
+) -> TransformationRule:
+ link_dest = parsed_data["path"].match_rule.path
+ replacement_rule: CreateSymlinkReplacementRule = parsed_data.get(
+ "replacement_rule",
+ "abort-on-non-empty-directory",
+ )
+ try:
+ link_target = debian_policy_normalize_symlink_target(
+ link_dest,
+ parsed_data["target"].symlink_target,
+ )
+ except ValueError as e: # pragma: no cover
+ raise AssertionError(
+ "Debian Policy normalization should not raise ValueError here"
+ ) from e
+
+ condition = parsed_data.get("when")
+
+ return CreateSymlinkPathTransformationRule(
+ link_target,
+ link_dest,
+ replacement_rule,
+ attribute_path,
+ condition,
+ )
+
+
+def _transformation_path_metadata(
+ _name: str,
+ parsed_data: PathManifestRule,
+ attribute_path: AttributePath,
+ _context: ParserContextData,
+) -> TransformationRule:
+ match_rules = parsed_data["paths"]
+ owner = parsed_data.get("owner")
+ group = parsed_data.get("group")
+ mode = parsed_data.get("mode")
+ recursive = parsed_data.get("recursive", False)
+ capabilities = parsed_data.get("capabilities")
+ capability_mode = parsed_data.get("capability_mode")
+
+ if capabilities is not None:
+ if capability_mode is None:
+ capability_mode = SymbolicMode.parse_filesystem_mode(
+ "a-s",
+ attribute_path["capability-mode"],
+ )
+ validate_cap = check_cap_checker()
+ validate_cap(capabilities, attribute_path["capabilities"].path)
+ elif capability_mode is not None and capabilities is None:
+ raise ManifestParseException(
+ "The attribute capability-mode cannot be provided without capabilities"
+ f" in {attribute_path.path}"
+ )
+ if owner is None and group is None and mode is None and capabilities is None:
+ raise ManifestParseException(
+ "At least one of owner, group, mode, or capabilities must be provided"
+ f" in {attribute_path.path}"
+ )
+ condition = parsed_data.get("when")
+
+ return PathMetadataTransformationRule(
+ [m.match_rule for m in match_rules],
+ owner,
+ group,
+ mode,
+ recursive,
+ capabilities,
+ capability_mode,
+ attribute_path.path,
+ condition,
+ )
+
+
+def _transformation_mkdirs(
+ _name: str,
+ parsed_data: EnsureDirectoryRule,
+ attribute_path: AttributePath,
+ _context: ParserContextData,
+) -> TransformationRule:
+ provided_paths = parsed_data["paths"]
+ owner = parsed_data.get("owner")
+ group = parsed_data.get("group")
+ mode = parsed_data.get("mode")
+
+ condition = parsed_data.get("when")
+
+ return CreateDirectoryTransformationRule(
+ [p.match_rule.path for p in provided_paths],
+ owner,
+ group,
+ mode,
+ attribute_path.path,
+ condition,
+ )
+
+
+def _at_least_two(
+ content: List[Any],
+ attribute_path: AttributePath,
+ attribute_name: str,
+) -> None:
+ if len(content) < 2:
+ raise ManifestParseException(
+ f"Must have at least two conditions in {attribute_path[attribute_name].path}"
+ )
+
+
+def _mc_any_of(
+ name: str,
+ parsed_data: MCAnyOfAllOf,
+ attribute_path: AttributePath,
+ _context: ParserContextData,
+) -> ManifestCondition:
+ conditions = parsed_data["conditions"]
+ _at_least_two(conditions, attribute_path, "conditions")
+ if name == "any-of":
+ return ManifestCondition.any_of(conditions)
+ assert name == "all-of"
+ return ManifestCondition.all_of(conditions)
+
+
+def _mc_not(
+ _name: str,
+ parsed_data: MCNot,
+ _attribute_path: AttributePath,
+ _context: ParserContextData,
+) -> ManifestCondition:
+ condition = parsed_data["negated_condition"]
+ return condition.negated()
+
+
+def _extract_arch_matches(
+ parsed_data: MCArchMatches,
+ attribute_path: AttributePath,
+) -> List[str]:
+ arch_matches_as_str = parsed_data["arch_matches"]
+ # Can we check arch list for typos? If we do, it must be tight in how close matches it does.
+ # Consider "arm" vs. "armel" (edit distance 2, but both are valid). Likewise, names often
+ # include a bit indicator "foo", "foo32", "foo64" - all of these have an edit distance of 2
+ # of each other.
+ arch_matches_as_list = arch_matches_as_str.split()
+ attr_path = attribute_path["arch_matches"]
+ if not arch_matches_as_list:
+ raise ManifestParseException(
+ f"The condition at {attr_path.path} must not be empty"
+ )
+
+ if arch_matches_as_list[0].startswith("[") or arch_matches_as_list[-1].endswith(
+ "]"
+ ):
+ raise ManifestParseException(
+ f"The architecture match at {attr_path.path} must be defined without enclosing it with "
+ '"[" or/and "]" brackets'
+ )
+ return arch_matches_as_list
+
+
+def _mc_source_context_arch_matches(
+ _name: str,
+ parsed_data: MCArchMatches,
+ attribute_path: AttributePath,
+ _context: ParserContextData,
+) -> ManifestCondition:
+ arch_matches = _extract_arch_matches(parsed_data, attribute_path)
+ return SourceContextArchMatchManifestCondition(arch_matches)
+
+
+def _mc_package_context_arch_matches(
+ name: str,
+ parsed_data: MCArchMatches,
+ attribute_path: AttributePath,
+ context: ParserContextData,
+) -> ManifestCondition:
+ arch_matches = _extract_arch_matches(parsed_data, attribute_path)
+
+ if not context.is_in_binary_package_state:
+ raise ManifestParseException(
+ f'The condition "{name}" at {attribute_path.path} can only be used in the context of a binary package.'
+ )
+
+ package_state = context.current_binary_package_state
+ if package_state.binary_package.is_arch_all:
+ result = context.dpkg_arch_query_table.architecture_is_concerned(
+ "all", arch_matches
+ )
+ attr_path = attribute_path["arch_matches"]
+ raise ManifestParseException(
+ f"The package architecture restriction at {attr_path.path} is applied to the"
+ f' "Architecture: all" package {package_state.binary_package.name}, which does not make sense'
+ f" as the condition will always resolves to `{str(result).lower()}`."
+ f" If you **really** need an architecture specific constraint for this rule, consider using"
+ f' "source-context-arch-matches" instead. However, this is a very rare use-case!'
+ )
+ return BinaryPackageContextArchMatchManifestCondition(arch_matches)
+
+
+def _mc_arch_matches(
+ name: str,
+ parsed_data: MCArchMatches,
+ attribute_path: AttributePath,
+ context: ParserContextData,
+) -> ManifestCondition:
+ if context.is_in_binary_package_state:
+ return _mc_package_context_arch_matches(
+ name, parsed_data, attribute_path, context
+ )
+ return _mc_source_context_arch_matches(name, parsed_data, attribute_path, context)
+
+
+def _mc_build_profile_matches(
+ _name: str,
+ parsed_data: MCBuildProfileMatches,
+ attribute_path: AttributePath,
+ _context: ParserContextData,
+) -> ManifestCondition:
+ build_profile_spec = parsed_data["build_profile_matches"].strip()
+ attr_path = attribute_path["build_profile_matches"]
+ if not build_profile_spec:
+ raise ManifestParseException(
+ f"The condition at {attr_path.path} must not be empty"
+ )
+ try:
+ active_profiles_match(build_profile_spec, frozenset())
+ except ValueError as e:
+ raise ManifestParseException(
+ f"Could not parse the build specification at {attr_path.path}: {e.args[0]}"
+ )
+ return BuildProfileMatch(build_profile_spec)