diff --git a/craft_application/services/project.py b/craft_application/services/project.py index 4aca1f2f7..44027b0a8 100644 --- a/craft_application/services/project.py +++ b/craft_application/services/project.py @@ -759,6 +759,9 @@ def check_base_is_supported(self, verb: str = "pack") -> None: def is_effective_base_eol(self) -> bool: """Determine whether the base on which to build is end-of-life.""" base = craft_platforms.DistroBase.from_str(self.get().effective_base) + if base.series == "devel": + # A devel series is never considered end-of-life. + return False return not self._is_supported_on(base=base, date=datetime.date.today()) def base_eol_soon_date(self) -> datetime.date | None: diff --git a/spread.yaml b/spread.yaml index ea98ccd87..9a90d80f8 100644 --- a/spread.yaml +++ b/spread.yaml @@ -157,6 +157,8 @@ backends: workers: 4 - ubuntu-25.10-64: workers: 4 + - ubuntu-26.04-64: + workers: 4 prepare: | # Ubuntu 20.04 requires a newer kernel, so we prepare it with an HWE kernel and # reboot. Note that we're only doing this here because for OpenStack we diff --git a/tests/spread/witchcraft/pack-devel-build-base/task.yaml b/tests/spread/witchcraft/pack-devel-build-base/task.yaml new file mode 100644 index 000000000..b996bb65d --- /dev/null +++ b/tests/spread/witchcraft/pack-devel-build-base/task.yaml @@ -0,0 +1,19 @@ +summary: test packing witchcraft with build-base devel and --ignore=unmaintained + +# Regression test for https://github.com/canonical/craft-application/issues/1152 +# +# ProjectService.is_effective_base_eol() did not handle a build-base of "devel" +# (or any base unrecognized by distro-support), crashing with an unhandled +# UnknownVersionError. This is only exercised when the --ignore=unmaintained +# flag is used, since that's the only path calling is_effective_base_eol(). + +execute: | + witchcraft pack --ignore=unmaintained --destructive-mode + + if [[ $(find . -maxdepth 1 -name '*.witchcraft') == "" ]]; then + echo "No witchcraft file created" >> /dev/stderr + exit 1 + fi + +restore: | + rm -f *.witchcraft diff --git a/tests/spread/witchcraft/pack-devel-build-base/witchcraft.yaml b/tests/spread/witchcraft/pack-devel-build-base/witchcraft.yaml new file mode 100644 index 000000000..6523a8a4a --- /dev/null +++ b/tests/spread/witchcraft/pack-devel-build-base/witchcraft.yaml @@ -0,0 +1,21 @@ +name: eol-devel-test +version: "0.1" +summary: Test packing with a devel build-base and --ignore=unmaintained. +description: | + Regression test for https://github.com/canonical/craft-application/issues/1152 + + This tests that witchcraft, which has check_supported_base enabled, can + pack a project with `base: bare` and `build-base: devel` when + `--ignore=unmaintained` is passed, without crashing with an + UnknownVersionError from distro-support. + +base: bare +build-base: ubuntu@devel +platforms: + platform-independent: + build-on: [amd64, arm64, ppc64el, s390x, riscv64] + build-for: [all] + +parts: + my-test: + plugin: nil diff --git a/tests/unit/services/test_project.py b/tests/unit/services/test_project.py index e82e16c98..5b7e6d664 100644 --- a/tests/unit/services/test_project.py +++ b/tests/unit/services/test_project.py @@ -32,6 +32,7 @@ from craft_application.services.project import ProjectService from craft_application.services.service_factory import ServiceFactory from craft_parts import ProjectVar, ProjectVarInfo +from distro_support.errors import UnknownDistributionError, UnknownVersionError from hypothesis import given, strategies @@ -1002,6 +1003,57 @@ def test_check_base_eol_soon_date( assert real_project_service.base_eol_soon_date() == expected_date +@freezegun.freeze_time("2027-01-01") +@pytest.mark.parametrize( + ("base", "build_base", "expected"), + [ + ("ubuntu@22.04", None, False), + pytest.param("ubuntu@16.04", None, True, id="eol-base"), + pytest.param("bare", "ubuntu@16.04", True, id="eol-build-base"), + pytest.param("ubuntu@22.04", "ubuntu@devel", False, id="devel-build-base"), + pytest.param("bare", "ubuntu@devel", False, id="devel-build-base-no-base"), + ], +) +@pytest.mark.usefixtures("fake_project_file") +def test_is_effective_base_eol( + real_project_service: ProjectService, + base: str, + build_base: str | None, + expected: bool, +): + real_project_service.configure(platform=None, build_for=None) + raw_project = real_project_service._load_raw_project() + if build_base: + raw_project["build-base"] = build_base + raw_project["base"] = base + + assert real_project_service.is_effective_base_eol() is expected + + +@freezegun.freeze_time("2027-01-01") +@pytest.mark.parametrize( + ("base", "build_base"), + [ + pytest.param("nonexistent@0.0", None, id="nonexistent-distribution"), + pytest.param("ubuntu@99.99", None, id="unknown-ubuntu-series"), + ], +) +@pytest.mark.usefixtures("fake_project_file") +def test_is_effective_base_eol_unknown_base_raises( + real_project_service: ProjectService, + base: str, + build_base: str | None, +): + real_project_service.configure(platform=None, build_for=None) + raw_project = real_project_service._load_raw_project() + if build_base: + raw_project["build-base"] = build_base + raw_project["base"] = base + + with pytest.raises((UnknownDistributionError, UnknownVersionError)): + real_project_service.is_effective_base_eol() + + def test_deep_update(fake_project_file, real_project_service: ProjectService): """Test the deep update of a project model.""" fake_project_file.write_text(