How can I make a library dependency graph with waf?
Asked Answered
V

2

8

I'd like to generate a simple DOT file when building a C++ project with waf. Ideally I'd like to just use the use and target attributes of the bld command to generate the file. Is this easily injectable into the system?

e.g. This wscript file (just mentioning the parts I'd like to use)

def build(bld):
    bld( use = [ 'lib1',
                 'lib2', ] ,
         target = 'lib3' )

Would produce output of

lib3 -> lib1
lib3 -> lib2

Where would be the best place to inject this behavior?

Thanks!

Virga answered 13/11, 2013 at 16:48 Comment(0)
S
7

You can add add a tool like this easily via the add_post_fun in the build step, something along like this:

from waflib.Errors import WafError
from waflib import Utils

def filter_uses(ctx, uses):
    filtered = []
    for use in uses:
        try:
            ctx.get_tgen_by_name(use)
            filtered.append(use)
        except WafError:
            pass
    return filtered

@Utils.run_once # print only once, even if used in multiple script
def make_dot_file(ctx):
    for group in ctx.groups:
        for taskgen in group:
            uses = Utils.to_list(getattr(taskgen, 'use', []))
            uses = filter_uses(ctx, uses) # Optional, only print TaskGens
            try:
                name = taskgen.name # Sometimes this fails, don't know why
                print "{} -> {}".format(name, ", ".join(uses))
            except AttributeError:
                pass


def build(bld):
    # Build stuff ...
    bld.add_post_fun(make_dot_file)

Note: To get real nice output some more filtering might be useful

Speculum answered 27/3, 2015 at 11:49 Comment(0)
D
5

I improved and adjusted @CK1 idea to my needs. My solution generates a DAG with graphviz and uses helper functions from this article by Matthias Eisen to display dependencies and targets.

The main part of the code looks like this:

import functools
import graphviz as gv

from pathlib import Path
from waflib import Utils

# Make sure that dot.exe is in your system path. I had to do this as
# Graphviz (the program, not the package) is installed with conda. I am
# sure there is a proper way to do this with Waf.
library_bin = Path(sys.executable).parent / 'Library' / 'bin' / 'graphviz'
os.environ['PATH'] += str(library_bin) + ';'


def make_dot_file(ctx):
    # Create DAG
    dag = digraph()

    # Loop over task groups
    for group in ctx.groups:

        # Loop over tasks
        for taskgen in group:
            # Get name and add node for task
            name = taskgen.get_name()
            add_nodes(dag, [name])

            # Add nodes for dependencies and edges to task
            deps = Utils.to_list(getattr(taskgen, 'deps', []))
            for dep in deps:
                dep = Path(dep).name
                add_nodes(dag, [dep])
                add_edges(dag, [(dep, name)])

            # Add nodes for targets and edges to task
            targets = Utils.to_list(getattr(taskgen, 'target', []))
            for target in targets:
                target = Path(target).name
                add_nodes(dag, [target])
                add_edges(dag, [(name, target)])

    # Make the DAG pretty
    dag = apply_styles(dag, styles)

    # Save DAG
    dag.render(<output path of graphic>)

def build(bld):
    # Build stuff ...
    bld.add_post_fun(make_dot_file)

The helper functions used for this example are here:

# -------------------- Start helper functions ----------------------------
graph = functools.partial(gv.Graph, format='png')
digraph = functools.partial(gv.Digraph, format='png')

styles = {
    'graph': {
        'label': 'Pretty Graph',
        'fontsize': '16',
        'fontcolor': 'white',
        'bgcolor': '#333333',
        'rankdir': 'BT',
    },
    'nodes': {
        'fontname': 'Helvetica',
        'shape': 'hexagon',
        'fontcolor': 'white',
        'color': 'white',
        'style': 'filled',
        'fillcolor': '#006699',
    },
    'edges': {
        'style': 'dashed',
        'color': 'white',
        'arrowhead': 'open',
        'fontname': 'Courier',
        'fontsize': '12',
        'fontcolor': 'white',
    }
}


def apply_styles(graph, styles):
    graph.graph_attr.update(
        ('graph' in styles and styles['graph']) or {}
    )
    graph.node_attr.update(
        ('nodes' in styles and styles['nodes']) or {}
    )
    graph.edge_attr.update(
        ('edges' in styles and styles['edges']) or {}
    )
    return graph


def add_nodes(graph, nodes):
    for n in nodes:
        if isinstance(n, tuple):
            graph.node(n[0], **n[1])
        else:
            graph.node(n)
    return graph


def add_edges(graph, edges):
    for e in edges:
        if isinstance(e[0], tuple):
            graph.edge(*e[0], **e[1])
        else:
            graph.edge(*e)
    return graph
# ----------------------- End helper functions -----------------------------
Dispassion answered 15/3, 2018 at 9:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.