Repository: canonical/snapcraft

Issue:
Raise error when using `BUILD_FOR` envvars on multi-arch and unknown builds
### What needs to get done

https://github.com/snapcore/snapcraft/pull/4313 added the following environment variables for core20 snaps:

- SNAPCRAFT_ARCH_BUILD_ON
- SNAPCRAFT_ARCH_TRIPLET_BUILD_ON
- SNAPCRAFT_ARCH_BUILD_FOR
- SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR

Multi-arch builds and builds with unknown architectures do not evaluate `SNAPCRAFT_ARCH_BUILD_FOR` or `SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR`.

When these envvars are used in a snapcraft.yaml, an error should be raised, explaining to the user why they cannot use these envvars.

### Why it needs to get done

This will improve the user experience for core20 snaps, where users are allowed to build for multiple architectures and unknown architectures.


Discussion:
In snapcraft.yaml
```
environment:
    LD_LIBRARY_PATH: /usr/lib/$SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR
```

Would fail if multiple build fors exist, this could also fail correctly

```
override-build: |
     mv lib.a $SNAPCRAFT_PART_INSTALL/usr/lib/$SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR/
```

but the following would not

```
override-build: |
    ./build.sh
```

build.sh
```shell
#!/bin/sh
mv lib.a $SNAPCRAFT_PART_INSTALL/usr/lib/$SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR/
```

nor,

```yaml
parts:
    source: .
    plugin: make
```

Makefile,

```Makefile
all:
    mv lib.a $SNAPCRAFT_PART_INSTALL/usr/lib/$(SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR)/
```
always emit a warning for an empty build-for

Resolving patch:
diff --git a/snapcraft_legacy/internal/pluginhandler/_part_environment.py b/snapcraft_legacy/internal/pluginhandler/_part_environment.py
--- a/snapcraft_legacy/internal/pluginhandler/_part_environment.py
+++ b/snapcraft_legacy/internal/pluginhandler/_part_environment.py
@@ -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(),
@@ -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(
diff --git a/snapcraft_legacy/internal/project_loader/__init__.py b/snapcraft_legacy/internal/project_loader/__init__.py
--- a/snapcraft_legacy/internal/project_loader/__init__.py
+++ b/snapcraft_legacy/internal/project_loader/__init__.py
@@ -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
@@ -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
@@ -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
@@ -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):
@@ -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",
+        )
diff --git a/snapcraft_legacy/internal/project_loader/errors.py b/snapcraft_legacy/internal/project_loader/errors.py
--- a/snapcraft_legacy/internal/project_loader/errors.py
+++ b/snapcraft_legacy/internal/project_loader/errors.py
@@ -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
@@ -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"


Test patch:
diff --git a/tests/legacy/unit/pluginhandler/test_environment.py b/tests/legacy/unit/pluginhandler/test_environment.py
--- a/tests/legacy/unit/pluginhandler/test_environment.py
+++ b/tests/legacy/unit/pluginhandler/test_environment.py
@@ -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",
@@ -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",
@@ -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
diff --git a/tests/legacy/unit/project_loader/test_errors.py b/tests/legacy/unit/project_loader/test_errors.py
--- a/tests/legacy/unit/project_loader/test_errors.py
+++ b/tests/legacy/unit/project_loader/test_errors.py
@@ -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
@@ -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"
+    )
diff --git a/tests/legacy/unit/project_loader/test_replace_attr.py b/tests/legacy/unit/project_loader/test_replace_attr.py
--- a/tests/legacy/unit/project_loader/test_replace_attr.py
+++ b/tests/legacy/unit/project_loader/test_replace_attr.py
@@ -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
@@ -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
@@ -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)
diff --git a/tests/spread/cross-compile/environment-errors/snaps/build-for-multi-arch-error/snap/snapcraft.yaml b/tests/spread/cross-compile/environment-errors/snaps/build-for-multi-arch-error/snap/snapcraft.yaml
new file mode 100644
--- /dev/null
+++ b/tests/spread/cross-compile/environment-errors/snaps/build-for-multi-arch-error/snap/snapcraft.yaml
@@ -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
diff --git a/tests/spread/cross-compile/environment-errors/snaps/build-for-multi-arch-triplet-error/snap/snapcraft.yaml b/tests/spread/cross-compile/environment-errors/snaps/build-for-multi-arch-triplet-error/snap/snapcraft.yaml
new file mode 100644
--- /dev/null
+++ b/tests/spread/cross-compile/environment-errors/snaps/build-for-multi-arch-triplet-error/snap/snapcraft.yaml
@@ -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
diff --git a/tests/spread/cross-compile/environment-errors/snaps/build-for-part-error/snap/snapcraft.yaml b/tests/spread/cross-compile/environment-errors/snaps/build-for-part-error/snap/snapcraft.yaml
new file mode 100644
--- /dev/null
+++ b/tests/spread/cross-compile/environment-errors/snaps/build-for-part-error/snap/snapcraft.yaml
@@ -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}
diff --git a/tests/spread/cross-compile/environment-errors/snaps/build-for-part-triplet-error/snap/snapcraft.yaml b/tests/spread/cross-compile/environment-errors/snaps/build-for-part-triplet-error/snap/snapcraft.yaml
new file mode 100644
--- /dev/null
+++ b/tests/spread/cross-compile/environment-errors/snaps/build-for-part-triplet-error/snap/snapcraft.yaml
@@ -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}
diff --git a/tests/spread/cross-compile/environment-errors/snaps/build-for-unknown-arch-error/snap/snapcraft.yaml b/tests/spread/cross-compile/environment-errors/snaps/build-for-unknown-arch-error/snap/snapcraft.yaml
new file mode 100644
--- /dev/null
+++ b/tests/spread/cross-compile/environment-errors/snaps/build-for-unknown-arch-error/snap/snapcraft.yaml
@@ -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
diff --git a/tests/spread/cross-compile/environment-errors/snaps/build-for-unknown-arch-triplet-error/snap/snapcraft.yaml b/tests/spread/cross-compile/environment-errors/snaps/build-for-unknown-arch-triplet-error/snap/snapcraft.yaml
new file mode 100644
--- /dev/null
+++ b/tests/spread/cross-compile/environment-errors/snaps/build-for-unknown-arch-triplet-error/snap/snapcraft.yaml
@@ -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
diff --git a/tests/spread/cross-compile/environment-errors/task.yaml b/tests/spread/cross-compile/environment-errors/task.yaml
new file mode 100644
--- /dev/null
+++ b/tests/spread/cross-compile/environment-errors/task.yaml
@@ -0,0 +1,22 @@
+summary: Test failing scenarios for architecture envvars
+
+environment:
+  SNAP/build_for_multi_arch_error: build-for-multi-arch-error
+  SNAP/build_for_multi_arch_triplet_error: build-for-multi-arch-triplet-error
+  SNAP/build_for_part_error: build-for-part-error
+  SNAP/build_for_part_triplet_error: build-for-part-triplet-error
+  SNAP/build_for_unknown_arch_error: build-for-unknown-arch-error
+  SNAP/build_for_unknown_arch_triplet_error: build-for-unknown-arch-triplet-error
+
+restore: |
+  cd "./snaps/$SNAP"
+  snapcraft clean --destructive-mode
+  rm -f ./*.snap
+
+execute: |
+  cd "./snaps/$SNAP"
+
+  if snapcraft --destructive-mode; then
+    echo "snapcraft did not fail but should have"
+    exit 1
+  fi
diff --git a/tests/spread/cross-compile/environment/snaps/build-for-multi-arch/expected-env.txt b/tests/spread/cross-compile/environment/snaps/build-for-multi-arch/expected-env.txt
--- a/tests/spread/cross-compile/environment/snaps/build-for-multi-arch/expected-env.txt
+++ b/tests/spread/cross-compile/environment/snaps/build-for-multi-arch/expected-env.txt
@@ -1,6 +1,4 @@
 SNAPCRAFT_ARCH_TRIPLET: x86_64-linux-gnu
-SNAPCRAFT_ARCH_BUILD_FOR: ${SNAPCRAFT_ARCH_BUILD_FOR}
-SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR}
 SNAPCRAFT_ARCH_BUILD_ON: amd64
 SNAPCRAFT_ARCH_TRIPLET_BUILD_ON: x86_64-linux-gnu
 SNAPCRAFT_TARGET_ARCH: amd64
diff --git a/tests/spread/cross-compile/environment/snaps/build-for-multi-arch/snap/snapcraft.yaml b/tests/spread/cross-compile/environment/snaps/build-for-multi-arch/snap/snapcraft.yaml
--- a/tests/spread/cross-compile/environment/snaps/build-for-multi-arch/snap/snapcraft.yaml
+++ b/tests/spread/cross-compile/environment/snaps/build-for-multi-arch/snap/snapcraft.yaml
@@ -2,7 +2,7 @@ name: build-for-multi-arch
 base: core20
 version: '1.0'
 summary: test
-description: BUILD_ON envvars should not be evaluated for multi-arch builds.
+description: Only BUILD_ON envvars can be evaluated for multi-arch builds.
 grade: stable
 confinement: strict
 
@@ -12,8 +12,6 @@ architectures:
 
 environment:
   SNAPCRAFT_ARCH_TRIPLET: ${SNAPCRAFT_ARCH_TRIPLET}
-  SNAPCRAFT_ARCH_BUILD_FOR: ${SNAPCRAFT_ARCH_BUILD_FOR}
-  SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR}
   SNAPCRAFT_ARCH_BUILD_ON: ${SNAPCRAFT_ARCH_BUILD_ON}
   SNAPCRAFT_ARCH_TRIPLET_BUILD_ON: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_ON}
   SNAPCRAFT_TARGET_ARCH: ${SNAPCRAFT_TARGET_ARCH}
@@ -21,3 +19,10 @@ environment:
 parts:
   my-part:
     plugin: nil
+    override-build: |
+      cat << EOF >> $SNAPCRAFT_PART_INSTALL/part-variables.txt
+      SNAPCRAFT_ARCH_TRIPLET: ${SNAPCRAFT_ARCH_TRIPLET}
+      SNAPCRAFT_ARCH_BUILD_ON: ${SNAPCRAFT_ARCH_BUILD_ON}
+      SNAPCRAFT_ARCH_TRIPLET_BUILD_ON: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_ON}
+      SNAPCRAFT_TARGET_ARCH: ${SNAPCRAFT_TARGET_ARCH}
+      EOF
diff --git a/tests/spread/cross-compile/environment/snaps/build-for-single-arch/snap/snapcraft.yaml b/tests/spread/cross-compile/environment/snaps/build-for-single-arch/snap/snapcraft.yaml
--- a/tests/spread/cross-compile/environment/snaps/build-for-single-arch/snap/snapcraft.yaml
+++ b/tests/spread/cross-compile/environment/snaps/build-for-single-arch/snap/snapcraft.yaml
@@ -2,7 +2,7 @@ name: build-for-single-arch
 base: core20
 version: '1.0'
 summary: test
-description: BUILD_ON envvars be evaluated when the build-on arch is a scalar.
+description: BUILD_ON and BUILD_FOR envvars should be evaluated when the build-for arch is a scalar.
 grade: stable
 confinement: strict
 
@@ -21,3 +21,12 @@ environment:
 parts:
   my-part:
     plugin: nil
+    override-build: |
+      cat << EOF >> $SNAPCRAFT_PART_INSTALL/part-variables.txt
+      SNAPCRAFT_ARCH_TRIPLET: ${SNAPCRAFT_ARCH_TRIPLET}
+      SNAPCRAFT_ARCH_BUILD_FOR: ${SNAPCRAFT_ARCH_BUILD_FOR}
+      SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR}
+      SNAPCRAFT_ARCH_BUILD_ON: ${SNAPCRAFT_ARCH_BUILD_ON}
+      SNAPCRAFT_ARCH_TRIPLET_BUILD_ON: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_ON}
+      SNAPCRAFT_TARGET_ARCH: ${SNAPCRAFT_TARGET_ARCH}
+      EOF
diff --git a/tests/spread/cross-compile/environment/snaps/build-for-unknown-arch/expected-env.txt b/tests/spread/cross-compile/environment/snaps/build-for-unknown-arch/expected-env.txt
--- a/tests/spread/cross-compile/environment/snaps/build-for-unknown-arch/expected-env.txt
+++ b/tests/spread/cross-compile/environment/snaps/build-for-unknown-arch/expected-env.txt
@@ -1,6 +1,4 @@
 SNAPCRAFT_ARCH_TRIPLET: x86_64-linux-gnu
-SNAPCRAFT_ARCH_BUILD_FOR: ${SNAPCRAFT_ARCH_BUILD_FOR}
-SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR}
 SNAPCRAFT_ARCH_BUILD_ON: amd64
 SNAPCRAFT_ARCH_TRIPLET_BUILD_ON: x86_64-linux-gnu
 SNAPCRAFT_TARGET_ARCH: amd64
diff --git a/tests/spread/cross-compile/environment/snaps/build-for-unknown-arch/snap/snapcraft.yaml b/tests/spread/cross-compile/environment/snaps/build-for-unknown-arch/snap/snapcraft.yaml
--- a/tests/spread/cross-compile/environment/snaps/build-for-unknown-arch/snap/snapcraft.yaml
+++ b/tests/spread/cross-compile/environment/snaps/build-for-unknown-arch/snap/snapcraft.yaml
@@ -2,7 +2,7 @@ name: build-for-unknown-arch
 base: core20
 version: '1.0'
 summary: test
-description: BUILD_ON envvars should not be evaluated when the build-on arch is unknown.
+description: Only BUILD_ON envvars can be evaluated when the build-for arch is unknown.
 grade: stable
 confinement: strict
 
@@ -12,8 +12,6 @@ architectures:
 
 environment:
   SNAPCRAFT_ARCH_TRIPLET: ${SNAPCRAFT_ARCH_TRIPLET}
-  SNAPCRAFT_ARCH_BUILD_FOR: ${SNAPCRAFT_ARCH_BUILD_FOR}
-  SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR}
   SNAPCRAFT_ARCH_BUILD_ON: ${SNAPCRAFT_ARCH_BUILD_ON}
   SNAPCRAFT_ARCH_TRIPLET_BUILD_ON: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_ON}
   SNAPCRAFT_TARGET_ARCH: ${SNAPCRAFT_TARGET_ARCH}
@@ -21,3 +19,10 @@ environment:
 parts:
   my-part:
     plugin: nil
+    override-build: |
+      cat << EOF >> $SNAPCRAFT_PART_INSTALL/part-variables.txt
+      SNAPCRAFT_ARCH_TRIPLET: ${SNAPCRAFT_ARCH_TRIPLET}
+      SNAPCRAFT_ARCH_BUILD_ON: ${SNAPCRAFT_ARCH_BUILD_ON}
+      SNAPCRAFT_ARCH_TRIPLET_BUILD_ON: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_ON}
+      SNAPCRAFT_TARGET_ARCH: ${SNAPCRAFT_TARGET_ARCH}
+      EOF
diff --git a/tests/spread/cross-compile/environment/snaps/target-arch/snap/snapcraft.yaml b/tests/spread/cross-compile/environment/snaps/target-arch/snap/snapcraft.yaml
--- a/tests/spread/cross-compile/environment/snaps/target-arch/snap/snapcraft.yaml
+++ b/tests/spread/cross-compile/environment/snaps/target-arch/snap/snapcraft.yaml
@@ -2,7 +2,7 @@ name: target-arch
 base: core20
 version: '1.0'
 summary: test
-description: use target-arch for build-for
+description: BUILD_ON and BUILD_FOR envvars can be evaluated when `--target-arch` is provided
 grade: stable
 confinement: strict
 
@@ -21,3 +21,12 @@ environment:
 parts:
   my-part:
     plugin: nil
+    override-build: |
+      cat << EOF >> $SNAPCRAFT_PART_INSTALL/part-variables.txt
+      SNAPCRAFT_ARCH_TRIPLET: ${SNAPCRAFT_ARCH_TRIPLET}
+      SNAPCRAFT_ARCH_BUILD_FOR: ${SNAPCRAFT_ARCH_BUILD_FOR}
+      SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR}
+      SNAPCRAFT_ARCH_BUILD_ON: ${SNAPCRAFT_ARCH_BUILD_ON}
+      SNAPCRAFT_ARCH_TRIPLET_BUILD_ON: ${SNAPCRAFT_ARCH_TRIPLET_BUILD_ON}
+      SNAPCRAFT_TARGET_ARCH: ${SNAPCRAFT_TARGET_ARCH}
+      EOF
diff --git a/tests/spread/cross-compile/environment/task.yaml b/tests/spread/cross-compile/environment/task.yaml
--- a/tests/spread/cross-compile/environment/task.yaml
+++ b/tests/spread/cross-compile/environment/task.yaml
@@ -1,14 +1,14 @@
 summary: Test envvars related to architectures
 
 environment:
-  SNAP/build_for_single_arch: build-for-single-arch
   SNAP/build_for_multi_arch: build-for-multi-arch
+  SNAP/build_for_single_arch: build-for-single-arch
   SNAP/build_for_unknown_arch: build-for-unknown-arch
   SNAP/target_arch: target-arch
 
 restore: |
   cd "./snaps/$SNAP"
-  snapcraft clean
+  snapcraft clean --destructive-mode
   rm -f ./*.snap
 
 execute: |
@@ -19,10 +19,19 @@ execute: |
   else
     snapcraft prime --destructive-mode
   fi
-  
+
+  # verify variables were evaluated in the environment in the snap.yaml
   while read -r expected_envvar; do
     if ! grep -qF "$expected_envvar" prime/meta/snap.yaml; then
       echo "Did not find '$expected_envvar' in 'prime/meta/snap.yaml'."
       exit 1
     fi
   done < expected-env.txt
+
+  # verify variables were evaluated inside a part's override script
+  while read -r expected_envvar; do
+    if ! grep -qF "$expected_envvar" prime/part-variables.txt; then
+      echo "Did not find '$expected_envvar' in 'prime/part-variables.txt'."
+      exit 1
+    fi
+  done < expected-env.txt