From cc5758e9ce840df617abf59d2b82275805661fc4 Mon Sep 17 00:00:00 2001 From: Imani Pelton Date: Tue, 4 Aug 2026 17:17:49 -0400 Subject: [PATCH 1/9] feat(metadata): deprecate non-SPDX license values --- snapcraft/models/project.py | 26 +++++++++++++++++++++++++- tests/unit/models/test_projects.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/snapcraft/models/project.py b/snapcraft/models/project.py index bc7ae5d5a8..18afa6ea80 100644 --- a/snapcraft/models/project.py +++ b/snapcraft/models/project.py @@ -32,6 +32,7 @@ VersionStr, ) from craft_application.models.constraints import ( + LicenseStr, SingleEntryDict, SingleEntryList, UniqueList, @@ -42,7 +43,14 @@ Grammar, ) from craft_platforms import DebianArchitecture -from pydantic import ConfigDict, PrivateAttr, StringConstraints, error_wrappers +from pydantic import ( + ConfigDict, + PrivateAttr, + StringConstraints, + TypeAdapter, + ValidationError, + error_wrappers, +) from pydantic.json_schema import ( SkipJsonSchema, # noqa: TC002 (typing-only-third-party-import) # pydantic needs to import types at runtime for validation ) @@ -312,6 +320,20 @@ def _get_partitions_from_components( return None +def _warn_deprecated_license(lic: str | None) -> str | None: + if lic is None: + return None + + try: + TypeAdapter(LicenseStr).validate_python(lic) + except ValidationError: + emit.warning( + "Non-SPDX licenses are deprecated. Use SPDX license strings or 'proprietary' instead." + ) + + return lic + + class Socket(models.CraftBaseModel): """Snapcraft app socket definition.""" @@ -1476,6 +1498,8 @@ class Project(models.Project): See :ref:`configure-package-information-reuse-information` for details. """ + license: Annotated[str | None, pydantic.BeforeValidator(_warn_deprecated_license)] + contact: UniqueList[str] | str | None = pydantic.Field( default=None, description="The snap author's contact links and email addresses.", diff --git a/tests/unit/models/test_projects.py b/tests/unit/models/test_projects.py index 00c38e6aad..7a66601252 100644 --- a/tests/unit/models/test_projects.py +++ b/tests/unit/models/test_projects.py @@ -19,11 +19,13 @@ from collections.abc import Callable from contextlib import nullcontext from typing import Any, cast +from unittest.mock import call import pydantic import pytest from craft_application.errors import CraftValidationError from craft_application.models import VersionStr +from craft_cli.pytest_plugin import RecordingEmitter from craft_platforms import DebianArchitecture import snapcraft.models @@ -982,6 +984,34 @@ def test_snapcraftctl_old_bases(self, key, base, project_yaml_data): Project.unmarshal(project_yaml_data(base=base, parts=parts_data)) + @pytest.mark.parametrize( + ("lic", "should_warn"), + [ + ("MIT", False), + ("proprietary", False), + (None, False), + ("DemonicContract", True), + ], + ) + def test_non_spdx_deprecation( + self, + lic: str, + should_warn: bool, + project_yaml_data: Callable[..., Any], + emitter: RecordingEmitter, + ) -> None: + proj = Project.unmarshal(project_yaml_data(license=lic)) + + assert should_warn == ( + call( + "warning", + "Non-SPDX licenses are deprecated. Use SPDX license strings or 'proprietary' instead.", + ) + in emitter.interactions + ) + # License should always remain unchanged + assert proj.license == lic + class TestHookValidation: """Validate hooks.""" From 4c139966f1d6e3505c289e309831fe88ffc0b0ee Mon Sep 17 00:00:00 2001 From: Imani Pelton Date: Wed, 5 Aug 2026 16:17:43 -0400 Subject: [PATCH 2/9] docs: add deprecation to release notes --- docs/release-notes/snapcraft-9-0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/release-notes/snapcraft-9-0.rst b/docs/release-notes/snapcraft-9-0.rst index 08ea8bf43a..e2e5a72705 100644 --- a/docs/release-notes/snapcraft-9-0.rst +++ b/docs/release-notes/snapcraft-9-0.rst @@ -147,6 +147,13 @@ files had to end in ``.7zip``. Additionally, 7zip files are now documented in the :ref:`source-type ` key in the project file reference. +Deprecation of non-SPDX licenses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, the ``license`` key could contain any value. Now, if any value is specified, +it must either be a valid `SPDX license expression `__ or +the literal value ``"proprietary"``. + Backwards-incompatible changes ------------------------------ From 2fcab049db62c177ea03dc3887d0a43d8797baf3 Mon Sep 17 00:00:00 2001 From: Imani Pelton Date: Wed, 5 Aug 2026 17:00:06 -0400 Subject: [PATCH 3/9] docs: release notes feedback --- docs/release-notes/snapcraft-9-0.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/snapcraft-9-0.rst b/docs/release-notes/snapcraft-9-0.rst index e2e5a72705..8dd2930710 100644 --- a/docs/release-notes/snapcraft-9-0.rst +++ b/docs/release-notes/snapcraft-9-0.rst @@ -150,9 +150,12 @@ key in the project file reference. Deprecation of non-SPDX licenses ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Previously, the ``license`` key could contain any value. Now, if any value is specified, -it must either be a valid `SPDX license expression `__ or -the literal value ``"proprietary"``. +Previously, the ``license`` key could contain any value. Snapcraft now warns when the +value is not a valid `SPDX license expression `__ or +``proprietary``. + +Non-SPDX values are still accepted for compatibility, but are deprecated and may be +rejected in a future release. Backwards-incompatible changes ------------------------------ From 275cb4d6c43ccbf2ef18306a5fa52a2ace11db40 Mon Sep 17 00:00:00 2001 From: Imani Pelton Date: Wed, 5 Aug 2026 17:00:26 -0400 Subject: [PATCH 4/9] chore: correct type hint --- tests/unit/models/test_projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/models/test_projects.py b/tests/unit/models/test_projects.py index 7a66601252..f2421dfce9 100644 --- a/tests/unit/models/test_projects.py +++ b/tests/unit/models/test_projects.py @@ -995,7 +995,7 @@ def test_snapcraftctl_old_bases(self, key, base, project_yaml_data): ) def test_non_spdx_deprecation( self, - lic: str, + lic: str | None, should_warn: bool, project_yaml_data: Callable[..., Any], emitter: RecordingEmitter, From 1ea7855c64706bad0ff15c0bbf0babde2e4aa9cc Mon Sep 17 00:00:00 2001 From: Imani Pelton Date: Wed, 5 Aug 2026 17:12:38 -0400 Subject: [PATCH 5/9] fix: don't override craft-application docs --- snapcraft/models/project.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/snapcraft/models/project.py b/snapcraft/models/project.py index 18afa6ea80..1e105ff5c5 100644 --- a/snapcraft/models/project.py +++ b/snapcraft/models/project.py @@ -320,20 +320,6 @@ def _get_partitions_from_components( return None -def _warn_deprecated_license(lic: str | None) -> str | None: - if lic is None: - return None - - try: - TypeAdapter(LicenseStr).validate_python(lic) - except ValidationError: - emit.warning( - "Non-SPDX licenses are deprecated. Use SPDX license strings or 'proprietary' instead." - ) - - return lic - - class Socket(models.CraftBaseModel): """Snapcraft app socket definition.""" @@ -1498,8 +1484,6 @@ class Project(models.Project): See :ref:`configure-package-information-reuse-information` for details. """ - license: Annotated[str | None, pydantic.BeforeValidator(_warn_deprecated_license)] - contact: UniqueList[str] | str | None = pydantic.Field( default=None, description="The snap author's contact links and email addresses.", @@ -2301,6 +2285,21 @@ def get_partitions(self) -> list[str] | None: """ return _get_partitions_from_components(self.components) + @pydantic.field_validator("license", mode="before") + @classmethod + def _warn_deprecated_license(cls, lic: str | None) -> str | None: + if lic is None: + return None + + try: + TypeAdapter(LicenseStr).validate_python(lic) + except ValidationError: + emit.warning( + "Non-SPDX licenses are deprecated. Use SPDX license strings or 'proprietary' instead." + ) + + return lic + def _custom_error(error_msg: str): def _validator(v: Any, next_: Any, ctx: pydantic.ValidationInfo): From 69918b24be5116de7de5fcf55ec5195fa5ff4786 Mon Sep 17 00:00:00 2001 From: Imani Pelton Date: Mon, 10 Aug 2026 13:51:38 -0400 Subject: [PATCH 6/9] docs: remove release notes entry --- docs/release-notes/snapcraft-9-0.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/release-notes/snapcraft-9-0.rst b/docs/release-notes/snapcraft-9-0.rst index 8dd2930710..08ea8bf43a 100644 --- a/docs/release-notes/snapcraft-9-0.rst +++ b/docs/release-notes/snapcraft-9-0.rst @@ -147,16 +147,6 @@ files had to end in ``.7zip``. Additionally, 7zip files are now documented in the :ref:`source-type ` key in the project file reference. -Deprecation of non-SPDX licenses -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Previously, the ``license`` key could contain any value. Snapcraft now warns when the -value is not a valid `SPDX license expression `__ or -``proprietary``. - -Non-SPDX values are still accepted for compatibility, but are deprecated and may be -rejected in a future release. - Backwards-incompatible changes ------------------------------ From ff4d37982976ba5f0922f4496899adef43a96e70 Mon Sep 17 00:00:00 2001 From: Imani Pelton Date: Mon, 10 Aug 2026 14:37:25 -0400 Subject: [PATCH 7/9] refactor: only warn once --- snapcraft/models/project.py | 18 ----------- snapcraft/services/project.py | 25 ++++++++++++++- tests/unit/models/test_projects.py | 30 ----------------- tests/unit/services/test_project.py | 50 +++++++++++++++++++++++++++-- 4 files changed, 72 insertions(+), 51 deletions(-) diff --git a/snapcraft/models/project.py b/snapcraft/models/project.py index 1e105ff5c5..95ac883082 100644 --- a/snapcraft/models/project.py +++ b/snapcraft/models/project.py @@ -32,7 +32,6 @@ VersionStr, ) from craft_application.models.constraints import ( - LicenseStr, SingleEntryDict, SingleEntryList, UniqueList, @@ -47,8 +46,6 @@ ConfigDict, PrivateAttr, StringConstraints, - TypeAdapter, - ValidationError, error_wrappers, ) from pydantic.json_schema import ( @@ -2285,21 +2282,6 @@ def get_partitions(self) -> list[str] | None: """ return _get_partitions_from_components(self.components) - @pydantic.field_validator("license", mode="before") - @classmethod - def _warn_deprecated_license(cls, lic: str | None) -> str | None: - if lic is None: - return None - - try: - TypeAdapter(LicenseStr).validate_python(lic) - except ValidationError: - emit.warning( - "Non-SPDX licenses are deprecated. Use SPDX license strings or 'proprietary' instead." - ) - - return lic - def _custom_error(error_msg: str): def _validator(v: Any, next_: Any, ctx: pydantic.ValidationInfo): diff --git a/snapcraft/services/project.py b/snapcraft/services/project.py index 7a8ecab13c..5fdc26844c 100644 --- a/snapcraft/services/project.py +++ b/snapcraft/services/project.py @@ -23,7 +23,9 @@ import craft_providers.bases from craft_application import ProjectService from craft_application.errors import CraftValidationError +from craft_application.models.constraints import LicenseStr from craft_application.util import is_managed_mode +from pydantic import TypeAdapter, ValidationError from typing_extensions import override from snapcraft.extensions import apply_extensions @@ -48,8 +50,9 @@ class Project(ProjectService): __project_file_path: pathlib.Path | None = None - # Used to only issue a warning once. + # Used to only issue key warnings once. _ua_service_warning: bool = False + _license_spdx_warning: bool = False @staticmethod @override @@ -65,10 +68,30 @@ def _app_preprocess_project( extract_parse_info(project) apply_root_packages(project) Project.validate_ua_services(project) + Project.validate_license_spdx(project) def get_parse_info(self) -> dict[str, list[str]]: return extract_parse_info(self.get_raw()) + @classmethod + def validate_license_spdx(cls, project: dict[str, Any]) -> None: + if cls._license_spdx_warning: + return None + cls._license_spdx_warning = True + + lic = project.get("license") + if lic is None: + return None + + try: + TypeAdapter(LicenseStr).validate_python(lic) + except ValidationError: + craft_cli.emit.warning( + "Non-SPDX licenses are deprecated. Use SPDX license strings or 'proprietary' instead. For more information, see https://spdx.org/licenses/." + ) + + return lic + @classmethod def validate_ua_services(cls, project: dict[str, Any]) -> None: """Warn if the 'ua-services' key is used for a base other than core22. diff --git a/tests/unit/models/test_projects.py b/tests/unit/models/test_projects.py index f2421dfce9..00c38e6aad 100644 --- a/tests/unit/models/test_projects.py +++ b/tests/unit/models/test_projects.py @@ -19,13 +19,11 @@ from collections.abc import Callable from contextlib import nullcontext from typing import Any, cast -from unittest.mock import call import pydantic import pytest from craft_application.errors import CraftValidationError from craft_application.models import VersionStr -from craft_cli.pytest_plugin import RecordingEmitter from craft_platforms import DebianArchitecture import snapcraft.models @@ -984,34 +982,6 @@ def test_snapcraftctl_old_bases(self, key, base, project_yaml_data): Project.unmarshal(project_yaml_data(base=base, parts=parts_data)) - @pytest.mark.parametrize( - ("lic", "should_warn"), - [ - ("MIT", False), - ("proprietary", False), - (None, False), - ("DemonicContract", True), - ], - ) - def test_non_spdx_deprecation( - self, - lic: str | None, - should_warn: bool, - project_yaml_data: Callable[..., Any], - emitter: RecordingEmitter, - ) -> None: - proj = Project.unmarshal(project_yaml_data(license=lic)) - - assert should_warn == ( - call( - "warning", - "Non-SPDX licenses are deprecated. Use SPDX license strings or 'proprietary' instead.", - ) - in emitter.interactions - ) - # License should always remain unchanged - assert proj.license == lic - class TestHookValidation: """Validate hooks.""" diff --git a/tests/unit/services/test_project.py b/tests/unit/services/test_project.py index a7976d177a..d3b28c3f20 100644 --- a/tests/unit/services/test_project.py +++ b/tests/unit/services/test_project.py @@ -19,10 +19,12 @@ import itertools import pathlib from typing import Any +from unittest.mock import call import pytest import pytest_mock from craft_application.errors import CraftValidationError +from craft_cli.pytest_plugin import RecordingEmitter from snapcraft import const from snapcraft.application import APP_METADATA @@ -30,11 +32,13 @@ @pytest.fixture(autouse=True) -def reset_ua_service_warning(): - """Reset the one-shot ua-service warning flag between tests.""" +def reset_warnings(): + """Reset the one-shot warning flags between tests.""" Project._ua_service_warning = False + Project._license_spdx_warning = False yield Project._ua_service_warning = False + Project._license_spdx_warning = False @pytest.mark.parametrize( @@ -158,3 +162,45 @@ def test_no_warning_for_core22(self, emitter): Project.validate_ua_services(project) emitter.assert_interactions(None) + + +class TestValidateLicense: + def test_non_spdx_deprecation_warns_once(self, emitter: RecordingEmitter) -> None: + project = {"license": "maybe"} + + Project.validate_license_spdx(project) + Project.validate_license_spdx(project) + + emitter.assert_warning( + "Non-SPDX licenses are deprecated. Use SPDX license strings or 'proprietary' instead. For more information, see https://spdx.org/licenses/." + ) + # assert it was only shown once + assert len(emitter.interactions) == 1 + + @pytest.mark.parametrize( + ("lic", "should_warn"), + [ + ("MIT", False), + ("proprietary", False), + (None, False), + ("DemonicContract", True), + ], + ) + def test_non_spdx_deprecation( + self, + lic: str | None, + should_warn: bool, + emitter: RecordingEmitter, + ) -> None: + project = {"license": lic} + Project.validate_license_spdx(project) + + assert should_warn == ( + call( + "warning", + "Non-SPDX licenses are deprecated. Use SPDX license strings or 'proprietary' instead. For more information, see https://spdx.org/licenses/.", + ) + in emitter.interactions + ) + # License should always remain unchanged + assert project.get("license") == lic From b12b001baf0a507a0e3ab1f1b9f36546f6a365cb Mon Sep 17 00:00:00 2001 From: Imani Pelton Date: Mon, 10 Aug 2026 14:38:45 -0400 Subject: [PATCH 8/9] chore: remove unnecessary returns --- snapcraft/services/project.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/snapcraft/services/project.py b/snapcraft/services/project.py index 5fdc26844c..79b5ff0359 100644 --- a/snapcraft/services/project.py +++ b/snapcraft/services/project.py @@ -76,12 +76,12 @@ def get_parse_info(self) -> dict[str, list[str]]: @classmethod def validate_license_spdx(cls, project: dict[str, Any]) -> None: if cls._license_spdx_warning: - return None + return cls._license_spdx_warning = True lic = project.get("license") if lic is None: - return None + return try: TypeAdapter(LicenseStr).validate_python(lic) @@ -90,8 +90,6 @@ def validate_license_spdx(cls, project: dict[str, Any]) -> None: "Non-SPDX licenses are deprecated. Use SPDX license strings or 'proprietary' instead. For more information, see https://spdx.org/licenses/." ) - return lic - @classmethod def validate_ua_services(cls, project: dict[str, Any]) -> None: """Warn if the 'ua-services' key is used for a base other than core22. From 0988e7d79109b697b83a7bc8ad8ee215b4744fb2 Mon Sep 17 00:00:00 2001 From: Imani Pelton Date: Mon, 10 Aug 2026 14:44:59 -0400 Subject: [PATCH 9/9] feat: don't warn in managed mode --- snapcraft/services/project.py | 3 +++ tests/unit/services/test_project.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/snapcraft/services/project.py b/snapcraft/services/project.py index 79b5ff0359..6b44647cd9 100644 --- a/snapcraft/services/project.py +++ b/snapcraft/services/project.py @@ -79,6 +79,9 @@ def validate_license_spdx(cls, project: dict[str, Any]) -> None: return cls._license_spdx_warning = True + if is_managed_mode(): + return + lic = project.get("license") if lic is None: return diff --git a/tests/unit/services/test_project.py b/tests/unit/services/test_project.py index d3b28c3f20..4435884836 100644 --- a/tests/unit/services/test_project.py +++ b/tests/unit/services/test_project.py @@ -177,6 +177,17 @@ def test_non_spdx_deprecation_warns_once(self, emitter: RecordingEmitter) -> Non # assert it was only shown once assert len(emitter.interactions) == 1 + def test_no_warning_in_managed_mode( + self, emitter: RecordingEmitter, mocker: pytest_mock.MockerFixture + ): + """Don't warn in managed-mode.""" + mocker.patch("snapcraft.services.project.is_managed_mode", return_value=True) + project = {"license": "maybe"} + + Project.validate_ua_services(project) + + emitter.assert_interactions(None) + @pytest.mark.parametrize( ("lic", "should_warn"), [