Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bases: improve errors for ESM bases #4274

Merged
merged 2 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 10 additions & 11 deletions snapcraft/commands/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
from craft_providers.util import snap_cmd
from overrides import overrides

from snapcraft import linters, projects, providers
from snapcraft.errors import LegacyFallback, SnapcraftError
from snapcraft import errors, linters, projects, providers
from snapcraft.meta import snap_yaml
from snapcraft.parts.lifecycle import apply_yaml, extract_parse_info, process_yaml
from snapcraft.utils import (
Expand Down Expand Up @@ -148,14 +147,14 @@ def _prepare_instance(
:param http_proxy: http proxy to add to environment
:param https_proxy: https proxy to add to environment

:raises SnapcraftError: If `snapcraft lint` fails inside the instance.
:raises errors.SnapcraftError: If `snapcraft lint` fails inside the instance.
"""
emit.progress("Checking build provider availability.")

provider = providers.get_provider()
if isinstance(provider, MultipassProvider):
# blocked by https://github.com/canonical/craft-providers/issues/169
raise SnapcraftError(
raise errors.SnapcraftError(
"'snapcraft lint' is not supported with Multipass as the build provider"
)
providers.ensure_provider_is_available(provider)
Expand Down Expand Up @@ -198,7 +197,7 @@ def _prepare_instance(
with emit.pause():
instance.execute_run(command, check=True)
except subprocess.CalledProcessError as error:
raise SnapcraftError(
raise errors.SnapcraftError(
f"failed to execute {shlex.join(command)!r} in instance",
) from error
finally:
Expand Down Expand Up @@ -231,7 +230,7 @@ def _unsquash_snap(self, snap_file: Path) -> Iterator[Path]:

:yields: Path to the snap's unsquashed directory.

:raises SnapcraftError: If the snap fails to unsquash.
:raises errors.SnapcraftError: If the snap fails to unsquash.
"""
snap_file = snap_file.resolve()

Expand All @@ -252,7 +251,7 @@ def _unsquash_snap(self, snap_file: Path) -> Iterator[Path]:
try:
subprocess.run(extract_command, capture_output=True, check=True)
except subprocess.CalledProcessError as error:
raise SnapcraftError(
raise errors.SnapcraftError(
f"could not unsquash snap file {snap_file.name!r}"
) from error

Expand All @@ -275,8 +274,8 @@ def _load_project(self, snapcraft_yaml_file: Path) -> Optional[projects.Project]
try:
# process_yaml will not parse core, core18, and core20 snaps
yaml_data = process_yaml(snapcraft_yaml_file)
except LegacyFallback as error:
raise SnapcraftError(
except (errors.LegacyFallback, errors.MaintenanceBase) as error:
raise errors.SnapcraftError(
"can not lint snap using a base older than core22"
) from error

Expand Down Expand Up @@ -305,7 +304,7 @@ def _install_snap(

:returns: Path to where snap was installed.

:raises SnapcraftError: If the snap cannot be installed.
:raises errors.SnapcraftError: If the snap cannot be installed.
"""
is_dangerous = not bool(assert_file)

Expand Down Expand Up @@ -338,7 +337,7 @@ def _install_snap(
try:
subprocess.run(install_command, capture_output=True, check=True)
except subprocess.CalledProcessError as error:
raise SnapcraftError(
raise errors.SnapcraftError(
f"could not install snap file {snap_file.name!r}"
) from error

Expand Down
23 changes: 23 additions & 0 deletions snapcraft/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

"""Snapcraft error definitions."""

from typing import Optional

from craft_cli import CraftError


Expand Down Expand Up @@ -117,6 +119,27 @@ class LegacyFallback(Exception):
"""Fall back to legacy snapcraft implementation."""


class MaintenanceBase(SnapcraftError):
"""Error for bases under ESM and no longer supported in this release."""

def __init__(self, base) -> None:
sergiusens marked this conversation as resolved.
Show resolved Hide resolved
channel: Optional[str] = None
if base == "core":
channel = "4.x"
elif base == "core18":
channel = "7.x"

resolution: Optional[str] = None
if channel:
resolution = f"Install from or refresh to the {channel!r} channel."

super().__init__(
f"{base!r} is not supported on this version of Snapcraft.",
resolution=resolution,
docs_url="https://snapcraft.io/docs/base-snaps",
)


class StoreCredentialsUnauthorizedError(SnapcraftError):
"""Error raised for 401 responses from the Snap Store."""

Expand Down
5 changes: 4 additions & 1 deletion snapcraft/parts/yaml_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@

from snapcraft import errors, utils

_LEGACY_BASES = {"core", "core18", "core20"}
_ESM_BASES = {"core", "core18"}
_LEGACY_BASES = {"core20"}


def _check_duplicate_keys(node):
Expand Down Expand Up @@ -90,6 +91,8 @@ def load(filestream: TextIO) -> Dict[str, Any]:

if build_base is None:
raise errors.LegacyFallback("no base defined")
if build_base in _ESM_BASES:
raise errors.MaintenanceBase(build_base)
if build_base in _LEGACY_BASES:
raise errors.LegacyFallback(f"base is {build_base}")
except yaml.error.YAMLError as err:
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/parts/test_yaml_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,19 @@ def test_yaml_load_not_core22_base():
assert str(raised.value) == "base is core20"


def test_yaml_load_esm_base():
with pytest.raises(errors.MaintenanceBase):
yaml_utils.load(
io.StringIO(
dedent(
"""\
base: core
tigarmo marked this conversation as resolved.
Show resolved Hide resolved
"""
)
)
)


def test_yaml_load_no_base():
with pytest.raises(errors.LegacyFallback) as raised:
yaml_utils.load(
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/test_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2022 Canonical Ltd.
sergiusens marked this conversation as resolved.
Show resolved Hide resolved
#
# 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
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Snapcraft error tests."""

import pytest

from snapcraft import errors


@pytest.mark.parametrize("base,channel", [("core", "4.x"), ("core18", "7.x")])
def test_maintenance_base(base, channel):
message = f"{base!r} is not supported on this version of Snapcraft."
resolution = f"Install from or refresh to the {channel!r} channel."

error = errors.MaintenanceBase(base)

assert str(error) == message
assert error.resolution == resolution
assert error.docs_url == "https://snapcraft.io/docs/base-snaps"


def test_maintenance_base_fallback():
"""For when the base is not explicitly handled in the exception."""
error = errors.MaintenanceBase("unknown-base")

assert str(error) == "'unknown-base' is not supported on this version of Snapcraft."
assert error.resolution is None
assert error.docs_url == "https://snapcraft.io/docs/base-snaps"