Generating graphs from your Makefiles

Anyone who has done any kind of programming at some point comes across GNU Make and Makefile.

So what is make(1) and what are these Makefiles?

Instead of trying to explain this I’ll simply quote here the official GNU Make documentation.

GNU Make is a tool which controls the generation of executables and other non-source files of a program from the program’s source files.

Make gets its knowledge of how to build your program from a file called the makefile, which lists each of the non-source files and how to compute it from other files. When you write a program, you should write a makefile for it, so that it is possible to use Make to build and install the program.

If you haven’t heard or used make(1) before, I’d recommend that you go and check the GNU Make documentation for introduction and a tutorial on creating your first Makefile.

In the rest of this post I assume that you have already used make(1) and know what it does.

Okay, with that out of the way it’s time to focus on the topic of this post. Working with Makefiles can sometimes be challenging, especially if your project is large enough to contain lots of targets and lots of dependencies between each of them.

Navigating through these targets and their dependencies may not be an easy task. For example including other Makefiles while it helps keeping things logically separated introduces additional complexity when a person needs to understand the additional targets and additional dependencies from the included Makefile.

Okay, so what can we do about that? How to navigate through large Makefiles and get a better understanding of all the targets and their dependencies?

Internally, GNU Make (and other make implementations) are building a dependency graph, where the targets represent the vertices and the edges which connect them are represented by the target prerequisite.

Knowing this we can actually build a graph from our Makefiles and visualize them, which will help us navigate through the jungle of targets and their dependencies.

Okay, first things first. How do we get the graph representation which make(1) is using? This one is easy with GNU Make.

All you have to do in order to dump make’s internal database is call make(1) with these flags.

make --print-data-base \
     --no-builtin-rules \
     --no-builtin-variables \
     --dry-run \
     --always-make \
     --question

Calling this make(1) command inside a directory with a Makefile will dump the internal database of GNU Make.

Here’s a sample output from one of my Makefiles.

# GNU Make 4.4.1
# Built for x86_64-pc-linux-gnu
# Copyright (C) 1988-2023 Free Software Foundation, Inc.
# License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
# This is free software: you are free to change and redistribute it.
# There is NO WARRANTY, to the extent permitted by law.

# Make data base, printed on Sun Apr 28 16:38:33 2024

# Variables

# environment
TMUX_PANE = %0
# environment
GO111MODULE = on
# environment
QT_WAYLAND_RECONNECT = 1
# variable set hash-table stats:
# Load=109/1024=11%, Rehash=0, Collisions=6/141=4%

# Pattern-specific Variable Values

# No pattern-specific variable values.

# Directories

# . (device 65041, inode 3154786): 17 files, no impossibilities.

# 17 files, no impossibilities in 1 directories.

# Implicit Rules

# No implicit rules.

# Files

test_cover:
#  Phony target (prerequisite of .PHONY).
#  Implicit rule search has not been done.
#  File does not exist.
#  File has not been updated.
#  recipe to execute (from 'Makefile', line 8):
	go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...

# Not a target:
Makefile:
#  Implicit rule search has been done.
#  File is secondary (prerequisite of .SECONDARY).
#  Last modified 2022-08-19 12:15:43.700764449
#  File has been updated.
#  Successfully updated.

# Not a target:
.DEFAULT:
#  Implicit rule search has not been done.
#  Modification time never checked.
#  File has not been updated.

test:
#  Phony target (prerequisite of .PHONY).
#  Implicit rule search has not been done.
#  File does not exist.
#  File has not been updated.
#  recipe to execute (from 'Makefile', line 5):
	go test -v -race ./...

get:
#  Phony target (prerequisite of .PHONY).
#  Implicit rule search has not been done.
#  Implicit/static pattern stem: ''
#  File does not exist.
#  File has been updated.
#  Needs to be updated (-q is set).
# automatic
# @ := get
# automatic
# * :=
# automatic
# < :=
# automatic
# + :=
# automatic
# % :=
# automatic
# ^ :=
# automatic
# ? :=
# automatic
# | :=
# variable set hash-table stats:
# Load=8/32=25%, Rehash=0, Collisions=1/11=9%
#  recipe to execute (from 'Makefile', line 2):
	go get -v -t -d ./...

# Not a target:
.SUFFIXES:
#  Implicit rule search has not been done.
#  Modification time never checked.
#  File has not been updated.

.PHONY: get test test_cover
#  Implicit rule search has not been done.
#  Modification time never checked.
#  File has not been updated.

# files hash-table stats:
# Load=7/1024=1%, Rehash=0, Collisions=0/23=0%
# VPATH Search Paths

# No 'vpath' search paths.

# No general ('VPATH' variable) search path.

# strcache buffers: 1 (0) / strings = 26 / storage = 226 B / avg = 8 B
# current buf: size = 8162 B / used = 226 B / count = 26 / avg = 8 B

# strcache performance: lookups = 36 / hit rate = 27%
# hash-table stats:
# Load=26/8192=0%, Rehash=0, Collisions=0/36=0%
# Finished Make data base on Sun Apr 28 16:38:33 2024

If you look closely you will see that this output contains multiple sections:

  • A header describing the version of the make tool
  • A section containing the environment variables
  • Pattern-specific Variable Values
  • No pattern-specific variable values
  • etc.

Within the Files section we can also see our targets and this is exactly what we need and what we are going to be parsing.

The good news is that I’ve already implemented the parser and you can install it as a CLI tool, or use it as a Go package in case you want to implement different graph representations.

You can find the code in the dnaeon/makefile-graph and here’s how you can install it.

go install github.com/dnaeon/makefile-graph/cmd/makefile-graph@latest

If you want to use the parser and do whatever you want to do with the parsed graph you can install the parser package.

go get -v github.com/dnaeon/makefile-graph/pkg/parser

Here are some screenshots of makefile-graph in action. The commands below are using the following Makefile, from which we will be generating the graph representation in dot format.

edit : main.o kbd.o command.o display.o \
       insert.o search.o files.o utils.o
	cc -o edit main.o kbd.o command.o display.o \
		   insert.o search.o files.o utils.o

main.o : main.c defs.h
	cc -c main.c
kbd.o : kbd.c defs.h command.h
	cc -c kbd.c
command.o : command.c defs.h command.h
	cc -c command.c
display.o : display.c defs.h buffer.h
	cc -c display.c
insert.o : insert.c defs.h buffer.h
	cc -c insert.c
search.o : search.c defs.h buffer.h
	cc -c search.c
files.o : files.c defs.h buffer.h command.h
	cc -c files.c
utils.o : utils.c defs.h
	cc -c utils.c
clean :
	rm edit main.o kbd.o command.o display.o \
	   insert.o search.o files.o utils.o

Here’s how to render the graph for all targets from our sample Makefile.

makefile-graph --makefile examples/Makefile --direction TB | dot -Tsvg -o graph.svg

And this is what it looks like.

_config.yml

In large enough Makefiles the generated graph may not be easy to understand as well, and for that reason makefile-graph supports highlighting specific targets and their dependencies.

For example this command will highlight the files.o target and it’s dependencies.

makefile-graph \
    --makefile examples/Makefile \
    --direction TB \
    --target files.o \
    --highlight \
    --highlight-color lightgreen

The results look like this.

_config.yml

And if that is not good enough, we can always focus on a specific target and it’s dependencies only.

makefile-graph \
    --makefile examples/Makefile \
    --direction TB \
    --target files.o \
    --related-only

The results look like this.

_config.yml

For additional information and examples, please refer to the dnaeon/makefile-graph repo.

Written on April 28, 2024