← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/store-buildinfo into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/store-buildinfo into lp:launchpad.

Commit message:
Store uploaded .buildinfo files.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1657704 in Launchpad itself: "please start storing buildinfo files, for new dpkg versions"
  https://bugs.launchpad.net/launchpad/+bug/1657704

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/store-buildinfo/+merge/321263
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/store-buildinfo into lp:launchpad.
=== added file 'lib/lp/archiveuploader/buildinfofile.py'
--- lib/lp/archiveuploader/buildinfofile.py	1970-01-01 00:00:00 +0000
+++ lib/lp/archiveuploader/buildinfofile.py	2017-03-29 09:33:19 +0000
@@ -0,0 +1,89 @@
+# Copyright 2017 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Build information files."""
+
+__metaclass__ = type
+
+__all__ = [
+    'BuildInfoFile',
+    ]
+
+from lp.app.errors import NotFoundError
+from lp.archiveuploader.dscfile import SignableTagFile
+from lp.archiveuploader.nascentuploadfile import PackageUploadFile
+from lp.archiveuploader.utils import (
+    re_isbuildinfo,
+    re_no_epoch,
+    UploadError,
+    )
+
+
+class BuildInfoFile(PackageUploadFile, SignableTagFile):
+    """Represents an uploaded build information file."""
+
+    def __init__(self, filepath, checksums, size, component_and_section,
+                 priority_name, package, version, changes, policy, logger):
+        super(BuildInfoFile, self).__init__(
+            filepath, checksums, size, component_and_section, priority_name,
+            package, version, changes, policy, logger)
+        self.parse(verify_signature=not policy.unsigned_buildinfo_ok)
+        arch_match = re_isbuildinfo.match(self.filename)
+        self.architecture = arch_match.group(3)
+
+    @property
+    def is_sourceful(self):
+        # XXX cjwatson 2017-03-29: We should get this from the parsed
+        # Architecture field instead.
+        return self.architecture == "source"
+
+    @property
+    def is_binaryful(self):
+        # XXX cjwatson 2017-03-29: We should get this from the parsed
+        # Architecture field instead.
+        return self.architecture != "source"
+
+    @property
+    def is_archindep(self):
+        # XXX cjwatson 2017-03-29: We should get this from the parsed
+        # Architecture field instead.
+        return self.architecture == "all"
+
+    def verify(self):
+        """Verify the uploaded buildinfo file.
+
+        It returns an iterator over all the encountered errors and warnings.
+        """
+        self.logger.debug("Verifying buildinfo file %s" % self.filename)
+
+        version_chopped = re_no_epoch.sub('', self.version)
+        buildinfo_match = re_isbuildinfo.match(self.filename)
+        filename_version = buildinfo_match.group(2)
+        if filename_version != version_chopped:
+            yield UploadError("%s: should be %s according to changes file."
+                % (filename_version, version_chopped))
+
+    def checkBuild(self, build):
+        """See `PackageUploadFile`."""
+        try:
+            das = self.policy.distroseries[self.architecture]
+        except NotFoundError:
+            raise UploadError(
+                "Upload to unknown architecture %s for distroseries %s" %
+                (self.architecture, self.policy.distroseries))
+
+        # Sanity check; raise an error if the build we've been
+        # told to link to makes no sense.
+        if (build.pocket != self.policy.pocket or
+            build.distro_arch_series != das or
+            build.archive != self.policy.archive):
+            raise UploadError(
+                "Attempt to upload buildinfo specifying build %s, where it "
+                "doesn't fit." % build.id)
+
+    def storeInDatabase(self):
+        """Create and return the corresponding `LibraryFileAlias` reference."""
+        with open(self.filepath, "rb") as f:
+            return self.librarian.create(
+                self.filename, self.size, f, self.content_type,
+                restricted=self.policy.archive.private)

=== modified file 'lib/lp/archiveuploader/changesfile.py'
--- lib/lp/archiveuploader/changesfile.py	2015-07-29 01:59:43 +0000
+++ lib/lp/archiveuploader/changesfile.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """ ChangesFile class
@@ -17,6 +17,7 @@
 
 import os
 
+from lp.archiveuploader.buildinfofile import BuildInfoFile
 from lp.archiveuploader.dscfile import (
     DSCFile,
     SignableTagFile,
@@ -36,6 +37,7 @@
     parse_and_merge_file_lists,
     re_changes_file_name,
     re_isadeb,
+    re_isbuildinfo,
     re_issource,
     rfc822_encode_address,
     UploadError,
@@ -75,6 +77,7 @@
         }
 
     dsc = None
+    buildinfo = None
     maintainer = None
     changed_by = None
     filename_archtag = None
@@ -209,6 +212,8 @@
 
                     if cls == DSCFile:
                         self.dsc = file_instance
+                    elif cls == BuildInfoFile:
+                        self.buildinfo = file_instance
             except UploadError as error:
                 yield error
             else:
@@ -366,6 +371,7 @@
     """Determine the name and PackageUploadFile subclass for the filename."""
     source_match = re_issource.match(filename)
     binary_match = re_isadeb.match(filename)
+    buildinfo_match = re_isbuildinfo.match(filename)
     if source_match:
         package = source_match.group(1)
         if (determine_source_file_type(filename) ==
@@ -380,6 +386,9 @@
             BinaryPackageFileType.DDEB: DdebBinaryUploadFile,
             BinaryPackageFileType.UDEB: UdebBinaryUploadFile,
             }[determine_binary_file_type(filename)]
+    elif buildinfo_match:
+        package = buildinfo_match.group(1)
+        cls = BuildInfoFile
     else:
         raise CannotDetermineFileTypeError(
             "Could not determine the type of %r" % filename)

=== modified file 'lib/lp/archiveuploader/dscfile.py'
--- lib/lp/archiveuploader/dscfile.py	2016-06-01 01:59:32 +0000
+++ lib/lp/archiveuploader/dscfile.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """ DSCFile and related.
@@ -671,6 +671,11 @@
         user_defined_fields = self.extractUserDefinedFields([
             (field, encoded[field]) for field in self._dict.iterkeys()])
 
+        if self.changes.buildinfo is not None:
+            buildinfo_lfa = self.changes.buildinfo.storeInDatabase()
+        else:
+            buildinfo_lfa = None
+
         release = self.policy.distroseries.createUploadedSourcePackageRelease(
             sourcepackagename=source_name,
             version=self.dsc_version,
@@ -698,6 +703,7 @@
             copyright=encoded.get('copyright'),
             # dateuploaded by default is UTC:now in the database
             user_defined_fields=user_defined_fields,
+            buildinfo=buildinfo_lfa,
             )
 
         # SourcePackageFiles should contain also the DSC

=== modified file 'lib/lp/archiveuploader/nascentupload.py'
--- lib/lp/archiveuploader/nascentupload.py	2015-12-30 23:34:34 +0000
+++ lib/lp/archiveuploader/nascentupload.py	2017-03-29 09:33:19 +0000
@@ -24,6 +24,7 @@
 from zope.component import getUtility
 
 from lp.app.errors import NotFoundError
+from lp.archiveuploader.buildinfofile import BuildInfoFile
 from lp.archiveuploader.changesfile import ChangesFile
 from lp.archiveuploader.dscfile import DSCFile
 from lp.archiveuploader.nascentuploadfile import (
@@ -267,6 +268,15 @@
                     files_archdep or not uploaded_file.is_archindep)
             elif isinstance(uploaded_file, SourceUploadFile):
                 files_sourceful = True
+            elif isinstance(uploaded_file, BuildInfoFile):
+                files_sourceful = (
+                    files_sourceful or uploaded_file.is_sourceful)
+                if uploaded_file.is_binaryful:
+                    files_binaryful = files_binaryful or True
+                    files_archindep = (
+                        files_archindep or uploaded_file.is_archindep)
+                    files_archdep = (
+                        files_archdep or not uploaded_file.is_archindep)
             else:
                 # This is already caught in ChangesFile.__init__
                 raise AssertionError("Unknown uploaded file type.")
@@ -857,6 +867,10 @@
                 assert self.queue_root.pocket == bpf_build.pocket, (
                     "Binary was not build for the claimed pocket.")
                 binary_package_file.storeInDatabase(bpf_build)
+                if self.changes.buildinfo is not None:
+                    self.changes.buildinfo.checkBuild(bpf_build)
+                    bpf_build.addBuildInfo(
+                        self.changes.buildinfo.storeInDatabase())
                 processed_builds.append(bpf_build)
 
             # Store the related builds after verifying they were built

=== modified file 'lib/lp/archiveuploader/tests/__init__.py'
--- lib/lp/archiveuploader/tests/__init__.py	2014-08-13 07:49:59 +0000
+++ lib/lp/archiveuploader/tests/__init__.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the archive uploader."""
@@ -81,8 +81,9 @@
 
     def __init__(self):
         AbstractUploadPolicy.__init__(self)
-        # We require the changes to be signed but not the dsc
+        # We require the changes to be signed but not the dsc or buildinfo
         self.unsigned_dsc_ok = True
+        self.unsigned_buildinfo_ok = True
 
     def validateUploadType(self, upload):
         """We accept uploads of any type."""

=== added directory 'lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_binary_buildinfo'
=== added file 'lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_binary_buildinfo/bar_1.0-1_i386.buildinfo'
--- lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_binary_buildinfo/bar_1.0-1_i386.buildinfo	1970-01-01 00:00:00 +0000
+++ lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_binary_buildinfo/bar_1.0-1_i386.buildinfo	2017-03-29 09:33:19 +0000
@@ -0,0 +1,20 @@
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+Format: 1.0
+Source: bar
+Binary: bar
+Architecture: i386
+Version: 1.0-1
+Checksums-Md5:
+ 39a6dde58f0b84139e9877892f6ac56a 644 devel optional bar_1.0-1_i386.deb
+Build-Origin: Ubuntu
+Build-Architecture: i386
+Build-Date: Wed, 29 Mar 2017 00:01:21 +0100
+
+-----BEGIN PGP SIGNATURE-----
+
+iF0EARECAB0WIQQ0DKO7Jw4nFsnuC3aOfrcIbGSoxQUCWNrrewAKCRCOfrcIbGSo
+xR+6AJ4nl/x722AMwRVIKkjiuS834aQ+cwCfZ6x6BxiLb2jKj7Vvt+0txNMDZ44=
+=RKsi
+-----END PGP SIGNATURE-----

=== added file 'lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_binary_buildinfo/bar_1.0-1_i386.changes'
--- lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_binary_buildinfo/bar_1.0-1_i386.changes	1970-01-01 00:00:00 +0000
+++ lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_binary_buildinfo/bar_1.0-1_i386.changes	2017-03-29 09:33:19 +0000
@@ -0,0 +1,29 @@
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+Format: 1.7
+Date: Thu, 16 Feb 2006 15:34:09 +0000
+Source: bar
+Binary: bar
+Architecture: i386
+Version: 1.0-1
+Distribution: breezy
+Urgency: low
+Maintainer: Launchpad team <launchpad@xxxxxxxxxxxxxxxxxxx>
+Changed-By: Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>
+Description: 
+ bar        - Stuff for testing
+Changes: 
+ bar (1.0-1) breezy; urgency=low
+ .
+   * Initial version
+Files: 
+ 39a6dde58f0b84139e9877892f6ac56a 644 devel optional bar_1.0-1_i386.deb
+ 219b626c05f0524aa4c958ee29e200d2 490 devel optional bar_1.0-1_i386.buildinfo
+
+-----BEGIN PGP SIGNATURE-----
+
+iF0EARECAB0WIQQ0DKO7Jw4nFsnuC3aOfrcIbGSoxQUCWNrrfAAKCRCOfrcIbGSo
+xTI3AJ9aO+JcL++Yed7SbxLY1q2oL4ePlQCeMV/dGsE7TRfwm0mGXOizrDK47H8=
+=akjD
+-----END PGP SIGNATURE-----

=== added file 'lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_binary_buildinfo/bar_1.0-1_i386.deb'
Binary files lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_binary_buildinfo/bar_1.0-1_i386.deb	1970-01-01 00:00:00 +0000 and lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_binary_buildinfo/bar_1.0-1_i386.deb	2017-03-29 09:33:19 +0000 differ
=== added directory 'lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo'
=== added file 'lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0-1.diff.gz'
Binary files lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0-1.diff.gz	1970-01-01 00:00:00 +0000 and lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0-1.diff.gz	2017-03-29 09:33:19 +0000 differ
=== added file 'lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0-1.dsc'
--- lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0-1.dsc	1970-01-01 00:00:00 +0000
+++ lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0-1.dsc	2017-03-29 09:33:19 +0000
@@ -0,0 +1,21 @@
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+Format: 1.0
+Source: bar
+Version: 1.0-1
+Binary: bar
+Maintainer: Launchpad team <launchpad@xxxxxxxxxxxxxxxxxxx>
+Architecture: any
+Standards-Version: 3.6.2
+Files: 
+ fc1464e5985b962a042d5354452f361d 164 bar_1.0.orig.tar.gz
+ 1e35b810764f140af9616de8274e6e73 537 bar_1.0-1.diff.gz
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.3 (GNU/Linux)
+
+iD8DBQFFt7Cojn63CGxkqMURAo6FAJ9ZUagUNtYpmZrqFwL6LXDKOUSOPwCdFqPa
+BdrMeT+0Hg+yMS69uO+qJRI=
+=mjFU
+-----END PGP SIGNATURE-----

=== added file 'lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0-1_source.buildinfo'
--- lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0-1_source.buildinfo	1970-01-01 00:00:00 +0000
+++ lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0-1_source.buildinfo	2017-03-29 09:33:19 +0000
@@ -0,0 +1,20 @@
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+Format: 1.0
+Source: bar
+Binary: bar
+Architecture: source
+Version: 1.0-1
+Checksums-Md5:
+ 5d533778b698edc1a122098a98c8490e 512 devel optional bar_1.0-1.dsc
+Build-Origin: Ubuntu
+Build-Architecture: amd64
+Build-Date: Tue, 28 Mar 2017 23:51:59 +0100
+
+-----BEGIN PGP SIGNATURE-----
+
+iF0EARECAB0WIQQ0DKO7Jw4nFsnuC3aOfrcIbGSoxQUCWNrrcQAKCRCOfrcIbGSo
+xZc8AKCTbuZAnDyUdoBEsKy3QFxo+2BAEgCgiTklyo2HZuTBJyag1ayw/65bfoM=
+=mB68
+-----END PGP SIGNATURE-----

=== added file 'lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0-1_source.changes'
--- lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0-1_source.changes	1970-01-01 00:00:00 +0000
+++ lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0-1_source.changes	2017-03-29 09:33:19 +0000
@@ -0,0 +1,31 @@
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+Format: 1.7
+Date: Thu, 16 Feb 2006 15:34:09 +0000
+Source: bar
+Binary: bar
+Architecture: source
+Version: 1.0-1
+Distribution: breezy
+Urgency: low
+Maintainer: Launchpad team <launchpad@xxxxxxxxxxxxxxxxxxx>
+Changed-By: Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>
+Description: 
+ bar        - Stuff for testing
+Changes: 
+ bar (1.0-1) breezy; urgency=low
+ .
+   * Initial version
+Files: 
+ 5d533778b698edc1a122098a98c8490e 512 devel optional bar_1.0-1.dsc
+ fc1464e5985b962a042d5354452f361d 164 devel optional bar_1.0.orig.tar.gz
+ 1e35b810764f140af9616de8274e6e73 537 devel optional bar_1.0-1.diff.gz
+ c45de110a35a944cd8d6bf990852d4a9 488 devel optional bar_1.0-1_source.buildinfo
+
+-----BEGIN PGP SIGNATURE-----
+
+iF0EARECAB0WIQQ0DKO7Jw4nFsnuC3aOfrcIbGSoxQUCWNrrcQAKCRCOfrcIbGSo
+xULLAKCMydlA1Ct6xd8f4ckvaM0YGWHRLACeKVyQO0SaCoLtJxd5SOJVe7VACNA=
+=A0cM
+-----END PGP SIGNATURE-----

=== added file 'lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0.orig.tar.gz'
Binary files lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0.orig.tar.gz	1970-01-01 00:00:00 +0000 and lib/lp/archiveuploader/tests/data/suite/bar_1.0-1_buildinfo/bar_1.0.orig.tar.gz	2017-03-29 09:33:19 +0000 differ
=== added file 'lib/lp/archiveuploader/tests/test_buildinfofile.py'
--- lib/lp/archiveuploader/tests/test_buildinfofile.py	1970-01-01 00:00:00 +0000
+++ lib/lp/archiveuploader/tests/test_buildinfofile.py	2017-03-29 09:33:19 +0000
@@ -0,0 +1,92 @@
+# Copyright 2017 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Build information file tests."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+from debian.deb822 import Changes
+
+from lp.archiveuploader.buildinfofile import BuildInfoFile
+from lp.archiveuploader.nascentuploadfile import UploadError
+from lp.archiveuploader.tests.test_nascentuploadfile import (
+    PackageUploadFileTestCase,
+    )
+from lp.testing.layers import LaunchpadZopelessLayer
+
+
+class TestBuildInfoFile(PackageUploadFileTestCase):
+
+    layer = LaunchpadZopelessLayer
+
+    def getBaseBuildInfo(self):
+        # XXX cjwatson 2017-03-20: This will need to be fleshed out if we
+        # ever start doing non-trivial buildinfo parsing.
+        # A Changes object is close enough.
+        buildinfo = Changes()
+        buildinfo["Format"] = "0.1"
+        return buildinfo
+
+    def makeBuildInfoFile(self, filename, buildinfo, component_and_section,
+                          priority_name, package, version, changes):
+        path, md5, sha1, size = self.writeUploadFile(
+            filename, buildinfo.dump())
+        return BuildInfoFile(
+            path, {"MD5": md5}, size, component_and_section, priority_name,
+            package, version, changes, self.policy, self.logger)
+
+    def test_properties(self):
+        buildinfo = self.getBaseBuildInfo()
+        changes = self.getBaseChanges()
+        for (arch, is_sourceful, is_binaryful, is_archindep) in (
+                ("source", True, False, False),
+                ("all", False, True, True),
+                ("i386", False, True, False),
+                ):
+            buildinfofile = self.makeBuildInfoFile(
+                "foo_0.1-1_%s.buildinfo" % arch, buildinfo,
+                "main/net", "extra", "dulwich", "0.42",
+                self.createChangesFile("foo_0.1-1_%s.changes" % arch, changes))
+            self.assertEqual(arch, buildinfofile.architecture)
+            self.assertEqual(is_sourceful, buildinfofile.is_sourceful)
+            self.assertEqual(is_binaryful, buildinfofile.is_binaryful)
+            self.assertEqual(is_archindep, buildinfofile.is_archindep)
+
+    def test_storeInDatabase(self):
+        buildinfo = self.getBaseBuildInfo()
+        changes = self.getBaseChanges()
+        buildinfofile = self.makeBuildInfoFile(
+            "foo_0.1-1_source.buildinfo", buildinfo,
+            "main/net", "extra", "dulwich", "0.42",
+            self.createChangesFile("foo_0.1-1_source.changes", changes))
+        lfa = buildinfofile.storeInDatabase()
+        self.layer.txn.commit()
+        self.assertEqual(buildinfo.dump(), lfa.read())
+
+    def test_checkBuild(self):
+        das = self.factory.makeDistroArchSeries(
+            distroseries=self.policy.distroseries, architecturetag="i386")
+        build = self.factory.makeBinaryPackageBuild(
+            distroarchseries=das, archive=self.policy.archive)
+        buildinfo = self.getBaseBuildInfo()
+        changes = self.getBaseChanges()
+        buildinfofile = self.makeBuildInfoFile(
+            "foo_0.1-1_i386.buildinfo", buildinfo,
+            "main/net", "extra", "dulwich", "0.42",
+            self.createChangesFile("foo_0.1-1_i386.changes", changes))
+        buildinfofile.checkBuild(build)
+
+    def test_checkBuild_inconsistent(self):
+        das = self.factory.makeDistroArchSeries(
+            distroseries=self.policy.distroseries, architecturetag="amd64")
+        build = self.factory.makeBinaryPackageBuild(
+            distroarchseries=das, archive=self.policy.archive)
+        buildinfo = self.getBaseBuildInfo()
+        changes = self.getBaseChanges()
+        buildinfofile = self.makeBuildInfoFile(
+            "foo_0.1-1_i386.buildinfo", buildinfo,
+            "main/net", "extra", "dulwich", "0.42",
+            self.createChangesFile("foo_0.1-1_i386.changes", changes))
+        self.assertRaises(UploadError, buildinfofile.checkBuild, build)

=== modified file 'lib/lp/archiveuploader/tests/test_changesfile.py'
--- lib/lp/archiveuploader/tests/test_changesfile.py	2015-07-29 04:17:26 +0000
+++ lib/lp/archiveuploader/tests/test_changesfile.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test ChangesFile functionality."""
@@ -15,6 +15,7 @@
     )
 from zope.component import getUtility
 
+from lp.archiveuploader.buildinfofile import BuildInfoFile
 from lp.archiveuploader.changesfile import (
     CannotDetermineFileTypeError,
     ChangesFile,
@@ -71,6 +72,12 @@
             ('foo', UdebBinaryUploadFile),
             determine_file_class_and_name('foo_1.0_all.udeb'))
 
+    def testBuildInfoFile(self):
+        # A buildinfo file is a BuildInfoFile.
+        self.assertEqual(
+            ('foo', BuildInfoFile),
+            determine_file_class_and_name('foo_1.0_all.buildinfo'))
+
     def testUnmatchingFile(self):
         # Files with unknown extensions or none at all are not
         # identified.

=== modified file 'lib/lp/archiveuploader/tests/test_uploadpolicy.py'
--- lib/lp/archiveuploader/tests/test_uploadpolicy.py	2016-05-09 18:06:07 +0000
+++ lib/lp/archiveuploader/tests/test_uploadpolicy.py	2017-03-29 09:33:19 +0000
@@ -1,6 +1,6 @@
 #!/usr/bin/python
 #
-# Copyright 2010-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 from zope.component import getUtility
@@ -174,6 +174,8 @@
         self.assertTrue(buildd_policy.unsigned_changes_ok)
         self.assertFalse(insecure_policy.unsigned_dsc_ok)
         self.assertTrue(buildd_policy.unsigned_dsc_ok)
+        self.assertFalse(insecure_policy.unsigned_buildinfo_ok)
+        self.assertTrue(buildd_policy.unsigned_buildinfo_ok)
 
     def test_setOptions_distro_name(self):
         # Policies pick up the distribution name from options.

=== modified file 'lib/lp/archiveuploader/tests/test_uploadprocessor.py'
--- lib/lp/archiveuploader/tests/test_uploadprocessor.py	2017-01-10 15:18:48 +0000
+++ lib/lp/archiveuploader/tests/test_uploadprocessor.py	2017-03-29 09:33:19 +0000
@@ -119,6 +119,7 @@
         self.name = "broken"
         self.unsigned_changes_ok = True
         self.unsigned_dsc_ok = True
+        self.unsigned_buildinfo_ok = True
 
     def checkUpload(self, upload):
         """Raise an exception upload processing is not expecting."""
@@ -2017,6 +2018,47 @@
             version=u"1.0-2", exact_match=True)
         self.assertEqual(PackagePublishingPocket.PROPOSED, queue_item.pocket)
 
+    def test_source_buildinfo(self):
+        # A buildinfo file is attached to the SPR.
+        uploadprocessor = self.setupBreezyAndGetUploadProcessor()
+        upload_dir = self.queueUpload("bar_1.0-1_buildinfo")
+        with open(os.path.join(upload_dir, "bar_1.0-1_source.buildinfo")) as f:
+            buildinfo_contents = f.read()
+        self.processUpload(uploadprocessor, upload_dir)
+        source_pub = self.publishPackage("bar", "1.0-1")
+        self.assertEqual(
+            buildinfo_contents,
+            source_pub.sourcepackagerelease.buildinfo.read())
+
+    def test_binary_buildinfo(self):
+        # A buildinfo file is attached to the BPB.
+        uploadprocessor = self.setupBreezyAndGetUploadProcessor()
+        upload_dir = self.queueUpload("bar_1.0-1")
+        self.processUpload(uploadprocessor, upload_dir)
+        source_pub = self.publishPackage("bar", "1.0-1")
+        [build] = source_pub.createMissingBuilds()
+        self.switchToAdmin()
+        [queue_item] = self.breezy.getPackageUploads(
+            status=PackageUploadStatus.ACCEPTED,
+            version=u"1.0-1", name=u"bar")
+        queue_item.setDone()
+        build.buildqueue_record.markAsBuilding(self.factory.makeBuilder())
+        build.updateStatus(BuildStatus.UPLOADING)
+        self.switchToUploader()
+        shutil.rmtree(upload_dir)
+        self.layer.txn.commit()
+        behaviour = IBuildFarmJobBehaviour(build)
+        leaf_name = behaviour.getUploadDirLeaf(build.build_cookie)
+        upload_dir = self.queueUpload(
+            "bar_1.0-1_binary_buildinfo", queue_entry=leaf_name)
+        with open(os.path.join(upload_dir, "bar_1.0-1_i386.buildinfo")) as f:
+            buildinfo_contents = f.read()
+        self.options.context = "buildd"
+        self.options.builds = True
+        BuildUploadHandler(
+            uploadprocessor, self.incoming_folder, leaf_name).process()
+        self.assertEqual(buildinfo_contents, build.buildinfo.read())
+
 
 class TestUploadHandler(TestUploadProcessorBase):
 

=== modified file 'lib/lp/archiveuploader/uploadpolicy.py'
--- lib/lp/archiveuploader/uploadpolicy.py	2016-01-26 15:47:37 +0000
+++ lib/lp/archiveuploader/uploadpolicy.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Policy management for the upload handler."""
@@ -87,6 +87,7 @@
         self.archive = None
         self.unsigned_changes_ok = False
         self.unsigned_dsc_ok = False
+        self.unsigned_buildinfo_ok = False
         self.create_people = True
         # future_time_grace is in seconds
         self.future_time_grace = 24 * HOURS
@@ -290,6 +291,7 @@
         # We permit unsigned uploads because we trust our build daemons
         self.unsigned_changes_ok = True
         self.unsigned_dsc_ok = True
+        self.unsigned_buildinfo_ok = True
 
     def setOptions(self, options):
         """Store the options for later."""
@@ -330,9 +332,10 @@
 
     def __init__(self):
         AbstractUploadPolicy.__init__(self)
-        # We don't require changes or dsc to be signed for syncs
+        # We don't require changes/dsc/buildinfo to be signed for syncs
         self.unsigned_changes_ok = True
         self.unsigned_dsc_ok = True
+        self.unsigned_buildinfo_ok = True
 
     def policySpecificChecks(self, upload):
         """Perform sync specific checks."""

=== modified file 'lib/lp/archiveuploader/utils.py'
--- lib/lp/archiveuploader/utils.py	2016-06-01 01:59:32 +0000
+++ lib/lp/archiveuploader/utils.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Archive uploader utilities."""
@@ -17,6 +17,7 @@
     'prefix_multi_line_string',
     're_taint_free',
     're_isadeb',
+    're_isbuildinfo',
     're_issource',
     're_is_component_orig_tar_ext',
     're_is_component_orig_tar_ext_sig',
@@ -66,6 +67,7 @@
 re_taint_free = re.compile(r"^[-+~/\.\w]+$")
 
 re_isadeb = re.compile(r"(.+?)_(.+?)_(.+)\.(u?d?deb)$")
+re_isbuildinfo = re.compile(r"(.+?)_(.+?)_(.+)\.buildinfo$")
 
 source_file_exts = [
     'orig(?:-.+)?\.tar\.(?:gz|bz2|xz)(?:\.asc)?', 'diff.gz',

=== modified file 'lib/lp/registry/interfaces/distroseries.py'
--- lib/lp/registry/interfaces/distroseries.py	2016-11-14 19:55:07 +0000
+++ lib/lp/registry/interfaces/distroseries.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Interfaces including and related to IDistroSeries."""
@@ -686,7 +686,7 @@
         dsc_binaries, archive, copyright, build_conflicts,
         build_conflicts_indep, dateuploaded=None,
         source_package_recipe_build=None, user_defined_fields=None,
-        homepage=None):
+        homepage=None, buildinfo=None):
         """Create an uploads `SourcePackageRelease`.
 
         Set this distroseries set to be the uploadeddistroseries.
@@ -726,6 +726,7 @@
                                      user defined fields.
          :param homepage: optional string with (unchecked) upstream homepage
                           URL
+         :param buildinfo: optional LFA with build information file
          :return: the just creates `SourcePackageRelease`
         """
 

=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py	2016-04-04 12:54:43 +0000
+++ lib/lp/registry/model/distroseries.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Database classes for a distribution series."""
@@ -1187,7 +1187,7 @@
         dsc_binaries, archive, copyright, build_conflicts,
         build_conflicts_indep, dateuploaded=DEFAULT,
         source_package_recipe_build=None, user_defined_fields=None,
-        homepage=None):
+        homepage=None, buildinfo=None):
         """See `IDistroSeries`."""
         return SourcePackageRelease(
             upload_distroseries=self, sourcepackagename=sourcepackagename,
@@ -1206,7 +1206,8 @@
             build_conflicts=build_conflicts,
             build_conflicts_indep=build_conflicts_indep,
             source_package_recipe_build=source_package_recipe_build,
-            user_defined_fields=user_defined_fields, homepage=homepage)
+            user_defined_fields=user_defined_fields, homepage=homepage,
+            buildinfo=buildinfo)
 
     def getComponentByName(self, name):
         """See `IDistroSeries`."""

=== modified file 'lib/lp/soyuz/enums.py'
--- lib/lp/soyuz/enums.py	2016-05-26 14:53:06 +0000
+++ lib/lp/soyuz/enums.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Enumerations used in the lp/soyuz modules."""
@@ -197,6 +197,13 @@
         similar operating systems for distributing debug symbols.
         """)
 
+    BUILDINFO = DBItem(5, """
+        Build information
+
+        A file used by Debian-based systems to record information about the
+        build environment.
+        """)
+
 
 class BinaryPackageFormat(DBEnumeratedType):
     """Binary Package Format

=== modified file 'lib/lp/soyuz/interfaces/binarypackagebuild.py'
--- lib/lp/soyuz/interfaces/binarypackagebuild.py	2016-01-08 13:15:02 +0000
+++ lib/lp/soyuz/interfaces/binarypackagebuild.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """BinaryPackageBuild interfaces."""
@@ -145,6 +145,10 @@
             description=_("The URL for the changes file for this build. "
                           "Will be None if the build was imported by Gina.")))
 
+    buildinfo = Attribute(
+        "The `LibraryFileAlias` object containing build information for "
+        "this build, if any.")
+
     package_upload = Attribute(
         "The `PackageUpload` record corresponding to the original upload "
         "of the binaries resulted from this build. It's 'None' if it is "
@@ -157,6 +161,15 @@
             ),
         exported_as="score")
 
+    def updateStatus(status, builder=None, slave_status=None,
+                     date_started=None, date_finished=None,
+                     force_invalid_transition=False, buildinfo=None):
+        """See `IBuildFarmJob.updateStatus`.
+
+        This version also accepts a `buildinfo` parameter which sets the
+        build's buildinfo file if it has not already been set.
+        """
+
     def updateDependencies():
         """Update the build-dependencies line within the targeted context."""
 
@@ -265,6 +278,12 @@
         If the build is not in a cancellable state, this method is a no-op.
         """
 
+    def addBuildInfo(buildinfo):
+        """Add a buildinfo file to this build.
+
+        :param buildinfo: An `ILibraryFileAlias`.
+        """
+
 
 class IBinaryPackageBuildRestricted(Interface):
     """Restricted `IBinaryPackageBuild` attributes.
@@ -337,7 +356,8 @@
     """Interface for BinaryPackageBuildSet"""
 
     def new(source_package_release, archive, distro_arch_series, pocket,
-            arch_indep=False, status=BuildStatus.NEEDSBUILD, builder=None):
+            arch_indep=False, status=BuildStatus.NEEDSBUILD, builder=None,
+            buildinfo=None):
         """Create a new `IBinaryPackageBuild`.
 
         :param source_package_release: An `ISourcePackageRelease`.
@@ -348,6 +368,7 @@
             addition to architecture specific ones.
         :param status: A `BuildStatus` item indicating the builds status.
         :param builder: An optional `IBuilder`.
+        :param buildinfo: An optional `ILibraryFileAlias`.
         """
 
     def getBySourceAndLocation(source_package_release, archive,

=== modified file 'lib/lp/soyuz/interfaces/sourcepackagerelease.py'
--- lib/lp/soyuz/interfaces/sourcepackagerelease.py	2016-07-29 07:37:33 +0000
+++ lib/lp/soyuz/interfaces/sourcepackagerelease.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2014 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Source package release interfaces."""
@@ -44,6 +44,9 @@
     change_summary = Attribute(
         "The message on the latest change in this release. This is usually "
         "a snippet from the changelog")
+    buildinfo = Attribute(
+        "LibraryFileAlias containing build information for this source "
+        "upload, if any.")
     builddepends = TextLine(
         title=_("DSC build depends"),
         description=_("A comma-separated list of packages on which this "

=== modified file 'lib/lp/soyuz/model/binarypackagebuild.py'
--- lib/lp/soyuz/model/binarypackagebuild.py	2016-01-08 13:15:02 +0000
+++ lib/lp/soyuz/model/binarypackagebuild.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -81,11 +81,11 @@
     LibraryFileAlias,
     LibraryFileContent,
     )
+from lp.soyuz.adapters.buildarch import determine_architectures_to_build
 from lp.soyuz.enums import (
     ArchivePurpose,
     PackagePublishingStatus,
     )
-from lp.soyuz.adapters.buildarch import determine_architectures_to_build
 from lp.soyuz.interfaces.archive import (
     InvalidExternalDependencies,
     validate_external_dependencies,
@@ -229,6 +229,9 @@
         name='external_dependencies',
         validator=storm_validate_external_dependencies)
 
+    buildinfo_id = Int(name='buildinfo')
+    buildinfo = Reference(buildinfo_id, 'LibraryFileAlias.id')
+
     def getLatestSourcePublication(self):
         from lp.soyuz.model.publishing import SourcePackagePublishingHistory
         store = Store.of(self)
@@ -703,6 +706,13 @@
             estimate = 5
         return datetime.timedelta(minutes=estimate)
 
+    def addBuildInfo(self, buildinfo):
+        """See `IBinaryPackageBuild`."""
+        if self.buildinfo is None:
+            self.buildinfo = buildinfo
+        else:
+            assert self.buildinfo == buildinfo
+
     def verifySuccessfulUpload(self):
         return bool(self.binarypackages)
 
@@ -783,7 +793,8 @@
 class BinaryPackageBuildSet(SpecificBuildFarmJobSourceMixin):
 
     def new(self, source_package_release, archive, distro_arch_series, pocket,
-            arch_indep=None, status=BuildStatus.NEEDSBUILD, builder=None):
+            arch_indep=None, status=BuildStatus.NEEDSBUILD, builder=None,
+            buildinfo=None):
         """See `IBinaryPackageBuildSet`."""
         # Force the current timestamp instead of the default UTC_NOW for
         # the transaction, avoid several row with same datecreated.
@@ -806,7 +817,7 @@
             distribution=distro_arch_series.distroseries.distribution,
             distro_series=distro_arch_series.distroseries,
             source_package_name=source_package_release.sourcepackagename,
-            date_created=date_created)
+            buildinfo=buildinfo, date_created=date_created)
 
     def getByID(self, id):
         """See `IBinaryPackageBuildSet`."""

=== modified file 'lib/lp/soyuz/model/sourcepackagerelease.py'
--- lib/lp/soyuz/model/sourcepackagerelease.py	2016-12-22 16:32:38 +0000
+++ lib/lp/soyuz/model/sourcepackagerelease.py	2017-03-29 09:33:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -100,6 +100,7 @@
     version = StringCol(dbName='version', notNull=True)
     changelog = ForeignKey(foreignKey='LibraryFileAlias', dbName='changelog')
     changelog_entry = StringCol(dbName='changelog_entry')
+    buildinfo = ForeignKey(foreignKey='LibraryFileAlias', dbName='buildinfo')
     builddepends = StringCol(dbName='builddepends')
     builddependsindep = StringCol(dbName='builddependsindep')
     build_conflicts = StringCol(dbName='build_conflicts')


Follow ups