From 3565071f226432336a54d0193d729fa4508a3394 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 11 Apr 2024 10:28:00 +0200 Subject: Adding debian version 6.6.15-2. Signed-off-by: Daniel Baumann --- debian/lib/python/debian_linux/debian.py | 705 +++++++++++++++++++++++++++++++ 1 file changed, 705 insertions(+) create mode 100644 debian/lib/python/debian_linux/debian.py (limited to 'debian/lib/python/debian_linux/debian.py') diff --git a/debian/lib/python/debian_linux/debian.py b/debian/lib/python/debian_linux/debian.py new file mode 100644 index 0000000000..78c5cdec7b --- /dev/null +++ b/debian/lib/python/debian_linux/debian.py @@ -0,0 +1,705 @@ +from __future__ import annotations + +import collections +import collections.abc +import dataclasses +import enum +import itertools +import os.path +import re +import typing +import warnings +from typing import ( + Iterable, + Self, + TypeAlias, +) + + +class Changelog(list): + _top_rules = r""" +^ +(?P + \w[-+0-9a-z.]+ +) +[ ] +\( +(?P + [^\(\)\ \t]+ +) +\) +\s+ +(?P + [-+0-9a-zA-Z.]+ +) +\;\s+urgency= +(?P + \w+ +) +(?:,|\n) +""" + _top_re = re.compile(_top_rules, re.X) + _bottom_rules = r""" +^ +[ ]--[ ] +(?P + \S(?:[ ]?\S)* +) +[ ]{2} +(?P + (.*) +) +\n +""" + _bottom_re = re.compile(_bottom_rules, re.X) + _ignore_re = re.compile(r'^(?: |\s*\n)') + + class Entry(object): + __slot__ = ('distribution', 'source', 'version', 'urgency', + 'maintainer', 'date') + + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + def __init__(self, dir='', version=None, file=None) -> None: + if version is None: + version = Version + if file: + self._parse(version, file) + else: + with open(os.path.join(dir, "debian/changelog"), + encoding="UTF-8") as f: + self._parse(version, f) + + def _parse(self, version, f) -> None: + top_match = None + line_no = 0 + + for line in f: + line_no += 1 + + if self._ignore_re.match(line): + pass + elif top_match is None: + top_match = self._top_re.match(line) + if not top_match: + raise Exception('invalid top line %d in changelog' % + line_no) + try: + v = version(top_match.group('version')) + except Exception: + if not len(self): + raise + v = Version(top_match.group('version')) + else: + bottom_match = self._bottom_re.match(line) + if not bottom_match: + raise Exception('invalid bottom line %d in changelog' % + line_no) + + self.append(self.Entry( + distribution=top_match.group('distribution'), + source=top_match.group('source'), + version=v, + urgency=top_match.group('urgency'), + maintainer=bottom_match.group('maintainer'), + date=bottom_match.group('date'))) + top_match = bottom_match = None + + +class Version(object): + revision: str | None + + _epoch_re = re.compile(r'\d+$') + _upstream_re = re.compile(r'[0-9][A-Za-z0-9.+\-:~]*$') + _revision_re = re.compile(r'[A-Za-z0-9+.~]+$') + + def __init__(self, version) -> None: + try: + split = version.index(':') + except ValueError: + epoch, rest = None, version + else: + epoch, rest = version[0:split], version[split+1:] + try: + split = rest.rindex('-') + except ValueError: + upstream, revision = rest, None + else: + upstream, revision = rest[0:split], rest[split+1:] + if (epoch is not None and not self._epoch_re.match(epoch)) or \ + not self._upstream_re.match(upstream) or \ + (revision is not None and not self._revision_re.match(revision)): + raise RuntimeError(u"Invalid debian version") + self.epoch = epoch and int(epoch) + self.upstream = upstream + self.revision = revision + + def __str__(self) -> str: + return self.complete + + @property + def complete(self) -> str: + if self.epoch is not None: + return u"%d:%s" % (self.epoch, self.complete_noepoch) + return self.complete_noepoch + + @property + def complete_noepoch(self) -> str: + if self.revision is not None: + return u"%s-%s" % (self.upstream, self.revision) + return self.upstream + + @property + def debian(self) -> str | None: + from warnings import warn + warn(u"debian argument was replaced by revision", DeprecationWarning, + stacklevel=2) + return self.revision + + +class VersionLinux(Version): + _upstream_re = re.compile(r""" +(?P + \d+\.\d+ +) +(?P + (?:\.\d+)? + (?:-[a-z]+\d+)? +) +(?: + ~ + (?P + .+? + ) +)? +(?: + \.dfsg\. + (?P + \d+ + ) +)? +$ + """, re.X) + _revision_re = re.compile(r""" +\d+ +(\.\d+)? +(?: + (?P + ~exp\d+ + ) + | + (?P + (?:[~+]deb\d+u\d+)+ + )? + (?P + ~bpo\d+\+\d+ + )? + | + (?P + .+? + ) +) +(?:\+b\d+)? +$ + """, re.X) + + def __init__(self, version) -> None: + super(VersionLinux, self).__init__(version) + up_match = self._upstream_re.match(self.upstream) + assert self.revision is not None + rev_match = self._revision_re.match(self.revision) + if up_match is None or rev_match is None: + raise RuntimeError(u"Invalid debian linux version") + d = up_match.groupdict() + self.linux_modifier = d['modifier'] + self.linux_version = d['version'] + if d['modifier'] is not None: + assert not d['update'] + self.linux_upstream = '-'.join((d['version'], d['modifier'])) + else: + self.linux_upstream = d['version'] + self.linux_upstream_full = self.linux_upstream + d['update'] + self.linux_dfsg = d['dfsg'] + d = rev_match.groupdict() + self.linux_revision_experimental = d['revision_experimental'] and True + self.linux_revision_security = d['revision_security'] and True + self.linux_revision_backports = d['revision_backports'] and True + self.linux_revision_other = d['revision_other'] and True + + +class PackageArchitecture(set[str]): + def __init__( + self, + v: str | Iterable[str] | None = None, + /, + ) -> None: + if v: + if isinstance(v, str): + v = re.split(r'\s+', v.strip()) + self |= frozenset(v) + + def __str__(self) -> str: + return ' '.join(sorted(self)) + + +class PackageDescription: + short: list[str] + long: list[str] + + def __init__( + self, + v: str | Self | None = None, + /, + ) -> None: + self.short = [] + self.long = [] + + if v: + if isinstance(v, str): + desc_split = v.split('\n', 1) + self.append_short(desc_split[0]) + if len(desc_split) == 2: + self.append(desc_split[1]) + else: + self.short.extend(v.short) + self.long.extend(v.long) + + def __str__(self) -> str: + from .utils import TextWrapper + wrap = TextWrapper(width=74, fix_sentence_endings=True).wrap + short = ', '.join(self.short) + long_pars = [] + for i in self.long: + long_pars.append(wrap(i)) + long = '\n .\n '.join('\n '.join(i) for i in long_pars) + return short + '\n ' + long if long else short + + def append(self, long: str) -> None: + long = long.strip() + if long: + self.long.extend(long.split('\n.\n')) + + def append_short(self, short: str) -> None: + for i in [i.strip() for i in short.split(',')]: + if i: + self.short.append(i) + + def extend(self, desc: PackageDescription) -> None: + self.short.extend(desc.short) + self.long.extend(desc.long) + + +class PackageRelationEntryOperator(enum.StrEnum): + OP_LT = '<<' + OP_LE = '<=' + OP_EQ = '=' + OP_NE = '!=' + OP_GE = '>=' + OP_GT = '>>' + + def __neg__(self) -> PackageRelationEntryOperator: + return typing.cast(PackageRelationEntryOperator, { + self.OP_LT: self.OP_GE, + self.OP_LE: self.OP_GT, + self.OP_EQ: self.OP_NE, + self.OP_NE: self.OP_EQ, + self.OP_GE: self.OP_LT, + self.OP_GT: self.OP_LE, + }[self]) + + +class PackageRelationEntry: + name: str + operator: typing.Optional[PackageRelationEntryOperator] + version: typing.Optional[str] + arches: PackageArchitecture + restrictions: PackageBuildprofile + + __re = re.compile( + r'^(?P\S+)' + r'(?: \((?P<<|<=|=|!=|>=|>>)\s*(?P[^)]+)\))?' + r'(?: \[(?P[^]]+)\])?' + r'(?P(?: <[^>]+>)*)$' + ) + + def __init__( + self, + v: str | Self, + /, *, + name: str | None = None, + arches: set[str] | None = None, + restrictions: PackageBuildprofile | str | None = None, + ) -> None: + if isinstance(v, str): + match = self.__re.match(v) + if not match: + raise RuntimeError('Unable to parse dependency "%s"' % v) + + self.name = name or match['name'] + + if operator := match['operator']: + self.operator = PackageRelationEntryOperator(operator) + else: + self.operator = None + + self.version = match['version'] + self.arches = PackageArchitecture(arches or match['arches']) + if isinstance(restrictions, PackageBuildprofile): + self.restrictions = restrictions.copy() + else: + self.restrictions = PackageBuildprofile.parse( + restrictions or match['restrictions'], + ) + + else: + self.name = name or v.name + self.operator = v.operator + self.version = v.version + self.arches = PackageArchitecture(arches or v.arches) + if isinstance(restrictions, str): + self.restrictions = PackageBuildprofile.parse(restrictions) + else: + self.restrictions = (restrictions or v.restrictions).copy() + + def __str__(self): + ret = [self.name] + if self.operator and self.version: + ret.append(f'({self.operator} {self.version})') + if self.arches: + ret.append(f'[{self.arches}]') + if self.restrictions: + ret.append(str(self.restrictions)) + return ' '.join(ret) + + +class PackageRelationGroup(list[PackageRelationEntry]): + def __init__( + self, + v: Iterable[PackageRelationEntry | str] | str | Self | None = None, + /, *, + arches: set[str] | None = None, + ) -> None: + if v: + if isinstance(v, str): + v = (i.strip() for i in re.split(r'\|', v.strip())) + self.extend(PackageRelationEntry(i, arches=arches) for i in v if i) + + def __str__(self) -> str: + return ' | '.join(str(i) for i in self) + + def _merge_eq(self, v: PackageRelationGroup) -> typing.Optional[PackageRelationGroup]: + if all( + ( + i.name == j.name and i.operator == j.operator + and i.version == j.version + ) for i, j in zip(self, v) + ): + return self + return None + + +class PackageRelation(list[PackageRelationGroup]): + Init: TypeAlias = PackageRelationGroup | Iterable[PackageRelationEntry] | str + + def __init__( + self, + v: Iterable[Init] | str | Self | None = None, + /, *, + arches: set[str] | None = None, + ) -> None: + if v: + if isinstance(v, str): + v = (i.strip() for i in re.split(r',', v.strip())) + self.extend(PackageRelationGroup(i, arches=arches) for i in v if i) + + def __str__(self) -> str: + return ', '.join(str(i) for i in self) + + def _merge_eq(self, v: PackageRelationGroup) -> typing.Optional[PackageRelationGroup]: + for i in self: + if i._merge_eq(v): + return i + return None + + def merge( + self, + v: Init | str, + /, + ) -> None: + v = PackageRelationGroup(v) + if g := self._merge_eq(v): + for i, j in zip(g, v): + i.arches |= j.arches + i.restrictions.update(j.restrictions) + else: + super().append(v) + + +@dataclasses.dataclass +class PackageBuildprofileEntry: + pos: set[str] = dataclasses.field(default_factory=set) + neg: set[str] = dataclasses.field(default_factory=set) + + __re = re.compile(r'^<(?P[a-z0-9. !-]+)>$') + + def copy(self) -> Self: + return self.__class__( + pos=set(self.pos), + neg=set(self.neg), + ) + + @classmethod + def parse(cls, v: str, /) -> Self: + match = cls.__re.match(v) + if not match: + raise RuntimeError('Unable to parse build profile "%s"' % v) + + ret = cls() + for i in re.split(r' ', match.group('profiles')): + if i: + if i[0] == '!': + ret.neg.add(i[1:]) + else: + ret.pos.add(i) + return ret + + def __eq__(self, other: object, /) -> bool: + if not isinstance(other, PackageBuildprofileEntry): + return NotImplemented + return self.pos == other.pos and self.neg == other.neg + + def isdisjoint(self, other: Self, /) -> bool: + return not (self.issubset(other)) and not (self.issuperset(other)) + + def issubset(self, other: Self, /) -> bool: + ''' + Test wether this build profile would select a subset of packages. + + For positive profile matches: Ading profiles will select a subset. + For negative profile matches: Removing profiles will select a subset. + ''' + return self.pos >= other.pos and self.neg <= other.neg + __le__ = issubset + + def issuperset(self, other: Self, /) -> bool: + ''' + Test wether this build profile would select a superset of packages. + + For positive profile matches: Removing profiles will select a superset. + For negative profile matches: Adding profiles will select a superset. + ''' + return self.pos <= other.pos and self.neg >= other.neg + __ge__ = issuperset + + def update(self, other: Self, /) -> None: + ''' + Update the build profiles, adding entries from other, merging if possible. + + Negating entries (profile vs !profile) are completely removed. + All others remain if they are used on both sides. + ''' + diff = (self.pos & other.neg) | (self.neg & other.pos) + self.pos &= other.pos - diff + self.neg &= other.neg - diff + __ior__ = update + + def __str__(self) -> str: + s = ' '.join(itertools.chain( + sorted(self.pos), + (f'!{i}' for i in sorted(self.neg)), + )) + return f'<{s}>' + + +class PackageBuildprofile(list[PackageBuildprofileEntry]): + __re = re.compile(r' *(<[^>]+>)(?: +|$)') + + def copy(self) -> Self: + return self.__class__(i.copy() for i in self) + + @classmethod + def parse(cls, v: str, /) -> Self: + ret = cls() + for match in cls.__re.finditer(v): + ret.append(PackageBuildprofileEntry.parse(match.group(1))) + return ret + + def update(self, v: Self, /) -> None: + for i in v: + for j in self: + if not j.isdisjoint(i): + j.update(i) + break + else: + self.append(i) + __ior__ = update + + def __str__(self) -> str: + return ' '.join(str(i) for i in self) + + +class _ControlFileDict(collections.abc.MutableMapping): + def __init__(self): + self.__data = {} + self.meta = {} + + def __getitem__(self, key): + return self.__data[key] + + def __setitem__(self, key, value): + if key.lower().startswith('meta-'): + self.meta[key.lower()[5:]] = value + return + + try: + cls = self._fields[key] + if not isinstance(value, cls): + if f := getattr(cls, 'parse', None): + value = f(value) + else: + value = cls(value) + except KeyError: + warnings.warn( + f'setting unknown field { key } in { type(self).__name__ }', + stacklevel=2) + self.__data[key] = value + + def __delitem__(self, key): + del self.__data[key] + + def __iter__(self): + keys = set(self.__data.keys()) + for key in self._fields.keys(): + if key in self.__data: + keys.remove(key) + yield key + for key in sorted(keys): + yield key + + def __len__(self): + return len(self.__data) + + def setdefault(self, key): + try: + return self[key] + except KeyError: + try: + ret = self[key] = self._fields[key]() + except KeyError: + warnings.warn( + f'setting unknown field { key } in { type(self).__name__ }', + stacklevel=2) + ret = self[key] = '' + return ret + + def copy(self): + ret = self.__class__() + ret.__data = self.__data.copy() + ret.meta = self.meta.copy() + return ret + + @classmethod + def read_rfc822(cls, f): + entries = [] + eof = False + + while not eof: + e = cls() + last = None + lines = [] + while True: + line = f.readline() + if not line: + eof = True + break + # Strip comments rather than trying to preserve them + if line[0] == '#': + continue + line = line.strip('\n') + if not line: + break + if line[0] in ' \t': + if not last: + raise ValueError( + 'Continuation line seen before first header') + lines.append(line.lstrip()) + continue + if last: + e[last] = '\n'.join(lines) + i = line.find(':') + if i < 0: + raise ValueError(u"Not a header, not a continuation: ``%s''" % + line) + last = line[:i] + lines = [line[i + 1:].lstrip()] + if last: + e[last] = '\n'.join(lines) + if e: + entries.append(e) + + return entries + + +class SourcePackage(_ControlFileDict): + _fields = collections.OrderedDict(( + ('Source', str), + ('Architecture', PackageArchitecture), + ('Section', str), + ('Priority', str), + ('Maintainer', str), + ('Uploaders', str), + ('Standards-Version', str), + ('Build-Depends', PackageRelation), + ('Build-Depends-Arch', PackageRelation), + ('Build-Depends-Indep', PackageRelation), + ('Rules-Requires-Root', str), + ('Homepage', str), + ('Vcs-Browser', str), + ('Vcs-Git', str), + ('XS-Autobuild', str), + )) + + +class BinaryPackage(_ControlFileDict): + _fields = collections.OrderedDict(( + ('Package', str), + ('Package-Type', str), # for udeb only + ('Architecture', PackageArchitecture), + ('Section', str), + ('Priority', str), + # Build-Depends* fields aren't allowed for binary packages in + # the real control file, but we move them to the source + # package + ('Build-Depends', PackageRelation), + ('Build-Depends-Arch', PackageRelation), + ('Build-Depends-Indep', PackageRelation), + ('Build-Profiles', PackageBuildprofile), + ('Built-Using', PackageRelation), + ('Provides', PackageRelation), + ('Pre-Depends', PackageRelation), + ('Depends', PackageRelation), + ('Recommends', PackageRelation), + ('Suggests', PackageRelation), + ('Replaces', PackageRelation), + ('Breaks', PackageRelation), + ('Conflicts', PackageRelation), + ('Multi-Arch', str), + ('Kernel-Version', str), # for udeb only + ('Description', PackageDescription), + ('Homepage', str), + )) + + +class TestsControl(_ControlFileDict): + _fields = collections.OrderedDict(( + ('Tests', str), + ('Test-Command', str), + ('Architecture', PackageArchitecture), + ('Restrictions', str), + ('Features', str), + ('Depends', PackageRelation), + ('Tests-Directory', str), + ('Classes', str), + )) -- cgit v1.2.3