Skip to content

Commit

Permalink
feat: support multi-base platform recipes (#2054)
Browse files Browse the repository at this point in the history
Adds support for multi-base recipes with the `platforms` keyword.

Internally, this moves to a new practice where the application stops modifying project data before passing the it to craft-platforms.  This will ensure consistent processing by charmcraft and by launchpad.

Documentation is coming soon via canonical/craft-platforms#75 and #2009.

To summarize the usage, `base` and `build-base` are removed from the project and the base is encoded into the platform.

Platforms can be defined in a shorthand notation:
```yaml
platforms:
  ubuntu@22.04:amd64:
  ubuntu@24.04:amd64:
```

Or they can be defined in standard form:
```yaml
platforms:
  jammy:
    build-on: [ubuntu@22.04:amd64]
    build-for: [ubuntu@22.04:amd64]
  noble:
    build-on: [ubuntu@24.04:amd64]
    build-for: [ubuntu@24.04:amd64]
```


Fixes #1789 
Fixes #2008 
Creates canonical/craft-platforms#85
Found during implementation canonical/craft-platforms#80
(CRAFT-3718)
  • Loading branch information
mr-cal authored Dec 19, 2024
1 parent d6ae069 commit 1ad02d8
Show file tree
Hide file tree
Showing 21 changed files with 538 additions and 250 deletions.
32 changes: 24 additions & 8 deletions charmcraft/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from craft_platforms import charm
from craft_providers import bases
from pydantic import dataclasses
from typing_extensions import Self
from typing_extensions import Self, override

from charmcraft import const, preprocess, utils
from charmcraft.const import (
Expand Down Expand Up @@ -323,6 +323,15 @@ class CharmcraftBuildPlanner(models.BuildPlanner):
build_base: str | None = None
platforms: dict[str, models.Platform | None] | None = None # type: ignore[assignment]

@override
@pydantic.field_validator("platforms", mode="before")
@classmethod
def _populate_platforms(cls, platforms: dict[str, Any]) -> dict[str, Any]:
"""Overrides the validator to prevent platforms from being modified.
Modifying the platforms field can break multi-base builds."""
return platforms

def get_build_plan(self) -> list[models.BuildInfo]:
"""Get build bases for this charm.
Expand All @@ -346,13 +355,9 @@ def get_build_plan(self) -> list[models.BuildInfo]:
),
)
]
if not self.base:
if not self.base and not self.platforms:
return list(CharmBuildInfo.gen_from_bases_configurations(*self.bases))

build_base = self.build_base or self.base
base_name, _, base_version = build_base.partition("@")
base = bases.BaseName(name=base_name, version=base_version)

if self.platforms is None:
raise CraftError("Must define at least one platform.")
platforms = cast(
Expand All @@ -373,7 +378,9 @@ def get_build_plan(self) -> list[models.BuildInfo]:
platform=info.platform,
build_on=str(info.build_on),
build_for=str(info.build_for),
base=base,
base=bases.BaseName(
name=info.build_base.distribution, version=info.build_base.series
),
)
for info in build_infos
]
Expand Down Expand Up @@ -1064,7 +1071,7 @@ class PlatformCharm(CharmProject):
"""Model for defining a charm using Platforms."""

# Silencing pyright because it complains about missing default value
base: BaseStr # pyright: ignore[reportGeneralTypeIssues]
base: BaseStr | None = None
build_base: BuildBaseStr | None = None
platforms: dict[str, models.Platform | None] # type: ignore[assignment]

Expand All @@ -1076,6 +1083,15 @@ def _validate_dev_base_needs_build_base(self) -> Self:
)
return self

@override
@pydantic.field_validator("platforms", mode="before")
@classmethod
def _populate_platforms(cls, platforms: dict[str, Any]) -> dict[str, Any]:
"""Overrides the validator to prevent platforms from being modified.
Modifying the platforms field can break multi-base builds."""
return platforms


Charm = BasesCharm | PlatformCharm

Expand Down
46 changes: 33 additions & 13 deletions charmcraft/services/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from typing import TYPE_CHECKING, cast

import craft_application
import craft_platforms
import yaml
from craft_application import services, util
from craft_cli import emit
Expand Down Expand Up @@ -213,20 +214,39 @@ def get_manifest_bases(self) -> list[models.Base]:
raise RuntimeError("Could not determine run-on bases.")
return run_on_bases
if isinstance(self._project, PlatformCharm):
if not self._platform:
architectures = [util.get_host_architecture()]
elif self._platform in (*const.SUPPORTED_ARCHITECTURES, "all"):
architectures = [self._platform]
elif platform := self._project.platforms.get(self._platform):
if platform.build_for:
architectures = [str(arch) for arch in platform.build_for]
else:
raise ValueError(
f"Platform {self._platform} contains unknown build-for."
archs = [self._build_plan[0].build_for]

# single base recipes will have a base
if self._project.base:
return [models.Base.from_str_and_arch(self._project.base, archs)]

# multi-base recipes may have the base in the platform name
platform_label = self._build_plan[0].platform
if base := craft_platforms.parse_base_and_name(platform_label)[0]:
return [
models.Base(
name=base.distribution,
channel=base.series,
architectures=archs,
)
else:
architectures = [util.get_host_architecture()]
return [models.Base.from_str_and_arch(self._project.base, architectures)]
]

# Otherwise, retrieve the build-for base from the platform in the project.
# This complexity arises from building on devel bases - the BuildInfo
# contains the devel base and not the compatibility base.
platform = self._project.platforms.get(platform_label)
if platform and platform.build_for:
if base := craft_platforms.parse_base_and_architecture(
platform.build_for[0]
)[0]:
return [
models.Base(
name=base.distribution,
channel=base.series,
architectures=archs,
)
]

raise TypeError(
f"Unknown charm type {self._project.__class__}, cannot get bases."
)
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ dynamic = ["version"]
description = "The main tool to build, upload, and develop in general the Juju charms."
readme = "README.md"
dependencies = [
"craft-application~=4.2",
"craft-application~=4.7",
"craft-cli>=2.3.0",
"craft-grammar>=2.0.0",
"craft-parts>=2.2.0",
"craft-providers>=2.0.0",
"craft-platforms~=0.3",
"craft-platforms~=0.5",
"craft-providers>=2.0.0",
"craft-store>=3.1.0",
"distro>=1.7.0",
Expand Down
6 changes: 3 additions & 3 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.4.0
coverage==7.6.9
craft-application==4.4.0
craft-application==4.7.0
craft-archives==2.0.2
craft-cli==2.10.1
craft-cli==2.13.0
craft-grammar==2.0.1
craft-parts==2.2.0
craft-platforms==0.4.0
craft-platforms==0.5.0
craft-providers==2.0.4
craft-store==3.1.0
cryptography==43.0.3
Expand Down
6 changes: 1 addition & 5 deletions tests/integration/commands/test_expand_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,7 @@ def fake_extensions(stub_extensions):
description: test-description
base: ubuntu@22.04
platforms:
amd64:
build-on:
- amd64
build-for:
- amd64
amd64: null
parts: {}
type: charm
terms:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ description: |
A description for an example charm with platforms.
base: ubuntu@22.04
platforms:
amd64:
build-on:
- amd64
build-for:
- amd64
amd64: null
parts:
charm:
plugin: charm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ description: |
A description for an example charm with platforms.
base: ubuntu@22.04
platforms:
amd64:
build-on:
- amd64
build-for:
- amd64
amd64: null
parts:
charm:
plugin: charm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ description: |
A description for an example charm with platforms.
base: ubuntu@22.04
platforms:
amd64:
build-on:
- amd64
build-for:
- amd64
amd64: null
parts:
charm:
plugin: charm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ description: |
A description for an example charm with platforms.
base: ubuntu@22.04
platforms:
amd64:
build-on:
- amd64
build-for:
- amd64
amd64: null
parts:
charm:
plugin: charm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ description: |
A description for an example charm with platforms.
base: ubuntu@22.04
platforms:
amd64:
build-on:
- amd64
build-for:
- amd64
amd64: null
parts:
charm:
plugin: charm
Expand Down
20 changes: 20 additions & 0 deletions tests/spread/smoketests/multi-base/all/charmcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: test-charm
type: charm
title: test
summary: test
description: |
A charm recipe that uses a multi-base platform syntax to define
architecture independent charms for 22.04 and 24.04.
platforms:
jammy:
build-on: [ubuntu@22.04:amd64]
build-for: [ubuntu@22.04:all]
noble:
build-on: [ubuntu@24.04:amd64]
build-for: [ubuntu@24.04:all]

parts:
my-charm:
plugin: dump
source: .
2 changes: 2 additions & 0 deletions tests/spread/smoketests/multi-base/all/expected-charms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test-charm_jammy.charm
test-charm_noble.charm
28 changes: 28 additions & 0 deletions tests/spread/smoketests/multi-base/basic/charmcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: test-charm
type: charm
title: test
summary: test
description: |
A charm recipe that uses a multi-base platform syntax to define
6 charms across different bases and architectures.
platforms:
# shorthand syntax
ubuntu@20.04:amd64:
ubuntu@20.04:riscv64:

ubuntu@22.04:amd64:
ubuntu@22.04:riscv64:

# standard syntax
noble-amd64:
build-on: [ubuntu@24.04:amd64]
build-for: [ubuntu@24.04:amd64]
noble-riscv64:
build-on: [ubuntu@24.04:riscv64]
build-for: [ubuntu@24.04:riscv64]

parts:
my-charm:
plugin: charm
source: .
3 changes: 3 additions & 0 deletions tests/spread/smoketests/multi-base/basic/expected-charms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test-charm_ubuntu@20.04:amd64.charm
test-charm_ubuntu@22.04:amd64.charm
test-charm_noble-amd64.charm
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--platform ubuntu@22.04:amd64
29 changes: 29 additions & 0 deletions tests/spread/smoketests/multi-base/one-platform/charmcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: test-charm
type: charm
title: test
summary: test
description: |
A charm recipe that uses a multi-base platform syntax to define
6 charms across different bases and architectures.
This test builds one of the charms using the `--platform` argument.
platforms:
# shorthand syntax
ubuntu@20.04:amd64:
ubuntu@20.04:riscv64:

ubuntu@22.04:amd64:
ubuntu@22.04:riscv64:

# standard syntax
noble-amd64:
build-on: [ubuntu@24.04:amd64]
build-for: [ubuntu@24.04:amd64]
noble-riscv64:
build-on: [ubuntu@24.04:riscv64]
build-for: [ubuntu@24.04:riscv64]

parts:
my-charm:
plugin: charm
source: .
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test-charm_ubuntu@22.04:amd64.charm
52 changes: 52 additions & 0 deletions tests/spread/smoketests/multi-base/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
summary: pack charm using multi-base notation
kill-timeout: 30m # These sometimes take a while to download bases.
priority: 50 # Because these can take a while, run them early.

environment:
CHARM/all: all
CHARM/basic: basic
CHARM/one_platform: one-platform

# test only on amd64
systems:
- ubuntu-22.04-64
- ubuntu-22.04-amd64

include:
- tests/

prepare: |
# '--force' because charmcraft.yaml already exists
charmcraft init --force --project-dir="$CHARM"
restore: |
cd $CHARM
charmcraft clean
execute: |
cd $CHARM
if [[ -e "arguments.txt" ]]; then
call_args=$(cat "arguments.txt")
else
call_args=""
fi
# shellcheck disable=SC2046 (quote to prevent word splitting)
charmcraft pack $call_args
# assert charms were built
while read -r charm_file; do
if [[ ! -e ${charm_file} ]]; then
echo "Could not find charm '${charm_file}'"
exit 1
fi
done < "expected-charms.txt"
# assert no other charms were built
expected_num=$(wc -l < "expected-charms.txt")
actual_num=$(find . -wholename "./*.charm" | wc -l)
if [[ $expected_num -ne $actual_num ]]; then
echo "Expected $expected_num charms, but found $actual_num."
exit 1
fi
Loading

0 comments on commit 1ad02d8

Please sign in to comment.