1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 package main
27
28 import (
29 "bufio"
30 "bytes"
31 "flag"
32 "fmt"
33 "io"
34 "log"
35 "os"
36 "sort"
37 "strings"
38
39 "golang.org/x/mod/semver"
40 )
41
42 func usage() {
43 fmt.Fprintf(os.Stderr, `Usage: go mod graph | modgraphviz | dot -Tpng -o graph.png
44
45 For each module, the node representing the greatest version (i.e., the
46 version chosen by Go's minimal version selection algorithm) is colored green.
47 Other nodes, which aren't in the final build list, are colored grey.
48 `)
49 os.Exit(2)
50 }
51
52 func main() {
53 log.SetFlags(0)
54 log.SetPrefix("modgraphviz: ")
55
56 flag.Usage = usage
57 flag.Parse()
58 if flag.NArg() != 0 {
59 usage()
60 }
61
62 if err := modgraphviz(os.Stdin, os.Stdout); err != nil {
63 log.Fatal(err)
64 }
65 }
66
67 func modgraphviz(in io.Reader, out io.Writer) error {
68 graph, err := convert(in)
69 if err != nil {
70 return err
71 }
72
73 fmt.Fprintf(out, "digraph gomodgraph {\n")
74 fmt.Fprintf(out, "\tnode [ shape=rectangle fontsize=12 ]\n")
75 out.Write(graph.edgesAsDOT())
76 for _, n := range graph.mvsPicked {
77 fmt.Fprintf(out, "\t%q [style = filled, fillcolor = green]\n", n)
78 }
79 for _, n := range graph.mvsUnpicked {
80 fmt.Fprintf(out, "\t%q [style = filled, fillcolor = gray]\n", n)
81 }
82 fmt.Fprintf(out, "}\n")
83
84 return nil
85 }
86
87 type edge struct{ from, to string }
88 type graph struct {
89 edges []edge
90 mvsPicked []string
91 mvsUnpicked []string
92 }
93
94
95
96 func convert(r io.Reader) (*graph, error) {
97 scanner := bufio.NewScanner(r)
98 var g graph
99 seen := map[string]bool{}
100 mvsPicked := map[string]string{}
101
102 for scanner.Scan() {
103 l := scanner.Text()
104 if l == "" {
105 continue
106 }
107 parts := strings.Fields(l)
108 if len(parts) != 2 {
109 return nil, fmt.Errorf("expected 2 words in line, but got %d: %s", len(parts), l)
110 }
111 from := parts[0]
112 to := parts[1]
113 g.edges = append(g.edges, edge{from: from, to: to})
114
115 for _, node := range []string{from, to} {
116 if _, ok := seen[node]; ok {
117
118 continue
119 }
120 seen[node] = true
121
122 var m, v string
123 if i := strings.IndexByte(node, '@'); i >= 0 {
124 m, v = node[:i], node[i+1:]
125 } else {
126
127 continue
128 }
129
130 if maxV, ok := mvsPicked[m]; ok {
131 if semver.Compare(maxV, v) < 0 {
132
133
134 g.mvsUnpicked = append(g.mvsUnpicked, m+"@"+maxV)
135 mvsPicked[m] = v
136 } else {
137
138
139 g.mvsUnpicked = append(g.mvsUnpicked, node)
140 }
141 } else {
142 mvsPicked[m] = v
143 }
144 }
145 }
146 if err := scanner.Err(); err != nil {
147 return nil, err
148 }
149
150 for m, v := range mvsPicked {
151 g.mvsPicked = append(g.mvsPicked, m+"@"+v)
152 }
153
154
155 sort.Strings(g.mvsPicked)
156 return &g, nil
157 }
158
159
160 func (g *graph) edgesAsDOT() []byte {
161 var buf bytes.Buffer
162 for _, e := range g.edges {
163 fmt.Fprintf(&buf, "\t%q -> %q\n", e.from, e.to)
164 }
165 return buf.Bytes()
166 }
167
View as plain text