Skip to content

Commit

Permalink
feat!: validate devel bases (#302)
Browse files Browse the repository at this point in the history
Signed-off-by: Callahan Kovacs <[email protected]>
  • Loading branch information
mr-cal authored Apr 18, 2024
1 parent 6074752 commit aeae554
Show file tree
Hide file tree
Showing 9 changed files with 324 additions and 98 deletions.
2 changes: 1 addition & 1 deletion craft_application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class AppMetadata:
"""Metadata about a *craft application."""

name: str
ProjectClass: type[models.Project]
summary: str | None = None
version: str = field(init=False)
source_ignore_patterns: list[str] = field(default_factory=lambda: [])
Expand All @@ -78,7 +79,6 @@ class AppMetadata:
project_variables: list[str] = field(default_factory=lambda: ["version"])
mandatory_adoptable_fields: list[str] = field(default_factory=lambda: ["version"])

ProjectClass: type[models.Project] = models.Project
BuildPlannerClass: type[models.BuildPlanner] = field(
default=NotImplementedError # type: ignore[assignment,type-abstract]
)
Expand Down
12 changes: 10 additions & 2 deletions craft_application/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is part of craft_application.
#
# Copyright 2023 Canonical Ltd.
# Copyright 2023-2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License version 3, as
Expand All @@ -25,12 +25,20 @@
)
from craft_application.models.grammar import GrammarAwareProject
from craft_application.models.metadata import BaseMetadata
from craft_application.models.project import BuildInfo, BuildPlanner, Project
from craft_application.models.project import (
DEVEL_BASE_INFOS,
DEVEL_BASE_WARNING,
BuildInfo,
BuildPlanner,
Project,
)


__all__ = [
"BaseMetadata",
"BuildInfo",
"DEVEL_BASE_INFOS",
"DEVEL_BASE_WARNING",
"CraftBaseConfig",
"CraftBaseModel",
"GrammarAwareProject",
Expand Down
74 changes: 74 additions & 0 deletions craft_application/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
import craft_parts
import craft_providers.bases
import pydantic
from craft_cli import emit
from pydantic import AnyUrl
from typing_extensions import override

from craft_application import errors
from craft_application.models.base import CraftBaseConfig, CraftBaseModel
from craft_application.models.constraints import (
MESSAGE_INVALID_NAME,
Expand All @@ -39,6 +41,33 @@
)


@dataclasses.dataclass
class DevelBaseInfo:
"""Devel base information for an OS."""

current_devel_base: craft_providers.bases.BaseAlias
"""The base that the 'devel' alias currently points to."""

devel_base: craft_providers.bases.BaseAlias
"""The devel base."""


# A list of DevelBaseInfo objects that define an OS's current devel base and devel base.
DEVEL_BASE_INFOS = [
DevelBaseInfo(
# TODO: current_devel_base should point to 24.10, which is not available yet
current_devel_base=craft_providers.bases.ubuntu.BuilddBaseAlias.DEVEL,
devel_base=craft_providers.bases.ubuntu.BuilddBaseAlias.DEVEL,
),
]

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."
)


@dataclasses.dataclass
class BuildInfo:
"""Platform build information."""
Expand Down Expand Up @@ -115,6 +144,51 @@ def effective_base(self) -> Any: # noqa: ANN401 app specific classes can improv
return self.base
raise RuntimeError("Could not determine effective base")

@classmethod
@abc.abstractmethod
def _providers_base(cls, base: str) -> craft_providers.bases.BaseAlias | None:
"""Get a BaseAlias from the application's base.
:param base: The application-specific base name.
:returns: The BaseAlias for the base or None if the base does not map to
a BaseAlias.
:raises CraftValidationError: If the project's base cannot be determined.
"""

@pydantic.root_validator( # pyright: ignore[reportUnknownMemberType,reportUntypedFunctionDecorator]
pre=False
)
def _validate_devel(cls, values: dict[str, Any]) -> dict[str, Any]:
"""Validate the build-base is 'devel' for the current devel base."""
base = values.get("base")
# if there is no base, do not validate the build-base
if not base:
return values

base_alias = cls._providers_base(base)

# if the base does not map to a base alias, do not validate the build-base
if not base_alias:
return values

build_base = values.get("build_base") or base
build_base_alias = cls._providers_base(build_base)

# warn if a devel build-base is being used, error if a devel build-base is not
# used for a devel base
for devel_base_info in DEVEL_BASE_INFOS:
if base_alias == devel_base_info.current_devel_base:
if build_base_alias == devel_base_info.devel_base:
emit.message(DEVEL_BASE_WARNING)
else:
raise errors.CraftValidationError(
f"A development build-base must be used when base is {base!r}"
)

return values

@override
@classmethod
def transform_pydantic_error(cls, error: pydantic.ValidationError) -> None:
Expand Down
2 changes: 1 addition & 1 deletion craft_application/services/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def instance(
) -> Generator[craft_providers.Executor, None, None]:
"""Context manager for getting a provider instance.
:param base_name: A craft_providers capable base name (tuple of name, version)
:param build_info: Build information for the instance.
:param work_dir: Local path to mount inside the provider instance.
:param allow_unstable: Whether to allow the use of unstable images.
:returns: a context manager of the provider instance.
Expand Down
23 changes: 20 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from craft_application import application, models, services, util
from craft_cli import EmitterMode, emit
from craft_providers import bases
from typing_extensions import override

if TYPE_CHECKING: # pragma: no cover
from collections.abc import Iterator
Expand Down Expand Up @@ -78,29 +79,45 @@ def default_app_metadata() -> craft_application.AppMetadata:
m.setattr(metadata, "version", lambda _: "3.14159")
return craft_application.AppMetadata(
"testcraft",
FakeProject,
"A fake app for testing craft-application",
BuildPlannerClass=MyBuildPlanner,
source_ignore_patterns=["*.snap", "*.charm", "*.starcraft"],
)


@pytest.fixture()
def app_metadata(features) -> craft_application.AppMetadata:
def app_metadata(features, fake_project_class) -> craft_application.AppMetadata:
with pytest.MonkeyPatch.context() as m:
m.setattr(metadata, "version", lambda _: "3.14159")
return craft_application.AppMetadata(
"testcraft",
fake_project_class,
"A fake app for testing craft-application",
BuildPlannerClass=MyBuildPlanner,
source_ignore_patterns=["*.snap", "*.charm", "*.starcraft"],
features=craft_application.AppFeatures(**features),
)


class FakeProject(models.Project):
"""A fake project for testing."""

@override
@classmethod
def _providers_base(cls, base: str) -> bases.BaseAlias:
return bases.get_base_alias(("ubuntu", "24.04"))


@pytest.fixture()
def fake_project_class() -> type[models.Project]:
return FakeProject


@pytest.fixture()
def fake_project() -> models.Project:
def fake_project(fake_project_class) -> models.Project:
arch = util.get_host_architecture()
return models.Project(
return fake_project_class(
name="full-project", # pyright: ignore[reportArgumentType]
title="A fully-defined project", # pyright: ignore[reportArgumentType]
base="core24",
Expand Down
Loading

0 comments on commit aeae554

Please sign in to comment.