diff --git a/snapcraft/application.py b/snapcraft/application.py index 8b91b6ac3b..6e61ba77b6 100644 --- a/snapcraft/application.py +++ b/snapcraft/application.py @@ -22,7 +22,7 @@ import pathlib import signal import sys -from typing import Any, List +from typing import Any, List, Sequence import craft_application.commands as craft_app_commands import craft_application.models @@ -37,7 +37,7 @@ from snapcraft import cli, commands, errors, models, services from snapcraft.commands import unimplemented -from snapcraft.const import SUPPORTED_ARCHS +from snapcraft.const import SUPPORTED_ARCHS, SnapArch from snapcraft.extensions import apply_extensions from snapcraft.models import Platform from snapcraft.models.project import apply_root_packages @@ -51,37 +51,37 @@ class SnapcraftBuildPlanner(craft_application.models.BuildPlanner): base: str | None build_base: str | None = None name: str - platforms: dict[str, Any] + platforms: dict[str, Any] | None = None project_type: str | None = pydantic.Field(default=None, alias="type") @pydantic.validator("platforms") @classmethod def _validate_all_platforms(cls, platforms: dict[str, Any]) -> dict[str, Any]: - """Make sure all provided platforms are tangible and sane.""" + """Validate and convert platform data to a dict of Platforms.""" for platform_label in platforms: - platform: dict[str, Any] = ( + platform_data: dict[str, Any] = ( platforms[platform_label] if platforms[platform_label] else {} ) error_prefix = f"Error for platform entry '{platform_label}'" # Make sure the provided platform_set is valid try: - platform = Platform(**platform).dict() + platform = Platform(**platform_data) except CraftValidationError as err: - # pylint: disable=raise-missing-from - raise CraftValidationError(f"{error_prefix}: {str(err)}") + raise CraftValidationError(f"{error_prefix}: {str(err)}") from None # build_on and build_for are validated # let's also validate the platform label - build_on_one_of = ( - platform["build_on"] if platform["build_on"] else [platform_label] - ) + if platform.build_on: + build_on_one_of: Sequence[SnapArch | str] = platform.build_on + else: + build_on_one_of = [platform_label] # If the label maps to a valid architecture and # `build-for` is present, then both need to have the same value, # otherwise the project is invalid. - if platform["build_for"]: - build_target = platform["build_for"][0] + if platform.build_for: + build_target = platform.build_for[0] if platform_label in SUPPORTED_ARCHS and platform_label != build_target: raise CraftValidationError( str( @@ -90,24 +90,21 @@ def _validate_all_platforms(cls, platforms: dict[str, Any]) -> dict[str, Any]: f"both values must match. {platform_label} != {build_target}" ) ) - else: - build_target = platform_label - - # Both build and target architectures must be supported - if not any(b_o in SUPPORTED_ARCHS for b_o in build_on_one_of): + # if no build-for is present, then the platform label needs to be a valid architecture + elif platform_label not in SUPPORTED_ARCHS: raise CraftValidationError( str( - f"{error_prefix}: trying to build snap in one of " - f"{build_on_one_of}, but none of these build architectures is supported. " - f"Supported architectures: {SUPPORTED_ARCHS}" + f"{error_prefix}: platform entry label must correspond to a " + "valid architecture if 'build-for' is not provided." ) ) - if build_target not in SUPPORTED_ARCHS: + # Both build and target architectures must be supported + if not any(b_o in SUPPORTED_ARCHS for b_o in build_on_one_of): raise CraftValidationError( str( - f"{error_prefix}: trying to build snap for target " - f"architecture {build_target}, which is not supported. " + f"{error_prefix}: trying to build snap in one of " + f"{build_on_one_of}, but none of these build architectures are supported. " f"Supported architectures: {SUPPORTED_ARCHS}" ) ) @@ -132,9 +129,12 @@ def get_build_plan(self) -> List[BuildInfo]: base = bases.BaseName("ubuntu", effective_base) + if self.platforms is None: + raise CraftValidationError("Must define at least one platform.") + for platform_entry, platform in self.platforms.items(): - for build_for in platform.get("build_for") or [platform_entry]: - for build_on in platform.get("build_on") or [platform_entry]: + for build_for in platform.build_for or [platform_entry]: + for build_on in platform.build_on or [platform_entry]: build_infos.append( BuildInfo( platform=platform_entry, diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index 62d28e4ea0..31035667a8 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2022-2023 Canonical Ltd. +# Copyright 2022-2024 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3 as diff --git a/snapcraft/models/__init__.py b/snapcraft/models/__init__.py index ccc155652d..e2b8912206 100644 --- a/snapcraft/models/__init__.py +++ b/snapcraft/models/__init__.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2022-2023 Canonical Ltd. +# Copyright 2022-2024 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3 as diff --git a/snapcraft/models/project.py b/snapcraft/models/project.py index abb41fccba..5d7ac0f1fa 100644 --- a/snapcraft/models/project.py +++ b/snapcraft/models/project.py @@ -23,7 +23,6 @@ from typing import ( TYPE_CHECKING, Any, - Callable, Dict, List, Literal, @@ -445,24 +444,18 @@ def _validate_default_provider(cls, default_provider): return default_provider -class Platform(pydantic.BaseModel): +class Platform(models.CraftBaseModel): """Snapcraft project platform definition.""" build_on: list[SnapArch] | None = pydantic.Field(min_items=1, unique_items=True) - build_for: list[SnapArch] | None = pydantic.Field(min_items=1, unique_items=True) - - class Config: # pylint: disable=too-few-public-methods - """Pydantic model configuration.""" - - allow_population_by_field_name = True - alias_generator: Callable[[str], str] = lambda s: s.replace( # noqa: E731 - "_", "-" - ) + build_for: list[SnapArch] | None = pydantic.Field( + min_items=1, max_items=1, unique_items=True + ) - @pydantic.validator("build_for", pre=True) + @pydantic.validator("build_on", "build_for", pre=True) @classmethod def _vectorise_build_for(cls, val: str | list[str]) -> list[str]: - """Vectorise target architecture if needed.""" + """Vectorise target architectures if needed.""" if isinstance(val, str): val = [val] return val @@ -470,22 +463,11 @@ def _vectorise_build_for(cls, val: str | list[str]) -> list[str]: @pydantic.root_validator(skip_on_failure=True) @classmethod def _validate_platform_set(cls, values: Mapping[str, Any]) -> Mapping[str, Any]: - """Validate the build_on build_for combination.""" - build_for: list[str] = values["build_for"] if values.get("build_for") else [] - build_on: list[str] = values["build_on"] if values.get("build_on") else [] + """If build_for is provided, then build_on must also be. - # We can only build for 1 arch at the moment - if len(build_for) > 1: - raise CraftValidationError( - str( - f"Trying to build a rock for {build_for} " - "but multiple target architectures are not " - "currently supported. Please specify only 1 value." - ) - ) - - # If build_for is provided, then build_on must also be - if not build_on and build_for: + This aligns with the precedent set by the `architectures` keyword. + """ + if not values.get("build_on") and values.get("build_for"): raise CraftValidationError( "'build_for' expects 'build_on' to also be provided." ) @@ -522,7 +504,6 @@ class Project(models.Project): ] grade: Optional[Literal["stable", "devel"]] architectures: List[Union[str, Architecture]] | None = None - # TODO: set architectures to [get_host_architecture()] if base is core22 assumes: UniqueStrList = cast(UniqueStrList, []) package_repositories: Optional[List[Dict[str, Any]]] hooks: Optional[Dict[str, Hook]] @@ -669,7 +650,7 @@ def _validate_platforms_and_architectures(cls, values): core24 and newer bases: - cannot define architectures - - must define platforms + - can optionally define platforms """ base = get_effective_base( base=values.get("base"), @@ -678,26 +659,24 @@ def _validate_platforms_and_architectures(cls, values): name=values.get("name"), ) - if base == "core24": - if values.get("architectures"): - raise ValueError( - ( - "'architectures' keyword is not supported for base %r. " - "Use 'platforms' keyword instead.", - base, - ), - ) - if not values.get("platforms"): + if base == "core22": + if values.get("platforms"): raise ValueError( - ("The 'platforms' keyword must be defined for base %r. ", base) + f"'platforms' keyword is not supported for base {base!r}. " + "Use 'architectures' keyword instead." ) - else: + # set default value if not values.get("architectures"): values["architectures"] = [get_host_architecture()] - if values.get("platforms"): + else: + if values.get("architectures"): raise ValueError( - "'platforms' keyword is not supported with 'base=core22'" + f"'architectures' keyword is not supported for base {base!r}. " + "Use 'platforms' keyword instead." ) + # set default value + if not values.get("platforms"): + values["platforms"] = {get_host_architecture(): None} return values diff --git a/spread.yaml b/spread.yaml index 121845e06a..166b7f2daa 100644 --- a/spread.yaml +++ b/spread.yaml @@ -272,10 +272,6 @@ suites: systems: - ubuntu-22.04* - # TODO: - # - add `platforms` keyword to all core24 snapcraft.yaml files - # - add core24 platforms tests - # - remove core24 architecture tests tests/spread/core24/: summary: core24 specific tests systems: diff --git a/tests/spread/core24/grammar/snap/snapcraft.yaml b/tests/spread/core24/grammar/snap/snapcraft.yaml index 7d489d89bc..cc23e71fc2 100644 --- a/tests/spread/core24/grammar/snap/snapcraft.yaml +++ b/tests/spread/core24/grammar/snap/snapcraft.yaml @@ -3,14 +3,16 @@ version: "1.0" summary: test description: | Exercise snapcraft's advanced grammar keywords, `on`, and `to`. - This test leverages architecture keywords, `build-on` and `build-for`. + This test leverages the platform keywords, `build-on` and `build-for`. grade: devel confinement: strict base: core22 -architectures: - - build-on: amd64 +platforms: + platform1: + build-on: amd64 build-for: amd64 - - build-on: amd64 + platform2: + build-on: amd64 build-for: arm64 parts: diff --git a/tests/spread/core24/package-repositories/test-foreign-armhf/snapcraft.yaml b/tests/spread/core24/package-repositories/test-foreign-armhf/snapcraft.yaml index 21cd4dbe9c..e19ed458a4 100644 --- a/tests/spread/core24/package-repositories/test-foreign-armhf/snapcraft.yaml +++ b/tests/spread/core24/package-repositories/test-foreign-armhf/snapcraft.yaml @@ -6,8 +6,8 @@ grade: devel confinement: strict base: core24 build-base: devel -architectures: - - build-on: amd64 +platforms: + amd64: package-repositories: # The repo that contains libpython3.11-minimal:armhf diff --git a/tests/spread/core24/package-repositories/test-foreign-i386/snapcraft.yaml b/tests/spread/core24/package-repositories/test-foreign-i386/snapcraft.yaml index 4d1b23e4fc..43de7758e0 100644 --- a/tests/spread/core24/package-repositories/test-foreign-i386/snapcraft.yaml +++ b/tests/spread/core24/package-repositories/test-foreign-i386/snapcraft.yaml @@ -6,8 +6,8 @@ grade: devel confinement: strict base: core24 build-base: devel -architectures: - - build-on: amd64 +platforms: + amd64: package-repositories: # The repo that contains libpython3.11-minimal:i386 diff --git a/tests/spread/core24/architectures/snaps/arg-match/arguments.txt b/tests/spread/core24/platforms/snaps/arg-match/arguments.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/arg-match/arguments.txt rename to tests/spread/core24/platforms/snaps/arg-match/arguments.txt diff --git a/tests/spread/core24/architectures/snaps/arg-match/expected-snaps.txt b/tests/spread/core24/platforms/snaps/arg-match/expected-snaps.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/arg-match/expected-snaps.txt rename to tests/spread/core24/platforms/snaps/arg-match/expected-snaps.txt diff --git a/tests/spread/core24/architectures/snaps/arg-match/snap/snapcraft.yaml b/tests/spread/core24/platforms/snaps/arg-match/snap/snapcraft.yaml similarity index 78% rename from tests/spread/core24/architectures/snaps/arg-match/snap/snapcraft.yaml rename to tests/spread/core24/platforms/snaps/arg-match/snap/snapcraft.yaml index fe39cb03d7..8255bb1482 100644 --- a/tests/spread/core24/architectures/snaps/arg-match/snap/snapcraft.yaml +++ b/tests/spread/core24/platforms/snaps/arg-match/snap/snapcraft.yaml @@ -8,10 +8,12 @@ grade: devel confinement: strict base: core24 build-base: devel -architectures: - - build-on: amd64 +platforms: + platform1: + build-on: amd64 build-for: amd64 - - build-on: [amd64, arm64] + platform2: + build-on: [amd64, arm64] build-for: arm64 parts: diff --git a/tests/spread/core24/architectures/snaps/arg-no-match/arguments.txt b/tests/spread/core24/platforms/snaps/arg-no-match/arguments.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/arg-no-match/arguments.txt rename to tests/spread/core24/platforms/snaps/arg-no-match/arguments.txt diff --git a/tests/spread/core24/architectures/snaps/arg-no-match/expected-failure.txt b/tests/spread/core24/platforms/snaps/arg-no-match/expected-failure.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/arg-no-match/expected-failure.txt rename to tests/spread/core24/platforms/snaps/arg-no-match/expected-failure.txt diff --git a/tests/spread/core24/architectures/snaps/arg-no-match/expected-snaps.txt b/tests/spread/core24/platforms/snaps/arg-no-match/expected-snaps.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/arg-no-match/expected-snaps.txt rename to tests/spread/core24/platforms/snaps/arg-no-match/expected-snaps.txt diff --git a/tests/spread/core24/architectures/snaps/arg-no-match/snap/snapcraft.yaml b/tests/spread/core24/platforms/snaps/arg-no-match/snap/snapcraft.yaml similarity index 78% rename from tests/spread/core24/architectures/snaps/arg-no-match/snap/snapcraft.yaml rename to tests/spread/core24/platforms/snaps/arg-no-match/snap/snapcraft.yaml index c83aef8d53..f66ec11694 100644 --- a/tests/spread/core24/architectures/snaps/arg-no-match/snap/snapcraft.yaml +++ b/tests/spread/core24/platforms/snaps/arg-no-match/snap/snapcraft.yaml @@ -8,10 +8,12 @@ grade: devel confinement: strict base: core24 build-base: devel -architectures: - - build-on: amd64 +platforms: + platform1: + build-on: amd64 build-for: amd64 - - build-on: [amd64, arm64] + platform2: + build-on: [amd64, arm64] build-for: arm64 parts: diff --git a/tests/spread/core24/architectures/snaps/build-on-no-match/expected-failure.txt b/tests/spread/core24/platforms/snaps/build-on-no-match/expected-failure.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/build-on-no-match/expected-failure.txt rename to tests/spread/core24/platforms/snaps/build-on-no-match/expected-failure.txt diff --git a/tests/spread/core24/architectures/snaps/build-on-no-match/expected-snaps.txt b/tests/spread/core24/platforms/snaps/build-on-no-match/expected-snaps.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/build-on-no-match/expected-snaps.txt rename to tests/spread/core24/platforms/snaps/build-on-no-match/expected-snaps.txt diff --git a/tests/spread/core24/architectures/snaps/build-on-no-match/snap/snapcraft.yaml b/tests/spread/core24/platforms/snaps/build-on-no-match/snap/snapcraft.yaml similarity index 78% rename from tests/spread/core24/architectures/snaps/build-on-no-match/snap/snapcraft.yaml rename to tests/spread/core24/platforms/snaps/build-on-no-match/snap/snapcraft.yaml index ab0bcb19d3..6fc3a674cc 100644 --- a/tests/spread/core24/architectures/snaps/build-on-no-match/snap/snapcraft.yaml +++ b/tests/spread/core24/platforms/snaps/build-on-no-match/snap/snapcraft.yaml @@ -8,10 +8,12 @@ grade: devel confinement: strict base: core24 build-base: devel -architectures: - - build-on: arm64 +platforms: + platform1: + build-on: arm64 build-for: arm64 - - build-on: armhf + platform2: + build-on: armhf build-for: armhf parts: diff --git a/tests/spread/core24/architectures/snaps/env-var-match/environmental-variables.txt b/tests/spread/core24/platforms/snaps/env-var-match/environmental-variables.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/env-var-match/environmental-variables.txt rename to tests/spread/core24/platforms/snaps/env-var-match/environmental-variables.txt diff --git a/tests/spread/core24/architectures/snaps/env-var-match/expected-snaps.txt b/tests/spread/core24/platforms/snaps/env-var-match/expected-snaps.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/env-var-match/expected-snaps.txt rename to tests/spread/core24/platforms/snaps/env-var-match/expected-snaps.txt diff --git a/tests/spread/core24/architectures/snaps/env-var-match/snap/snapcraft.yaml b/tests/spread/core24/platforms/snaps/env-var-match/snap/snapcraft.yaml similarity index 78% rename from tests/spread/core24/architectures/snaps/env-var-match/snap/snapcraft.yaml rename to tests/spread/core24/platforms/snaps/env-var-match/snap/snapcraft.yaml index a4f703a275..ff493e4d47 100644 --- a/tests/spread/core24/architectures/snaps/env-var-match/snap/snapcraft.yaml +++ b/tests/spread/core24/platforms/snaps/env-var-match/snap/snapcraft.yaml @@ -8,10 +8,12 @@ grade: devel confinement: strict base: core24 build-base: devel -architectures: - - build-on: amd64 +platforms: + platform1: + build-on: amd64 build-for: amd64 - - build-on: [amd64, arm64] + platform2: + build-on: [amd64, arm64] build-for: arm64 parts: diff --git a/tests/spread/core24/architectures/snaps/env-var-no-match/environmental-variables.txt b/tests/spread/core24/platforms/snaps/env-var-no-match/environmental-variables.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/env-var-no-match/environmental-variables.txt rename to tests/spread/core24/platforms/snaps/env-var-no-match/environmental-variables.txt diff --git a/tests/spread/core24/architectures/snaps/env-var-no-match/expected-failure.txt b/tests/spread/core24/platforms/snaps/env-var-no-match/expected-failure.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/env-var-no-match/expected-failure.txt rename to tests/spread/core24/platforms/snaps/env-var-no-match/expected-failure.txt diff --git a/tests/spread/core24/architectures/snaps/env-var-no-match/expected-snaps.txt b/tests/spread/core24/platforms/snaps/env-var-no-match/expected-snaps.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/env-var-no-match/expected-snaps.txt rename to tests/spread/core24/platforms/snaps/env-var-no-match/expected-snaps.txt diff --git a/tests/spread/core24/architectures/snaps/env-var-no-match/snap/snapcraft.yaml b/tests/spread/core24/platforms/snaps/env-var-no-match/snap/snapcraft.yaml similarity index 78% rename from tests/spread/core24/architectures/snaps/env-var-no-match/snap/snapcraft.yaml rename to tests/spread/core24/platforms/snaps/env-var-no-match/snap/snapcraft.yaml index d94a202809..e95d04c840 100644 --- a/tests/spread/core24/architectures/snaps/env-var-no-match/snap/snapcraft.yaml +++ b/tests/spread/core24/platforms/snaps/env-var-no-match/snap/snapcraft.yaml @@ -8,10 +8,12 @@ grade: devel confinement: strict base: core24 build-base: devel -architectures: - - build-on: amd64 +platforms: + platform1: + build-on: amd64 build-for: amd64 - - build-on: [amd64, arm64] + platform2: + build-on: [amd64, arm64] build-for: arm64 parts: diff --git a/tests/spread/core24/architectures/snaps/multiple-build-for/expected-failure.txt b/tests/spread/core24/platforms/snaps/multiple-build-for/expected-failure.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/multiple-build-for/expected-failure.txt rename to tests/spread/core24/platforms/snaps/multiple-build-for/expected-failure.txt diff --git a/tests/spread/core24/architectures/snaps/multiple-build-for/expected-snaps.txt b/tests/spread/core24/platforms/snaps/multiple-build-for/expected-snaps.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/multiple-build-for/expected-snaps.txt rename to tests/spread/core24/platforms/snaps/multiple-build-for/expected-snaps.txt diff --git a/tests/spread/core24/architectures/snaps/multiple-build-for/snap/snapcraft.yaml b/tests/spread/core24/platforms/snaps/multiple-build-for/snap/snapcraft.yaml similarity index 64% rename from tests/spread/core24/architectures/snaps/multiple-build-for/snap/snapcraft.yaml rename to tests/spread/core24/platforms/snaps/multiple-build-for/snap/snapcraft.yaml index 948701770b..ba1a31906c 100644 --- a/tests/spread/core24/architectures/snaps/multiple-build-for/snap/snapcraft.yaml +++ b/tests/spread/core24/platforms/snaps/multiple-build-for/snap/snapcraft.yaml @@ -7,11 +7,13 @@ grade: devel confinement: strict base: core24 build-base: devel -architectures: - - build-on: amd64 - build-for: amd64 - - build-on: [amd64, arm64] - build-for: arm64 +platforms: + platform1: + build-on: [amd64] + build-for: [amd64] + platform2: + build-on: [amd64, arm64] + build-for: [arm64] parts: nil: diff --git a/tests/spread/core24/architectures/snaps/single-arch/expected-snaps.txt b/tests/spread/core24/platforms/snaps/single-arch/expected-snaps.txt similarity index 100% rename from tests/spread/core24/architectures/snaps/single-arch/expected-snaps.txt rename to tests/spread/core24/platforms/snaps/single-arch/expected-snaps.txt diff --git a/tests/spread/core24/architectures/snaps/single-arch/snap/snapcraft.yaml b/tests/spread/core24/platforms/snaps/single-arch/snap/snapcraft.yaml similarity index 54% rename from tests/spread/core24/architectures/snaps/single-arch/snap/snapcraft.yaml rename to tests/spread/core24/platforms/snaps/single-arch/snap/snapcraft.yaml index f8e1c31e60..7e082a42ab 100644 --- a/tests/spread/core24/architectures/snaps/single-arch/snap/snapcraft.yaml +++ b/tests/spread/core24/platforms/snaps/single-arch/snap/snapcraft.yaml @@ -2,14 +2,15 @@ name: single-arch version: "1.0" summary: test description: | - A single architecture should create a single snap file. + A single platform should create a single snap file. grade: devel confinement: strict base: core24 build-base: devel -architectures: - - build-on: amd64 - build-for: amd64 +platforms: + platform1: + build-on: [amd64, arm64] + build-for: [amd64] parts: nil: diff --git a/tests/spread/core24/architectures/task.yaml b/tests/spread/core24/platforms/task.yaml similarity index 100% rename from tests/spread/core24/architectures/task.yaml rename to tests/spread/core24/platforms/task.yaml diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 7685bc7755..0e975bd56c 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -397,41 +397,18 @@ def default_project(extra_project_params): grade="devel", parts=parts, license="MIT", - platforms={"amd64": {"build-on": ["amd64"], "build-for": ["amd64"]}}, **extra_project_params, ) @pytest.fixture() -def default_project_core22(extra_project_params): - from craft_application.models import SummaryStr, VersionStr - - from snapcraft.models.project import Project - - parts = extra_project_params.pop("parts", {}) - - return Project( - name="default", - version=VersionStr("1.0"), - summary=SummaryStr("default project"), - description="default project", - base="core22", - build_base="devel", - grade="devel", - parts=parts, - license="MIT", - **extra_project_params, - ) - - -@pytest.fixture() -def default_factory(default_project_core22): +def default_factory(default_project): from snapcraft.application import APP_METADATA from snapcraft.services import SnapcraftServiceFactory factory = SnapcraftServiceFactory( app=APP_METADATA, - project=default_project_core22, + project=default_project, ) return factory diff --git a/tests/unit/models/test_projects.py b/tests/unit/models/test_projects.py index a585c7a210..09acefdcec 100644 --- a/tests/unit/models/test_projects.py +++ b/tests/unit/models/test_projects.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2022-2023 Canonical Ltd. +# Copyright 2022-2024 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3 as @@ -14,24 +14,30 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import itertools from typing import Any, Dict, cast import pydantic import pytest +from craft_application.errors import CraftValidationError from craft_application.models import UniqueStrList -from snapcraft import errors +from snapcraft import const, errors from snapcraft.models import ( MANDATORY_ADOPTABLE_FIELDS, Architecture, ContentPlug, GrammarAwareProject, Hook, + Platform, Project, ) from snapcraft.models.project import apply_root_packages from snapcraft.utils import get_host_architecture +# required project data for core24 snaps +CORE24_DATA = {"base": "core24", "build_base": "devel", "grade": "devel"} + @pytest.fixture def project_yaml_data(): @@ -579,14 +585,7 @@ def test_project_development_base_error(self, project_yaml_data): error = "build-base must be 'devel' when base is 'core24'" with pytest.raises(errors.ProjectValidationError, match=error): - Project.unmarshal( - project_yaml_data( - base="core24", - platforms={ - "amd64v2": {"build-on": ["amd64"], "build-for": ["amd64"]} - }, - ) - ) + Project.unmarshal(project_yaml_data(base="core24")) def test_project_global_plugs_warning(self, project_yaml_data, emitter): data = project_yaml_data(plugs={"desktop": None, "desktop-legacy": None}) @@ -669,6 +668,57 @@ def test_project_hooks_plugs_empty(self, project_yaml_data): Project.unmarshal(project_yaml_data(hooks=hook)) +class TestPlatforms: + """Validate platforms.""" + + VALID_PLATFORM_ARCHITECTURES = [ + # single architecture in a list + *(list(x) for x in itertools.combinations(const.SnapArch, 1)), + # two architectures in a list + *(list(x) for x in itertools.combinations(const.SnapArch, 2)), + ] + + @pytest.mark.parametrize("build_on", VALID_PLATFORM_ARCHITECTURES) + @pytest.mark.parametrize("build_for", [[arch] for arch in const.SnapArch]) + def test_platform_validation_lists(self, build_on, build_for, project_yaml_data): + """Unmarshal build-on and build-for lists.""" + platform_data = Platform(**{"build-on": build_on, "build-for": build_for}) + + assert platform_data.build_for == build_for + assert platform_data.build_on == build_on + + @pytest.mark.parametrize("build_on", const.SnapArch) + @pytest.mark.parametrize("build_for", const.SnapArch) + def test_platform_validation_strings(self, build_on, build_for, project_yaml_data): + """Unmarshal and vectorize build-on and build-for strings.""" + platform_data = Platform(**{"build-on": build_on, "build-for": build_for}) + + assert platform_data.build_for == [build_for] + assert platform_data.build_on == [build_on] + + def test_platform_build_for_requires_build_on(self, project_yaml_data): + """Raise an error if build-for is provided by build-on is not.""" + with pytest.raises(CraftValidationError) as raised: + Platform(**{"build-for": [const.SnapArch.amd64]}) + + assert "'build_for' expects 'build_on' to also be provided" in str(raised.value) + + def test_platform_default(self, project_yaml_data): + """Default value for platforms is the host architecture.""" + project = Project.unmarshal(project_yaml_data(**CORE24_DATA)) + + assert project.platforms == {get_host_architecture(): None} + + def test_platforms_not_allowed_core22(self, project_yaml_data): + with pytest.raises(errors.ProjectValidationError) as raised: + Project.unmarshal(project_yaml_data(platforms={"amd64": None})) + + assert ( + "'platforms' keyword is not supported for base 'core22'. " + "Use 'architectures' keyword instead." in str(raised.value) + ) + + class TestAppValidation: """Validate apps.""" @@ -1759,6 +1809,16 @@ def test_project_get_build_for_arch_triplet_all(self, project_yaml_data): assert not arch_triplet + def test_architectures_not_allowed(self, project_yaml_data): + """'architectures' keyword is not allowed if base is not core22.""" + with pytest.raises(errors.ProjectValidationError) as raised: + Project.unmarshal(project_yaml_data(**CORE24_DATA, architectures=["amd64"])) + + assert ( + "'architectures' keyword is not supported for base 'core24'. " + "Use 'platforms' keyword instead." + ) in str(raised.value) + class TestApplyRootPackages: """Test Transform the Project.""" diff --git a/tests/unit/test_application.py b/tests/unit/test_application.py index 99d4f72bdf..d3e813cf5b 100644 --- a/tests/unit/test_application.py +++ b/tests/unit/test_application.py @@ -19,10 +19,11 @@ from textwrap import dedent import pytest -from craft_providers import bases +from craft_application.errors import CraftValidationError +from craft_application.models import BuildInfo +from craft_providers.bases import BaseName from snapcraft import application, services -from snapcraft.const import SnapArch from snapcraft.models.project import Architecture @@ -38,25 +39,160 @@ def architectures(request): return request.param -def test_build_planner_success_base_only(): - data = { - "base": "core24", - "name": "test", - "platforms": {"amd64v2": {"build-on": ["amd64"], "build-for": ["amd64"]}}, - } +@pytest.mark.parametrize( + ("platforms", "expected_build_infos"), + [ + pytest.param( + {"amd64": None}, + [ + BuildInfo( + build_on="amd64", + build_for="amd64", + base=BaseName(name="ubuntu", version="24.04"), + platform="amd64", + ) + ], + id="single_platform_as_arch", + ), + pytest.param( + { + "arm64": { + "build-on": ["arm64", "armhf"], + "build-for": ["arm64"], + }, + }, + [ + BuildInfo( + build_on="arm64", + build_for="arm64", + base=BaseName(name="ubuntu", version="24.04"), + platform="arm64", + ), + BuildInfo( + build_on="armhf", + build_for="arm64", + base=BaseName(name="ubuntu", version="24.04"), + platform="arm64", + ), + ], + id="multiple_build_on", + ), + pytest.param( + { + "amd64v2": { + "build-on": ["amd64"], + "build-for": "amd64", + }, + }, + [ + BuildInfo( + build_on="amd64", + build_for="amd64", + base=BaseName(name="ubuntu", version="24.04"), + platform="amd64v2", + ) + ], + id="custom_platform_name", + ), + ], +) +def test_build_planner_get_build_plan(platforms, expected_build_infos): + """Test `get_build_plan()` function with different platforms.""" + planner = application.SnapcraftBuildPlanner.parse_obj( + {"name": "test-snap", "base": "core24", "platforms": platforms} + ) + + actual_build_infos = planner.get_build_plan() + + assert actual_build_infos == expected_build_infos + + +def test_build_planner_get_build_plan_base(mocker): + """Test `get_build_plan()` uses the correct base.""" + mock_get_effective_base = mocker.patch( + "snapcraft.application.get_effective_base", return_value="core24" + ) + planner = application.SnapcraftBuildPlanner.parse_obj( + { + "name": "test-snap", + "base": "test-base", + "build-base": "test-build-base", + "platforms": {"amd64": None}, + "project_type": "test-type", + } + ) + + actual_build_infos = planner.get_build_plan() + + assert actual_build_infos == [ + BuildInfo( + platform="amd64", + build_on="amd64", + build_for="amd64", + base=BaseName(name="ubuntu", version="24.04"), + ) + ] + mock_get_effective_base.assert_called_once_with( + base="test-base", + build_base="test-build-base", + project_type="test-type", + name="test-snap", + ) + + +def test_project_platform_error_has_context(): + """Platform validation errors include which platform entry is invalid.""" + with pytest.raises(CraftValidationError) as raised: + application.SnapcraftBuildPlanner.parse_obj( + { + "name": "test-snap", + "base": "test-base", + "build-base": "test-build-base", + "platforms": {"test-platform": {"build-for": ["amd64"]}}, + "project_type": "test-type", + } + ) - planner = application.SnapcraftBuildPlanner.parse_obj(data) + assert "'test-platform': 'build_for' expects 'build_on'" in str(raised.value) - actual = planner.get_build_plan() - for build_info in actual: - assert build_info.base == bases.BaseName("ubuntu", "24.04") - assert build_info.build_for == SnapArch.amd64 - assert build_info.build_on == SnapArch.amd64 - assert build_info.platform == "amd64v2" +def test_project_platform_mismatch(): + """Raise an error if platform name and build-for are valid but different archs.""" + with pytest.raises(CraftValidationError) as raised: + application.SnapcraftBuildPlanner.parse_obj( + { + "name": "test-snap", + "base": "test-base", + "build-base": "test-build-base", + "platforms": {"amd64": {"build-on": ["amd64"], "build-for": ["arm64"]}}, + "project_type": "test-type", + } + ) + + assert ( + "if 'build_for' is provided and the platform entry label " + "corresponds to a valid architecture, then both values must match. " + "amd64 != arm64" in str(raised.value) + ) -# TODO: add tests `get_build_plan()` +def test_project_platform_unknown_name(): + """Raise an error if an empty platform is not a valid architecture.""" + with pytest.raises(CraftValidationError) as raised: + application.SnapcraftBuildPlanner.parse_obj( + { + "name": "test-snap", + "base": "test-base", + "build-base": "test-build-base", + "platforms": {"unknown": None}, + "project_type": "test-type", + } + ) + + assert ( + "platform entry label must correspond to a valid architecture " + "if 'build-for' is not provided." in str(raised.value) + ) @pytest.mark.parametrize("env_vars", application.MAPPED_ENV_VARS.items()) @@ -110,11 +246,7 @@ def test_application_expand_extensions(emitter, monkeypatch, extension_source, n base: core24 build-base: devel platforms: - amd64: - build-on: - - amd64 - build-for: - - amd64 + amd64: null license: MIT parts: fake-extension/fake-part: