Skip to content

Commit

Permalink
environment: error when build-for arch variable cannot be evaluated (#…
Browse files Browse the repository at this point in the history
…4332)

For core20 snaps, if the project variable `SNAPCRAFT_ARCH_BUILD_FOR` or
`SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR` cannot be determined and is used in
a snapcraft.yaml file, then an error is raised.

Signed-off-by: Callahan Kovacs <callahan.kovacs@canonical.com>
  • Loading branch information
mr-cal authored Aug 25, 2023
1 parent e1491d1 commit 48bd063
Show file tree
Hide file tree
Showing 20 changed files with 327 additions and 34 deletions.
10 changes: 3 additions & 7 deletions snapcraft_legacy/internal/pluginhandler/_part_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ def get_snapcraft_global_environment(
else:
content_dirs_envvar = ""

environment = {
return {
"SNAPCRAFT_ARCH_BUILD_FOR": project.arch_build_for,
"SNAPCRAFT_ARCH_BUILD_ON": project.arch_build_on,
"SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR": project.arch_triplet_build_for,
"SNAPCRAFT_ARCH_TRIPLET_BUILD_ON": project.arch_triplet_build_on,
"SNAPCRAFT_ARCH_TRIPLET": project.arch_triplet,
"SNAPCRAFT_EXTENSIONS_DIR": common.get_extensionsdir(),
Expand All @@ -86,12 +88,6 @@ def get_snapcraft_global_environment(
"SNAPCRAFT_TARGET_ARCH": project.target_arch,
"SNAPCRAFT_CONTENT_DIRS": content_dirs_envvar,
}
# build-for arch is not defined for multi-arch builds or for unknown archs
if project.arch_build_for:
environment["SNAPCRAFT_ARCH_BUILD_FOR"] = project.arch_build_for
if project.arch_triplet_build_for:
environment["SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR"] = project.arch_triplet_build_for
return environment


def get_snapcraft_part_directory_environment(
Expand Down
51 changes: 48 additions & 3 deletions snapcraft_legacy/internal/project_loader/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2017 Canonical Ltd
# Copyright (C) 2017, 2023 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
Expand All @@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from typing import TYPE_CHECKING, Dict, List, Union, cast
from typing import TYPE_CHECKING, Dict, List, Optional, Union, cast

from ._env import environment_to_replacements, runtime_env # noqa: F401
from ._extensions import ( # noqa: F401
Expand All @@ -23,6 +23,7 @@
supported_extension_names,
)
from ._parts_config import PartsConfig # noqa: F401
from .errors import VariableEvaluationError

if TYPE_CHECKING:
from snapcraft_legacy.project import Project # noqa: F401
Expand All @@ -35,10 +36,18 @@ def load_config(project: "Project"):


def replace_attr(
attr: Union[List[str], Dict[str, str], str], replacements: Dict[str, str]
attr: Union[List[str], Dict[str, str], str], replacements: Dict[str, Optional[str]]
) -> Union[List[str], Dict[str, str], str]:
"""Recurse through a complex data structure and replace values.
:param attr: The data to modify, which may contain nested lists, dicts, and strings.
:param replacements: A mapping of replacements to make.
:returns: The data structure with replaced values.
"""
if isinstance(attr, str):
for replacement, value in replacements.items():
_validate_replacement(attr, replacement, value)
attr = attr.replace(replacement, str(value))
return attr
elif isinstance(attr, list) or isinstance(attr, tuple):
Expand All @@ -53,3 +62,39 @@ def replace_attr(
return result

return attr


def _validate_replacement(attr: str, variable: str, value: Optional[str]) -> None:
"""Validate if a replacement can occur for an attribute.
Some replacement data cannot be used if it is not defined, such as the build-for
arch and build-for arch triplet. If the value of these replacements is None, then
a validation error is raised.
:param attr: String that may contain data to replace.
:param variable: Project variable to replace.
:param value: New value for replacement.
:raises Exception: If a replacement cannot occur.
"""
# return early when there is a replacement value (this is the most common case)
if value is not None:
return

variables_to_validate = [
"SNAPCRAFT_ARCH_BUILD_FOR",
"SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR",
]

# expand to shell syntax for variables (`$item` and `${item}`)
expanded_variables_to_validate = (
*(f"${item}" for item in variables_to_validate ),
*(f"${{{item}}}" for item in variables_to_validate),
)

if variable in attr and variable in expanded_variables_to_validate:
raise VariableEvaluationError(
variable=variable,
reason="the build-for architecture could not be determined",
docs_url="https://snapcraft.io/docs/architectures",
)
13 changes: 12 additions & 1 deletion snapcraft_legacy/internal/project_loader/errors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2017-2018 Canonical Ltd
# Copyright (C) 2017-2018, 2023 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
Expand All @@ -24,6 +24,17 @@ class ProjectLoaderError(snapcraft_legacy.internal.errors.SnapcraftError):
fmt = ""


class VariableEvaluationError(ProjectLoaderError):

fmt = (
"Cannot evaluate project variable {variable!r}: {reason}\n"
"For more information, check out: {docs_url}"
)

def __init__(self, variable: str, reason: str, docs_url: str) -> None:
super().__init__(variable=variable, reason=reason, docs_url=docs_url)


class InvalidEpochError(ProjectLoaderError):

fmt = "epochs are positive integers followed by an optional asterisk"
Expand Down
12 changes: 6 additions & 6 deletions tests/legacy/unit/pluginhandler/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def test_implicit_build_for_arch(tmp_path):


def test_no_build_for_unknown_arch(tmp_path):
"""build-for envvars should not be defined for unknown build-for architectures."""
"""build-for envvars should be `None` for unknown build-for architectures."""
snapcraft_yaml = SnapcraftYaml(
tmp_path,
base="core20",
Expand All @@ -95,12 +95,12 @@ def test_no_build_for_unknown_arch(tmp_path):
assert (
environment["SNAPCRAFT_ARCH_TRIPLET_BUILD_ON"] == project.arch_triplet_build_on
)
assert "SNAPCRAFT_ARCH_BUILD_FOR" not in environment
assert "SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR" not in environment
assert environment["SNAPCRAFT_ARCH_BUILD_FOR"] is None
assert environment["SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR"] is None


def test_no_build_for_multi_arch(tmp_path):
"""build-for envvars should not be defined for multi-arch builds."""
"""build-for envvars should be `None` for multi-arch builds."""
snapcraft_yaml = SnapcraftYaml(
tmp_path,
base="core20",
Expand All @@ -116,5 +116,5 @@ def test_no_build_for_multi_arch(tmp_path):
assert (
environment["SNAPCRAFT_ARCH_TRIPLET_BUILD_ON"] == project.arch_triplet_build_on
)
assert "SNAPCRAFT_ARCH_BUILD_FOR" not in environment
assert "SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR" not in environment
assert environment["SNAPCRAFT_ARCH_BUILD_FOR"] is None
assert environment["SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR"] is None
13 changes: 12 additions & 1 deletion tests/legacy/unit/project_loader/test_errors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2020 Canonical Ltd
# Copyright (C) 2020, 2023 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
Expand All @@ -27,3 +27,14 @@ def test_SnapcraftProjectUnusedKeyAssetError():
assert error.get_brief() == "Found unused key asset 'foo'."
assert error.get_details() == "All configured key assets must be utilized."
assert error.get_resolution() == "Verify key usage and remove all unused keys."


def test_variable_evaluation_error():
error = errors.VariableEvaluationError(
variable="TEST_VARIABLE", reason="reason", docs_url="www.example.com"
)

assert str(error) == (
"Cannot evaluate project variable 'TEST_VARIABLE': reason\n"
"For more information, check out: www.example.com"
)
71 changes: 70 additions & 1 deletion tests/legacy/unit/project_loader/test_replace_attr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2015-2018 Canonical Ltd
# Copyright (C) 2015-2018, 2023 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
Expand All @@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import pytest
from testtools.matchers import Equals

from snapcraft_legacy.internal import project_loader
Expand Down Expand Up @@ -200,3 +201,71 @@ def test_string_replacement_with_complex_data(self):
),
Equals(expected),
)


def test_validate_replacements():
"""Verify replacements are successfully validated for SNAPCRAFT_ARCH_BUILD_FOR and
SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR."""
input_attr = {
"stage": [
"test/file",
"$EMPTY_VAR",
"$SNAPCRAFT_PROJECT_NAME",
"test/SNAPCRAFT_ARCH_BUILD_FOR/file",
"test/$SNAPCRAFT_ARCH_BUILD_FOR/file",
"test/${SNAPCRAFT_ARCH_BUILD_FOR}/file",
"test/SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR/file",
"test/$SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR/file",
"test/${SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR}/file",
],
}
expected_attr = {
"stage": [
"test/file",
"None",
"project_name",
"test/SNAPCRAFT_ARCH_BUILD_FOR/file",
"test/arm64/file",
"test/arm64/file",
"test/SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR/file",
"test/aarch64-linux-gnu/file",
"test/aarch64-linux-gnu/file",
]
}
replacements = project_loader.environment_to_replacements(
{
"SNAPCRAFT_ARCH_BUILD_FOR": "arm64",
"SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR": "aarch64-linux-gnu",
"SNAPCRAFT_PROJECT_NAME": "project_name",
# this variable will evaluate to 'None' because it is not validated
"EMPTY_VAR": None,
}
)

actual_attr = project_loader.replace_attr(input_attr, replacements)

assert actual_attr == expected_attr


@pytest.mark.parametrize(
"data",
[
"test/$SNAPCRAFT_ARCH_BUILD_FOR/file",
"test/${SNAPCRAFT_ARCH_BUILD_FOR}/file",
"test/$SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR/file",
"test/${SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR}/file",
],
)
def test_validate_replacements_error(data):
"""Raise an error when a replacement fails validation for SNAPCRAFT_ARCH_BUILD_FOR
and SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR."""
input_attr = {"stage": [data]}
replacements = project_loader.environment_to_replacements(
{
"SNAPCRAFT_ARCH_BUILD_FOR": None,
"SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR": None,
}
)

with pytest.raises(project_loader.errors.VariableEvaluationError):
project_loader.replace_attr(input_attr, replacements)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: build-for-multi-arch-error
base: core20
version: '1.0'
summary: test
description: Using SNAPCRAFT_ARCH_BUILD_FOR should raise an error for multi-arch builds.
grade: stable
confinement: strict

architectures:
- build-on: amd64
run-on: [amd64, armhf]

environment:
SNAPCRAFT_ARCH_BUILD_FOR: ${SNAPCRAFT_ARCH_BUILD_FOR}

parts:
my-part:
plugin: nil
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: build-for-multi-arch-triplet-error
base: core20
version: '1.0'
summary: test
description: Using SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR should raise an error for multi-arch builds.
grade: stable
confinement: strict

architectures:
- build-on: amd64
run-on: [amd64, armhf]

environment:
SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR}

parts:
my-part:
plugin: nil
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: build-for-part-error
base: core20
version: '1.0'
summary: test
description: Using SNAPCRAFT_ARCH_BUILD_FOR inside a part definition should raise an error if the arch cannot be determined.
grade: stable
confinement: strict

architectures:
- build-on: amd64
run-on: [amd64, armhf]

parts:
my-part:
plugin: nil
override-pull:
echo ${SNAPCRAFT_ARCH_BUILD_FOR}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: build-for-part-triplet-error
base: core20
version: '1.0'
summary: test
description: Using SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR inside a part definition should raise an error if the arch cannot be determined.
grade: stable
confinement: strict

architectures:
- build-on: amd64
run-on: [amd64, armhf]

parts:
my-part:
plugin: nil
override-pull:
echo ${SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: build-for-unknown-arch-error
base: core20
version: '1.0'
summary: test
description: Using SNAPCRAFT_ARCH_BUILD_FOR should raise an error when the build-for arch is unknown.
grade: stable
confinement: strict

architectures:
- build-on: amd64
run-on: badvalue

environment:
SNAPCRAFT_ARCH_BUILD_FOR: ${SNAPCRAFT_ARCH_BUILD_FOR}

parts:
my-part:
plugin: nil
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: build-for-unknown-arch-triplet-error
base: core20
version: '1.0'
summary: test
description: Using SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR should raise an error when the build-for arch is unknown.
grade: stable
confinement: strict

architectures:
- build-on: amd64
run-on: badvalue

environment:
SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR}

parts:
my-part:
plugin: nil
Loading

0 comments on commit 48bd063

Please sign in to comment.