launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #32317
[Merge] ~alvarocs/launchpad:charms-craft-platforms into launchpad:master
Alvaro Crespo Serrano has proposed merging ~alvarocs/launchpad:charms-craft-platforms into launchpad:master.
Commit message:
Use craft-platforms to determine builds for charms
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~alvarocs/launchpad/+git/launchpad/+merge/483337
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~alvarocs/launchpad:charms-craft-platforms into launchpad:master.
diff --git a/lib/lp/charms/adapters/buildarch.py b/lib/lp/charms/adapters/buildarch.py
index 66bc20e..f500d2a 100644
--- a/lib/lp/charms/adapters/buildarch.py
+++ b/lib/lp/charms/adapters/buildarch.py
@@ -6,9 +6,10 @@ __all__ = [
]
import json
-import re
from collections import Counter, OrderedDict
+from craft_platforms import CraftPlatformsError, charm
+
from lp.services.helpers import english_list
@@ -126,91 +127,16 @@ 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"
- )
- 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 = [
- 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}'."
- )
- configs.append(cls(build_on, build_for))
- return configs
-
-
def determine_instances_to_build(
charmcraft_data, supported_arches, default_distro_series
):
"""Return a list of instances to build based on charmcraft.yaml.
+ Handles three cases:
+ 1) 'bases' configuration
+ 2) Unified format configuration
+ 3) No charmcraft.yaml
+
:param charmcraft_data: A parsed charmcraft.yaml.
:param supported_arches: An ordered list of all `DistroArchSeries` that
we can create builds for. Note that these may span multiple
@@ -219,18 +145,140 @@ def determine_instances_to_build(
charmcraft.yaml does not explicitly declare any bases.
:return: A list of `DistroArchSeries`.
"""
+
+ def _check_duplicate_run_on(configs):
+ """Ensure that multiple `run-on` items don't overlap;
+ this is ambiguous and forbidden by charmcraft.
+
+ :param configs: List of CharmBaseConfiguration objects
+ :raises DuplicateRunOnError if any architecture appears in
+ multiple run-on configurations
+ """
+
+ run_ons = Counter()
+ for config in configs:
+ run_ons.update(config.run_on)
+ duplicates = {config for config, count in run_ons.items() if count > 1}
+ if duplicates:
+ raise DuplicateRunOnError(duplicates)
+
+ def _process_configs_to_instances(configs, supported_arches):
+ """Convert base configurations to buildable instances.
+
+ Filters configurations to only include supported architectures and
+ distro series.
+
+ :param configs: List of CharmBaseConfiguration objects
+ :param supported_arches: List of supported DistroArchSeries
+ :return: OrderedDict of filtered DistroArchSeries instances
+ """
+
+ _check_duplicate_run_on(configs)
+ instances = OrderedDict()
+ for config in configs:
+ # Charms are allowed to declare that they build on architectures
+ # that Launchpad doesn't currently support (perhaps they're
+ # upcoming, or perhaps they used to be supported). We just ignore
+ # those.
+ for build_on in config.build_on:
+ for das in supported_arches:
+ if das.distroseries.distribution.name != build_on.name:
+ continue
+ if build_on.channel not in (
+ das.distroseries.name,
+ das.distroseries.version,
+ ):
+ continue
+ if build_on.architectures is None:
+ # Build on all supported architectures for the
+ # requested series.
+ instances[das] = None
+ elif das.architecturetag in build_on.architectures:
+ # Build on the first matching supported architecture
+ # for the requested series.
+ instances[das] = None
+ break
+ else:
+ continue
+ break
+ return instances
+
from lp.charms.model.charmrecipe import is_unified_format
+ # 1) Charm with 'bases' format
bases_list = charmcraft_data.get("bases")
-
if bases_list:
configs = [
CharmBaseConfiguration.from_dict(item) for item in bases_list
]
+ instances = _process_configs_to_instances(configs, supported_arches)
+ instances_to_build = [(None, das) for das in instances.keys()]
+ return instances
+ return instances_to_build
+
+ # 2) Charm with unified format
elif is_unified_format(charmcraft_data):
- configs = UnifiedCharmBaseConfiguration.from_dict(
- charmcraft_data, supported_arches
- )
+
+ base = charmcraft_data["base"]
+ build_base = charmcraft_data.get("build-base", None)
+ platforms = charmcraft_data.get("platforms", None)
+
+ # Potentially need to move this into craft-platforms.charms
+ if isinstance(base, dict):
+ base = f"{base['name']}@{base['channel']}"
+
+ # Generate exhaustive build plan
+ try:
+ exhaustive_build_plan = charm.get_platforms_charm_build_plan(
+ base=base,
+ platforms=platforms,
+ build_base=build_base,
+ )
+ except (CraftPlatformsError, ValueError) as e:
+ raise BadPropertyError(str(e))
+
+ # Filter exhaustive build plan
+ filtered_plan = []
+ for info in exhaustive_build_plan:
+ for das in supported_arches:
+ # Compare DAS-BuildInfo and append if match
+ if (
+ das.distroseries.distribution.name
+ == info.build_base.distribution
+ and info.build_base.series
+ in (das.distroseries.name, das.distroseries.version)
+ and das.architecturetag == info.build_on.value
+ ):
+ filtered_plan.append((info, das))
+ break
+ # Group by platform
+ platform_plans = {}
+ for info, das in filtered_plan:
+ platform_plans.setdefault(info.platform, []).append((info, das))
+ # Pick one BuildInfo per platform
+ instances_to_build = []
+ instances = []
+ for _platform, pairs in platform_plans.items():
+ # One way of building for that platform, i.e one (info, das)
+ if len(pairs) == 1:
+ instances_to_build.append(pairs[0])
+ instances.append(pairs[0][1])
+ continue
+ # More than one way of building for that platform
+ for info, das in pairs:
+ # Pick the native build
+ if info.build_on == info.build_for:
+ instances_to_build.append((info, das))
+ instances.append(das)
+ break
+ # Pick first one if none are native
+ else:
+ instances_to_build.append(pairs[0])
+ instances.append(pairs[0][1])
+ return instances
+ return instances_to_build
+
+ # 3) Charms without charmcraft.yaml
else:
# If no bases are specified, build one for each supported
# architecture for the default series.
@@ -247,41 +295,7 @@ def determine_instances_to_build(
for das in supported_arches
if das.distroseries == default_distro_series
]
-
- # Ensure that multiple `run-on` items don't overlap; this is ambiguous
- # and forbidden by charmcraft.
- run_ons = Counter()
- for config in configs:
- run_ons.update(config.run_on)
- duplicates = {config for config, count in run_ons.items() if count > 1}
- if duplicates:
- raise DuplicateRunOnError(duplicates)
-
- instances = OrderedDict()
- for config in configs:
- # Charms are allowed to declare that they build on architectures
- # that Launchpad doesn't currently support (perhaps they're
- # upcoming, or perhaps they used to be supported). We just ignore
- # those.
- for build_on in config.build_on:
- for das in supported_arches:
- if das.distroseries.distribution.name != build_on.name:
- continue
- if build_on.channel not in (
- das.distroseries.name,
- das.distroseries.version,
- ):
- continue
- if build_on.architectures is None:
- # Build on all supported architectures for the requested
- # series.
- instances[das] = None
- elif das.architecturetag in build_on.architectures:
- # Build on the first matching supported architecture for
- # the requested series.
- instances[das] = None
- break
- else:
- continue
- break
- return list(instances)
+ instances = _process_configs_to_instances(configs, supported_arches)
+ instances_to_build = [(None, das) for das in instances.keys()]
+ return instances
+ return instances_to_build
diff --git a/requirements/launchpad.txt b/requirements/launchpad.txt
index bf2650b..5ddcc78 100644
--- a/requirements/launchpad.txt
+++ b/requirements/launchpad.txt
@@ -31,6 +31,7 @@ Chameleon==3.6.2
configobj==5.0.6
contextvars==2.4
constantly==15.1.0
+craft-platforms==0.6.0
cryptography==2.7
cssselect==0.9.1
cssutils==2.11.1
Follow ups