github.com/maruel/nin@v0.0.0-20220112143044-f35891e3ce7e/graphviz.go (about)

     1  // Copyright 2011 Google Inc. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package nin
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"os"
    21  	"strings"
    22  )
    23  
    24  // GraphViz is the object to initialize the parameters to create GraphViz .dot
    25  // file output.
    26  type GraphViz struct {
    27  	out          io.Writer
    28  	dyndepLoader DyndepLoader
    29  	visitedNodes map[*Node]struct{}
    30  	visitedEdges map[*Edge]struct{}
    31  }
    32  
    33  // NewGraphViz returns an initialized GraphViz.
    34  func NewGraphViz(state *State, di DiskInterface) GraphViz {
    35  	return GraphViz{
    36  		out:          os.Stdout,
    37  		dyndepLoader: NewDyndepLoader(state, di),
    38  		visitedNodes: map[*Node]struct{}{},
    39  		visitedEdges: map[*Edge]struct{}{},
    40  	}
    41  }
    42  
    43  // AddTarget adds a node to include in the graph.
    44  func (g *GraphViz) AddTarget(node *Node) {
    45  	if _, ok := g.visitedNodes[node]; ok {
    46  		return
    47  	}
    48  
    49  	fmt.Fprintf(g.out, "\"%p\" [label=\"%s\"]\n", node, strings.ReplaceAll(node.Path, "\\", "/"))
    50  	g.visitedNodes[node] = struct{}{}
    51  
    52  	edge := node.InEdge
    53  
    54  	if edge == nil {
    55  		// Leaf node.
    56  		// Draw as a rect?
    57  		return
    58  	}
    59  
    60  	if _, ok := g.visitedEdges[edge]; ok {
    61  		return
    62  	}
    63  	g.visitedEdges[edge] = struct{}{}
    64  
    65  	if edge.Dyndep != nil && edge.Dyndep.DyndepPending {
    66  		if err := g.dyndepLoader.LoadDyndeps(edge.Dyndep, DyndepFile{}); err != nil {
    67  			warningf("%s\n", err)
    68  		}
    69  	}
    70  
    71  	if len(edge.Inputs) == 1 && len(edge.Outputs) == 1 {
    72  		// Can draw simply.
    73  		// Note extra space before label text -- this is cosmetic and feels
    74  		// like a graphviz bug.
    75  		fmt.Fprintf(g.out, "\"%p\" -> \"%p\" [label=\" %s\"]\n", edge.Inputs[0], edge.Outputs[0], edge.Rule.Name)
    76  	} else {
    77  		fmt.Fprintf(g.out, "\"%p\" [label=\"%s\", shape=ellipse]\n", edge, edge.Rule.Name)
    78  		for _, out := range edge.Outputs {
    79  			fmt.Fprintf(g.out, "\"%p\" -> \"%p\"\n", edge, out)
    80  		}
    81  		for i, in := range edge.Inputs {
    82  			orderOnly := ""
    83  			if edge.IsOrderOnly(i) {
    84  				orderOnly = " style=dotted"
    85  			}
    86  			fmt.Fprintf(g.out, "\"%p\" -> \"%p\" [arrowhead=none%s]\n", in, edge, orderOnly)
    87  		}
    88  	}
    89  
    90  	for _, in := range edge.Inputs {
    91  		g.AddTarget(in)
    92  	}
    93  }
    94  
    95  // Start prints out the header.
    96  func (g *GraphViz) Start() {
    97  	fmt.Fprintf(g.out, "digraph ninja {\n")
    98  	fmt.Fprintf(g.out, "rankdir=\"LR\"\n")
    99  	fmt.Fprintf(g.out, "node [fontsize=10, shape=box, height=0.25]\n")
   100  	fmt.Fprintf(g.out, "edge [fontsize=10]\n")
   101  }
   102  
   103  // Finish prints out the footer.
   104  func (g *GraphViz) Finish() {
   105  	fmt.Fprintf(g.out, "}\n")
   106  }