Skip to content

Commit

Permalink
environment: add build-on and build-for envvars for core20 (#4313)
Browse files Browse the repository at this point in the history
New environment variables:
- SNAPCRAFT_ARCH_BUILD_FOR
- SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR
- SNAPCRAFT_ARCH_BUILD_ON
- SNAPCRAFT_ARCH_TRIPLET_BUILD_ON

The run-on envvars are only available when the run-on architecture can
be determined. They are not available for multi-architecture builds
or for unknown run-on architectures.

Signed-off-by: Callahan Kovacs <[email protected]>
  • Loading branch information
mr-cal authored Aug 15, 2023
1 parent 363d2bd commit 93a9d81
Show file tree
Hide file tree
Showing 14 changed files with 566 additions and 9 deletions.
12 changes: 10 additions & 2 deletions snapcraft_legacy/internal/pluginhandler/_part_environment.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 Down Expand Up @@ -71,7 +71,9 @@ def get_snapcraft_global_environment(
else:
content_dirs_envvar = ""

return {
environment = {
"SNAPCRAFT_ARCH_BUILD_ON": project.arch_build_on,
"SNAPCRAFT_ARCH_TRIPLET_BUILD_ON": project.arch_triplet_build_on,
"SNAPCRAFT_ARCH_TRIPLET": project.arch_triplet,
"SNAPCRAFT_EXTENSIONS_DIR": common.get_extensionsdir(),
"SNAPCRAFT_PARALLEL_BUILD_COUNT": str(project.parallel_build_count),
Expand All @@ -84,6 +86,12 @@ 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
11 changes: 8 additions & 3 deletions snapcraft_legacy/project/_project.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) 2018-2020 Canonical Ltd
# Copyright (C) 2018-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 Down Expand Up @@ -47,8 +47,6 @@ def __init__(
else:
work_dir = project_dir

super().__init__(target_deb_arch, debug, work_dir=work_dir)

# This here check is mostly for backwards compatibility with the
# rest of the code base.
if snapcraft_yaml_file_path is None:
Expand All @@ -57,6 +55,13 @@ def __init__(
else:
self.info = ProjectInfo(snapcraft_yaml_file_path=snapcraft_yaml_file_path)

super().__init__(
target_deb_arch,
debug,
work_dir=work_dir,
architectures=self.info.architectures if self.info else None
)

self._is_managed_host = is_managed_host
self._project_dir = project_dir
self._work_dir = work_dir
Expand Down
146 changes: 143 additions & 3 deletions snapcraft_legacy/project/_project_options.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) 2016-2019 Canonical Ltd
# Copyright (C) 2016-2019, 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 @@ -19,7 +19,7 @@
import os
import platform
import sys
from typing import Set
from typing import Dict, List, Optional, Set, Union

from snapcraft_legacy import file_utils
from snapcraft_legacy.internal import common, errors, os_release
Expand Down Expand Up @@ -166,6 +166,10 @@ def is_cross_compiling(self):

@property
def target_arch(self):
"""Returns the debian architecture of the build-for platform when
cross-compiling. Returns the debian architecture of the build-on platform
when not cross-compiling.
"""
return self.__target_arch

@property
Expand Down Expand Up @@ -194,10 +198,18 @@ def additional_build_packages(self):

@property
def arch_triplet(self):
"""Returns the architecture triplet of the build-for platform when
cross-compiling. Otherwise returns the architecture triplet of the build-on
platform.
"""
return self.__machine_info["triplet"]

@property
def deb_arch(self):
"""Returns the debian architecture of the build-for platform when
cross-compiling. Otherwise returns the debian architecture of the build-on
platform.
"""
return self.__machine_info["deb"]

@property
Expand All @@ -206,6 +218,7 @@ def kernel_arch(self):

@property
def host_deb_arch(self):
"""Returns the debian architecture of the build-on platform."""
return self.__host_info["deb"]

@property
Expand All @@ -224,8 +237,134 @@ def prime_dir(self) -> str:
def debug(self):
return self._debug

@property
def arch_build_on(self) -> str:
"""Returns the build-on architecture."""
return self.__host_info["deb"]

@property
def arch_triplet_build_on(self) -> str:
"""Returns the build-on architecture."""
return self.__host_info["triplet"]

@property
def arch_build_for(self) -> str:
"""Returns the build-for architecture."""
return self.__build_for_info["deb"] if self.__build_for_info else None

@property
def arch_triplet_build_for(self) -> str:
"""Returns the build-for architecture."""
return self.__build_for_info["triplet"] if self.__build_for_info else None

def _set_build_for_info(
self, target_deb_arch: str, architectures: List[Union[str, Dict[str, str]]]
) -> None:
"""Set machine info for the build-for platform, if possible.
If `target_deb_arch` was provided for cross-compiling, this architecture is used
for the build-for platform. If `target_deb_arch` was not provided, the build-for
platform will be determined from the architectures in the snapcraft.yaml.
The build-for architecture cannot be determined for multi-arch builds. These are
defined with a shorthand list `architectures: [arch1, arch2]` or when multiple
build-for architectures are provided `run-on: [arch1, arch2]`.
If no architectures are provided, the build-for platform will be set to the
build-on platform. Finally, if the build-for architecture could not be
determined then it will be set to None.
:param target_deb_arch: the target architecture when cross-compiling
:param architectures: the project's architecture data to process
:returns: a dictionary of information about the build-for architecture or None
"""
self.__build_for_info = None

# if `target-arch` was provided, use it for the build-for arch
if target_deb_arch:
self.__build_for_info = _ARCH_TRANSLATIONS[self.__target_machine]
# if no architectures were provided, set build-for arch to the build-on arch
elif not architectures:
self.__build_for_info = self.__host_info
# else look for a build-for architecture from the snapcraft.yaml
else:
self.__build_for_info = self._process_architecture_data(architectures)

if self.__build_for_info:
logger.debug("Set build-for platform to %r", self.__build_for_info["deb"])
else:
logger.debug("Could not determine a build-for platform.")

def _process_architecture_data(
self, architectures: List[Union[str, Dict[str, str]]]
) -> Optional[Dict[str, str]]:
"""Process architecture data and determine the build-for architecture.
This occurs before the architecture data is fully validated, so this function
will not raise errors on invalid data. Instead, it will return None and let the
validators raise an error for the user later on.
:param architectures: the project's architecture data to process
:returns: a dictionary of information about the build-for architecture or None
"""
# if not a list, the validator will raise an error later
if not isinstance(architectures, list):
return None

# if a list of strings was provided, then the architecture can be decoded
if isinstance(architectures[0], str):
return self._determine_build_for_architecture(architectures)

# otherwise, parse through the 'build-on' and 'run-on' (build-for) fields
for item in architectures:
arch_build_on = item.get("build-on")
arch_build_for = item.get("run-on")
if arch_build_on and self.__host_info["deb"] in arch_build_on:
# build-for must be a scalar, not a multi-arch list
if arch_build_for:
return self._determine_build_for_architecture(arch_build_for)
# if there is no build-for, try to use the build-on field
return self._determine_build_for_architecture(arch_build_on)
return None

def _determine_build_for_architecture(
self, architectures: Union[str, List[str]]
) -> Optional[Dict[str, str]]:
"""Determine the build-for architecture.
The build-for architecture can only be determined when a string or a single
element list is provided. The build-for arch must also be a valid architecture.
:param architectures: a single architecture or a list of architectures
:returns: a dictionary of information about the build-for architecture or None
"""
if isinstance(architectures, list):
# convert single element list to string
if len(architectures) == 1:
architectures = architectures[0]
# lists of architectures cannot be decoded
else:
logger.debug("Cannot set build-for info for multi-arch build")
return None

try:
return _ARCH_TRANSLATIONS[_find_machine(architectures)]
except errors.SnapcraftEnvironmentError:
logger.debug(
"Cannot set build-for info from unknown architectures %r", architectures
)
return None

def __init__(
self, target_deb_arch=None, debug=False, *, work_dir: str = None
self,
target_deb_arch=None,
debug=False,
*,
work_dir: str = None,
architectures = None,
) -> None:

# Here for backwards compatibility.
Expand All @@ -244,6 +383,7 @@ def __init__(
logger.debug("Prime dir {}".format(self._prime_dir))

self._set_machine(target_deb_arch)
self._set_build_for_info(target_deb_arch, architectures)

def _get_content_snaps(self) -> Set[str]:
"""Temporary shim for unit tests using ProjectOptions
Expand Down
120 changes: 120 additions & 0 deletions tests/legacy/unit/pluginhandler/test_environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 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
# 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/>.

import sys

import pytest

from snapcraft_legacy.internal.pluginhandler._part_environment import (
get_snapcraft_global_environment,
)
from snapcraft_legacy.project import Project
from tests.legacy.fixture_setup import SnapcraftYaml


@pytest.fixture(autouse=True)
def mock_platform(mocker):
mocker.patch.object(sys, "platform", "linux")
mocker.patch("platform.machine", return_value="aarch64")


def test_no_build_for_arch(tmp_path):
"""build-for envvars should exist when the build-for arch can be determined."""
snapcraft_yaml = SnapcraftYaml(
tmp_path,
base="core20",
parts={"test-part": {"plugin": "nil"}},
architectures=[{"build-on": ["amd64", "arm64"], "run-on": "amd64"}],
)
snapcraft_yaml.write_snapcraft_yaml()
project = Project(snapcraft_yaml_file_path=snapcraft_yaml.snapcraft_yaml_file_path)

environment = get_snapcraft_global_environment(project)

assert environment["SNAPCRAFT_ARCH_BUILD_ON"] == project.arch_build_on
assert (
environment["SNAPCRAFT_ARCH_TRIPLET_BUILD_ON"] == project.arch_triplet_build_on
)
assert environment["SNAPCRAFT_ARCH_BUILD_FOR"] == project.arch_build_for
assert (
environment["SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR"]
== project.arch_triplet_build_for
)


def test_implicit_build_for_arch(tmp_path):
"""build-for envvars should exist for when the build-for arch is implicit."""
snapcraft_yaml = SnapcraftYaml(
tmp_path,
base="core20",
parts={"test-part": {"plugin": "nil"}},
architectures=[{"build-on": "arm64"}],
)
snapcraft_yaml.write_snapcraft_yaml()
project = Project(snapcraft_yaml_file_path=snapcraft_yaml.snapcraft_yaml_file_path)

environment = get_snapcraft_global_environment(project)

assert environment["SNAPCRAFT_ARCH_BUILD_ON"] == project.arch_build_on
assert (
environment["SNAPCRAFT_ARCH_TRIPLET_BUILD_ON"] == project.arch_triplet_build_on
)
assert environment["SNAPCRAFT_ARCH_BUILD_FOR"] == project.arch_build_for
assert (
environment["SNAPCRAFT_ARCH_TRIPLET_BUILD_FOR"]
== project.arch_triplet_build_for
)


def test_no_build_for_unknown_arch(tmp_path):
"""build-for envvars should not be defined for unknown build-for architectures."""
snapcraft_yaml = SnapcraftYaml(
tmp_path,
base="core20",
parts={"test-part": {"plugin": "nil"}},
architectures=[{"build-on": ["amd64", "arm64"], "run-on": "unknown-arch"}],
)
snapcraft_yaml.write_snapcraft_yaml()
project = Project(snapcraft_yaml_file_path=snapcraft_yaml.snapcraft_yaml_file_path)

environment = get_snapcraft_global_environment(project)
assert environment["SNAPCRAFT_ARCH_BUILD_ON"] == project.arch_build_on
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


def test_no_build_for_multi_arch(tmp_path):
"""build-for envvars should not be defined for multi-arch builds."""
snapcraft_yaml = SnapcraftYaml(
tmp_path,
base="core20",
parts={"test-part": {"plugin": "nil"}},
architectures=[{"build-on": ["amd64", "arm64"], "run-on": ["amd64", "arm64"]}],
)
snapcraft_yaml.write_snapcraft_yaml()
project = Project(snapcraft_yaml_file_path=snapcraft_yaml.snapcraft_yaml_file_path)

environment = get_snapcraft_global_environment(project)

assert environment["SNAPCRAFT_ARCH_BUILD_ON"] == project.arch_build_on
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
Loading

0 comments on commit 93a9d81

Please sign in to comment.