← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~andrey-fedoseev/launchpad:snap-target-architectures into launchpad:master

 

Andrey Fedoseev has proposed merging ~andrey-fedoseev/launchpad:snap-target-architectures into launchpad:master.

Commit message:
Add `target_architectures` to snap builds

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~andrey-fedoseev/launchpad/+git/launchpad/+merge/423921

Allow snap builds to have an associated list of target architectures to build the snap for.

The target architectures are either specified using the `build-to` property in snapcraft.yml, or 
may can be passed as an argument to the `Snap.requestBuild` method.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~andrey-fedoseev/launchpad:snap-target-architectures into launchpad:master.
diff --git a/lib/lp/snappy/adapters/buildarch.py b/lib/lp/snappy/adapters/buildarch.py
index 76e7a4f..9697ff8 100644
--- a/lib/lp/snappy/adapters/buildarch.py
+++ b/lib/lp/snappy/adapters/buildarch.py
@@ -6,6 +6,13 @@ __all__ = [
     ]
 
 from collections import Counter
+from typing import (
+    Any,
+    Dict,
+    List,
+    Optional,
+    Union,
+    )
 
 from lp.services.helpers import english_list
 
@@ -56,7 +63,12 @@ class UnsupportedBuildOnError(SnapArchitecturesParserError):
 class SnapArchitecture:
     """A single entry in the snapcraft.yaml 'architectures' list."""
 
-    def __init__(self, build_on, build_to=None, build_error=None):
+    def __init__(
+        self,
+        build_on: Union[str, List[str]],
+        build_to: Optional[Union[str, List[str]]] = None,
+        build_error: Optional[str] = None
+    ):
         """Create a new architecture entry.
 
         :param build_on: string or list; build-on property from
@@ -67,10 +79,12 @@ class SnapArchitecture:
             snapcraft.yaml.
         """
         self.build_on = (
-            [build_on] if isinstance(build_on, str) else build_on)
+            [build_on] if isinstance(build_on, str) else build_on
+        )  # type: List[str]
         if build_to:
             self.build_to = (
-                [build_to] if isinstance(build_to, str) else build_to)
+                [build_to] if isinstance(build_to, str) else build_to
+            )  # type: List[str]
         else:
             self.build_to = self.build_on
         self.build_error = build_error
@@ -83,9 +97,11 @@ class SnapArchitecture:
         except KeyError:
             raise MissingPropertyError("build-on")
 
+        build_to = properties.get("build-to", properties.get("run-on"))
+
         return cls(
             build_on=build_on,
-            build_to=properties.get("build-to", properties.get("run-on")),
+            build_to=build_to,
             build_error=properties.get("build-error"),
         )
 
@@ -97,11 +113,17 @@ class SnapBuildInstance:
 
       - architecture: The architecture tag that should be used to build the
             snap.
+      - target_architectures: The architecture tags that should be used to
+            build the snap for.
       - required: Whether or not failure to build should cause the entire
             set to fail.
     """
 
-    def __init__(self, architecture, supported_architectures):
+    def __init__(
+        self,
+        architecture: SnapArchitecture,
+        supported_architectures: List[str],
+    ):
         """Construct a new `SnapBuildInstance`.
 
         :param architecture: `SnapArchitecture` instance.
@@ -115,10 +137,14 @@ class SnapBuildInstance:
         except StopIteration:
             raise UnsupportedBuildOnError(architecture.build_on)
 
+        self.target_architectures = architecture.build_to
         self.required = architecture.build_error != "ignore"
 
 
-def determine_architectures_to_build(snapcraft_data, supported_arches):
+def determine_architectures_to_build(
+    snapcraft_data: Dict[str, Any],
+    supported_arches: List[str],
+) -> List[SnapBuildInstance]:
     """Return a list of architectures to build based on snapcraft.yaml.
 
     :param snapcraft_data: A parsed snapcraft.yaml.
@@ -126,7 +152,9 @@ def determine_architectures_to_build(snapcraft_data, supported_arches):
         we can create builds for.
     :return: a list of `SnapBuildInstance`s.
     """
-    architectures_list = snapcraft_data.get("architectures")
+    architectures_list = (
+        snapcraft_data.get("architectures")
+    )  # type: Optional[List]
 
     if architectures_list:
         # First, determine what style we're parsing.  Is it a list of
diff --git a/lib/lp/snappy/adapters/tests/test_buildarch.py b/lib/lp/snappy/adapters/tests/test_buildarch.py
index 53d3937..b3018ed 100644
--- a/lib/lp/snappy/adapters/tests/test_buildarch.py
+++ b/lib/lp/snappy/adapters/tests/test_buildarch.py
@@ -77,12 +77,14 @@ class TestSnapBuildInstance(WithScenarios, TestCase):
                 build_on="i386", build_to=["amd64", "i386"]),
             "supported_architectures": ["amd64", "i386", "armhf"],
             "expected_architecture": "i386",
+            "expected_target_architectures": ["amd64", "i386"],
             "expected_required": True,
             }),
         ("amd64", {
             "architecture": SnapArchitecture(build_on="amd64", build_to="all"),
             "supported_architectures": ["amd64", "i386", "armhf"],
             "expected_architecture": "amd64",
+            "expected_target_architectures": ["all"],
             "expected_required": True,
             }),
         ("amd64 priority", {
@@ -90,6 +92,7 @@ class TestSnapBuildInstance(WithScenarios, TestCase):
                 build_on=["amd64", "i386"], build_to="all"),
             "supported_architectures": ["amd64", "i386", "armhf"],
             "expected_architecture": "amd64",
+            "expected_target_architectures": ["all"],
             "expected_required": True,
             }),
         ("i386 priority", {
@@ -97,6 +100,7 @@ class TestSnapBuildInstance(WithScenarios, TestCase):
                 build_on=["amd64", "i386"], build_to="all"),
             "supported_architectures": ["i386", "amd64", "armhf"],
             "expected_architecture": "i386",
+            "expected_target_architectures": ["all"],
             "expected_required": True,
             }),
         ("optional", {
@@ -104,6 +108,7 @@ class TestSnapBuildInstance(WithScenarios, TestCase):
                 build_on="amd64", build_error="ignore"),
             "supported_architectures": ["amd64", "i386", "armhf"],
             "expected_architecture": "amd64",
+            "expected_target_architectures": ["amd64"],
             "expected_required": False,
             }),
         ]
@@ -112,6 +117,8 @@ class TestSnapBuildInstance(WithScenarios, TestCase):
         instance = SnapBuildInstance(
             self.architecture, self.supported_architectures)
         self.assertEqual(self.expected_architecture, instance.architecture)
+        self.assertEqual(self.expected_target_architectures,
+                         instance.target_architectures)
         self.assertEqual(self.expected_required, instance.required)
 
 
@@ -128,50 +135,91 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCase):
 
     # Scenarios taken from the architectures document:
     # https://forum.snapcraft.io/t/architectures/4972
+
     scenarios = [
         ("none", {
             "architectures": None,
             "supported_architectures": ["amd64", "i386", "armhf"],
             "expected": [
-                {"architecture": "amd64", "required": True},
-                {"architecture": "i386", "required": True},
-                {"architecture": "armhf", "required": True},
-                ],
-            }),
+                {
+                    "architecture": "amd64",
+                    "target_architectures": ["amd64"],
+                    "required": True,
+                },
+                {
+                    "architecture": "i386",
+                    "target_architectures": ["i386"],
+                    "required": True
+                },
+                {
+                    "architecture": "armhf",
+                    "target_architectures": ["armhf"],
+                    "required": True
+                },
+            ],
+        }),
         ("i386", {
             "architectures": [
                 {"build-on": "i386", "build-to": ["amd64", "i386"]},
-                ],
+            ],
             "supported_architectures": ["amd64", "i386", "armhf"],
-            "expected": [{"architecture": "i386", "required": True}],
-            }),
+            "expected": [
+                {
+                    "architecture": "i386",
+                    "target_architectures": ["amd64", "i386"],
+                    "required": True,
+                }
+            ],
+        }),
         ("amd64", {
             "architectures": [{"build-on": "amd64", "build-to": "all"}],
             "supported_architectures": ["amd64", "i386", "armhf"],
-            "expected": [{"architecture": "amd64", "required": True}],
-            }),
+            "expected": [
+                {
+                    "architecture": "amd64",
+                    "target_architectures": ["all"],
+                    "required": True
+                }
+            ],
+        }),
         ("amd64 and i386", {
             "architectures": [
                 {"build-on": "amd64", "build-to": "amd64"},
                 {"build-on": "i386", "build-to": "i386"},
-                ],
+            ],
             "supported_architectures": ["amd64", "i386", "armhf"],
             "expected": [
-                {"architecture": "amd64", "required": True},
-                {"architecture": "i386", "required": True},
-                ],
-            }),
+                {
+                    "architecture": "amd64",
+                    "target_architectures": ["amd64"],
+                    "required": True,
+                },
+                {
+                    "architecture": "i386",
+                    "target_architectures": ["i386"],
+                    "required": True,
+                },
+            ],
+        }),
         ("amd64 and i386 shorthand", {
             "architectures": [
                 {"build-on": "amd64"},
                 {"build-on": "i386"},
-                ],
+            ],
             "supported_architectures": ["amd64", "i386", "armhf"],
             "expected": [
-                {"architecture": "amd64", "required": True},
-                {"architecture": "i386", "required": True},
-                ],
-            }),
+                {
+                    "architecture": "amd64",
+                    "target_architectures": ["amd64"],
+                    "required": True,
+                },
+                {
+                    "architecture": "i386",
+                    "target_architectures": ["i386"],
+                    "required": True,
+                },
+            ],
+        }),
         ("amd64, i386, and armhf", {
             "architectures": [
                 {"build-on": "amd64", "build-to": "amd64"},
@@ -180,51 +228,95 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCase):
                     "build-on": "armhf",
                     "build-to": "armhf",
                     "build-error": "ignore",
-                    },
-                ],
+                },
+            ],
             "supported_architectures": ["amd64", "i386", "armhf"],
             "expected": [
-                {"architecture": "amd64", "required": True},
-                {"architecture": "i386", "required": True},
-                {"architecture": "armhf", "required": False},
-                ],
-            }),
+                {
+                    "architecture": "amd64",
+                    "target_architectures": ["amd64"],
+                    "required": True,
+                },
+                {
+                    "architecture": "i386",
+                    "target_architectures": ["i386"],
+                    "required": True,
+                },
+                {
+                    "architecture": "armhf",
+                    "target_architectures": ["armhf"],
+                    "required": False,
+                },
+            ],
+        }),
         ("amd64 priority", {
             "architectures": [
                 {"build-on": ["amd64", "i386"], "build-to": "all"},
-                ],
+            ],
             "supported_architectures": ["amd64", "i386", "armhf"],
-            "expected": [{"architecture": "amd64", "required": True}],
-            }),
+            "expected": [
+                {
+                    "architecture": "amd64",
+                    "target_architectures": ["all"],
+                    "required": True,
+                }
+            ],
+        }),
         ("i386 priority", {
             "architectures": [
                 {"build-on": ["amd64", "i386"], "build-to": "all"},
-                ],
+            ],
             "supported_architectures": ["i386", "amd64", "armhf"],
-            "expected": [{"architecture": "i386", "required": True}],
-            }),
+            "expected": [
+                {
+                    "architecture": "i386",
+                    "target_architectures": ["all"],
+                    "required": True,
+                }
+            ],
+        }),
         ("old style i386 priority", {
             "architectures": ["amd64", "i386"],
             "supported_architectures": ["i386", "amd64", "armhf"],
-            "expected": [{"architecture": "i386", "required": True}],
-            }),
+            "expected": [
+                {
+                    "architecture": "i386",
+                    "target_architectures": ["amd64", "i386"],
+                    "required": True
+                }
+            ],
+        }),
         ("old style amd64 priority", {
             "architectures": ["amd64", "i386"],
             "supported_architectures": ["amd64", "i386", "armhf"],
-            "expected": [{"architecture": "amd64", "required": True}],
-            }),
+            "expected": [
+                {
+                    "architecture": "amd64",
+                    "target_architectures": ["amd64", "i386"],
+                    "required": True,
+                }
+            ],
+        }),
         ("more architectures listed than are supported", {
             "architectures": [
                 {"build-on": "amd64"},
                 {"build-on": "i386"},
                 {"build-on": "armhf"},
-                ],
+            ],
             "supported_architectures": ["amd64", "i386"],
             "expected": [
-                {"architecture": "amd64", "required": True},
-                {"architecture": "i386", "required": True},
-                ],
-            })
+                {
+                    "architecture": "amd64",
+                    "target_architectures": ["amd64"],
+                    "required": True,
+                },
+                {
+                    "architecture": "i386",
+                    "target_architectures": ["i386"],
+                    "required": True,
+                },
+            ],
+        })
     ]
 
     def test_parser(self):
diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
index a809b08..8cb4d37 100644
--- a/lib/lp/snappy/interfaces/snap.py
+++ b/lib/lp/snappy/interfaces/snap.py
@@ -418,12 +418,22 @@ class ISnapView(Interface):
                 "A dictionary mapping snap names to channels to use for this "
                 "build.  Currently only 'core', 'core18', 'core20', 'core22', "
                 "and 'snapcraft' keys are supported."),
-            key_type=TextLine(), required=False))
+            key_type=TextLine(), required=False),
+        target_architectures=List(
+            required=False,
+            title=_("The list of target architectures."),
+            description=_(
+                "The optional list of target architectures to build snap for. "
+                "If omitted then no target architecture is assumed."
+            ),
+        ),
+    )
     # Really ISnapBuild, patched in lp.snappy.interfaces.webservice.
     @export_factory_operation(Interface, [])
     @operation_for_version("devel")
     def requestBuild(requester, archive, distro_arch_series, pocket,
-                     snap_base=None, channels=None, build_request=None):
+                     snap_base=None, channels=None, build_request=None,
+                     target_architectures=None):
         """Request that the snap package be built.
 
         :param requester: The person requesting the build.
@@ -435,6 +445,8 @@ class ISnapView(Interface):
             for this build.
         :param build_request: The `ISnapBuildRequest` job being processed,
             if any.
+        :param target_architectures: The optional list of target architectures
+            to build snap for.
         :return: `ISnapBuild`.
         """
 
diff --git a/lib/lp/snappy/interfaces/snapbuild.py b/lib/lp/snappy/interfaces/snapbuild.py
index 4012391..60dd0f4 100644
--- a/lib/lp/snappy/interfaces/snapbuild.py
+++ b/lib/lp/snappy/interfaces/snapbuild.py
@@ -159,12 +159,20 @@ class ISnapBuildView(IPackageBuildView, IPrivacy):
 
     distro_arch_series = exported(Reference(
         IDistroArchSeries,
-        title=_("The series and architecture for which to build."),
+        title=_("The series and architecture to build on."),
         required=True, readonly=True))
 
     arch_tag = exported(
         TextLine(title=_("Architecture tag"), required=True, readonly=True))
 
+    target_architectures = exported(
+        List(
+            TextLine(),
+            title=_("The target architectures to build for."),
+            required=False, readonly=True,
+        )
+    )
+
     pocket = exported(Choice(
         title=_("The pocket for which to build."),
         description=(
@@ -327,7 +335,8 @@ class ISnapBuildSet(ISpecificBuildFarmJobSource):
 
     def new(requester, snap, archive, distro_arch_series, pocket,
             snap_base=None, channels=None, date_created=DEFAULT,
-            store_upload_metadata=None, build_request=None):
+            store_upload_metadata=None, build_request=None,
+            target_architectures=None):
         """Create an `ISnapBuild`."""
 
     def preloadBuildsData(builds):
diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
index 16bbfed..bfc5795 100644
--- a/lib/lp/snappy/model/snap.py
+++ b/lib/lp/snappy/model/snap.py
@@ -13,6 +13,7 @@ from datetime import (
     timedelta,
     )
 from operator import attrgetter
+import typing as t
 from urllib.parse import urlsplit
 
 from breezy import urlutils
@@ -116,6 +117,7 @@ from lp.registry.interfaces.accesspolicy import (
     IAccessArtifactGrantSource,
     IAccessArtifactSource,
     )
+from lp.registry.interfaces.distroseries import IDistroSeries
 from lp.registry.interfaces.person import (
     IPerson,
     IPersonSet,
@@ -205,7 +207,10 @@ from lp.snappy.interfaces.snapbase import (
     ISnapBaseSet,
     NoSuchSnapBase,
     )
-from lp.snappy.interfaces.snapbuild import ISnapBuildSet
+from lp.snappy.interfaces.snapbuild import (
+    ISnapBuild,
+    ISnapBuildSet,
+    )
 from lp.snappy.interfaces.snapjob import ISnapRequestBuildsJobSource
 from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet
 from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
@@ -213,6 +218,7 @@ from lp.snappy.model.snapbuild import SnapBuild
 from lp.snappy.model.snapjob import SnapJob
 from lp.snappy.model.snapsubscription import SnapSubscription
 from lp.soyuz.interfaces.archive import ArchiveDisabled
+from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
 from lp.soyuz.model.archive import (
     Archive,
     get_enabled_archive_filter,
@@ -639,7 +645,11 @@ class Snap(Storm, WebhookTargetMixin):
             das.getChroot(pocket=pocket) is not None
             and self._isBuildableArchitectureAllowed(das, snap_base=snap_base))
 
-    def getAllowedArchitectures(self, distro_series=None, snap_base=None):
+    def getAllowedArchitectures(
+        self,
+        distro_series: t.Optional[IDistroSeries] = None,
+        snap_base = None
+    ) -> t.List[IDistroArchSeries]:
         """See `ISnap`."""
         if distro_series is None:
             distro_series = self.distro_series
@@ -783,14 +793,26 @@ class Snap(Storm, WebhookTargetMixin):
             # See rationale in `SnapBuildArchiveOwnerMismatch` docstring.
             raise SnapBuildArchiveOwnerMismatch()
 
-    def requestBuild(self, requester, archive, distro_arch_series, pocket,
-                     snap_base=None, channels=None, build_request=None):
+    def requestBuild(
+        self,
+        requester,
+        archive,
+        distro_arch_series,
+        pocket,
+        snap_base=None,
+        channels=None,
+        build_request=None,
+        target_architectures: t.Optional[t.List[str]] = None
+    ) -> ISnapBuild:
         """See `ISnap`."""
         self._checkRequestBuild(requester, archive)
         if not self._isArchitectureAllowed(
                 distro_arch_series, pocket, snap_base=snap_base):
             raise SnapBuildDisallowedArchitecture(distro_arch_series, pocket)
 
+        if target_architectures:
+            target_architectures = sorted(target_architectures)
+
         if not channels:
             channels_clause = Or(
                 SnapBuild.channels == None, SnapBuild.channels == {})
@@ -802,6 +824,7 @@ class Snap(Storm, WebhookTargetMixin):
             SnapBuild.archive_id == archive.id,
             SnapBuild.distro_arch_series_id == distro_arch_series.id,
             SnapBuild.pocket == pocket,
+            SnapBuild.target_architectures == target_architectures,
             channels_clause,
             SnapBuild.status == BuildStatus.NEEDSBUILD)
         if pending.any() is not None:
@@ -810,7 +833,8 @@ class Snap(Storm, WebhookTargetMixin):
         build = getUtility(ISnapBuildSet).new(
             requester, self, archive, distro_arch_series, pocket,
             snap_base=snap_base, channels=channels,
-            build_request=build_request)
+            build_request=build_request,
+            target_architectures=target_architectures)
         build.queueBuild()
         notify(ObjectCreatedEvent(build, user=requester))
         return build
@@ -917,7 +941,7 @@ class Snap(Storm, WebhookTargetMixin):
                 if (architectures is None or
                     das.architecturetag in architectures))
             architectures_to_build = determine_architectures_to_build(
-                snapcraft_data, supported_arches.keys())
+                snapcraft_data, list(supported_arches.keys()))
         except Exception as e:
             if not allow_failures:
                 raise
@@ -938,7 +962,9 @@ class Snap(Storm, WebhookTargetMixin):
                 build = self.requestBuild(
                     requester, archive, supported_arches[arch], pocket,
                     snap_base=snap_base, channels=arch_channels,
-                    build_request=build_request)
+                    build_request=build_request,
+                    target_architectures=build_instance.target_architectures
+                )
                 if logger is not None:
                     logger.debug(
                         " - %s/%s/%s: Build requested.",
diff --git a/lib/lp/snappy/model/snapbuild.py b/lib/lp/snappy/model/snapbuild.py
index 4ec5275..dece4fe 100644
--- a/lib/lp/snappy/model/snapbuild.py
+++ b/lib/lp/snappy/model/snapbuild.py
@@ -11,6 +11,7 @@ from operator import attrgetter
 
 import pytz
 import six
+from storm.databases import postgres
 from storm.expr import (
     Column,
     Table,
@@ -156,6 +157,10 @@ class SnapBuild(PackageBuildMixin, Storm):
     distro_arch_series = Reference(
         distro_arch_series_id, 'DistroArchSeries.id')
 
+    target_architectures = postgres.JSON(
+        'target_architectures', allow_none=True
+    )
+
     pocket = DBEnum(enum=PackagePublishingPocket, allow_none=False)
 
     snap_base_id = Int(name='snap_base', allow_none=True)
@@ -197,7 +202,8 @@ class SnapBuild(PackageBuildMixin, Storm):
     def __init__(self, build_farm_job, requester, snap, archive,
                  distro_arch_series, pocket, snap_base, channels,
                  processor, virtualized, date_created,
-                 store_upload_metadata=None, build_request=None):
+                 store_upload_metadata=None, build_request=None,
+                 target_architectures=None):
         """Construct a `SnapBuild`."""
         super().__init__()
         self.build_farm_job = build_farm_job
@@ -212,6 +218,7 @@ class SnapBuild(PackageBuildMixin, Storm):
         self.virtualized = virtualized
         self.date_created = date_created
         self.store_upload_metadata = store_upload_metadata
+        self.target_architectures = target_architectures
         if build_request is not None:
             self.build_request_id = build_request.id
         self.status = BuildStatus.NEEDSBUILD
@@ -522,7 +529,8 @@ class SnapBuildSet(SpecificBuildFarmJobSourceMixin):
 
     def new(self, requester, snap, archive, distro_arch_series, pocket,
             snap_base=None, channels=None, date_created=DEFAULT,
-            store_upload_metadata=None, build_request=None):
+            store_upload_metadata=None, build_request=None,
+            target_architectures=None):
         """See `ISnapBuildSet`."""
         store = IMasterStore(SnapBuild)
         build_farm_job = getUtility(IBuildFarmJobSource).new(
@@ -534,7 +542,8 @@ class SnapBuildSet(SpecificBuildFarmJobSourceMixin):
             not distro_arch_series.processor.supports_nonvirtualized
             or snap.require_virtualized or archive.require_virtualized,
             date_created, store_upload_metadata=store_upload_metadata,
-            build_request=build_request)
+            build_request=build_request,
+            target_architectures=target_architectures)
         store.add(snapbuild)
         store.flush()
         return snapbuild
diff --git a/lib/lp/snappy/model/snapbuildbehaviour.py b/lib/lp/snappy/model/snapbuildbehaviour.py
index 62cb2f9..612e963 100644
--- a/lib/lp/snappy/model/snapbuildbehaviour.py
+++ b/lib/lp/snappy/model/snapbuildbehaviour.py
@@ -10,6 +10,8 @@ __all__ = [
     'SnapBuildBehaviour',
     ]
 
+import typing
+
 from twisted.internet import defer
 from zope.component import adapter
 from zope.interface import implementer
@@ -101,11 +103,11 @@ class SnapBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
             config.builddmaster.authentication_timeout)
 
     @defer.inlineCallbacks
-    def extraBuildArgs(self, logger=None):
+    def extraBuildArgs(self, logger=None) -> typing.Dict[str, typing.Any]:
         """
         Return the extra arguments required by the worker for the given build.
         """
-        build = self.build
+        build = self.build  # type: ISnapBuild
         args = yield super().extraBuildArgs(logger=logger)
         yield self.addProxyArgs(args, build.snap.allow_internet)
         args["name"] = build.snap.store_name or build.snap.name
@@ -165,6 +167,9 @@ class SnapBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
             # (matching snapd, SAS and snapcraft representation)
             timestamp = format_as_rfc3339(build_request.date_requested)
             args["build_request_timestamp"] = timestamp
+
+        args["target_architectures"] = build.target_architectures
+
         return args
 
     def verifySuccessfulBuild(self):
diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
index abd99d3..41f8c0d 100644
--- a/lib/lp/snappy/tests/test_snap.py
+++ b/lib/lp/snappy/tests/test_snap.py
@@ -238,8 +238,12 @@ class TestSnap(TestCaseWithFactory):
             distroseries=distroarchseries.distroseries,
             processors=[distroarchseries.processor])
         build = snap.requestBuild(
-            snap.owner, snap.distro_series.main_archive, distroarchseries,
-            PackagePublishingPocket.UPDATES)
+            snap.owner,
+            snap.distro_series.main_archive,
+            distroarchseries,
+            PackagePublishingPocket.UPDATES,
+            target_architectures=["amd64", "i386"],
+        )
         self.assertTrue(ISnapBuild.providedBy(build))
         self.assertThat(build, MatchesStructure(
             requester=Equals(snap.owner),
@@ -249,7 +253,8 @@ class TestSnap(TestCaseWithFactory):
             snap_base=Is(None),
             channels=Is(None),
             status=Equals(BuildStatus.NEEDSBUILD),
-            ))
+            target_architectures=Equals(["amd64", "i386"]),
+        ))
         store = Store.of(build)
         store.flush()
         build_queue = store.find(
@@ -359,6 +364,19 @@ class TestSnap(TestCaseWithFactory):
             SnapBuildAlreadyPending, snap.requestBuild,
             snap.owner, snap.distro_series.main_archive, arches[0],
             PackagePublishingPocket.UPDATES, channels={"core": "edge"})
+        # target_architectures are taken into account when looking for pending
+        # builds. the order of the list should not matter.
+        snap.requestBuild(
+            snap.owner, snap.distro_series.main_archive, arches[0],
+            PackagePublishingPocket.UPDATES,
+            target_architectures=["i386", "amd64"]
+        )
+        self.assertRaises(
+            SnapBuildAlreadyPending, snap.requestBuild,
+            snap.owner, snap.distro_series.main_archive, arches[0],
+            PackagePublishingPocket.UPDATES,
+            target_architectures=["amd64", "i386"]
+        )
         # Changing the status of the old build allows a new build.
         old_build.updateStatus(BuildStatus.BUILDING)
         old_build.updateStatus(BuildStatus.FULLYBUILT)
@@ -3866,11 +3884,13 @@ class TestSnapWebservice(TestCaseWithFactory):
         snap = self.makeSnap(distroseries=distroseries, processors=[processor])
         response = self.webservice.named_post(
             snap["self_link"], "requestBuild", archive=archive_url,
-            distro_arch_series=distroarchseries_url, pocket="Updates")
+            distro_arch_series=distroarchseries_url, pocket="Updates",
+            target_architectures=[distroarchseries.architecturetag])
         self.assertEqual(201, response.status)
         response = self.webservice.named_post(
             snap["self_link"], "requestBuild", archive=archive_url,
-            distro_arch_series=distroarchseries_url, pocket="Updates")
+            distro_arch_series=distroarchseries_url, pocket="Updates",
+            target_architectures=[distroarchseries.architecturetag])
         self.assertEqual(400, response.status)
         self.assertEqual(
             b"An identical build of this snap package is already pending.",
@@ -4201,7 +4221,8 @@ class TestSnapWebservice(TestCaseWithFactory):
             auto_build_pocket=PackagePublishingPocket.PROPOSED)
         response = self.webservice.named_post(
             snap["self_link"], "requestBuild", archive=archive_url,
-            distro_arch_series=das_urls[0], pocket="Proposed")
+            distro_arch_series=das_urls[0], pocket="Proposed",
+            target_architectures=[dases[0].architecturetag])
         self.assertEqual(201, response.status)
         response = self.webservice.named_post(
             snap["self_link"], "requestAutoBuilds")
diff --git a/lib/lp/snappy/tests/test_snapbuild.py b/lib/lp/snappy/tests/test_snapbuild.py
index f11a097..fe16fc5 100644
--- a/lib/lp/snappy/tests/test_snapbuild.py
+++ b/lib/lp/snappy/tests/test_snapbuild.py
@@ -694,6 +694,10 @@ class TestSnapBuild(TestCaseWithFactory):
                 logger.output, LogsScheduledWebhooks([
                     (hook, "snap:build:0.1", MatchesDict(expected_payload))]))
 
+    def test_can_have_target_architectures(self):
+        build = self.factory.makeSnapBuild(target_architectures=["amd64"])
+        self.assertEqual(build.target_architectures, ["amd64"])
+
 
 class TestSnapBuildSet(TestCaseWithFactory):
 
diff --git a/lib/lp/snappy/tests/test_snapbuildbehaviour.py b/lib/lp/snappy/tests/test_snapbuildbehaviour.py
index db202cb..618c741 100644
--- a/lib/lp/snappy/tests/test_snapbuildbehaviour.py
+++ b/lib/lp/snappy/tests/test_snapbuildbehaviour.py
@@ -148,7 +148,7 @@ class TestSnapBuildBehaviourBase(TestCaseWithFactory):
 
         build = self.factory.makeSnapBuild(
             archive=archive, distroarchseries=distroarchseries, pocket=pocket,
-            name="test-snap", **kwargs)
+            name="test-snap", target_architectures=["i386"], **kwargs)
         return IBuildFarmJobBehaviour(build)
 
 
@@ -353,6 +353,7 @@ class TestAsyncSnapBuildBehaviour(StatsMixin, TestSnapBuildBehaviourBase):
             "revocation_endpoint":  RevocationEndpointMatcher(job, self.now),
             "series": Equals("unstable"),
             "trusted_keys": Equals(expected_trusted_keys),
+            "target_architectures": Equals(["i386"]),
             }))
 
     @defer.inlineCallbacks
@@ -394,6 +395,7 @@ class TestAsyncSnapBuildBehaviour(StatsMixin, TestSnapBuildBehaviourBase):
             "revocation_endpoint":  RevocationEndpointMatcher(job, self.now),
             "series": Equals("unstable"),
             "trusted_keys": Equals(expected_trusted_keys),
+            "target_architectures": Equals(["i386"]),
             }))
 
     @defer.inlineCallbacks
@@ -424,6 +426,7 @@ class TestAsyncSnapBuildBehaviour(StatsMixin, TestSnapBuildBehaviourBase):
             "revocation_endpoint":  RevocationEndpointMatcher(job, self.now),
             "series": Equals("unstable"),
             "trusted_keys": Equals(expected_trusted_keys),
+            "target_architectures": Equals(["i386"]),
             }))
 
     @defer.inlineCallbacks
@@ -471,6 +474,7 @@ class TestAsyncSnapBuildBehaviour(StatsMixin, TestSnapBuildBehaviourBase):
             "revocation_endpoint":  RevocationEndpointMatcher(job, self.now),
             "series": Equals("unstable"),
             "trusted_keys": Equals(expected_trusted_keys),
+            "target_architectures": Equals(["i386"]),
             }))
 
     @defer.inlineCallbacks
@@ -503,6 +507,7 @@ class TestAsyncSnapBuildBehaviour(StatsMixin, TestSnapBuildBehaviourBase):
             "revocation_endpoint":  RevocationEndpointMatcher(job, self.now),
             "series": Equals("unstable"),
             "trusted_keys": Equals(expected_trusted_keys),
+            "target_architectures": Equals(["i386"]),
             }))
 
     @defer.inlineCallbacks
@@ -533,6 +538,7 @@ class TestAsyncSnapBuildBehaviour(StatsMixin, TestSnapBuildBehaviourBase):
             "revocation_endpoint":  RevocationEndpointMatcher(job, self.now),
             "series": Equals("unstable"),
             "trusted_keys": Equals(expected_trusted_keys),
+            "target_architectures": Equals(["i386"]),
             }))
 
     @defer.inlineCallbacks
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 6a5e459..db3c389 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -5017,7 +5017,8 @@ class BareLaunchpadObjectFactory(ObjectFactory):
                       archive=None, distroarchseries=None, pocket=None,
                       snap_base=None, channels=None, date_created=DEFAULT,
                       build_request=None, status=BuildStatus.NEEDSBUILD,
-                      builder=None, duration=None, **kwargs):
+                      builder=None, duration=None, target_architectures=None,
+                      **kwargs):
         """Make a new SnapBuild."""
         if requester is None:
             requester = self.makePerson()
@@ -5046,7 +5047,8 @@ class BareLaunchpadObjectFactory(ObjectFactory):
         snapbuild = getUtility(ISnapBuildSet).new(
             requester, snap, archive, distroarchseries, pocket,
             snap_base=snap_base, channels=channels, date_created=date_created,
-            build_request=build_request)
+            build_request=build_request,
+            target_architectures=target_architectures)
         if duration is not None:
             removeSecurityProxy(snapbuild).updateStatus(
                 BuildStatus.BUILDING, builder=builder,
diff --git a/requirements/launchpad.txt b/requirements/launchpad.txt
index c2081c1..abde93b 100644
--- a/requirements/launchpad.txt
+++ b/requirements/launchpad.txt
@@ -73,7 +73,7 @@ lazr.delegates==2.0.4
 lazr.enum==1.2.1
 lazr.jobrunner==0.17
 lazr.lifecycle==1.2.1
-lazr.restful==1.1.0
+lazr.restful==2.0.0
 lazr.restfulclient==0.14.4
 lazr.sshserver==0.1.13
 lazr.uri==1.0.6