From fab2ff7839aba397d97271a67e58052e40d97823 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Mon, 10 Aug 2026 21:32:14 +0000 Subject: [PATCH 1/3] fix(project): handle devel/unknown bases in is_effective_base_eol() is_effective_base_eol() did not handle a devel series or a base unrecognized by distro-support, causing an unhandled UnknownVersionError crash (e.g. build-base: devel with --ignore=unmaintained). Give it the same treatment as check_base_is_supported()/base_eol_soon_date(): treat devel as never EOL, and assume unknown bases are supported. Fixes #1152 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- craft_application/services/project.py | 9 ++++++++- tests/unit/services/test_project.py | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/craft_application/services/project.py b/craft_application/services/project.py index 4aca1f2f7..2f5ce75e5 100644 --- a/craft_application/services/project.py +++ b/craft_application/services/project.py @@ -759,7 +759,14 @@ 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) - return not self._is_supported_on(base=base, date=datetime.date.today()) + if base.series == "devel": + return False + try: + return not self._is_supported_on(base=base, date=datetime.date.today()) + except (UnknownDistributionError, UnknownVersionError) as error: + # If distro-support doesn't know about this base, assume it's supported. + emit.debug(str(error)) + return False def base_eol_soon_date(self) -> datetime.date | None: """Return the date of the base's EOL if it happens soon. diff --git a/tests/unit/services/test_project.py b/tests/unit/services/test_project.py index e82e16c98..38d68f7ab 100644 --- a/tests/unit/services/test_project.py +++ b/tests/unit/services/test_project.py @@ -1002,6 +1002,33 @@ 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("nonexistent@0.0", None, False, id="nonexistent-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, # noqa: FBT001 +): + 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 + + def test_deep_update(fake_project_file, real_project_service: ProjectService): """Test the deep update of a project model.""" fake_project_file.write_text( From 1da0cb12ad4b66c962d5e5c2143060c0e895d093 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Mon, 10 Aug 2026 21:41:03 +0000 Subject: [PATCH 2/3] fix(project): treat devel base as never EOL in is_effective_base_eol() Simplify is_effective_base_eol() to special-case a devel series the same way check_base_is_supported() does, rather than broadly catching UnknownDistributionError/UnknownVersionError for any unrecognized base. This more precisely fixes the crash on build-base: devel without silently treating other unrecognized bases as non-EOL. Also add a spread test that packs a project with build-base: devel and --ignore=unmaintained, and update/split the unit tests to cover the devel special-case versus genuinely unknown bases (which now correctly propagate the underlying distro-support error). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- craft_application/services/project.py | 8 ++--- .../pack-devel-build-base/task.yaml | 19 ++++++++++++ .../pack-devel-build-base/witchcraft.yaml | 21 ++++++++++++++ tests/unit/services/test_project.py | 29 +++++++++++++++++-- 4 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 tests/spread/witchcraft/pack-devel-build-base/task.yaml create mode 100644 tests/spread/witchcraft/pack-devel-build-base/witchcraft.yaml diff --git a/craft_application/services/project.py b/craft_application/services/project.py index 2f5ce75e5..44027b0a8 100644 --- a/craft_application/services/project.py +++ b/craft_application/services/project.py @@ -760,13 +760,9 @@ 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 - try: - return not self._is_supported_on(base=base, date=datetime.date.today()) - except (UnknownDistributionError, UnknownVersionError) as error: - # If distro-support doesn't know about this base, assume it's supported. - emit.debug(str(error)) - return False + return not self._is_supported_on(base=base, date=datetime.date.today()) def base_eol_soon_date(self) -> datetime.date | None: """Return the date of the base's EOL if it happens soon. 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 38d68f7ab..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 @@ -1010,7 +1011,7 @@ def test_check_base_eol_soon_date( 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("nonexistent@0.0", None, False, id="nonexistent-base"), + pytest.param("bare", "ubuntu@devel", False, id="devel-build-base-no-base"), ], ) @pytest.mark.usefixtures("fake_project_file") @@ -1018,7 +1019,7 @@ def test_is_effective_base_eol( real_project_service: ProjectService, base: str, build_base: str | None, - expected: bool, # noqa: FBT001 + expected: bool, ): real_project_service.configure(platform=None, build_for=None) raw_project = real_project_service._load_raw_project() @@ -1029,6 +1030,30 @@ def test_is_effective_base_eol( 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( From 0a2d54e1cc412d9e89fe9c371c9596142b6aacad Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Mon, 10 Aug 2026 21:55:20 +0000 Subject: [PATCH 3/3] test(spread): add ubuntu-26.04-64 to multipass backend Adds the newly-available Ubuntu 26.04 LTS multipass image as a spread test system, matching the existing openstack backend coverage. Verified locally by running the pack-devel-build-base regression test on multipass:ubuntu-26.04-64. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- spread.yaml | 2 ++ 1 file changed, 2 insertions(+) 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