Skip to content

Commit

Permalink
Add command to draw a dependency graph
Browse files Browse the repository at this point in the history
The new command `fusesoc dep graph` creates a GraphViz .dot file
listing all dependencies for a given core with given flags. This file
can be either manually converted into an image using the `dot` tool
(part of the GraphViz package), or automatically converted with FuseSoC
through the `--output-format` command line option.

Full documentation:

```
$ fusesoc dep graph --help
usage: fusesoc dep graph [-h] [--target TARGET] [--tool TOOL] [--output-format OUTPUT_FORMAT] core

positional arguments:
  core                  Core to build the dependency graph for.

optional arguments:
  -h, --help            show this help message and exit
  --target TARGET       Target to build the dependency graph for.
  --tool TOOL           Tool to build the dependency graph for.
  --output-format OUTPUT_FORMAT
                        Convert output into desired format (requires dot). All dot-supported formats can be used, run 'dot -T?' for a complete list.
```

Based on inital work by Stefan Wallentowitz in olofk#99

Fixes olofk#98
  • Loading branch information
imphil authored and rswarbrick committed Jul 1, 2020
1 parent d21e21c commit 9923913
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 1 deletion.
34 changes: 34 additions & 0 deletions fusesoc/coremanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

from fusesoc.core import Core
from fusesoc.librarymanager import LibraryManager
from fusesoc.vlnv import Vlnv


logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -253,6 +255,38 @@ def get_depends(self, core, flags):
logger.debug(" with dependencies " + ", ".join([str(c.name) for c in deps]))
return deps

def get_dependency_graph(self, core_vlnv, flags):
""" Generate a dependency graph
The graph is represented as flat dict. The key is the core, the value
is a set of dependencies.
"""
is_toplevel = True
work_list = [self.get_core(core_vlnv)]
seen = {core_vlnv}
graph = {}

while work_list:
core = work_list.pop()
deps = core.get_depends({**flags, "is_toplevel": is_toplevel})
is_toplevel = False

dep_names = set()
for dep in deps:
# Find a core object for the dependency name
dep_core = self.get_core(dep)
dep_names.add(dep_core.name)

# Add the dependency to the work list if we haven't seen it
# before
if dep not in seen:
seen.add(dep)
work_list.append(dep_core)

graph[core] = dep_names

return graph

def get_cores(self):
""" Get a dict with all cores, indexed by the core name """
return {str(x.name): x for x in self.db.find()}
Expand Down
81 changes: 80 additions & 1 deletion fusesoc/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from fusesoc.edalizer import Edalizer
from edalize import get_edatool
from fusesoc.vlnv import Vlnv
from fusesoc.utils import Launcher, setup_logging, yaml_fread
from fusesoc.utils import Launcher, setup_logging, yaml_fread, depgraph_to_dot

import logging

Expand Down Expand Up @@ -176,6 +176,56 @@ def init(cm, args):
logger.info("FuseSoC is ready to use!")


def dep_graph(cm, args):
flags = {"target": args.target, "tool": args.tool}
core_vlnv = Vlnv(args.core)

flags_str = ", ".join([k + "=" + v for k, v in flags.items() if v])
print(
"Building dependency graph for {core} with flags {flags}.".format(
core=args.core, flags=flags_str
)
)
core_graph = cm.get_dependency_graph(core_vlnv, flags)

dot_filepath = core_vlnv.sanitized_name + ".dot"
converted_output_filepath = (
core_vlnv.sanitized_name + "." + args.output_format.lower()
)

with open(dot_filepath, "w") as f:
f.write(depgraph_to_dot(core_graph))

if args.output_format != "dot":
dot_cmd = [
"dot",
"-T",
args.output_format,
"-o",
converted_output_filepath,
dot_filepath,
]
try:
subprocess.run(dot_cmd)
except subprocess.CalledProcessError as e:
logger.fatal(
"Unable to call dot to convert dependency graph. "
"Install graphviz (which includes dot) and try again."
)
print(
"Kept graph in a dot file at {}. You can manually convert it "
"by running {}".format(dot_filepath),
" ".join(dot_cmd),
)
sys.exit(1)

# Remove the dot file if the conversion went well.
os.unlink(dot_filepath)
print(
"Successfully written dependency graph to {}.".format(converted_output_filepath)
)


def list_paths(cm, args):
cores_root = [x.location for x in cm.get_libraries()]
print("\n".join(cores_root))
Expand Down Expand Up @@ -802,6 +852,35 @@ def parse_args():
warn="'fusesoc update' is deprecated. Use 'fusesoc library update' instead"
)

# dep subparser
parser_dep = subparsers.add_parser("dep", help="Subcommands dependency management")
dep_subparsers = parser_dep.add_subparsers()
parser_dep.set_defaults(subparser=parser_dep)

# dep graph subparser
parser_dep_graph = dep_subparsers.add_parser(
"graph", help="Produce a dependency graph for GraphViz"
)
parser_dep_graph.add_argument(
"core", help="Core to build the dependency graph for."
)
parser_dep_graph.add_argument(
"--target", help="Target to build the dependency graph for.", default="default"
)
parser_dep_graph.add_argument(
"--tool", help="Tool to build the dependency graph for.", default=""
)
parser_dep_graph.add_argument(
"--output-format",
help=(
"Convert output into desired format (requires dot). "
"All dot-supported formats can be used, run 'dot -T?' for a "
"complete list."
),
default="dot",
)
parser_dep_graph.set_defaults(func=dep_graph)

args = parser.parse_args()

if hasattr(args, "func"):
Expand Down
16 changes: 16 additions & 0 deletions fusesoc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,22 @@ def merge_dict(d1, d2):
d1[key] = merge_dict(d1.get(key, {}), value)
elif isinstance(value, list):
d1[key] = d1.get(key, []) + value
elif isinstance(value, set):
d1[key] = d1.get(key, set()) | value
else:
d1[key] = value
return d1


def depgraph_to_dot(core_graph):
""" Convert a dependency graph into the dot format used by GraphViz
The dependency graph is expected to be in the form produced by
CoreManager.get_dependency_graph().
"""
s = "digraph dependencies {\n"
for core, deps in core_graph.items():
for dep in deps:
s += '"{}"->"{}"\n'.format(core, dep)
s += "}\n"
return s

0 comments on commit 9923913

Please sign in to comment.