launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #32332
[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:
Support craft-platforms build plan in charm builds
Updates Launchpad to support charm builds using 'craft-platforms' for the unified format. It also ensures the platform name ('recipe_platform_name') is passed through the build pipeline.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~alvarocs/launchpad/+git/launchpad/+merge/483337
Changes include:
Charms base configuration and build planning
- lp/charms/adapters/buildarch.py - replace custom unified base with craft-platforms API, reformat code with helper functions, add comments.
- lp/charms/adapters/tests/test_buildarch.py - improve and extend existing test coverage, add multi-base platforms tests handled by craft-platforms.
Charm recipe model
- lp/charms/interfaces/charmrecipebuild.py, model/charmrecipebuild.py - store recipe_platform_name on CharmRecipeBuild storm object.
- lp/charms/model/charmrecipe.py - pass platform name through 'requestBuild' and 'requestBuildsFromJob'.
- lp/charms/tests/test_charmrecipe.py - add tests to verify 'recipe_platform_name' is passed through.
Build behaviour
- lp/charms/model/charmrecipebuildbehaviour.py - pass 'recipe_platform_name' as extra argument to the builder.
- lp/charms/tests/test_charmrecipebuildbehaviour.py - test coverage for 'recipe_platform_name' argument.
- lp/buildmaster/interfaces/buildfarmjobbehaviour.py - extend BuildArgs interface class to include the new 'recipe_platform_name' field.
Testing
- lp/testing/factory.py - extend factory methods to support setting the new arg
launchpad-buildd MP: https://code.launchpad.net/~alvarocs/launchpad-buildd/+git/launchpad-buildd/+merge/483410
craft-platforms repo https://github.com/canonical/craft-platforms/tree
--
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/buildmaster/interfaces/buildfarmjobbehaviour.py b/lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py
index 74da7e9..e721f7d 100644
--- a/lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py
+++ b/lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py
@@ -121,6 +121,8 @@ class BuildArgs(TypedDict, total=False):
private: bool
# The URL of the proxy for internet access [charm, ci, oci, snap].
proxy_url: str
+ # The platform name to build for [charm, snap, sources, rocks].
+ recipe_platform_name: str
# The text of the recipe to build [required for
# sourcepackagerecipe].
recipe_text: str
diff --git a/lib/lp/charms/adapters/buildarch.py b/lib/lp/charms/adapters/buildarch.py
index 66bc20e..7a63117 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
@@ -42,6 +43,17 @@ class DuplicateRunOnError(CharmBasesParserError):
)
+class CraftPlatformsBuildPlanError(CharmBasesParserError):
+ """Error raised when craft-platforms fails while generating a build
+ plan.
+ """
+
+ def __init__(self, message, resolution=None):
+ if resolution:
+ message += f" Resolution: {resolution}"
+ super().__init__(message)
+
+
class CharmBase:
"""A single base in charmcraft.yaml."""
@@ -126,91 +138,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) With no bases specified configuration
+
: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 +156,139 @@ 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_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)
+ # Reformat base dict style
+ 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:
+ message = getattr(e, "message", str(e))
+ resolution = getattr(e, "resolution", None)
+ raise CraftPlatformsBuildPlanError(
+ f"Failed to compute the build plan for base={base}, "
+ f"build base={build_base}, platforms={platforms}: {message}",
+ resolution=resolution,
+ )
+ # 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 = []
+ 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])
+ 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))
+ break
+ # Pick first one if none are native
+ else:
+ instances_to_build.append(pairs[0])
+ return instances_to_build
+
+ # 3) Charms with no bases specified
else:
# If no bases are specified, build one for each supported
# architecture for the default series.
@@ -247,41 +305,6 @@ 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_to_build
diff --git a/lib/lp/charms/adapters/tests/test_buildarch.py b/lib/lp/charms/adapters/tests/test_buildarch.py
index a87528d..d14ca60 100644
--- a/lib/lp/charms/adapters/tests/test_buildarch.py
+++ b/lib/lp/charms/adapters/tests/test_buildarch.py
@@ -21,6 +21,7 @@ from lp.buildmaster.interfaces.processor import (
from lp.charms.adapters.buildarch import (
CharmBase,
CharmBaseConfiguration,
+ CraftPlatformsBuildPlanError,
DuplicateRunOnError,
determine_instances_to_build,
)
@@ -122,6 +123,7 @@ class TestDetermineInstancesToBuild(WithScenarios, TestCaseWithFactory):
# Scenarios taken from the charmcraft build providers specification:
# https://discourse.charmhub.io/t/charmcraft-bases-provider-support/4713
scenarios = [
+ # 'Bases' format scenarios
(
"single entry, single arch",
{
@@ -373,17 +375,6 @@ class TestDetermineInstancesToBuild(WithScenarios, TestCaseWithFactory):
},
),
(
- "no bases specified",
- {
- "bases": None,
- "expected": [
- ("20.04", "amd64"),
- ("20.04", "arm64"),
- ("20.04", "riscv64"),
- ],
- },
- ),
- (
"abbreviated, no architectures specified",
{
"bases": [
@@ -399,6 +390,178 @@ class TestDetermineInstancesToBuild(WithScenarios, TestCaseWithFactory):
],
},
),
+ # Unified format scenarios
+ (
+ "unified single platform",
+ {
+ "bases": None,
+ "base": "ubuntu@20.04",
+ "platforms": {
+ "ubuntu-amd64": {
+ "build-on": ["amd64"],
+ "build-for": ["amd64"],
+ }
+ },
+ "expected": [("20.04", "amd64")],
+ },
+ ),
+ (
+ "unified multi-platforms",
+ {
+ "bases": None,
+ "base": {"name": "ubuntu", "channel": "20.04"},
+ "platforms": {
+ "ubuntu-amd64": {
+ "build-on": ["amd64"],
+ "build-for": ["amd64"],
+ },
+ "ubuntu-arm64": {
+ "build-on": ["arm64"],
+ "build-for": ["arm64"],
+ },
+ },
+ "expected": [("20.04", "amd64"), ("20.04", "arm64")],
+ },
+ ),
+ (
+ "unified mismatched platform base",
+ {
+ "bases": None,
+ "base": "ubuntu@20.04",
+ "platforms": {
+ "ubuntu:18.04": {
+ "build-on": ["amd64"],
+ "build-for": ["amd64"],
+ },
+ },
+ "expected_exception": MatchesException(
+ CraftPlatformsBuildPlanError,
+ r"Failed to compute the build plan for base=.+",
+ ),
+ },
+ ),
+ (
+ "unified build-for all single platform",
+ {
+ "bases": None,
+ "base": "ubuntu@20.04",
+ "platforms": {
+ "ubuntu-amd64": {
+ "build-on": ["amd64"],
+ "build-for": ["all"],
+ },
+ },
+ "expected": [("20.04", "amd64")],
+ },
+ ),
+ (
+ "unified multiple build-for all platforms (invalid)",
+ {
+ "bases": None,
+ "base": "ubuntu@20.04",
+ "platforms": {
+ "ubuntu-amd64": {
+ "build-on": ["amd64"],
+ "build-for": ["all"],
+ },
+ "ubuntu-arm64": {
+ "build-on": ["arm64"],
+ "build-for": ["all"],
+ },
+ },
+ "expected": [("20.04", "amd64"), ("20.04", "arm64")],
+ },
+ ),
+ (
+ "unified without platforms, builds for all allowed archs",
+ {
+ "bases": None,
+ "base": "ubuntu@20.04",
+ "platforms": None,
+ "expected": [
+ ("20.04", "amd64"),
+ ("20.04", "arm64"),
+ ("20.04", "riscv64"),
+ ],
+ },
+ ),
+ (
+ "unified without supported series",
+ {
+ "bases": None,
+ "base": "ubuntu@22.04",
+ "platforms": {
+ "ubuntu-amd64": {
+ "build-on": ["amd64"],
+ "build-for": ["amd64"],
+ },
+ },
+ "expected": [],
+ },
+ ),
+ (
+ "unified invalid platform name",
+ {
+ "bases": None,
+ "base": "ubuntu@20.04",
+ "platforms": {"not-a-valid-arch": None},
+ "expected_exception": MatchesException(
+ CraftPlatformsBuildPlanError,
+ r"Failed to compute the build plan for base=.+",
+ ),
+ },
+ ),
+ (
+ "unified missing build-on",
+ {
+ "bases": None,
+ "base": "ubuntu@20.04",
+ "platforms": {
+ "ubuntu-amd64": {
+ "build-on": [],
+ "build-for": ["amd64"],
+ },
+ },
+ "expected": [],
+ },
+ ),
+ (
+ "unified cross-compiling combinations, taking the first one",
+ {
+ "bases": None,
+ "base": "ubuntu@20.04",
+ "platforms": {
+ "ubuntu-cross": {
+ "build-on": ["amd64", "arm64"],
+ "build-for": ["riscv64"],
+ },
+ },
+ "expected": [("20.04", "amd64")],
+ },
+ ),
+ (
+ "unified base only with unsupported series",
+ {
+ "bases": None,
+ "base": "ubuntu@99.99",
+ "platforms": None,
+ "expected": [],
+ },
+ ),
+ # No bases specified scenario
+ (
+ "no bases specified",
+ {
+ "bases": None,
+ "base": None,
+ "platforms": None,
+ "expected": [
+ ("20.04", "amd64"),
+ ("20.04", "arm64"),
+ ("20.04", "riscv64"),
+ ],
+ },
+ ),
]
def test_parser(self):
@@ -409,7 +572,7 @@ class TestDetermineInstancesToBuild(WithScenarios, TestCaseWithFactory):
)
for version in ("20.04", "18.04")
]
- dases = []
+ supported_dases = []
for arch_tag in ("amd64", "arm64", "riscv64"):
try:
processor = getUtility(IProcessorSet).getByName(arch_tag)
@@ -418,7 +581,7 @@ class TestDetermineInstancesToBuild(WithScenarios, TestCaseWithFactory):
name=arch_tag, supports_virtualized=True
)
for distro_series in distro_serieses:
- dases.append(
+ supported_dases.append(
self.factory.makeDistroArchSeries(
distroseries=distro_series,
architecturetag=arch_tag,
@@ -426,12 +589,16 @@ class TestDetermineInstancesToBuild(WithScenarios, TestCaseWithFactory):
)
)
charmcraft_data = {}
- if self.bases is not None:
+ if getattr(self, "bases", None) is not None:
charmcraft_data["bases"] = self.bases
+ if getattr(self, "base", None) is not None:
+ charmcraft_data["base"] = self.base
+ if getattr(self, "platforms", None) is not None:
+ charmcraft_data["platforms"] = self.platforms
build_instances_factory = partial(
determine_instances_to_build,
charmcraft_data,
- dases,
+ supported_dases,
distro_serieses[0],
)
if hasattr(self, "expected_exception"):
@@ -439,8 +606,11 @@ class TestDetermineInstancesToBuild(WithScenarios, TestCaseWithFactory):
build_instances_factory, Raises(self.expected_exception)
)
else:
+ result = build_instances_factory()
+ # Only asserting DistroArchSeries
+ result = [das for (info, das) in result]
self.assertThat(
- build_instances_factory(),
+ result,
MatchesListwise(
[
MatchesStructure(
diff --git a/lib/lp/charms/interfaces/charmrecipe.py b/lib/lp/charms/interfaces/charmrecipe.py
index 7ad2b5a..4037681 100644
--- a/lib/lp/charms/interfaces/charmrecipe.py
+++ b/lib/lp/charms/interfaces/charmrecipe.py
@@ -371,7 +371,11 @@ class ICharmRecipeView(Interface):
"""Can the specified user see this charm recipe?"""
def requestBuild(
- build_request, distro_arch_series, charm_base=None, channels=None
+ build_request,
+ distro_arch_series,
+ charm_base=None,
+ channels=None,
+ recipe_platform_name=None,
):
"""Request a single build of this charm recipe.
@@ -384,6 +388,7 @@ class ICharmRecipeView(Interface):
:param charm_base: The `ICharmBase` to use for this build.
:param channels: A dictionary mapping snap names to channels to use
for this build.
+ :param recipe_platform_name: The platform name to build for.
:return: `ICharmRecipeBuild`.
"""
diff --git a/lib/lp/charms/interfaces/charmrecipebuild.py b/lib/lp/charms/interfaces/charmrecipebuild.py
index 78cec6b..56e4801 100644
--- a/lib/lp/charms/interfaces/charmrecipebuild.py
+++ b/lib/lp/charms/interfaces/charmrecipebuild.py
@@ -154,6 +154,14 @@ class ICharmRecipeBuildView(IPackageBuildView):
)
)
+ recipe_platform_name = exported(
+ TextLine(
+ title=_("Recipe platform name"),
+ required=False,
+ readonly=True,
+ )
+ )
+
virtualized = Bool(
title=_("If True, this build is virtualized."), readonly=True
)
@@ -318,6 +326,7 @@ class ICharmRecipeBuildSet(ISpecificBuildFarmJobSource):
recipe,
distro_arch_series,
channels=None,
+ recipe_platform_name=None,
store_upload_metadata=None,
date_created=DEFAULT,
):
diff --git a/lib/lp/charms/model/charmrecipe.py b/lib/lp/charms/model/charmrecipe.py
index 8d64194..9637dbe 100644
--- a/lib/lp/charms/model/charmrecipe.py
+++ b/lib/lp/charms/model/charmrecipe.py
@@ -540,7 +540,12 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
)
def requestBuild(
- self, build_request, distro_arch_series, charm_base=None, channels=None
+ self,
+ build_request,
+ distro_arch_series,
+ charm_base=None,
+ channels=None,
+ recipe_platform_name=None,
):
"""Request a single build of this charm recipe.
@@ -552,6 +557,7 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
:param distro_arch_series: The architecture to build for.
:param channels: A dictionary mapping snap names to channels to use
for this build.
+ :param recipe_platform_name: The platform name to build for.
:return: `ICharmRecipeBuild`.
"""
self._checkRequestBuild(build_request.requester)
@@ -572,13 +578,18 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
CharmRecipeBuild.recipe == self,
CharmRecipeBuild.distro_arch_series == distro_arch_series,
channels_clause,
+ CharmRecipeBuild.recipe_platform_name == recipe_platform_name,
CharmRecipeBuild.status == BuildStatus.NEEDSBUILD,
)
if pending.any() is not None:
raise CharmRecipeBuildAlreadyPending
build = getUtility(ICharmRecipeBuildSet).new(
- build_request, self, distro_arch_series, channels=channels
+ build_request,
+ self,
+ distro_arch_series,
+ channels=channels,
+ recipe_platform_name=recipe_platform_name,
)
build.queueBuild()
notify(ObjectCreatedEvent(build, user=build_request.requester))
@@ -645,7 +656,7 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
)
builds = []
- for das in instances_to_build:
+ for info, das in instances_to_build:
try:
charm_base = getUtility(ICharmBaseSet).getByDistroSeries(
das.distroseries
@@ -662,18 +673,23 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
else:
arch_channels = channels
try:
+ platform_name = info.platform if info is not None else None
build = self.requestBuild(
- build_request, das, channels=arch_channels
+ build_request,
+ das,
+ channels=arch_channels,
+ recipe_platform_name=platform_name,
)
if logger is not None:
logger.debug(
- " - %s/%s/%s %s/%s/%s: Build requested.",
+ " - %s/%s/%s %s/%s/%s/%s: Build requested.",
self.owner.name,
self.project.name,
self.name,
das.distroseries.distribution.name,
das.distroseries.name,
das.architecturetag,
+ platform_name,
)
builds.append(build)
except CharmRecipeBuildAlreadyPending:
@@ -683,13 +699,14 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
raise
elif logger is not None:
logger.exception(
- " - %s/%s/%s %s/%s/%s: %s",
+ " - %s/%s/%s %s/%s/%s/%s: %s",
self.owner.name,
self.project.name,
self.name,
das.distroseries.distribution.name,
das.distroseries.name,
das.architecturetag,
+ platform_name,
e,
)
return builds
diff --git a/lib/lp/charms/model/charmrecipebuild.py b/lib/lp/charms/model/charmrecipebuild.py
index e63743e..15da7f2 100644
--- a/lib/lp/charms/model/charmrecipebuild.py
+++ b/lib/lp/charms/model/charmrecipebuild.py
@@ -93,6 +93,10 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
recipe_id = Int(name="recipe", allow_none=False)
recipe = Reference(recipe_id, "CharmRecipe.id")
+ recipe_platform_name = Unicode(
+ name="recipe_platform_name", allow_none=True
+ )
+
distro_arch_series_id = Int(name="distro_arch_series", allow_none=False)
distro_arch_series = Reference(
distro_arch_series_id, "DistroArchSeries.id"
@@ -149,6 +153,7 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
processor,
virtualized,
channels=None,
+ recipe_platform_name=None,
store_upload_metadata=None,
date_created=DEFAULT,
):
@@ -163,6 +168,7 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
self.processor = processor
self.virtualized = virtualized
self.channels = channels
+ self.recipe_platform_name = recipe_platform_name
self.store_upload_metadata = store_upload_metadata
self.date_created = date_created
self.status = BuildStatus.NEEDSBUILD
@@ -475,6 +481,7 @@ class CharmRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
recipe,
distro_arch_series,
channels=None,
+ recipe_platform_name=None,
store_upload_metadata=None,
date_created=DEFAULT,
):
@@ -495,6 +502,7 @@ class CharmRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
distro_arch_series.processor,
virtualized,
channels=channels,
+ recipe_platform_name=recipe_platform_name,
store_upload_metadata=store_upload_metadata,
date_created=date_created,
)
diff --git a/lib/lp/charms/model/charmrecipebuildbehaviour.py b/lib/lp/charms/model/charmrecipebuildbehaviour.py
index f1ee46c..3e961ca 100644
--- a/lib/lp/charms/model/charmrecipebuildbehaviour.py
+++ b/lib/lp/charms/model/charmrecipebuildbehaviour.py
@@ -110,6 +110,8 @@ class CharmRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
)
)
args["private"] = build.is_private
+ if build.recipe_platform_name:
+ args["recipe_platform_name"] = build.recipe_platform_name
return args
def verifySuccessfulBuild(self):
diff --git a/lib/lp/charms/tests/test_charmrecipe.py b/lib/lp/charms/tests/test_charmrecipe.py
index 2cb3f41..8a32bc6 100644
--- a/lib/lp/charms/tests/test_charmrecipe.py
+++ b/lib/lp/charms/tests/test_charmrecipe.py
@@ -50,7 +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.adapters.buildarch import CraftPlatformsBuildPlanError
from lp.charms.interfaces.charmrecipe import (
CHARM_RECIPE_ALLOW_CREATE,
CHARM_RECIPE_BUILD_DISTRIBUTION,
@@ -546,6 +546,15 @@ class TestCharmRecipe(TestCaseWithFactory):
),
)
+ def test_requestBuild_with_recipe_platform_name(self):
+ recipe = self.factory.makeCharmRecipe()
+ das = self.makeBuildableDistroArchSeries()
+ build_request = self.factory.makeCharmRecipeBuildRequest(recipe=recipe)
+ build = recipe.requestBuild(
+ build_request, das, recipe_platform_name="ubuntu-amd64"
+ )
+ self.assertEqual("ubuntu-amd64", build.recipe_platform_name)
+
def test_requestBuilds(self):
# requestBuilds schedules a job and returns a corresponding
# CharmRecipeBuildRequest.
@@ -780,11 +789,7 @@ class TestCharmRecipe(TestCaseWithFactory):
)
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'",
- ):
+ with ExpectedException(CraftPlatformsBuildPlanError):
job.recipe.requestBuildsFromJob(
job.build_request,
channels=removeSecurityProxy(job.channels),
@@ -793,6 +798,8 @@ class TestCharmRecipe(TestCaseWithFactory):
def test_requestBuildsFromJob_unified_charmcraft_yaml_platforms_missing(
self,
):
+ # If the recipe has no platforms specified, craft-platforms builds
+ # for all default architectures without an option of cross-compiling.
self.useFixture(
GitHostingFixture(
blob=dedent(
@@ -809,13 +816,13 @@ class TestCharmRecipe(TestCaseWithFactory):
)
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),
- )
+ builds = job.recipe.requestBuildsFromJob(
+ job.build_request,
+ channels=removeSecurityProxy(job.channels),
+ )
+ self.assertRequestedBuildsMatch(
+ builds, job, "24.04", ["amd64", "riscv64", "arm64"], job.channels
+ )
def test_requestBuildsFromJob_unified_charmcraft_yaml_fully_expanded(self):
self.useFixture(
@@ -890,8 +897,8 @@ class TestCharmRecipe(TestCaseWithFactory):
channel: 24.04
platforms:
ubuntu-amd64:
- build-on: amd64
- build-for: amd64
+ build-on: [amd64]
+ build-for: [amd64]
"""
)
)
@@ -959,9 +966,7 @@ class TestCharmRecipe(TestCaseWithFactory):
transaction.commit()
with person_logged_in(job.requester):
with ExpectedException(
- BadPropertyError,
- "'foobar' is not a supported architecture for "
- "'ubuntu@24.04'",
+ CraftPlatformsBuildPlanError,
):
job.recipe.requestBuildsFromJob(
job.build_request,
diff --git a/lib/lp/charms/tests/test_charmrecipebuildbehaviour.py b/lib/lp/charms/tests/test_charmrecipebuildbehaviour.py
index 29fefa6..27512bc 100644
--- a/lib/lp/charms/tests/test_charmrecipebuildbehaviour.py
+++ b/lib/lp/charms/tests/test_charmrecipebuildbehaviour.py
@@ -420,6 +420,22 @@ class TestAsyncCharmRecipeBuildBehaviour(
self.assertEqual({"charmcraft": "edge"}, args["channels"])
@defer.inlineCallbacks
+ def test_extraBuildArgs_recipe_platform_name(self):
+ # If the build is for a particular platform name, extraBuildArgs
+ # sends it.
+ job = self.makeJob(recipe_platform_name="ubuntu-amd64")
+ (
+ expected_archives,
+ expected_trusted_keys,
+ ) = yield get_sources_list_for_building(
+ job, job.build.distro_arch_series, None
+ )
+ with dbuser(config.builddmaster.dbuser):
+ args = yield job.extraBuildArgs()
+ self.assertFalse(isProxy(args["recipe_platform_name"]))
+ self.assertEqual("ubuntu-amd64", args["recipe_platform_name"])
+
+ @defer.inlineCallbacks
def test_extraBuildArgs_archives_primary(self):
# The build uses the release, security, and updates pockets from the
# primary archive.
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 427e323..b956c6d 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -6826,6 +6826,7 @@ class LaunchpadObjectFactory(ObjectFactory):
requester=None,
distro_arch_series=None,
channels=None,
+ recipe_platform_name=None,
store_upload_metadata=None,
date_created=DEFAULT,
status=BuildStatus.NEEDSBUILD,
@@ -6851,6 +6852,7 @@ class LaunchpadObjectFactory(ObjectFactory):
recipe,
distro_arch_series,
channels=channels,
+ recipe_platform_name=recipe_platform_name,
store_upload_metadata=store_upload_metadata,
date_created=date_created,
)
Follow ups