diff --git a/src/e3/anod/spec.py b/src/e3/anod/spec.py index df9c7bdd..113ddf25 100644 --- a/src/e3/anod/spec.py +++ b/src/e3/anod/spec.py @@ -412,7 +412,7 @@ def check_shared_libraries_closure( ignored_libs: list[str] | None = None, ldd_output: str | None = None, case_sensitive: bool | None = None, - ) -> None: + ) -> dict[str, list[tuple[str, str]]]: """Sanity check the shared library closure. Make sure that the libraries only depend on authorized system shared @@ -446,12 +446,17 @@ def check_shared_libraries_closure( value in *ignored_libs* should be case-sensitive or not. If ``None``, the comparison is case-sensitive, but on Windows hosts. + :return: A dictionary, where keys are the analyzed files, and the + values are the shared libraries linked to that file (a tuple made + of the file name and its path). + :raise AnodError: if some of the shared libraries in *prefix* (or in *ldd_output*) is not in the same directory as the analysed element. :raise FileNotFoundError: if *ldd_output* is not provided and ``ldd`` application cannot be found in the ``PATH``. """ # noqa RST304 ignored: list[str] = ignored_libs or [] + result: dict[str, list[tuple[str, str]]] = {} errors: dict[str, list[str]] = {} lib_file: str = "" @@ -473,6 +478,11 @@ def check_shared_libraries_closure( root=prefix, pattern=f"*{OS_INFO[e3.env.Env().build.os.name]['dllext']}" ) ldd_output = e3.os.process.Run(["ldd"] + lib_files).out or "" + + # When only one path is specified, ldd does not add the name of the + # file in the output. This would break the parsing below. + if len(lib_files) == 1: + ldd_output = f"{lib_files[0]}:\n{ldd_output}" else: # An ldd output has been provided. lib_files = re.findall(r"^([^\t].*):$", ldd_output, flags=re.M) @@ -492,6 +502,8 @@ def check_shared_libraries_closure( # Line is like ``/mypath/mydll.so:``, it's the result of # ldd for ``mydll.so``. lib_file = line[:-1] + if lib_file not in result: + result[lib_file] = [] elif lib_file and " => " in line: # Line looks like: # `` otherdll.so => /other/otherdll.so (0xabcd)`` @@ -507,6 +519,7 @@ def check_shared_libraries_closure( # Make sure there are no "not found" errors if "not found" in line.lower(): + result[lib_file].append((name, path)) if not in_ignored: if lib_file not in errors: errors[lib_file] = [] @@ -514,6 +527,7 @@ def check_shared_libraries_closure( continue path = re.sub(" (.*)", "", path) + result[lib_file].append((name, path)) # Make sure a path is defined, we may have lines like:: # @@ -538,6 +552,8 @@ def check_shared_libraries_closure( ) raise AnodError(err_msg) + return result + def load_config_file( self, extended: bool = False, diff --git a/src/e3/pytest.py b/src/e3/pytest.py index ff01dc10..9078d00a 100644 --- a/src/e3/pytest.py +++ b/src/e3/pytest.py @@ -227,7 +227,7 @@ def fix_coverage_paths(origin_dir: str, new_dir: str, cov_db: str) -> None: if "got an unexpected keyword argument 'map_path'" in str(te): # Try with the old API ... # noinspection PyArgumentList - new_coverage_data.update(old_coverage_data, aliases=paths) + new_coverage_data.update(old_coverage_data, aliases=paths) # type: ignore[call-arg] else: raise te new_coverage_data.write() diff --git a/tests/tests_e3/anod/spec_test.py b/tests/tests_e3/anod/spec_test.py index 13678b5c..667c42b7 100644 --- a/tests/tests_e3/anod/spec_test.py +++ b/tests/tests_e3/anod/spec_test.py @@ -8,6 +8,10 @@ from e3.anod.error import AnodError, SpecError from e3.anod.sandbox import SandBox from e3.anod.spec import Anod, __version__, check_api_version, has_primitive +from e3.env import Env +from e3.fs import cp +from e3.os.process import Run +from e3.platform_db.knowledge_base import OS_INFO import pytest @@ -119,7 +123,7 @@ ), ["libc.so.6", "libselinux.so.1"], ), - (("- libpcre2-8.so.0: not found"),), + ("- libpcre2-8.so.0: not found",), ), ( ( @@ -180,7 +184,7 @@ def build(self): # noinspection PyUnusedLocal @pytest.mark.parametrize("arguments,expected", CHECK_DLL_CLOSURE_ARGUMENTS) -def test_spec_check_dll_closure(ldd, arguments: tuple, expected: tuple) -> None: +def test_spec_check_dll_closure(ldd, arguments: tuple, expected: tuple) -> None: # type: ignore[no-untyped-def] """Create a simple spec with dependency to python and run dll closure.""" ldd_output, ignored = arguments (errors,) = expected @@ -221,11 +225,11 @@ def test_spec_check_dll_closure(ldd, arguments: tuple, expected: tuple) -> None: else: raise ae elif errors: - with pytest.raises(AnodError) as ae: + with pytest.raises(AnodError) as anod_error: test_spec.check_shared_libraries_closure( prefix=None, ignored_libs=ignored, ldd_output=ldd_output ) - assert errors in ae.value.args[0] + assert errors in anod_error.value.args[0] else: # There is an ldd_output, but no errors may be raised on unix hosts. test_spec.check_shared_libraries_closure( @@ -233,6 +237,54 @@ def test_spec_check_dll_closure(ldd, arguments: tuple, expected: tuple) -> None: ) +# noinspection PyUnusedLocal +def test_spec_check_dll_closure_single_file(ldd) -> None: # type: ignore[no-untyped-def] + """Create a simple spec with dependency to python and run dll closure.""" + name: str | None = None + path: str | None = None + + test_spec: Anod = Anod("", kind="install") + test_spec.sandbox = SandBox(root_dir=os.getcwd()) + + # Get the ldd output of the current executable. + ldd_output = Run(["ldd"] + [sys.executable]).out or "" + + # Extract the first dll with a path from the ldd output. + for line in ldd_output.splitlines(): + if " => " in line: + name, path = line.strip().split(" => ", 1) + # Remove the load address from the file path. + path = path.split("(")[0].strip() + break + + if name is None or path is None: + # Skip test. + pytest.skip("No shared library to analyse") + + # Copy that file in here. As the share lib may have a name like + # my_shlib.so.1.0, better rename it simply my_shlib.so. + shlib_ext: str = f"{OS_INFO[Env().build.os.name]['dllext']}" + prefix: Path = Path(Path.cwd(), "prefix") + name = name.split(shlib_ext)[0] + shlib_ext + shlib_path: str = Path(prefix, name).as_posix() + prefix.mkdir() + cp(path, shlib_path) + + # And now run check_shared_libraries_closure() on that shared library. + # As we do not define exceptions (ignored system libraries), and that the + # library may link with system libraries, take all possibilities into + # account: + # - exception: the analysis was ok, since it detected system libraries + # - result: make sure there is only one element in the result + + try: + result = test_spec.check_shared_libraries_closure(prefix=str(prefix)) + assert len(result) == 1 + assert Path(sys.executable).as_posix() in result + except AnodError as ae: + assert shlib_path in ae.messages[0] + + def test_spec_wrong_dep(): """Check exception message when wrong dependency is set.""" with pytest.raises(SpecError) as err: @@ -335,7 +387,7 @@ class GeneratorDisabled(Anod): def test_missing_property(): class NoProperty(Anod): - def source_pkg_build(self) -> list: + def source_pkg_build(self) -> list: # type: ignore[override] return [] noproperty = NoProperty(qualifier="", kind="source")