Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ repos:
- id: fix-byte-order-marker
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.15.20"
rev: "v0.16.2"
hooks:
# Run the linter
- id: ruff
Expand Down
2 changes: 1 addition & 1 deletion craft_application/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ class Project(base.CraftBaseModel):

For “or later” and “with exception” license styles, refer to `Appendix V of the SPDX
Specification 2.1
<https://web.archive.org/web/20230902152422/https://spdx.dev/spdx-specification-21-web-version/#h.twlc0ztnng3b>`_.
<https://spdx.dev/wp-content/uploads/sites/31/2023/09/spdxversion2.1.pdf#h.twlc0ztnng3b>`__.
"""

adopt_info: str | None = pydantic.Field(
Expand Down
3 changes: 3 additions & 0 deletions craft_application/services/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 10 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@
"https://documentation.ubuntu.com/craft-cli/latest",
None,
),
"craft-archives": (
"https://documentation.ubuntu.com/craft-archives/latest",
None,
),
"craft-grammar": ("https://craft-grammar.readthedocs.io/en/latest", None),
"craft-parts": (
"https://documentation.ubuntu.com/craft-parts/latest",
Expand All @@ -245,7 +249,12 @@
None,
),
"craft-providers": (
"https://documentation.ubuntu.com/craft-providers/en/latest", None
"https://documentation.ubuntu.com/craft-providers/latest",
None,
),
"craft-store": (
"https://documentation.ubuntu.com/craft-store/latest",
None,
),
"starflow": (
"https://documentation.ubuntu.com/starflow/latest", None
Expand Down
17 changes: 8 additions & 9 deletions docs/explanation/cryptography.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,31 @@ Public key signing
~~~~~~~~~~~~~~~~~~

Craft Application supports the adding and verification of arbitrary package
repositories. For more information, see the cryptographic documentation for `Craft
Archives`_.
repositories. For more information, see the cryptographic documentation for
:external+craft-archives:ref:`Craft Archives <explanation-cryptographic-technology>`.

The parts system
~~~~~~~~~~~~~~~~

Craft Application makes use of *parts* in project files for declarative builds. Parts
specified by the user may download arbitrary files, install packages, and more. For more
information, see the cryptographic documentation for `Craft Parts`_.
information, see the cryptographic documentation for :external+craft-parts:ref:`Craft
Parts <explanation_cryptographic-technology>`.

Creating virtual build environments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Craft Application instantiates and executes builds on self-allocated virtual instances.
For more information, see `Craft Providers`_.
For more information, see the cryptographic documentation for
:external+craft-providers:ref:`Craft Providers <explanation_cryptographic-technology>`.

Interaction with storefronts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Craft Application is able to interface with supported storefronts over the internet. For
more information, see `Craft Store`_.
more information, see the cryptographic documentation for
:external+craft-store:ref:`Craft Store <explanation_cryptographic-technology>`.

.. _launchpadlib: https://help.launchpad.net/API/launchpadlib
.. _Launchpad: https://launchpad.net
.. _Requests: https://requests.readthedocs.io/
.. _Craft Archives: https://canonical-craft-archives.readthedocs-hosted.com/en/latest/explanation/cryptography/
.. _Craft Parts: https://canonical-craft-parts.readthedocs-hosted.com/en/latest/explanation/cryptography/
.. _Craft Providers: https://canonical-craft-providers.readthedocs-hosted.com/en/latest/explanation/cryptography/
.. _Craft Store: https://canonical-craft-store.readthedocs-hosted.com/en/latest/explanation/cryptography/
13 changes: 13 additions & 0 deletions docs/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ Changelog

For a complete list of commits, check out the `1.2.3`_ release on GitHub.

7.1.1 (2026-08-11)
------------------

Bug fixes
=========

- `#1152 <https://github.com/canonical/craft-application/issues/1152>`__
``is_effective_base_eol()`` crashes with ``UnknownVersionError`` when effective
base is ``devel``

For a complete list of commits, check out the `7.1.1`_ release on GitHub.

7.1.0 (2026-07-07)
------------------

Expand Down Expand Up @@ -1380,3 +1392,4 @@ For a complete list of commits, check out the `2.7.0`_ release on GitHub.
.. _7.0.0: https://github.com/canonical/craft-application/releases/tag/7.0.0
.. _7.0.1: https://github.com/canonical/craft-application/releases/tag/7.0.1
.. _7.1.0: https://github.com/canonical/craft-application/releases/tag/7.1.0
.. _7.1.1: https://github.com/canonical/craft-application/releases/tag/7.1.1
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ dev-questing = [
dev-resolute = [
"python-apt~=3.0.0;sys_platform=='linux'",
]
dev-stonking = [
"python-apt~=3.0.0;sys_platform=='linux'",
]
tics = ["flake8", "pylint"]

[tool.uv]
Expand Down Expand Up @@ -167,6 +170,7 @@ conflicts = [
{ group = "dev-plucky" },
{ group = "dev-questing" },
{ group = "dev-resolute" },
{ group = "dev-stonking" },
]
]

Expand Down Expand Up @@ -399,6 +403,7 @@ classmethod-decorators = ["pydantic.validator", "pydantic.root_validator"]
"S103", # Allow `os.chmod` setting a permissive mask `0o555` on file or directory
"S108", # Allow Probable insecure usage of temporary file or directory
"PLR0913", # Allow many arguments for test functions (useful if we need many fixtures)
"PLR0917", # Allow many positional arguments for test functions (same reason as PLR0913)
"PLR2004", # Allow magic values in tests
"SLF", # Allow accessing private members from tests.
"FBT001", # Boolean-typed parametrized tests can be useful
Expand Down
2 changes: 2 additions & 0 deletions spread.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ backends:
workers: 4
- ubuntu-25.10-64:
workers: 4
- ubuntu-26.04-64:
workers: 4
craft:
type: adhoc
allocate: "false"
Expand Down
19 changes: 19 additions & 0 deletions tests/spread/witchcraft/pack-devel-build-base/task.yaml
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions tests/spread/witchcraft/pack-devel-build-base/witchcraft.yaml
Original file line number Diff line number Diff line change
@@ -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
18 changes: 8 additions & 10 deletions tests/unit/models/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,10 @@

@pytest.fixture
def basic_project():
# ty doesn't understand pydantic semantics (https://github.com/astral-sh/ty/issues/2403)
return Project(
name="project-name",
version="1.0",
platforms={"arm64": None}, # ty: ignore[invalid-argument-type]
platforms={"arm64": None},
parts=PARTS_DICT,
Comment on lines 47 to 51
)

Expand Down Expand Up @@ -348,12 +347,11 @@ class FakeBuildBaseProject(Project):


def test_effective_base_is_build_base():
# ty doesn't understand pydantic semantics (https://github.com/astral-sh/ty/issues/2403)
project = FakeBuildBaseProject(
name="project-name",
version="1.0",
parts={},
platforms={"arm64": None}, # ty: ignore[invalid-argument-type]
platforms={"arm64": None},
base="ubuntu@22.04",
build_base="ubuntu@24.04",
)
Expand All @@ -366,7 +364,7 @@ def test_effective_base_unknown():
name="project-name",
version="1.0",
parts={},
platforms={"arm64": None}, # ty: ignore[invalid-argument-type]
platforms={"arm64": None},
base=None,
build_base=None,
)
Expand All @@ -384,7 +382,7 @@ def test_devel_base_devel_build_base(emitter, devel_info):
name="project-name",
version="1.0",
parts={},
platforms={"arm64": None}, # ty: ignore[invalid-argument-type]
platforms={"arm64": None},
base=f"ubuntu@{DEVEL_BASE_INFOS[0].current_devel_base.value}",
build_base=f"ubuntu@{DEVEL_BASE_INFOS[0].devel_base.value}",
)
Expand All @@ -402,7 +400,7 @@ def test_devel_base_wrong_build_base(devel_info):
name="project-name",
version="1.0",
parts={},
platforms={"arm64": None}, # ty: ignore[invalid-argument-type]
platforms={"arm64": None},
base=f"ubuntu@{DEVEL_BASE_INFOS[0].current_devel_base.value}",
build_base=f"ubuntu@{DEVEL_BASE_INFOS[0].current_devel_base.value}",
)
Expand All @@ -414,7 +412,7 @@ def test_devel_base_no_base():
name="project-name",
version="1.0",
parts={},
platforms={"arm64": None}, # ty: ignore[invalid-argument-type]
platforms={"arm64": None},
)


Expand All @@ -429,7 +427,7 @@ def test_devel_base_no_base_alias(mocker):
name="project-name",
version="1.0",
parts={},
platforms={"arm64": None}, # ty: ignore[invalid-argument-type]
platforms={"arm64": None},
)


Expand All @@ -442,7 +440,7 @@ def test_devel_base_no_build_base(devel_info):
name="project-name",
version="1.0",
parts={},
platforms={"arm64": None}, # ty: ignore[invalid-argument-type]
platforms={"arm64": None},
base=f"ubuntu@{DEVEL_BASE_INFOS[0].current_devel_base.value}",
build_base=None,
)
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/services/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading