launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #31162
[Merge] ~lgp171188/launchpad:charmcraft-unified-format into launchpad:master
Guruprasad has proposed merging ~lgp171188/launchpad:charmcraft-unified-format into launchpad:master with ~jugmac00/launchpad:add-is_unified_format-for-charms as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~lgp171188/launchpad/+git/launchpad/+merge/467335
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~lgp171188/launchpad:charmcraft-unified-format into launchpad:master.
diff --git a/lib/lp/charms/adapters/buildarch.py b/lib/lp/charms/adapters/buildarch.py
index 2579ad8..9292bdb 100644
--- a/lib/lp/charms/adapters/buildarch.py
+++ b/lib/lp/charms/adapters/buildarch.py
@@ -6,6 +6,7 @@ __all__ = [
]
import json
+import re
from collections import Counter, OrderedDict
from lp.services.helpers import english_list
@@ -18,10 +19,10 @@ class CharmBasesParserError(Exception):
class MissingPropertyError(CharmBasesParserError):
"""Error for when an expected property is not present in the YAML."""
- 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)
self.property = prop
@@ -107,6 +108,14 @@ class CharmBaseConfiguration:
# Expand short-form configuration into long-form. Account for
# common typos in case the user intends to use long-form but did so
# incorrectly (for better error message handling).
+ # build-on:
+ # - name: ubuntu
+ # channel: 20.04
+ # architectures: [amd64]
+ # run-on:
+ # - name: ubuntu
+ # channel: 20.04
+ # architectures: [amd64]
if not any(
item in properties
for item in ("run-on", "run_on", "build-on", "build_on")
@@ -125,6 +134,85 @@ class CharmBaseConfiguration:
return cls(build_on, run_on=run_on)
+class UnifiedCharmBaseConfiguration:
+ """A unified base configuration in charmcraft.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, charmcraft_data, supported_arches):
+ base = charmcraft_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 lgp171188 2024-06-11: Find out if we need 'build-base' or not.
+ # There is no existing code that is using that.
+
+ platforms = charmcraft_data.get("platforms")
+ if not platforms:
+ raise MissingPropertyError(
+ "platforms", "The 'platforms' property is required"
+ )
+
+ 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 = [
+ CharmBase(name, channel, architecture)
+ for architecture in build_on
+ ]
+
+ build_for = configuration["build-for"]
+ if isinstance(build_for, str):
+ build_for = [build_for]
+
+ build_for = [
+ CharmBase(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 = [CharmBase(name, channel, platform)]
+ build_for = [CharmBase(name, channel, platform)]
+ else:
+ raise BadPropertyError(
+ f"'{platform}' is not a supported architecture "
+ f"for '{base_name}@{base_channel}'."
+ )
+ return cls(build_on, build_for)
+
+
def determine_instances_to_build(
charmcraft_data, supported_arches, default_distro_series
):
@@ -138,12 +226,20 @@ def determine_instances_to_build(
charmcraft.yaml does not explicitly declare any bases.
:return: A list of `DistroArchSeries`.
"""
+ from lp.charms.model.charmrecipe import is_unified_format
+
bases_list = charmcraft_data.get("bases")
if bases_list:
configs = [
CharmBaseConfiguration.from_dict(item) for item in bases_list
]
+ elif is_unified_format(charmcraft_data):
+ configs = [
+ UnifiedCharmBaseConfiguration.from_dict(
+ charmcraft_data, supported_arches
+ )
+ ]
else:
# If no bases are specified, build one for each supported
# architecture for the default series.
diff --git a/lib/lp/charms/tests/test_charmrecipe.py b/lib/lp/charms/tests/test_charmrecipe.py
index d6f5819..b149cbc 100644
--- a/lib/lp/charms/tests/test_charmrecipe.py
+++ b/lib/lp/charms/tests/test_charmrecipe.py
@@ -31,6 +31,7 @@ from testtools.matchers import (
MatchesSetwise,
MatchesStructure,
)
+from testtools.testcase import ExpectedException
from zope.component import getUtility
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
@@ -49,6 +50,7 @@ from lp.buildmaster.interfaces.processor import (
)
from lp.buildmaster.model.buildfarmjob import BuildFarmJob
from lp.buildmaster.model.buildqueue import BuildQueue
+from lp.charms.adapters.buildarch import BadPropertyError, MissingPropertyError
from lp.charms.interfaces.charmrecipe import (
CHARM_RECIPE_ALLOW_CREATE,
CHARM_RECIPE_BUILD_DISTRIBUTION,
@@ -755,6 +757,212 @@ class TestCharmRecipe(TestCaseWithFactory):
builds, job, "20.04", ["mips64el", "riscv64"], job.channels
)
+ def test_requestBuildsFromJob_unified_charmcraft_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_charmcraft_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_charmcraft_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_charmcraft_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_charmcraft_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_charmcraft_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_charmcraft_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_no_charmcraft_yaml(self):
# If the recipe has no charmcraft.yaml file, requestBuildsFromJob
# treats this as equivalent to a charmcraft.yaml file that doesn't