diff options
Diffstat (limited to 'src/debputy/architecture_support.py')
-rw-r--r-- | src/debputy/architecture_support.py | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/src/debputy/architecture_support.py b/src/debputy/architecture_support.py new file mode 100644 index 0000000..e190722 --- /dev/null +++ b/src/debputy/architecture_support.py @@ -0,0 +1,233 @@ +import os +import subprocess +from functools import lru_cache +from typing import Dict, Optional, Iterator, Tuple + + +class DpkgArchitectureBuildProcessValuesTable: + """Dict-like interface to dpkg-architecture values""" + + def __init__(self, *, mocked_answers: Optional[Dict[str, str]] = None) -> None: + """Create a new dpkg-architecture table; NO INSTANTIATION + + This object will be created for you; if you need a production instance + then call dpkg_architecture_table(). If you need a testing instance, + then call mock_arch_table(...) + + :param mocked_answers: Used for testing purposes. Do not use directly; + instead use mock_arch_table(...) to create the table you want. + """ + self._architecture_cache: Dict[str, str] = {} + self._has_run_dpkg_architecture = False + if mocked_answers is None: + self._architecture_cache = {} + self._respect_environ: bool = True + self._has_run_dpkg_architecture = False + else: + self._architecture_cache = mocked_answers + self._respect_environ = False + self._has_run_dpkg_architecture = True + + def __contains__(self, item: str) -> bool: + try: + self[item] + except KeyError: + return False + else: + return True + + def __getitem__(self, item: str) -> str: + if item not in self._architecture_cache: + if self._respect_environ: + value = os.environ.get(item) + if value is not None: + self._architecture_cache[item] = value + return value + if not self._has_run_dpkg_architecture: + self._load_dpkg_architecture_values() + # Fall through and look it up in the cache + return self._architecture_cache[item] + + def __iter__(self) -> Iterator[str]: + if not self._has_run_dpkg_architecture: + self._load_dpkg_architecture_values() + yield from self._architecture_cache + + @property + def current_host_arch(self) -> str: + """The architecture we are building for + + This is the architecture name you need if you are in doubt. + """ + return self["DEB_HOST_ARCH"] + + @property + def current_host_multiarch(self) -> str: + """The multi-arch path basename + + This is the multi-arch basename name you need if you are in doubt. It + goes here: + + "/usr/lib/{MA}".format(table.current_host_multiarch) + + """ + return self["DEB_HOST_MULTIARCH"] + + @property + def is_cross_compiling(self) -> bool: + """Whether we are cross-compiling + + This is defined as DEB_BUILD_GNU_TYPE != DEB_HOST_GNU_TYPE and + affects whether we can rely on being able to run the binaries + that are compiled. + """ + return self["DEB_BUILD_GNU_TYPE"] != self["DEB_HOST_GNU_TYPE"] + + def _load_dpkg_architecture_values(self) -> None: + env = dict(os.environ) + # For performance, disable dpkg's translation later + env["DPKG_NLS"] = "0" + kw_pairs = _parse_dpkg_arch_output( + subprocess.check_output( + ["dpkg-architecture"], + env=env, + ) + ) + for k, v in kw_pairs: + self._architecture_cache[k] = os.environ.get(k, v) + self._has_run_dpkg_architecture = True + + +def _parse_dpkg_arch_output(output: bytes) -> Iterator[Tuple[str, str]]: + text = output.decode("utf-8") + for line in text.splitlines(): + k, v = line.strip().split("=", 1) + yield k, v + + +def _rewrite(value: str, from_pattern: str, to_pattern: str) -> str: + assert value.startswith(from_pattern) + return to_pattern + value[len(from_pattern) :] + + +def faked_arch_table( + host_arch: str, + *, + build_arch: Optional[str] = None, + target_arch: Optional[str] = None, +) -> DpkgArchitectureBuildProcessValuesTable: + """Creates a mocked instance of DpkgArchitectureBuildProcessValuesTable + + + :param host_arch: The dpkg architecture to mock answers for. This affects + DEB_HOST_* values and defines the default for DEB_{BUILD,TARGET}_* if + not overridden. + :param build_arch: If set and has a different value than host_arch, then + pretend this is a cross-build. This value affects the DEB_BUILD_* values. + :param target_arch: If set and has a different value than host_arch, then + pretend this is a build _of_ a cross-compiler. This value affects the + DEB_TARGET_* values. + """ + + if build_arch is None: + build_arch = host_arch + + if target_arch is None: + target_arch = host_arch + return _faked_arch_tables(host_arch, build_arch, target_arch) + + +@lru_cache +def _faked_arch_tables( + host_arch: str, build_arch: str, target_arch: str +) -> DpkgArchitectureBuildProcessValuesTable: + mock_table = {} + + env = dict(os.environ) + # Set CC to /bin/true avoid a warning from dpkg-architecture + env["CC"] = "/bin/true" + # For performance, disable dpkg's translation later + env["DPKG_NLS"] = "0" + # Clear environ variables that might confuse dpkg-architecture + for k in os.environ: + if k.startswith("DEB_"): + del env[k] + + if build_arch == host_arch: + # easy / common case - we can handle this with a single call + kw_pairs = _parse_dpkg_arch_output( + subprocess.check_output( + ["dpkg-architecture", "-a", host_arch, "-A", target_arch], + env=env, + ) + ) + for k, v in kw_pairs: + if k.startswith(("DEB_HOST_", "DEB_TARGET_")): + mock_table[k] = v + # Clone DEB_HOST_* into DEB_BUILD_* as well + if k.startswith("DEB_HOST_"): + k2 = _rewrite(k, "DEB_HOST_", "DEB_BUILD_") + mock_table[k2] = v + elif build_arch != host_arch and host_arch != target_arch: + # This will need two dpkg-architecture calls because we cannot set + # DEB_BUILD_* directly. But we can set DEB_HOST_* and then rewrite + # it + # First handle the build arch + kw_pairs = _parse_dpkg_arch_output( + subprocess.check_output( + ["dpkg-architecture", "-a", build_arch], + env=env, + ) + ) + for k, v in kw_pairs: + if k.startswith("DEB_HOST_"): + k = _rewrite(k, "DEB_HOST_", "DEB_BUILD_") + mock_table[k] = v + + kw_pairs = _parse_dpkg_arch_output( + subprocess.check_output( + ["dpkg-architecture", "-a", host_arch, "-A", target_arch], + env=env, + ) + ) + for k, v in kw_pairs: + if k.startswith(("DEB_HOST_", "DEB_TARGET_")): + mock_table[k] = v + else: + # This is a fun special case. We know that: + # * build_arch != host_arch + # * host_arch == target_arch + # otherwise we would have hit one of the previous cases. + # + # We can do this in a single call to dpkg-architecture by + # a bit of "cleaver" rewriting. + # + # - Use -a to set DEB_HOST_* and then rewrite that as + # DEB_BUILD_* + # - use -A to set DEB_TARGET_* and then use that for both + # DEB_HOST_* and DEB_TARGET_* + + kw_pairs = _parse_dpkg_arch_output( + subprocess.check_output( + ["dpkg-architecture", "-a", build_arch, "-A", target_arch], env=env + ) + ) + for k, v in kw_pairs: + if k.startswith("DEB_HOST_"): + k2 = _rewrite(k, "DEB_HOST_", "DEB_BUILD_") + mock_table[k2] = v + continue + if k.startswith("DEB_TARGET_"): + mock_table[k] = v + k2 = _rewrite(k, "DEB_TARGET_", "DEB_HOST_") + mock_table[k2] = v + + table = DpkgArchitectureBuildProcessValuesTable(mocked_answers=mock_table) + return table + + +_ARCH_TABLE = DpkgArchitectureBuildProcessValuesTable() + + +def dpkg_architecture_table() -> DpkgArchitectureBuildProcessValuesTable: + return _ARCH_TABLE |