Skip to content

Commit

Permalink
Merge branch 'mr/leger/master/fix-dll-closure-check-with-single-file'…
Browse files Browse the repository at this point in the history
… into 'master'

Fix shared libraries closure logic

Closes #17

See merge request it/e3-core!45
  • Loading branch information
grouigrokon committed Oct 21, 2024
2 parents a1f3d30 + 55e583c commit 8b6d115
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 7 deletions.
18 changes: 17 additions & 1 deletion src/e3/anod/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = ""

Expand All @@ -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)
Expand All @@ -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)``
Expand All @@ -507,13 +519,15 @@ 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] = []
errors[lib_file].append(f"\n\t- {name}: {path}")
continue

path = re.sub(" (.*)", "", path)
result[lib_file].append((name, path))

# Make sure a path is defined, we may have lines like::
#
Expand All @@ -538,6 +552,8 @@ def check_shared_libraries_closure(
)
raise AnodError(err_msg)

return result

def load_config_file(
self,
extended: bool = False,
Expand Down
2 changes: 1 addition & 1 deletion src/e3/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
62 changes: 57 additions & 5 deletions tests/tests_e3/anod/spec_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -119,7 +123,7 @@
),
["libc.so.6", "libselinux.so.1"],
),
(("- libpcre2-8.so.0: not found"),),
("- libpcre2-8.so.0: not found",),
),
(
(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -221,18 +225,66 @@ 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(
prefix=None, ignored_libs=ignored, ldd_output=ldd_output
)


# 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:
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit 8b6d115

Please sign in to comment.