Skip to content

Commit

Permalink
meta: use build-for in snap.yaml architecture (#4150)
Browse files Browse the repository at this point in the history
Signed-off-by: Callahan Kovacs <callahan.kovacs@canonical.com>
  • Loading branch information
mr-cal committed May 19, 2023
1 parent e52221b commit 77d3454
Show file tree
Hide file tree
Showing 13 changed files with 249 additions and 105 deletions.
16 changes: 12 additions & 4 deletions snapcraft/elf/elf_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import platform
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, List, Set
from typing import Iterable, List, Optional, Set

from craft_cli import emit
from elftools.common.exceptions import ELFError
Expand Down Expand Up @@ -127,9 +127,17 @@ def get_dynamic_linker(*, root_path: Path, snap_path: Path) -> str:
return str(snap_path / arch_config.dynamic_linker)


def get_arch_triplet() -> str:
"""Inform the arch triplet string for the current architecture."""
arch = platform.machine()
def get_arch_triplet(arch: Optional[str] = None) -> str:
"""Get the arch triplet string for an architecture.
:param arch: Architecture to get the triplet of. If None, then get the arch triplet
of the host.
:returns: The arch triplet.
"""
if not arch:
arch = platform.machine()

arch_config = _ARCH_CONFIG.get(arch)
if not arch_config:
raise RuntimeError(f"Arch triplet not defined for arch {arch!r}")
Expand Down
15 changes: 12 additions & 3 deletions snapcraft/meta/snap_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,12 @@ def _get_grade(grade: Optional[str], build_base: Optional[str]) -> str:
return grade


def write(project: Project, prime_dir: Path, *, arch: str, arch_triplet: str):
def write(project: Project, prime_dir: Path, *, arch: str):
"""Create a snap.yaml file.
:param project: Snapcraft project.
:param prime_dir: The directory containing the content to be snapped.
:param arch: Target architecture the snap project is built to.
:param arch_triplet: Architecture triplet of the platform.
"""
meta_dir = prime_dir / "meta"
meta_dir.mkdir(parents=True, exist_ok=True)
Expand All @@ -395,6 +394,9 @@ def write(project: Project, prime_dir: Path, *, arch: str, arch_triplet: str):
if project.hooks and any(h for h in project.hooks.values() if h.command_chain):
assumes.add("command-chain")

# if arch is "all", do not include architecture-specific paths in the environment
arch_triplet = None if arch == "all" else project.get_build_for_arch_triplet()

environment = _populate_environment(project.environment, prime_dir, arch_triplet)
version = process_version(project.version)

Expand Down Expand Up @@ -449,14 +451,21 @@ def _repr_str(dumper, data):


def _populate_environment(
environment: Optional[Dict[str, Optional[str]]], prime_dir: Path, arch_triplet: str
environment: Optional[Dict[str, Optional[str]]],
prime_dir: Path,
arch_triplet: Optional[str],
):
"""Populate default app environmental variables.
Three cases for LD_LIBRARY_PATH and PATH variables:
- If LD_LIBRARY_PATH or PATH are defined, keep user-defined values.
- If LD_LIBRARY_PATH or PATH are not defined, set to default values.
- If LD_LIBRARY_PATH or PATH are null, do not use default values.
:param environment: Dictionary of environment variables from the project.
:param prime_dir: The directory containing the content to be snapped.
:param arch_triplet: Architecture triplet of the target arch. If None, the
environment will not contain architecture-specific paths.
"""
if environment is None:
return {
Expand Down
9 changes: 2 additions & 7 deletions snapcraft/parts/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,12 +410,7 @@ def _generate_metadata(
)

emit.progress("Generating snap metadata...")
snap_yaml.write(
project,
lifecycle.prime_dir,
arch=lifecycle.target_arch,
arch_triplet=lifecycle.target_arch_triplet,
)
snap_yaml.write(project, lifecycle.prime_dir, arch=project.get_build_for())
emit.progress("Generated snap metadata", permanent=True)

if parsed_args.enable_manifest:
Expand Down Expand Up @@ -449,7 +444,7 @@ def _generate_manifest(
manifest.write(
project,
lifecycle.prime_dir,
arch=lifecycle.target_arch,
arch=project.get_build_for(),
parts=parts,
start_time=start_time,
image_information=image_information,
Expand Down
19 changes: 18 additions & 1 deletion snapcraft/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,13 @@
from pydantic import PrivateAttr, conlist, constr

from snapcraft import parts, utils
from snapcraft.elf.elf_utils import get_arch_triplet
from snapcraft.errors import ProjectValidationError
from snapcraft.utils import get_effective_base, get_host_architecture
from snapcraft.utils import (
convert_architecture_deb_to_platform,
get_effective_base,
get_host_architecture,
)


class ProjectModel(pydantic.BaseModel):
Expand Down Expand Up @@ -713,6 +718,18 @@ def get_build_for(self) -> str:
# will not happen after schema validation
raise RuntimeError("cannot determine build-for architecture")

def get_build_for_arch_triplet(self) -> Optional[str]:
"""Get the architecture triplet for the first build-for architecture.
:returns: The build-for arch triplet. If build-for is "all", then return None.
"""
arch = self.get_build_for()

if arch != "all":
return get_arch_triplet(convert_architecture_deb_to_platform(arch))

return None


class _GrammarAwareModel(pydantic.BaseModel):
class Config:
Expand Down
36 changes: 29 additions & 7 deletions snapcraft/utils.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 2021-2022 Canonical Ltd.
# Copyright 2021-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 @@ -329,20 +329,42 @@ def humanize_list(
return f"{humanized} {conjunction} {quoted_items[-1]}"


def get_common_ld_library_paths(prime_dir: Path, arch_triplet: str) -> List[str]:
"""Return common existing PATH entries for a snap."""
def get_common_ld_library_paths(
prime_dir: Path, arch_triplet: Optional[str]
) -> List[str]:
"""Return common existing PATH entries for a snap.
:param prime_dir: Path to the prime directory.
:param arch_triplet: Architecture triplet of target arch. If None, the list of paths
will not contain architecture-specific paths.
:returns: List of common library paths in the prime directory that exist.
"""
paths = [
prime_dir / "lib",
prime_dir / "usr" / "lib",
prime_dir / "lib" / arch_triplet,
prime_dir / "usr" / "lib" / arch_triplet,
]

if arch_triplet:
paths.extend(
[
prime_dir / "lib" / arch_triplet,
prime_dir / "usr" / "lib" / arch_triplet,
]
)

return [str(p) for p in paths if p.exists()]


def get_ld_library_paths(prime_dir: Path, arch_triplet: str) -> str:
"""Return a usable in-snap LD_LIBRARY_PATH variable."""
def get_ld_library_paths(prime_dir: Path, arch_triplet: Optional[str]) -> str:
"""Return a usable in-snap LD_LIBRARY_PATH variable.
:param prime_dir: Path to the prime directory.
:param arch_triplet: Architecture triplet of target arch. If None, LD_LIBRARY_PATH
will not contain architecture-specific paths.
:returns: The LD_LIBRARY_PATH environment variable to be used for the snap.
"""
paths = ["${SNAP_LIBRARY_PATH}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"]
# Add the default LD_LIBRARY_PATH
paths += get_common_ld_library_paths(prime_dir, arch_triplet)
Expand Down
21 changes: 19 additions & 2 deletions tests/unit/elf/test_elf_utils.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 2016-2022 Canonical Ltd.
# Copyright 2016-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 @@ -159,13 +159,30 @@ class TestArchConfig:
("x86_64", "x86_64-linux-gnu"),
],
)
def test_get_arch_triplet(self, mocker, machine, expected_arch_triplet):
def test_get_arch_triplet_host(self, mocker, machine, expected_arch_triplet):
"""Verify `get_arch_triplet()` gets the host's architecture triplet."""
mocker.patch("snapcraft.elf.elf_utils.platform.machine", return_value=machine)
arch_triplet = elf_utils.get_arch_triplet()

assert arch_triplet == expected_arch_triplet

@pytest.mark.parametrize(
"machine, expected_arch_triplet",
[
("aarch64", "aarch64-linux-gnu"),
("armv7l", "arm-linux-gnueabihf"),
("ppc64le", "powerpc64le-linux-gnu"),
("riscv64", "riscv64-linux-gnu"),
("s390x", "s390x-linux-gnu"),
("x86_64", "x86_64-linux-gnu"),
],
)
def test_get_arch_triplet(self, mocker, machine, expected_arch_triplet):
"""Get the architecture triplet from the architecture passed as a parameter."""
arch_triplet = elf_utils.get_arch_triplet(machine)

assert arch_triplet == expected_arch_triplet

def test_get_arch_triplet_error(self, mocker):
"""Verify `get_arch_triplet()` raises an error for invalid machines."""
mocker.patch("snapcraft.elf.elf_utils.platform.machine", return_value="4004")
Expand Down
16 changes: 3 additions & 13 deletions tests/unit/linters/test_classic_linter.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 2022 Canonical Ltd.
# Copyright 2022-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 @@ -64,12 +64,7 @@ def test_classic_linter(mocker, new_dir, confinement, stage_libc, text):
}

project = projects.Project.unmarshal(yaml_data)
snap_yaml.write(
project,
prime_dir=Path(new_dir),
arch="amd64",
arch_triplet="x86_64-linux-gnu",
)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")

issues = linters.run_linters(new_dir, lint=None)

Expand Down Expand Up @@ -131,12 +126,7 @@ def test_classic_linter_filter(mocker, new_dir):
}

project = projects.Project.unmarshal(yaml_data)
snap_yaml.write(
project,
prime_dir=Path(new_dir),
arch="amd64",
arch_triplet="x86_64-linux-gnu",
)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")

issues = linters.run_linters(
new_dir, lint=projects.Lint(ignore=[{"classic": ["elf.*"]}])
Expand Down
35 changes: 5 additions & 30 deletions tests/unit/linters/test_library_linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,7 @@ def test_library_linter_missing_library(mocker, new_dir):
}

project = projects.Project.unmarshal(yaml_data)
snap_yaml.write(
project,
prime_dir=Path(new_dir),
arch="amd64",
arch_triplet="x86_64-linux-gnu",
)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")

issues = linters.run_linters(new_dir, lint=None)
assert issues == [
Expand Down Expand Up @@ -114,12 +109,7 @@ def test_library_linter_unused_library(mocker, new_dir):
}

project = projects.Project.unmarshal(yaml_data)
snap_yaml.write(
project,
prime_dir=Path(new_dir),
arch="amd64",
arch_triplet="x86_64-linux-gnu",
)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")

issues = linters.run_linters(new_dir, lint=None)
assert issues == [
Expand Down Expand Up @@ -160,12 +150,7 @@ def test_library_linter_filter_missing_library(mocker, new_dir, filter_name):
}

project = projects.Project.unmarshal(yaml_data)
snap_yaml.write(
project,
prime_dir=Path(new_dir),
arch="amd64",
arch_triplet="x86_64-linux-gnu",
)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")

issues = linters.run_linters(
new_dir, lint=projects.Lint(ignore=[{filter_name: ["elf.*"]}])
Expand Down Expand Up @@ -210,12 +195,7 @@ def test_library_linter_filter_unused_library(mocker, new_dir, filter_name):
}

project = projects.Project.unmarshal(yaml_data)
snap_yaml.write(
project,
prime_dir=Path(new_dir),
arch="amd64",
arch_triplet="x86_64-linux-gnu",
)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")

issues = linters.run_linters(
new_dir, lint=projects.Lint(ignore=[{filter_name: ["lib/libfoo.*"]}])
Expand Down Expand Up @@ -252,12 +232,7 @@ def test_library_linter_mixed_filters(mocker, new_dir):
}

project = projects.Project.unmarshal(yaml_data)
snap_yaml.write(
project,
prime_dir=Path(new_dir),
arch="amd64",
arch_triplet="x86_64-linux-gnu",
)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")

# lib/libfoo.so is an *unused* library, but here we filter out *missing* library
# issues for this path.
Expand Down
11 changes: 2 additions & 9 deletions tests/unit/linters/test_linters.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 2022 Canonical Ltd.
# Copyright 2022-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 @@ -169,7 +169,6 @@ def test_run_linters(self, mocker, new_dir, linter_issue):
project,
prime_dir=Path(new_dir),
arch="amd64",
arch_triplet="x86_64-linux-gnu",
)

issues = linters.run_linters(new_dir, lint=None)
Expand Down Expand Up @@ -199,7 +198,6 @@ def test_run_linters_ignore(self, mocker, new_dir, linter_issue):
project,
prime_dir=Path(new_dir),
arch="amd64",
arch_triplet="x86_64-linux-gnu",
)

lint = projects.Lint(ignore=["test"])
Expand All @@ -221,12 +219,7 @@ def test_run_linters_ignore_all_categories(self, mocker, new_dir, linter_issue):
}

project = projects.Project.unmarshal(yaml_data)
snap_yaml.write(
project,
prime_dir=Path(new_dir),
arch="amd64",
arch_triplet="x86_64-linux-gnu",
)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")

lint = projects.Lint(ignore=["test-1", "test-2"])
issues = linters.run_linters(new_dir, lint=lint)
Expand Down
Loading

0 comments on commit 77d3454

Please sign in to comment.