launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #31445
[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