11
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

`Makefile`の依存関係の可視化

Last updated at Posted at 2014-09-09

LANG=C make -pによって出力されたMakefileのデータベースをパースして,依存関係をDOT形式で出力します.
再帰的なmakeの呼び出しにも対応しています.
GNU Makeを仮定しています.

LANG=C make -p | python3 make_p_to_json.py | python3 json_to_dot.py | dot -Tpdf >| workflow.pdf

のように利用します.
たとえば,sjackman/uniqtag-paperに適用すると,以下のようなPDFが得られます.

workflow.png

make_p_to_json.py

Makefileに記述された依存関係を

[{"target1": ["dep1", "dep2", ...], ...}, ...]

という構造のJSON形式で出力します.
一番外側の配列の各要素は,makeを再帰的に呼び出している場合に対応します.

LANG=C make -p | python3 make_p_to_json.py > graph.json

の様に利用します.

#!/usr/bin/python

import sys
import json
import re


def main(args):
    _parse_args(args)
    json.dump(parse_make_p(sys.stdin), sys.stdout)


def parse_make_p(fp, graphs=None):
    if graphs is None:
        graphs = []
    for l in fp:
        if l.startswith('# Make data base, printed on '):
            graphs.append(_parse_db(fp))
    if not graphs:
        raise ValueError("{} seems not connected to `LANG=C make -p`".format(fp))
    return graphs


def _parse_db(fp):
    for l in fp:
        if l.startswith('# Files'):
            fp.readline() # skip the first empty line
            return _parse_entries(fp)
    return {}


def _parse_entries(fp):
    deps_graph = {}
    for l in fp:
        if l.startswith('# files hash-table stats:'):
            return deps_graph
        elif l.startswith('# Not a target:'):
            _skip_until_next_entry(fp)
        elif l.startswith("# makefile (from '"):
            fp.readline() # skip information on target specific variable value
        else:
            _parse_entry(l, deps_graph)
            _skip_until_next_entry(fp)
    return deps_graph



TARGET_SPLIT_REGEX = re.compile(r':{1,2} *')
def _parse_entry(l, deps_graph):
    target, deps = TARGET_SPLIT_REGEX.split(l, 1)
    deps_graph[target] = [dep for dep in deps.split() if dep != '|']


def _skip_until_next_entry(fp):
    for l in fp:
        if _is_new_entry(l):
            return


def _is_new_entry(s):
    return s.startswith('\n')


def _parse_args(args):
    if len(args) != 1:
        print("# parse Makefile's database and print dependency graph in JSON format")
        print("LANG=C gmake -p | {} | json_to_dot.py | dot -Tpdf >| workflow.pdf".format(args[0]))
        sys.exit(1)


if __name__ == '__main__':
    main(sys.argv)

json_to_dot.py

make_p_to_json.pyが出力したJSONをDOT形式に変換します.

python3 json_to_dot.py < graph.json | dot -Tpdf > workflow.pdf

の様に利用します.

#!/usr/bin/python

import sys
import json


class Id(object):

    def __init__(self):
        super().__init__()
        self._i = 0

    @property
    def i(self):
        self._i += 1
        return self._i


def main(args):
    _parse_args(args)
    print("""
digraph G {
   graph [rankdir=LR]
   node [shape=plaintext]
    edge [color="#00000088"]
    """)
    i = Id()
    for graph in json.load(sys.stdin):
        print_single_graph(graph, i)
    print("}")


def print_single_graph(graph, i):
    name_to_node = {}
    print("subgraph cluster{}{{".format(i.i))
    for target, deps in graph.items():
        target_str = _escape(target)
        _register_node(target_str, i, name_to_node)
        target_node = name_to_node[target_str]
        for dep_str in (_escape(dep) for dep in deps):
            _register_node(dep_str, i, name_to_node)
            print('{} -> {}'.format(target_node, name_to_node[dep_str]))
    print("}")


def _register_node(name, i, name_to_node):
    if not (name in name_to_node):
        node = 'n{}'.format(i.i)
        name_to_node[name] = node
        print('{}[label={}]'.format(node, name))


def _escape(s):
    return '"{}"'.format(s.replace('"', r'\"'))


def _parse_args(args):
    if len(args) != 1:
        print('# convert a dependency graph stored in JSON format to DOT format')
        print('{} < deps.json | dot -Tpdf >| workflow.pdf'.format(args[0]))
        sys.exit(1)


if __name__ == '__main__':
    main(sys.argv)

必要なもの

  • GNU Make
  • Python 3
  • Graphviz

License

GPL version 3.

11
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?