Merge lp:~cjwatson/launchpad/snap-build-behaviour into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17677
Proposed branch: lp:~cjwatson/launchpad/snap-build-behaviour
Merge into: lp:launchpad
Diff against target: 648 lines (+457/-40)
10 files modified
database/schema/security.cfg (+2/-0)
lib/lp/code/model/recipebuilder.py (+4/-26)
lib/lp/code/model/tests/test_recipebuilder.py (+3/-3)
lib/lp/services/config/schema-lazr.conf (+7/-0)
lib/lp/snappy/configure.zcml (+7/-0)
lib/lp/snappy/model/snapbuildbehaviour.py (+110/-0)
lib/lp/snappy/tests/test_snapbuildbehaviour.py (+242/-0)
lib/lp/soyuz/adapters/archivedependencies.py (+36/-11)
lib/lp/soyuz/doc/archive-dependencies.txt (+44/-0)
lib/lp/soyuz/interfaces/archive.py (+2/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-build-behaviour
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+266736@code.launchpad.net

Commit message

Add a build behaviour for snap packages.

Description of the change

Add a build behaviour for snap packages.

This involves adding support for a build tools archive which can be set in a config variable for individual build behaviours, since snapcraft is currently only available in a PPA. We should eventually make this more fine-grained, but that isn't urgent.

You may want to review this alongside https://code.launchpad.net/~cjwatson/launchpad-buildd/snapcraft/+merge/266541, which implements the slave side of the builder protocol.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
William Grant (wgrant) wrote :

What happens if the Git repository or Bazaar branch is deleted? I think it'll raise an AssertionError and be failure-counted, but we should check.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2015-07-30 12:43:33 +0000
3+++ database/schema/security.cfg 2015-08-05 12:26:41 +0000
4@@ -967,6 +967,8 @@
5 public.distroseriesparent = SELECT
6 public.emailaddress = SELECT
7 public.flatpackagesetinclusion = SELECT
8+public.gitref = SELECT
9+public.gitrepository = SELECT
10 public.gpgkey = SELECT
11 public.libraryfilealias = SELECT, INSERT
12 public.libraryfilecontent = SELECT, INSERT
13
14=== modified file 'lib/lp/code/model/recipebuilder.py'
15--- lib/lp/code/model/recipebuilder.py 2015-07-09 12:18:51 +0000
16+++ lib/lp/code/model/recipebuilder.py 2015-08-05 12:26:41 +0000
17@@ -8,8 +8,6 @@
18 'RecipeBuildBehaviour',
19 ]
20
21-import traceback
22-
23 from zope.component import adapter
24 from zope.interface import implementer
25 from zope.security.proxy import removeSecurityProxy
26@@ -67,31 +65,11 @@
27 args["ogrecomponent"] = get_primary_current_component(
28 self.build.archive, self.build.distroseries,
29 None)
30- args['archives'] = get_sources_list_for_building(self.build,
31- distroarchseries, None)
32+ args['archives'] = get_sources_list_for_building(
33+ self.build, distroarchseries, None,
34+ tools_source=config.builddmaster.bzr_builder_sources_list,
35+ logger=logger)
36 args['archive_private'] = self.build.archive.private
37-
38- # config.builddmaster.bzr_builder_sources_list can contain a
39- # sources.list entry for an archive that will contain a
40- # bzr-builder package that needs to be used to build this
41- # recipe.
42- try:
43- extra_archive = config.builddmaster.bzr_builder_sources_list
44- except AttributeError:
45- extra_archive = None
46-
47- if extra_archive is not None:
48- try:
49- sources_line = extra_archive % (
50- {'series': self.build.distroseries.name})
51- args['archives'].append(sources_line)
52- except StandardError:
53- # Someone messed up the config, don't add it.
54- if logger:
55- logger.error(
56- "Exception processing bzr_builder_sources_list:\n%s"
57- % traceback.format_exc())
58-
59 args['distroseries_name'] = self.build.distroseries.name
60 return args
61
62
63=== modified file 'lib/lp/code/model/tests/test_recipebuilder.py'
64--- lib/lp/code/model/tests/test_recipebuilder.py 2015-04-20 09:48:57 +0000
65+++ lib/lp/code/model/tests/test_recipebuilder.py 2015-08-05 12:26:41 +0000
66@@ -137,8 +137,8 @@
67 distroarchseries = job.build.distroseries.architectures[0]
68 expected_archives = get_sources_list_for_building(
69 job.build, distroarchseries, None)
70- expected_archives.append(
71- "deb http://foo %s main" % job.build.distroseries.name)
72+ expected_archives.insert(
73+ 0, "deb http://foo %s main" % job.build.distroseries.name)
74 self.assertEqual({
75 'archive_private': False,
76 'arch_tag': 'i386',
77@@ -238,7 +238,7 @@
78 'distroseries_name': job.build.distroseries.name,
79 }, job._extraBuildArgs(distroarchseries, logger))
80 self.assertIn(
81- "Exception processing bzr_builder_sources_list:",
82+ "Exception processing build tools sources.list entry:",
83 logger.getLogBuffer())
84
85 def test_extraBuildArgs_withNoBZrBuilderConfigSet(self):
86
87=== modified file 'lib/lp/services/config/schema-lazr.conf'
88--- lib/lp/services/config/schema-lazr.conf 2015-08-03 08:18:09 +0000
89+++ lib/lp/services/config/schema-lazr.conf 2015-08-05 12:26:41 +0000
90@@ -1722,6 +1722,13 @@
91 # credentials, so the proxy needs to be very carefully secured.
92 http_proxy: none
93
94+[snappy]
95+# Optional sources.list entry to send to build slaves when doing snap
96+# package builds. Use this form so that the series is set:
97+# deb http://foo %(series)s main
98+# datatype: string
99+tools_source: none
100+
101 [process-job-source-groups]
102 # This section is used by cronscripts/process-job-source-groups.py.
103 dbuser: process-job-source-groups
104
105=== modified file 'lib/lp/snappy/configure.zcml'
106--- lib/lp/snappy/configure.zcml 2015-07-30 15:19:51 +0000
107+++ lib/lp/snappy/configure.zcml 2015-08-05 12:26:41 +0000
108@@ -66,6 +66,13 @@
109 <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" />
110 </securedutility>
111
112+ <!-- SnapBuildBehaviour -->
113+ <adapter
114+ for="lp.snappy.interfaces.snapbuild.ISnapBuild"
115+ provides="lp.buildmaster.interfaces.buildfarmjobbehaviour.IBuildFarmJobBehaviour"
116+ factory="lp.snappy.model.snapbuildbehaviour.SnapBuildBehaviour"
117+ permission="zope.Public" />
118+
119 <!-- SnapFile -->
120 <class class="lp.snappy.model.snapbuild.SnapFile">
121 <allow interface="lp.snappy.interfaces.snapbuild.ISnapFile" />
122
123=== added file 'lib/lp/snappy/model/snapbuildbehaviour.py'
124--- lib/lp/snappy/model/snapbuildbehaviour.py 1970-01-01 00:00:00 +0000
125+++ lib/lp/snappy/model/snapbuildbehaviour.py 2015-08-05 12:26:41 +0000
126@@ -0,0 +1,110 @@
127+# Copyright 2015 Canonical Ltd. This software is licensed under the
128+# GNU Affero General Public License version 3 (see the file LICENSE).
129+
130+"""An `IBuildFarmJobBehaviour` for `SnapBuild`.
131+
132+Dispatches snap package build jobs to build-farm slaves.
133+"""
134+
135+__metaclass__ = type
136+__all__ = [
137+ 'SnapBuildBehaviour',
138+ ]
139+
140+from zope.component import adapter
141+from zope.interface import implementer
142+
143+from lp.buildmaster.interfaces.builder import CannotBuild
144+from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
145+ IBuildFarmJobBehaviour,
146+ )
147+from lp.buildmaster.model.buildfarmjobbehaviour import (
148+ BuildFarmJobBehaviourBase,
149+ )
150+from lp.registry.interfaces.series import SeriesStatus
151+from lp.services.config import config
152+from lp.snappy.interfaces.snap import SnapBuildArchiveOwnerMismatch
153+from lp.snappy.interfaces.snapbuild import ISnapBuild
154+from lp.soyuz.adapters.archivedependencies import (
155+ get_sources_list_for_building,
156+ )
157+from lp.soyuz.interfaces.archive import ArchiveDisabled
158+
159+
160+@adapter(ISnapBuild)
161+@implementer(IBuildFarmJobBehaviour)
162+class SnapBuildBehaviour(BuildFarmJobBehaviourBase):
163+ """Dispatches `SnapBuild` jobs to slaves."""
164+
165+ def getLogFileName(self):
166+ das = self.build.distro_arch_series
167+
168+ # Examples:
169+ # buildlog_snap_ubuntu_wily_amd64_name_FULLYBUILT.txt
170+ return 'buildlog_snap_%s_%s_%s_%s_%s.txt' % (
171+ das.distroseries.distribution.name, das.distroseries.name,
172+ das.architecturetag, self.build.snap.name, self.build.status.name)
173+
174+ def verifyBuildRequest(self, logger):
175+ """Assert some pre-build checks.
176+
177+ The build request is checked:
178+ * Virtualized builds can't build on a non-virtual builder
179+ * The source archive may not be disabled
180+ * If the source archive is private, the snap owner must match the
181+ archive owner (see `SnapBuildArchiveOwnerMismatch` docstring)
182+ * Ensure that we have a chroot
183+ """
184+ build = self.build
185+ if build.virtualized and not self._builder.virtualized:
186+ raise AssertionError(
187+ "Attempt to build virtual item on a non-virtual builder.")
188+
189+ if not build.archive.enabled:
190+ raise ArchiveDisabled(build.archive.displayname)
191+ if build.archive.private and build.snap.owner != build.archive.owner:
192+ raise SnapBuildArchiveOwnerMismatch()
193+
194+ chroot = build.distro_arch_series.getChroot()
195+ if chroot is None:
196+ raise CannotBuild(
197+ "Missing chroot for %s" % build.distro_arch_series.displayname)
198+
199+ def _extraBuildArgs(self, logger=None):
200+ """
201+ Return the extra arguments required by the slave for the given build.
202+ """
203+ build = self.build
204+ args = {}
205+ args["name"] = build.snap.name
206+ args["arch_tag"] = build.distro_arch_series.architecturetag
207+ # XXX cjwatson 2015-08-03: Allow tools_source to be overridden at
208+ # some more fine-grained level.
209+ args["archives"] = get_sources_list_for_building(
210+ build, build.distro_arch_series, None,
211+ tools_source=config.snappy.tools_source, logger=logger)
212+ args["archive_private"] = build.archive.private
213+ if build.snap.branch is not None:
214+ args["branch"] = build.snap.branch.bzr_identity
215+ elif build.snap.git_repository is not None:
216+ args["git_repository"] = build.snap.git_repository.git_https_url
217+ args["git_path"] = build.snap.git_path
218+ else:
219+ raise CannotBuild(
220+ "Source branch/repository for ~%s/%s has been deleted." %
221+ (build.snap.owner.name, build.snap.name))
222+ return args
223+
224+ def composeBuildRequest(self, logger):
225+ return (
226+ "snap", self.build.distro_arch_series, {},
227+ self._extraBuildArgs(logger=logger))
228+
229+ def verifySuccessfulBuild(self):
230+ """See `IBuildFarmJobBehaviour`."""
231+ # The implementation in BuildFarmJobBehaviourBase checks whether the
232+ # target suite is modifiable in the target archive. However, a
233+ # `SnapBuild`'s archive is a source rather than a target, so that
234+ # check does not make sense. We do, however, refuse to build for
235+ # obsolete series.
236+ assert self.build.distro_series.status != SeriesStatus.OBSOLETE
237
238=== added file 'lib/lp/snappy/tests/test_snapbuildbehaviour.py'
239--- lib/lp/snappy/tests/test_snapbuildbehaviour.py 1970-01-01 00:00:00 +0000
240+++ lib/lp/snappy/tests/test_snapbuildbehaviour.py 2015-08-05 12:26:41 +0000
241@@ -0,0 +1,242 @@
242+# Copyright 2015 Canonical Ltd. This software is licensed under the
243+# GNU Affero General Public License version 3 (see the file LICENSE).
244+
245+"""Test snap package build behaviour."""
246+
247+__metaclass__ = type
248+
249+import fixtures
250+import transaction
251+from twisted.trial.unittest import TestCase as TrialTestCase
252+from zope.component import getUtility
253+
254+from lp.buildmaster.enums import BuildStatus
255+from lp.buildmaster.interfaces.builder import CannotBuild
256+from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
257+ IBuildFarmJobBehaviour,
258+ )
259+from lp.buildmaster.interfaces.processor import IProcessorSet
260+from lp.buildmaster.tests.mock_slaves import (
261+ MockBuilder,
262+ OkSlave,
263+ )
264+from lp.buildmaster.tests.test_buildfarmjobbehaviour import (
265+ TestGetUploadMethodsMixin,
266+ TestHandleStatusMixin,
267+ TestVerifySuccessfulBuildMixin,
268+ )
269+from lp.registry.interfaces.pocket import PackagePublishingPocket
270+from lp.registry.interfaces.series import SeriesStatus
271+from lp.services.features.testing import FeatureFixture
272+from lp.services.log.logger import BufferLogger
273+from lp.snappy.interfaces.snap import (
274+ SNAP_FEATURE_FLAG,
275+ SnapBuildArchiveOwnerMismatch,
276+ )
277+from lp.snappy.model.snapbuildbehaviour import SnapBuildBehaviour
278+from lp.soyuz.adapters.archivedependencies import (
279+ get_sources_list_for_building,
280+ )
281+from lp.soyuz.interfaces.archive import ArchiveDisabled
282+from lp.testing import TestCaseWithFactory
283+from lp.testing.layers import LaunchpadZopelessLayer
284+
285+
286+class TestSnapBuildBehaviour(TestCaseWithFactory):
287+
288+ layer = LaunchpadZopelessLayer
289+
290+ def setUp(self):
291+ super(TestSnapBuildBehaviour, self).setUp()
292+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
293+
294+ def makeJob(self, pocket=PackagePublishingPocket.RELEASE, **kwargs):
295+ """Create a sample `ISnapBuildBehaviour`."""
296+ distribution = self.factory.makeDistribution(name="distro")
297+ distroseries = self.factory.makeDistroSeries(
298+ distribution=distribution, name="unstable")
299+ processor = getUtility(IProcessorSet).getByName("386")
300+ distroarchseries = self.factory.makeDistroArchSeries(
301+ distroseries=distroseries, architecturetag="i386",
302+ processor=processor)
303+ build = self.factory.makeSnapBuild(
304+ distroarchseries=distroarchseries, pocket=pocket,
305+ name=u"test-snap", **kwargs)
306+ return IBuildFarmJobBehaviour(build)
307+
308+ def test_provides_interface(self):
309+ # SnapBuildBehaviour provides IBuildFarmJobBehaviour.
310+ job = SnapBuildBehaviour(None)
311+ self.assertProvides(job, IBuildFarmJobBehaviour)
312+
313+ def test_adapts_ISnapBuild(self):
314+ # IBuildFarmJobBehaviour adapts an ISnapBuild.
315+ build = self.factory.makeSnapBuild()
316+ job = IBuildFarmJobBehaviour(build)
317+ self.assertProvides(job, IBuildFarmJobBehaviour)
318+
319+ def test_verifyBuildRequest_valid(self):
320+ # verifyBuildRequest doesn't raise any exceptions when called with a
321+ # valid builder set.
322+ job = self.makeJob()
323+ lfa = self.factory.makeLibraryFileAlias()
324+ transaction.commit()
325+ job.build.distro_arch_series.addOrUpdateChroot(lfa)
326+ builder = MockBuilder()
327+ job.setBuilder(builder, OkSlave())
328+ logger = BufferLogger()
329+ job.verifyBuildRequest(logger)
330+ self.assertEqual("", logger.getLogBuffer())
331+
332+ def test_verifyBuildRequest_virtual_mismatch(self):
333+ # verifyBuildRequest raises on an attempt to build a virtualized
334+ # build on a non-virtual builder.
335+ job = self.makeJob()
336+ lfa = self.factory.makeLibraryFileAlias()
337+ transaction.commit()
338+ job.build.distro_arch_series.addOrUpdateChroot(lfa)
339+ builder = MockBuilder(virtualized=False)
340+ job.setBuilder(builder, OkSlave())
341+ logger = BufferLogger()
342+ e = self.assertRaises(AssertionError, job.verifyBuildRequest, logger)
343+ self.assertEqual(
344+ "Attempt to build virtual item on a non-virtual builder.", str(e))
345+
346+ def test_verifyBuildRequest_archive_disabled(self):
347+ archive = self.factory.makeArchive(
348+ enabled=False, displayname="Disabled Archive")
349+ job = self.makeJob(archive=archive)
350+ lfa = self.factory.makeLibraryFileAlias()
351+ transaction.commit()
352+ job.build.distro_arch_series.addOrUpdateChroot(lfa)
353+ builder = MockBuilder()
354+ job.setBuilder(builder, OkSlave())
355+ logger = BufferLogger()
356+ e = self.assertRaises(ArchiveDisabled, job.verifyBuildRequest, logger)
357+ self.assertEqual("Disabled Archive is disabled.", str(e))
358+
359+ def test_verifyBuildRequest_archive_private_owners_match(self):
360+ archive = self.factory.makeArchive(private=True)
361+ job = self.makeJob(
362+ archive=archive, registrant=archive.owner, owner=archive.owner)
363+ lfa = self.factory.makeLibraryFileAlias()
364+ transaction.commit()
365+ job.build.distro_arch_series.addOrUpdateChroot(lfa)
366+ builder = MockBuilder()
367+ job.setBuilder(builder, OkSlave())
368+ logger = BufferLogger()
369+ job.verifyBuildRequest(logger)
370+ self.assertEqual("", logger.getLogBuffer())
371+
372+ def test_verifyBuildRequest_archive_private_owners_mismatch(self):
373+ archive = self.factory.makeArchive(private=True)
374+ job = self.makeJob(archive=archive)
375+ lfa = self.factory.makeLibraryFileAlias()
376+ transaction.commit()
377+ job.build.distro_arch_series.addOrUpdateChroot(lfa)
378+ builder = MockBuilder()
379+ job.setBuilder(builder, OkSlave())
380+ logger = BufferLogger()
381+ e = self.assertRaises(
382+ SnapBuildArchiveOwnerMismatch, job.verifyBuildRequest, logger)
383+ self.assertEqual(
384+ "Snap package builds against private archives are only allowed "
385+ "if the snap package owner and the archive owner are equal.",
386+ str(e))
387+
388+ def test_verifyBuildRequest_no_chroot(self):
389+ # verifyBuildRequest raises when the DAS has no chroot.
390+ job = self.makeJob()
391+ builder = MockBuilder()
392+ job.setBuilder(builder, OkSlave())
393+ logger = BufferLogger()
394+ e = self.assertRaises(CannotBuild, job.verifyBuildRequest, logger)
395+ self.assertIn("Missing chroot", str(e))
396+
397+ def test_extraBuildArgs_bzr(self):
398+ # _extraBuildArgs returns appropriate arguments if asked to build a
399+ # job for a Bazaar branch.
400+ branch = self.factory.makeBranch()
401+ job = self.makeJob(branch=branch)
402+ expected_archives = get_sources_list_for_building(
403+ job.build, job.build.distro_arch_series, None)
404+ self.assertEqual({
405+ "archive_private": False,
406+ "archives": expected_archives,
407+ "arch_tag": "i386",
408+ "branch": branch.bzr_identity,
409+ "name": u"test-snap",
410+ }, job._extraBuildArgs())
411+
412+ def test_extraBuildArgs_git(self):
413+ # _extraBuildArgs returns appropriate arguments if asked to build a
414+ # job for a Git branch.
415+ [ref] = self.factory.makeGitRefs()
416+ job = self.makeJob(git_ref=ref)
417+ expected_archives = get_sources_list_for_building(
418+ job.build, job.build.distro_arch_series, None)
419+ self.assertEqual({
420+ "archive_private": False,
421+ "archives": expected_archives,
422+ "arch_tag": "i386",
423+ "git_repository": ref.repository.git_https_url,
424+ "git_path": ref.path,
425+ "name": u"test-snap",
426+ }, job._extraBuildArgs())
427+
428+ def test_composeBuildRequest(self):
429+ job = self.makeJob()
430+ lfa = self.factory.makeLibraryFileAlias(db_only=True)
431+ job.build.distro_arch_series.addOrUpdateChroot(lfa)
432+ self.assertEqual(
433+ ('snap', job.build.distro_arch_series, {},
434+ job._extraBuildArgs()),
435+ job.composeBuildRequest(None))
436+
437+ def test_composeBuildRequest_deleted(self):
438+ # If the source branch/repository has been deleted,
439+ # composeBuildRequest raises CannotBuild.
440+ branch = self.factory.makeBranch()
441+ owner = self.factory.makePerson(name="snap-owner")
442+ job = self.makeJob(registrant=owner, owner=owner, branch=branch)
443+ branch.destroySelf(break_references=True)
444+ self.assertIsNone(job.build.snap.branch)
445+ self.assertIsNone(job.build.snap.git_repository)
446+ self.assertRaisesWithContent(
447+ CannotBuild,
448+ "Source branch/repository for ~snap-owner/test-snap has been "
449+ "deleted.",
450+ job.composeBuildRequest, None)
451+
452+
453+class MakeSnapBuildMixin:
454+ """Provide the common makeBuild method returning a queued build."""
455+
456+ def makeBuild(self):
457+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
458+ build = self.factory.makeSnapBuild(status=BuildStatus.BUILDING)
459+ build.queueBuild()
460+ return build
461+
462+ def makeUnmodifiableBuild(self):
463+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
464+ build = self.factory.makeSnapBuild(status=BuildStatus.BUILDING)
465+ build.distro_series.status = SeriesStatus.OBSOLETE
466+ build.queueBuild()
467+ return build
468+
469+
470+class TestGetUploadMethodsForSnapBuild(
471+ MakeSnapBuildMixin, TestGetUploadMethodsMixin, TestCaseWithFactory):
472+ """IPackageBuild.getUpload-related methods work with Snap builds."""
473+
474+
475+class TestVerifySuccessfulBuildForSnapBuild(
476+ MakeSnapBuildMixin, TestVerifySuccessfulBuildMixin, TestCaseWithFactory):
477+ """IBuildFarmJobBehaviour.verifySuccessfulBuild works."""
478+
479+
480+class TestHandleStatusForSnapBuild(
481+ MakeSnapBuildMixin, TestHandleStatusMixin, TrialTestCase,
482+ fixtures.TestWithFixtures):
483+ """IPackageBuild.handleStatus works with Snap builds."""
484
485=== modified file 'lib/lp/soyuz/adapters/archivedependencies.py'
486--- lib/lp/soyuz/adapters/archivedependencies.py 2014-12-11 21:24:19 +0000
487+++ lib/lp/soyuz/adapters/archivedependencies.py 2015-08-05 12:26:41 +0000
488@@ -133,7 +133,7 @@
489
490
491 def expand_dependencies(archive, distro_arch_series, pocket, component,
492- source_package_name):
493+ source_package_name, tools_source=None, logger=None):
494 """Return the set of dependency archives, pockets and components.
495
496 :param archive: the context `IArchive`.
497@@ -141,6 +141,10 @@
498 :param pocket: the context `PackagePublishingPocket`.
499 :param component: the context `IComponent`.
500 :param source_package_name: A source package name (as text)
501+ :param tools_source: if not None, a sources.list entry to use as an
502+ additional dependency for build tools, just before the default
503+ primary archive.
504+ :param logger: an optional logger.
505 :return: a list of (archive, distro_arch_series, pocket, [component]),
506 representing the dependencies defined by the given build context.
507 """
508@@ -172,6 +176,17 @@
509 (archive_dependency.dependency, distro_arch_series, pocket,
510 components))
511
512+ # Consider build tools archive dependencies.
513+ if tools_source is not None:
514+ try:
515+ deps.append(tools_source % {'series': distro_series.name})
516+ except Exception:
517+ # Someone messed up the configuration; don't add it.
518+ if logger is not None:
519+ logger.error(
520+ "Exception processing build tools sources.list entry:\n%s"
521+ % traceback.format_exc())
522+
523 # Consider primary archive dependency override. Add the default
524 # primary archive dependencies if it's not present.
525 if archive.getArchiveDependency(
526@@ -201,7 +216,8 @@
527 return deps
528
529
530-def get_sources_list_for_building(build, distroarchseries, sourcepackagename):
531+def get_sources_list_for_building(build, distroarchseries, sourcepackagename,
532+ tools_source=None, logger=None):
533 """Return the sources_list entries required to build the given item.
534
535 The entries are returned in the order that is most useful;
536@@ -213,11 +229,16 @@
537 :param build: a context `IBuild`.
538 :param distroarchseries: A `IDistroArchSeries`
539 :param sourcepackagename: A source package name (as text)
540+ :param tools_source: if not None, a sources.list entry to use as an
541+ additional dependency for build tools, just before the default
542+ primary archive.
543+ :param logger: an optional logger.
544 :return: a deb sources_list entries (lines).
545 """
546 deps = expand_dependencies(
547 build.archive, distroarchseries, build.pocket,
548- build.current_component, sourcepackagename)
549+ build.current_component, sourcepackagename,
550+ tools_source=tools_source, logger=logger)
551 sources_list_lines = \
552 _get_sources_list_for_dependencies(deps)
553
554@@ -296,14 +317,18 @@
555 :return: a list of sources_list formatted lines.
556 """
557 sources_list_lines = []
558- for archive, distro_arch_series, pocket, components in dependencies:
559- has_published_binaries = _has_published_binaries(
560- archive, distro_arch_series, pocket)
561- if not has_published_binaries:
562- continue
563- sources_list_line = _get_binary_sources_list_line(
564- archive, distro_arch_series, pocket, components)
565- sources_list_lines.append(sources_list_line)
566+ for dep in dependencies:
567+ if isinstance(dep, basestring):
568+ sources_list_lines.append(dep)
569+ else:
570+ archive, distro_arch_series, pocket, components = dep
571+ has_published_binaries = _has_published_binaries(
572+ archive, distro_arch_series, pocket)
573+ if not has_published_binaries:
574+ continue
575+ sources_list_line = _get_binary_sources_list_line(
576+ archive, distro_arch_series, pocket, components)
577+ sources_list_lines.append(sources_list_line)
578
579 return sources_list_lines
580
581
582=== modified file 'lib/lp/soyuz/doc/archive-dependencies.txt'
583--- lib/lp/soyuz/doc/archive-dependencies.txt 2014-12-11 21:52:15 +0000
584+++ lib/lp/soyuz/doc/archive-dependencies.txt 2015-08-05 12:26:41 +0000
585@@ -499,6 +499,50 @@
586 deb http://archive.launchpad.dev/ubuntu hoary-updates
587 main restricted universe multiverse
588
589+ >>> cprov.archive.external_dependencies = None
590+
591+
592+== Build tools sources.list entries ==
593+
594+We can force an extra build tools line to be added to the sources.list,
595+which is useful for specialised build types.
596+
597+ >>> for line in get_sources_list_for_building(
598+ ... a_build, a_build.distro_arch_series,
599+ ... a_build.source_package_release.name,
600+ ... tools_source="deb http://example.org %(series)s main"):
601+ ... print line
602+ deb http://ppa.launchpad.dev/cprov/ppa/ubuntu hoary main
603+ deb http://example.org hoary main
604+ deb http://archive.launchpad.dev/ubuntu hoary
605+ main restricted universe multiverse
606+ deb http://archive.launchpad.dev/ubuntu hoary-security
607+ main restricted universe multiverse
608+ deb http://archive.launchpad.dev/ubuntu hoary-updates
609+ main restricted universe multiverse
610+
611+If tools_source is badly formatted, we log the error but don't blow up.
612+(Note the missing "s" at the end of "%(series)".)
613+
614+ >>> from lp.services.log.logger import BufferLogger
615+ >>> logger = BufferLogger()
616+ >>> for line in get_sources_list_for_building(
617+ ... a_build, a_build.distro_arch_series,
618+ ... a_build.source_package_release.name,
619+ ... tools_source="deb http://example.org %(series) main",
620+ ... logger=logger):
621+ ... print line
622+ deb http://ppa.launchpad.dev/cprov/ppa/ubuntu hoary main
623+ deb http://archive.launchpad.dev/ubuntu hoary
624+ main restricted universe multiverse
625+ deb http://archive.launchpad.dev/ubuntu hoary-security
626+ main restricted universe multiverse
627+ deb http://archive.launchpad.dev/ubuntu hoary-updates
628+ main restricted universe multiverse
629+ >>> print logger.getLogBuffer()
630+ ERROR Exception processing build tools sources.list entry:
631+ ...
632+
633
634 == Overlays ==
635
636
637=== modified file 'lib/lp/soyuz/interfaces/archive.py'
638--- lib/lp/soyuz/interfaces/archive.py 2015-05-18 06:36:51 +0000
639+++ lib/lp/soyuz/interfaces/archive.py 2015-08-05 12:26:41 +0000
640@@ -2290,6 +2290,8 @@
641 :param ext_deps: The dependencies form field to check.
642 """
643 errors = []
644+ if ext_deps is None:
645+ return errors
646 # The field can consist of multiple entries separated by
647 # newlines, so process each in turn.
648 for dep in ext_deps.splitlines():