diff --git a/docs/reference/rockcraft.yaml.rst b/docs/reference/rockcraft.yaml.rst index 0c714e3b7..61c88a014 100644 --- a/docs/reference/rockcraft.yaml.rst +++ b/docs/reference/rockcraft.yaml.rst @@ -70,7 +70,7 @@ The rock version, used to tag the OCI image and name the rock file. ``base`` -------- -**Type**: One of ``ubuntu@20.04 | ubuntu@22.04 | bare`` +**Type**: One of ``ubuntu@20.04 | ubuntu@22.04 | ubuntu@24.04 | bare`` **Required**: Yes @@ -84,10 +84,14 @@ which is typically used with static binaries or The notation "ubuntu:" is also supported for some channels, but this format is deprecated and should be avoided. +.. note:: + Base ``ubuntu@24.04`` is still unstable and under active development. To use + it, ``build-base`` *must* be ``devel``. + ``build-base`` -------------- -**Type**: One of ``ubuntu@20.04 | ubuntu@22.04`` +**Type**: One of ``ubuntu@20.04 | ubuntu@22.04 | devel`` **Required**: Yes, if ``base`` is ``bare`` @@ -101,6 +105,11 @@ the value of ``base``. The notation "ubuntu:" is also supported for some channels, but this format is deprecated and should be avoided. +.. note:: + ``devel`` is a "special" value that means "the next Ubuntu version, currently + in development". This means that the contents of this system changes + frequently and should not be relied on for production rocks. + ``license`` ----------- diff --git a/rockcraft/models/project.py b/rockcraft/models/project.py index 1b68a070c..78c54a1e3 100644 --- a/rockcraft/models/project.py +++ b/rockcraft/models/project.py @@ -117,6 +117,14 @@ def _validate_platform_set(cls, values: Mapping[str, Any]) -> Mapping[str, Any]: DEPRECATED_COLON_BASES = ["ubuntu:20.04", "ubuntu:22.04"] +CURRENT_DEVEL_BASE = "ubuntu@24.04" + +DEVEL_BASE_WARNING = ( + "The development build-base should only be used for testing purposes, " + "as its contents are bound to change with the opening of new Ubuntu releases, " + "suddenly and without warning." +) + class NameStr(pydantic.ConstrainedStr): """Constrained string type only accepting valid rock names.""" @@ -133,8 +141,8 @@ class Project(YamlModelMixin, BaseProject): description: str # type: ignore[reportIncompatibleVariableOverride] rock_license: str = pydantic.Field(alias="license") platforms: dict[str, Any] - base: Literal["bare", "ubuntu@20.04", "ubuntu@22.04"] - build_base: Literal["ubuntu@20.04", "ubuntu@22.04"] | None + base: Literal["bare", "ubuntu@20.04", "ubuntu@22.04", "ubuntu@24.04"] + build_base: Literal["ubuntu@20.04", "ubuntu@22.04", "devel"] | None environment: dict[str, str] | None run_user: _RunUser services: dict[str, Service] | None @@ -154,7 +162,12 @@ class Config(CraftBaseConfig): # pylint: disable=too-few-public-methods def effective_base(self) -> bases.BaseName: """Get the Base name for craft-providers.""" base = super().effective_base - name, channel = base.split("@") + + if base == "devel": + name, channel = "ubuntu", "devel" + else: + name, channel = base.split("@") + return bases.BaseName(name, channel) @pydantic.root_validator(pre=True) @@ -197,6 +210,24 @@ def _validate_title(cls, title: str | None, values: Mapping[str, Any]) -> str: title = values.get("name", "") return cast(str, title) + @pydantic.root_validator(skip_on_failure=True) + @classmethod + def _validate_devel_base(cls, values: Mapping[str, Any]) -> Mapping[str, Any]: + """If 'base' is currently unstable, 'build-base' must be 'devel'.""" + base = values.get("base") + build_base = values.get("build_base") + + if base == CURRENT_DEVEL_BASE and build_base != "devel": + raise CraftValidationError( + f'To use the unstable base "{CURRENT_DEVEL_BASE}", ' + '"build-base" must be "devel".' + ) + + if base == CURRENT_DEVEL_BASE: + craft_cli.emit.message(DEVEL_BASE_WARNING) + + return values + @pydantic.validator("build_base", always=True) @classmethod def _validate_build_base( diff --git a/schema/rockcraft.json b/schema/rockcraft.json index a2d5d35e7..bd2a7ae69 100644 --- a/schema/rockcraft.json +++ b/schema/rockcraft.json @@ -35,7 +35,8 @@ "enum": [ "bare", "ubuntu@20.04", - "ubuntu@22.04" + "ubuntu@22.04", + "ubuntu@24.04" ], "type": "string" }, @@ -95,7 +96,8 @@ "title": "Build-Base", "enum": [ "ubuntu@20.04", - "ubuntu@22.04" + "ubuntu@22.04", + "devel" ], "type": "string" }, diff --git a/tests/integration/plugins/test_python_plugin.py b/tests/integration/plugins/test_python_plugin.py index 052891127..0229f995b 100644 --- a/tests/integration/plugins/test_python_plugin.py +++ b/tests/integration/plugins/test_python_plugin.py @@ -25,7 +25,7 @@ from craft_parts.utils.os_utils import OsRelease from rockcraft import plugins -from rockcraft.models.project import Project +from rockcraft.models.project import CURRENT_DEVEL_BASE, Project from rockcraft.plugins.python_plugin import SITECUSTOMIZE_TEMPLATE from tests.testing.project import create_project from tests.util import ubuntu_only @@ -60,10 +60,11 @@ def create_python_project(base, extra_part_props=None) -> Project: } } - return create_project( - base=base, - parts=parts, - ) + build_base = None + if base == CURRENT_DEVEL_BASE: + build_base = "devel" + + return create_project(base=base, parts=parts, build_base=build_base) @dataclass diff --git a/tests/spread/general/base-devel/rockcraft.orig.yaml b/tests/spread/general/base-devel/rockcraft.orig.yaml new file mode 100644 index 000000000..abd75f53b --- /dev/null +++ b/tests/spread/general/base-devel/rockcraft.orig.yaml @@ -0,0 +1,18 @@ +name: base-devel +base: placeholder-base +build-base: devel +version: '0.1' +summary: A project using the "devel" build-base +description: A project using the "devel" build-base +license: GPL-3.0 +platforms: + amd64: + +parts: + from-chisel-slices: + plugin: nil + stage-packages: [dotnet-runtime-8.0_libs] + + from-deb: + plugin: nil + stage-packages: [hello] diff --git a/tests/spread/general/base-devel/task.yaml b/tests/spread/general/base-devel/task.yaml new file mode 100644 index 000000000..be86072f4 --- /dev/null +++ b/tests/spread/general/base-devel/task.yaml @@ -0,0 +1,12 @@ +summary: Build rocks using the "devel" build-base +environment: + BASE/base_2404: "ubuntu@24.04" + BASE/bare: "bare" +execute: | + # Make sure the yaml file has the "placeholder-base" string, and replace + # it with the correct base. + grep placeholder-base rockcraft.orig.yaml + sed "s/placeholder-base/$BASE/" rockcraft.orig.yaml > rockcraft.yaml + + # Build the rock & load it into docker + run_rockcraft pack diff --git a/tests/spread/general/plugin-python/base-2404/rockcraft.yaml b/tests/spread/general/plugin-python/base-2404/rockcraft.yaml new file mode 100644 index 000000000..4e4455cb2 --- /dev/null +++ b/tests/spread/general/plugin-python/base-2404/rockcraft.yaml @@ -0,0 +1,5 @@ +name: base-2404 +base: ubuntu@24.04 +build-base: devel + +# Remaining contents will come from "parts.yaml" diff --git a/tests/spread/general/plugin-python/task.yaml b/tests/spread/general/plugin-python/task.yaml index fca5194cc..3bf5d118d 100644 --- a/tests/spread/general/plugin-python/task.yaml +++ b/tests/spread/general/plugin-python/task.yaml @@ -2,6 +2,7 @@ summary: Python plugin tests environment: SCENARIO/base_2004: base-2004 SCENARIO/base_2204: base-2204 + SCENARIO/base_2404: base-2404 SCENARIO/bare_build_2004: bare-build-2004 SCENARIO/bare_build_2204: bare-build-2204 execute: | diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index 8bc99a7a8..2b29c6d92 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -31,7 +31,13 @@ from rockcraft.errors import ProjectLoadError from rockcraft.models import Project -from rockcraft.models.project import INVALID_NAME_MESSAGE, Platform, load_project +from rockcraft.models.project import ( + CURRENT_DEVEL_BASE, + DEVEL_BASE_WARNING, + INVALID_NAME_MESSAGE, + Platform, + load_project, +) from rockcraft.pebble import Service _ARCH_MAPPING = {"x86": "amd64", "x64": "amd64"} @@ -233,7 +239,8 @@ def test_project_base_invalid(yaml_loaded_data): load_project_yaml(yaml_loaded_data) assert str(err.value) == ( "Bad rockcraft.yaml content:\n" - "- unexpected value; permitted: 'bare', 'ubuntu@20.04', 'ubuntu@22.04' (in field 'base')" + "- unexpected value; permitted: 'bare', 'ubuntu@20.04', " + "'ubuntu@22.04', 'ubuntu@24.04' (in field 'base')" ) @@ -706,3 +713,36 @@ def test_project_get_build_plan(yaml_loaded_data, platforms, expected_build_info yaml_loaded_data["platforms"] = platforms project = Project.unmarshal(yaml_loaded_data) assert project.get_build_plan() == expected_build_infos + + +def test_project_devel_base(yaml_loaded_data): + yaml_loaded_data["base"] = CURRENT_DEVEL_BASE + yaml_loaded_data["build-base"] = "ubuntu@22.04" + + with pytest.raises(CraftValidationError) as err: + _ = Project.unmarshal(yaml_loaded_data) + + expected = ( + f'To use the unstable base "{CURRENT_DEVEL_BASE}", ' + f'"build-base" must be "devel".' + ) + assert str(err.value) == expected + + +def test_get_effective_devel_base(yaml_loaded_data): + yaml_loaded_data["base"] = CURRENT_DEVEL_BASE + yaml_loaded_data["build-base"] = "devel" + project = Project.unmarshal(yaml_loaded_data) + + base = project.effective_base + assert base.name == "ubuntu" + assert base.version == "devel" + + +def test_devel_base_warning(yaml_loaded_data, emitter): + yaml_loaded_data["base"] = CURRENT_DEVEL_BASE + yaml_loaded_data["build-base"] = "devel" + del yaml_loaded_data["entrypoint-service"] + _ = Project.unmarshal(yaml_loaded_data) + + emitter.assert_message(DEVEL_BASE_WARNING)