From 7b6605716e76de92a2d80505cf6dad73f63e51bd Mon Sep 17 00:00:00 2001
From: Jonas Danielsson <jonas@threetimestwo.org>
Date: Tue, 20 Oct 2020 13:21:51 +0200
Subject: [PATCH] Add '-g' opt for graph output of modified targets

A graph visualisation can be helpful in order to get a better
understanding of the workings of a GNU Make based project.
Particularly if you are debugging why something keeps rebuilding each
pass or if you are very new to a projekt.

All the information that can be gleaned from this graph output is
already present in the output of --print-database. However we strive to
present it more succinct. Only the targets that were modified in the
goal is present and the relationship between targets and prerequisite
is hopefully more clear.

The output from the goal grahps is stored in files called [goal].dot
in the starting directory.

The hope is also that this can be paired with already existing viewers
and explorers, since the dot language is well known (29 years!).

An example (rebuilding GNU Make):
  $ touch src/file.c
  $ AM_MAKEFLAGS="-g" make -g
  Making all in lib
  make[1]: Entering directory '/home/jonas/sandbox/mymake/lib'
  /home/jonas/sandbox/mymake/./make -g all-recursive
  make[2]: Entering directory '/home/jonas/sandbox/mymake/lib'
  make[3]: Entering directory '/home/jonas/sandbox/mymake/lib'
  make[3]: Nothing to be done for 'all-am'.
  make[3]: Leaving directory '/home/jonas/sandbox/mymake/lib'
  make[2]: Writing dependency graph to '/home/jonas/sandbox/mymake/lib/all-recursive.dot'
  make[2]: Leaving directory '/home/jonas/sandbox/mymake/lib'
  make[1]: Writing dependency graph to '/home/jonas/sandbox/mymake/lib/all.dot'
  make[1]: Leaving directory '/home/jonas/sandbox/mymake/lib'
  Making all in po
  make[1]: Entering directory '/home/jonas/sandbox/mymake/po'
  make[1]: Nothing to be done for 'all'.
  make[1]: Leaving directory '/home/jonas/sandbox/mymake/po'
  Making all in doc
  make[1]: Entering directory '/home/jonas/sandbox/mymake/doc'
  make[1]: Nothing to be done for 'all'.
  make[1]: Leaving directory '/home/jonas/sandbox/mymake/doc'
  make[1]: Entering directory '/home/jonas/sandbox/mymake'

  [...]
  [gcc building]
  [...]

  make[1]: Writing dependency graph to '/home/jonas/sandbox/mymake/all-am.dot'
  make[1]: Leaving directory '/home/jonas/sandbox/mymake'
  make: Writing dependency graph to '/home/jonas/sandbox/mymake/all.dot'

  $ cat all-am.dot
  digraph all-am {
  "make" -> "src/file.o"
  "all-am" -> "make"
  "all-am"
  }

  $ cat all.dot
  digraph all {
  "all" -> "all-recursive" [label="forced: PHONY prerequisite"]
  "all"
  }
---
 doc/make.1    |   6 +++
 doc/make.texi |   9 ++++
 src/file.c    | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/filedef.h |   2 +
 src/main.c    |   5 ++
 src/remake.c  |   4 ++
 6 files changed, 149 insertions(+)

diff --git a/doc/make.1 b/doc/make.1
index dbd023f..a9d56df 100644
--- a/doc/make.1
+++ b/doc/make.1
@@ -160,6 +160,12 @@ Use
 .I file
 as a makefile.
 .TP 0.5i
+\fB\-g\fR, \fB\-\-output\-graph\fR
+Output build dependency graph for updated make goals. The graph is
+written to a file called
+.I [goal].dot
+and is represented using the Graphvis (dot) syntax. Only targets that
+have been updated is present in the graph.
 \fB\-i\fR, \fB\-\-ignore\-errors\fR
 Ignore all errors in commands executed to remake files.
 .TP 0.5i
diff --git a/doc/make.texi b/doc/make.texi
index 21573c0..bf7db47 100644
--- a/doc/make.texi
+++ b/doc/make.texi
@@ -8925,6 +8925,15 @@ been defined, but before any makefiles are read.
 Read the file named @var{file} as a makefile.
 @xref{Makefiles, ,Writing Makefiles}.
 
+@item -g
+@cindex @code{-g}
+@itemx --output-graph
+@cindex @code{--output-graph}
+Output build dependency graph for updated make goals. The graph is
+written to a file called @code{[goal].dot} and is represented using
+the Graphvis (dot) syntax. Only targets that have been updated is
+present in the graph.
+
 @item -h
 @cindex @code{-h}
 @itemx --help
diff --git a/src/file.c b/src/file.c
index a979ca5..4ffc7f5 100644
--- a/src/file.c
+++ b/src/file.c
@@ -1199,4 +1199,127 @@ init_hash_files (void)
   hash_init (&files, 1000, file_hash_1, file_hash_2, file_hash_cmp);
 }
 
+static const char *
+get_edge_label (const struct file *file)
+{
+  if (file->last_mtime == NONEXISTENT_MTIME)
+    {
+      if (file->phony)
+        return "[label=\"forced: PHONY prerequisite\"]";
+
+      return "[label=\"forced: non-existent file\"]";
+    }
+  return "";
+}
+
+static int
+add_edge (const struct file *parent, const struct file *child, FILE *fp)
+{
+  return fprintf (fp, "\"%s\" -> \"%s\" %s\n",
+                  parent->name, child->name, get_edge_label (child));
+}
+
+static int
+output_graph_node (const void *item, struct hash_table *graph_seen, FILE *fp)
+{
+  int n;
+  const struct file *file = item;
+  struct file **file_slot;
+
+  if (!file->updated)
+    return 0;
+
+  if (file->deps)
+    {
+      struct dep *d;
+      for (d = file->deps; d != 0; d = d->next)
+          if (output_graph_node (d->file, graph_seen, fp) < 0)
+            return -1;
+    }
+
+  /* We only want to include the files that actually changed during
+     the updating of this goal. But we make an exception for non-existent
+     files, as they are a clue as to why something was rebuilt.
+  */
+  if (file->last_mtime != NONEXISTENT_MTIME)
+    if (file->last_mtime == file->mtime_before_update)
+      return 0;
+
+  /* Make sure we have not seen this file before in the graph */
+  file_slot = (struct file **) hash_find_slot (graph_seen, file);
+  if (!HASH_VACANT (*file_slot))
+     return 0;
+  hash_insert_at (graph_seen, file, file_slot);
+
+  /* If file has a parent, add it as a child to that parent.
+     Otherwise just add it on its own to the graph. This way we
+     support the case of only one target in a goal.
+  */
+
+  if (file->parent)
+    {
+      if (add_edge(file->parent, file, fp) < 0)
+        return -1;
+    }
+  else
+   {
+      n = fprintf (fp, "\"%s\"\n", file->name);
+      if (n < 0)
+        return n;
+   }
+  return 0;
+}
+
+
+/* Traverse the dependency graph for a goal and output it in the
+   Graphviz (dot) syntax.
+
+   Only targets that have been updated (last_mtime != mtime_before_update)
+   will be included in the graph.
+*/
+
+void
+output_graph_file (const char *goal)
+{
+  FILE *fp;
+  struct file *root;
+  struct hash_table graph_seen;
+  char dot_fname[PATH_MAX + FILENAME_MAX + 1];
+
+  hash_init (&graph_seen, 1000, file_hash_1, file_hash_2, file_hash_cmp);
+
+  /* Make sure we always save the graph file in the starting directory */
+  sprintf (dot_fname, "%s/%s.dot", starting_directory, goal);
+  OS (message, 1, "Writing dependency graph to '%s'", dot_fname);
+
+  fp = fopen (dot_fname, "w");
+  if (!fp)
+    {
+      OS (error, NILF, _("Failed to open to graph file: %s"), strerror (errno));
+      goto out;
+    }
+  if (fprintf (fp, "digraph \"%s\" {\n", goal) < 0)
+    {
+      OS (error, NILF, _("Failed to write to graph file: %s"), strerror (errno));
+      goto out;
+    }
+
+  root = lookup_file (goal);
+  if (!root)
+    goto out;
+
+  if (output_graph_node(root, &graph_seen, fp) < 0)
+    {
+      OS (error, NILF, _("Failed to write to graph file: %s"), strerror (errno));
+      goto out;
+    }
+
+  if (fprintf (fp, "}\n") < 0)
+    OS (error, NILF, _("Failed to write to graph file: %s"), strerror (errno));
+
+out:
+  hash_free (&graph_seen, 0);
+  fclose (fp);
+}
+
 /* EOF */
diff --git a/src/filedef.h b/src/filedef.h
index 972f853..8c73bb9 100644
--- a/src/filedef.h
+++ b/src/filedef.h
@@ -110,6 +110,7 @@ struct file
 
 extern struct file *default_file;
 
+extern int output_graph_flag;
 
 struct file *lookup_file (const char *name);
 struct file *enter_file (const char *name);
@@ -129,6 +130,7 @@ void print_prereqs (const struct dep *deps);
 void print_file_data_base (void);
 int try_implicit_rule (struct file *file, unsigned int depth);
 int stemlen_compare (const void *v1, const void *v2);
+void output_graph_file (const char *goal);
 
 #if FILE_TIMESTAMP_HI_RES
 # define FILE_TIMESTAMP_STAT_MODTIME(fname, st) \
diff --git a/src/main.c b/src/main.c
index c79a9f6..76fc6d9 100644
--- a/src/main.c
+++ b/src/main.c
@@ -239,6 +239,8 @@ int print_version_flag = 0;
 
 static struct stringlist *makefiles = 0;
 
+int output_graph_flag = 0;
+
 /* Size of the stack when we started.  */
 
 #ifdef SET_STACK_SIZE
@@ -350,6 +352,8 @@ static const char *const usage[] =
   -f FILE, --file=FILE, --makefile=FILE\n\
                               Read FILE as a makefile.\n"),
     N_("\
+  -g, --output-graph          Output (dot) graph of modified targets for each goal.\n"),
+    N_("\
   -h, --help                  Print this message and exit.\n"),
     N_("\
   -i, --ignore-errors         Ignore errors from recipes.\n"),
@@ -418,6 +422,7 @@ static const struct command_switch switches[] =
     { 'd', flag, &debug_flag, 1, 1, 0, 0, 0, 0 },
     { 'e', flag, &env_overrides, 1, 1, 0, 0, 0, "environment-overrides", },
     { 'E', strlist, &eval_strings, 1, 0, 0, 0, 0, "eval" },
+    { 'g', flag, &output_graph_flag, 0, 0, 0, 0, 0, "output-graph" },
     { 'h', flag, &print_usage_flag, 0, 0, 0, 0, 0, "help" },
     { 'i', flag, &ignore_errors_flag, 1, 1, 0, 0, 0, "ignore-errors" },
     { 'k', flag, &keep_going_flag, 1, 1, 0, 0, &default_keep_going_flag,
diff --git a/src/remake.c b/src/remake.c
index fb237c5..ac22ec1 100644
--- a/src/remake.c
+++ b/src/remake.c
@@ -233,6 +233,10 @@ update_goal_chain (struct goaldep *goaldeps)
               else
                 lastgoal->next = g->next;
 
+              /* Output graph if goal has changed and if requested */
+              if (g->changed && output_graph_flag)
+                output_graph_file (dep_name(g));
+
               /* Free the storage.  */
               free (g);
 
-- 
2.25.1

