diff --git a/charmcraft/application/main.py b/charmcraft/application/main.py index 54ef2ea4c..7727d960d 100644 --- a/charmcraft/application/main.py +++ b/charmcraft/application/main.py @@ -130,12 +130,11 @@ def _get_app_plugins(self) -> dict[str, PluginType]: ) except ProjectFileMissingError: return plugins - bases = {build_info.build_base for build_info in full_build_plan} - for base in bases: - if str(base) not in const.CHARM_OR_REACTIVE_BASES: - plugins.pop("charm") - plugins.pop("reactive") - break + bases = {str(build_info.build_base) for build_info in full_build_plan} + if any(base not in const.CHARM_PLUGIN_BASES for base in bases): + plugins.pop("charm", None) + if any(base not in const.REACTIVE_PLUGIN_BASES for base in bases): + plugins.pop("reactive", None) return plugins diff --git a/charmcraft/const.py b/charmcraft/const.py index 9b6166295..f3316c523 100644 --- a/charmcraft/const.py +++ b/charmcraft/const.py @@ -62,7 +62,7 @@ "almalinux@9", ) -CHARM_OR_REACTIVE_BASES = frozenset( # Bases with the 'charm' & 'reactive' plugins. +CHARM_PLUGIN_BASES = frozenset( # Bases with the 'charm' plugin. ( *LEGACY_BASES, "ubuntu@24.04", @@ -71,6 +71,13 @@ ) ) +REACTIVE_PLUGIN_BASES = frozenset( # Bases with the 'reactive' plugin. + ( + *CHARM_PLUGIN_BASES, + "ubuntu@26.04", + ) +) + CommonBaseStr = Literal[ # Bases supported as both build bases and run bases "ubuntu@18.04", "ubuntu@20.04", @@ -78,15 +85,15 @@ "ubuntu@24.04", "ubuntu@25.04", "ubuntu@25.10", + "ubuntu@26.04", "almalinux@9", ] -BaseStr = CommonBaseStr | Literal["ubuntu@26.04"] +BaseStr = CommonBaseStr BuildBaseStr = CommonBaseStr | Literal["ubuntu@devel"] DEVEL_BASE_STRINGS = ( "ubuntu@25.04", "ubuntu@25.10", - "ubuntu@26.04", ) # Bases that require a specified build base. SUPPORTED_BASE_STRINGS = frozenset( @@ -94,7 +101,15 @@ ( *( f"ubuntu@{series}" - for series in ("18.04", "20.04", "22.04", "24.04", "25.04", "25.10") + for series in ( + "18.04", + "20.04", + "22.04", + "24.04", + "25.04", + "25.10", + "26.04", + ) ), *(("almalinux@9",)), ) diff --git a/charmcraft/models/project.py b/charmcraft/models/project.py index 2560c6ccb..f22aff030 100644 --- a/charmcraft/models/project.py +++ b/charmcraft/models/project.py @@ -236,6 +236,11 @@ def started_at(self) -> datetime.datetime: """Get the time that Charmcraft started running.""" return self._started_at + @classmethod + def _get_devel_bases(cls) -> Iterable[Any]: + """Return craft-application devel bases that require build-base: devel.""" + return () + @classmethod def unmarshal(cls, data: dict[str, Any]): """Create a Charmcraft project from a dictionary of data.""" @@ -927,27 +932,27 @@ def _populate_platforms(cls, platforms: dict[str, Any]) -> dict[str, Any]: def _validate_removed_questing_plugins( cls, value: dict[str, dict[str, Any]], info: pydantic.ValidationInfo ) -> dict[str, dict[str, Any]]: - """Check that the charm and reactive plugins aren't used on Ubuntu 25.10+.""" + """Check that the charm and reactive plugins are only used on supported bases.""" plugins = {v.get("plugin", k) for k, v in value.items()} - if not plugins & {"charm", "reactive"}: + legacy_plugins = plugins & {"charm", "reactive"} + if not legacy_plugins: return value - if (base := info.data.get("base")) in const.CHARM_OR_REACTIVE_BASES: - return value - if base is not None: - raise ValueError( - f"Cannot use 'charm' or 'reactive' plugins with base {base!r}" - ) - # Multi-base charms. - build_bases = { - str(info.build_base) - for info in craft_platforms.charm.get_platforms_charm_build_plan( - base=None, - platforms=pydantic.TypeAdapter(PlatformsDict).dump_python( - info.data["platforms"], mode="json", by_alias=True - ), - ) - } - if invalid_bases := build_bases - const.CHARM_OR_REACTIVE_BASES: + + if base := info.data.get("base"): + build_bases = {base} + else: + # Multi-base charms. + build_bases = { + str(info.build_base) + for info in craft_platforms.charm.get_platforms_charm_build_plan( + base=None, + platforms=pydantic.TypeAdapter(PlatformsDict).dump_python( + info.data["platforms"], mode="json", by_alias=True + ), + ) + } + + if invalid_bases := build_bases - const.REACTIVE_PLUGIN_BASES: if len(invalid_bases) == 1: raise ValueError( f"Cannot use 'charm' or 'reactive' plugins with base {invalid_bases.pop()!r}" @@ -956,6 +961,19 @@ def _validate_removed_questing_plugins( raise ValueError( f"Cannot use 'charm' or 'reactive' plugins with bases {invalid_bases_str}" ) + + if "charm" in legacy_plugins: + if invalid_bases := build_bases - const.CHARM_PLUGIN_BASES: + if len(invalid_bases) == 1: + raise ValueError( + f"Cannot use 'charm' plugin with base {invalid_bases.pop()!r}" + ) + invalid_bases_str = humanize_list( + sorted(invalid_bases), conjunction="or" + ) + raise ValueError( + f"Cannot use 'charm' plugin with bases {invalid_bases_str}" + ) return value diff --git a/charmcraft/parts/plugins/_reactive.py b/charmcraft/parts/plugins/_reactive.py index 818e610c0..6096a9815 100644 --- a/charmcraft/parts/plugins/_reactive.py +++ b/charmcraft/parts/plugins/_reactive.py @@ -103,7 +103,14 @@ def get_build_snaps(cls) -> set[str]: def get_build_packages(self) -> set[str]: """Return a set of required packages to install in the build environment.""" - return set() + return { + "git", + "python3-pip", + "python3-setuptools", + "python3-venv", + "python3-wheel", + "virtualenv", + } def get_build_environment(self) -> dict[str, str]: """Return a dictionary with the environment to use in the build step.""" diff --git a/pyproject.toml b/pyproject.toml index e676edcee..32c86ff57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dependencies = [ "craft-cli>=2.15.0", "craft-grammar>=2.0.0", "craft-parts~=2.20", - "craft-providers~=3.1", + "craft-providers>=3.4.0,<4.0", "craft-platforms~=0.10", "craft-store~=3.2", "distro>=1.7.0", diff --git a/schema/charmcraft.json b/schema/charmcraft.json index 66f455bad..7b8fcb3e5 100644 --- a/schema/charmcraft.json +++ b/schema/charmcraft.json @@ -900,14 +900,11 @@ "ubuntu@24.04", "ubuntu@25.04", "ubuntu@25.10", + "ubuntu@26.04", "almalinux@9" ], "type": "string" }, - { - "const": "ubuntu@26.04", - "type": "string" - }, { "type": "null" } @@ -925,6 +922,7 @@ "ubuntu@24.04", "ubuntu@25.04", "ubuntu@25.10", + "ubuntu@26.04", "almalinux@9" ], "type": "string" diff --git a/tests/integration/invalid-charms/multibase-charm-plugin/errors.json b/tests/integration/invalid-charms/multibase-charm-plugin/errors.json index fc6732de0..9aa9e274b 100644 --- a/tests/integration/invalid-charms/multibase-charm-plugin/errors.json +++ b/tests/integration/invalid-charms/multibase-charm-plugin/errors.json @@ -4,7 +4,7 @@ "loc": [ "parts" ], - "msg": "Value error, Cannot use 'charm' or 'reactive' plugins with bases 'ubuntu@25.10' or 'ubuntu@26.04'", + "msg": "Value error, Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@25.10'", "input": { "my-part": { "plugin": "charm", @@ -12,7 +12,7 @@ } }, "ctx": { - "error": "Cannot use 'charm' or 'reactive' plugins with bases 'ubuntu@25.10' or 'ubuntu@26.04'" + "error": "Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@25.10'" } } ] diff --git a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml index 50e15b20c..ca161f914 100644 --- a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml +++ b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml @@ -1,13 +1,12 @@ -type: charm -name: test-charm +name: invalid-charm summary: An invalid charm based on Ubuntu Resolute (26.04 LTS) with the charm plugin. -description: test-charm - +description: | + Charm plugin is not supported on Ubuntu 26.04 LTS. +type: charm platforms: - "resolute": + resolute: build-on: ubuntu@26.04:amd64 build-for: ubuntu@26.04:amd64 - parts: my-part: plugin: charm diff --git a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json index 8941bc0fd..b016b7e19 100644 --- a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json +++ b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json @@ -4,7 +4,7 @@ "loc": [ "parts" ], - "msg": "Value error, Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@26.04'", + "msg": "Value error, Cannot use 'charm' plugin with base 'ubuntu@26.04'", "input": { "my-part": { "plugin": "charm", @@ -12,7 +12,7 @@ } }, "ctx": { - "error": "Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@26.04'" + "error": "Cannot use 'charm' plugin with base 'ubuntu@26.04'" } } ] diff --git a/tests/integration/invalid-charms/resolute-charm-plugin/charmcraft.yaml b/tests/integration/invalid-charms/resolute-charm-plugin/charmcraft.yaml index 1669b50f2..27680d916 100644 --- a/tests/integration/invalid-charms/resolute-charm-plugin/charmcraft.yaml +++ b/tests/integration/invalid-charms/resolute-charm-plugin/charmcraft.yaml @@ -1,15 +1,11 @@ -type: charm -name: test-charm +name: invalid-charm summary: An invalid charm based on Ubuntu Resolute (26.04 LTS) with the charm plugin. -description: test-charm - +description: | + Charm plugin is not supported on Ubuntu 26.04 LTS. +type: charm base: ubuntu@26.04 -build-base: ubuntu@devel platforms: amd64: - arm64: - riscv64: - parts: my-part: plugin: charm diff --git a/tests/integration/invalid-charms/resolute-charm-plugin/errors.json b/tests/integration/invalid-charms/resolute-charm-plugin/errors.json index 8941bc0fd..b016b7e19 100644 --- a/tests/integration/invalid-charms/resolute-charm-plugin/errors.json +++ b/tests/integration/invalid-charms/resolute-charm-plugin/errors.json @@ -4,7 +4,7 @@ "loc": [ "parts" ], - "msg": "Value error, Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@26.04'", + "msg": "Value error, Cannot use 'charm' plugin with base 'ubuntu@26.04'", "input": { "my-part": { "plugin": "charm", @@ -12,7 +12,7 @@ } }, "ctx": { - "error": "Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@26.04'" + "error": "Cannot use 'charm' plugin with base 'ubuntu@26.04'" } } ] diff --git a/tests/integration/invalid-charms/resolute-reactive-plugin/charmcraft.yaml b/tests/integration/invalid-charms/resolute-reactive-plugin/charmcraft.yaml deleted file mode 100644 index 9f8e460f5..000000000 --- a/tests/integration/invalid-charms/resolute-reactive-plugin/charmcraft.yaml +++ /dev/null @@ -1,16 +0,0 @@ -type: charm -name: test-charm -summary: An invalid charm based on Ubuntu Resolute (26.04 LTS) with the reactive plugin. -description: test-charm - -base: ubuntu@26.04 -build-base: ubuntu@devel -platforms: - amd64: - arm64: - riscv64: - -parts: - my-part: - plugin: reactive - source: . diff --git a/tests/integration/invalid-charms/resolute-reactive-plugin/errors.json b/tests/integration/invalid-charms/resolute-reactive-plugin/errors.json deleted file mode 100644 index a2e144b7f..000000000 --- a/tests/integration/invalid-charms/resolute-reactive-plugin/errors.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "type": "value_error", - "loc": [ - "parts" - ], - "msg": "Value error, Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@26.04'", - "input": { - "my-part": { - "plugin": "reactive", - "source": "." - } - }, - "ctx": { - "error": "Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@26.04'" - } - } -] diff --git a/tests/integration/sample-charms/platforms-resolute-reactive/charmcraft.yaml b/tests/integration/sample-charms/platforms-resolute-reactive/charmcraft.yaml new file mode 100644 index 000000000..c5ec3373f --- /dev/null +++ b/tests/integration/sample-charms/platforms-resolute-reactive/charmcraft.yaml @@ -0,0 +1,12 @@ +name: example-charm +summary: An example charm with platforms +description: | + A description for an example charm with platforms. +type: charm +base: ubuntu@26.04 +platforms: + amd64: + +parts: + reactive: + plugin: reactive diff --git a/tests/integration/sample-charms/platforms-resolute-reactive/expected.yaml b/tests/integration/sample-charms/platforms-resolute-reactive/expected.yaml new file mode 100644 index 000000000..629f42c2d --- /dev/null +++ b/tests/integration/sample-charms/platforms-resolute-reactive/expected.yaml @@ -0,0 +1,15 @@ +name: example-charm +summary: An example charm with platforms +description: | + A description for an example charm with platforms. +base: ubuntu@26.04 +platforms: + amd64: + build-on: + - amd64 + build-for: + - amd64 +parts: + reactive: + plugin: reactive +type: charm diff --git a/tests/integration/test_application.py b/tests/integration/test_application.py index e8ca426c8..342342676 100644 --- a/tests/integration/test_application.py +++ b/tests/integration/test_application.py @@ -125,5 +125,12 @@ def test_remove_charm_reactive_plugins( with pytest.raises(ValueError, match="plugin not registered: 'charm'"): craft_parts.plugins.plugins.get_plugin_class("charm") - with pytest.raises(ValueError, match="plugin not registered: 'reactive'"): + + if charm_dir.name in { + "multibase-resolute-charm-plugin", + "resolute-charm-plugin", + }: craft_parts.plugins.plugins.get_plugin_class("reactive") + else: + with pytest.raises(ValueError, match="plugin not registered: 'reactive'"): + craft_parts.plugins.plugins.get_plugin_class("reactive") diff --git a/tests/spread/smoketests/reactive/reactivecharm/charmcraft.yaml b/tests/spread/smoketests/reactive/reactivecharm/charmcraft.yaml index d393d5262..98eefa708 100644 --- a/tests/spread/smoketests/reactive/reactivecharm/charmcraft.yaml +++ b/tests/spread/smoketests/reactive/reactivecharm/charmcraft.yaml @@ -2,14 +2,19 @@ type: "charm" bases: - build-on: - name: "ubuntu" - channel: "22.04" + channel: "BASE_CHANNEL" run-on: - name: "ubuntu" - channel: "22.04" + channel: "BASE_CHANNEL" parts: charm: source: . plugin: reactive + reactive-charm-build-arguments: + - -v + - --binary-wheels-from-source + - --upgrade-buildvenv-core-deps + - --ignore-requires-python build-snaps: - charm/CHARM_SNAP_CHANNEL build-packages: [python3-dev] diff --git a/tests/spread/smoketests/reactive/task.yaml b/tests/spread/smoketests/reactive/task.yaml index 2df2752ee..9d8628a58 100644 --- a/tests/spread/smoketests/reactive/task.yaml +++ b/tests/spread/smoketests/reactive/task.yaml @@ -5,11 +5,36 @@ priority: 100 environment: CHARM_SNAP_CHANNEL/stable: stable CHARM_SNAP_CHANNEL/two: 2.x/stable + CHARM_SNAP_CHANNEL/resolute: latest/candidate + BASE_CHANNEL/stable,two: "22.04" + BASE_CHANNEL/resolute: "26.04" prepare: | cd reactivecharm - sed -i "s|CHARM_SNAP_CHANNEL|$CHARM_SNAP_CHANNEL|" charmcraft.yaml + if [ "$BASE_CHANNEL" = "26.04" ]; then + cat > charmcraft.yaml < Iterator[mock.Mock]: bases.BaseName("ubuntu", "20.04"), bases.BaseName("ubuntu", "22.04"), bases.BaseName("ubuntu", "24.04"), + bases.BaseName("ubuntu", "26.04"), bases.BaseName("ubuntu", "devel"), bases.BaseName("almalinux", "9"), ], @@ -94,6 +95,7 @@ def test_get_base_forwards_cache( bases.BaseName("ubuntu", "20.04"), bases.BaseName("ubuntu", "22.04"), bases.BaseName("ubuntu", "24.04"), + bases.BaseName("ubuntu", "26.04"), bases.BaseName("ubuntu", "devel"), bases.BaseName("almalinux", "9"), ], diff --git a/uv.lock b/uv.lock index 9156ed5f7..d9871b631 100644 --- a/uv.lock +++ b/uv.lock @@ -434,7 +434,7 @@ requires-dist = [ { name = "craft-grammar", specifier = ">=2.0.0" }, { name = "craft-parts", specifier = "~=2.20" }, { name = "craft-platforms", specifier = "~=0.10" }, - { name = "craft-providers", specifier = "~=3.1" }, + { name = "craft-providers", specifier = ">=3.4.0,<4.0" }, { name = "craft-store", specifier = "~=3.2" }, { name = "distro", specifier = ">=1.7.0" }, { name = "docker", specifier = ">=7.0.0" },