diff --git a/snapcraft/linters/library_linter.py b/snapcraft/linters/library_linter.py index a6c5976919..5c84ad3501 100644 --- a/snapcraft/linters/library_linter.py +++ b/snapcraft/linters/library_linter.py @@ -15,6 +15,8 @@ # along with this program. If not, see . """Library linter implementation.""" +import re +import subprocess from pathlib import Path from typing import List, Set @@ -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.""" @@ -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. @@ -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, @@ -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) diff --git a/tests/spread/core22/linters/library-missing/expected_linter_output.txt b/tests/spread/core22/linters/library-missing/expected_linter_output.txt index fc910046c6..1b23411f5d 100644 --- a/tests/spread/core22/linters/library-missing/expected_linter_output.txt +++ b/tests/spread/core22/linters/library-missing/expected_linter_output.txt @@ -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) +- library: linter-test: missing dependency 'libslang.so.2'. (provided by 'libslang2') (https://snapcraft.io/docs/linters-library) Creating snap package... diff --git a/tests/unit/linters/test_library_linter.py b/tests/unit/linters/test_library_linter.py index 57fcca3fe9..683439e0bb 100644 --- a/tests/unit/linters/test_library_linter.py +++ b/tests/unit/linters/test_library_linter.py @@ -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