"""architecture matching
This leverages code from dpkg's Dpkg::Arch as well as python rewrites from
other people. Copyright years imported from the sources.
@copyright: 2006-2015 Guillem Jover <guillem@debian.org>
@copyright: 2014, Ansgar Burchardt <ansgar@debian.org>
@copyright: 2014-2017, Johannes Schauer Marin Rodrigues <josch@debian.org>
@copyright: 2022, Niels Thykier <niels@thykier.net>
@license: GPL-2+
"""
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import os
try:
from typing import Iterable, Optional, IO, List, Dict, Union
from os import PathLike
except ImportError:
pass
import collections.abc
def _parse_table_file(fd):
# type: (IO[str]) -> Iterable[List[str]]
for line in fd:
line = line.rstrip()
if not line or line.startswith("#"):
continue
yield line.split()
_QuadTuple = collections.namedtuple("_QuadTuple", ['api_name', 'libc_name', 'os_name', 'cpu_name'])
class QuadTupleDpkgArchitecture(_QuadTuple):
"""Implementation detail of ArchTable"""
def __contains__(self, item):
# type: (object) -> bool
if isinstance(item, QuadTupleDpkgArchitecture):
# This covers both equal and wildcard matches and semantically matches how dpkg does it
return self.api_name in ('any', item.api_name) \
and self.libc_name in ('any', item.libc_name) \
and self.os_name in ('any', item.os_name) \
and self.cpu_name in ('any', item.cpu_name)
return super().__contains__(item)
@property
def is_wildcard(self):
# type: () -> bool
return any(x == 'any' for x in self)
class DpkgArchTable:
def __init__(self, arch2tuple):
# type: (Dict[str, QuadTupleDpkgArchitecture]) -> None
self._arch2table = arch2tuple
self._wildcard_cache = {
'any': QuadTupleDpkgArchitecture('any', 'any', 'any', 'any')
} # type: Dict[str, QuadTupleDpkgArchitecture]
@classmethod
def load_arch_table(cls, path='/usr/share/dpkg'):
# type: (Union[str, PathLike[str]]) -> DpkgArchTable
# NOTE! This method is stubbed in including doctests to support non-Debian systems
# See conftest.py for the concrete implementation and the limited data set available.
"""Load the Dpkg Architecture Table
This class method loads the architecture table from dpkg, so it can be used.
>>> arch_table = DpkgArchTable.load_arch_table()
>>> arch_table.matches_architecture("amd64", "any")
True
The method assumes the dpkg "tuple arch" format version 1.0 or the older triplet format.
:param path: Choose a different directory for loading the architecture data. The provided
directory must contain the architecture data files from dpkg (such as "tupletable" and
"cputable")
"""
tupletable_path = os.path.join(path, 'tupletable')
cputable_path = os.path.join(path, 'cputable')
triplet_compat = False
if not os.path.exists(tupletable_path):
triplettable_path = os.path.join(path, 'triplettable')
if os.path.join(triplettable_path):
triplet_compat = True
tupletable_path = triplettable_path
with open(tupletable_path, encoding='utf-8') as tuple_fd,\
open(cputable_path, encoding='utf-8') as cpu_fd:
return cls._from_file(tuple_fd, cpu_fd, triplet_compat=triplet_compat)
@classmethod
def _from_file(cls, tuple_table_fd, cpu_table_fd, triplet_compat=False):
# type: (IO[str], IO[str], bool) -> DpkgArchTable
arch2tuple = {} # type: Dict[str, QuadTupleDpkgArchitecture]
cpu_list = [x[0] for x in _parse_table_file(cpu_table_fd)]
for row in _parse_table_file(tuple_table_fd):
# Manual unpack (so we support new columns)
dpkg_tuple = row[0]
dpkg_arch = row[1]
if triplet_compat:
dpkg_tuple = "base-" + dpkg_tuple
if '<cpu>' in dpkg_tuple:
for cpu_name in cpu_list:
debtuple_cpu = dpkg_tuple.replace('<cpu>', cpu_name)
dpkg_arch_cpu = dpkg_arch.replace('<cpu>', cpu_name)
arch2tuple[dpkg_arch_cpu] = QuadTupleDpkgArchitecture(
*debtuple_cpu.split('-', 3)
)
else:
arch2tuple[dpkg_arch] = QuadTupleDpkgArchitecture(*dpkg_tuple.split('-', 3))
return DpkgArchTable(arch2tuple)
def _dpkg_wildcard_to_tuple(self, arch):
# type: (str) -> QuadTupleDpkgArchitecture
try:
return self._wildcard_cache[arch]
except KeyError:
pass
arch_tuple = arch.split('-', 3)
if 'any' in arch_tuple:
# This loop was written with the wildcard 'any' is always pre-cached.
# (it might still work)
while len(arch_tuple) < 4:
arch_tuple.insert(0, 'any')
result = QuadTupleDpkgArchitecture(*arch_tuple)
else:
result = self._dpkg_arch_to_tuple(arch)
self._wildcard_cache[arch] = result
return result
def _dpkg_arch_to_tuple(self, dpkg_arch):
# type: (str) -> QuadTupleDpkgArchitecture
if dpkg_arch.startswith("linux-"):
dpkg_arch = dpkg_arch[6:]
return self._arch2table[dpkg_arch]
def matches_architecture(self, architecture, alias):
# type: (str, str) -> bool
"""Determine if a dpkg architecture matches another architecture or a wildcard [debarch_is]
This method is the closest match to dpkg's Dpkg::Arch::debarch_is function.
>>> arch_table = DpkgArchTable.load_arch_table()
>>> arch_table.matches_architecture("amd64", "linux-any")
True
>>> arch_table.matches_architecture("i386", "linux-any")
True
>>> arch_table.matches_architecture("amd64", "amd64")
True
>>> arch_table.matches_architecture("i386", "amd64")
False
>>> arch_table.matches_architecture("all", "amd64")
False
>>> arch_table.matches_architecture("all", "all")
True
>>> # i386 is the short form of linux-i386. Therefore, it does not match kfreebsd-i386
>>> arch_table.matches_architecture("i386", "kfreebsd-i386")
False
>>> # Note that "armel" and "armhf" are "arm" CPUs, so it is matched by "any-arm"
>>> # (similar holds for some other architecture <-> CPU name combinations)
>>> all(arch_table.matches_architecture(n, 'any-arm') for n in ['armel', 'armhf'])
True
>>> # Since "armel" is not a valid CPU name, this returns False (the correct would be
>>> # any-arm as noted above)
>>> arch_table.matches_architecture("armel", "any-armel")
False
>>> # Wildcards used as architecture always fail (except for special cases noted in the
>>> # compatibility notes below)
>>> arch_table.matches_architecture("any-i386", "i386")
False
>>> # any-i386 is not a subset of linux-any (they only have i386/linux-i386 as overlap)
>>> arch_table.matches_architecture("any-i386", "linux-any")
False
>>> # Compatibility with dpkg - if alias is `any` then it always returns True
>>> # even if the input otherwise would not make sense.
>>> arch_table.matches_architecture("any-unknown", "any")
True
>>> # Another side effect of the dpkg compatibility
>>> arch_table.matches_architecture("all", "any")
True
Compatibility note: The method emulates Dpkg::Arch::debarch_is function and therefore
returns True if both parameters are the same even though they are wildcards or not known
to be architectures. Additionally, if `alias` is `any`, then this method always returns
True as `any` is the "match-everything-wildcard".
:param architecture: A string representing a dpkg architecture.
:param alias: A string representing a dpkg architecture or wildcard
to match with.
:returns: True if the `architecture` parameter is (logically) the same as the `alias`
parameter OR, if `alias` is a wildcard, the `architecture` parameter is a
subset of the wildcard.
The method returns False if `architecture` is not a known dpkg architecture,
or it is a wildcard.
"""
if alias in ('any', architecture):
# Dpkg::Arch has this shortcut too, which does not check whether they are valid
# architectures.
return True
try:
dpkg_arch = self._dpkg_arch_to_tuple(architecture)
dpkg_wildcard = self._dpkg_wildcard_to_tuple(alias)
except KeyError:
return False
return dpkg_arch in dpkg_wildcard
def architecture_equals(self, arch1, arch2):
# type: (str, str) -> bool
"""Determine whether two dpkg architecture are exactly the same [debarch_eq]
Unlike Python's `==` operator, this method also accounts for things like `linux-amd64` is
a valid spelling of the dpkg architecture `amd64` (i.e.,
`architecture_equals("linux-amd64", "amd64")` is True).
This method is the closest match to dpkg's Dpkg::Arch::debarch_eq function.
>>> arch_table = DpkgArchTable.load_arch_table()
>>> arch_table.architecture_equals("linux-amd64", "amd64")
True
>>> arch_table.architecture_equals("amd64", "linux-i386")
False
>>> arch_table.architecture_equals("i386", "linux-amd64")
False
>>> arch_table.architecture_equals("amd64", "amd64")
True
>>> arch_table.architecture_equals("i386", "amd64")
False
>>> # Compatibility with dpkg: if the parameters are equal, then it always return True
>>> arch_table.architecture_equals("unknown", "unknown")
True
Compatibility note: The method emulates Dpkg::Arch::debarch_eq function and therefore
returns True if both parameters are the same even though they are wildcards or not known
to be architectures.
:param arch1: A string representing a dpkg architecture.
:param arch2: A string representing a dpkg architecture.
:returns: True if the dpkg architecture parameters are (logically) the exact same.
"""
if arch1 == arch2:
# Dpkg::Arch has this shortcut too, which does not check whether they are valid
# architectures.
return True
try:
dpkg_arch1 = self._dpkg_arch_to_tuple(arch1)
dpkg_arch2 = self._dpkg_arch_to_tuple(arch2)
except KeyError:
return False
return dpkg_arch1 == dpkg_arch2
def architecture_is_concerned(self, architecture, architecture_restrictions,
allow_mixing_positive_and_negative=False,
):
# type: (str, Iterable[str], bool) -> bool
"""Determine if a dpkg architecture is part of a list of restrictions [debarch_is_concerned]
This method is the closest match to dpkg's Dpkg::Arch::debarch_is_concerned function.
Compatibility notes:
* The Dpkg::Arch::debarch_is_concerned function allow matching of negative and positive
restrictions by default. Often, this behaviour is not allowed nor recommended and the
Debian Policy does not allow this practice in e.g., Build-Depends. Therefore, this
implementation defaults to raising ValueError when this occurs. If the original
behaviour is needed, set `allow_mixing_positive_and_negative` to True.
* The Dpkg::Arch::debarch_is_concerned function is lazy and exits as soon as it finds a
match. This means that if negative and positive restrictions are mixed, then order of
the matches are important. This adaption matches that behaviour (provided that
`allow_mixing_positive_and_negative` is set to True)
>>> arch_table = DpkgArchTable.load_arch_table()
>>> arch_table.architecture_is_concerned("linux-amd64", ["amd64", "i386"])
True
>>> arch_table.architecture_is_concerned("amd64", ["!amd64", "!i386"])
False
>>> # This is False because the "!amd64" is matched first.
>>> arch_table.architecture_is_concerned("linux-amd64", ["!linux-amd64", "linux-any"],
... allow_mixing_positive_and_negative=True)
False
>>> # This is True because the "linux-any" is matched first.
>>> arch_table.architecture_is_concerned("linux-amd64", ["linux-any", "!linux-amd64"],
... allow_mixing_positive_and_negative=True)
True
:param architecture: A string representing a dpkg architecture/wildcard.
:param architecture_restrictions: A list of positive (amd64) or negative (!amd64) dpkg
architectures or/and wildcards.
:param allow_mixing_positive_and_negative: If True, the `architecture_restrictions` list
can mix positive and negative (e.g., ["!any-amd64", "any"])
restrictions. If False, mixing will trigger a ValueError.
:returns: True if `architecture` is accepted by the `architecture_restrictions`.
"""
# Our implementation diverges a bit from the Dpkg::Arch one because we want to enforce
# allow_mixing_positive_and_negative=False even if the input matches before the
# inconsistency is detected.
verdict = None # type: Optional[bool]
positive_match_seen = False
negative_match_seen = False
arch_restriction_iter = iter(architecture_restrictions)
try:
dpkg_arch = self._dpkg_arch_to_tuple(architecture)
except KeyError:
return False
for arch_restriction in arch_restriction_iter:
# Should not happen in practice, but remove the special-case to avoid IndexError
# (dpkg assumes invalid/unknown input does not match, so we can use a "continue" here)
if arch_restriction == '':
continue
if arch_restriction[0] == '!':
negative_match_seen = True
else:
positive_match_seen = True
if verdict is not None:
# We already know what the answer is. However, we are running through the remaining
# input to ensure there is mixing of positive and negative restrictions.
continue
# Blindly matching Dpkg::Arch here, which also forgives uppercase letters.
arch_restriction = arch_restriction.lower()
verdict_if_matched = True
arch_restriction_positive = arch_restriction
if arch_restriction[0] == '!':
verdict_if_matched = False
arch_restriction_positive = arch_restriction[1:]
dpkg_wildcard = self._dpkg_wildcard_to_tuple(arch_restriction_positive)
# Inlined version of self.matches_architecture to reduce the number of lookups
if dpkg_arch in dpkg_wildcard:
verdict = verdict_if_matched
if allow_mixing_positive_and_negative:
# If we do not care about the mixing, then we can closer emulate the dpkg
# implementation by existing early now
return verdict
if not allow_mixing_positive_and_negative and positive_match_seen and negative_match_seen:
raise ValueError("architecture_restrictions contained mixed positive and negative"
"restrictions (and allow_mixing_positive_and_negative was not True)")
# If none of the restrictions directly matched the architecture, then this is now
# a question of whether there was a negative match. If there was a negative match,
# then it would have included the input as it is basically "any except <this>"
if verdict is None:
verdict = negative_match_seen
return verdict
def is_wildcard(self, wildcard):
# type: (str) -> bool
"""Determine if a given string is a dpkg wildcard [debarch_is_wildcard]
This method is the closest match to dpkg's Dpkg::Arch::debarch_is_wildcard function.
>>> arch_table = DpkgArchTable.load_arch_table()
>>> arch_table.is_wildcard("linux-any")
True
>>> arch_table.is_wildcard("amd64")
False
>>> arch_table.is_wildcard("unknown")
False
>>> # Compatibility with the dpkg version of the function.
>>> arch_table.is_wildcard("unknown-any")
True
Compatibility note: The original dpkg function does not ensure that the wildcard matches
any supported architecture and this re-implementation matches that behaviour. Therefore,
this method can return True for a wildcard that can never match anything in practice.
:param wildcard: A string that might represent a dpkg architecture or wildcard.
:returns: True the parameter is a known dpkg wildcard.
"""
try:
dpkg_arch = self._dpkg_wildcard_to_tuple(wildcard)
except KeyError:
return False
else:
# _dpkg_wildcard_to_tuple falls back to concrete architectures so this can be False
return dpkg_arch.is_wildcard
|