← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~ruinedyourlife/launchpad:requesting-subset-of-arch-craft-builds into launchpad:master

 

Quentin Debhi has proposed merging ~ruinedyourlife/launchpad:requesting-subset-of-arch-craft-builds into launchpad:master with ~ruinedyourlife/launchpad:add-webservice-for-craft-recipes as a prerequisite.

Commit message:
Enable requesting a subset of architectures for craft builds

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~ruinedyourlife/launchpad/+git/launchpad/+merge/474360
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~ruinedyourlife/launchpad:requesting-subset-of-arch-craft-builds into launchpad:master.
diff --git a/lib/lp/crafts/interfaces/craftrecipe.py b/lib/lp/crafts/interfaces/craftrecipe.py
index 30be82e..1290e2e 100644
--- a/lib/lp/crafts/interfaces/craftrecipe.py
+++ b/lib/lp/crafts/interfaces/craftrecipe.py
@@ -69,6 +69,7 @@ from lp.app.errors import NameLookupFailed
 from lp.app.interfaces.informationtype import IInformationType
 from lp.app.validators.name import name_validator
 from lp.app.validators.path import path_does_not_escape
+from lp.buildmaster.interfaces.processor import IProcessor
 from lp.code.interfaces.gitref import IGitRef
 from lp.code.interfaces.gitrepository import IGitRepository
 from lp.registry.interfaces.person import IPerson
@@ -378,7 +379,12 @@ class ICraftRecipeView(Interface):
             ),
             key_type=TextLine(),
             required=False,
-        )
+        ),
+        architectures=List(
+            title=_("The list of architectures to build for this recipe."),
+            value_type=Reference(schema=IProcessor),
+            required=False,
+        ),
     )
     @export_factory_operation(ICraftRecipeBuildRequest, [])
     @operation_for_version("devel")
diff --git a/lib/lp/crafts/model/craftrecipe.py b/lib/lp/crafts/model/craftrecipe.py
index cd2203c..1368194 100644
--- a/lib/lp/crafts/model/craftrecipe.py
+++ b/lib/lp/crafts/model/craftrecipe.py
@@ -490,23 +490,16 @@ class CraftRecipe(StormBase):
             # Sort by (Distribution.id, DistroSeries.id, Processor.id) for
             # determinism.  This is chosen to be a similar order as in
             # BinaryPackageBuildSet.createForSource, to minimize confusion.
-            supported_arches = [
-                das
-                for das in sorted(
-                    self.getAllowedArchitectures(),
-                    key=attrgetter(
-                        "distroseries.distribution.id",
-                        "distroseries.id",
-                        "processor.id",
-                    ),
-                )
-                if (
-                    architectures is None
-                    or das.architecturetag in architectures
-                )
-            ]
+            supported_arches = sorted(
+                self.getAllowedArchitectures(),
+                key=attrgetter(
+                    "distroseries.distribution.id",
+                    "distroseries.id",
+                    "processor.id",
+                ),
+            )
             instances_to_build = determine_instances_to_build(
-                sourcecraft_data, supported_arches
+                sourcecraft_data, supported_arches, architectures
             )
         except Exception as e:
             if not allow_failures:
diff --git a/lib/lp/crafts/model/craftrecipejob.py b/lib/lp/crafts/model/craftrecipejob.py
index 799e6e7..d573665 100644
--- a/lib/lp/crafts/model/craftrecipejob.py
+++ b/lib/lp/crafts/model/craftrecipejob.py
@@ -174,6 +174,14 @@ class CraftRecipeRequestBuildsJob(CraftRecipeJobDerived):
     @classmethod
     def create(cls, recipe, requester, channels=None, architectures=None):
         """See `ICraftRecipeRequestBuildsJobSource`."""
+        # architectures can be a iterable of strings or Processors
+        # in the latter case, we need to convert them to strings
+        if architectures and all(
+            not isinstance(arch, str) for arch in architectures
+        ):
+            architectures = [
+                architecture.name for architecture in architectures
+            ]
         metadata = {
             "requester": requester.id,
             "channels": channels,
diff --git a/lib/lp/crafts/tests/test_craftrecipe.py b/lib/lp/crafts/tests/test_craftrecipe.py
index eac12e9..aaa2316 100644
--- a/lib/lp/crafts/tests/test_craftrecipe.py
+++ b/lib/lp/crafts/tests/test_craftrecipe.py
@@ -1497,6 +1497,130 @@ class TestCraftRecipeWebservice(TestCaseWithFactory):
             ),
         )
 
+    def test_requestBuilds_with_architectures(self):
+        # when a subset of architectures are requested, we only build them
+        # not all listed in the sourcecraft.yaml file
+        distroseries = self.factory.makeDistroSeries(
+            distribution=getUtility(ILaunchpadCelebrities).ubuntu,
+            registrant=self.person,
+        )
+        amd640 = self.factory.makeProcessor(
+            name="amd640", supports_virtualized=True
+        )
+        risc500 = self.factory.makeProcessor(
+            name="risc500", supports_virtualized=True
+        )
+        s400x = self.factory.makeProcessor(
+            name="s400x", supports_virtualized=True
+        )
+        processors = [amd640, risc500, s400x]
+        for processor in processors:
+            self.makeBuildableDistroArchSeries(
+                distroseries=distroseries,
+                architecturetag=processor.name,
+                processor=processor,
+                owner=self.person,
+            )
+        [git_ref] = self.factory.makeGitRefs()
+        recipe = self.makeCraftRecipe(git_ref=git_ref)
+        now = get_transaction_timestamp(IStore(distroseries))
+        # api_base = "http://api.launchpad.test/devel";
+        # amd640_api_url = api_base  api_url(amd640)
+        response = self.webservice.named_post(
+            recipe["self_link"],
+            "requestBuilds",
+            channels={"sourcecraft": "edge"},
+            architectures=[api_url(amd640), api_url(risc500)],
+        )
+        self.assertEqual(201, response.status)
+        build_request_url = response.getHeader("Location")
+        build_request = self.webservice.get(build_request_url).jsonBody()
+        self.assertThat(
+            build_request,
+            ContainsDict(
+                {
+                    "date_requested": AfterPreprocessing(
+                        iso8601.parse_date, GreaterThan(now)
+                    ),
+                    "date_finished": Is(None),
+                    "recipe_link": Equals(recipe["self_link"]),
+                    "status": Equals("Pending"),
+                    "error_message": Is(None),
+                    "builds_collection_link": Equals(
+                        build_request_url + "/builds"
+                    ),
+                }
+            ),
+        )
+        self.assertEqual([], self.getCollectionLinks(build_request, "builds"))
+        with person_logged_in(self.person):
+            sourcecraft_yaml = f"""
+                name: ruff
+                license: MIT
+                version: 0.4.9
+                summary: An extremely fast Python linter, written in Rust.
+                description: Ruff aims to be orders of magnitude faster...
+                base: ubuntu@{distroseries.version}
+                platforms:
+                """
+            for processor in processors:
+                sourcecraft_yaml += f"""
+                    {processor.name}:
+                        build-on: [{processor.name}]
+                        build-for: {processor.name}
+                    """
+            self.useFixture(GitHostingFixture(blob=sourcecraft_yaml))
+            [job] = getUtility(ICraftRecipeRequestBuildsJobSource).iterReady()
+            with dbuser(config.ICraftRecipeRequestBuildsJobSource.dbuser):
+                JobRunner([job]).runAll()
+        date_requested = iso8601.parse_date(build_request["date_requested"])
+        now = get_transaction_timestamp(IStore(distroseries))
+        build_request = self.webservice.get(
+            build_request["self_link"]
+        ).jsonBody()
+        self.assertThat(
+            build_request,
+            ContainsDict(
+                {
+                    "date_requested": AfterPreprocessing(
+                        iso8601.parse_date, Equals(date_requested)
+                    ),
+                    "date_finished": AfterPreprocessing(
+                        iso8601.parse_date,
+                        MatchesAll(GreaterThan(date_requested), LessThan(now)),
+                    ),
+                    "recipe_link": Equals(recipe["self_link"]),
+                    "status": Equals("Completed"),
+                    "error_message": Is(None),
+                    "builds_collection_link": Equals(
+                        build_request_url + "/builds"
+                    ),
+                }
+            ),
+        )
+        builds = self.webservice.get(
+            build_request["builds_collection_link"]
+        ).jsonBody()["entries"]
+        with person_logged_in(self.person):
+            self.assertThat(
+                builds,
+                MatchesSetwise(
+                    *(
+                        ContainsDict(
+                            {
+                                "recipe_link": Equals(recipe["self_link"]),
+                                "archive_link": Equals(
+                                    self.getURL(distroseries.main_archive)
+                                ),
+                                "arch_tag": Equals(processor.name),
+                                "channels": Equals({"sourcecraft": "edge"}),
+                            }
+                        )
+                        for processor in (amd640, risc500)  # requested arches
+                    )
+                ),
+            )
+
     def test_getBuilds(self):
         # The builds, completed_builds, and pending_builds properties are as
         # expected.

Follow ups