Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

meta: use build-for in snap.yaml architecture #4150

Merged
merged 3 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -446,14 +448,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