Skip to content

Commit

Permalink
Fix tests, update README, unique (#1)
Browse files Browse the repository at this point in the history
Signed-off-by: Christian Heimes <[email protected]>
  • Loading branch information
tiran authored Jun 26, 2024
1 parent e8a4553 commit 253fc0b
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 20 deletions.
67 changes: 66 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,68 @@
# ELF deps

Python implementation of RPM [elfdeps](https://github.com/rpm-software-management/rpm/blob/master/tools/elfdeps.c).
Python implementation of RPM [`elfdeps`](https://github.com/rpm-software-management/rpm/blob/master/tools/elfdeps.c). The `elfdeps` tool can extract dependencies and provides from an ELF binary.

## Example

```shell-session
$ elfdeps --requires /usr/bin/python3.12
libc.so.6(GLIBC_2.34)(64bit)
libc.so.6(GLIBC_2.2.5)(64bit)
libpython3.12.so.1.0()(64bit)
libc.so.6()(64bit)
rtld(GNU_HASH)
$ elfdeps --provides /usr/lib64/libpython3.12.so
libpython3.12.so.1.0()(64bit)
```

```shell-session
$ elfdeps --provides /lib64/libc.so.6
libc.so.6(GLIBC_2.2.5)(64bit)
libc.so.6(GLIBC_2.2.6)(64bit)
libc.so.6(GLIBC_2.3)(64bit)
...
libc.so.6(GLIBC_2.36)(64bit)
libc.so.6(GLIBC_2.38)(64bit)
libc.so.6(GLIBC_ABI_DT_RELR)(64bit)
libc.so.6(GLIBC_PRIVATE)(64bit)
libc.so.6()(64bit)
```

## RPM

In Fedora-based distributions, RPM packages provide and require virtual packages with ELF sonames and versions. The package manager can install virtual provides.

The `python3` base package depends on `libpython3.12.so.1.0()(64bit)` and `libc.so.6(GLIBC_2.34)(64bit)`:

```shell-session
$ rpm -qR python3
libc.so.6()(64bit)
libc.so.6(GLIBC_2.2.5)(64bit)
libc.so.6(GLIBC_2.34)(64bit)
libpython3.12.so.1.0()(64bit)
...
rtld(GNU_HASH)
```

The `python3-libs` package virtually provides `libpython3.12.so.1.0()(64bit)`:

```shell-session
$ rpm -qP python3-libs
bundled(libb2) = 0.98.1
libpython3.12.so.1.0()(64bit)
libpython3.so()(64bit)
python-libs = 3.12.3-2.fc39
python3-libs = 3.12.3-2.fc39
python3-libs(x86-64) = 3.12.3-2.fc39
python3.12-libs = 3.12.3-2.fc39
```

```shell-session
$ sudo dnf install 'libc.so.6(GLIBC_2.34)(64bit)' 'libpython3.12.so.1.0()(64bit)'
Package glibc-2.38-18.fc39.x86_64 is already installed.
Package python3-libs-3.12.3-2.fc39.x86_64 is already installed.
Dependencies resolved.
Nothing to do.
Complete!
```
15 changes: 14 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand Down Expand Up @@ -70,7 +69,21 @@ select = [
"TID", # flake8-tidy-imports
]
ignore = [
"UP006", # not compatible with 3.9 type annotations
]

[tool.ruff.lint.isort]
known-first-party = ["elfdeps"]

[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true

[[tool.mypy.overrides]]
module = ["elfdeps.*"]
disallow_untyped_defs = true

[[tool.mypy.overrides]]
module = ["elftools.*"]
ignore_missing_imports = true
11 changes: 10 additions & 1 deletion src/elfdeps/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import argparse
import pathlib
import pprint
import typing

from ._elfdeps import ELFDeps

Expand Down Expand Up @@ -46,16 +47,24 @@
dest="require_interp",
help="Include ELF interpreter name",
)
parser.add_argument(
"--unique",
"-u",
action="store_true",
dest="unique",
help="Remove duplicate entries",
)


def main(argv=None):
def main(argv: typing.Optional[typing.List[str]] = None) -> None:
args = parser.parse_args(argv)
e = ELFDeps(
args.filename,
soname_only=args.soname_only,
fake_soname=args.fake_soname,
filter_soname=args.filter_soname,
require_interp=args.require_interp,
unique=args.unique,
)
if args.provides:
for p in e.info.provides:
Expand Down
43 changes: 29 additions & 14 deletions src/elfdeps/_elfdeps.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
import os
import pathlib
import stat
import typing

from elftools.elf.constants import VER_FLAGS
from elftools.elf.dynamic import DynamicSection
from elftools.elf.elffile import ELFFile
from elftools.elf.gnuversions import GNUVerDefSection, GNUVerNeedSection


@dataclasses.dataclass(slots=True, frozen=True)
@dataclasses.dataclass(frozen=True, order=True)
class SOInfo:
"""Shared object information"""

Expand All @@ -38,16 +39,19 @@ def __repr__(self) -> str:

@dataclasses.dataclass
class ELFInfo:
# requires and provides are ordered by occurence in ELF metadata and
# can contain duplicate entries. The order can be different than output
# of elfdeps.c, but that usually does not matter.
requires: list[SOInfo]
provides: list[SOInfo]
machine: str | None = None
machine: typing.Optional[str] = None
is_dso: bool = False
is_exec: bool = False
got_debug: bool = False
got_hash: bool = False
got_gnuhash: bool = False
soname: str | None = None
interp: str | None = None
soname: typing.Optional[str] = None
interp: typing.Optional[str] = None
marker: str = ""


Expand Down Expand Up @@ -79,6 +83,7 @@ def __init__(
fake_soname: bool = True,
filter_soname: bool = False,
require_interp: bool = False,
unique: bool = True,
) -> None:
if not isinstance(filename, pathlib.Path):
raise TypeError(f"filename is not a pathlib.Path: {type(filename)}")
Expand All @@ -87,11 +92,13 @@ def __init__(
self.fake_soname = fake_soname
self.filter_soname = filter_soname
self.require_interp = require_interp
self.unique = unique

self.info = ELFInfo(
requires=[],
provides=[],
)
self._seen: typing.Set[typing.Tuple[bool, SOInfo]] = set()
with self.filename.open("rb") as f:
self._process_file(f)

Expand Down Expand Up @@ -137,23 +144,30 @@ def _process_file(self, f: io.BufferedReader) -> None:
# direct add
self.info.requires.append(SOInfo(self.info.interp, version="", marker=""))

def _add_soinfo(self, provides: bool, soname: str, version: str | None) -> bool:
def _add_soinfo(
self, provides: bool, soname: str, version: typing.Optional[str]
) -> None:
if skip_soname(soname, filter_soname=self.filter_soname):
return False
return
version = version if version else ""
marker = self.info.marker or ""
soinfo = SOInfo(soname, version, marker)

key = (provides, soinfo)
if self.unique and key in self._seen:
return
self._seen.add(key)

if provides:
self.info.provides.append(soinfo)
else:
self.info.requires.append(soinfo)
return True

def add_provides(self, soname: str, version: str | None = None) -> bool:
return self._add_soinfo(True, soname, version)
def add_provides(self, soname: str, version: typing.Optional[str] = None) -> None:
self._add_soinfo(True, soname, version)

def add_requires(self, soname: str, version: str | None = None) -> bool:
return self._add_soinfo(False, soname, version)
def add_requires(self, soname: str, version: typing.Optional[str] = None) -> None:
self._add_soinfo(False, soname, version)

@property
def gen_requires(self) -> bool:
Expand Down Expand Up @@ -200,7 +214,7 @@ def process_verdef(self, sec: GNUVerDefSection) -> None:
processVerDef(Elf_Scn *scn, GElf_Shdr *shdr, elfInfo *ei)
"""
soname: str | None = None
soname: typing.Optional[str] = None
for verdef, vernaux in sec.iter_versions():
for aux in vernaux:
if not aux.name:
Expand Down Expand Up @@ -242,12 +256,13 @@ def process_dynamic(self, sec: DynamicSection) -> None:
elif d_tag == "DT_NEEDED":
self.add_requires(tag.needed)

def process_prog_headers(self, elffile: ELFFile) -> str | None:
def process_prog_headers(self, elffile: ELFFile) -> typing.Optional[str]:
"""Get interpreter from PT_INTERP segment
void processProgHeaders(elfInfo *ei, GElf_Ehdr *ehdr)
"""
for seg in elffile.iter_segments("PT_INTERP"):
return seg.get_interp_name()
interp: str = seg.get_interp_name()
return interp
else:
return None
13 changes: 10 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
# SPDX-License-Identifier: Apache-2.0

[tox]
envlist=py{9,10,11,12},lint
envlist=py3{9,10,11,12},lint

[testenv]
package = wheel
wheel_build_env = pkg
extras = test
commands =
python -m pytest tests

[testenv:lint]
deps=
basepython = python3
deps =
ruff
mypy
pytest-stub
commands =
ruff check src tests
ruff format --check src tests
mypy src tests
skip_install = true
skip_sdist = true

[testenv:fix]
deps=
basepython = python3
deps =
ruff
commands =
ruff format src tests
Expand Down

0 comments on commit 253fc0b

Please sign in to comment.