Skip to content

Commit c9e395e

Browse files
committed
Add a simple script for mapping linker dependencies
This only supports FreeBSD currently. The graphviz portion is split from the linker dependency enumeration portion because the host where the linker dependencies are enumerated may not have easy access to graphviz and its associated libraries.
1 parent b8d424d commit c9e395e

File tree

6 files changed

+159
-0
lines changed

6 files changed

+159
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include graph_linker_dependencies/templates/*.pyt
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env python3
2+
3+
import platform
4+
import sys
5+
from setuptools import find_packages
6+
from setuptools import setup
7+
8+
9+
osname = platform.system().lower()
10+
if osname not in "freebsd":
11+
sys.exit("This package only works on FreeBSD.")
12+
13+
GLD = "graph_linker_dependencies"
14+
15+
setup(
16+
name="graph_linker_dependencies",
17+
version="0.1",
18+
description="Tool for graphing FreeBSD linker dependencies.",
19+
author="Enji Cooper",
20+
author_email="yaneurabeya@gmail.com",
21+
url="https://github.com/ngie-eign/scratch",
22+
include_package_data=True,
23+
packages=find_packages(where="src"),
24+
package_data={f"{GLD}": ["templates/*.pyt"]},
25+
package_dir={"": "src"},
26+
entry_points={
27+
"console_scripts": [
28+
f"create_link_dependency_graph={GLD}.create_link_dependency_graph:main"
29+
]
30+
},
31+
requirements=["jinja2"],
32+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import pkg_resources
2+
pkg_resources.declare_namespace(__name__)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env python
2+
3+
import argparse
4+
import collections
5+
import os
6+
import pathlib
7+
8+
# import pprint
9+
import pkg_resources
10+
import re
11+
import subprocess
12+
13+
from jinja2 import Environment
14+
from jinja2 import FileSystemLoader
15+
from jinja2 import Template
16+
from jinja2 import select_autoescape
17+
18+
19+
LIBDEPS_CACHE = collections.defaultdict(list)
20+
LDCONFIG_HINT_RE = re.compile(r".+\s+=>\s+(.+)")
21+
NEEDED_SHLIB_RE = re.compile(r".+NEEDED\s+Shared library: \[(.+)\]")
22+
23+
24+
def build_library_path_cache():
25+
ldconfig_hints_map = {}
26+
27+
all_ldconfig_hints = subprocess.check_output(["ldconfig", "-r"], text=True)
28+
for ldconfig_hint in all_ldconfig_hints.splitlines(False):
29+
matches = LDCONFIG_HINT_RE.match(ldconfig_hint)
30+
if matches is None:
31+
continue
32+
33+
so_full = so_split = matches.group(1)
34+
ldconfig_hints_map[so_full] = so_full
35+
36+
while True:
37+
so_short = os.path.basename(so_split)
38+
ldconfig_hints_map[so_short] = ldconfig_hints_map[so_split] = so_full
39+
so_split, ext = os.path.splitext(so_split)
40+
if ext == ".so":
41+
break
42+
43+
return ldconfig_hints_map
44+
45+
46+
def find_library_dependencies(library_, ldconfig_hints_map, libdep_cache):
47+
if library_ in libdep_cache:
48+
return
49+
50+
lib_path = ldconfig_hints_map[library_]
51+
52+
readelf_lines = subprocess.check_output(["readelf", "-d", lib_path], text=True)
53+
for readelf_line in readelf_lines.splitlines(False):
54+
matches = NEEDED_SHLIB_RE.match(readelf_line)
55+
if matches is None:
56+
continue
57+
libdep = matches.group(1)
58+
libdep_cache[library_].append(libdep)
59+
find_library_dependencies(libdep, ldconfig_hints_map, libdep_cache)
60+
61+
62+
def main(argv=None):
63+
parser = argparse.ArgumentParser()
64+
parser.add_argument("library")
65+
parser.add_argument("--graph-generator-file")
66+
parser.add_argument("--graph-output-file")
67+
args = parser.parse_args(args=argv)
68+
69+
libdep_cache = collections.defaultdict(list)
70+
71+
ldconfig_hints_map = build_library_path_cache()
72+
73+
library_full_path = str(pathlib.Path(args.library).resolve())
74+
assert (
75+
library_full_path in ldconfig_hints_map
76+
), f"{library_full_path} not found in ldconfig cache"
77+
78+
find_library_dependencies(library_full_path, ldconfig_hints_map, libdep_cache)
79+
80+
env = Environment(
81+
loader=FileSystemLoader(
82+
pkg_resources.resource_filename(
83+
"graph_linker_dependencies",
84+
"templates",
85+
)
86+
),
87+
autoescape=select_autoescape(),
88+
)
89+
template = env.get_template("libdep_template.pyt")
90+
91+
library_name = pathlib.Path(args.library).stem
92+
graph_py_filename = (
93+
args.graph_generator_file or f"graph_{library_name}_dependencies.py"
94+
)
95+
graph_output_file = (
96+
args.graph_output_file or f"{library_name}_dependencies_graph.png"
97+
)
98+
with open(graph_py_filename, "w") as filep:
99+
filep.write(
100+
template.render(
101+
libdep_cache=libdep_cache, output_file=args.graph_output_file
102+
)
103+
)
104+
print(f"Please run {graph_py_filename} on host where python-graphviz is installed.")
105+
106+
107+
if __name__ == "__main__":
108+
main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import pkg_resources
2+
pkg_resources.declare_namespace(__name__)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env python
2+
3+
try:
4+
import graphviz
5+
except ImportError:
6+
warnings.warn("This script requires python-graphviz")
7+
raise
8+
9+
dot = graphviz.Digraph("libdependencies-graph", comment="Library Dependencies Graph")
10+
11+
{% for library, dependencies in libdep_cache.items() %}dot.node("{{library}}")
12+
{% for dependency in dependencies %}dot.edge("{{library}}", "{{dependency}}")
13+
{% endfor %}{% endfor %}
14+
dot.render(format="png", outfile="{{output_file}}")

0 commit comments

Comments
 (0)