← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~ruinedyourlife/launchpad:rock-build-parser into launchpad:master

 

Quentin Debhi has proposed merging ~ruinedyourlife/launchpad:rock-build-parser into launchpad:master.

Commit message:
Add rockcraft parser

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~ruinedyourlife/launchpad/+git/launchpad/+merge/473143
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~ruinedyourlife/launchpad:rock-build-parser into launchpad:master.
diff --git a/lib/lp/rocks/adapters/__init__.py b/lib/lp/rocks/adapters/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/lp/rocks/adapters/__init__.py
diff --git a/lib/lp/rocks/adapters/buildarch.py b/lib/lp/rocks/adapters/buildarch.py
new file mode 100644
index 0000000..1c1bfec
--- /dev/null
+++ b/lib/lp/rocks/adapters/buildarch.py
@@ -0,0 +1,109 @@
+# Copyright 2024 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__all__ = [
+    "determine_instances_to_build",
+]
+
+from collections import OrderedDict
+
+
+class RockBasesParserError(Exception):
+    """Base class for all exceptions in this module."""
+
+
+class MissingPropertyError(RockBasesParserError):
+    """Error for when an expected property is not present in the YAML."""
+
+    def __init__(self, prop, msg=None):
+        if msg is None:
+            msg = f"Rock specification is missing the {prop!r} property"
+        super().__init__(msg)
+        self.property = prop
+
+
+class BadPropertyError(RockBasesParserError):
+    """Error for when a YAML property is malformed in some way."""
+
+
+class RockBase:
+    """A single base in rockcraft.yaml."""
+
+    def __init__(self, name, channel):
+        self.name = name
+        if not isinstance(channel, str):
+            raise BadPropertyError(
+                f"Channel {channel!r} is not a string (missing quotes?)"
+            )
+        self.channel = channel
+
+    @classmethod
+    def from_string(cls, base_string):
+        """Create a new base from a string like 'ubuntu@22.04'."""
+        name, channel = base_string.split("@")
+        return cls(name, channel)
+
+    def __eq__(self, other):
+        if not isinstance(other, RockBase):
+            return NotImplemented
+        return self.name == other.name and self.channel == other.channel
+
+    def __hash__(self):
+        return hash((self.name, self.channel))
+
+    def __str__(self):
+        return f"{self.name}@{self.channel}"
+
+
+def determine_instances_to_build(
+    rockcraft_data, supported_arches, default_distro_series
+):
+    """Return a list of instances to build based on rockcraft.yaml.
+
+    :param rockcraft_data: A parsed rockcraft.yaml.
+    :param supported_arches: An ordered list of all `DistroArchSeries` that
+        we can create builds for. Note that these may span multiple
+        `DistroSeries`.
+    :param default_distro_series: The default `DistroSeries` to use if
+        rockcraft.yaml does not explicitly declare a base.
+    :return: A list of `DistroArchSeries`.
+    """
+    base = rockcraft_data.get("base")
+    if not base:
+        raise MissingPropertyError("base")
+
+    if base == "bare":
+        base = rockcraft_data.get("build-base")
+        if not base:
+            raise MissingPropertyError(
+                "build-base",
+                "When 'base' is 'bare', 'build-base' must be specified",
+            )
+
+    rock_base = RockBase.from_string(base)
+
+    platforms = rockcraft_data.get("platforms")
+    if not platforms:
+        raise MissingPropertyError("platforms")
+
+    instances = OrderedDict()
+    for platform, config in platforms.items():
+        build_on = config.get("build-on", [platform])
+        build_for = config.get("build-for", [platform])
+
+        if isinstance(build_on, str):
+            build_on = [build_on]
+        if isinstance(build_for, str):
+            build_for = [build_for]
+
+        for arch in build_on:
+            for das in supported_arches:
+                if (
+                    das.distroseries.distribution.name == rock_base.name
+                    and das.distroseries.version == rock_base.channel
+                    and das.architecturetag == arch
+                ):
+                    instances[das] = None
+                    break
+
+    return list(instances)
diff --git a/lib/lp/rocks/adapters/tests/__init__.py b/lib/lp/rocks/adapters/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/lp/rocks/adapters/tests/__init__.py
diff --git a/lib/lp/rocks/adapters/tests/test_buildarch.py b/lib/lp/rocks/adapters/tests/test_buildarch.py
new file mode 100644
index 0000000..0ff0b45
--- /dev/null
+++ b/lib/lp/rocks/adapters/tests/test_buildarch.py
@@ -0,0 +1,211 @@
+# Copyright 2024 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from functools import partial
+
+from testscenarios import WithScenarios, load_tests_apply_scenarios
+from testtools.matchers import (
+    Equals,
+    MatchesException,
+    MatchesListwise,
+    MatchesStructure,
+    Raises,
+)
+from zope.component import getUtility
+
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.buildmaster.interfaces.processor import (
+    IProcessorSet,
+    ProcessorNotFound,
+)
+from lp.rocks.adapters.buildarch import (
+    MissingPropertyError,
+    RockBase,
+    determine_instances_to_build,
+)
+from lp.testing import TestCase, TestCaseWithFactory
+from lp.testing.layers import LaunchpadZopelessLayer
+
+
+class TestRockBase(TestCase):
+    def test_from_string(self):
+        base = RockBase.from_string("ubuntu@22.04")
+        self.assertEqual("ubuntu", base.name)
+        self.assertEqual("22.04", base.channel)
+
+    def test_str(self):
+        base = RockBase("ubuntu", "22.04")
+        self.assertEqual("ubuntu@22.04", str(base))
+
+    def test_equality(self):
+        base1 = RockBase("ubuntu", "22.04")
+        base2 = RockBase("ubuntu", "22.04")
+        base3 = RockBase("ubuntu", "20.04")
+        self.assertEqual(base1, base2)
+        self.assertNotEqual(base1, base3)
+
+
+class TestDetermineInstancesToBuild(WithScenarios, TestCaseWithFactory):
+    layer = LaunchpadZopelessLayer
+
+    scenarios = [
+        (
+            "single platform",
+            {
+                "rockcraft_data": {
+                    "base": "ubuntu@22.04",
+                    "platforms": {
+                        "amd64": {},
+                    },
+                },
+                "expected": [("22.04", "amd64")],
+            },
+        ),
+        (
+            "multiple platforms",
+            {
+                "rockcraft_data": {
+                    "base": "ubuntu@22.04",
+                    "platforms": {
+                        "amd64": {},
+                        "arm64": {},
+                    },
+                },
+                "expected": [("22.04", "amd64"), ("22.04", "arm64")],
+            },
+        ),
+        (
+            "build-on specified",
+            {
+                "rockcraft_data": {
+                    "base": "ubuntu@22.04",
+                    "platforms": {
+                        "amd64": {"build-on": ["amd64", "arm64"]},
+                    },
+                },
+                "expected": [("22.04", "amd64"), ("22.04", "arm64")],
+            },
+        ),
+        (
+            "build-for specified",
+            {
+                "rockcraft_data": {
+                    "base": "ubuntu@22.04",
+                    "platforms": {
+                        "amd64": {"build-for": "amd64"},
+                        "arm64": {"build-for": "arm64"},
+                    },
+                },
+                "expected": [("22.04", "amd64"), ("22.04", "arm64")],
+            },
+        ),
+        (
+            "bare base",
+            {
+                "rockcraft_data": {
+                    "base": "bare",
+                    "build-base": "ubuntu@20.04",
+                    "platforms": {
+                        "amd64": {},
+                    },
+                },
+                "expected": [("20.04", "amd64")],
+            },
+        ),
+        (
+            "missing base",
+            {
+                "rockcraft_data": {
+                    "platforms": {
+                        "amd64": {},
+                    },
+                },
+                "expected_exception": MatchesException(
+                    MissingPropertyError,
+                    "Rock specification is missing the 'base' property",
+                ),
+            },
+        ),
+        (
+            "missing build-base for bare",
+            {
+                "rockcraft_data": {
+                    "base": "bare",
+                    "platforms": {
+                        "amd64": {},
+                    },
+                },
+                "expected_exception": MatchesException(
+                    MissingPropertyError,
+                    "When 'base' is 'bare', 'build-base' must be specified",
+                ),
+            },
+        ),
+        (
+            "missing platforms",
+            {
+                "rockcraft_data": {
+                    "base": "ubuntu@22.04",
+                },
+                "expected_exception": MatchesException(
+                    MissingPropertyError,
+                    "Rock specification is missing the 'platforms' property",
+                ),
+            },
+        ),
+    ]
+
+    def test_determine_instances_to_build(self):
+        distro_serieses = [
+            self.factory.makeDistroSeries(
+                distribution=getUtility(ILaunchpadCelebrities).ubuntu,
+                version=version,
+            )
+            for version in ("22.04", "20.04")
+        ]
+        dases = []
+        for arch_tag in ("amd64", "arm64"):
+            try:
+                processor = getUtility(IProcessorSet).getByName(arch_tag)
+            except ProcessorNotFound:
+                processor = self.factory.makeProcessor(
+                    name=arch_tag, supports_virtualized=True
+                )
+            for distro_series in distro_serieses:
+                dases.append(
+                    self.factory.makeDistroArchSeries(
+                        distroseries=distro_series,
+                        architecturetag=arch_tag,
+                        processor=processor,
+                    )
+                )
+
+        build_instances_factory = partial(
+            determine_instances_to_build,
+            self.rockcraft_data,
+            dases,
+            distro_serieses[0],
+        )
+
+        if hasattr(self, "expected_exception"):
+            self.assertThat(
+                build_instances_factory, Raises(self.expected_exception)
+            )
+        else:
+            self.assertThat(
+                build_instances_factory(),
+                MatchesListwise(
+                    [
+                        MatchesStructure(
+                            distroseries=MatchesStructure.byEquality(
+                                version=version
+                            ),
+                            architecturetag=Equals(arch_tag),
+                        )
+                        for version, arch_tag in self.expected
+                    ]
+                ),
+            )
+
+
+load_tests = load_tests_apply_scenarios

Follow ups