summaryrefslogtreecommitdiffstats
path: root/src/debputy/architecture_support.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/architecture_support.py')
-rw-r--r--src/debputy/architecture_support.py233
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