← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~jugmac00/launchpad:parse-actual-rockcraft-yaml-format into launchpad:master

 

Jürgen Gmach has proposed merging ~jugmac00/launchpad:parse-actual-rockcraft-yaml-format into launchpad:master with ~jugmac00/launchpad:add-explicit-model-for-rock-bases as a prerequisite.

Commit message:
Parse actual rockcraft.yaml syntax

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jugmac00/launchpad/+git/launchpad/+merge/473458
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/launchpad:parse-actual-rockcraft-yaml-format into launchpad:master.
diff --git a/lib/lp/rocks/adapters/buildarch.py b/lib/lp/rocks/adapters/buildarch.py
index 281924a..bea1040 100644
--- a/lib/lp/rocks/adapters/buildarch.py
+++ b/lib/lp/rocks/adapters/buildarch.py
@@ -6,6 +6,10 @@ __all__ = [
 ]
 
 import json
+<<<<<<< lib/lp/rocks/adapters/buildarch.py
+=======
+import re
+>>>>>>> lib/lp/rocks/adapters/buildarch.py
 from collections import Counter, OrderedDict
 
 from lp.services.helpers import english_list
@@ -18,10 +22,17 @@ class RockBasesParserError(Exception):
 class MissingPropertyError(RockBasesParserError):
     """Error for when an expected property is not present in the YAML."""
 
+<<<<<<< lib/lp/rocks/adapters/buildarch.py
     def __init__(self, prop):
         super().__init__(
             f"Base specification is missing the {prop!r} property"
         )
+=======
+    def __init__(self, prop, msg=None):
+        if msg is None:
+            msg = f"Base specification is missing the {prop!r} property"
+        super().__init__(msg)
+>>>>>>> lib/lp/rocks/adapters/buildarch.py
         self.property = prop
 
 
@@ -127,6 +138,86 @@ def determine_instances_to_build(
     rockcraft_data, supported_arches, default_distro_series
 ):
 =======
+class UnifiedRockBaseConfiguration:
+    """A unified base configuration in rockcraft.yaml"""
+
+    def __init__(self, build_on, run_on=None):
+        self.build_on = build_on
+        self.run_on = list(build_on) if run_on is None else run_on
+
+    @classmethod
+    def from_dict(cls, rockcraft_data, supported_arches):
+        base = rockcraft_data["base"]
+        if isinstance(base, str):
+            # Expected short-form value looks like 'ubuntu@24.04'
+            match = re.match(r"(.+)@(.+)", base)
+            if not match:
+                raise BadPropertyError(
+                    f"Invalid value for base '{base}'. Expected value should "
+                    "be like 'ubuntu@24.04'"
+                )
+            base_name, base_channel = match.groups()
+        else:
+            # Expected value looks like {"name": "ubuntu", "channel": "24.04"}
+            base_name = base["name"]
+            # If a value like 24.04 is unquoted in yaml, it will be
+            # interpreted as a float. So we convert it to a string.
+            base_channel = str(base["channel"])
+
+        # XXX jugmac00 2024-09-18: Find out if we need 'build-base' or not.
+        # There is no existing code that is using that.
+
+        platforms = rockcraft_data.get("platforms")
+        if not platforms:
+            raise MissingPropertyError(
+                "platforms", "The 'platforms' property is required"
+            )
+        configs = []
+        for platform, configuration in 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'.
+            name = base_name
+            channel = base_channel
+            if configuration:
+                build_on = configuration["build-on"]
+                if isinstance(build_on, str):
+                    build_on = [build_on]
+
+                build_on = [
+                    RockBase(name, channel, architecture)
+                    for architecture in build_on
+                ]
+
+                build_for = configuration["build-for"]
+                if isinstance(build_for, str):
+                    build_for = [build_for]
+
+                build_for = [
+                    RockBase(name, channel, architecture)
+                    for architecture in build_for
+                ]
+            else:
+                supported_arch_names = (
+                    das.architecturetag for das in supported_arches
+                )
+                if platform in supported_arch_names:
+                    build_on = [RockBase(name, channel, platform)]
+                    build_for = [RockBase(name, channel, platform)]
+                else:
+                    raise BadPropertyError(
+                        f"'{platform}' is not a supported architecture "
+                        f"for '{base_name}@{base_channel}'."
+                    )
+            configs.append(cls(build_on, build_for))
+        return configs
+
+
 def determine_instances_to_build(rockcraft_data, supported_arches):
 >>>>>>> lib/lp/rocks/adapters/buildarch.py
     """Return a list of instances to build based on rockcraft.yaml.
@@ -166,8 +257,9 @@ def determine_instances_to_build(rockcraft_data, supported_arches):
 =======
     :return: A list of `DistroArchSeries`.
     """
-    bases_list = rockcraft_data.get("bases")
-    configs = [RockBaseConfiguration.from_dict(item) for item in bases_list]
+    configs = UnifiedRockBaseConfiguration.from_dict(
+        rockcraft_data, supported_arches
+    )
 >>>>>>> lib/lp/rocks/adapters/buildarch.py
     # Ensure that multiple `run-on` items don't overlap; this is ambiguous
     # and forbidden by rockcraft.
diff --git a/lib/lp/rocks/tests/test_rockrecipe.py b/lib/lp/rocks/tests/test_rockrecipe.py
index 8c41291..7e39d08 100644
--- a/lib/lp/rocks/tests/test_rockrecipe.py
+++ b/lib/lp/rocks/tests/test_rockrecipe.py
@@ -29,6 +29,10 @@ from testtools.matchers import (
     MatchesSetwise,
     MatchesStructure,
 )
+<<<<<<< lib/lp/rocks/tests/test_rockrecipe.py
+=======
+from testtools.testcase import ExpectedException
+>>>>>>> lib/lp/rocks/tests/test_rockrecipe.py
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -59,6 +63,7 @@ from lp.buildmaster.model.buildfarmjob import BuildFarmJob
 from lp.buildmaster.model.buildqueue import BuildQueue
 from lp.code.tests.helpers import GitHostingFixture
 from lp.registry.enums import PersonVisibility, TeamMembershipPolicy
+from lp.rocks.adapters.buildarch import BadPropertyError, MissingPropertyError
 from lp.rocks.interfaces.rockrecipe import (
     ROCK_RECIPE_ALLOW_CREATE,
     ROCK_RECIPE_PRIVATE_FEATURE_FLAG,
@@ -455,6 +460,276 @@ class TestRockRecipe(TestCaseWithFactory):
             builds, job, "20.04", ["sparc", "avr"], job.channels
         )
 
+    def test_requestBuildsFromJob_unified_rockcraft_yaml_invalid_short_base(
+        self,
+    ):
+        self.useFixture(
+            GitHostingFixture(
+                blob=dedent(
+                    """\
+                    base: ubuntu-24.04
+                    platforms:
+                        ubuntu-amd64:
+                            build-on: [amd64]
+                            build-for: [amd64]
+                    """
+                )
+            )
+        )
+        job = self.makeRequestBuildsJob("24.04", ["amd64", "riscv64", "arm64"])
+
+        self.assertEqual(
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
+        transaction.commit()
+        with person_logged_in(job.requester):
+            with ExpectedException(
+                BadPropertyError,
+                "Invalid value for base 'ubuntu-24.04'. "
+                "Expected value should be like 'ubuntu@24.04'",
+            ):
+                job.recipe.requestBuildsFromJob(
+                    job.build_request,
+                    channels=removeSecurityProxy(job.channels),
+                )
+
+    def test_requestBuildsFromJob_unified_rockcraft_yaml_platforms_missing(
+        self,
+    ):
+        self.useFixture(
+            GitHostingFixture(
+                blob=dedent(
+                    """\
+                    base: ubuntu@24.04
+                    """
+                )
+            )
+        )
+        job = self.makeRequestBuildsJob("24.04", ["amd64", "riscv64", "arm64"])
+
+        self.assertEqual(
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
+        transaction.commit()
+        with person_logged_in(job.requester):
+            with ExpectedException(
+                MissingPropertyError, "The 'platforms' property is required"
+            ):
+                job.recipe.requestBuildsFromJob(
+                    job.build_request,
+                    channels=removeSecurityProxy(job.channels),
+                )
+
+    def test_requestBuildsFromJob_unified_rockcraft_yaml_fully_expanded(self):
+        self.useFixture(
+            GitHostingFixture(
+                blob=dedent(
+                    """\
+                    base:
+                        name: ubuntu
+                        channel: 24.04
+                    platforms:
+                        ubuntu-amd64:
+                            build-on: [amd64]
+                            build-for: [amd64]
+                    """
+                )
+            )
+        )
+        job = self.makeRequestBuildsJob("24.04", ["amd64", "riscv64", "arm64"])
+        self.assertEqual(
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
+        transaction.commit()
+        with person_logged_in(job.requester):
+            builds = job.recipe.requestBuildsFromJob(
+                job.build_request, channels=removeSecurityProxy(job.channels)
+            )
+        self.assertRequestedBuildsMatch(
+            builds, job, "24.04", ["amd64"], job.channels
+        )
+
+    def test_requestBuildsFromJob_unified_rockcraft_yaml_multi_platforms(
+        self,
+    ):
+        self.useFixture(
+            GitHostingFixture(
+                blob=dedent(
+                    """\
+                    base:
+                        name: ubuntu
+                        channel: 24.04
+                    platforms:
+                        ubuntu-amd64:
+                            build-on: [amd64]
+                            build-for: [amd64]
+                        ubuntu-arm64:
+                            build-on: [arm64]
+                            build-for: [arm64]
+                    """
+                )
+            )
+        )
+        job = self.makeRequestBuildsJob("24.04", ["amd64", "riscv64", "arm64"])
+        self.assertEqual(
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
+        transaction.commit()
+        with person_logged_in(job.requester):
+            builds = job.recipe.requestBuildsFromJob(
+                job.build_request, channels=removeSecurityProxy(job.channels)
+            )
+        self.assertRequestedBuildsMatch(
+            builds, job, "24.04", ["amd64", "arm64"], job.channels
+        )
+
+    def test_requestBuildsFromJob_unified_rockcraft_yaml_arch_as_str(self):
+        self.useFixture(
+            GitHostingFixture(
+                blob=dedent(
+                    """\
+                    base:
+                        name: ubuntu
+                        channel: 24.04
+                    platforms:
+                        ubuntu-amd64:
+                            build-on: amd64
+                            build-for: amd64
+                    """
+                )
+            )
+        )
+        job = self.makeRequestBuildsJob("24.04", ["amd64", "riscv64", "arm64"])
+        self.assertEqual(
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
+        transaction.commit()
+        with person_logged_in(job.requester):
+            builds = job.recipe.requestBuildsFromJob(
+                job.build_request, channels=removeSecurityProxy(job.channels)
+            )
+        self.assertRequestedBuildsMatch(
+            builds, job, "24.04", ["amd64"], job.channels
+        )
+
+    def test_requestBuildsFromJob_unified_rockcraft_yaml_base_short_form(
+        self,
+    ):
+        self.useFixture(
+            GitHostingFixture(
+                blob=dedent(
+                    """\
+                    base: ubuntu@24.04
+                    platforms:
+                        ubuntu-amd64:
+                            build-on: [amd64]
+                            build-for: [amd64]
+                    """
+                )
+            )
+        )
+        job = self.makeRequestBuildsJob("24.04", ["amd64", "riscv64", "arm64"])
+        self.assertEqual(
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
+        transaction.commit()
+        with person_logged_in(job.requester):
+            builds = job.recipe.requestBuildsFromJob(
+                job.build_request, channels=removeSecurityProxy(job.channels)
+            )
+        self.assertRequestedBuildsMatch(
+            builds, job, "24.04", ["amd64"], job.channels
+        )
+
+    def test_requestBuildsFromJob_unified_rockcraft_yaml_unknown_arch(self):
+        self.useFixture(
+            GitHostingFixture(
+                blob=dedent(
+                    """\
+                    base:
+                        name: ubuntu
+                        channel: 24.04
+                    platforms:
+                        foobar:
+                    """
+                )
+            )
+        )
+        job = self.makeRequestBuildsJob("24.04", ["amd64", "riscv64", "arm64"])
+        self.assertEqual(
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
+        transaction.commit()
+        with person_logged_in(job.requester):
+            with ExpectedException(
+                BadPropertyError,
+                "'foobar' is not a supported architecture for "
+                "'ubuntu@24.04'",
+            ):
+                job.recipe.requestBuildsFromJob(
+                    job.build_request,
+                    channels=removeSecurityProxy(job.channels),
+                )
+
+    def test_requestBuildsFromJob_unified_rockcraft_yaml_platforms_short_form(
+        self,
+    ):
+        self.useFixture(
+            GitHostingFixture(
+                blob=dedent(
+                    """\
+                    base:
+                        name: ubuntu
+                        channel: 24.04
+                    platforms:
+                        amd64:
+                    """
+                )
+            )
+        )
+        job = self.makeRequestBuildsJob("24.04", ["amd64", "riscv64", "arm64"])
+        self.assertEqual(
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
+        transaction.commit()
+        with person_logged_in(job.requester):
+            builds = job.recipe.requestBuildsFromJob(
+                job.build_request, channels=removeSecurityProxy(job.channels)
+            )
+        self.assertRequestedBuildsMatch(
+            builds, job, "24.04", ["amd64"], job.channels
+        )
+
+    def test_requestBuildsFromJob_unified_rockcraft_yaml_2_platforms_short(
+        self,
+    ):
+        self.useFixture(
+            GitHostingFixture(
+                blob=dedent(
+                    """\
+                    base:
+                        name: ubuntu
+                        channel: 24.04
+                    platforms:
+                        amd64:
+                        arm64:
+                    """
+                )
+            )
+        )
+        job = self.makeRequestBuildsJob("24.04", ["amd64", "riscv64", "arm64"])
+        self.assertEqual(
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
+        transaction.commit()
+        with person_logged_in(job.requester):
+            builds = job.recipe.requestBuildsFromJob(
+                job.build_request, channels=removeSecurityProxy(job.channels)
+            )
+        self.assertRequestedBuildsMatch(
+            builds, job, "24.04", ["amd64", "arm64"], job.channels
+        )
+
     def test_requestBuildsFromJob_architectures_parameter(self):
         # If an explicit set of architectures was given as a parameter,
         # requestBuildsFromJob intersects those with any other constraints

Follow ups