github.com/wolfi-dev/wolfictl@v0.16.11/pkg/cli/dot.go (about)

     1  package cli
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"net"
     7  	"net/http"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"sort"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/spf13/cobra"
    16  	"github.com/tmc/dot"
    17  	"github.com/wolfi-dev/wolfictl/pkg/dag"
    18  	"golang.org/x/sync/errgroup"
    19  
    20  	"github.com/skratchdot/open-golang/open"
    21  )
    22  
    23  func cmdSVG() *cobra.Command { //nolint:gocyclo
    24  	var dir, pipelineDir string
    25  	var showDependents, recursive, span, web bool
    26  	var extraKeys, extraRepos []string
    27  	d := &cobra.Command{
    28  		Use:   "dot",
    29  		Short: "Generate graphviz .dot output",
    30  		Args:  cobra.MinimumNArgs(1),
    31  		Long: `
    32  Generate .dot output and pipe it to dot to generate an SVG
    33  
    34    wolfictl dot zlib | dot -Tsvg > graph.svg
    35  
    36  Generate .dot output and pipe it to dot to generate a PNG
    37  
    38    wolfictl dot zlib | dot -Tpng > graph.png
    39  
    40  Open browser to explore crane
    41  
    42    wolfictl dot --web crane
    43  
    44  Open browser to explore crane's deps recursively, only showing a minimum subgraph
    45  
    46    wolfictl dot --web -R -S crane
    47  `,
    48  		RunE: func(cmd *cobra.Command, args []string) error {
    49  			ctx := cmd.Context()
    50  			if pipelineDir == "" {
    51  				pipelineDir = filepath.Join(dir, "pipelines")
    52  			}
    53  
    54  			pkgs, err := dag.NewPackages(ctx, os.DirFS(dir), dir, pipelineDir)
    55  			if err != nil {
    56  				return fmt.Errorf("NewPackages: %w", err)
    57  			}
    58  
    59  			g, err := dag.NewGraph(ctx, pkgs,
    60  				dag.WithKeys(extraKeys...),
    61  				dag.WithRepos(extraRepos...),
    62  			)
    63  			if err != nil {
    64  				return fmt.Errorf("building graph: %w", err)
    65  			}
    66  
    67  			amap, err := g.Graph.AdjacencyMap()
    68  			if err != nil {
    69  				return err
    70  			}
    71  
    72  			pmap, err := g.Graph.PredecessorMap()
    73  			if err != nil {
    74  				return err
    75  			}
    76  
    77  			render := func(args []string) (*dot.Graph, error) {
    78  				todo := []string{}
    79  				queued := map[string]struct{}{}
    80  
    81  				out := dot.NewGraph("images")
    82  				if err := out.Set("rankdir", "LR"); err != nil {
    83  					return nil, err
    84  				}
    85  				out.SetType(dot.DIGRAPH)
    86  
    87  				renderNode := func(node string) error {
    88  					var byName []dag.Package
    89  					config := pkgs.ConfigByKey(node)
    90  					if config != nil {
    91  						byName = append(byName, config)
    92  					} else {
    93  						byName, err = g.NodesByName(node)
    94  						if err != nil {
    95  							return err
    96  						}
    97  
    98  						if len(byName) == 0 {
    99  							return fmt.Errorf("could not find node %q", node)
   100  						}
   101  					}
   102  
   103  					for _, name := range byName {
   104  						h := dag.PackageHash(name)
   105  
   106  						pkgver, source := split(h)
   107  						n := dot.NewNode(pkgver)
   108  						if err := n.Set("tooltip", source); err != nil {
   109  							return err
   110  						}
   111  						if pkgs.ConfigByKey(pkgver) != nil {
   112  							if web {
   113  								if err := n.Set("URL", link(args, pkgver)); err != nil {
   114  									return err
   115  								}
   116  							}
   117  						} else {
   118  							if err := n.Set("color", "red"); err != nil {
   119  								return err
   120  							}
   121  						}
   122  						out.AddNode(n)
   123  
   124  						dependencies, ok := amap[h]
   125  						if !ok {
   126  							continue
   127  						}
   128  
   129  						deps := make([]string, 0, len(dependencies))
   130  						for dep := range dependencies {
   131  							deps = append(deps, dep)
   132  						}
   133  						sort.Strings(deps)
   134  
   135  						for _, dep := range deps {
   136  							if recursive || span {
   137  								if _, ok := queued[dep]; ok {
   138  									if span {
   139  										continue
   140  									}
   141  								} else {
   142  									todo = append(todo, dep)
   143  									queued[dep] = struct{}{}
   144  								}
   145  							}
   146  
   147  							pkgver, source := split(dep)
   148  							d := dot.NewNode(pkgver)
   149  							if err := d.Set("tooltip", source); err != nil {
   150  								return err
   151  							}
   152  							if pkgs.ConfigByKey(pkgver) != nil {
   153  								if web {
   154  									if err := d.Set("URL", link(args, pkgver)); err != nil {
   155  										return err
   156  									}
   157  								}
   158  							} else {
   159  								if err := d.Set("color", "red"); err != nil {
   160  									return err
   161  								}
   162  							}
   163  							out.AddNode(d)
   164  							out.AddEdge(dot.NewEdge(n, d))
   165  						}
   166  
   167  						if !showDependents {
   168  							continue
   169  						}
   170  
   171  						predecessors, ok := pmap[h]
   172  						if !ok {
   173  							continue
   174  						}
   175  
   176  						preds := make([]string, 0, len(predecessors))
   177  						for pred := range predecessors {
   178  							preds = append(preds, pred)
   179  						}
   180  						sort.Strings(preds)
   181  
   182  						for _, pred := range preds {
   183  							pkgver, source := split(pred)
   184  							d := dot.NewNode(pkgver)
   185  							if err := d.Set("tooltip", source); err != nil {
   186  								return err
   187  							}
   188  							if pkgs.ConfigByKey(pkgver) != nil {
   189  								if web {
   190  									if err := d.Set("URL", link(args, pkgver)); err != nil {
   191  										return err
   192  									}
   193  								}
   194  							}
   195  							out.AddNode(d)
   196  							out.AddEdge(dot.NewEdge(d, n))
   197  						}
   198  					}
   199  
   200  					return nil
   201  				}
   202  
   203  				for _, node := range args {
   204  					if err := renderNode(node); err != nil {
   205  						return nil, err
   206  					}
   207  				}
   208  
   209  				if recursive {
   210  					var node string
   211  					for len(todo) != 0 {
   212  						node, todo = pop(todo)
   213  
   214  						pkgver, _ := split(node)
   215  						if pkgs.ConfigByKey(pkgver) == nil {
   216  							continue
   217  						}
   218  
   219  						if err := renderNode(pkgver); err != nil {
   220  							return nil, err
   221  						}
   222  					}
   223  				}
   224  
   225  				return out, nil
   226  			}
   227  
   228  			if web {
   229  				http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   230  					if r.URL.Path != "/" {
   231  						return
   232  					}
   233  					nodes := r.URL.Query()["node"]
   234  
   235  					if len(nodes) == 0 {
   236  						nodes = args
   237  					}
   238  
   239  					out, err := render(nodes)
   240  					if err != nil {
   241  						fmt.Fprintf(w, "error rendering %v: %v", nodes, err)
   242  						log.Fatal(err)
   243  					}
   244  
   245  					log.Printf("%s: rendering %v", r.URL, nodes)
   246  					cmd := exec.Command("dot", "-Tsvg")
   247  					cmd.Stdin = strings.NewReader(out.String())
   248  					cmd.Stdout = w
   249  
   250  					if err := cmd.Run(); err != nil {
   251  						fmt.Fprintf(w, "error rendering %v: %v", nodes, err)
   252  						log.Fatal(err)
   253  					}
   254  				})
   255  
   256  				l, err := net.Listen("tcp", "127.0.0.1:0")
   257  				if err != nil {
   258  					return err
   259  				}
   260  
   261  				server := &http.Server{
   262  					Addr:              l.Addr().String(),
   263  					ReadHeaderTimeout: 3 * time.Second,
   264  				}
   265  
   266  				log.Printf("%s", l.Addr().String())
   267  
   268  				var g errgroup.Group
   269  				g.Go(func() error {
   270  					return server.Serve(l)
   271  				})
   272  
   273  				g.Go(func() error {
   274  					return open.Run(fmt.Sprintf("http://localhost:%d", l.Addr().(*net.TCPAddr).Port))
   275  				})
   276  
   277  				g.Go(func() error {
   278  					<-ctx.Done()
   279  					server.Close()
   280  					return ctx.Err()
   281  				})
   282  
   283  				return g.Wait()
   284  			}
   285  
   286  			out, err := render(args)
   287  			if err != nil {
   288  				return err
   289  			}
   290  
   291  			fmt.Println(out.String())
   292  			return nil
   293  		},
   294  	}
   295  	d.Flags().StringVarP(&dir, "dir", "d", ".", "directory to search for melange configs")
   296  	d.Flags().StringVar(&pipelineDir, "pipeline-dir", "", "directory used to extend defined built-in pipelines")
   297  	d.Flags().BoolVarP(&showDependents, "show-dependents", "D", false, "show packages that depend on these packages, instead of these packages' dependencies")
   298  	d.Flags().BoolVarP(&recursive, "recursive", "R", false, "recurse through package dependencies")
   299  	d.Flags().BoolVarP(&span, "spanning-tree", "S", false, "does something like a spanning tree to avoid a huge number of edges")
   300  	d.Flags().StringSliceVarP(&extraKeys, "keyring-append", "k", []string{"https://packages.wolfi.dev/os/wolfi-signing.rsa.pub"}, "path to extra keys to include in the build environment keyring")
   301  	d.Flags().StringSliceVarP(&extraRepos, "repository-append", "r", []string{"https://packages.wolfi.dev/os"}, "path to extra repositories to include in the build environment")
   302  	d.Flags().BoolVar(&web, "web", false, "do a website")
   303  	return d
   304  }
   305  
   306  func split(in string) (pkgver, source string) {
   307  	before, source, ok := strings.Cut(in, "@")
   308  	if !ok {
   309  		panic(in)
   310  	}
   311  
   312  	pkg, ver, ok := strings.Cut(before, ":")
   313  	if !ok {
   314  		panic(in)
   315  	}
   316  
   317  	return fmt.Sprintf("%s-%s", pkg, ver), source
   318  }
   319  
   320  func pop(a []string) (result string, stack []string) {
   321  	return a[len(a)-1], a[:len(a)-1]
   322  }
   323  
   324  func link(args []string, pkgver string) string {
   325  	filtered := []string{}
   326  	for _, a := range args {
   327  		if a != pkgver {
   328  			filtered = append(filtered, a)
   329  		}
   330  	}
   331  	return "/?node=" + pkgver + "&node=" + strings.Join(filtered, "&node=")
   332  }