launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28446
[Merge] ~cjwatson/launchpad:dominator-channel-map into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:dominator-channel-map into launchpad:master.
Commit message:
Support dominating publications by channel
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/422233
This means that, once we start publishing binaries to snap-store-style semantic channels (e.g. "stable", "1.0/candidate", etc.), the dominator will be able to correctly supersede publications that have been replaced with a different version of the same package in the same channel.
I had to prepare for this by adding channel support to various publishing primitives. There's no UI or API support for any of this yet, but I at least needed enough to support the dominator and its tests.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:dominator-channel-map into launchpad:master.
diff --git a/lib/lp/archivepublisher/domination.py b/lib/lp/archivepublisher/domination.py
index 5cc9422..7160a28 100644
--- a/lib/lp/archivepublisher/domination.py
+++ b/lib/lp/archivepublisher/domination.py
@@ -64,6 +64,7 @@ from storm.expr import (
And,
Count,
Desc,
+ Not,
Select,
)
from zope.component import getUtility
@@ -74,6 +75,7 @@ from lp.services.database.constants import UTC_NOW
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.interfaces import IStore
from lp.services.database.sqlbase import flush_database_updates
+from lp.services.database.stormexpr import IsDistinctFrom
from lp.services.orderingcheck import OrderingCheck
from lp.soyuz.enums import (
BinaryPackageFormat,
@@ -204,6 +206,31 @@ class GeneralizedPublication:
return sorted(publications, key=cmp_to_key(self.compare), reverse=True)
+class PublicationLocation:
+ """A representation of a publication's location.
+
+ This currently consists of just its name and channel.
+ """
+ def __init__(self, pub, generalization):
+ self.name = generalization.getPackageName(pub)
+ self.channel = pub.channel
+
+ def __str__(self):
+ s = self.name
+ if self.channel is not None:
+ s += " (%s)" % self.channel
+ return s
+
+ def __eq__(self, other):
+ return self.name == other.name and self.channel == other.channel
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __hash__(self):
+ return hash((self.name, self.channel))
+
+
def find_live_source_versions(sorted_pubs):
"""Find versions out of Published publications that should stay live.
@@ -336,10 +363,19 @@ def find_live_binary_versions_pass_2(sorted_pubs, cache):
[pub.binarypackagerelease for pub in arch_indep_pubs], ['buildID'])
load_related(SourcePackageRelease, bpbs, ['source_package_release_id'])
+ # XXX cjwatson 2022-05-01: Skip the architecture-specific check for
+ # publications from CI builds for now, until we figure out how to
+ # approximate source package releases for groups of CI builds. We don't
+ # currently expect problematic situations to come up on production; CI
+ # builds are currently only expected to be used in situations where
+ # either we don't build both architecture-specific and
+ # architecture-independent packages, or where tight dependencies between
+ # the two aren't customary.
reprieved_pubs = [
pub
for pub in arch_indep_pubs
- if cache.hasArchSpecificPublications(pub)]
+ if pub.binarypackagerelease.ci_build_id is None and
+ cache.hasArchSpecificPublications(pub)]
return get_binary_versions([latest] + arch_specific_pubs + reprieved_pubs)
@@ -382,9 +418,9 @@ class Dominator:
list we import.
:param sorted_pubs: A list of publications for the same package,
- in the same archive, series, and pocket, all with status
- `PackagePublishingStatus.PUBLISHED`. They must be sorted from
- most current to least current, as would be the result of
+ in the same archive, series, pocket, and channel, all with
+ status `PackagePublishingStatus.PUBLISHED`. They must be sorted
+ from most current to least current, as would be the result of
`generalization.sortPublications`.
:param live_versions: Iterable of versions that are still considered
"live" for this package. For any of these, the latest publication
@@ -458,31 +494,33 @@ class Dominator:
return supersede, keep, delete
def _sortPackages(self, publications, generalization):
- """Partition publications by package name, and sort them.
+ """Partition publications by package location, and sort them.
The publications are sorted from most current to least current,
- as required by `planPackageDomination` etc.
+ as required by `planPackageDomination` etc. Locations are currently
+ (package name, channel).
:param publications: An iterable of `SourcePackagePublishingHistory`
or of `BinaryPackagePublishingHistory`.
:param generalization: A `GeneralizedPublication` helper representing
the kind of publications these are: source or binary.
- :return: A dict mapping each package name to a sorted list of
- publications from `publications`.
+ :return: A dict mapping each package location (package name,
+ channel) to a sorted list of publications from `publications`.
"""
- pubs_by_package = defaultdict(list)
+ pubs_by_location = defaultdict(list)
for pub in publications:
- pubs_by_package[generalization.getPackageName(pub)].append(pub)
+ location = PublicationLocation(pub, generalization)
+ pubs_by_location[location].append(pub)
# Sort the publication lists. This is not an in-place sort, so
# it involves altering the dict while we iterate it. Listify
# the keys so that we can be sure that we're not altering the
# iteration order while iteration is underway.
- for package in list(pubs_by_package.keys()):
- pubs_by_package[package] = generalization.sortPublications(
- pubs_by_package[package])
+ for location in list(pubs_by_location):
+ pubs_by_location[location] = generalization.sortPublications(
+ pubs_by_location[location])
- return pubs_by_package
+ return pubs_by_location
def _setScheduledDeletionDate(self, pub_record):
"""Set the scheduleddeletiondate on a publishing record.
@@ -541,7 +579,10 @@ class Dominator:
BinaryPackagePublishingHistory.binarypackagerelease ==
BinaryPackageRelease.id,
BinaryPackageRelease.build == BinaryPackageBuild.id,
- BinaryPackagePublishingHistory.pocket == pub_record.pocket)
+ BinaryPackagePublishingHistory.pocket == pub_record.pocket,
+ Not(IsDistinctFrom(
+ BinaryPackagePublishingHistory._channel,
+ pub_record._channel)))
# There is at least one non-removed binary to consider
if not considered_binaries.is_empty():
@@ -552,6 +593,7 @@ class Dominator:
SourcePackagePublishingHistory,
distroseries=pub_record.distroseries,
pocket=pub_record.pocket,
+ channel=pub_record.channel,
status=PackagePublishingStatus.PUBLISHED,
archive=self.archive,
sourcepackagerelease=srcpkg_release)
@@ -588,7 +630,8 @@ class Dominator:
]
candidate_binary_names = Select(
BPPH.binarypackagenameID, And(*bpph_location_clauses),
- group_by=BPPH.binarypackagenameID, having=(Count() > 1))
+ group_by=(BPPH.binarypackagenameID, BPPH._channel),
+ having=(Count() > 1))
main_clauses = bpph_location_clauses + [
BPR.id == BPPH.binarypackagereleaseID,
BPR.binarypackagenameID.is_in(candidate_binary_names),
@@ -664,13 +707,13 @@ class Dominator:
bins = self.findBinariesForDomination(distroarchseries, pocket)
sorted_packages = self._sortPackages(bins, generalization)
self.logger.info("Planning domination of binaries...")
- for name, pubs in sorted_packages.items():
- self.logger.debug("Planning domination of %s" % name)
+ for location, pubs in sorted_packages.items():
+ self.logger.debug("Planning domination of %s" % location)
assert len(pubs) > 0, "Dominating zero binaries!"
live_versions = find_live_binary_versions_pass_1(pubs)
plan(pubs, live_versions)
if contains_arch_indep(pubs):
- packages_w_arch_indep.add(name)
+ packages_w_arch_indep.add(location)
execute_plan()
@@ -692,9 +735,10 @@ class Dominator:
bins = self.findBinariesForDomination(distroarchseries, pocket)
sorted_packages = self._sortPackages(bins, generalization)
self.logger.info("Planning domination of binaries...(2nd pass)")
- for name in packages_w_arch_indep.intersection(sorted_packages):
- pubs = sorted_packages[name]
- self.logger.debug("Planning domination of %s" % name)
+ for location in packages_w_arch_indep.intersection(
+ sorted_packages):
+ pubs = sorted_packages[location]
+ self.logger.debug("Planning domination of %s" % location)
assert len(pubs) > 0, "Dominating zero binaries in 2nd pass!"
live_versions = find_live_binary_versions_pass_2(
pubs, reprieve_cache)
@@ -732,7 +776,7 @@ class Dominator:
candidate_source_names = Select(
SPPH.sourcepackagenameID,
And(join_spph_spr(), spph_location_clauses),
- group_by=SPPH.sourcepackagenameID,
+ group_by=(SPPH.sourcepackagenameID, SPPH._channel),
having=(Count() > 1))
# We'll also access the SourcePackageReleases associated with
@@ -769,8 +813,8 @@ class Dominator:
delete = []
self.logger.debug("Dominating sources...")
- for name, pubs in sorted_packages.items():
- self.logger.debug("Dominating %s" % name)
+ for location, pubs in sorted_packages.items():
+ self.logger.debug("Dominating %s" % location)
assert len(pubs) > 0, "Dominating zero sources!"
live_versions = find_live_source_versions(pubs)
cur_supersede, _, cur_delete = self.planPackageDomination(
diff --git a/lib/lp/archivepublisher/tests/test_dominator.py b/lib/lp/archivepublisher/tests/test_dominator.py
old mode 100755
new mode 100644
index 01c56e4..98917b1
--- a/lib/lp/archivepublisher/tests/test_dominator.py
+++ b/lib/lp/archivepublisher/tests/test_dominator.py
@@ -30,7 +30,10 @@ from lp.archivepublisher.publishing import Publisher
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.registry.interfaces.series import SeriesStatus
from lp.services.log.logger import DevNullLogger
-from lp.soyuz.enums import PackagePublishingStatus
+from lp.soyuz.enums import (
+ BinaryPackageFormat,
+ PackagePublishingStatus,
+ )
from lp.soyuz.interfaces.publishing import (
IPublishingSet,
ISourcePackagePublishingHistory,
@@ -398,6 +401,51 @@ class TestDominator(TestNativePublishingBase):
for pub in overrides_2:
self.assertEqual(PackagePublishingStatus.PUBLISHED, pub.status)
+ def test_dominate_by_channel(self):
+ # Publications only dominate other publications in the same channel.
+ # (Currently only tested for binary publications.)
+ with lp_dbuser():
+ archive = self.factory.makeArchive()
+ distroseries = self.factory.makeDistroSeries(
+ distribution=archive.distribution)
+ das = self.factory.makeDistroArchSeries(distroseries=distroseries)
+ repository = self.factory.makeGitRepository(
+ target=self.factory.makeDistributionSourcePackage(
+ distribution=archive.distribution))
+ ci_builds = [
+ self.factory.makeCIBuild(
+ git_repository=repository, distro_arch_series=das)
+ for _ in range(3)]
+ bpn = self.factory.makeBinaryPackageName()
+ bprs = [
+ self.factory.makeBinaryPackageRelease(
+ binarypackagename=bpn, version=version, ci_build=ci_build,
+ binpackageformat=BinaryPackageFormat.WHL)
+ for version, ci_build in zip(("1.0", "1.1", "1.2"), ci_builds)]
+ stable_bpphs = [
+ self.factory.makeBinaryPackagePublishingHistory(
+ binarypackagerelease=bpr, archive=archive,
+ distroarchseries=das, status=PackagePublishingStatus.PUBLISHED,
+ pocket=PackagePublishingPocket.RELEASE, channel="stable")
+ for bpr in bprs[:2]]
+ candidate_bpph = self.factory.makeBinaryPackagePublishingHistory(
+ binarypackagerelease=bprs[2], archive=archive,
+ distroarchseries=das, status=PackagePublishingStatus.PUBLISHED,
+ pocket=PackagePublishingPocket.RELEASE, channel="candidate")
+
+ dominator = Dominator(self.logger, archive)
+ dominator.judgeAndDominate(
+ distroseries, PackagePublishingPocket.RELEASE)
+
+ # The older of the two stable publications is superseded, while the
+ # current stable publication and the candidate publication are left
+ # alone.
+ self.checkPublication(
+ stable_bpphs[0], PackagePublishingStatus.SUPERSEDED)
+ self.checkPublications(
+ (stable_bpphs[1], candidate_bpph),
+ PackagePublishingStatus.PUBLISHED)
+
class TestDomination(TestNativePublishingBase):
"""Test overall domination procedure."""
@@ -1315,7 +1363,7 @@ class TestArchSpecificPublicationsCache(TestCaseWithFactory):
return removeSecurityProxy(self.factory.makeSourcePackageRelease())
def makeBPPH(self, spr=None, arch_specific=True, archive=None,
- distroseries=None):
+ distroseries=None, binpackageformat=None, channel=None):
"""Create a `BinaryPackagePublishingHistory`."""
if spr is None:
spr = self.makeSPR()
@@ -1323,12 +1371,13 @@ class TestArchSpecificPublicationsCache(TestCaseWithFactory):
bpb = self.factory.makeBinaryPackageBuild(
source_package_release=spr, distroarchseries=das)
bpr = self.factory.makeBinaryPackageRelease(
- build=bpb, architecturespecific=arch_specific)
+ build=bpb, binpackageformat=binpackageformat,
+ architecturespecific=arch_specific)
return removeSecurityProxy(
self.factory.makeBinaryPackagePublishingHistory(
binarypackagerelease=bpr, archive=archive,
distroarchseries=das, pocket=PackagePublishingPocket.UPDATES,
- status=PackagePublishingStatus.PUBLISHED))
+ status=PackagePublishingStatus.PUBLISHED, channel=channel))
def test_getKey_is_consistent_and_distinguishing(self):
# getKey consistently returns the same key for the same BPPH,
@@ -1351,14 +1400,19 @@ class TestArchSpecificPublicationsCache(TestCaseWithFactory):
spr, arch_specific=False, archive=dependent.archive,
distroseries=dependent.distroseries)
bpph2 = self.makeBPPH(arch_specific=False)
+ bpph3 = self.makeBPPH(
+ arch_specific=False, binpackageformat=BinaryPackageFormat.WHL,
+ channel="edge")
cache = self.makeCache()
self.assertEqual(
- [True, True, False, False],
+ [True, True, False, False, False, False],
[
cache.hasArchSpecificPublications(bpph1),
cache.hasArchSpecificPublications(bpph1),
cache.hasArchSpecificPublications(bpph2),
cache.hasArchSpecificPublications(bpph2),
+ cache.hasArchSpecificPublications(bpph3),
+ cache.hasArchSpecificPublications(bpph3),
])
def test_hasArchSpecificPublications_caches_results(self):
diff --git a/lib/lp/code/interfaces/cibuild.py b/lib/lp/code/interfaces/cibuild.py
index ed9dedf..0827108 100644
--- a/lib/lp/code/interfaces/cibuild.py
+++ b/lib/lp/code/interfaces/cibuild.py
@@ -163,6 +163,14 @@ class ICIBuildView(IPackageBuildView, IPrivacy):
:return: The corresponding `ILibraryFileAlias`.
"""
+ def createBinaryPackageRelease(
+ binarypackagename, version, summary, description, binpackageformat,
+ architecturespecific, installedsize=None, homepage=None):
+ """Create and return a `BinaryPackageRelease` for this CI build.
+
+ The new binary package release will be linked to this build.
+ """
+
class ICIBuildEdit(IBuildFarmJobEdit):
"""`ICIBuild` methods that require launchpad.Edit."""
diff --git a/lib/lp/code/model/cibuild.py b/lib/lp/code/model/cibuild.py
index f2b44a0..24e7e7f 100644
--- a/lib/lp/code/model/cibuild.py
+++ b/lib/lp/code/model/cibuild.py
@@ -81,6 +81,7 @@ from lp.services.macaroons.interfaces import (
)
from lp.services.macaroons.model import MacaroonIssuerBase
from lp.services.propertycache import cachedproperty
+from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
from lp.soyuz.model.distroarchseries import DistroArchSeries
@@ -452,6 +453,18 @@ class CIBuild(PackageBuildMixin, StormBase):
"""See `IPackageBuild`."""
# We don't currently send any notifications.
+ def createBinaryPackageRelease(
+ self, binarypackagename, version, summary, description,
+ binpackageformat, architecturespecific, installedsize=None,
+ homepage=None):
+ """See `ICIBuild`."""
+ return BinaryPackageRelease(
+ ci_build=self, binarypackagename=binarypackagename,
+ version=version, summary=summary, description=description,
+ binpackageformat=binpackageformat,
+ architecturespecific=architecturespecific,
+ installedsize=installedsize, homepage=homepage)
+
@implementer(ICIBuildSet)
class CIBuildSet(SpecificBuildFarmJobSourceMixin):
diff --git a/lib/lp/code/model/tests/test_cibuild.py b/lib/lp/code/model/tests/test_cibuild.py
index 80558cf..c06038f 100644
--- a/lib/lp/code/model/tests/test_cibuild.py
+++ b/lib/lp/code/model/tests/test_cibuild.py
@@ -17,6 +17,7 @@ import pytz
from storm.locals import Store
from testtools.matchers import (
Equals,
+ Is,
MatchesListwise,
MatchesSetwise,
MatchesStructure,
@@ -65,6 +66,7 @@ from lp.services.log.logger import BufferLogger
from lp.services.macaroons.interfaces import IMacaroonIssuer
from lp.services.macaroons.testing import MacaroonTestMixin
from lp.services.propertycache import clear_property_cache
+from lp.soyuz.enums import BinaryPackageFormat
from lp.testing import (
person_logged_in,
StormStatementRecorder,
@@ -386,6 +388,24 @@ class TestCIBuild(TestCaseWithFactory):
commit_sha1=build.commit_sha1,
ci_build=build))
+ def test_createBinaryPackageRelease(self):
+ build = self.factory.makeCIBuild()
+ bpn = self.factory.makeBinaryPackageName()
+ bpr = build.createBinaryPackageRelease(
+ bpn, "1.0", "test summary", "test description",
+ BinaryPackageFormat.WHL, False, installedsize=1024,
+ homepage="https://example.com/")
+ self.assertThat(bpr, MatchesStructure(
+ binarypackagename=Equals(bpn),
+ version=Equals("1.0"),
+ summary=Equals("test summary"),
+ description=Equals("test description"),
+ binpackageformat=Equals(BinaryPackageFormat.WHL),
+ architecturespecific=Is(False),
+ installedsize=Equals(1024),
+ homepage=Equals("https://example.com/"),
+ ))
+
class TestCIBuildSet(TestCaseWithFactory):
diff --git a/lib/lp/soyuz/enums.py b/lib/lp/soyuz/enums.py
index d60ea48..9953f43 100644
--- a/lib/lp/soyuz/enums.py
+++ b/lib/lp/soyuz/enums.py
@@ -208,6 +208,13 @@ class BinaryPackageFileType(DBEnumeratedType):
build environment.
""")
+ WHL = DBItem(6, """
+ Python Wheel
+
+ The "wheel" binary package format for Python, originally defined in
+ U{https://peps.python.org/pep-0427/}.
+ """)
+
class BinaryPackageFormat(DBEnumeratedType):
"""Binary Package Format
@@ -251,6 +258,13 @@ class BinaryPackageFormat(DBEnumeratedType):
This is the binary package format used for shipping debug symbols
in Ubuntu and similar distributions.""")
+ WHL = DBItem(6, """
+ Python Wheel
+
+ The "wheel" binary package format for Python, originally defined in
+ U{https://peps.python.org/pep-0427/}.
+ """)
+
class PackageCopyPolicy(DBEnumeratedType):
"""Package copying policy.
diff --git a/lib/lp/soyuz/interfaces/publishing.py b/lib/lp/soyuz/interfaces/publishing.py
index a1cfeab..21a3cac 100644
--- a/lib/lp/soyuz/interfaces/publishing.py
+++ b/lib/lp/soyuz/interfaces/publishing.py
@@ -50,7 +50,6 @@ from zope.schema import (
Date,
Datetime,
Int,
- List,
Text,
TextLine,
)
@@ -269,9 +268,8 @@ class ISourcePackagePublishingHistoryPublic(IPublishingView):
vocabulary=PackagePublishingPocket,
required=True, readonly=True,
))
- channel = List(
- value_type=TextLine(), title=_("Channel"),
- required=False, readonly=False,
+ channel = TextLine(
+ title=_("Channel"), required=False, readonly=False,
description=_(
"The channel into which this entry is published "
"(only for archives published using Artifactory)"))
@@ -700,9 +698,8 @@ class IBinaryPackagePublishingHistoryPublic(IPublishingView):
vocabulary=PackagePublishingPocket,
required=True, readonly=True,
))
- channel = List(
- value_type=TextLine(), title=_("Channel"),
- required=False, readonly=False,
+ channel = TextLine(
+ title=_("Channel"), required=False, readonly=False,
description=_(
"The channel into which this entry is published "
"(only for archives published using Artifactory)"))
@@ -970,7 +967,8 @@ class IPublishingSet(Interface):
def newSourcePublication(archive, sourcepackagerelease, distroseries,
component, section, pocket, ancestor,
create_dsd_job=True, copied_from_archive=None,
- creator=None, sponsor=None, packageupload=None):
+ creator=None, sponsor=None, packageupload=None,
+ channel=None):
"""Create a new `SourcePackagePublishingHistory`.
:param archive: An `IArchive`
diff --git a/lib/lp/soyuz/model/binarypackagerelease.py b/lib/lp/soyuz/model/binarypackagerelease.py
index 25cae48..7134895 100644
--- a/lib/lp/soyuz/model/binarypackagerelease.py
+++ b/lib/lp/soyuz/model/binarypackagerelease.py
@@ -157,6 +157,8 @@ class BinaryPackageRelease(SQLBase):
determined_filetype = BinaryPackageFileType.UDEB
elif file.filename.endswith(".ddeb"):
determined_filetype = BinaryPackageFileType.DDEB
+ elif file.filename.endswith(".whl"):
+ determined_filetype = BinaryPackageFileType.WHL
else:
raise AssertionError(
'Unsupported file type: %s' % file.filename)
diff --git a/lib/lp/soyuz/model/publishing.py b/lib/lp/soyuz/model/publishing.py
index cdab414..2e2cc1f 100644
--- a/lib/lp/soyuz/model/publishing.py
+++ b/lib/lp/soyuz/model/publishing.py
@@ -12,6 +12,7 @@ __all__ = [
from collections import defaultdict
from datetime import datetime
+import json
from operator import (
attrgetter,
itemgetter,
@@ -20,6 +21,7 @@ from pathlib import Path
import sys
import pytz
+from storm.databases.postgres import JSON
from storm.expr import (
And,
Cast,
@@ -31,10 +33,6 @@ from storm.expr import (
Sum,
)
from storm.info import ClassAlias
-from storm.properties import (
- List,
- Unicode,
- )
from storm.store import Store
from storm.zope import IResultSet
from storm.zope.interfaces import ISQLObjectResultSet
@@ -51,6 +49,10 @@ from lp.registry.interfaces.person import validate_public_person
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.registry.interfaces.sourcepackage import SourcePackageType
from lp.registry.model.sourcepackagename import SourcePackageName
+from lp.services.channels import (
+ channel_list_to_string,
+ channel_string_to_list,
+ )
from lp.services.database import bulk
from lp.services.database.constants import UTC_NOW
from lp.services.database.datetimecol import UtcDateTimeCol
@@ -267,7 +269,7 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
pocket = DBEnum(name='pocket', enum=PackagePublishingPocket,
default=PackagePublishingPocket.RELEASE,
allow_none=False)
- channel = List(name="channel", type=Unicode(), allow_none=True)
+ _channel = JSON(name="channel", allow_none=True)
archive = ForeignKey(dbName="archive", foreignKey="Archive", notNull=True)
copied_from_archive = ForeignKey(
dbName="copied_from_archive", foreignKey="Archive", notNull=False)
@@ -315,6 +317,13 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
self.distroseries.setNewerDistroSeriesVersions([self])
return get_property_cache(self).newer_distroseries_version
+ @property
+ def channel(self):
+ """See `ISourcePackagePublishingHistory`."""
+ if self._channel is None:
+ return None
+ return channel_list_to_string(*self._channel)
+
def getPublishedBinaries(self):
"""See `ISourcePackagePublishingHistory`."""
publishing_set = getUtility(IPublishingSet)
@@ -538,7 +547,8 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
component=new_component,
section=new_section,
creator=creator,
- archive=self.archive)
+ archive=self.archive,
+ channel=self.channel)
def copyTo(self, distroseries, pocket, archive, override=None,
create_dsd_job=True, creator=None, sponsor=None,
@@ -564,7 +574,8 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
creator=creator,
sponsor=sponsor,
copied_from_archive=self.archive,
- packageupload=packageupload)
+ packageupload=packageupload,
+ channel=self.channel)
def getStatusSummaryForBuilds(self):
"""See `ISourcePackagePublishingHistory`."""
@@ -685,7 +696,7 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
datemadepending = UtcDateTimeCol(default=None)
dateremoved = UtcDateTimeCol(default=None)
pocket = DBEnum(name='pocket', enum=PackagePublishingPocket)
- channel = List(name="channel", type=Unicode(), allow_none=True)
+ _channel = JSON(name="channel", allow_none=True)
archive = ForeignKey(dbName="archive", foreignKey="Archive", notNull=True)
copied_from_archive = ForeignKey(
dbName="copied_from_archive", foreignKey="Archive", notNull=False)
@@ -779,6 +790,13 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
distroseries.name,
self.distroarchseries.architecturetag)
+ @property
+ def channel(self):
+ """See `ISourcePackagePublishingHistory`."""
+ if self._channel is None:
+ return None
+ return channel_list_to_string(*self._channel)
+
def getDownloadCount(self):
"""See `IBinaryPackagePublishingHistory`."""
return self.archive.getPackageDownloadTotal(self.binarypackagerelease)
@@ -836,21 +854,26 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
dominant.distroarchseries.architecturetag))
dominant_build = dominant.binarypackagerelease.build
- distroarchseries = dominant_build.distro_arch_series
- if logger is not None:
- logger.debug(
- "The %s build of %s has been judged as superseded by the "
- "build of %s. Arch-specific == %s" % (
- distroarchseries.architecturetag,
- self.binarypackagerelease.title,
- dominant_build.source_package_release.title,
- self.binarypackagerelease.architecturespecific))
- # Binary package releases are superseded by the new build,
- # not the new binary package release. This is because
- # there may not *be* a new matching binary package -
- # source packages can change the binaries they build
- # between releases.
- self.supersededby = dominant_build
+ # XXX cjwatson 2022-05-01: We can't currently dominate with CI
+ # builds, since supersededby is a reference to a BPB. Just
+ # leave supersededby unset in that case for now, which isn't
+ # ideal but will work well enough.
+ if dominant_build is not None:
+ distroarchseries = dominant_build.distro_arch_series
+ if logger is not None:
+ logger.debug(
+ "The %s build of %s has been judged as superseded by "
+ "the build of %s. Arch-specific == %s" % (
+ distroarchseries.architecturetag,
+ self.binarypackagerelease.title,
+ dominant_build.source_package_release.title,
+ self.binarypackagerelease.architecturespecific))
+ # Binary package releases are superseded by the new build,
+ # not the new binary package release. This is because
+ # there may not *be* a new matching binary package -
+ # source packages can change the binaries they build
+ # between releases.
+ self.supersededby = dominant_build
debug = getUtility(IPublishingSet).findCorrespondingDDEBPublications(
[self])
@@ -941,7 +964,8 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
priority=new_priority,
creator=creator,
archive=debug.archive,
- phased_update_percentage=new_phased_update_percentage)
+ phased_update_percentage=new_phased_update_percentage,
+ _channel=removeSecurityProxy(debug)._channel)
# Append the modified package publishing entry
return BinaryPackagePublishingHistory(
@@ -957,7 +981,8 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
priority=new_priority,
archive=self.archive,
creator=creator,
- phased_update_percentage=new_phased_update_percentage)
+ phased_update_percentage=new_phased_update_percentage,
+ _channel=self._channel)
def copyTo(self, distroseries, pocket, archive):
"""See `BinaryPackagePublishingHistory`."""
@@ -1086,10 +1111,12 @@ class PublishingSet:
"""Utilities for manipulating publications in batches."""
def publishBinaries(self, archive, distroseries, pocket, binaries,
- copied_from_archives=None):
+ copied_from_archives=None, channel=None):
"""See `IPublishingSet`."""
if copied_from_archives is None:
copied_from_archives = {}
+ if channel is not None:
+ channel = channel_string_to_list(channel)
# Expand the dict of binaries into a list of tuples including the
# architecture.
if distroseries.distribution != archive.distribution:
@@ -1124,6 +1151,9 @@ class PublishingSet:
BinaryPackageRelease.binarypackagenameID,
BinaryPackageRelease.version),
BinaryPackagePublishingHistory.pocket == pocket,
+ Not(IsDistinctFrom(
+ BinaryPackagePublishingHistory._channel,
+ json.dumps(channel) if channel is not None else None)),
BinaryPackagePublishingHistory.status.is_in(
active_publishing_status),
BinaryPackageRelease.id ==
@@ -1141,12 +1171,13 @@ class PublishingSet:
BPPH = BinaryPackagePublishingHistory
return bulk.create(
(BPPH.archive, BPPH.copied_from_archive,
- BPPH.distroarchseries, BPPH.pocket,
+ BPPH.distroarchseries, BPPH.pocket, BPPH._channel,
BPPH.binarypackagerelease, BPPH.binarypackagename,
+ BPPH._binarypackageformat,
BPPH.component, BPPH.section, BPPH.priority,
BPPH.phased_update_percentage, BPPH.status, BPPH.datecreated),
- [(archive, copied_from_archives.get(bpr), das, pocket, bpr,
- bpr.binarypackagename,
+ [(archive, copied_from_archives.get(bpr), das, pocket, channel,
+ bpr, bpr.binarypackagename, bpr.binpackageformat,
get_component(archive, das.distroseries, component),
section, priority, phased_update_percentage,
PackagePublishingStatus.PENDING, UTC_NOW)
@@ -1156,7 +1187,7 @@ class PublishingSet:
get_objects=True)
def copyBinaries(self, archive, distroseries, pocket, bpphs, policy=None,
- source_override=None):
+ source_override=None, channel=None):
"""See `IPublishingSet`."""
from lp.soyuz.adapters.overrides import BinaryOverride
if distroseries.distribution != archive.distribution:
@@ -1228,13 +1259,14 @@ class PublishingSet:
bpph.binarypackagerelease: bpph.archive for bpph in bpphs}
return self.publishBinaries(
archive, distroseries, pocket, with_overrides,
- copied_from_archives)
+ copied_from_archives, channel=channel)
def newSourcePublication(self, archive, sourcepackagerelease,
distroseries, component, section, pocket,
ancestor=None, create_dsd_job=True,
copied_from_archive=None,
- creator=None, sponsor=None, packageupload=None):
+ creator=None, sponsor=None, packageupload=None,
+ channel=None):
"""See `IPublishingSet`."""
# Circular import.
from lp.registry.model.distributionsourcepackage import (
@@ -1246,6 +1278,14 @@ class PublishingSet:
"Series distribution %s doesn't match archive distribution %s."
% (distroseries.distribution.name, archive.distribution.name))
+ if (sourcepackagerelease.format == SourcePackageType.DPKG and
+ channel is not None):
+ raise AssertionError(
+ "Can't publish dpkg source packages to a channel")
+
+ if channel is not None:
+ channel = channel_string_to_list(channel)
+
pub = SourcePackagePublishingHistory(
distroseries=distroseries,
pocket=pocket,
@@ -1261,7 +1301,8 @@ class PublishingSet:
ancestor=ancestor,
creator=creator,
sponsor=sponsor,
- packageupload=packageupload)
+ packageupload=packageupload,
+ _channel=channel)
DistributionSourcePackage.ensure(pub)
if create_dsd_job and archive == distroseries.main_archive:
diff --git a/lib/lp/soyuz/tests/test_publishing.py b/lib/lp/soyuz/tests/test_publishing.py
index 41cdf25..580df13 100644
--- a/lib/lp/soyuz/tests/test_publishing.py
+++ b/lib/lp/soyuz/tests/test_publishing.py
@@ -36,6 +36,7 @@ from lp.registry.interfaces.sourcepackage import (
SourcePackageUrgency,
)
from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
+from lp.services.channels import channel_string_to_list
from lp.services.config import config
from lp.services.database.constants import UTC_NOW
from lp.services.librarian.interfaces import ILibraryFileAliasSet
@@ -212,7 +213,8 @@ class SoyuzTestPublisher:
build_conflicts_indep=None,
dsc_maintainer_rfc822='Foo Bar <foo@xxxxxxx>',
maintainer=None, creator=None, date_uploaded=UTC_NOW,
- spr_only=False, user_defined_fields=None):
+ spr_only=False, user_defined_fields=None,
+ format=SourcePackageType.DPKG, channel=None):
"""Return a mock source publishing record.
if spr_only is specified, the source is not published and the
@@ -238,7 +240,7 @@ class SoyuzTestPublisher:
spr = distroseries.createUploadedSourcePackageRelease(
sourcepackagename=spn,
- format=SourcePackageType.DPKG,
+ format=format,
maintainer=maintainer,
creator=creator,
component=component,
@@ -289,6 +291,8 @@ class SoyuzTestPublisher:
datepublished = UTC_NOW
else:
datepublished = None
+ if channel is not None:
+ channel = channel_string_to_list(channel)
spph = SourcePackagePublishingHistory(
distroseries=distroseries,
@@ -304,7 +308,8 @@ class SoyuzTestPublisher:
scheduleddeletiondate=scheduleddeletiondate,
pocket=pocket,
archive=archive,
- creator=creator)
+ creator=creator,
+ _channel=channel)
return spph
@@ -328,7 +333,8 @@ class SoyuzTestPublisher:
builder=None,
component='main',
phased_update_percentage=None,
- with_debug=False, user_defined_fields=None):
+ with_debug=False, user_defined_fields=None,
+ channel=None):
"""Return a list of binary publishing records."""
if distroseries is None:
distroseries = self.distroseries
@@ -366,7 +372,7 @@ class SoyuzTestPublisher:
pub_binaries += self.publishBinaryInArchive(
binarypackagerelease_ddeb, archive, status,
pocket, scheduleddeletiondate, dateremoved,
- phased_update_percentage)
+ phased_update_percentage, channel=channel)
else:
binarypackagerelease_ddeb = None
@@ -378,7 +384,8 @@ class SoyuzTestPublisher:
user_defined_fields=user_defined_fields)
pub_binaries += self.publishBinaryInArchive(
binarypackagerelease, archive, status, pocket,
- scheduleddeletiondate, dateremoved, phased_update_percentage)
+ scheduleddeletiondate, dateremoved, phased_update_percentage,
+ channel=channel)
published_binaries.extend(pub_binaries)
package_upload = self.addPackageUpload(
archive, distroseries, pocket,
@@ -476,7 +483,7 @@ class SoyuzTestPublisher:
status=PackagePublishingStatus.PENDING,
pocket=PackagePublishingPocket.RELEASE,
scheduleddeletiondate=None, dateremoved=None,
- phased_update_percentage=None):
+ phased_update_percentage=None, channel=None):
"""Return the corresponding BinaryPackagePublishingHistory."""
distroarchseries = binarypackagerelease.build.distro_arch_series
@@ -485,6 +492,8 @@ class SoyuzTestPublisher:
archs = [distroarchseries]
else:
archs = distroarchseries.distroseries.architectures
+ if channel is not None:
+ channel = channel_string_to_list(channel)
pub_binaries = []
for arch in archs:
@@ -502,7 +511,8 @@ class SoyuzTestPublisher:
datecreated=UTC_NOW,
pocket=pocket,
archive=archive,
- phased_update_percentage=phased_update_percentage)
+ phased_update_percentage=phased_update_percentage,
+ _channel=channel)
if status == PackagePublishingStatus.PUBLISHED:
pub.datepublished = UTC_NOW
pub_binaries.append(pub)
@@ -1583,11 +1593,11 @@ class TestPublishBinaries(TestCaseWithFactory):
layer = LaunchpadZopelessLayer
- def makeArgs(self, bprs, distroseries, archive=None):
+ def makeArgs(self, bprs, distroseries, archive=None, channel=None):
"""Create a dict of arguments for publishBinaries."""
if archive is None:
archive = distroseries.main_archive
- return {
+ args = {
'archive': archive,
'distroseries': distroseries,
'pocket': PackagePublishingPocket.BACKPORTS,
@@ -1596,6 +1606,9 @@ class TestPublishBinaries(TestCaseWithFactory):
self.factory.makeSection(),
PackagePublishingPriority.REQUIRED, 50) for bpr in bprs},
}
+ if channel is not None:
+ args['channel'] = channel
+ return args
def test_architecture_dependent(self):
# Architecture-dependent binaries get created as PENDING in the
@@ -1614,8 +1627,8 @@ class TestPublishBinaries(TestCaseWithFactory):
overrides = args['binaries'][bpr]
self.assertEqual(bpr, bpph.binarypackagerelease)
self.assertEqual(
- (args['archive'], target_das, args['pocket']),
- (bpph.archive, bpph.distroarchseries, bpph.pocket))
+ (args['archive'], target_das, args['pocket'], None),
+ (bpph.archive, bpph.distroarchseries, bpph.pocket, bpph.channel))
self.assertEqual(
overrides,
(bpph.component, bpph.section, bpph.priority,
@@ -1670,30 +1683,59 @@ class TestPublishBinaries(TestCaseWithFactory):
args['pocket'] = PackagePublishingPocket.RELEASE
[another_bpph] = getUtility(IPublishingSet).publishBinaries(**args)
+ def test_channel(self):
+ bpr = self.factory.makeBinaryPackageRelease(
+ binpackageformat=BinaryPackageFormat.WHL)
+ target_das = self.factory.makeDistroArchSeries()
+ args = self.makeArgs([bpr], target_das.distroseries, channel="stable")
+ [bpph] = getUtility(IPublishingSet).publishBinaries(**args)
+ self.assertEqual(bpr, bpph.binarypackagerelease)
+ self.assertEqual(
+ (args["archive"], target_das, args["pocket"], args["channel"]),
+ (bpph.archive, bpph.distroarchseries, bpph.pocket, bpph.channel))
+ self.assertEqual(PackagePublishingStatus.PENDING, bpph.status)
+
+ def test_does_not_duplicate_by_channel(self):
+ bpr = self.factory.makeBinaryPackageRelease(
+ binpackageformat=BinaryPackageFormat.WHL)
+ target_das = self.factory.makeDistroArchSeries()
+ args = self.makeArgs([bpr], target_das.distroseries, channel="stable")
+ [bpph] = getUtility(IPublishingSet).publishBinaries(**args)
+ self.assertContentEqual(
+ [], getUtility(IPublishingSet).publishBinaries(**args))
+ args["channel"] = "edge"
+ [another_bpph] = getUtility(IPublishingSet).publishBinaries(**args)
+
class TestChangeOverride(TestNativePublishingBase):
"""Test that changing overrides works."""
def setUpOverride(self, status=SeriesStatus.DEVELOPMENT,
- pocket=PackagePublishingPocket.RELEASE, binary=False,
- ddeb=False, **kwargs):
+ pocket=PackagePublishingPocket.RELEASE, channel=None,
+ binary=False, format=None, ddeb=False, **kwargs):
self.distroseries.status = status
+ get_pub_kwargs = {"pocket": pocket, "channel": channel}
+ if format is not None:
+ get_pub_kwargs["format"] = format
if ddeb:
- pub = self.getPubBinaries(pocket=pocket, with_debug=True)[2]
+ pub = self.getPubBinaries(with_debug=True, **get_pub_kwargs)[2]
self.assertEqual(
BinaryPackageFormat.DDEB,
pub.binarypackagerelease.binpackageformat)
elif binary:
- pub = self.getPubBinaries(pocket=pocket)[0]
+ pub = self.getPubBinaries(**get_pub_kwargs)[0]
else:
- pub = self.getPubSource(pocket=pocket)
+ pub = self.getPubSource(**get_pub_kwargs)
return pub.changeOverride(**kwargs)
def assertCanOverride(self, status=SeriesStatus.DEVELOPMENT,
- pocket=PackagePublishingPocket.RELEASE, **kwargs):
- new_pub = self.setUpOverride(status=status, pocket=pocket, **kwargs)
+ pocket=PackagePublishingPocket.RELEASE, channel=None,
+ **kwargs):
+ new_pub = self.setUpOverride(
+ status=status, pocket=pocket, channel=channel, **kwargs)
self.assertEqual(new_pub.status, PackagePublishingStatus.PENDING)
self.assertEqual(new_pub.pocket, pocket)
+ self.assertEqual(new_pub.channel, channel)
if "new_component" in kwargs:
self.assertEqual(kwargs["new_component"], new_pub.component.name)
if "new_section" in kwargs:
@@ -1784,6 +1826,12 @@ class TestChangeOverride(TestNativePublishingBase):
self.assertCannotOverride(new_component="partner")
self.assertCannotOverride(binary=True, new_component="partner")
+ def test_preserves_channel(self):
+ self.assertCanOverride(
+ binary=True, format=BinaryPackageFormat.WHL, channel="stable",
+ new_component="universe", new_section="misc", new_priority="extra",
+ new_phased_update_percentage=90)
+
class TestPublishingHistoryView(TestCaseWithFactory):
layer = LaunchpadFunctionalLayer
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 89925b6..29c6059 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -4029,6 +4029,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
creator=None,
packageupload=None,
spr_creator=None,
+ channel=None,
**kwargs):
"""Make a `SourcePackagePublishingHistory`.
@@ -4050,6 +4051,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
is scheduled to be removed.
:param ancestor: The publication ancestor parameter.
:param creator: The publication creator.
+ :param channel: An optional channel to publish into, as a string.
:param **kwargs: All other parameters are passed through to the
makeSourcePackageRelease call if needed.
"""
@@ -4087,7 +4089,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
archive, sourcepackagerelease, distroseries,
sourcepackagerelease.component, sourcepackagerelease.section,
pocket, ancestor=ancestor, creator=creator,
- packageupload=packageupload)
+ packageupload=packageupload, channel=channel)
naked_spph = removeSecurityProxy(spph)
naked_spph.status = status
@@ -4113,7 +4115,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
version=None,
architecturespecific=False,
with_debug=False, with_file=False,
- creator=None):
+ creator=None, channel=None):
"""Make a `BinaryPackagePublishingHistory`."""
if distroarchseries is None:
if archive is None:
@@ -4144,7 +4146,10 @@ class BareLaunchpadObjectFactory(ObjectFactory):
if priority is None:
priority = PackagePublishingPriority.OPTIONAL
if binpackageformat is None:
- binpackageformat = BinaryPackageFormat.DEB
+ if binarypackagerelease is not None:
+ binpackageformat = binarypackagerelease.binpackageformat
+ else:
+ binpackageformat = BinaryPackageFormat.DEB
if binarypackagerelease is None:
# Create a new BinaryPackageBuild and BinaryPackageRelease
@@ -4182,7 +4187,8 @@ class BareLaunchpadObjectFactory(ObjectFactory):
archive, distroarchseries.distroseries, pocket,
{binarypackagerelease: (
binarypackagerelease.component, binarypackagerelease.section,
- priority, None)})
+ priority, None)},
+ channel=channel)
for bpph in bpphs:
naked_bpph = removeSecurityProxy(bpph)
naked_bpph.status = status
@@ -4256,7 +4262,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
libraryfile=library_file, filetype=filetype))
def makeBinaryPackageRelease(self, binarypackagename=None,
- version=None, build=None,
+ version=None, build=None, ci_build=None,
binpackageformat=None, component=None,
section_name=None, priority=None,
architecturespecific=False,
@@ -4270,22 +4276,27 @@ class BareLaunchpadObjectFactory(ObjectFactory):
date_created=None, debug_package=None,
homepage=None):
"""Make a `BinaryPackageRelease`."""
- if build is None:
+ if build is None and ci_build is None:
build = self.makeBinaryPackageBuild()
if binarypackagename is None or isinstance(binarypackagename, str):
binarypackagename = self.getOrMakeBinaryPackageName(
binarypackagename)
- if version is None:
+ if version is None and build is not None:
version = build.source_package_release.version
if binpackageformat is None:
binpackageformat = BinaryPackageFormat.DEB
- if component is None:
+ if component is None and build is not None:
component = build.source_package_release.component
elif isinstance(component, str):
component = getUtility(IComponentSet)[component]
if isinstance(section_name, str):
section_name = self.makeSection(section_name)
- section = section_name or build.source_package_release.section
+ if section_name is not None:
+ section = section_name
+ elif build is not None:
+ section = build.source_package_release.section
+ else:
+ section = None
if priority is None:
priority = PackagePublishingPriority.OPTIONAL
if summary is None:
@@ -4294,18 +4305,35 @@ class BareLaunchpadObjectFactory(ObjectFactory):
description = self.getUniqueString("description")
if installed_size is None:
installed_size = self.getUniqueInteger()
- bpr = build.createBinaryPackageRelease(
- binarypackagename=binarypackagename, version=version,
- binpackageformat=binpackageformat,
- component=component, section=section, priority=priority,
- summary=summary, description=description,
- architecturespecific=architecturespecific,
- shlibdeps=shlibdeps, depends=depends, recommends=recommends,
- suggests=suggests, conflicts=conflicts, replaces=replaces,
- provides=provides, pre_depends=pre_depends,
- enhances=enhances, breaks=breaks, essential=essential,
- installedsize=installed_size, debug_package=debug_package,
- homepage=homepage)
+ kwargs = {
+ "binarypackagename": binarypackagename,
+ "version": version,
+ "binpackageformat": binpackageformat,
+ "summary": summary,
+ "description": description,
+ "architecturespecific": architecturespecific,
+ "installedsize": installed_size,
+ "homepage": homepage,
+ }
+ if build is not None:
+ kwargs.update({
+ "component": component,
+ "section": section,
+ "priority": priority,
+ "shlibdeps": shlibdeps,
+ "depends": depends,
+ "recommends": recommends,
+ "suggests": suggests,
+ "conflicts": conflicts,
+ "replaces": replaces,
+ "provides": provides,
+ "pre_depends": pre_depends,
+ "enhances": enhances,
+ "breaks": breaks,
+ "essential": essential,
+ "debug_package": debug_package,
+ })
+ bpr = (build or ci_build).createBinaryPackageRelease(**kwargs)
if date_created is not None:
removeSecurityProxy(bpr).datecreated = date_created
return bpr