Skip to content

Commit

Permalink
feat: make build plans for snaps with platforms (#21)
Browse files Browse the repository at this point in the history
Adds a function to make build plans for snaps that use the platforms keyword.

Fixes #5
Requires #39
CRAFT-3008
  • Loading branch information
lengau authored Sep 9, 2024
1 parent 557aca9 commit 5278aa0
Show file tree
Hide file tree
Showing 9 changed files with 749 additions and 6 deletions.
10 changes: 8 additions & 2 deletions craft_platforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@
from ._architectures import DebianArchitecture
from ._buildinfo import BuildInfo
from . import charm, rock
from . import snap
from ._distro import BaseName, DistroBase, is_ubuntu_like
from ._errors import (
CraftError,
CraftPlatformsError,
AllOnlyBuildError,
AllSinglePlatformError,
NeedBuildBaseError,
InvalidBaseError,
InvalidPlatformNameError,
InvalidPlatformError,
NeedBuildBaseError,
RequiresBaseError,
)
from ._platforms import Platforms, get_platforms_build_plan

Expand All @@ -48,6 +51,7 @@
"BuildInfo",
"charm",
"rock",
"snap",
"get_platforms_build_plan",
"BaseName",
"DistroBase",
Expand All @@ -56,7 +60,9 @@
"CraftPlatformsError",
"AllOnlyBuildError",
"AllSinglePlatformError",
"NeedBuildBaseError",
"InvalidBaseError",
"InvalidPlatformNameError",
"InvalidPlatformError",
"RequiresBaseError",
"NeedBuildBaseError",
]
3 changes: 3 additions & 0 deletions craft_platforms/_distro.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ def from_linux_distribution(cls, distribution: distro.LinuxDistribution) -> Self
"""
return cls(distribution=distribution.id(), series=distribution.version())

def __str__(self) -> str:
return f"{self.distribution}@{self.series}"


def is_ubuntu_like(distribution: distro.LinuxDistribution | None = None) -> bool:
"""Determine whether the given distribution is Ubuntu or Ubuntu-like.
Expand Down
36 changes: 34 additions & 2 deletions craft_platforms/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import typing
from collections.abc import Collection, Iterable

# Workaround for Windows...
EX_DATAERR = getattr(os, "EX_DATAERR", 65)


@typing.runtime_checkable
class CraftError(typing.Protocol):
Expand Down Expand Up @@ -133,14 +136,14 @@ def __init__(
)


class NeedBuildBaseError(CraftPlatformsError):
class NeedBuildBaseError(CraftPlatformsError, ValueError):
"""Error when ``base`` requires a ``build_base``, but none is unspecified."""

def __init__(self, base: str) -> None:
super().__init__(
message=f"base '{base}' requires a 'build-base', but none is specified",
resolution="Specify a build-base.",
retcode=os.EX_DATAERR,
retcode=EX_DATAERR,
)


Expand Down Expand Up @@ -175,3 +178,32 @@ def __init__(
docs_url=docs_url,
doc_slug=doc_slug,
)


class InvalidBaseError(CraftPlatformsError, ValueError):
"""Error when a specified base name is invalid."""

def __init__(
self,
base: str,
*,
message: str | None = None,
resolution: str | None = None,
docs_url: str | None = None,
build_base: bool = False,
) -> None:
self.base = base
if resolution is None:
resolution = "Ensure the base matches the <distro>@<series> pattern and is a supported series."
if not message:
message = (
f"build-base '{base}' is unknown or invalid"
if build_base
else f"base '{base}' is unknown or invalid"
)

super().__init__(message=message, resolution=resolution, docs_url=docs_url)


class RequiresBaseError(CraftPlatformsError, ValueError):
"""Error when a base is required in this configuration."""
7 changes: 5 additions & 2 deletions craft_platforms/_platforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@


def get_platforms_build_plan(
base: str,
base: str | _distro.DistroBase,
platforms: Platforms,
build_base: str | None = None,
) -> Sequence[_buildinfo.BuildInfo]:
"""Generate the build plan for a platforms-based artefact."""
distro_base = _distro.DistroBase.from_str(build_base or base)
if isinstance(base, _distro.DistroBase):
distro_base = base
else:
distro_base = _distro.DistroBase.from_str(build_base or base)
build_plan: list[_buildinfo.BuildInfo] = []

for platform_name, platform in platforms.items():
Expand Down
22 changes: 22 additions & 0 deletions craft_platforms/snap/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# This file is part of craft-platforms.
#
# Copyright 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
# 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 warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Snap-specific build information for craft-platforms."""

from ._build import (
get_default_architectures,
get_distro_base_from_core_base,
get_platforms_snap_build_plan,
)
182 changes: 182 additions & 0 deletions craft_platforms/snap/_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# This file is part of craft-platforms.
#
# Copyright 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
# 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 warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Snapcraft-specific platforms information."""

import re
import typing
from collections.abc import Sequence

from craft_platforms import _architectures, _buildinfo, _distro, _errors, _platforms

CORE16_18_DEFAULT_ARCHITECTURES = (
_architectures.DebianArchitecture.AMD64,
_architectures.DebianArchitecture.ARM64,
_architectures.DebianArchitecture.ARMHF,
_architectures.DebianArchitecture.I386,
_architectures.DebianArchitecture.PPC64EL,
_architectures.DebianArchitecture.S390X,
)

CORE20_DEFAULT_ARCHITECTURES = (
_architectures.DebianArchitecture.AMD64,
_architectures.DebianArchitecture.ARM64,
_architectures.DebianArchitecture.ARMHF,
_architectures.DebianArchitecture.PPC64EL,
_architectures.DebianArchitecture.S390X,
)

DEFAULT_ARCHITECTURES_BY_BASE = {
"core": CORE16_18_DEFAULT_ARCHITECTURES,
"core16": CORE16_18_DEFAULT_ARCHITECTURES,
"core18": CORE16_18_DEFAULT_ARCHITECTURES,
"core20": CORE20_DEFAULT_ARCHITECTURES,
}

# The default architectures if not otherwise specified for a base.
DEFAULT_ARCHITECTURES = (
_architectures.DebianArchitecture.AMD64,
_architectures.DebianArchitecture.ARM64,
_architectures.DebianArchitecture.ARMHF,
_architectures.DebianArchitecture.PPC64EL,
_architectures.DebianArchitecture.RISCV64,
_architectures.DebianArchitecture.S390X,
)

CORE_BASE_REGEX = re.compile("^core(?P<version>16|18|[2-9][02468])?$")

SNAP_TYPES_WITHOUT_BASE = ("base", "kernel", "snapd")

BASE_SNAPS_DOC_URL = (
"https://canonical-snapcraft.readthedocs-hosted.com/en/stable/reference/bases/"
)


def get_default_architectures(base: str) -> Sequence[_architectures.DebianArchitecture]:
if base in DEFAULT_ARCHITECTURES_BY_BASE:
return DEFAULT_ARCHITECTURES_BY_BASE[base]
return DEFAULT_ARCHITECTURES


def get_distro_base_from_core_base(base: str) -> _distro.DistroBase:
"""Get a DistroBase from the existing snap."""
if base == "core":
return _distro.DistroBase("ubuntu", "16.04")
if match := CORE_BASE_REGEX.match(base):
version = match.group("version")
return _distro.DistroBase("ubuntu", f"{version}.04")
return _distro.DistroBase.from_str(base)


def get_snap_base(
*, base: str | None, build_base: str | None, snap_type: str | None
) -> _distro.DistroBase:
if not base:
if snap_type not in SNAP_TYPES_WITHOUT_BASE:
raise _errors.RequiresBaseError(
f"snaps of type {snap_type!r} require a 'base'",
resolution="Declare a 'base' in 'snapcraft.yaml'",
docs_url=BASE_SNAPS_DOC_URL,
)
if build_base == "devel" and snap_type != "base":
raise _errors.RequiresBaseError(
"non-base snaps require a 'base' if 'build-base' is 'devel",
resolution="Declare a 'base' in 'snapcraft.yaml'",
docs_url=BASE_SNAPS_DOC_URL,
)
if not build_base:
raise _errors.RequiresBaseError(
f"{snap_type!r} snaps require a 'build-base' if no 'base' is declared",
resolution="Declare a 'build-base' in 'snapcraft.yaml'",
docs_url=BASE_SNAPS_DOC_URL,
)
try:
return get_distro_base_from_core_base(build_base)
except ValueError:
raise _errors.InvalidBaseError(
build_base,
build_base=True,
resolution="Provide a valid 'build-base'",
docs_url=BASE_SNAPS_DOC_URL,
)
if CORE_BASE_REGEX.match(base):
if not build_base:
return get_distro_base_from_core_base(base)
if build_base == "devel":
return _distro.DistroBase("ubuntu", "devel")
if CORE_BASE_REGEX.match(build_base):
raise _errors.InvalidBaseError(
build_base,
build_base=True,
message="cannot specify a core 'build-base' alongside a 'base'",
docs_url=BASE_SNAPS_DOC_URL,
)
if snap_type != "kernel":
raise _errors.InvalidBaseError(
build_base,
build_base=True,
message="non-kernel snaps cannot use 'base: coreXY' and arbitrary build-bases",
docs_url=BASE_SNAPS_DOC_URL,
)
return _distro.DistroBase.from_str(build_base)
if not build_base:
raise _errors.InvalidBaseError(
base,
message="must declare a 'build-base' if 'base' does not match 'coreXY'",
resolution="Provide a 'build-base'.",
docs_url=BASE_SNAPS_DOC_URL,
)
try:
return get_distro_base_from_core_base(build_base)
except ValueError:
raise _errors.InvalidBaseError(
build_base,
build_base=True,
resolution="Ensure the build-base is supported.",
docs_url=BASE_SNAPS_DOC_URL,
)


@typing.overload
def get_platforms_snap_build_plan(
base: None,
*,
platforms: _platforms.Platforms | None,
build_base: str | None = None,
snap_type: typing.Literal["base", "kernel"],
) -> Sequence[_buildinfo.BuildInfo]: ...
@typing.overload
def get_platforms_snap_build_plan(
base: str,
*,
platforms: _platforms.Platforms | None,
build_base: str | None = None,
snap_type: str | None = None,
) -> Sequence[_buildinfo.BuildInfo]: ...
def get_platforms_snap_build_plan(
base: str | None,
*,
platforms: _platforms.Platforms | None,
build_base: str | None = None,
snap_type: str | None = None,
) -> Sequence[_buildinfo.BuildInfo]:
"""Generate the build plan for a platforms-based charm."""
distro_base = get_snap_base(base=base, build_base=build_base, snap_type=snap_type)
if not platforms:
platforms = {
arch: None
for arch in get_default_architectures(base or build_base or "default")
}
return _platforms.get_platforms_build_plan(distro_base, platforms)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ requires-python = ">=3.10"
dev = [
"build",
"coverage[toml]~=7.4",
"hypothesis>=6.108.4",
"pytest~=8.0",
"pytest-check~=2.3",
"pytest-cov~=5.0",
Expand Down
Empty file added tests/unit/snap/__init__.py
Empty file.
Loading

0 comments on commit 5278aa0

Please sign in to comment.