← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-build-behaviour into lp:launchpad.

Commit message:
Add a build behaviour for snap packages.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1476405 in Launchpad itself: "Add support for building snaps"
  https://bugs.launchpad.net/launchpad/+bug/1476405

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-build-behaviour/+merge/266736

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.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-build-behaviour into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2015-07-30 12:43:33 +0000
+++ database/schema/security.cfg	2015-08-03 14:55:59 +0000
@@ -967,6 +967,8 @@
 public.distroseriesparent                     = SELECT
 public.emailaddress                           = SELECT
 public.flatpackagesetinclusion                = SELECT
+public.gitref                                 = SELECT
+public.gitrepository                          = SELECT
 public.gpgkey                                 = SELECT
 public.libraryfilealias                       = SELECT, INSERT
 public.libraryfilecontent                     = SELECT, INSERT

=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf	2015-08-03 08:18:09 +0000
+++ lib/lp/services/config/schema-lazr.conf	2015-08-03 14:55:59 +0000
@@ -1722,6 +1722,9 @@
 # credentials, so the proxy needs to be very carefully secured.
 http_proxy: none
 
+[snappy]
+tools_archive: none
+
 [process-job-source-groups]
 # This section is used by cronscripts/process-job-source-groups.py.
 dbuser: process-job-source-groups

=== modified file 'lib/lp/snappy/configure.zcml'
--- lib/lp/snappy/configure.zcml	2015-07-30 15:19:51 +0000
+++ lib/lp/snappy/configure.zcml	2015-08-03 14:55:59 +0000
@@ -66,6 +66,13 @@
         <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" />
     </securedutility>
 
+    <!-- SnapBuildBehaviour -->
+    <adapter
+        for="lp.snappy.interfaces.snapbuild.ISnapBuild"
+        provides="lp.buildmaster.interfaces.buildfarmjobbehaviour.IBuildFarmJobBehaviour"
+        factory="lp.snappy.model.snapbuildbehaviour.SnapBuildBehaviour"
+        permission="zope.Public" />
+
     <!-- SnapFile -->
     <class class="lp.snappy.model.snapbuild.SnapFile">
         <allow interface="lp.snappy.interfaces.snapbuild.ISnapFile" />

=== added file 'lib/lp/snappy/model/snapbuildbehaviour.py'
--- lib/lp/snappy/model/snapbuildbehaviour.py	1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/model/snapbuildbehaviour.py	2015-08-03 14:55:59 +0000
@@ -0,0 +1,117 @@
+# Copyright 2015 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""An `IBuildFarmJobBehaviour` for `SnapBuild`.
+
+Dispatches snap package build jobs to build-farm slaves.
+"""
+
+__metaclass__ = type
+__all__ = [
+    'SnapBuildBehaviour',
+    ]
+
+from zope.component import (
+    adapter,
+    getUtility,
+    )
+from zope.interface import implementer
+
+from lp.buildmaster.interfaces.builder import CannotBuild
+from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
+    IBuildFarmJobBehaviour,
+    )
+from lp.buildmaster.model.buildfarmjobbehaviour import (
+    BuildFarmJobBehaviourBase,
+    )
+from lp.registry.interfaces.series import SeriesStatus
+from lp.services.config import config
+from lp.snappy.interfaces.snap import SnapBuildArchiveOwnerMismatch
+from lp.snappy.interfaces.snapbuild import ISnapBuild
+from lp.soyuz.adapters.archivedependencies import (
+    get_sources_list_for_building,
+    )
+from lp.soyuz.interfaces.archive import (
+    ArchiveDisabled,
+    IArchiveSet,
+    )
+
+
+@adapter(ISnapBuild)
+@implementer(IBuildFarmJobBehaviour)
+class SnapBuildBehaviour(BuildFarmJobBehaviourBase):
+    """Dispatches `SnapBuild` jobs to slaves."""
+
+    def getLogFileName(self):
+        das = self.build.distro_arch_series
+
+        # Examples:
+        #   buildlog_ubuntu_wily_amd64_name_FULLYBUILT.txt
+        return 'buildlog_%s_%s_%s_%s_%s.txt' % (
+            das.distroseries.distribution.name, das.distroseries.name,
+            das.architecturetag, self.build.snap.name, self.build.status.name)
+
+    def verifyBuildRequest(self, logger):
+        """Assert some pre-build checks.
+
+        The build request is checked:
+         * Virtualized builds can't build on a non-virtual builder
+         * The source archive may not be disabled
+         * If the source archive is private, the snap owner must match the
+           archive owner (see `SnapBuildArchiveOwnerMismatch` docstring)
+         * Ensure that we have a chroot
+        """
+        build = self.build
+        if build.virtualized and not self._builder.virtualized:
+            raise AssertionError(
+                "Attempt to build virtual item on a non-virtual builder.")
+
+        if not build.archive.enabled:
+            raise ArchiveDisabled(build.archive.displayname)
+        if build.archive.private and build.snap.owner != build.archive.owner:
+            raise SnapBuildArchiveOwnerMismatch()
+
+        chroot = build.distro_arch_series.getChroot()
+        if chroot is None:
+            raise CannotBuild(
+                "Missing chroot for %s" % build.distro_arch_series.displayname)
+
+    def _extraBuildArgs(self):
+        """
+        Return the extra arguments required by the slave for the given build.
+        """
+        build = self.build
+        args = {}
+        args["name"] = build.snap.name
+        args["arch_tag"] = build.distro_arch_series.architecturetag
+        # XXX cjwatson 2015-08-03: Allow this to be overridden at some more
+        # fine-grained level.
+        if config.snappy.tools_archive is not None:
+            tools_archive = getUtility(IArchiveSet).getByReference(
+                config.snappy.tools_archive)
+        else:
+            tools_archive = None
+        args["archives"] = get_sources_list_for_building(
+            build, build.distro_arch_series, None, tools_archive=tools_archive)
+        args["archive_private"] = build.archive.private
+        if build.snap.branch is not None:
+            args["branch"] = build.snap.branch.bzr_identity
+        else:
+            assert build.snap.git_repository is not None
+            args["git_repository"] = build.snap.git_repository.git_https_url
+            args["git_path"] = build.snap.git_path
+        return args
+
+    def composeBuildRequest(self, logger):
+        return (
+            "snap", self.build.distro_arch_series, {},
+            self._extraBuildArgs())
+
+    def verifySuccessfulBuild(self):
+        """See `IBuildFarmJobBehaviour`."""
+        # The implementation in BuildFarmJobBehaviourBase checks whether the
+        # target suite is modifiable in the target archive.  However, a
+        # `SnapBuild`'s archive is a source rather than a target, so that
+        # check does not make sense.  We do, however, refuse to build for
+        # obsolete series.
+        assert self.build.distro_series.status != SeriesStatus.OBSOLETE

=== added file 'lib/lp/snappy/tests/test_snapbuildbehaviour.py'
--- lib/lp/snappy/tests/test_snapbuildbehaviour.py	1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/tests/test_snapbuildbehaviour.py	2015-08-03 14:55:59 +0000
@@ -0,0 +1,227 @@
+# Copyright 2015 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test snap package build behaviour."""
+
+__metaclass__ = type
+
+import fixtures
+import transaction
+from twisted.trial.unittest import TestCase as TrialTestCase
+from zope.component import getUtility
+
+from lp.buildmaster.enums import BuildStatus
+from lp.buildmaster.interfaces.builder import CannotBuild
+from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
+    IBuildFarmJobBehaviour,
+    )
+from lp.buildmaster.interfaces.processor import IProcessorSet
+from lp.buildmaster.tests.mock_slaves import (
+    MockBuilder,
+    OkSlave,
+    )
+from lp.buildmaster.tests.test_buildfarmjobbehaviour import (
+    TestGetUploadMethodsMixin,
+    TestHandleStatusMixin,
+    TestVerifySuccessfulBuildMixin,
+    )
+from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.registry.interfaces.series import SeriesStatus
+from lp.services.features.testing import FeatureFixture
+from lp.services.log.logger import BufferLogger
+from lp.snappy.interfaces.snap import (
+    SNAP_FEATURE_FLAG,
+    SnapBuildArchiveOwnerMismatch,
+    )
+from lp.snappy.model.snapbuildbehaviour import SnapBuildBehaviour
+from lp.soyuz.adapters.archivedependencies import (
+    get_sources_list_for_building,
+    )
+from lp.soyuz.interfaces.archive import ArchiveDisabled
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import LaunchpadZopelessLayer
+
+
+class TestSnapBuildBehaviour(TestCaseWithFactory):
+
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        super(TestSnapBuildBehaviour, self).setUp()
+        self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+
+    def makeJob(self, pocket=PackagePublishingPocket.RELEASE, **kwargs):
+        """Create a sample `ISnapBuildBehaviour`."""
+        distribution = self.factory.makeDistribution(name="distro")
+        distroseries = self.factory.makeDistroSeries(
+            distribution=distribution, name="unstable")
+        processor = getUtility(IProcessorSet).getByName("386")
+        distroarchseries = self.factory.makeDistroArchSeries(
+            distroseries=distroseries, architecturetag="i386",
+            processor=processor)
+        build = self.factory.makeSnapBuild(
+            distroarchseries=distroarchseries, pocket=pocket,
+            name=u"test-snap", **kwargs)
+        return IBuildFarmJobBehaviour(build)
+
+    def test_provides_interface(self):
+        # SnapBuildBehaviour provides IBuildFarmJobBehaviour.
+        job = SnapBuildBehaviour(None)
+        self.assertProvides(job, IBuildFarmJobBehaviour)
+
+    def test_adapts_ISnapBuild(self):
+        # IBuildFarmJobBehaviour adapts an ISnapBuild.
+        build = self.factory.makeSnapBuild()
+        job = IBuildFarmJobBehaviour(build)
+        self.assertProvides(job, IBuildFarmJobBehaviour)
+
+    def test_verifyBuildRequest_valid(self):
+        # verifyBuildRequest doesn't raise any exceptions when called with a
+        # valid builder set.
+        job = self.makeJob()
+        lfa = self.factory.makeLibraryFileAlias()
+        transaction.commit()
+        job.build.distro_arch_series.addOrUpdateChroot(lfa)
+        builder = MockBuilder()
+        job.setBuilder(builder, OkSlave())
+        logger = BufferLogger()
+        job.verifyBuildRequest(logger)
+        self.assertEqual("", logger.getLogBuffer())
+
+    def test_verifyBuildRequest_virtual_mismatch(self):
+        # verifyBuildRequest raises on an attempt to build a virtualized
+        # build on a non-virtual builder.
+        job = self.makeJob()
+        lfa = self.factory.makeLibraryFileAlias()
+        transaction.commit()
+        job.build.distro_arch_series.addOrUpdateChroot(lfa)
+        builder = MockBuilder(virtualized=False)
+        job.setBuilder(builder, OkSlave())
+        logger = BufferLogger()
+        e = self.assertRaises(AssertionError, job.verifyBuildRequest, logger)
+        self.assertEqual(
+            "Attempt to build virtual item on a non-virtual builder.", str(e))
+
+    def test_verifyBuildRequest_archive_disabled(self):
+        archive = self.factory.makeArchive(
+            enabled=False, displayname="Disabled Archive")
+        job = self.makeJob(archive=archive)
+        lfa = self.factory.makeLibraryFileAlias()
+        transaction.commit()
+        job.build.distro_arch_series.addOrUpdateChroot(lfa)
+        builder = MockBuilder()
+        job.setBuilder(builder, OkSlave())
+        logger = BufferLogger()
+        e = self.assertRaises(ArchiveDisabled, job.verifyBuildRequest, logger)
+        self.assertEqual("Disabled Archive is disabled.", str(e))
+
+    def test_verifyBuildRequest_archive_private_owners_match(self):
+        archive = self.factory.makeArchive(private=True)
+        job = self.makeJob(
+            archive=archive, registrant=archive.owner, owner=archive.owner)
+        lfa = self.factory.makeLibraryFileAlias()
+        transaction.commit()
+        job.build.distro_arch_series.addOrUpdateChroot(lfa)
+        builder = MockBuilder()
+        job.setBuilder(builder, OkSlave())
+        logger = BufferLogger()
+        job.verifyBuildRequest(logger)
+        self.assertEqual("", logger.getLogBuffer())
+
+    def test_verifyBuildRequest_archive_private_owners_mismatch(self):
+        archive = self.factory.makeArchive(private=True)
+        job = self.makeJob(archive=archive)
+        lfa = self.factory.makeLibraryFileAlias()
+        transaction.commit()
+        job.build.distro_arch_series.addOrUpdateChroot(lfa)
+        builder = MockBuilder()
+        job.setBuilder(builder, OkSlave())
+        logger = BufferLogger()
+        e = self.assertRaises(
+            SnapBuildArchiveOwnerMismatch, job.verifyBuildRequest, logger)
+        self.assertEqual(
+            "Snap package builds against private archives are only allowed "
+            "if the snap package owner and the archive owner are equal.",
+            str(e))
+
+    def test_verifyBuildRequest_no_chroot(self):
+        # verifyBuildRequest raises when the DAS has no chroot.
+        job = self.makeJob()
+        builder = MockBuilder()
+        job.setBuilder(builder, OkSlave())
+        logger = BufferLogger()
+        e = self.assertRaises(CannotBuild, job.verifyBuildRequest, logger)
+        self.assertIn("Missing chroot", str(e))
+
+    def test_extraBuildArgs_bzr(self):
+        # _extraBuildArgs returns appropriate arguments if asked to build a
+        # job for a Bazaar branch.
+        branch = self.factory.makeBranch()
+        job = self.makeJob(branch=branch)
+        expected_archives = get_sources_list_for_building(
+            job.build, job.build.distro_arch_series, None)
+        self.assertEqual({
+            "archive_private": False,
+            "archives": expected_archives,
+            "arch_tag": "i386",
+            "branch": branch.bzr_identity,
+            "name": u"test-snap",
+            }, job._extraBuildArgs())
+
+    def test_extraBuildArgs_git(self):
+        # _extraBuildArgs returns appropriate arguments if asked to build a
+        # job for a Git branch.
+        [ref] = self.factory.makeGitRefs()
+        job = self.makeJob(git_ref=ref)
+        expected_archives = get_sources_list_for_building(
+            job.build, job.build.distro_arch_series, None)
+        self.assertEqual({
+            "archive_private": False,
+            "archives": expected_archives,
+            "arch_tag": "i386",
+            "git_repository": ref.repository.git_https_url,
+            "git_path": ref.path,
+            "name": u"test-snap",
+            }, job._extraBuildArgs())
+
+    def test_composeBuildRequest(self):
+        job = self.makeJob()
+        lfa = self.factory.makeLibraryFileAlias(db_only=True)
+        job.build.distro_arch_series.addOrUpdateChroot(lfa)
+        self.assertEqual(
+            ('snap', job.build.distro_arch_series, {},
+             job._extraBuildArgs()),
+            job.composeBuildRequest(None))
+
+
+class MakeSnapBuildMixin:
+    """Provide the common makeBuild method returning a queued build."""
+
+    def makeBuild(self):
+        self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+        build = self.factory.makeSnapBuild(status=BuildStatus.BUILDING)
+        build.queueBuild()
+        return build
+
+    def makeUnmodifiableBuild(self):
+        self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+        build = self.factory.makeSnapBuild(status=BuildStatus.BUILDING)
+        build.distro_series.status = SeriesStatus.OBSOLETE
+        build.queueBuild()
+        return build
+
+
+class TestGetUploadMethodsForSnapBuild(
+    MakeSnapBuildMixin, TestGetUploadMethodsMixin, TestCaseWithFactory):
+    """IPackageBuild.getUpload-related methods work with Snap builds."""
+
+
+class TestVerifySuccessfulBuildForSnapBuild(
+    MakeSnapBuildMixin, TestVerifySuccessfulBuildMixin, TestCaseWithFactory):
+    """IBuildFarmJobBehaviour.verifySuccessfulBuild works."""
+
+
+class TestHandleStatusForSnapBuild(
+    MakeSnapBuildMixin, TestHandleStatusMixin, TrialTestCase,
+    fixtures.TestWithFixtures):
+    """IPackageBuild.handleStatus works with Snap builds."""

=== modified file 'lib/lp/soyuz/adapters/archivedependencies.py'
--- lib/lp/soyuz/adapters/archivedependencies.py	2014-12-11 21:24:19 +0000
+++ lib/lp/soyuz/adapters/archivedependencies.py	2015-08-03 14:55:59 +0000
@@ -133,7 +133,7 @@
 
 
 def expand_dependencies(archive, distro_arch_series, pocket, component,
-                        source_package_name):
+                        source_package_name, tools_archive=None):
     """Return the set of dependency archives, pockets and components.
 
     :param archive: the context `IArchive`.
@@ -141,6 +141,8 @@
     :param pocket: the context `PackagePublishingPocket`.
     :param component: the context `IComponent`.
     :param source_package_name: A source package name (as text)
+    :param tools_archive: if not None, an archive to use as an additional
+        dependency for build tools, just before the default primary archive.
     :return: a list of (archive, distro_arch_series, pocket, [component]),
         representing the dependencies defined by the given build context.
     """
@@ -172,6 +174,16 @@
                 (archive_dependency.dependency, distro_arch_series, pocket,
                  components))
 
+    # Consider build tools archive dependencies.
+    if tools_archive is not None:
+        components = [
+            component.name
+            for component in tools_archive.getComponentsForSeries(
+                distro_series)]
+        deps.append(
+            (tools_archive, distro_arch_series,
+             PackagePublishingPocket.RELEASE, components))
+
     # Consider primary archive dependency override. Add the default
     # primary archive dependencies if it's not present.
     if archive.getArchiveDependency(
@@ -201,7 +213,8 @@
     return deps
 
 
-def get_sources_list_for_building(build, distroarchseries, sourcepackagename):
+def get_sources_list_for_building(build, distroarchseries, sourcepackagename,
+                                  tools_archive=None):
     """Return the sources_list entries required to build the given item.
 
     The entries are returned in the order that is most useful;
@@ -213,11 +226,14 @@
     :param build: a context `IBuild`.
     :param distroarchseries: A `IDistroArchSeries`
     :param sourcepackagename: A source package name (as text)
+    :param tools_archive: if not None, an archive to use as an additional
+        dependency for build tools, just before the default primary archive.
     :return: a deb sources_list entries (lines).
     """
     deps = expand_dependencies(
         build.archive, distroarchseries, build.pocket,
-        build.current_component, sourcepackagename)
+        build.current_component, sourcepackagename,
+        tools_archive=tools_archive)
     sources_list_lines = \
         _get_sources_list_for_dependencies(deps)
 

=== modified file 'lib/lp/soyuz/doc/archive-dependencies.txt'
--- lib/lp/soyuz/doc/archive-dependencies.txt	2014-12-11 21:52:15 +0000
+++ lib/lp/soyuz/doc/archive-dependencies.txt	2015-08-03 14:55:59 +0000
@@ -499,6 +499,34 @@
     deb http://archive.launchpad.dev/ubuntu hoary-updates
         main restricted universe multiverse
 
+    >>> cprov.archive.external_dependencies = None
+
+
+== Tools archives ==
+
+We can force a "tools archive" to be added to the sources.list, which is
+useful for specialised build types.
+
+    >>> tools_archive = factory.makeArchive(
+    ...     distribution=ubuntu, owner=cprov, name="tools",
+    ...     purpose=ArchivePurpose.PPA)
+    >>> pub_binaries = test_publisher.getPubBinaries(
+    ...     binaryname="tool", archive=tools_archive,
+    ...     status=PackagePublishingStatus.PUBLISHED)
+    >>> for line in get_sources_list_for_building(
+    ...         a_build, a_build.distro_arch_series,
+    ...         a_build.source_package_release.name,
+    ...         tools_archive=tools_archive):
+    ...     print line
+    deb http://ppa.launchpad.dev/cprov/ppa/ubuntu hoary main
+    deb http://ppa.launchpad.dev/cprov/tools/ubuntu hoary main
+    deb http://archive.launchpad.dev/ubuntu hoary
+        main restricted universe multiverse
+    deb http://archive.launchpad.dev/ubuntu hoary-security
+        main restricted universe multiverse
+    deb http://archive.launchpad.dev/ubuntu hoary-updates
+        main restricted universe multiverse
+
 
 == Overlays ==
 

=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py	2015-05-18 06:36:51 +0000
+++ lib/lp/soyuz/interfaces/archive.py	2015-08-03 14:55:59 +0000
@@ -2290,6 +2290,8 @@
     :param ext_deps: The dependencies form field to check.
     """
     errors = []
+    if ext_deps is None:
+        return errors
     # The field can consist of multiple entries separated by
     # newlines, so process each in turn.
     for dep in ext_deps.splitlines():


Follow ups