← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~alvarocs/launchpad:use-craft-platforms-for-snap into launchpad:master

 

Alvaro Crespo Serrano has proposed merging ~alvarocs/launchpad:use-craft-platforms-for-snap into launchpad:master.

Commit message:
Support craft-platforms build plan in charm builds

Updates Launchpad to support charm builds using 'craft-platforms' for the unified format. It also ensures the platform name ('craft_platform') is passed through the build pipeline.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~alvarocs/launchpad/+git/launchpad/+merge/485084
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~alvarocs/launchpad:use-craft-platforms-for-snap into launchpad:master.
diff --git a/lib/lp/snappy/adapters/buildarch.py b/lib/lp/snappy/adapters/buildarch.py
index 55e9238..31b77d8 100644
--- a/lib/lp/snappy/adapters/buildarch.py
+++ b/lib/lp/snappy/adapters/buildarch.py
@@ -6,6 +6,8 @@ __all__ = ["determine_architectures_to_build", "BadPropertyError"]
 from collections import Counter
 from typing import Any, Dict, List, Optional, Union
 
+from craft_platforms import BuildInfo, CraftPlatformsError, get_build_plan
+
 from lp.services.helpers import english_list
 from lp.snappy.interfaces.snapbase import SnapBaseFeature
 from lp.snappy.model.snapbase import SnapBase
@@ -83,6 +85,16 @@ class UnsupportedBuildOnError(SnapArchitecturesParserError):
         self.build_on = build_on
 
 
+class CraftPlatformsBuildPlanError(SnapArchitecturesParserError):
+    """Error raised when craft-platforms fails while generating
+    a build plan."""
+
+    def __init__(Self, message, resolution=None):
+        if resolution:
+            message += f" Resolution: {resolution}"
+        super().__init__(message)
+
+
 class SnapArchitecture:
     """A single entry in the snapcraft.yaml 'architectures' list."""
 
@@ -91,6 +103,7 @@ class SnapArchitecture:
         build_on: Union[str, List[str]],
         build_for: Optional[Union[str, List[str]]] = None,
         build_error: Optional[str] = None,
+        build_info: Optional[BuildInfo] = None,
     ):
         """Create a new architecture entry.
 
@@ -111,6 +124,7 @@ class SnapArchitecture:
         else:
             self.build_for = self.build_on
         self.build_error = build_error
+        self.build_info = build_info
 
     @classmethod
     def from_dict(cls, properties):
@@ -132,7 +146,7 @@ class SnapArchitecture:
 class SnapBuildInstance:
     """A single instance of a snap that should be built.
 
-    It has two useful attributes:
+    If has the following useful attributes:
 
       - architecture: The architecture tag that should be used to build the
             snap.
@@ -141,18 +155,21 @@ class SnapBuildInstance:
             in the case of cross-building)
       - required: Whether or not failure to build should cause the entire
             set to fail.
+      - platform_name: The platform to build for.
     """
 
     def __init__(
         self,
         architecture: SnapArchitecture,
         supported_architectures: List[str],
+        platform_name: str = None,
     ):
         """Construct a new `SnapBuildInstance`.
 
         :param architecture: `SnapArchitecture` instance.
         :param supported_architectures: List of supported architectures,
             sorted by priority.
+        : param platform_name: The platform to build for.
         """
         build_on = architecture.build_on
         # "all" indicates that the architecture doesn't matter.  Try to pick
@@ -172,6 +189,7 @@ class SnapBuildInstance:
 
         self.target_architectures = architecture.build_for
         self.required = architecture.build_error != "ignore"
+        self.platform_name = platform_name
 
 
 def determine_architectures_to_build(
@@ -187,16 +205,24 @@ def determine_architectures_to_build(
         we can create builds for.
     :return: a list of `SnapBuildInstance`s.
     """
-    architectures_list: Optional[List] = snapcraft_data.get("architectures")
-
-    if architectures_list:
-        architectures = parse_architectures_list(architectures_list)
-    elif "platforms" in snapcraft_data:
-        snap_base_name = snap_base.name if snap_base else "unknown"
-        architectures = parse_platforms(
-            snapcraft_data, supported_arches, snap_base_name
+    architectures = None
+    # 1) Snap with 'architectures' format
+    if "architectures" in snapcraft_data and snapcraft_data.get(
+        "architectures"
+    ):
+        # XXX tushar5526 2025-04-15: craft_platforms do not support
+        # "architectures" format used in core22 or older,
+        # fallback to the existing LP parsing logic in that case.
+        architectures_list: Optional[List] = snapcraft_data.get(
+            "architectures"
         )
-    else:
+        architectures = parse_architectures_list(architectures_list)
+    # 2) Snap with 'platforms' format
+    elif "platforms" in snapcraft_data and snapcraft_data.get("platforms"):
+        # Use craft-platforms to generate the build plan
+        architectures = parse_platforms(snapcraft_data)
+    # 3) Snap with no 'architectures' or 'platforms' format
+    if not architectures:
         # If no architectures are specified, build one for each supported
         # architecture.
         architectures = [
@@ -235,40 +261,55 @@ def parse_architectures_list(
 
 def parse_platforms(
     snapcraft_data: Dict[str, Any],
-    supported_arches: List[str],
-    base_name: str,
+    # supported_arches: List[str],
+    # base_name: str,
 ) -> List[SnapArchitecture]:
-    architectures = []
-    supported_arch_names = supported_arches
-
-    for platform, configuration in snapcraft_data["platforms"].items():
-        # The 'platforms' property and its values look like
-        # platforms:
-        #   ubuntu-amd64:
-        #     build-on: [amd64]
-        #     build-for: [amd64]
-        # 'ubuntu-amd64' will be the value of 'platform' and its value dict
-        # containing the keys 'build-on', 'build-for' will be the value of
-        # 'configuration'.
-        if configuration:
-            build_on = configuration.get("build-on", [platform])
-            build_for = configuration.get("build-for", build_on)
-            architectures.append(
-                SnapArchitecture(
-                    build_on=build_on,
-                    build_for=build_for,
-                )
-            )
-        elif platform in supported_arch_names:
-            architectures.append(
-                SnapArchitecture(build_on=[platform], build_for=[platform])
-            )
-        else:
-            raise BadPropertyError(
-                f"'{platform}' is not a supported platform for '{base_name}'."
-            )
 
-    return architectures
+    try:
+        exhaustive_build_plan = get_build_plan(
+            app="snapcraft",
+            project_data=snapcraft_data,
+        )
+    # XXX alvarocs 2025-04-04: craft-platforms currently raises
+    # 'ValueError' when it encounters malformed input such as an invalid
+    # base or platform name. These should instead raise
+    # 'CraftPlatformsError'. Bug tracked at:
+    # https://github.com/canonical/craft-platforms/issues/116
+    except (CraftPlatformsError, ValueError) as e:
+        message = getattr(e, "message", str(e))
+        resolution = getattr(e, "resolution", None)
+        raise CraftPlatformsBuildPlanError(
+            "Failed to compute the build plan for the snapcraft file "
+            "with error: "
+            f"{message}",
+            resolution=resolution,
+        )
+    platform_plans: Dict[str, BuildInfo] = {}
+    for plan in exhaustive_build_plan:
+        platform_plans.setdefault(plan.platform, []).append(plan)
+    instances_to_build: List[BuildInfo] = []
+    for _platform, pairs in platform_plans.items():
+        # One way of building for that platform, i.e one (info, das)
+        if len(pairs) == 1:
+            instances_to_build.append(pairs[0])
+            continue
+        # Multiple ways of building for that platform:
+        for info in pairs:
+            # Pick the native build
+            if info.build_on == info.build_for:
+                instances_to_build.append(info)
+                break
+        # Pick first one if none are native
+        else:
+            instances_to_build.append(pairs[0])
+    return [
+        SnapArchitecture(
+            build_on=str(instance.build_on),
+            build_for=str(instance.build_for),
+            build_info=instance,
+        )
+        for instance in instances_to_build
+    ]
 
 
 def validate_architectures(architectures: List[SnapArchitecture]):
@@ -298,8 +339,13 @@ def build_architectures_list(
     architectures_to_build = []
     for arch in architectures:
         try:
+            platform_name = (
+                arch.build_info.platform
+                if arch.build_info is not None
+                else None
+            )
             architectures_to_build.append(
-                SnapBuildInstance(arch, supported_arches)
+                SnapBuildInstance(arch, supported_arches, platform_name)
             )
         except UnsupportedBuildOnError:
             # Snaps are allowed to declare that they build on architectures
diff --git a/lib/lp/snappy/adapters/tests/test_buildarch.py b/lib/lp/snappy/adapters/tests/test_buildarch.py
index be9528c..bac0128 100644
--- a/lib/lp/snappy/adapters/tests/test_buildarch.py
+++ b/lib/lp/snappy/adapters/tests/test_buildarch.py
@@ -9,7 +9,7 @@ from testtools.matchers import HasLength, MatchesException, Raises
 from lp.snappy.adapters.buildarch import (
     AllConflictInBuildForError,
     AllConflictInBuildOnError,
-    BadPropertyError,
+    CraftPlatformsBuildPlanError,
     DuplicateBuildOnError,
     SnapArchitecture,
     SnapBuildInstance,
@@ -209,7 +209,7 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
 
     scenarios = [
         (
-            "none",
+            "none architectures, build one per supported architecture",
             {
                 "architectures": None,
                 "supported_architectures": ["amd64", "i386", "armhf"],
@@ -218,16 +218,19 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "amd64",
                         "target_architectures": ["amd64"],
                         "required": True,
+                        "platform_name": None,
                     },
                     {
                         "architecture": "i386",
                         "target_architectures": ["i386"],
                         "required": True,
+                        "platform_name": None,
                     },
                     {
                         "architecture": "armhf",
                         "target_architectures": ["armhf"],
                         "required": True,
+                        "platform_name": None,
                     },
                 ],
             },
@@ -244,6 +247,7 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "i386",
                         "target_architectures": ["amd64", "i386"],
                         "required": True,
+                        "platform_name": None,
                     }
                 ],
             },
@@ -258,6 +262,7 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "amd64",
                         "target_architectures": ["all"],
                         "required": True,
+                        "platform_name": None,
                     }
                 ],
             },
@@ -275,11 +280,13 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "amd64",
                         "target_architectures": ["amd64"],
                         "required": True,
+                        "platform_name": None,
                     },
                     {
                         "architecture": "i386",
                         "target_architectures": ["i386"],
                         "required": True,
+                        "platform_name": None,
                     },
                 ],
             },
@@ -297,11 +304,13 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "amd64",
                         "target_architectures": ["amd64"],
                         "required": True,
+                        "platform_name": None,
                     },
                     {
                         "architecture": "i386",
                         "target_architectures": ["i386"],
                         "required": True,
+                        "platform_name": None,
                     },
                 ],
             },
@@ -324,16 +333,19 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "amd64",
                         "target_architectures": ["amd64"],
                         "required": True,
+                        "platform_name": None,
                     },
                     {
                         "architecture": "i386",
                         "target_architectures": ["i386"],
                         "required": True,
+                        "platform_name": None,
                     },
                     {
                         "architecture": "armhf",
                         "target_architectures": ["armhf"],
                         "required": False,
+                        "platform_name": None,
                     },
                 ],
             },
@@ -350,6 +362,7 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "amd64",
                         "target_architectures": ["all"],
                         "required": True,
+                        "platform_name": None,
                     }
                 ],
             },
@@ -366,6 +379,7 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "i386",
                         "target_architectures": ["all"],
                         "required": True,
+                        "platform_name": None,
                     }
                 ],
             },
@@ -380,6 +394,7 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "i386",
                         "target_architectures": ["amd64", "i386"],
                         "required": True,
+                        "platform_name": None,
                     }
                 ],
             },
@@ -394,6 +409,7 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "amd64",
                         "target_architectures": ["amd64", "i386"],
                         "required": True,
+                        "platform_name": None,
                     }
                 ],
             },
@@ -412,11 +428,13 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "amd64",
                         "target_architectures": ["amd64"],
                         "required": True,
+                        "platform_name": None,
                     },
                     {
                         "architecture": "i386",
                         "target_architectures": ["i386"],
                         "required": True,
+                        "platform_name": None,
                     },
                 ],
             },
@@ -459,11 +477,13 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "amd64",
                         "target_architectures": ["amd64"],
                         "required": True,
+                        "platform_name": None,
                     },
                     {
                         "architecture": "amd64",
                         "target_architectures": ["i386"],
                         "required": True,
+                        "platform_name": None,
                     },
                 ],
             },
@@ -485,6 +505,7 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
         (
             "platforms with configuration",
             {
+                "base": "core24",
                 "platforms": {
                     "ubuntu-amd64": {
                         "build-on": ["amd64"],
@@ -501,11 +522,13 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "amd64",
                         "target_architectures": ["amd64"],
                         "required": True,
+                        "platform_name": "ubuntu-amd64",
                     },
                     {
                         "architecture": "i386",
                         "target_architectures": ["i386"],
                         "required": True,
+                        "platform_name": "ubuntu-i386",
                     },
                 ],
             },
@@ -513,9 +536,10 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
         (
             "platforms with shorthand configuration",
             {
+                "base": "core24",
                 "platforms": {
-                    "amd64": {},
-                    "i386": {},
+                    "amd64": None,
+                    "i386": None,
                 },
                 "supported_architectures": ["amd64", "i386", "armhf"],
                 "expected": [
@@ -523,11 +547,13 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "amd64",
                         "target_architectures": ["amd64"],
                         "required": True,
+                        "platform_name": "amd64",
                     },
                     {
                         "architecture": "i386",
                         "target_architectures": ["i386"],
                         "required": True,
+                        "platform_name": "i386",
                     },
                 ],
             },
@@ -535,20 +561,27 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
         (
             "platforms with unsupported architecture",
             {
+                "base": "core24",
                 "platforms": {
-                    "ubuntu-unsupported": {},
+                    "ubuntu-unsupported": None,
                 },
                 "supported_architectures": ["amd64", "i386", "armhf"],
                 "expected_exception": MatchesException(
-                    BadPropertyError,
-                    r"\'ubuntu-unsupported\' is not a supported platform for "
-                    r"\'snap-base-name-.*\'",
+                    CraftPlatformsBuildPlanError,
+                    "Failed to compute the build plan for the snapcraft "
+                    r"file with error*",
                 ),
             },
         ),
         (
+            # multiple architecture values in "build-for" and "build-on"
+            # is not allowed by snapcraft and such configs are invalid.
+            # As craft_platforms is a separate, generalized API, it still
+            # returns a build plan which we then filter and pair a native
+            # build with native architecture to run on.
             "platforms with multiple architectures",
             {
+                "base": "core24",
                 "platforms": {
                     "ubuntu-amd64-i386": {
                         "build-on": ["amd64", "i386"],
@@ -559,8 +592,9 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                 "expected": [
                     {
                         "architecture": "amd64",
-                        "target_architectures": ["amd64", "i386"],
+                        "target_architectures": ["amd64"],
                         "required": True,
+                        "platform_name": "ubuntu-amd64-i386",
                     },
                 ],
             },
@@ -568,6 +602,7 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
         (
             "platforms with conflict in build-on",
             {
+                "base": "core24",
                 "platforms": {
                     "ubuntu-conflict": {
                         "build-on": ["all", "amd64"],
@@ -575,13 +610,16 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                 },
                 "supported_architectures": ["amd64", "i386", "armhf"],
                 "expected_exception": MatchesException(
-                    AllConflictInBuildOnError
+                    CraftPlatformsBuildPlanError,
+                    "Failed to compute the build plan for the snapcraft "
+                    "file with error: 'all' is not a valid DebianArchitecture",
                 ),
             },
         ),
         (
             "platforms with conflict in build-for",
             {
+                "base": "core24",
                 "platforms": {
                     "ubuntu-conflict": {
                         "build-on": ["amd64"],
@@ -590,41 +628,55 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                 },
                 "supported_architectures": ["amd64", "i386", "armhf"],
                 "expected_exception": MatchesException(
-                    AllConflictInBuildForError, r".*"
+                    CraftPlatformsBuildPlanError,
+                    "Failed to compute the build plan for the snapcraft "
+                    "file with error: build-for: all must be the only "
+                    "build-for architecture Resolution: Provide only one "
+                    "platform with only build-for: all or remove 'all' from "
+                    "build-for options.",
                 ),
             },
         ),
         (
-            "platforms with unsupported architecture in build-on",
+            "platforms with invalid architecture in build-on",
             {
+                "base": "core24",
                 "platforms": {
                     "ubuntu-amd64": {
-                        "build-on": ["unsupported"],
+                        "build-on": ["invalid"],
                         "build-for": ["amd64"],
                     },
                 },
                 "supported_architectures": ["amd64", "i386", "armhf"],
-                # Launchpad ignores architectures that it does not know about
-                "expected": [],
+                "expected_exception": MatchesException(
+                    CraftPlatformsBuildPlanError,
+                    (
+                        "Failed to compute the build plan for the snapcraft "
+                        "file with error: 'invalid' is not a valid "
+                        "DebianArchitecture"
+                    ),
+                ),
             },
         ),
         (
-            "platforms with 1/2 unsupported architectures in build-on",
+            "platforms with invalid architecture in build-for",
             {
+                "base": "core24",
                 "platforms": {
                     "ubuntu-amd64": {
-                        "build-on": ["unsupported", "amd64"],
-                        "build-for": ["amd64"],
+                        "build-on": ["amd64"],
+                        "build-for": ["invalid"],
                     },
                 },
                 "supported_architectures": ["amd64", "i386", "armhf"],
-                "expected": [
-                    {
-                        "architecture": "amd64",
-                        "target_architectures": ["amd64"],
-                        "required": True,
-                    },
-                ],
+                "expected_exception": MatchesException(
+                    CraftPlatformsBuildPlanError,
+                    (
+                        "Failed to compute the build plan for the snapcraft "
+                        "file with error: 'invalid' is not a valid "
+                        "DebianArchitecture"
+                    ),
+                ),
             },
         ),
         (
@@ -633,6 +685,7 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                 "snap_base_features": {
                     SnapBaseFeature.ALLOW_DUPLICATE_BUILD_ON: False
                 },
+                "base": "core24",
                 "platforms": {
                     "ubuntu-amd64": {
                         "build-on": ["amd64"],
@@ -653,6 +706,7 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                 "snap_base_features": {
                     SnapBaseFeature.ALLOW_DUPLICATE_BUILD_ON: True
                 },
+                "base": "core24",
                 "platforms": {
                     "ubuntu-amd64": {
                         "build-on": ["amd64"],
@@ -669,18 +723,21 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                         "architecture": "amd64",
                         "target_architectures": ["amd64"],
                         "required": True,
+                        "platform_name": "ubuntu-amd64",
                     },
                     {
                         "architecture": "amd64",
                         "target_architectures": ["i386"],
                         "required": True,
+                        "platform_name": "ubuntu-amd64-i386",
                     },
                 ],
             },
         ),
         (
-            "platforms with all keyword",
+            "platforms with 'all' keyword in 'build-on'",
             {
+                "base": "core24",
                 "platforms": {
                     "ubuntu-all": {
                         "build-on": ["all"],
@@ -688,11 +745,52 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
                     },
                 },
                 "supported_architectures": ["amd64", "i386", "armhf"],
+                "expected_exception": MatchesException(
+                    CraftPlatformsBuildPlanError,
+                    "Failed to compute the build plan for the snapcraft "
+                    "file with error: 'all' is not a valid DebianArchitecture",
+                ),
+            },
+        ),
+        (
+            "platforms with 'all' keyword in 'build-for'",
+            {
+                "base": "core24",
+                "platforms": {
+                    "ubuntu-all": {
+                        "build-on": ["amd64"],
+                        "build-for": ["all"],
+                    },
+                },
+                "supported_architectures": ["amd64", "i386", "armhf"],
                 "expected": [
                     {
                         "architecture": "amd64",
                         "target_architectures": ["all"],
                         "required": True,
+                        "platform_name": "ubuntu-all",
+                    },
+                ],
+            },
+        ),
+        (
+            "empty platforms dictionary",
+            {
+                "base": "core24",
+                "platforms": {},
+                "supported_architectures": ["amd64", "i386"],
+                "expected": [
+                    {
+                        "architecture": "amd64",
+                        "target_architectures": ["amd64"],
+                        "required": True,
+                        "platform_name": None,
+                    },
+                    {
+                        "architecture": "i386",
+                        "target_architectures": ["i386"],
+                        "required": True,
+                        "platform_name": None,
                     },
                 ],
             },
@@ -705,6 +803,12 @@ class TestDetermineArchitecturesToBuild(WithScenarios, TestCaseWithFactory):
             snapcraft_data["architectures"] = self.architectures
         if hasattr(self, "platforms"):
             snapcraft_data["platforms"] = self.platforms
+        if hasattr(self, "base"):
+            snapcraft_data["base"] = self.base
+        # if hasattr(self, "type"):
+        #     snapcraft_data["type"] = self.type
+        # if hasattr(self, "build-base"):
+        #     snapcraft_data["build-base"] = self.build_base
         snap_base_features = getattr(self, "snap_base_features", {})
         snap_base = self.factory.makeSnapBase(features=snap_base_features)
         if hasattr(self, "expected_exception"):
diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
index 90bfbed..890dcbc 100644
--- a/lib/lp/snappy/interfaces/snap.py
+++ b/lib/lp/snappy/interfaces/snap.py
@@ -471,6 +471,7 @@ class ISnapView(Interface):
         channels=None,
         build_request=None,
         target_architectures=None,
+        craft_platfrom=None,
     ):
         """Request that the snap package be built.
 
@@ -485,6 +486,7 @@ class ISnapView(Interface):
             if any.
         :param target_architectures: The optional list of target architectures
             to build the snap for.
+        :param craft_platform: The platform name to build for.
         :return: `ISnapBuild`.
         """
 
diff --git a/lib/lp/snappy/interfaces/snapbuild.py b/lib/lp/snappy/interfaces/snapbuild.py
index 3416200..ef877a9 100644
--- a/lib/lp/snappy/interfaces/snapbuild.py
+++ b/lib/lp/snappy/interfaces/snapbuild.py
@@ -352,6 +352,14 @@ class ISnapBuildView(IPackageBuildView, IPrivacy):
         )
     )
 
+    craft_platform = exported(
+        TextLine(
+            title=_("Craft platform name"),
+            required=False,
+            readonly=True,
+        )
+    )
+
     def getFiles():
         """Retrieve the build's `ISnapFile` records.
 
@@ -432,6 +440,7 @@ class ISnapBuildSet(ISpecificBuildFarmJobSource):
         store_upload_metadata=None,
         build_request=None,
         target_architectures=None,
+        craft_platform=None,
     ):
         """Create an `ISnapBuild`."""
 
diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
index 735dd1d..dfe2282 100644
--- a/lib/lp/snappy/model/snap.py
+++ b/lib/lp/snappy/model/snap.py
@@ -858,6 +858,7 @@ class Snap(StormBase, WebhookTargetMixin):
         channels=None,
         build_request=None,
         target_architectures: t.Optional[t.List[str]] = None,
+        craft_platform: str = None,
     ) -> ISnapBuild:
         """See `ISnap`."""
         self._checkRequestBuild(requester, archive)
@@ -884,6 +885,7 @@ class Snap(StormBase, WebhookTargetMixin):
             SnapBuild.target_architectures == target_architectures,
             channels_clause,
             SnapBuild.status == BuildStatus.NEEDSBUILD,
+            SnapBuild.craft_platform == craft_platform,
         )
         if pending.any() is not None:
             raise SnapBuildAlreadyPending
@@ -898,6 +900,7 @@ class Snap(StormBase, WebhookTargetMixin):
             channels=channels,
             build_request=build_request,
             target_architectures=target_architectures,
+            craft_platform=craft_platform,
         )
         build.queueBuild()
         notify(ObjectCreatedEvent(build, user=requester))
@@ -1059,13 +1062,15 @@ class Snap(StormBase, WebhookTargetMixin):
                     channels=arch_channels,
                     build_request=build_request,
                     target_architectures=build_instance.target_architectures,
+                    craft_platform=build_instance.platform_name,
                 )
                 if logger is not None:
                     logger.debug(
-                        " - %s/%s/%s: Build requested.",
+                        " - %s/%s/%s/%s: Build requested.",
                         self.owner.name,
                         self.name,
                         arch,
+                        build_instance.platform_name,
                     )
                 builds.append(build)
             except SnapBuildAlreadyPending:
@@ -1075,7 +1080,12 @@ class Snap(StormBase, WebhookTargetMixin):
                     raise
                 elif logger is not None:
                     logger.exception(
-                        " - %s/%s/%s: %s", self.owner.name, self.name, arch, e
+                        " - %s/%s/%s/%s: %s",
+                        self.owner.name,
+                        self.name,
+                        arch,
+                        build_instance.platform_name,
+                        e,
                     )
         return builds
 
diff --git a/lib/lp/snappy/model/snapbuild.py b/lib/lp/snappy/model/snapbuild.py
index 842b142..5d83c3e 100644
--- a/lib/lp/snappy/model/snapbuild.py
+++ b/lib/lp/snappy/model/snapbuild.py
@@ -186,6 +186,8 @@ class SnapBuild(PackageBuildMixin, StormBase):
 
     store_upload_metadata = JSON("store_upload_json_data", allow_none=True)
 
+    craft_platform = Unicode(name="craft_platform", allow_none=True)
+
     def __init__(
         self,
         build_farm_job,
@@ -202,6 +204,7 @@ class SnapBuild(PackageBuildMixin, StormBase):
         store_upload_metadata=None,
         build_request=None,
         target_architectures=None,
+        craft_platform=None,
     ):
         """Construct a `SnapBuild`."""
         super().__init__()
@@ -221,6 +224,7 @@ class SnapBuild(PackageBuildMixin, StormBase):
         if build_request is not None:
             self.build_request_id = build_request.id
         self.status = BuildStatus.NEEDSBUILD
+        self.craft_platform = craft_platform
 
     @property
     def build_request(self):
@@ -566,6 +570,7 @@ class SnapBuildSet(SpecificBuildFarmJobSourceMixin):
         store_upload_metadata=None,
         build_request=None,
         target_architectures=None,
+        craft_platform=None,
     ):
         """See `ISnapBuildSet`."""
         store = IPrimaryStore(SnapBuild)
@@ -593,6 +598,7 @@ class SnapBuildSet(SpecificBuildFarmJobSourceMixin):
             store_upload_metadata=store_upload_metadata,
             build_request=build_request,
             target_architectures=target_architectures,
+            craft_platform=craft_platform,
         )
         store.add(snapbuild)
         store.flush()
diff --git a/lib/lp/snappy/model/snapbuildbehaviour.py b/lib/lp/snappy/model/snapbuildbehaviour.py
index bfbb1e5..cbd9391 100644
--- a/lib/lp/snappy/model/snapbuildbehaviour.py
+++ b/lib/lp/snappy/model/snapbuildbehaviour.py
@@ -201,6 +201,8 @@ class SnapBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
         args["target_architectures"] = removeSecurityProxy(
             build.target_architectures
         )
+        if build.craft_platform:
+            args["craft_platform"] = build.craft_platform
         return args
 
     def verifySuccessfulBuild(self):
diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
index df0343b..dba8e01 100644
--- a/lib/lp/snappy/tests/test_snap.py
+++ b/lib/lp/snappy/tests/test_snap.py
@@ -696,6 +696,23 @@ class TestSnap(TestCaseWithFactory):
                     ),
                 )
 
+    def test_requestBuild_with_platform_name(self):
+        processor = self.factory.makeProcessor(supports_virtualized=True)
+        distroarchseries = self.makeBuildableDistroArchSeries(
+            processor=processor
+        )
+        snap = self.factory.makeSnap(
+            distroseries=distroarchseries.distroseries, processors=[processor]
+        )
+        build = snap.requestBuild(
+            snap.owner,
+            snap.distro_series.main_archive,
+            distroarchseries,
+            PackagePublishingPocket.UPDATES,
+            craft_platform="ubuntu-amd64",
+        )
+        self.assertEqual("ubuntu-amd64", build.craft_platform)
+
     def test_requestBuilds(self):
         # requestBuilds schedules a job and returns a corresponding
         # SnapBuildRequest.

Follow ups