Merge ~cjwatson/launchpad:built-using-guard-copying into launchpad:master
- Git
- lp:~cjwatson/launchpad
- built-using-guard-copying
- Merge into master
Status: | Needs review | ||||
---|---|---|---|---|---|
Proposed branch: | ~cjwatson/launchpad:built-using-guard-copying | ||||
Merge into: | launchpad:master | ||||
Prerequisite: | ~cjwatson/launchpad:built-using-guard-deletion | ||||
Diff against target: |
311 lines (+226/-3) 5 files modified
lib/lp/soyuz/interfaces/binarysourcereference.py (+17/-0) lib/lp/soyuz/model/binarysourcereference.py (+29/-1) lib/lp/soyuz/scripts/packagecopier.py (+26/-2) lib/lp/soyuz/scripts/tests/test_copypackage.py (+95/-0) lib/lp/soyuz/tests/test_binarysourcereference.py (+59/-0) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ioana Lasc (community) | Approve | ||
Review via email: mp+382792@code.launchpad.net |
Commit message
Guard copies of binaries with Built-Using references
Description of the change
If binaries have Built-Using references, then we need to make sure that we can resolve those references and keep the corresponding sources published while the binaries are published. Prevent copies of binaries if any such references can't be resolved in the target publishing context.
Unmerged commits
- 9c6dce7... by Colin Watson
-
Guard copies of binaries with Built-Using references
If binaries have Built-Using references, then we need to make sure that
we can resolve those references and keep the corresponding sources
published while the binaries are published. Prevent copies of binaries
if any such references can't be resolved in the target publishing
context.LP: #1868558
- 06ccc3e... by Colin Watson
-
Simplify tests using createFromSourc
ePackageRelease s - c0c4aa2... by Colin Watson
-
Expand deletion guard to other pockets
It now checks all pockets that could legitimately depend on the one from
which the publication is being deleted. - d7fbcfd... by Colin Watson
-
Guard removal of sources referenced by Built-Using
Prevent SourcePackagePu
blishingHistory .requestDeletio n from deleting
source publications that have Built-Using references from active binary
publications in the same archive and suite.This isn't necessarily complete: in particular, it can miss references
from other pockets, and in any case it might race with a build still in
progress. The intent of this is not to ensure integrity, but to avoid
some easily-detectable mistakes that could cause confusion.LP: #1868558
Preview Diff
1 | diff --git a/lib/lp/soyuz/interfaces/binarysourcereference.py b/lib/lp/soyuz/interfaces/binarysourcereference.py | |||
2 | index e625cf9..f702959 100644 | |||
3 | --- a/lib/lp/soyuz/interfaces/binarysourcereference.py | |||
4 | +++ b/lib/lp/soyuz/interfaces/binarysourcereference.py | |||
5 | @@ -99,3 +99,20 @@ class IBinarySourceReferenceSet(Interface): | |||
6 | 99 | pointing to any of this sequence of `ISourcePackageRelease`s. | 99 | pointing to any of this sequence of `ISourcePackageRelease`s. |
7 | 100 | :return: A `ResultSet` of matching `IBinarySourceReference`s. | 100 | :return: A `ResultSet` of matching `IBinarySourceReference`s. |
8 | 101 | """ | 101 | """ |
9 | 102 | |||
10 | 103 | def findMissingSources(archive, distroseries, pockets, reference_type, | ||
11 | 104 | binary_package_releases=None): | ||
12 | 105 | """Find references to unpublished sources in a given context. | ||
13 | 106 | |||
14 | 107 | :param archive: An `IArchive` to search for source publications. | ||
15 | 108 | :param distroseries: An `IDistroSeries` to search for source | ||
16 | 109 | publications. | ||
17 | 110 | :param pockets: A sequence of `PackagePublishingPocket`s to search | ||
18 | 111 | for source publications. | ||
19 | 112 | :param reference_type: A `BinarySourceReferenceType` to search for. | ||
20 | 113 | :param binary_package_releases: Only return references from any of | ||
21 | 114 | this sequence of `IBinaryPackageRelease`s. | ||
22 | 115 | :return: A `ResultSet` of matching `IBinarySourceReference`s where | ||
23 | 116 | the `source_package_release` is not published in the given | ||
24 | 117 | publishing context. | ||
25 | 118 | """ | ||
26 | diff --git a/lib/lp/soyuz/model/binarysourcereference.py b/lib/lp/soyuz/model/binarysourcereference.py | |||
27 | index ca3563b..c2b41e0 100644 | |||
28 | --- a/lib/lp/soyuz/model/binarysourcereference.py | |||
29 | +++ b/lib/lp/soyuz/model/binarysourcereference.py | |||
30 | @@ -14,7 +14,9 @@ __all__ = [ | |||
31 | 14 | import warnings | 14 | import warnings |
32 | 15 | 15 | ||
33 | 16 | from debian.deb822 import PkgRelation | 16 | from debian.deb822 import PkgRelation |
34 | 17 | from storm.expr import LeftJoin | ||
35 | 17 | from storm.locals import ( | 18 | from storm.locals import ( |
36 | 19 | And, | ||
37 | 18 | Int, | 20 | Int, |
38 | 19 | Reference, | 21 | Reference, |
39 | 20 | ) | 22 | ) |
40 | @@ -35,7 +37,10 @@ from lp.soyuz.interfaces.binarysourcereference import ( | |||
41 | 35 | UnparsableBuiltUsing, | 37 | UnparsableBuiltUsing, |
42 | 36 | ) | 38 | ) |
43 | 37 | from lp.soyuz.model.distroarchseries import DistroArchSeries | 39 | from lp.soyuz.model.distroarchseries import DistroArchSeries |
45 | 38 | from lp.soyuz.model.publishing import BinaryPackagePublishingHistory | 40 | from lp.soyuz.model.publishing import ( |
46 | 41 | BinaryPackagePublishingHistory, | ||
47 | 42 | SourcePackagePublishingHistory, | ||
48 | 43 | ) | ||
49 | 39 | 44 | ||
50 | 40 | 45 | ||
51 | 41 | @implementer(IBinarySourceReference) | 46 | @implementer(IBinarySourceReference) |
52 | @@ -175,3 +180,26 @@ class BinarySourceReferenceSet: | |||
53 | 175 | spr.id for spr in source_package_releases)) | 180 | spr.id for spr in source_package_releases)) |
54 | 176 | return IStore(BinarySourceReference).find( | 181 | return IStore(BinarySourceReference).find( |
55 | 177 | BinarySourceReference, *clauses).config(distinct=True) | 182 | BinarySourceReference, *clauses).config(distinct=True) |
56 | 183 | |||
57 | 184 | @classmethod | ||
58 | 185 | def findMissingSources(cls, archive, distroseries, pockets, reference_type, | ||
59 | 186 | binary_package_releases): | ||
60 | 187 | """See `IBinarySourceReferenceSet`.""" | ||
61 | 188 | origin = [ | ||
62 | 189 | BinarySourceReference, | ||
63 | 190 | LeftJoin( | ||
64 | 191 | SourcePackagePublishingHistory, | ||
65 | 192 | And( | ||
66 | 193 | BinarySourceReference.source_package_release_id == | ||
67 | 194 | SourcePackagePublishingHistory.sourcepackagereleaseID, | ||
68 | 195 | SourcePackagePublishingHistory.archive == archive, | ||
69 | 196 | SourcePackagePublishingHistory.distroseries == | ||
70 | 197 | distroseries, | ||
71 | 198 | SourcePackagePublishingHistory.pocket.is_in(pockets))), | ||
72 | 199 | ] | ||
73 | 200 | return IStore(BinarySourceReference).using(*origin).find( | ||
74 | 201 | BinarySourceReference, | ||
75 | 202 | BinarySourceReference.binary_package_release_id.is_in( | ||
76 | 203 | bpr.id for bpr in binary_package_releases), | ||
77 | 204 | BinarySourceReference.reference_type == reference_type, | ||
78 | 205 | SourcePackagePublishingHistory.id == None).config(distinct=True) | ||
79 | diff --git a/lib/lp/soyuz/scripts/packagecopier.py b/lib/lp/soyuz/scripts/packagecopier.py | |||
80 | index 3891af5..03f4c77 100644 | |||
81 | --- a/lib/lp/soyuz/scripts/packagecopier.py | |||
82 | +++ b/lib/lp/soyuz/scripts/packagecopier.py | |||
83 | @@ -1,4 +1,4 @@ | |||
85 | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
86 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
87 | 3 | 3 | ||
88 | 4 | """Package copying utilities.""" | 4 | """Package copying utilities.""" |
89 | @@ -19,10 +19,17 @@ from zope.component import getUtility | |||
90 | 19 | from zope.security.proxy import removeSecurityProxy | 19 | from zope.security.proxy import removeSecurityProxy |
91 | 20 | 20 | ||
92 | 21 | from lp.services.database.bulk import load_related | 21 | from lp.services.database.bulk import load_related |
93 | 22 | from lp.soyuz.adapters.archivedependencies import pocket_dependencies | ||
94 | 22 | from lp.soyuz.adapters.overrides import SourceOverride | 23 | from lp.soyuz.adapters.overrides import SourceOverride |
96 | 23 | from lp.soyuz.enums import SourcePackageFormat | 24 | from lp.soyuz.enums import ( |
97 | 25 | BinarySourceReferenceType, | ||
98 | 26 | SourcePackageFormat, | ||
99 | 27 | ) | ||
100 | 24 | from lp.soyuz.interfaces.archive import CannotCopy | 28 | from lp.soyuz.interfaces.archive import CannotCopy |
101 | 25 | from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus | 29 | from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus |
102 | 30 | from lp.soyuz.interfaces.binarysourcereference import ( | ||
103 | 31 | IBinarySourceReferenceSet, | ||
104 | 32 | ) | ||
105 | 26 | from lp.soyuz.interfaces.publishing import ( | 33 | from lp.soyuz.interfaces.publishing import ( |
106 | 27 | active_publishing_status, | 34 | active_publishing_status, |
107 | 28 | IBinaryPackagePublishingHistory, | 35 | IBinaryPackagePublishingHistory, |
108 | @@ -439,6 +446,7 @@ class CopyChecker: | |||
109 | 439 | built_binaries = source.getBuiltBinaries(want_files=True) | 446 | built_binaries = source.getBuiltBinaries(want_files=True) |
110 | 440 | if len(built_binaries) == 0: | 447 | if len(built_binaries) == 0: |
111 | 441 | raise CannotCopy("source has no binaries to be copied") | 448 | raise CannotCopy("source has no binaries to be copied") |
112 | 449 | |||
113 | 442 | # Deny copies of binary publications containing files with | 450 | # Deny copies of binary publications containing files with |
114 | 443 | # expiration date set. We only set such value for immediate | 451 | # expiration date set. We only set such value for immediate |
115 | 444 | # expiration of old superseded binaries, so no point in | 452 | # expiration of old superseded binaries, so no point in |
116 | @@ -449,6 +457,22 @@ class CopyChecker: | |||
117 | 449 | if binary_file.libraryfile.expires is not None: | 457 | if binary_file.libraryfile.expires is not None: |
118 | 450 | raise CannotCopy('source has expired binaries') | 458 | raise CannotCopy('source has expired binaries') |
119 | 451 | 459 | ||
120 | 460 | # Deny copies of binary publications that contain Built-Using | ||
121 | 461 | # references to sources that do not exist in the target. The | ||
122 | 462 | # dominator will not be able to rectify the situation. | ||
123 | 463 | bsr_set = getUtility(IBinarySourceReferenceSet) | ||
124 | 464 | missing_sources = bsr_set.findMissingSources( | ||
125 | 465 | self.archive, series, pocket_dependencies[pocket], | ||
126 | 466 | BinarySourceReferenceType.BUILT_USING, | ||
127 | 467 | [binary_pub.binarypackagerelease | ||
128 | 468 | for binary_pub in built_binaries]) | ||
129 | 469 | if not missing_sources.is_empty(): | ||
130 | 470 | # XXX cjwatson 2020-04-19: It may also be useful to show the | ||
131 | 471 | # specific Built-Using references that don't exist. | ||
132 | 472 | raise CannotCopy( | ||
133 | 473 | 'source has binaries with Built-Using references that do ' | ||
134 | 474 | 'not exist in the target') | ||
135 | 475 | |||
136 | 452 | # Check if there is already a source with the same name and version | 476 | # Check if there is already a source with the same name and version |
137 | 453 | # published in the destination archive. | 477 | # published in the destination archive. |
138 | 454 | self._checkArchiveConflicts(source, series) | 478 | self._checkArchiveConflicts(source, series) |
139 | diff --git a/lib/lp/soyuz/scripts/tests/test_copypackage.py b/lib/lp/soyuz/scripts/tests/test_copypackage.py | |||
140 | index 388fcf7..a41cfb1 100644 | |||
141 | --- a/lib/lp/soyuz/scripts/tests/test_copypackage.py | |||
142 | +++ b/lib/lp/soyuz/scripts/tests/test_copypackage.py | |||
143 | @@ -1046,6 +1046,101 @@ class CopyCheckerTestCase(TestCaseWithFactory): | |||
144 | 1046 | copied_from_archive=binary.archive), | 1046 | copied_from_archive=binary.archive), |
145 | 1047 | ])) | 1047 | ])) |
146 | 1048 | 1048 | ||
147 | 1049 | def test_checkCopy_cannot_copy_dangling_built_using_references(self): | ||
148 | 1050 | # checkCopy() raises CannotCopy if the copy includes binaries and | ||
149 | 1051 | # the binaries contain Built-Using references to sources that do not | ||
150 | 1052 | # exist in the target. | ||
151 | 1053 | |||
152 | 1054 | # Create testing sources and binaries. | ||
153 | 1055 | source = self.test_publisher.getPubSource() | ||
154 | 1056 | built_using_source = self.test_publisher.getPubSource( | ||
155 | 1057 | sourcename='built-using') | ||
156 | 1058 | built_using_relationship = '%s (= %s)' % ( | ||
157 | 1059 | built_using_source.sourcepackagerelease.name, | ||
158 | 1060 | built_using_source.sourcepackagerelease.version) | ||
159 | 1061 | self.test_publisher.getPubBinaries( | ||
160 | 1062 | pub_source=source, built_using=built_using_relationship) | ||
161 | 1063 | |||
162 | 1064 | # Create a fresh PPA which will be the destination copy. | ||
163 | 1065 | archive = self.factory.makeArchive( | ||
164 | 1066 | distribution=self.test_publisher.ubuntutest, | ||
165 | 1067 | purpose=ArchivePurpose.PPA) | ||
166 | 1068 | series = source.distroseries | ||
167 | 1069 | pocket = source.pocket | ||
168 | 1070 | |||
169 | 1071 | # Now source-only copies are allowed. | ||
170 | 1072 | copy_checker = CopyChecker(archive, include_binaries=False) | ||
171 | 1073 | self.assertIsNone( | ||
172 | 1074 | copy_checker.checkCopy( | ||
173 | 1075 | source, series, pocket, check_permissions=False)) | ||
174 | 1076 | |||
175 | 1077 | # Copies with binaries are denied. | ||
176 | 1078 | copy_checker = CopyChecker(archive, include_binaries=True) | ||
177 | 1079 | self.assertRaisesWithContent( | ||
178 | 1080 | CannotCopy, | ||
179 | 1081 | 'source has binaries with Built-Using references that do not ' | ||
180 | 1082 | 'exist in the target', | ||
181 | 1083 | copy_checker.checkCopy, | ||
182 | 1084 | source, series, pocket, check_permissions=False) | ||
183 | 1085 | |||
184 | 1086 | def test_checkCopy_can_copy_resolvable_built_using_references(self): | ||
185 | 1087 | # checkCopy() allows copying binaries with Built-Using references to | ||
186 | 1088 | # sources that exist in the target, even if no longer published or | ||
187 | 1089 | # in a pocket that the target depends on. | ||
188 | 1090 | |||
189 | 1091 | # Create testing sources and binaries. | ||
190 | 1092 | source = self.test_publisher.getPubSource() | ||
191 | 1093 | published_source = self.test_publisher.getPubSource( | ||
192 | 1094 | sourcename='published') | ||
193 | 1095 | superseded_source = self.test_publisher.getPubSource( | ||
194 | 1096 | sourcename='superseded') | ||
195 | 1097 | release_pocket_source = self.test_publisher.getPubSource( | ||
196 | 1098 | sourcename='release-pocket') | ||
197 | 1099 | relationships = [ | ||
198 | 1100 | '%s (= %s)' % ( | ||
199 | 1101 | spph.sourcepackagerelease.name, | ||
200 | 1102 | spph.sourcepackagerelease.version) | ||
201 | 1103 | for spph in ( | ||
202 | 1104 | published_source, superseded_source, release_pocket_source)] | ||
203 | 1105 | self.test_publisher.getPubBinaries( | ||
204 | 1106 | built_using=', '.join(relationships), | ||
205 | 1107 | status=PackagePublishingStatus.PUBLISHED, pub_source=source) | ||
206 | 1108 | target_series = self.factory.makeDistroSeries( | ||
207 | 1109 | distribution=source.distroseries.distribution) | ||
208 | 1110 | getUtility(ISourcePackageFormatSelectionSet).add( | ||
209 | 1111 | target_series, SourcePackageFormat.FORMAT_1_0) | ||
210 | 1112 | self.factory.makeSourcePackagePublishingHistory( | ||
211 | 1113 | distroseries=target_series, archive=source.archive, | ||
212 | 1114 | sourcepackagerelease=published_source.sourcepackagerelease, | ||
213 | 1115 | pocket=PackagePublishingPocket.PROPOSED, | ||
214 | 1116 | status=PackagePublishingStatus.PUBLISHED) | ||
215 | 1117 | self.factory.makeSourcePackagePublishingHistory( | ||
216 | 1118 | distroseries=target_series, archive=source.archive, | ||
217 | 1119 | sourcepackagerelease=superseded_source.sourcepackagerelease, | ||
218 | 1120 | pocket=PackagePublishingPocket.PROPOSED, | ||
219 | 1121 | status=PackagePublishingStatus.SUPERSEDED) | ||
220 | 1122 | self.factory.makeSourcePackagePublishingHistory( | ||
221 | 1123 | distroseries=target_series, archive=source.archive, | ||
222 | 1124 | sourcepackagerelease=release_pocket_source.sourcepackagerelease, | ||
223 | 1125 | pocket=PackagePublishingPocket.RELEASE, | ||
224 | 1126 | status=PackagePublishingStatus.PUBLISHED) | ||
225 | 1127 | |||
226 | 1128 | # Copies of binaries are permitted. | ||
227 | 1129 | copy_checker = CopyChecker(source.archive, include_binaries=True) | ||
228 | 1130 | copy_checker.checkCopy( | ||
229 | 1131 | source, target_series, PackagePublishingPocket.PROPOSED, | ||
230 | 1132 | check_permissions=False) | ||
231 | 1133 | |||
232 | 1134 | # Since some of the sources were only published in PROPOSED, copies | ||
233 | 1135 | # of binaries to RELEASE that refer to them are denied. | ||
234 | 1136 | self.assertRaisesWithContent( | ||
235 | 1137 | CannotCopy, | ||
236 | 1138 | 'source has binaries with Built-Using references that do not ' | ||
237 | 1139 | 'exist in the target', | ||
238 | 1140 | copy_checker.checkCopy, | ||
239 | 1141 | source, target_series, PackagePublishingPocket.RELEASE, | ||
240 | 1142 | check_permissions=False) | ||
241 | 1143 | |||
242 | 1049 | 1144 | ||
243 | 1050 | class BaseDoCopyTests: | 1145 | class BaseDoCopyTests: |
244 | 1051 | 1146 | ||
245 | diff --git a/lib/lp/soyuz/tests/test_binarysourcereference.py b/lib/lp/soyuz/tests/test_binarysourcereference.py | |||
246 | index 0c4db7f..b8ebcef 100644 | |||
247 | --- a/lib/lp/soyuz/tests/test_binarysourcereference.py | |||
248 | +++ b/lib/lp/soyuz/tests/test_binarysourcereference.py | |||
249 | @@ -306,3 +306,62 @@ class TestBinarySourceReference(TestCaseWithFactory): | |||
250 | 306 | source_package_release=bsr.source_package_release, | 306 | source_package_release=bsr.source_package_release, |
251 | 307 | reference_type=BinarySourceReferenceType.BUILT_USING) | 307 | reference_type=BinarySourceReferenceType.BUILT_USING) |
252 | 308 | for bsr in [bsrs[0], bsrs[2]]))) | 308 | for bsr in [bsrs[0], bsrs[2]]))) |
253 | 309 | |||
254 | 310 | def test_findMissingSources(self): | ||
255 | 311 | # findMissingSources finds references whose source publications | ||
256 | 312 | # aren't present in the given publishing context. | ||
257 | 313 | archive = self.factory.makeArchive() | ||
258 | 314 | distroseries = archive.distribution.currentseries | ||
259 | 315 | pockets = ( | ||
260 | 316 | PackagePublishingPocket.RELEASE, PackagePublishingPocket.PROPOSED) | ||
261 | 317 | spphs = [ | ||
262 | 318 | self.factory.makeSourcePackagePublishingHistory( | ||
263 | 319 | archive=archive, distroseries=distroseries, pocket=pocket) | ||
264 | 320 | for pocket in pockets] | ||
265 | 321 | bprs = [] | ||
266 | 322 | for spph in spphs: | ||
267 | 323 | build = self.factory.makeBinaryPackageBuild( | ||
268 | 324 | distroarchseries=self.factory.makeDistroArchSeries( | ||
269 | 325 | distroseries=spph.distroseries), | ||
270 | 326 | archive=spph.archive, pocket=spph.pocket) | ||
271 | 327 | bprs.append(self.factory.makeBinaryPackageRelease(build=build)) | ||
272 | 328 | for bpr, spph in zip(bprs, spphs): | ||
273 | 329 | self.reference_set.createFromSourcePackageReleases( | ||
274 | 330 | bpr, [spph.sourcepackagerelease], | ||
275 | 331 | BinarySourceReferenceType.BUILT_USING) | ||
276 | 332 | self.assertTrue( | ||
277 | 333 | self.reference_set.findMissingSources( | ||
278 | 334 | archive, distroseries, pockets, | ||
279 | 335 | BinarySourceReferenceType.BUILT_USING, bprs).is_empty()) | ||
280 | 336 | # Try searching with slight mismatches; findMissingSources should | ||
281 | 337 | # return appropriate results since the necessary SPPHs aren't | ||
282 | 338 | # present. | ||
283 | 339 | self.assertThat( | ||
284 | 340 | self.reference_set.findMissingSources( | ||
285 | 341 | self.factory.makeArchive(), distroseries, pockets, | ||
286 | 342 | BinarySourceReferenceType.BUILT_USING, bprs), | ||
287 | 343 | MatchesSetwise(*( | ||
288 | 344 | MatchesStructure.byEquality( | ||
289 | 345 | binary_package_release=bpr, | ||
290 | 346 | source_package_release=spph.sourcepackagerelease, | ||
291 | 347 | reference_type=BinarySourceReferenceType.BUILT_USING) | ||
292 | 348 | for bpr, spph in zip(bprs, spphs)))) | ||
293 | 349 | self.assertThat( | ||
294 | 350 | self.reference_set.findMissingSources( | ||
295 | 351 | archive, self.factory.makeDistroSeries(), pockets, | ||
296 | 352 | BinarySourceReferenceType.BUILT_USING, bprs), | ||
297 | 353 | MatchesSetwise(*( | ||
298 | 354 | MatchesStructure.byEquality( | ||
299 | 355 | binary_package_release=bpr, | ||
300 | 356 | source_package_release=spph.sourcepackagerelease, | ||
301 | 357 | reference_type=BinarySourceReferenceType.BUILT_USING) | ||
302 | 358 | for bpr, spph in zip(bprs, spphs)))) | ||
303 | 359 | self.assertThat( | ||
304 | 360 | self.reference_set.findMissingSources( | ||
305 | 361 | archive, distroseries, [PackagePublishingPocket.PROPOSED], | ||
306 | 362 | BinarySourceReferenceType.BUILT_USING, bprs), | ||
307 | 363 | MatchesSetwise( | ||
308 | 364 | MatchesStructure.byEquality( | ||
309 | 365 | binary_package_release=bprs[0], | ||
310 | 366 | source_package_release=spphs[0].sourcepackagerelease, | ||
311 | 367 | reference_type=BinarySourceReferenceType.BUILT_USING))) |
Looks good.