Skip to content

Commit e08bac2

Browse files
authored
Major rework (#2)
- drop 3.9 support - make `_ELFDeps` class internal - support streaming IO, zipfile, and tarfile - move settings to `ELFAnalyzeSettings` Signed-off-by: Christian Heimes <christian@python.org>
1 parent 253fc0b commit e08bac2

File tree

10 files changed

+317
-91
lines changed

10 files changed

+317
-91
lines changed

.github/workflows/ci.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ jobs:
1616
strategy:
1717
matrix:
1818
python:
19-
- "3.9"
2019
- "3.10"
2120
- "3.11"
2221
- "3.12"

.github/workflows/pypi.yaml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,17 @@
44
name: Build, test, and upload PyPI package
55

66
on:
7-
- push
8-
- pull_request
9-
- release
7+
push:
8+
branches:
9+
- main
10+
tags:
11+
- "v*"
12+
pull_request:
13+
branches:
14+
- main
15+
release:
16+
types:
17+
- published
1018

1119
permissions:
1220
contents: read

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,14 @@ Dependencies resolved.
6666
Nothing to do.
6767
Complete!
6868
```
69+
70+
## Public API
71+
72+
* dataclass `elfdeps.ELFAnalyzeSettings`
73+
* exception `elfdeps.ELFError`
74+
* dataclass `elfdeps.ELFInfo`
75+
* dataclass `elfdeps.SOInfo`
76+
* `elfdeps.analyze_elffile(elffile, *, filename, is_exec, settings=None) -> ELFInfo`
77+
* `elfdeps.analyze_file(filename, *, settings=None) -> ELFInfo`
78+
* `elfdeps.analyze_tarmember(tfile, tarinfo, *, settings=None) -> ELFInfo`
79+
* `elfdeps.analyze_zipmember(zfile, zipinfo, *, settings=None) -> ELFInfo`

pyproject.toml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ classifiers = [
2828
"Programming Language :: Python :: Implementation :: CPython",
2929
"Topic :: Utilities",
3030
]
31-
requires-python = ">=3.9"
31+
requires-python = ">=3.10"
3232
dependencies = [
3333
"pyelftools",
3434
]
@@ -54,8 +54,9 @@ line-length = 88
5454

5555
[tool.ruff.lint]
5656
# Allow fix for all enabled rules (when `--fix`) is provided.
57-
fixable = ["ALL"]
57+
fixable = ["ALL", "UP006"]
5858
unfixable = []
59+
extend-safe-fixes = ["UP006", "UP007"]
5960
select = [
6061
"B", # flake8-bugbear
6162
"E", # pycodestyle
@@ -68,15 +69,12 @@ select = [
6869
"UP", # pyupgrade
6970
"TID", # flake8-tidy-imports
7071
]
71-
ignore = [
72-
"UP006", # not compatible with 3.9 type annotations
73-
]
7472

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

7876
[tool.mypy]
79-
python_version = "3.9"
77+
python_version = "3.10"
8078
warn_return_any = true
8179
warn_unused_configs = true
8280

src/elfdeps/__init__.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# SPDX-License-Identifier: Apache-2.0
22

3-
__all__ = ("ELFDeps",)
3+
__all__ = (
4+
"ELFAnalyzeSettings",
5+
"ELFError",
6+
"ELFInfo",
7+
"SOInfo",
8+
"analyze_elffile",
9+
"analyze_file",
10+
"analyze_tarmember",
11+
"analyze_zipmember",
12+
)
413

5-
from ._elfdeps import ELFDeps
14+
from elftools.common.exceptions import ELFError
15+
16+
from ._archives import (
17+
analyze_tarmember,
18+
analyze_zipmember,
19+
)
20+
from ._elfdeps import (
21+
ELFAnalyzeSettings,
22+
ELFInfo,
23+
SOInfo,
24+
analyze_elffile,
25+
analyze_file,
26+
)

src/elfdeps/__main__.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
import argparse
44
import pathlib
55
import pprint
6+
import tarfile
67
import typing
8+
import zipfile
79

8-
from ._elfdeps import ELFDeps
10+
from . import _archives, _elfdeps
11+
12+
ZIPEXT = frozenset({".zip", ".whl"})
13+
TAREXT = frozenset({".tar", ".tar.gz", ".tgz", ".tar.bz2", "tbz2", "tar.xz", "txz"})
914

1015
parser = argparse.ArgumentParser("elfdeps")
1116
parser.add_argument("filename", type=pathlib.Path)
@@ -56,24 +61,55 @@
5661
)
5762

5863

59-
def main(argv: typing.Optional[typing.List[str]] = None) -> None:
64+
def zip_requires(
65+
filename: pathlib.Path, settings: _elfdeps.ELFAnalyzeSettings
66+
) -> typing.Iterable[_elfdeps.ELFInfo]:
67+
with zipfile.ZipFile(filename) as zf:
68+
for zipinfo in zf.infolist():
69+
if zipinfo.filename.endswith(".so"):
70+
yield _archives.analyze_zipmember(zf, zipinfo, settings=settings)
71+
72+
73+
def tar_requires(
74+
filename: pathlib.Path, settings: _elfdeps.ELFAnalyzeSettings
75+
) -> typing.Iterable[_elfdeps.ELFInfo]:
76+
with tarfile.TarFile.open(filename, mode="r:*") as tf:
77+
for tarinfo in tf.getmembers():
78+
if tarinfo.name.endswith(".so"):
79+
yield _archives.analyze_tarmember(tf, tarinfo, settings=settings)
80+
81+
82+
def main(argv: list[str] | None = None) -> None:
6083
args = parser.parse_args(argv)
61-
e = ELFDeps(
62-
args.filename,
84+
settings = _elfdeps.ELFAnalyzeSettings(
6385
soname_only=args.soname_only,
6486
fake_soname=args.fake_soname,
6587
filter_soname=args.filter_soname,
6688
require_interp=args.require_interp,
6789
unique=args.unique,
6890
)
91+
filename: pathlib.Path = args.filename
92+
if filename.suffix in ZIPEXT:
93+
infos = list(zip_requires(filename, settings=settings))
94+
elif filename.suffix in TAREXT:
95+
infos = list(tar_requires(filename, settings=settings))
96+
else:
97+
infos = [_elfdeps.analyze_file(filename, settings=settings)]
6998
if args.provides:
70-
for p in e.info.provides:
99+
provides = set()
100+
for info in infos:
101+
provides.update(info.provides)
102+
for p in sorted(provides):
71103
print(p)
72-
if args.requires:
73-
for r in e.info.requires:
104+
elif args.requires:
105+
requires = set()
106+
for info in infos:
107+
requires.update(info.requires)
108+
for r in sorted(requires):
74109
print(r)
75-
if not args.requires and not args.provides:
76-
pprint.pprint(e.info)
110+
else:
111+
for info in infos:
112+
pprint.pprint(info)
77113

78114

79115
if __name__ == "__main__":

src/elfdeps/_archives.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""Analyze archive members"""
3+
4+
import pathlib
5+
import stat
6+
import tarfile
7+
import typing
8+
import zipfile
9+
10+
from elftools.elf.elffile import ELFFile
11+
12+
from ._elfdeps import ELFAnalyzeSettings, ELFInfo, analyze_elffile
13+
14+
15+
def analyze_zipmember(
16+
zfile: zipfile.ZipFile,
17+
zipinfo: zipfile.ZipInfo,
18+
*,
19+
settings: ELFAnalyzeSettings | None = None,
20+
) -> ELFInfo:
21+
"""Analyze a zipfile member"""
22+
mode = zipinfo.external_attr >> 16
23+
is_exec = bool(mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH))
24+
filename = pathlib.Path(zipinfo.filename)
25+
with zfile.open(zipinfo, mode="r") as f:
26+
elffile = ELFFile(f)
27+
return analyze_elffile(
28+
elffile, filename=filename, is_exec=is_exec, settings=settings
29+
)
30+
31+
32+
def analyze_tarmember(
33+
tfile: tarfile.TarFile,
34+
tarinfo: tarfile.TarInfo,
35+
*,
36+
settings: ELFAnalyzeSettings | None = None,
37+
) -> ELFInfo:
38+
"""Analze a tarfile member"""
39+
mode = tarinfo.mode
40+
is_exec = bool(mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH))
41+
filename = pathlib.Path(tarinfo.name)
42+
f = tfile.extractfile(tarinfo)
43+
if typing.TYPE_CHECKING:
44+
assert f is not None
45+
with f:
46+
elffile = ELFFile(f)
47+
return analyze_elffile(
48+
elffile, filename=filename, is_exec=is_exec, settings=settings
49+
)

0 commit comments

Comments
 (0)