summaryrefslogtreecommitdiffstats
path: root/src/debputy/packages.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/packages.py')
-rw-r--r--src/debputy/packages.py332
1 files changed, 332 insertions, 0 deletions
diff --git a/src/debputy/packages.py b/src/debputy/packages.py
new file mode 100644
index 0000000..3204f46
--- /dev/null
+++ b/src/debputy/packages.py
@@ -0,0 +1,332 @@
+from typing import (
+ Dict,
+ Union,
+ Tuple,
+ Optional,
+ Set,
+ cast,
+ Mapping,
+ FrozenSet,
+ TYPE_CHECKING,
+)
+
+from debian.deb822 import Deb822
+from debian.debian_support import DpkgArchTable
+
+from ._deb_options_profiles import DebBuildOptionsAndProfiles
+from .architecture_support import (
+ DpkgArchitectureBuildProcessValuesTable,
+ dpkg_architecture_table,
+)
+from .util import DEFAULT_PACKAGE_TYPE, UDEB_PACKAGE_TYPE, _error, active_profiles_match
+
+if TYPE_CHECKING:
+ from .plugin.api import VirtualPath
+
+
+_MANDATORY_BINARY_PACKAGE_FIELD = [
+ "Package",
+ "Architecture",
+]
+
+
+def parse_source_debian_control(
+ debian_control: "VirtualPath",
+ selected_packages: Union[Set[str], FrozenSet[str]],
+ excluded_packages: Union[Set[str], FrozenSet[str]],
+ select_arch_all: bool,
+ select_arch_any: bool,
+ dpkg_architecture_variables: Optional[
+ DpkgArchitectureBuildProcessValuesTable
+ ] = None,
+ dpkg_arch_query_table: Optional[DpkgArchTable] = None,
+ build_env: Optional[DebBuildOptionsAndProfiles] = None,
+) -> Tuple["SourcePackage", Dict[str, "BinaryPackage"]]:
+ if dpkg_architecture_variables is None:
+ dpkg_architecture_variables = dpkg_architecture_table()
+ if dpkg_arch_query_table is None:
+ dpkg_arch_query_table = DpkgArchTable.load_arch_table()
+ if build_env is None:
+ build_env = DebBuildOptionsAndProfiles.instance()
+
+ # If no selection option is set, then all packages are acted on (except the
+ # excluded ones)
+ if not selected_packages and not select_arch_all and not select_arch_any:
+ select_arch_all = True
+ select_arch_any = True
+
+ with debian_control.open() as fd:
+ dctrl_paragraphs = list(Deb822.iter_paragraphs(fd))
+
+ if len(dctrl_paragraphs) < 2:
+ _error(
+ "debian/control must contain at least two stanza (1 Source + 1-N Package stanza)"
+ )
+
+ source_package = SourcePackage(dctrl_paragraphs[0])
+
+ bin_pkgs = [
+ _create_binary_package(
+ p,
+ selected_packages,
+ excluded_packages,
+ select_arch_all,
+ select_arch_any,
+ dpkg_architecture_variables,
+ dpkg_arch_query_table,
+ build_env,
+ i,
+ )
+ for i, p in enumerate(dctrl_paragraphs[1:], 1)
+ ]
+ bin_pkgs_table = {p.name: p for p in bin_pkgs}
+ if not selected_packages.issubset(bin_pkgs_table.keys()):
+ unknown = selected_packages - bin_pkgs_table.keys()
+ _error(
+ f"The following *selected* packages (-p) are not listed in debian/control: {sorted(unknown)}"
+ )
+ if not excluded_packages.issubset(bin_pkgs_table.keys()):
+ unknown = selected_packages - bin_pkgs_table.keys()
+ _error(
+ f"The following *excluded* packages (-N) are not listed in debian/control: {sorted(unknown)}"
+ )
+
+ return source_package, bin_pkgs_table
+
+
+def _check_package_sets(
+ provided_packages: Set[str],
+ valid_package_names: Set[str],
+ option_name: str,
+) -> None:
+ # SonarLint proposes to use `provided_packages > valid_package_names`, which is valid for boolean
+ # logic, but not for set logic. We want to assert that provided_packages is a proper subset
+ # of valid_package_names. The rewrite would cause no errors for {'foo'} > {'bar'} - in set logic,
+ # neither is a superset / subset of the other, but we want an error for this case.
+ #
+ # Bug filed:
+ # https://community.sonarsource.com/t/sonarlint-python-s1940-rule-does-not-seem-to-take-set-logic-into-account/79718
+ if not (provided_packages <= valid_package_names):
+ non_existing_packages = sorted(provided_packages - valid_package_names)
+ invalid_package_list = ", ".join(non_existing_packages)
+ msg = (
+ f"Invalid package names passed to {option_name}: {invalid_package_list}: "
+ f'Valid package names are: {", ".join(valid_package_names)}'
+ )
+ _error(msg)
+
+
+def _create_binary_package(
+ paragraph: Union[Deb822, Dict[str, str]],
+ selected_packages: Union[Set[str], FrozenSet[str]],
+ excluded_packages: Union[Set[str], FrozenSet[str]],
+ select_arch_all: bool,
+ select_arch_any: bool,
+ dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
+ dpkg_arch_query_table: DpkgArchTable,
+ build_env: DebBuildOptionsAndProfiles,
+ paragraph_index: int,
+) -> "BinaryPackage":
+ try:
+ package_name = paragraph["Package"]
+ except KeyError:
+ _error(f'Missing mandatory field "Package" in stanza number {paragraph_index}')
+ # The raise is there to help PyCharm type-checking (which fails at "NoReturn")
+ raise
+
+ for mandatory_field in _MANDATORY_BINARY_PACKAGE_FIELD:
+ if mandatory_field not in paragraph:
+ _error(
+ f'Missing mandatory field "{mandatory_field}" for binary package {package_name}'
+ f" (stanza number {paragraph_index})"
+ )
+
+ architecture = paragraph["Architecture"]
+
+ if paragraph_index < 1:
+ raise ValueError("stanza index must be 1-indexed (1, 2, ...)")
+ is_main_package = paragraph_index == 1
+
+ if package_name in excluded_packages:
+ should_act_on = False
+ elif package_name in selected_packages:
+ should_act_on = True
+ elif architecture == "all":
+ should_act_on = select_arch_all
+ else:
+ should_act_on = select_arch_any
+
+ profiles_raw = paragraph.get("Build-Profiles", "").strip()
+ if should_act_on and profiles_raw:
+ try:
+ should_act_on = active_profiles_match(
+ profiles_raw, build_env.deb_build_profiles
+ )
+ except ValueError as e:
+ _error(f"Invalid Build-Profiles field for {package_name}: {e.args[0]}")
+
+ return BinaryPackage(
+ paragraph,
+ dpkg_architecture_variables,
+ dpkg_arch_query_table,
+ should_be_acted_on=should_act_on,
+ is_main_package=is_main_package,
+ )
+
+
+def _check_binary_arch(
+ arch_table: DpkgArchTable,
+ binary_arch: str,
+ declared_arch: str,
+) -> bool:
+ if binary_arch == "all":
+ return True
+ arch_wildcards = declared_arch.split()
+ for arch_wildcard in arch_wildcards:
+ if arch_table.matches_architecture(binary_arch, arch_wildcard):
+ return True
+ return False
+
+
+class BinaryPackage:
+ __slots__ = [
+ "_package_fields",
+ "_dbgsym_binary_package",
+ "_should_be_acted_on",
+ "_dpkg_architecture_variables",
+ "_declared_arch_matches_output_arch",
+ "_is_main_package",
+ "_substvars",
+ "_maintscript_snippets",
+ ]
+
+ def __init__(
+ self,
+ fields: Union[Mapping[str, str], Deb822],
+ dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
+ dpkg_arch_query: DpkgArchTable,
+ *,
+ is_main_package: bool = False,
+ should_be_acted_on: bool = True,
+ ) -> None:
+ super(BinaryPackage, self).__init__()
+ # Typing-wise, Deb822 is *not* a Mapping[str, str] but it behaves enough
+ # like one that we rely on it and just cast it.
+ self._package_fields = cast("Mapping[str, str]", fields)
+ self._dbgsym_binary_package = None
+ self._should_be_acted_on = should_be_acted_on
+ self._dpkg_architecture_variables = dpkg_architecture_variables
+ self._is_main_package = is_main_package
+ self._declared_arch_matches_output_arch = _check_binary_arch(
+ dpkg_arch_query, self.resolved_architecture, self.declared_architecture
+ )
+
+ @property
+ def name(self) -> str:
+ return self.fields["Package"]
+
+ @property
+ def archive_section(self) -> str:
+ value = self.fields.get("Section")
+ if value is None:
+ return "Unknown"
+ return value
+
+ @property
+ def archive_component(self) -> str:
+ component = ""
+ section = self.archive_section
+ if "/" in section:
+ component = section.rsplit("/", 1)[0]
+ # The "main" component is always shortened to ""
+ if component == "main":
+ component = ""
+ return component
+
+ @property
+ def is_essential(self) -> bool:
+ return self._package_fields.get("Essential") == "yes"
+
+ @property
+ def is_udeb(self) -> bool:
+ return self.package_type == UDEB_PACKAGE_TYPE
+
+ @property
+ def should_be_acted_on(self) -> bool:
+ return self._should_be_acted_on and self._declared_arch_matches_output_arch
+
+ @property
+ def fields(self) -> Mapping[str, str]:
+ return self._package_fields
+
+ @property
+ def resolved_architecture(self) -> str:
+ arch = self.declared_architecture
+ if arch == "all":
+ return arch
+ if self._x_dh_build_for_type == "target":
+ return self._dpkg_architecture_variables["DEB_TARGET_ARCH"]
+ return self._dpkg_architecture_variables.current_host_arch
+
+ def package_deb_architecture_variable(self, variable_suffix: str) -> str:
+ if self._x_dh_build_for_type == "target":
+ return self._dpkg_architecture_variables[f"DEB_TARGET_{variable_suffix}"]
+ return self._dpkg_architecture_variables[f"DEB_HOST_{variable_suffix}"]
+
+ @property
+ def deb_multiarch(self) -> str:
+ return self.package_deb_architecture_variable("MULTIARCH")
+
+ @property
+ def _x_dh_build_for_type(self) -> str:
+ v = self._package_fields.get("X-DH-Build-For-Type")
+ if v is None:
+ return "host"
+ return v.lower()
+
+ @property
+ def package_type(self) -> str:
+ """Short for Package-Type (with proper default if absent)"""
+ v = self.fields.get("Package-Type")
+ if v is None:
+ return DEFAULT_PACKAGE_TYPE
+ return v
+
+ @property
+ def is_main_package(self) -> bool:
+ return self._is_main_package
+
+ def cross_command(self, command: str) -> str:
+ arch_table = self._dpkg_architecture_variables
+ if self._x_dh_build_for_type == "target":
+ target_gnu_type = arch_table["DEB_TARGET_GNU_TYPE"]
+ if arch_table["DEB_HOST_GNU_TYPE"] != target_gnu_type:
+ return f"{target_gnu_type}-{command}"
+ if arch_table.is_cross_compiling:
+ return f"{arch_table['DEB_HOST_GNU_TYPE']}-{command}"
+ return command
+
+ @property
+ def declared_architecture(self) -> str:
+ return self.fields["Architecture"]
+
+ @property
+ def is_arch_all(self) -> bool:
+ return self.declared_architecture == "all"
+
+
+class SourcePackage:
+ __slots__ = ("_package_fields",)
+
+ def __init__(self, fields: Union[Mapping[str, str], Deb822]):
+ # Typing-wise, Deb822 is *not* a Mapping[str, str] but it behaves enough
+ # like one that we rely on it and just cast it.
+ self._package_fields = cast("Mapping[str, str]", fields)
+
+ @property
+ def fields(self) -> Mapping[str, str]:
+ return self._package_fields
+
+ @property
+ def name(self) -> str:
+ return self._package_fields["Source"]