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

feat(linter): show missing libraries corresponding package #4430

Merged
merged 2 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
64 changes: 62 additions & 2 deletions snapcraft/linters/library_linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Library linter implementation."""
import re
import subprocess
from pathlib import Path
from typing import List, Set

Expand All @@ -24,12 +26,16 @@
from snapcraft.elf import ElfFile, SonameCache, elf_utils
from snapcraft.elf import errors as elf_errors

from .base import Linter, LinterIssue, LinterResult
from .base import Linter, LinterIssue, LinterResult, Optional


class LibraryLinter(Linter):
"""Linter for dynamic library availability in snap."""

def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._ld_config_cache: dict[str, Path] = {}

@staticmethod
def get_categories() -> List[str]:
"""Get the specific sub-categories that can be filtered against."""
Expand All @@ -52,6 +58,8 @@ def run(self) -> List[LinterIssue]:
all_libraries: Set[Path] = set()
used_libraries: Set[Path] = set()

self._generate_ld_config_cache()

for elf_file in elf_files:
# Skip linting files listed in the ignore list for the main "library"
# filter.
Expand Down Expand Up @@ -101,6 +109,54 @@ def run(self) -> List[LinterIssue]:

return issues

def _generate_ld_config_cache(self) -> None:
"""Generate a cache of ldconfig output that maps library names to paths."""
# Match lines like:
# libcurl.so.4 (libc6,x86-64) => /lib/x86_64-linux-gnu/libcurl.so.4
# Ignored any architecture in it, may be a problem in the future?
ld_regex = re.compile(r"^\s*(\S+)\s+\(.*\)\s+=>\s+(\S+)$")

try:
output = subprocess.run(
["ldconfig", "-N", "-p"],
check=True,
stdout=subprocess.PIPE,
)
except subprocess.CalledProcessError:
return

for line in output.stdout.decode("UTF-8").splitlines():
match = ld_regex.match(line)
if match:
self._ld_config_cache[match.group(1)] = Path(match.group(2))

def _find_deb_package(self, library_name: str) -> Optional[str]:
"""Find the deb package that provides a library.

:param library_name: The filename of the library to find.

:returns: the corresponding deb package name, or None if the library
is not provided by any system package.
"""
if library_name in self._ld_config_cache:
# Must be resolved to an absolute path for dpkg to find it
library_absolute_path = self._ld_config_cache[library_name].resolve()
try:
output = subprocess.run(
["dpkg", "-S", library_absolute_path.as_posix()],
check=True,
stdout=subprocess.PIPE,
)
except subprocess.CalledProcessError:
# If the specified file doesn't belong to any package, the
# call will trigger an exception.
return None
except FileNotFoundError:
# In case that dpkg isn't available
return None
return output.stdout.decode("UTF-8").split(":", maxsplit=1)[0]
return None

def _check_dependencies_satisfied(
self,
elf_file: ElfFile,
Expand Down Expand Up @@ -132,11 +188,15 @@ def _check_dependencies_satisfied(
if path in dependency.parents:
break
else:
deb_package = self._find_deb_package(dependency.name)
message = f"missing dependency {dependency.name!r}."
if deb_package:
message += f" (provided by '{deb_package}')"
issue = LinterIssue(
name=self._name,
result=LinterResult.WARNING,
filename=str(elf_file.path),
text=f"missing dependency {dependency.name!r}.",
text=message,
url="https://snapcraft.io/docs/linters-library",
)
issues.append(issue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ Running linters...
Running linter: classic
Running linter: library
Lint warnings:
- library: linter-test: missing dependency 'libcaca.so.0'. (https://snapcraft.io/docs/linters-library)
- library: linter-test: missing dependency 'libslang.so.2'. (https://snapcraft.io/docs/linters-library)
- library: linter-test: missing dependency 'libcaca.so.0'. (provided by 'libcaca0') (https://snapcraft.io/docs/linters-library)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not sure it will be easy to correlate provided by with a possible stage-packages entry. Should be fine if the documentation mentions that.

- library: linter-test: missing dependency 'libslang.so.2'. (provided by 'libslang2') (https://snapcraft.io/docs/linters-library)
Creating snap package...
90 changes: 90 additions & 0 deletions tests/unit/linters/test_library_linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,93 @@ def test_is_library_path_directory(mocker):
result = linter._is_library_path(path=Path("/test/dir"))

assert not result


def test_ld_config_cache(fake_process):
"""Check that the ldconfig cache is generated correctly."""
fake_process.register_subprocess(
["ldconfig", "-N", "-p"],
stdout=b"""\
1223 libs found in cache `/etc/ld.so.cache'
libcurl.so.4 (libc6,x86-64) => /lib/x86_64-linux-gnu/libcurl.so.4
libcurl.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libcurl.so
libcrypto.so.3 (libc6,x86-64) => /lib/x86_64-linux-gnu/libcrypto.so.3
libcrypto.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libcrypto.so
libcrypt.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libcrypt.so.1
libcrypt.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libcrypt.so
Cache generated by: ldconfig (Ubuntu GLIBC 2.38-1ubuntu6) stable release version 2.38
""",
returncode=0,
)

linter = LibraryLinter(name="library", snap_metadata=Mock(), lint=None)

linter._generate_ld_config_cache()

assert linter._ld_config_cache == {
"libcurl.so.4": Path("/lib/x86_64-linux-gnu/libcurl.so.4"),
"libcurl.so": Path("/lib/x86_64-linux-gnu/libcurl.so"),
"libcrypto.so.3": Path("/lib/x86_64-linux-gnu/libcrypto.so.3"),
"libcrypto.so": Path("/lib/x86_64-linux-gnu/libcrypto.so"),
"libcrypt.so.1": Path("/lib/x86_64-linux-gnu/libcrypt.so.1"),
"libcrypt.so": Path("/lib/x86_64-linux-gnu/libcrypt.so"),
}


def test_find_deb_package(mocker, fake_process):
"""Sarching a system package that includes a library file"""
mocker.patch(
"snapcraft.linters.library_linter.LibraryLinter._generate_ld_config_cache"
)

fake_process.register_subprocess(
["dpkg", "-S", "/usr/lib/x86_64-linux-gnu/libcurl.so.4"],
stdout=b"libcurl4:amd64: /usr/lib/x86_64-linux-gnu/libcurl.so.4",
)

linter = LibraryLinter(name="library", snap_metadata=Mock(), lint=None)
linter._ld_config_cache = {
"libcurl.so.4": Path("/lib/x86_64-linux-gnu/libcurl.so.4"),
"libcurl.so": Path("/lib/x86_64-linux-gnu/libcurl.so"),
"libcrypto.so.3": Path("/lib/x86_64-linux-gnu/libcrypto.so.3"),
"libcrypto.so": Path("/lib/x86_64-linux-gnu/libcrypto.so"),
"libcrypt.so.1": Path("/lib/x86_64-linux-gnu/libcrypt.so.1"),
"libcrypt.so": Path("/lib/x86_64-linux-gnu/libcrypt.so"),
}

mocker.patch("pathlib.Path.resolve").return_value = Path(
"/usr/lib/x86_64-linux-gnu/libcurl.so.4"
)
result = linter._find_deb_package("libcurl.so.4")
assert result == "libcurl4"


def test_find_deb_package_no_available(mocker, fake_process):
"""Sarching a system package that includes a library file but not found"""
mocker.patch(
"snapcraft.linters.library_linter.LibraryLinter._generate_ld_config_cache"
)

fake_process.register_subprocess(
["dpkg", "-S", "/usr/lib/x86_64-linux-gnu/libcurl.so.4"],
stdout=b"dpkg-query: no path found matching pattern /usr/lib/x86_64-linux-gnu/libcurl.so.4",
returncode=1,
)

linter = LibraryLinter(name="library", snap_metadata=Mock(), lint=None)
linter._ld_config_cache = {
"libcurl.so.4": Path("/lib/x86_64-linux-gnu/libcurl.so.4"),
"libcurl.so": Path("/lib/x86_64-linux-gnu/libcurl.so"),
"libcrypto.so.3": Path("/lib/x86_64-linux-gnu/libcrypto.so.3"),
"libcrypto.so": Path("/lib/x86_64-linux-gnu/libcrypto.so"),
"libcrypt.so.1": Path("/lib/x86_64-linux-gnu/libcrypt.so.1"),
"libcrypt.so": Path("/lib/x86_64-linux-gnu/libcrypt.so"),
}

mocker.patch("pathlib.Path.resolve").return_value = Path(
"/usr/lib/x86_64-linux-gnu/libcurl.so.4"
)

result = linter._find_deb_package("libcurl.so.4")

assert not result
Loading