Skip to content

Commit

Permalink
Add a simple script for mapping linker dependencies
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ngie-eign committed May 10, 2023
1 parent b8d424d commit c9e395e
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 0 deletions.
1 change: 1 addition & 0 deletions tools/graph-linker-dependencies/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include graph_linker_dependencies/templates/*.pyt
32 changes: 32 additions & 0 deletions tools/graph-linker-dependencies/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env python3

import platform
import sys
from setuptools import find_packages
from setuptools import setup


osname = platform.system().lower()
if osname not in "freebsd":
sys.exit("This package only works on FreeBSD.")

GLD = "graph_linker_dependencies"

setup(
name="graph_linker_dependencies",
version="0.1",
description="Tool for graphing FreeBSD linker dependencies.",
author="Enji Cooper",
author_email="[email protected]",
url="https://github.com/ngie-eign/scratch",
include_package_data=True,
packages=find_packages(where="src"),
package_data={f"{GLD}": ["templates/*.pyt"]},
package_dir={"": "src"},
entry_points={
"console_scripts": [
f"create_link_dependency_graph={GLD}.create_link_dependency_graph:main"
]
},
requirements=["jinja2"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import pkg_resources
pkg_resources.declare_namespace(__name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python

import argparse
import collections
import os
import pathlib

# import pprint
import pkg_resources
import re
import subprocess

from jinja2 import Environment
from jinja2 import FileSystemLoader
from jinja2 import Template
from jinja2 import select_autoescape


LIBDEPS_CACHE = collections.defaultdict(list)
LDCONFIG_HINT_RE = re.compile(r".+\s+=>\s+(.+)")
NEEDED_SHLIB_RE = re.compile(r".+NEEDED\s+Shared library: \[(.+)\]")


def build_library_path_cache():
ldconfig_hints_map = {}

all_ldconfig_hints = subprocess.check_output(["ldconfig", "-r"], text=True)
for ldconfig_hint in all_ldconfig_hints.splitlines(False):
matches = LDCONFIG_HINT_RE.match(ldconfig_hint)
if matches is None:
continue

so_full = so_split = matches.group(1)
ldconfig_hints_map[so_full] = so_full

while True:
so_short = os.path.basename(so_split)
ldconfig_hints_map[so_short] = ldconfig_hints_map[so_split] = so_full
so_split, ext = os.path.splitext(so_split)
if ext == ".so":
break

return ldconfig_hints_map


def find_library_dependencies(library_, ldconfig_hints_map, libdep_cache):
if library_ in libdep_cache:
return

lib_path = ldconfig_hints_map[library_]

readelf_lines = subprocess.check_output(["readelf", "-d", lib_path], text=True)
for readelf_line in readelf_lines.splitlines(False):
matches = NEEDED_SHLIB_RE.match(readelf_line)
if matches is None:
continue
libdep = matches.group(1)
libdep_cache[library_].append(libdep)
find_library_dependencies(libdep, ldconfig_hints_map, libdep_cache)


def main(argv=None):
parser = argparse.ArgumentParser()
parser.add_argument("library")
parser.add_argument("--graph-generator-file")
parser.add_argument("--graph-output-file")
args = parser.parse_args(args=argv)

libdep_cache = collections.defaultdict(list)

ldconfig_hints_map = build_library_path_cache()

library_full_path = str(pathlib.Path(args.library).resolve())
assert (
library_full_path in ldconfig_hints_map
), f"{library_full_path} not found in ldconfig cache"

find_library_dependencies(library_full_path, ldconfig_hints_map, libdep_cache)

env = Environment(
loader=FileSystemLoader(
pkg_resources.resource_filename(
"graph_linker_dependencies",
"templates",
)
),
autoescape=select_autoescape(),
)
template = env.get_template("libdep_template.pyt")

library_name = pathlib.Path(args.library).stem
graph_py_filename = (
args.graph_generator_file or f"graph_{library_name}_dependencies.py"
)
graph_output_file = (
args.graph_output_file or f"{library_name}_dependencies_graph.png"
)
with open(graph_py_filename, "w") as filep:
filep.write(
template.render(
libdep_cache=libdep_cache, output_file=args.graph_output_file
)
)
print(f"Please run {graph_py_filename} on host where python-graphviz is installed.")


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import pkg_resources
pkg_resources.declare_namespace(__name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env python

try:
import graphviz
except ImportError:
warnings.warn("This script requires python-graphviz")
raise

dot = graphviz.Digraph("libdependencies-graph", comment="Library Dependencies Graph")

{% for library, dependencies in libdep_cache.items() %}dot.node("{{library}}")
{% for dependency in dependencies %}dot.edge("{{library}}", "{{dependency}}")
{% endfor %}{% endfor %}
dot.render(format="png", outfile="{{output_file}}")

0 comments on commit c9e395e

Please sign in to comment.