diff -Nru software-properties-0.99.36/add-apt-repository software-properties-0.99.37/add-apt-repository --- software-properties-0.99.36/add-apt-repository 2023-05-04 08:04:59.000000000 +0000 +++ software-properties-0.99.37/add-apt-repository 2023-05-16 08:57:35.000000000 +0000 @@ -16,11 +16,10 @@ from softwareproperties.cloudarchive import CloudArchiveShortcutHandler from softwareproperties.sourceslist import SourcesListShortcutHandler from softwareproperties.uri import URIShortcutHandler +from softwareproperties.sourceutils import * -from softwareproperties.extendedsourceslist import (SourceEntry, - SourcesList, - CollapsedSourcesList) +from aptsources.sourceslist import SourcesList, SourceEntry from aptsources.distro import get_distro from copy import copy from gettext import gettext as _ @@ -149,101 +148,182 @@ print(_("Adding repository.")) self.prompt_user() + def _add_components(self, sources): + components = {} + for entry in sources: + key = (entry.uri, entry.dist, entry.type) + if key in components: + components[key] += entry.comps + else: + components[key] = copy(entry.comps) + + for (uri, dist, type), comps in components.items(): + added = set(self.components) - set(comps) + if added: + comps = list(set(comps) | set(self.components)) + entry = self.sourceslist.add( + type=type, + dist=dist, + uri=uri, + orig_comps=comps, + file=SOURCESLIST + ) + print(_("Added %s to: %s") % (' '.join(added), str(entry))) + + def _remove_components(self, sources): + for entry in sources: + removed = set(entry.comps) & set(self.components) + if removed: + entry.comps = list(set(entry.comps) - set(self.components)) + print(_("Removed %s from: %s") % (' '.join(removed), str(entry))) + if not entry.comps: + self.sourceslist.remove(entry) + def change_components(self): - collapsedlist = CollapsedSourcesList(self.sourceslist, files=[SOURCESLIST]) + sources = [s for s in self.sourceslist if not s.invalid \ + and not s.disabled and s.file == SOURCESLIST] - for entry in collapsedlist.filter(invalid=False, disabled=False): - if self.options.remove: - removed = set(entry.comps) & set(self.components) - if removed: - entry.comps = list(set(entry.comps) - set(self.components)) - print(_("Removed %s from: %s") % (' '.join(removed), str(entry))) - else: - added = set(self.components) - set(entry.comps) - if added: - entry.comps = list(set(entry.comps) | set(self.components)) - print(_("Added %s to: %s") % (' '.join(added), str(entry))) + if self.options.remove: + self._remove_components(sources) + else: + self._add_components(sources) if not self.dry_run: self.sourceslist.save() - def _add_pocket(self, collapsedlist): - binary_entries = collapsedlist.filter(invalid=False, disabled=False, - type=self.binary_type) - - for uri in set([e.uri for e in binary_entries]): - pockets = {e.pocket: e for e in binary_entries if e.uri == uri} - if self.pocket in pockets: - print(_("Existing: %s") % str(pockets[self.pocket])) + def _add_pocket(self, sources): + binary_entries = {} + for s in sources: + if s.invalid or s.disabled: + continue + if s.type != self.binary_type or s.file != SOURCESLIST: + continue + + suite = get_source_entry_suite(s) + if (s.uri, suite) in binary_entries: + binary_entries[(s.uri, suite)].append(s) + else: + binary_entries[(s.uri, suite)] = [s] + + for (uri, suite), entries in binary_entries.items(): + pockets = [] + have_pocket = False + have_release = False + + for e in entries: + p = get_source_entry_pocket(e) + if p == self.pocket: + print(_("Existing: %s") % str(e)) + have_pocket = True + elif p == 'release': + have_release = True + pockets.append(p) + + if have_pocket: continue - if 'release' in pockets: - entry = copy(pockets['release']) + + if have_release: + comps = [] + for e in entries: + if get_source_entry_pocket(e) == 'release': + comps += e.comps + comps = list(set(comps)) else: - # without existing release pocket, use default comps comps = ['main', 'restricted'] - entry = next(iter(pockets.values()))._replace(comps=comps) - entry = entry._replace(pocket=self.pocket) + + entry = sources.add( + type=self.binary_type, + uri=uri, + dist='{}-{}'.format(suite, self.pocket), + orig_comps=comps, + file=SOURCESLIST + ) print(_("Adding: %s") % str(entry)) - collapsedlist.add_entry(entry) - def _remove_pocket(self, collapsedlist): - for entry in collapsedlist.filter(invalid=False, disabled=False, - pocket=self.pocket): + def _remove_pocket(self, sources): + for entry in sources: + if entry.invalid or entry.disabled or entry.file != SOURCESLIST: + continue + + if get_source_entry_pocket(entry) != self.pocket: + continue + entry.set_enabled(False) print(_("Disabled: %s") % str(entry)) def change_pocket(self): - collapsedlist = CollapsedSourcesList(self.sourceslist, files=[SOURCESLIST]) - if self.options.remove: - self._remove_pocket(collapsedlist) + self._remove_pocket(self.sourceslist) else: - self._add_pocket(collapsedlist) + self._add_pocket(self.sourceslist) if not self.dry_run: self.sourceslist.save() def _enable_source(self): - collapsedlist = CollapsedSourcesList(self.sourceslist) - # Check each disabled deb-src line, enable if matching deb line exists - for s in self.sourceslist.filter(invalid=False, disabled=True, type=self.source_type): - b_merged_entry = collapsedlist.get_merged_entry(s._replace(type=self.binary_type, disabled=False)) - if not b_merged_entry: + for s in self.sourceslist: + if s.invalid or not s.disabled or s.type != self.source_type: + continue + + b_entry = None + for b in self.sourceslist: + if b.type != self.binary_type or b.disabled: + continue + + if (b.uri, b.dist, b.comps) != (s.uri, s.dist, s.comps): + continue + + b_entry = b + break + + if not b_entry: # no matching binary lines, leave the source line disabled continue - disabled_comps = list(set(s.comps) - set(b_merged_entry.comps)) - enabled_comps = list(set(s.comps) & set(b_merged_entry.comps)) + disabled_comps = list(set(s.comps) - set(b_entry.comps)) + enabled_comps = list(set(s.comps) & set(b_entry.comps)) if not enabled_comps: # we can't enable any of the line continue if disabled_comps: - index = self.sourceslist.list.index(s) - self.sourceslist.list.insert(index + 1, s._replace(comps=disabled_comps)) + index = sources.list.index(s) + tmp = replace_source_entry(s, comps=disabled_comps) + self.sourceslist.list.insert(index + 1, tmp) s.comps = enabled_comps s.set_enabled(True) - collapsedlist.refresh() print(_("Enabled: %s") % str(s).strip()) # Check each enabled deb line, to warn about missing deb-src lines, or add one if -ss - for b in self.sourceslist.filter(invalid=False, disabled=False, type=self.binary_type): - s = b._replace(type=self.source_type) - s_merged_entry = collapsedlist.get_merged_entry(s) - scomps = set(s_merged_entry.comps if s_merged_entry else []) + for b in self.sourceslist: + if b.invalid or b.disabled or b.type != self.binary_type: + continue + + s = replace_source_entry(b, type=self.source_type) + s_entry = get_source_entry_from_list(self.sourceslist, s) + + scomps = set(s_entry.comps if s_entry else []) missing_comps = list(set(b.comps) - scomps) if not missing_comps: continue s.comps = missing_comps if self.options.enable_source > 1: # with multiple -s, add new deb-src entries if needed for all deb entries - collapsedlist.add_entry(s, after=b) + self.sourceslist.add( + type=s.type, + uri=s.uri, + dist=s.dist, + orig_comps=s.comps, + file=s.file, + ) print(_("Added: %s") % str(s).strip()) else: # if only one -s used, don't add missing deb-src, just notify print(_("Warning, missing deb-src for: %s") % str(s).strip()) def _disable_source(self): - for s in self.sourceslist.filter(invalid=False, disabled=False, type=self.source_type): + for s in self.sourceslist: + if s.invalid or s.disabled or s.type != self.source_type: + continue s.set_enabled(False) print(_("Disabled: %s") % str(s).strip()) @@ -252,6 +332,7 @@ self._disable_source() else: self._enable_source() + if not self.dry_run: self.sourceslist.save() @@ -280,11 +361,20 @@ self.change_source() def show_list(self): - for s in CollapsedSourcesList(self.sourceslist): + merged = {} + for s in self.sourceslist: if s.invalid or s.disabled: continue if not self.enable_source and s.type == self.source_type: continue + + k = (s.type, s.uri, s.dist) + if k in merged: + merged[k].comps += list(set(s.comps) - set(merged[k].comps)) + else: + merged[k] = s + + for s in merged.values(): print(s) def main(self, args=sys.argv[1:]): diff -Nru software-properties-0.99.36/debian/changelog software-properties-0.99.37/debian/changelog --- software-properties-0.99.36/debian/changelog 2023-05-04 08:12:11.000000000 +0000 +++ software-properties-0.99.37/debian/changelog 2023-05-16 08:57:35.000000000 +0000 @@ -1,3 +1,13 @@ +software-properties (0.99.37) mantic; urgency=medium + + [ Nick Rosbrook ] + * Enable deb822 support for PPAs, with GPG key embedded in .sources file. + + [ Julian Andres Klode ] + * Minor tweaks to the deb822 PPA support + + -- Julian Andres Klode Tue, 16 May 2023 10:57:35 +0200 + software-properties (0.99.36) mantic; urgency=medium * cloudarchive: Enable support for the Bobcat Ubuntu Cloud Archive on diff -Nru software-properties-0.99.36/softwareproperties/extendedsourceslist.py software-properties-0.99.37/softwareproperties/extendedsourceslist.py --- software-properties-0.99.36/softwareproperties/extendedsourceslist.py 2023-05-04 08:04:59.000000000 +0000 +++ software-properties-0.99.37/softwareproperties/extendedsourceslist.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,626 +0,0 @@ -# NOTE: do not expect anything in this file to be stable for use outside this package! -# the interface here is a confusing mess because it attempts to provide a compatible -# interface to the python-apt sourceslist classes; however it didn't make it into that -# package, so it's temporarily here, and there's no longer any reason it needs to be -# interface-compatible, so creating a completely new set of classes will be better, -# so these classes should not be relied on in the future. - -import apt_pkg -import logging -import os - -from copy import copy - -from aptsources import sourceslist -from aptsources.distro import get_distro - - -apt_pkg.init() - - -class SourceEntry(sourceslist.SourceEntry): - """ single sources.list entry """ - - @classmethod - def create_entry(cls, **kwargs): - return cls(cls.create_line(**kwargs)) - - @classmethod - def create_line(cls, uri, - disabled=None, - type=None, - dist=None, - suite=None, - pocket=None, - comps=None, - architectures=None, - trusted=None, - comment=None): - """ Create a line from the given parts. - - The 'uri' parameter is mandatory; the rest will be filled with defaults - if not set or if set to None. - - If 'dist' and 'suite' are both provided, 'suite' is ignored. If 'dist' - includes a pocket and 'pocket' is provided, the 'pocket' parameter - will replace the pocket in 'dist'. - """ - if disabled is None: - disabled = False - if type is None: - type = get_distro().binary_type - if suite is None: - suite = get_distro().codename - if comps is None: - comps = [] - if architectures is None: - architectures = [] - - if type.startswith("#"): - # backwards compatibility; please just use disabled param - disabled = True - type = type.lstrip("# ") - - hashmark = "# " if disabled else "" - - options = [] - if architectures: - options.append(f'arch={",".join(architectures)}') - if trusted is not None: - options.append(f'trusted={"yes" if trusted else "no"}') - options = " ".join(options) - if options: - options = f' [{options}]' - - if not dist: - dist = suite - if pocket: - dist = dist.partition('-')[0] - dist = f'{dist}-{pocket}' - - comps = " ".join(comps) - if comps: - comps = f' {comps}' - - if comment: - if not comment.startswith("#"): - comment = f' #{comment}' - elif not comment.startswith(" "): - comment = f' {comment}' - else: - comment = '' - - return f'{hashmark}{type}{options} {uri} {dist}{comps}{comment}'.strip() - - def __eq__(self, other): - """ equal operator for two sources.list entries """ - try: - if self.invalid or other.invalid: - return (self.invalid == other.invalid and - self.line == other.line) - return (self.disabled == other.disabled and - self.type == other.type and - set(self.architectures) == set(other.architectures) and - self.trusted == other.trusted and - self.uri.rstrip('/') == other.uri.rstrip('/') and - self.suite == other.suite and - self.pocket == other.pocket and - set(self.comps) == set(other.comps)) - except AttributeError: - return False - - @property - def suite(self): - """ Return the suite, without pocket - - This always returns the suite in lowercase. - """ - return self.dist.partition('-')[0].lower() - - @suite.setter - def suite(self, new_suite): - if not new_suite: - return - pocket = self._pocket - if pocket: - self.dist = f'{new_suite}-{pocket}' - else: - self.dist = new_suite - - @property - def _pocket(self): - """ Return the pocket, or if unset return None """ - return self.dist.partition('-')[2] - - @property - def pocket(self): - """ Return the pocket, or if unset return 'release' - - This always returns the pocket in lowercase. - """ - return (self._pocket or 'release').lower() - - @pocket.setter - def pocket(self, new_pocket): - if new_pocket: - self.dist = f'{self.suite}-{new_pocket}' - else: - self.dist = self.suite - - def __copy__(self): - """ Copy this SourceEntry """ - return SourceEntry(str(self), file=self.file) - - def _replace(self, **kwargs): - """ Return copy of this SourceEntry with replaced field(s) """ - entry = copy(self) - for (k, v) in kwargs.items(): - setattr(entry, k, v) - return entry - - def __str__(self): - """ debug helper """ - return self.str().strip() - - def str(self): - """ return the current entry as string """ - if self.invalid: - return self.line - line = self.create_line(uri=self.uri, disabled=self.disabled, type=self.type, - suite=self.suite, pocket=self._pocket, comps=self.comps, - architectures=self.architectures, - trusted=self.trusted, comment=self.comment) - return f'{line}\n' - - -class MergedSourceEntry(SourceEntry): - """ A SourceEntry representing one or more identical SourceEntries - - The SourceEntries this represents are identical except for the components - they contain. This will contain all the contained entries' components. - - If all components are removed, this will still act as a normal SourceEntry - without any components, but all corresponding real SourceEntries will be - removed from the SourcesList. - - This may contain multiple SourceEntries that overlap components (e.g. - two entries that both contain 'main' component), however once any - changes are made to the set of comps, duplicates will be removed. - """ - def __init__(self, entry, sourceslist): - self._initialized = False - super(MergedSourceEntry, self).__init__(str(entry), file=entry.file) - self._initialized = True - - self._sourceslist = sourceslist - self._entries = [entry] - - def match(self, other): - """ Check if this is equal to other, ignoring comps """ - if not isinstance(other, SourceEntry): - return False - if self.invalid: - # Never match an invalid entry; those are full-line comments - # and whitespace and should be left as-is - return False - return self._replace(comps=[]) == other._replace(comps=[]) - - def _append(self, entry): - """ Append entry - - This should be called only with an entry that is equal to us, - besides comps. The new entry may contain comps already in - other entries we contain, but any modification of our comps - will remove all duplicate comps. - """ - self._entries.append(entry) - - def get_entry(self, comps, add=False, isolate=False): - """ Get single SourceEntry with comps - - This moves all the components into a single entry in our entries - list, and returns that entry. - - If add is False, this will return None if we do not already - contain all the requested comps; otherwise this adds any - new comps. - - If isolate is False, the entry returned may contain more - components than those requested in comps. If isolate is True, - this moves any excess components into a new entry, located - immediately after the returned entry. - - If any of the components already exist in one of our entries, - it is used to move all the components to; otherwise the first - entry in our list is used. - - Any of our entries that has all its components moved (thus has - no components left) will be removed from our entries list and - removed from our sourceslist. - - If called with no comps this will return our first SourceEntry. - If we contain no SourceEntries (because we contain no components), - this will return None if called with no comps. - """ - comps = set(copy(comps)) - - if not set(self.comps) >= comps and not add: - # we don't contain all requested comps - return None - - for e in self._entries: - if set(e.comps) & comps: - # we want to move requested comps to e - comps -= set(e.comps) - # remove other comps from other entries - self.comps = list(set(self.comps) - comps) - # add them all to this entry - e.comps = list(set(e.comps) | comps) - break - else: - # all comps are new to us (or no comps requested), - # add them to our first entry - self.comps = list(set(self.comps) | comps) - try: - e = self._entries[0] - except KeyError: - # we are empty, return None - return None - - if isolate and comps and set(e.comps) > comps: - newe = e._replace(comps=list(set(e.comps) - comps)) - e.comps = list(comps) - newi = self._sourceslist.list.index(e) + 1 - self._sourceslist.list.insert(newi, newe) - self._append(newe) - - return e - - @property - def comps(self): - c = set() - for e in self._entries: - c |= set(e.comps) - return list(c) - - @comps.setter - def comps(self, comps): - if not self._initialized: - return - - comps = set(copy(comps)) - if comps == set(self.comps): - # no change needed - return - - for e in self._entries: - e.comps = list(set(e.comps) & comps) - comps -= set(e.comps) - - if comps: - if not self._entries: - self._entries = [copy(self)] - self._sourceslist.list.append(self._entries[0]) - self._entries[0].comps = list(set(self._entries[0].comps) | comps) - - for e in list(self._entries): - if not e.comps: - self._entries.remove(e) - self._sourceslist.list.remove(e) - - def set_enabled(self, enabled): - for e in self._entries: - e.set_enabled(enabled) - super(MergedSourceEntry, self).set_enabled(enabled) - - def __setattribute__(self, name, value): - if not (name == 'comps' or name.startswith('_')): - for e in self._entries: - setattr(e, name, value) - super(MergedSourceEntry, self).__setattribute__(name, value) - - -class SourcesList(sourceslist.SourcesList): - """ represents the full sources.list + sources.list.d file """ - - def refresh(self): - """ update the list of known entries """ - self._files = set() - super(SourcesList, self).refresh() - - def filter(self, **kwargs): - """ convenience method to get filtered list """ - l = list(self.list) - for (key, value) in kwargs.items(): - l = [e for e in l if getattr(e, key, not value) == value] - return l - - def __iter__(self): - """ simple iterator to go over self.list, returns SourceEntry - types """ - for entry in self.list: - yield entry - - def __len__(self): - """ calculate len of self.list """ - return len(self.list) - - def __eq__(self, other): - """ equal operator for two sources.list entries """ - return (all([e in other for e in self]) and - all([e in self for e in other])) - - def add(self, type, uri, dist, comps, comment="", pos=-1, file=None, - architectures=[], disabled=False): - """ Create a new entry and add it to our list """ - if pos >= 0 and pos < len(self.list): - before = self.list[pos] - else: - before = None - - line = SourceEntry.create_line(disabled=disabled, type=type, uri=uri, - dist=dist, comps=comps, comment=comment, - architectures=architectures) - new_entry = SourceEntry(line, file=file) - collapsed = CollapsedSourcesList(self) - return collapsed.add_entry(new_entry, before=before) - - def load(self, file): - """ (re)load the current sources """ - try: - with open(file, "r") as f: - for line in f: - source = SourceEntry(line, file) - self.list.append(source) - self._files.add(file) - except Exception: - logging.warning("could not open file '%s'\n" % file) - - def save(self, remove=False): - """ save the current sources - - By default, this will NOT remove any files that we no longer - have entries for; those files will not be modified at all. - - If 'remove' is True, any files that we initially parsed, but - no longer have any entries for, will be removed. - """ - files = set(e.file for e in self.list) - - for filename in files: - with open(filename, "w") as f: - for source in [s for s in self.list if s.file == filename]: - f.write(source.str()) - - if remove: - # remove any files that are now empty - for filename in self._files - files: - try: - os.remove(filename) - except (OSError, IOError): - pass - self._files = files - - # re-create empty sources.list if needed - sourcelist = apt_pkg.config.find_file("Dir::Etc::sourcelist") - if sourcelist not in files: - header = ( - "## See sources.list(5) for more information, especialy\n" - "# Remember that you can only use http, ftp or file URIs\n" - "# CDROMs are managed through the apt-cdrom tool.\n") - - with open(sourcelist, "w") as f: - f.write(header) - - -class CollapsedSourcesList(object): - """ collapsed version of SourcesList - - This provides a 'collapsed' view of a SourcesList. - Each entry in our list is a MergedSourceEntry, representing real - SourceEntry(s) from our backing SourcesList. - - Any changes to our MergedSourceEntries are reflected in our - backing SourcesList, however direct changes to SourceEntries - in our backing SourcesList are not reflected in our list until - after our refresh() method is called. - - If you change any part of any MergedSourceEntry besides the comps, - you must refresh the CollapsedSourcesList to pick up those changes. - - The 'files' kwarg may be used to restrict which lines of the backing - SourcesList are included, by providing a list of filenames. This - can be used, for example, to get a CollapsedSourcesList of only - SourceEntry lines from the main sources.list file. - """ - def __init__(self, sourceslist=None, /, files=None): - self.sourceslist = sourceslist or SourcesList() - self._files = files - self.list = [] - self.refresh() - - def __iter__(self): - """ iterator for self.list - - Returns MergedSourceEntry objects - """ - for entry in self.list: - yield entry - - def __len__(self): - """ calculate len of self.list """ - return len(self.list) - - def __eq__(self, other): - """ equal operator for two sources.list entries """ - return (all([e in other for e in self]) and - all([e in self for e in other])) - - def __contains__(self, other): - """ check if other is contained in this list """ - return self.has_entry(other) - - def filter(self, **kwargs): - """ convenience method to get filtered list """ - l = list(self.list) - for (key, value) in kwargs.items(): - l = [e for e in l if getattr(e, key, not value) == value] - return l - - def refresh(self): - """ update only our list of MergedSourceEntries - - This updates only our list of MergedSourceEntries, our backing - SourcesList is not updated. This should be called anytime - our backing SourcesList is updated directly. - - This does not refresh our backing SourcesList. - """ - self.list = [] - for entry in self.sourceslist: - if self._files and entry.file not in self._files: - continue - for mergedentry in self.list: - if mergedentry.match(entry): - mergedentry._append(entry) - break - else: - self.list.append(MergedSourceEntry(entry, self.sourceslist)) - - def add_entry(self, new_entry, after=None, before=None): - """ Add a new entry to the sources.list. - - This will try to find an existing entry, or an existing entry with - the opposite 'disabled' state, to reuse. - - If an existing entry does not exist, new_entry is inserted. - If either 'after' or 'before' are specified, and match an existing - SourceEntry in our list, then new_entry will be inserted before - or after the specified SourceEntry. If both 'after' and 'before' - are provided, 'after' has precedence. If neither 'after' or 'before' - are provided, new_entry is appended to the end of the list. - """ - # the 'inverse' is just the entry with disabled field toggled - # this is so we can correctly maintain the requested comps - # for both the enabled and disabled entry matches - inverse = new_entry._replace(disabled=not new_entry.disabled) - - # the list of comps is the same for new_entry and inverse - comps = set(new_entry.comps) - - match_entry = None - match_inverse = None - for c in self.list: - if c.match(new_entry): - match_entry = c - if c.match(inverse): - match_inverse = c - if match_entry and match_inverse: - break - - if match_entry: - # at least one existing entry - if match_inverse: - # remove comps from inverse - inverse_comps = set(match_inverse.comps) - comps - match_inverse.comps = list(inverse_comps) - # add comps to existing entry - return match_entry.get_entry(comps, add=True) - - if match_inverse: - # at least one existing inverse entry, and no matching entry; - # replace the inverse entry and toggle its disabled state - new_inverse = match_inverse.get_entry(comps, - add=True, isolate=True) - - # after modifying the entry, we must refresh our merged entries - new_inverse.set_enabled(new_inverse.disabled) - self.refresh() - return self.get_entry(new_entry) - - # no match at all: just append/insert new_entry - if after and after in self.sourceslist.list: - new_index = self.sourceslist.list.index(after) + 1 - self.sourceslist.list.insert(new_index, new_entry) - elif before and before in self.sourceslist.list: - new_index = self.sourceslist.list.index(before) - self.sourceslist.list.insert(new_index, new_entry) - else: - self.sourceslist.list.append(new_entry) - self.list.append(MergedSourceEntry(new_entry, self.sourceslist)) - return new_entry - - def remove_entry(self, entry): - """ Remove the specified entry form the sources.list - - This removes as much as possible of the entry. If the entry - matches an existing entry in our list, but our entry contains - more components, only the specified components will be removed - from our list's entry. Similarly, if entry contains multiple - components, this will remove those components from one or multiple - entries, if needed. - - Any entries in our list that have all their components removed will - be removed from our list. - """ - c = self.get_merged_entry(entry) - if c: - c.comps = list(set(c.comps) - set(entry.comps)) - - def get_entry(self, new_entry): - """ If we already contain new_entry, find and return it - - If new_entry is already contained in our list, with at least - all the components in new_entry, this returns our existing entry. - The returned entry may have more components than new_entry. - - If new_entry is not contained in our list, or we do not - have all the components in new_entry, this returns None. - - This may combine multiple existing SourceEntry lines into a - single SourceEntry line so it contains all the requested - components. - - This returns a SourceEntry. - """ - c = self.get_merged_entry(new_entry) - if c: - return c.get_entry(new_entry.comps) - return None - - def get_merged_entry(self, new_entry): - """ This is similar to get_entry(), but we return the MergedSourceEntry - - This returns the MergedSourceEntry in our list that matches - the new_entry. - - Note that this IGNORES any comps in the new_entry, so the - returned MergedSourceEntry may have more or less comps than - the new_entry. - - If we contain no match for the new_entry, return None. - - This method will never combine SourceEntry lines like the - get_entry method sometimes does. - - This returns a MergedSourceEntry. - """ - for c in self.list: - if c.match(new_entry): - return c - return None - - def has_entry(self, new_entry): - """ Check if we already contain new_entry - - If new_entry contains multiple components, they may be located - in multiple lines; this only checks that all requested components, - for exactly the SourceEntry that equals new_entry (besides comps), - are included in our list. - - This will not change our list. - """ - c = self.get_merged_entry(new_entry) - if c: - return set(c.comps) >= set(new_entry.comps) - return False diff -Nru software-properties-0.99.36/softwareproperties/ppa.py software-properties-0.99.37/softwareproperties/ppa.py --- software-properties-0.99.36/softwareproperties/ppa.py 2023-05-04 08:04:59.000000000 +0000 +++ software-properties-0.99.37/softwareproperties/ppa.py 2023-05-16 08:57:35.000000000 +0000 @@ -30,6 +30,8 @@ from softwareproperties.sourceslist import SourcesListShortcutHandler from softwareproperties.uri import URIShortcutHandler +from aptsources.sourceslist import Deb822SourceEntry + from urllib.parse import urlparse @@ -48,7 +50,7 @@ class PPAShortcutHandler(ShortcutHandler): def __init__(self, shortcut, login=False, **kwargs): - super(PPAShortcutHandler, self).__init__(shortcut, **kwargs) + super(PPAShortcutHandler, self).__init__(shortcut, deb822=True, **kwargs) self._lp_anon = not login self._signing_key_data = None @@ -85,8 +87,14 @@ uri_format = PRIVATE_PPA_URI_FORMAT if self.lpppa.private else PPA_URI_FORMAT uri = uri_format.format(team=self.teamname, ppa=self.ppaname) - line = ('%s %s %s %s' % (self.binary_type, uri, self.dist, ' '.join(comps))) - self._set_source_entry(line) + + entry = Deb822SourceEntry(None, '') + entry.types = [self.binary_type] + entry.uris = [uri] + entry.suites = [self.dist] + entry.comps = comps + + self._set_source_entry(str(entry)) @property def lp(self): diff -Nru software-properties-0.99.36/softwareproperties/shortcuthandler.py software-properties-0.99.37/softwareproperties/shortcuthandler.py --- software-properties-0.99.36/softwareproperties/shortcuthandler.py 2023-05-04 08:04:59.000000000 +0000 +++ software-properties-0.99.37/softwareproperties/shortcuthandler.py 2023-05-16 08:57:35.000000000 +0000 @@ -22,19 +22,18 @@ import tempfile from aptsources.distro import get_distro - -from softwareproperties.extendedsourceslist import (SourceEntry, - SourcesList, - CollapsedSourcesList) +from aptsources.sourceslist import (SourceEntry, SourcesList, Deb822SourceEntry) from contextlib import suppress -from copy import copy - from gettext import gettext as _ from urllib.parse import urlparse +from softwareproperties.sourceutils import (get_source_entry_from_list, + copy_source_entry, + deb822_source_entry_contains) + apt_pkg.init() @@ -53,7 +52,8 @@ should be modified. The only exception to that rule is adding or removing sourceslist lines or components of existing source entries. ''' - def __init__(self, shortcut, components=None, enable_source=False, codename=None, pocket=None, dry_run=False, **kwargs): + def __init__(self, shortcut, components=None, enable_source=False, + codename=None, pocket=None, dry_run=False, deb822=False, **kwargs): self.shortcut = shortcut self.components = components or [] self.enable_source = enable_source @@ -61,6 +61,7 @@ self.codename = codename or self.distro.codename self.pocket = pocket self.dry_run = dry_run + self.deb822 = deb822 # Subclasses should not directly reference _source_entry, # use _set_source_entry() and SourceEntry() @@ -174,18 +175,22 @@ ''' if not self._source_entry: raise NotImplementedError('Implementation class did not set self._source_entry') - e = copy(self._source_entry) + e = copy_source_entry(self._source_entry) if not pkgtype: return e if pkgtype == self.binary_type: e.set_enabled(True) - e.type = self.binary_type elif pkgtype == self.source_type: e.set_enabled(self.enable_source) - e.type = self.source_type else: raise ValueError('Invalid pkgtype: %s' % pkgtype) - return SourceEntry(str(e), file=e.file) + + if isinstance(e, Deb822SourceEntry): + e.types = [pkgtype] + else: + e.type = pkgtype + + return copy_source_entry(e) @property def username(self): @@ -233,6 +238,65 @@ self.remove_source() self.remove_login() + def _add_deb822_source(self): + sourceslist = SourcesList(deb822=True) + + newentry = None + binentry = self.SourceEntry(self.binary_type) + for s in sourceslist: + if not isinstance(s, Deb822SourceEntry): + continue + + if deb822_source_entry_contains(s, binentry): + newentry = s + break + + if newentry is not None: + print(_("Found existing %s entry in %s") % (self.binary_type, + newentry.file)) + else: + newentry = sourceslist.add('', '', '', [], + file=self.sourceparts_file) + newentry.types = binentry.types + newentry.uris = binentry.uris + newentry.suites = binentry.suites + newentry.comps = binentry.comps + + if self.trustedparts_content: + lines = self.trustedparts_content.splitlines() + lines = [' ' + (l if l.strip() else '.') for l in lines] + newentry.section['Signed-By'] = '\n' + '\n'.join(lines) + + binentry = newentry + + if self.enable_source and self.source_type not in set(binentry.types): + newentry = None + srcentry = self.SourceEntry(self.source_type) + + for s in sourceslist: + if not isinstance(s, Deb822SourceEntry): + continue + + if deb822_source_entry_contains(s, srcentry): + newentry = s + break + + if newentry is not None: + print(_("Found existing %s entry in %s") % (self.source_type, + newentry.file)) + else: + binentry.types = [self.binary_type, self.source_type] + + if not self.dry_run: + if not os.path.exists(binentry.file): + # Create the dir if needed + if (binentry.file.startswith(self.sourceparts_path) + and not os.path.exists(self.sourceparts_path)): + os.mkdir(self.sourceparts_path, 0o755) + with open(binentry.file, 'w'): + os.chmod(binentry.file, self.sourceparts_mode) + sourceslist.save() + def add_source(self): '''Add the apt SourceEntries. @@ -248,18 +312,28 @@ SourcesList, the existing entries are updated instead of placing the entries in the sourceparts_file. ''' + if self.deb822: + self._add_deb822_source() + return + binentry = self.SourceEntry(self.binary_type) srcentry = self.SourceEntry(self.source_type) mode = self.sourceparts_mode sourceslist = SourcesList() - collapsedlist = CollapsedSourcesList(sourceslist) - newentry = collapsedlist.get_entry(binentry) - if newentry: + newentry = get_source_entry_from_list(sourceslist, binentry) + if newentry is not None: print(_("Found existing %s entry in %s") % (newentry.type, newentry.file)) else: - newentry = collapsedlist.add_entry(binentry) + newentry = sourceslist.add( + uri=binentry.uri, + dist=binentry.dist, + file=binentry.file, + type=binentry.type, + orig_comps=binentry.comps + ) + newentry.disabled = binentry.disabled if binentry.file != newentry.file: # existing binentry, but not in file we were expecting, just update it @@ -278,11 +352,18 @@ # Unless it already exists somewhere, add the srcentry right after the binentry srcentry.file = binentry.file - newentry = collapsedlist.get_entry(srcentry) - if newentry: + newentry = get_source_entry_from_list(sourceslist, srcentry) + if newentry is not None: print(_("Found existing %s entry in %s") % (newentry.type, newentry.file)) else: - newentry = collapsedlist.add_entry(srcentry, after=binentry) + newentry = sourceslist.add( + uri=srcentry.uri, + dist=srcentry.dist, + file=srcentry.file, + type=srcentry.type, + orig_comps=srcentry.comps + ) + newentry.disabled = srcentry.disabled if srcentry.file != newentry.file: # existing srcentry, but not in file we were expecting, just update it @@ -306,6 +387,28 @@ os.chmod(entryfile, mode) sourceslist.save() + def _remove_deb822_source(self): + sourceslist = SourcesList(deb822=True) + binentry = self.SourceEntry(self.binary_type) + + for s in sourceslist: + if not isinstance(s, Deb822SourceEntry): + continue + + if deb822_source_entry_contains(s, binentry): + print(_("Removing entry from %s") % (s.file)) + binentry = s + sourceslist.remove(binentry) + break + + if not self.dry_run: + sourceslist.save() + if not [s for s in sourceslist if s.file == binentry.file]: + try: + os.remove(binentry.file) + except (OSError, IOError): + pass + def remove_source(self): '''Remove the apt SourceEntries. @@ -321,21 +424,23 @@ empty or contains only invalid and/or disabled SourceEntries, this may remove the sourceparts_file. ''' + if self.deb822: + self._remove_deb822_source() + return + sourceslist = SourcesList() - collapsedlist = CollapsedSourcesList(sourceslist) + orig_files = set([e.file for e in sourceslist]) binentry = self.SourceEntry(self.binary_type) srcentry = self.SourceEntry(self.source_type) - # Disable the entries - binentry.set_enabled(True) - if collapsedlist.has_entry(binentry): - print(_("Disabling %s entry in %s") % (binentry.type, binentry.file)) - collapsedlist.add_entry(binentry._replace(disabled=True)) - srcentry.set_enabled(True) - if collapsedlist.has_entry(srcentry): - print(_("Disabling %s entry in %s") % (srcentry.type, srcentry.file)) - collapsedlist.add_entry(srcentry._replace(disabled=True)) + binentry.disabled = False + srcentry.disabled = False + + for s in sourceslist: + if get_source_entry_from_list([binentry,srcentry], s) is not None: + print(_("Disabling %s entry in %s") % (s.type, s.file)) + s.disabled = True file_entries = [s for s in sourceslist if s.file == self.sourceparts_file] if not [e for e in file_entries if not e.invalid and not e.disabled]: @@ -346,7 +451,12 @@ sourceslist.remove(e) if not self.dry_run: - sourceslist.save(remove=True) + sourceslist.save() + for file in orig_files - set([e.file for e in sourceslist]): + try: + os.remove(file) + except (OSError, IOError): + pass @property def sourceparts_path(self): @@ -358,10 +468,12 @@ '''Get the sources.list.d filename, without the leading path. By default, this combines the filebase with the codename, and uses a - extension of 'list'. This is different than the trustedparts or + extension of 'list' if deb822 is not enabled, and 'sources' if deb822 + is enabled. This is different than the trustedparts or netrcparts filenames, which use only the filebase plus extension. ''' - return self._filebase_to_filename('list', suffix=self.codename) + ext = 'sources' if self.deb822 else 'list' + return self._filebase_to_filename(ext, suffix=self.codename) @property def sourceparts_file(self): @@ -395,6 +507,10 @@ If the file does not yet exist, and self.trustedparts_mode is set, the file will be created with that mode. ''' + if self.deb822: + # The key is embedded into the file when the source is added. + return + if not all((self.trustedparts_file, self.trustedparts_content)): return @@ -610,9 +726,13 @@ The self.components, if any, will be added to the line's component(s). ''' - e = SourceEntry(line) + if self.deb822: + e = Deb822SourceEntry(line, file=self.sourceparts_file) + else: + e = SourceEntry(line, file=self.sourceparts_file) + e.comps = list(set(e.comps) | set(self.components)) - self._source_entry = SourceEntry(str(e), file=self.sourceparts_file) + self._source_entry = copy_source_entry(e) def _encode_filebase(self, suffix=None): base = self._filebase diff -Nru software-properties-0.99.36/softwareproperties/sourceslist.py software-properties-0.99.37/softwareproperties/sourceslist.py --- software-properties-0.99.36/softwareproperties/sourceslist.py 2023-05-04 08:04:59.000000000 +0000 +++ software-properties-0.99.37/softwareproperties/sourceslist.py 2023-05-16 08:57:35.000000000 +0000 @@ -17,7 +17,7 @@ from gettext import gettext as _ -from softwareproperties.extendedsourceslist import SourceEntry +from aptsources.sourceslist import SourceEntry from softwareproperties.shortcuthandler import (ShortcutHandler, InvalidShortcutException) diff -Nru software-properties-0.99.36/softwareproperties/sourceutils.py software-properties-0.99.37/softwareproperties/sourceutils.py --- software-properties-0.99.36/softwareproperties/sourceutils.py 1970-01-01 00:00:00.000000000 +0000 +++ software-properties-0.99.37/softwareproperties/sourceutils.py 2023-05-16 08:57:35.000000000 +0000 @@ -0,0 +1,88 @@ +from aptsources.sourceslist import (SourceEntry, Deb822SourceEntry) + +def copy_source_entry(orig): + """ Return a shallow copy of the source entry. """ + if isinstance(orig, Deb822SourceEntry): + return Deb822SourceEntry(str(orig), file=orig.file) + + return SourceEntry(str(orig), file=orig.file) + +def replace_source_entry(orig, **kwargs): + """Return a copy of the given source entry with replaced field(s).""" + entry = copy_source_entry(orig) + for (k, v) in kwargs.items(): + setattr(entry, k, v) + return entry + +def get_source_entry_pocket(source_entry): + """ + Return the pocket, or if unset return 'release'. + + This always returns the pocket in lowercase. + """ + parts = source_entry.dist.partition('-') + return (parts[2] or 'release').lower() + +def get_source_entry_suite(source_entry): + """ + Return the suite, without pocket. + + This always returns the suite in lowercase. + """ + return source_entry.dist.partition('-')[0] + +def get_source_entry_from_list(entries, entry): + """ + Return the source entry from entries that matches entry, if found. + Otherwise return None. + + This function uses a modified equality check, i.e. it considers components + as a set rather than a list so that different ordering does not affect the + comparison. + """ + target = replace_source_entry(entry, comps=set(entry.comps)) + + for e in entries: + if replace_source_entry(e, comps=set(e.comps)) == target: + return e + + return None + +def deb822_source_entry_contains(a, b): + """ + Return True if the source defined by b is already satisfied + by the source defined by a. + + For example, if source a is: + + Types: deb + URIs: http://archive.ubuntu.com/ubuntu + Suites: jammy + Components: main universe + + and source b is: + + Types: deb + URIs: http://archive.ubuntu.com/ubuntu + Suites: jammy + Components: universe + + Then source a contains source b. + + But if source b was: + + Types: deb-src + URIs: http://archive.ubuntu.com/ubuntu + Suites: jammy + Components: universe + + Then a does not contain b because it does not include deb-src. + """ + if a.disabled != b.disabled: + return False + + for attr in ['types', 'comps', 'suites', 'uris']: + if set(getattr(b, attr)) - set(getattr(a, attr)): + return False + + return True diff -Nru software-properties-0.99.36/softwareproperties/uri.py software-properties-0.99.37/softwareproperties/uri.py --- software-properties-0.99.36/softwareproperties/uri.py 2023-05-04 08:04:59.000000000 +0000 +++ software-properties-0.99.37/softwareproperties/uri.py 2023-05-16 08:57:35.000000000 +0000 @@ -15,7 +15,9 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 # USA -from softwareproperties.extendedsourceslist import SourceEntry +from aptsources.distro import get_distro +from aptsources.sourceslist import SourceEntry + from softwareproperties.sourceslist import SourcesListShortcutHandler @@ -35,9 +37,16 @@ suite = kwargs.get('codename') pocket = kwargs.get('pocket') - line = SourceEntry.create_line(uri, suite=suite, pocket=pocket) - super(URIShortcutHandler, self).__init__(line, **kwargs) + s = SourceEntry('') + s.invalid = False + s.uri = uri + s.type = get_distro().binary_type + s.dist = suite or get_distro().codename + if pocket is not None: + s.dist = '{}-{}'.format(s.dist, pocket) + + super(URIShortcutHandler, self).__init__(str(s), **kwargs) # vi: ts=4 expandtab diff -Nru software-properties-0.99.36/tests/test_add_apt_repository.py software-properties-0.99.37/tests/test_add_apt_repository.py --- software-properties-0.99.36/tests/test_add_apt_repository.py 2023-05-04 08:04:59.000000000 +0000 +++ software-properties-0.99.37/tests/test_add_apt_repository.py 2023-05-16 08:57:35.000000000 +0000 @@ -8,6 +8,7 @@ import unittest from aptsources.distro import get_distro +from aptsources.sourceslist import (SourceEntry, Deb822SourceEntry) from copy import copy from pathlib import Path from types import SimpleNamespace @@ -15,7 +16,7 @@ from urllib.error import (URLError, HTTPError) from softwareproperties.cloudarchive import RELEASE_MAP -from softwareproperties.extendedsourceslist import SourceEntry +from softwareproperties.sourceutils import replace_source_entry CODENAME = get_distro().codename @@ -46,8 +47,7 @@ PPA_NAME = 'ubuntu-support-team/software-properties-autopkgtest' PPA_URI = f'https://ppa.launchpadcontent.net/{PPA_NAME}/ubuntu/' -PPA_FILENAME = f'ubuntu-support-team-ubuntu-software-properties-autopkgtest-{CODENAME}.list' -PPA_TRUSTED_FILENAME = 'ubuntu-support-team-ubuntu-software-properties-autopkgtest.gpg' +PPA_FILENAME = f'ubuntu-support-team-ubuntu-software-properties-autopkgtest-{CODENAME}.sources' PPA_TRUSTED_FINGERPRINT = 'A17A D76F CBB7 A7D5 73C4 DF43 1E03 2FCE 2F88 6048' ADD_APT_REPOSITORY_LONG_PARAMS = SimpleNamespace( @@ -92,6 +92,20 @@ components = ' '.join(components or MAIN) return f'{deb} {uri} {CODENAME}{pocket} {components}' +def deb822_lines(uri=None, binary=True, source=False, pocket=None, components=None): + uris = uri or FAKE_URI + + suites = f'{CODENAME}' + (f'-{pocket}' if pocket else '') + + types = [TYPE_BINARY] if binary else [] + types += [TYPE_SOURCE] if source else [] + types = ' '.join(types) + + components = components or MAIN + components = ' '.join(components) + + return [f'Types: {types}',f'URIs: {uris}', f'Suites: {suites}', f'Components: {components}'] + @unittest.skipIf(ADD_APT_REPOSITORY is None, 'Could not find add-apt-repository script') class TestAddAptRepository(unittest.TestCase): @@ -220,9 +234,11 @@ def check_sourceentry(self, expected_line, content_lines): expected_entry = SourceEntry(expected_line) + expected_entry.comps = set(expected_entry.comps) for (index, line) in enumerate(content_lines): entry = SourceEntry(line) + entry.comps = set(entry.comps) if entry == expected_entry: return content_lines[:index] + content_lines[index+1:] @@ -240,6 +256,33 @@ unexpected_lines = '\n'.join(lines) self.fail(f"Unexpected lines:\n{unexpected_lines}") + def check_expected_deb822_lines(self, expected_lines, lines): + def filter_lines(lines): + in_ignored_field = False + for line in lines: + if line.startswith('Signed-By:'): + in_ignored_field = True + continue + + if in_ignored_field and line.startswith(' '): + continue + + in_ignored_field = False + yield line + + # Ignore lines that should be there, but we are not checking for, e.g. + # Signed-By. + lines = filter_lines(lines) + missing_lines = list(set(expected_lines) - set(lines)) + unexpected_lines = list(set(lines) - set(expected_lines)) + + + if missing_lines: + self.fail(f'Missing lines: {missing_lines} in: {lines}') + + if unexpected_lines: + self.fail(f'Unexpected lines: {unexpected_lines} in: {lines}') + def apt_get_output_lines(self): try: return self.apt_get_output.read_text().splitlines() @@ -260,13 +303,27 @@ if not self.testing_ppa: return - self.assertIsNotNone(self.trustedfile) + with open(self.sourcesfile, mode='r', encoding='utf-8') as f: + entry = Deb822SourceEntry(f.read(), '') - with tempfile.TemporaryDirectory() as homedir: - Path(homedir).chmod(0o700) # so gpg doesn't complain to stderr - cmd = f'gpg -q --homedir {homedir} --no-default-keyring --keyring {self.trustedfile} --fingerprint' - result = subprocess.run(cmd.split(), stdout=subprocess.PIPE, encoding='UTF-8') - self.assertIn(PPA_TRUSTED_FINGERPRINT, result.stdout) + keydata = entry.section.get('Signed-By') + keydata = keydata.splitlines() + keydata = [l.lstrip() for l in keydata] + keydata = [l if l != '.' else '' for l in keydata] + keydata = '\n'.join(keydata) + + with tempfile.NamedTemporaryFile(mode='w') as f: + f.write(keydata) + f.flush() + + with tempfile.TemporaryDirectory() as homedir: + Path(homedir).chmod(0o700) # so gpg doesn't complain to stderr + cmd = f'gpg -q --homedir {homedir} --import {f.name}' + subprocess.check_call(cmd.split()) + keyring = os.path.join(homedir, 'pubring.kbx') + cmd = f'gpg -q --homedir {homedir} --no-default-keyring --keyring {keyring} --fingerprint' + result = subprocess.run(cmd.split(), stdout=subprocess.PIPE, encoding='UTF-8') + self.assertIn(PPA_TRUSTED_FINGERPRINT, result.stdout) def check_file(self, testfile, exists=True): if not testfile: @@ -283,7 +340,10 @@ self.check_file(self.sourcesfile) self.check_file(self.trustedfile) - self.check_expected_lines(expected_lines, self.sourcesfile.read_text().splitlines()) + if self.testing_ppa: + self.check_expected_deb822_lines(expected_lines, self.sourcesfile.read_text().splitlines()) + else: + self.check_expected_lines(expected_lines, self.sourcesfile.read_text().splitlines()) self.check_apt_get_update() self.check_ppa_gpgkey() @@ -297,10 +357,13 @@ self.check_file(self.trustedfile) def run_sourceslistd_test(self, args, expected_line): - source_entry = SourceEntry(expected_line)._replace(type=TYPE_SOURCE) - source_entry.set_enabled(self.enable_source > 0) + if self.testing_ppa: + expected_lines = deb822_lines(uri=PPA_URI) + else: + source_entry = replace_source_entry(SourceEntry(expected_line), type=TYPE_SOURCE) + source_entry.set_enabled(self.enable_source > 0) - expected_lines = [expected_line, str(source_entry)] + expected_lines = [expected_line, str(source_entry)] test_args = self.parameter_all test_args += args self.run_sourceslistd_add_remove_test(test_args, expected_lines) @@ -329,7 +392,7 @@ self.testing_ppa = True self.sourcesfile = PPA_FILENAME - self.trustedfile = PPA_TRUSTED_FILENAME + self.trustedfile = None args = [] if with_param: args += [self.params.ppa] @@ -337,7 +400,7 @@ args += [f'ppa:{PPA_NAME}'] else: args += [PPA_NAME] - self.run_sourceslistd_test(args, self.line(uri=PPA_URI)) + self.run_sourceslistd_test(args, None) def test_ppa_noupdate(self): self.noupdate = True @@ -584,9 +647,9 @@ self._test_pocket() def add_lines_es(self, group, pocket, components, /, binary=True, disabled=False, alone=False, plus_comps=[], minus_comps=False): - binary_entry = group.entry(pocket=pocket, components=components)._replace(disabled=disabled) + binary_entry = replace_source_entry(group.entry(pocket=pocket, components=components), disabled=disabled) enabled_source_entry = group.entry(binary=False, pocket=pocket, components=components) - disabled_source_entry = enabled_source_entry._replace(disabled=True) + disabled_source_entry = replace_source_entry(enabled_source_entry, disabled=True) if binary: binary_line = str(binary_entry) @@ -609,8 +672,8 @@ if plus_comps and self.enable_source > 1: # include comps from other binary-only lines comps = set(enabled_source_entry.comps) | set(plus_comps) - enabled_line = str(enabled_source_entry._replace(comps=comps)) - disabled_line = str(disabled_source_entry._replace(comps=comps)) + enabled_line = str(replace_source_entry(enabled_source_entry, comps=comps)) + disabled_line = str(replace_source_entry(disabled_source_entry, comps=comps)) if disabled and alone: # with source, 'alone' indicates no matching (enabled) binary line exists, # so the source line will *not* be enabled diff -Nru software-properties-0.99.36/tests/test_shortcuts.py software-properties-0.99.37/tests/test_shortcuts.py --- software-properties-0.99.36/tests/test_shortcuts.py 2023-05-04 08:04:59.000000000 +0000 +++ software-properties-0.99.37/tests/test_shortcuts.py 2023-05-16 08:57:35.000000000 +0000 @@ -7,7 +7,7 @@ import os from aptsources.distro import get_distro -from softwareproperties.extendedsourceslist import SourceEntry +from aptsources.sourceslist import (SourceEntry, Deb822SourceEntry) from contextlib import contextmanager from http.client import HTTPException from launchpadlib.launchpad import Launchpad @@ -29,14 +29,21 @@ CODENAME = DISTRO.codename # These must match the ppa used in the VALID_PPAS -PPA_LINE = f"deb https://ppa.launchpadcontent.net/ddstreet/ppa/ubuntu/ {CODENAME} main" +PPA_LINE = f"""Types: deb +URIs: https://ppa.launchpadcontent.net/ddstreet/ppa/ubuntu/ +Suites: {CODENAME} +Components: main +""" PPA_FILEBASE = "ddstreet-ubuntu-ppa" -PPA_SOURCEFILE = f"{PPA_FILEBASE}-{CODENAME}.list" -PPA_TRUSTEDFILE = f"{PPA_FILEBASE}.gpg" +PPA_SOURCEFILE = f"{PPA_FILEBASE}-{CODENAME}.sources" PPA_NETRCFILE = f"{PPA_FILEBASE}.conf" PRIVATE_PPA_PASSWORD = "thisisnotarealpassword" -PRIVATE_PPA_LINE = f"deb https://private-ppa.launchpadcontent.net/ddstreet/ppa/ubuntu/ {CODENAME} main" +PRIVATE_PPA_LINE = f"""Types: deb +URIs: https://private-ppa.launchpadcontent.net/ddstreet/ppa/ubuntu/ +Suites: {CODENAME} +Components: main +""" PRIVATE_PPA_NETRCCONTENT = f"machine private-ppa.launchpadcontent.net/ddstreet/ppa/ubuntu/ login ddstreet password {PRIVATE_PPA_PASSWORD}" PRIVATE_PPA_SUBSCRIPTION_URLS = [f"https://ddstreet:{PRIVATE_PPA_PASSWORD}@private-ppa.launchpadcontent.net/ddstreet/ppa/ubuntu/"] @@ -138,12 +145,22 @@ self.assertEqual(shortcut.sourceparts_filename, sourcefile) self.assertEqual(shortcut.sourceparts_file, os.path.join(sourceparts, sourcefile)) - binentry = SourceEntry(line) - binentry.type = DISTRO.binary_type + if shortcut.deb822: + binentry = Deb822SourceEntry(line, '') + binentry.types = [DISTRO.binary_type] + else: + binentry = SourceEntry(line) + binentry.type = DISTRO.binary_type + self.assertEqual(shortcut.SourceEntry(shortcut.binary_type), binentry) - srcentry = SourceEntry(line) - srcentry.type = DISTRO.source_type + if shortcut.deb822: + srcentry = Deb822SourceEntry(line, '') + srcentry.types = [DISTRO.source_type] + else: + srcentry = SourceEntry(line) + srcentry.type = DISTRO.source_type + srcentry.set_enabled(self.enable_source) self.assertEqual(shortcut.SourceEntry(shortcut.source_type), srcentry) @@ -182,7 +199,6 @@ for shortcut in self.create_handlers(ppa, PPAShortcutHandler): self.check_shortcut(shortcut, PPA_LINE, sourcefile=PPA_SOURCEFILE, - trustedfile=PPA_TRUSTEDFILE, netrcfile=PPA_NETRCFILE, trustedcontent=True) @@ -195,7 +211,6 @@ for shortcut in self.create_handlers(ppa, PPAShortcutHandler, login=True): self.check_shortcut(shortcut, PRIVATE_PPA_LINE, sourcefile=PPA_SOURCEFILE, - trustedfile=PPA_TRUSTEDFILE, netrcfile=PPA_NETRCFILE, trustedcontent=True, netrccontent=PRIVATE_PPA_NETRCCONTENT)