Repository: dnaeon/makefile-graph Branch: v1 Commit: 409b0d23cc2b Files: 17 Total size: 46.2 KB Directory structure: gitextract_z9ia9a0m/ ├── .github/ │ ├── FUNDING.yml │ ├── dependabot.yaml │ └── workflows/ │ └── test.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd/ │ └── makefile-graph/ │ └── main.go ├── examples/ │ └── Makefile ├── go.mod ├── go.sum ├── pkg/ │ ├── fixtures/ │ │ ├── fixtures.go │ │ ├── sample_gnu_make_db_3.81.txt │ │ └── sample_gnu_make_db_4.4.1.txt │ └── parser/ │ ├── parser.go │ └── parser_test.go └── renovate.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ --- github: [dnaeon] ================================================ FILE: .github/dependabot.yaml ================================================ --- version: 2 updates: # Create PRs for dependency updates - package-ecosystem: gomod directory: / schedule: interval: weekly commit-message: prefix: "deps:" ================================================ FILE: .github/workflows/test.yaml ================================================ # .github/workflows/test.yaml on: [push, pull_request] name: test jobs: test: strategy: matrix: go-version: [1.23.x, 1.24.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} - uses: actions/checkout@v6 - run: make test-cover - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v6.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} slug: dnaeon/makefile-graph ================================================ FILE: .gitignore ================================================ # Ignore Emacs backup files *~ bin/ coverage.txt ================================================ FILE: LICENSE ================================================ Copyright (c) 2024 Marin Atanasov Nikolov All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Makefile ================================================ .DEFAULT_GOAL := build LOCAL_BIN ?= $(shell pwd)/bin BINARY ?= $(LOCAL_BIN)/makefile-graph SRC_DIRS := $(shell go list -f '{{.Dir}}' ./...) $(LOCAL_BIN): mkdir -p $(LOCAL_BIN) $(BINARY): $(SRC_DIRS) | $(LOCAL_BIN) go build -o $(BINARY) cmd/makefile-graph/main.go build: $(BINARY) get: go get -v -t -d ./... test: go test -v -race $(shell go list ./... | grep -v -E 'cmd|fixtures') test-cover: go test -v -race -coverprofile=coverage.txt -covermode=atomic $(shell go list ./... | grep -v -E 'cmd|fixtures') .PHONY: get test test-cover build ================================================ FILE: README.md ================================================ # makefile-graph [![Build Status](https://github.com/dnaeon/makefile-graph/actions/workflows/test.yaml/badge.svg)](https://github.com/dnaeon/makefile-graph/actions/workflows/test.yaml/badge.svg) [![Go Reference](https://pkg.go.dev/badge/github.com/dnaeon/makefile-graph.svg)](https://pkg.go.dev/github.com/dnaeon/makefile-graph) [![Go Report Card](https://goreportcard.com/badge/github.com/dnaeon/makefile-graph)](https://goreportcard.com/report/github.com/dnaeon/makefile-graph) [![codecov](https://codecov.io/gh/dnaeon/makefile-graph/branch/master/graph/badge.svg)](https://codecov.io/gh/dnaeon/makefile-graph) `makefile-graph` is a Go module and CLI application, which parses [GNU Make](https://www.gnu.org/software/make/)'s internal database and generates a graph representing the relationships between the discovered Makefile targets. ![echarts dark demo](./images/echarts-dark.gif) ## Requirements * [GNU Make](https://www.gnu.org/software/make/) * Go version 1.21.x or later ## Installation You can install the CLI application using one of the following ways. If you have cloned the repository you can build the CLI app using the provided Makefile target. ``` shell make build ``` The resulting binary will be located in `bin/makefile-graph`. Install the CLI application using `go install`. ``` shell go install github.com/dnaeon/makefile-graph/cmd/makefile-graph@latest ``` In order to install the parser package and use it in your own Go code run the following command within your Go module. ``` shell go get -v github.com/dnaeon/makefile-graph/pkg/parser ``` ## Usage Let's use the following [example Makefile](https://www.gnu.org/software/make/manual/html_node/Simple-Makefile.html) from [GNU make's documentation](https://www.gnu.org/software/make/manual/make.html). ``` makefile 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 ``` You can also find this example Makefile in the [examples](./examples/) directory of this repo. Running the following command will generate the [Dot representation](https://graphviz.org/doc/info/lang.html) for the Makefile targets and their dependencies from our example Makefile. ``` shell makefile-graph --makefile examples/Makefile --direction TB ``` In order to render the graph you can pipe it directly to the [dot command](https://graphviz.org/doc/info/command.html), e.g. ``` makefile makefile-graph --makefile examples/Makefile --direction TB | dot -Tsvg -o graph.svg ``` This is what the graph looks like when we render it using `dot(1)`. ![Example Makefile Graph](./images/image-1.svg) Sometimes when rendering large graphs it may not be obvious at first glance what are the dependencies for a specific target. In order to help with such situations `makefile-graph` supports a flag which allows you to highlight specific targets and their dependencies. The following command will highlight the `files.o` target along with it's dependencies. ``` makefile makefile-graph \ --makefile examples/Makefile \ --direction TB \ --target files.o \ --highlight \ --highlight-color lightgreen ``` When we render the output from above command we will see this graph representation. ![Example Makefile Graph Highlighted](./images/image-2.svg) If we want to focus on a specific target and it's dependencies only we can use the following command, which will generate a graph only for the target and it's dependencies. ``` makefile makefile-graph \ --makefile examples/Makefile \ --direction TB \ --target files.o \ --related-only ``` This is what the resulting graph looks like. ![Example Makefile Graph Related Only](./images/image-3.svg) The `--direction` option is used for specifying the [direction of graph layout](https://graphviz.org/docs/attrs/rankdir/). You can set it to `TB`, `BT`, `LR` or `RL`. The `--format` option is used for specifying the output format for the graph. By default it will produce the `dot` representation for the graph. You can also view the [topological order](https://en.wikipedia.org/wiki/Topological_sorting) for a given target by setting the format to `tsort`, e.g. ``` makefile makefile-graph \ --makefile examples/Makefile \ --target files.o \ --related-only \ --format tsort ``` Running above command produces the following output, which represents the topological order for the `files.o` target. ``` text defs.h buffer.h command.h files.c files.o ``` The graph can also be rendered with [Apache ECharts](https://echarts.apache.org/en/index.html) using the [go-echarts](https://github.com/go-echarts/go-echarts) library. The following command will generate an HTML file, which renders the graph in Apache ECharts. ``` shell makefile-graph \ --makefile examples/Makefile \ --direction LR \ --theme dark \ --format echarts ``` ![echarts dark demo](./images/echarts-dark.gif) A different theme can be specified using the `--theme` option. This example renders the same graph using `--theme=white`. ![echarts white demo](./images/echarts-white.gif) ## Tests Run the tests. ``` shell make test ``` Run test coverage. ``` shell make test-cover ``` ## Contributing `makefile-graph` is hosted on [Github](https://github.com/dnaeon/makefile-graph). Please contribute by reporting issues, suggesting features or by sending patches using pull requests. ## License `makefile-graph` is Open Source and licensed under the [BSD License](http://opensource.org/licenses/BSD-2-Clause). ================================================ FILE: cmd/makefile-graph/main.go ================================================ // Copyright (c) 2024 Marin Atanasov Nikolov // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // // 1. Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. package main import ( "bytes" "errors" "flag" "fmt" "io" "os" "os/exec" "path" "slices" "strings" "github.com/go-echarts/go-echarts/v2/charts" "github.com/go-echarts/go-echarts/v2/components" "github.com/go-echarts/go-echarts/v2/opts" "gopkg.in/dnaeon/go-graph.v1" "github.com/dnaeon/makefile-graph/pkg/parser" ) var errNoTargetName = errors.New("Must specify target name") var errInvalidLayoutDirection = errors.New("Invalid layout direction") var errInvalidFormat = errors.New("Invalid format specified") const ( formatDot = "dot" formatTopoSort = "tsort" formatEcharts = "echarts" ) func main() { var relatedOnly, highlight bool var makefile, target, highlightColor, direction, format, theme string var fontSize float64 var fontStyle string flag.StringVar(&makefile, "makefile", "Makefile", "path to Makefile") flag.StringVar(&target, "target", "", "name of a target") flag.BoolVar(&highlight, "highlight", false, "highlight target and related targets") flag.StringVar(&highlightColor, "highlight-color", "green", "color to use for highlighting") flag.BoolVar(&relatedOnly, "related-only", false, "return only related vertices for a target") flag.StringVar(&direction, "direction", "TB", "layout direction: TB, BT, LR or RL") flag.StringVar(&format, "format", "dot", "format to use: dot, tsort or echarts") flag.StringVar(&theme, "theme", "default", "echarts theme to use, e.g. white, dark, vintage, etc.") flag.Float64Var(&fontSize, "font-size", 0.0, "echarts font size") flag.StringVar(&fontStyle, "font-style", "normal", "echarts font style, e.g. normal, italic, oblique") flag.Parse() // What format to print the graph in: Dot representation or topo sort formats := []string{formatDot, formatTopoSort, formatEcharts} if !slices.Contains(formats, format) { printErrAndExit(errInvalidFormat) } // Valid directions directions := []string{"TB", "BT", "LR", "RL"} direction = strings.ToUpper(direction) if !slices.Contains(directions, direction) { printErrAndExit(errInvalidLayoutDirection) } if relatedOnly && target == "" { printErrAndExit(errNoTargetName) } if highlight && target == "" { printErrAndExit(errNoTargetName) } info, err := os.Stat(makefile) if err != nil { printErrAndExit(err) } if info.IsDir() { printErrAndExit(fmt.Errorf("Invalid Makefile: %s is a directory", makefile)) } // Dump and parse the db reader, err := dumpMakeDb(makefile) if err != nil { printErrAndExit(err) } p := parser.New() g, err := p.Parse(reader) if err != nil { printErrAndExit(err) } // Set layout direction (applicable for Dot format only) attrs := g.GetDotAttributes() attrs["rankdir"] = direction // Highlight, if requested if highlight { if err := highlightVertices(g, target, highlightColor); err != nil { printErrAndExit(err) } } // Keep only vertices related to the specified target if relatedOnly { if err := keepRelatedVerticesOnly(g, target); err != nil { printErrAndExit(err) } } switch format { case formatDot: if err := graph.WriteDot(g, os.Stdout); err != nil { printErrAndExit(err) } case formatTopoSort: collector := g.NewCollector() if err := graph.WalkTopoOrder(g, collector.WalkFunc); err != nil { printErrAndExit(err) } for _, v := range collector.Get() { fmt.Println(v.Value) } case formatEcharts: globalOpts := []charts.GlobalOpts{ charts.WithInitializationOpts( opts.Initialization{ Width: "100%", Height: "95vh", Theme: theme, }, ), charts.WithTooltipOpts(opts.Tooltip{Show: opts.Bool(true)}), } seriesOpts := []charts.SeriesOpts{ charts.WithTreeOpts( opts.TreeChart{ Roam: opts.Bool(true), ExpandAndCollapse: opts.Bool(true), SymbolKeepAspect: opts.Bool(true), Layout: "orthogonal", Orient: direction, InitialTreeDepth: 2, Leaves: &opts.TreeLeaves{ Label: &opts.Label{ Show: opts.Bool(true), Position: "top", FontSize: float32(fontSize), FontStyle: fontStyle, }, }, }, ), charts.WithLabelOpts(opts.Label{ Show: opts.Bool(true), Position: "top", FontSize: float32(fontSize), FontStyle: fontStyle, }), } if err := writeEchartsTree(g, os.Stdout, globalOpts, seriesOpts); err != nil { printErrAndExit(err) } } } // keepRelatedVerticesOnly removes all vertices from the graph, which are not // reachable from the given source vertex. func keepRelatedVerticesOnly(g graph.Graph[string], source string) error { // A dummy walker which we use only so that we can paint the vertices. // The ones which remain graph.White are not related to our source // vertex, since they are not reachable from it. dummyWalker := func(*graph.Vertex[string]) error { return nil } if err := graph.WalkPostOrderDFS(g, source, dummyWalker); err != nil { return err } toRemove := make([]*graph.Vertex[string], 0) for _, v := range g.GetVertices() { if v.Color == graph.White { toRemove = append(toRemove, v) } } for _, v := range toRemove { g.DeleteVertex(v.Value) } return nil } // highlightTarget colors all vertices reachable from source with the given // color. func highlightVertices(g graph.Graph[string], source string, color string) error { walker := func(v *graph.Vertex[string]) error { // Dot attributes v.DotAttributes["color"] = color v.DotAttributes["fillcolor"] = color // Echarts attributes v.EchartsStyle = &opts.ItemStyle{ Color: color, } return nil } return graph.WalkPostOrderDFS(g, source, walker) } // printErrAndExit prints the given error and calls [os.Exit] func printErrAndExit(err error) { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } // dumpMakeDb dumps the internal make(1) database and returns it func dumpMakeDb(file string) (io.Reader, error) { dir := path.Clean(path.Dir(file)) args := []string{ "--makefile", file, "--directory", dir, "--print-data-base", "--no-builtin-rules", "--no-builtin-variables", "--dry-run", "--always-make", "--question", } // Pass in the calling process environment, which might be needed when // evaluating dynamic targets coming from Makefile variables calling out // to shell. Also, sanitize the environment from LC_* vars and set // LC_ALL=C, so that we have deterministic output of the internal // database. env := os.Environ() sanitizedEnv := slices.DeleteFunc(env, func(item string) bool { return strings.HasPrefix(item, "LC_") }) sanitizedEnv = append(sanitizedEnv, "LC_ALL=C") cmd := exec.Command("make", args...) cmd.Env = sanitizedEnv cmd.Dir = dir output, err := cmd.Output() if err != nil { if exiterr, ok := err.(*exec.ExitError); ok { // We ignore exit code 1 and 2 here. Exit code 1 will be // returned when a target is not up-to-date and usually // exit code 2 is returned when a pre-requisite file is // missing. In both cases we can ignore the exit codes, // since we are interested in dumping the internal db // only. exitCode := exiterr.ExitCode() if exitCode != 1 && exitCode != 2 { return nil, err } } else { // Some other error occurred, bubble it up return nil, err } } r := bytes.NewReader(output) return r, nil } // writeEchartsTree generates a tree of the Makefile targets using echarts. func writeEchartsTree(g graph.Graph[string], w io.Writer, globalOpts []charts.GlobalOpts, seriesOpts []charts.SeriesOpts) error { // Build a map of the tree nodes and use it later for building the tree. nodesMap := make(map[string]*opts.TreeData) for _, u := range g.GetVertices() { node := &opts.TreeData{ Name: u.Label, SymbolSize: 15, ItemStyle: u.EchartsStyle, } nodesMap[u.Label] = node } // Topowalk the graph and build the tree data treeData := make([]*opts.TreeData, 0) walker := func(u *graph.Vertex[string]) error { node := nodesMap[u.Label] children := make([]*opts.TreeData, 0) for _, v := range g.GetNeighbourVertices(u.Value) { child := nodesMap[v.Label] children = append(children, child) } node.Children = children // Add only top-level targets to the tree. Children nodes will // already be attached to their respective parents. if u.Degree.In == 0 { treeData = append(treeData, node) } return nil } if err := graph.WalkTopoOrder(g, walker); err != nil { return err } // Attach nodes to a root node. root := opts.TreeData{ Name: "Root", Children: treeData, } tree := charts.NewTree() tree.SetGlobalOptions(globalOpts...) tree.AddSeries("Targets", []opts.TreeData{root}).SetSeriesOptions(seriesOpts...) tree.AddJSFuncStrs(`%MY_ECHARTS%.setOption({"emphasis": {"focus": "descendant"}});`) page := components.NewPage() page.SetPageTitle("makefile-graph") page.AddCharts(tree) return page.Render(w) } ================================================ FILE: examples/Makefile ================================================ # Example Makefile from [1]. # # [1]: https://www.gnu.org/software/make/manual/html_node/Simple-Makefile.html 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 ================================================ FILE: go.mod ================================================ module github.com/dnaeon/makefile-graph go 1.23 require gopkg.in/dnaeon/go-graph.v1 v1.0.2 require ( github.com/go-echarts/go-echarts/v2 v2.5.4 // indirect gopkg.in/dnaeon/go-deque.v1 v1.0.0-20250203064611-7d48f7299755 // indirect gopkg.in/dnaeon/go-priorityqueue.v1 v1.1.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-echarts/go-echarts/v2 v2.5.4 h1:bw0REczgtgI/o7GPqae4AzsiJwwyJvyWwJ7vuM0G6tQ= github.com/go-echarts/go-echarts/v2 v2.5.4/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho= github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gopkg.in/dnaeon/go-deque.v1 v1.0.0-20250203064611-7d48f7299755 h1:wB8d7G8z0rrFgS/Wr16pBVL3MxFp8M+/fTJGcBAxn1k= gopkg.in/dnaeon/go-deque.v1 v1.0.0-20250203064611-7d48f7299755/go.mod h1:mAZ2p0oPgDAeRlUKq3IJOE9RvqVvA3BIA4WKxG/9zDM= gopkg.in/dnaeon/go-graph.v1 v1.0.2 h1:OHShBkXqG65QSQwU0/CQfcqMvJh/+XoR7gZiPprtEVc= gopkg.in/dnaeon/go-graph.v1 v1.0.2/go.mod h1:N9qN2W5CwdzCVmrqtLPmQ3HawuV81HBhuIyUJiMoXT0= gopkg.in/dnaeon/go-priorityqueue.v1 v1.1.1 h1:AS4IIyStH/47cZpPdBk/Db6QXg+UmcDjZvSOoDgGsbU= gopkg.in/dnaeon/go-priorityqueue.v1 v1.1.1/go.mod h1:JNUtwj2QQsBHhsIHNjxdDaSmLW4RZtvJHO8VYtsMkY4= gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: pkg/fixtures/fixtures.go ================================================ // Copyright (c) 2024 Marin Atanasov Nikolov // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // // 1. Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. package fixtures import _ "embed" //go:embed sample_gnu_make_db_3.81.txt var SampleDb_v3_81 string //go:embed sample_gnu_make_db_4.4.1.txt var SampleDb_v4_4_1 string ================================================ FILE: pkg/fixtures/sample_gnu_make_db_3.81.txt ================================================ # GNU Make 3.81 # Copyright (C) 2006 Free Software Foundation, Inc. # This is free software; see the source for copying conditions. # There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A # PARTICULAR PURPOSE. # This program built for i386-apple-darwin11.3.0 mkdir -p /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin go build -o /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph cmd/main.go # Make data base, printed on Fri Apr 20 15:08:33 2024 # Variables # automatic # 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 Sat Apr 20 14:59:39 2024 # Variables # default PREPROCESS.S = $(CPP) $(CPPFLAGS) # environment GO111MODULE = on # default COMPILE.m = $(OBJC) $(OBJCFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c # default ARFLAGS = -rv # default AS = as # default AR = ar # default OBJC = cc # environment XPC_SERVICE_NAME = 0 # default LINK.S = $(CC) $(ASFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_MACH) # default LINK.s = $(CC) $(ASFLAGS) $(LDFLAGS) $(TARGET_MACH) # default MAKE_COMMAND := gmake # automatic @D = $(patsubst %/,%,$(dir $@)) # default COFLAGS = # default COMPILE.mod = $(M2C) $(M2FLAGS) $(MODFLAGS) $(TARGET_ARCH) # makefile (from 'Makefile', line 3) BINARY = $(LOCAL_BIN)/makefile-graph # default .VARIABLES := # environment COMMAND_MODE = unix2003 # automatic %D = $(patsubst %/,%,$(dir $%)) # default LINK.o = $(CC) $(LDFLAGS) $(TARGET_ARCH) # default TEXI2DVI = texi2dvi # automatic ^D = $(patsubst %/,%,$(dir $^)) # automatic %F = $(notdir $%) # default LEX.l = $(LEX) $(LFLAGS) -t # environment LANG = en_US.UTF-8 # default .LOADED := # environment COLORFGBG = 12;8 # environment __CF_USER_TEXT_ENCODING = 0x0:0:0 # default COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c # makefile MAKEFLAGS = Bnpr # default LINK.f = $(FC) $(FFLAGS) $(LDFLAGS) $(TARGET_ARCH) # default TANGLE = tangle # default PREPROCESS.F = $(FC) $(FFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -F # automatic *D = $(patsubst %/,%,$(dir $*)) # environment MFLAGS = -Bnpr # default .SHELLFLAGS := -c # default M2C = m2c # default COMPILE.p = $(PC) $(PFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c # default COMPILE.cpp = $(COMPILE.cc) # default TEX = tex # automatic +D = $(patsubst %/,%,$(dir $+)) # makefile (from 'Makefile', line 1) MAKEFILE_LIST := Makefile # default F77FLAGS = $(FFLAGS) # automatic @F = $(notdir $@) # automatic ?D = $(patsubst %/,%,$(dir $?)) # default COMPILE.def = $(M2C) $(M2FLAGS) $(DEFFLAGS) $(TARGET_ARCH) # default CTANGLE = ctangle # automatic *F = $(notdir $*) # automatic // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // // 1. Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. package parser import ( "bufio" "errors" "fmt" "io" "strings" "gopkg.in/dnaeon/go-graph.v1" ) // ErrInvalidTarget is an error returned by the [Parser] when trying to parse an // invalid target. var ErrInvalidTarget = errors.New("invalid target") const ( // Name of the .PHONY target phonyTarget = ".PHONY" // Marker specifying the beginning of Files section filesSectionMarker = "# Files" ) // Parser is a naive parser which knows how to parse the internal GNU Make // database. type Parser struct{} // New creates a new [Parser] func New() *Parser { p := &Parser{} return p } // readLine reads a single line from the given reader and returns it as a string func (p *Parser) readLine(r *bufio.Reader) (string, error) { var b strings.Builder for { line, isPrefix, err := r.ReadLine() if err != nil { return "", err } b.Write(line) if !isPrefix { break } } return b.String(), nil } // Parse parses the data from the given [io.Reader] line by line and generates a // dependency graph for the discovered targets. func (p *Parser) Parse(r io.Reader) (graph.Graph[string], error) { g := graph.New[string](graph.KindDirected) scanner := bufio.NewReader(r) // Navigate to the `Files` section for { line, err := p.readLine(scanner) if errors.Is(err, io.EOF) { break } if err != nil { return nil, err } if line == filesSectionMarker { break } } // Parse the targets L: for { line, err := p.readLine(scanner) if errors.Is(err, io.EOF) { break L } if err != nil { return nil, err } switch { case line == "# Not a target:": // Skip next line _, err := p.readLine(scanner) if errors.Is(err, io.EOF) { break L } if err != nil { return nil, err } continue case strings.Contains(line, " = "), strings.Contains(line, " := "): // Variables continue case strings.HasPrefix(line, "#"): // Comment continue case strings.HasPrefix(line, "\t"): // Recipe continue case strings.Contains(line, ":"): // Target if err := p.parseVertices(g, line); err != nil { return nil, err } } } return g, nil } // parseVertices parses a single line, which represents a GNU Make target with // it's prerequisites. func (p *Parser) parseVertices(g graph.Graph[string], line string) error { // A typical target with pre-requisites looks like this // // target-name: pre-req-1 pre-req-2 ... items := strings.Split(line, ":") if len(items) < 2 { return fmt.Errorf("%w: %s", ErrInvalidTarget, line) } // Add the vertices and edges fromItems := items[0] toItems := items[1] for _, u := range strings.Split(fromItems, " ") { u = strings.TrimSpace(u) if u == "" { continue } if u != phonyTarget { g.AddVertex(u) } for _, v := range strings.Split(toItems, " ") { v = strings.TrimSpace(v) if v == "" || v == "|" { continue } g.AddVertex(v) if u != phonyTarget { g.AddEdge(u, v) } } } return nil } ================================================ FILE: pkg/parser/parser_test.go ================================================ // Copyright (c) 2024 Marin Atanasov Nikolov // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // // 1. Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. package parser import ( "errors" "fmt" "strings" "testing" "github.com/dnaeon/makefile-graph/pkg/fixtures" "gopkg.in/dnaeon/go-graph.v1" ) func TestWithSampleDatabases(t *testing.T) { type testCase struct { desc string db string wantVs int wantEs int wantErr error } testCases := []testCase{ { desc: "GNU Make 3.81 database", db: fixtures.SampleDb_v3_81, wantVs: 6, wantEs: 2, wantErr: nil, }, { desc: "GNU Make 4.4.1 database", db: fixtures.SampleDb_v4_4_1, wantVs: 6, wantEs: 2, wantErr: nil, }, } p := New() for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { r := strings.NewReader(tc.db) g, err := p.Parse(r) if !errors.Is(err, tc.wantErr) { t.Fatalf("got unexpected error: %s", err) } gotVs := len(g.GetVertices()) gotEs := len(g.GetEdges()) if tc.wantVs != gotVs { t.Errorf("want |V|=%d, got |V|=%d", tc.wantVs, gotVs) } if tc.wantEs != gotEs { t.Errorf("want |E|=%d, got |E|=%d", tc.wantEs, gotEs) } }) } } func TestParseVerticesLine(t *testing.T) { type testCase struct { // A line representing a target line string // expected error wantErr error // number of expected vertices wantVs int // number of expected edges wantEs int } testCases := []testCase{ {line: "foo:", wantErr: nil, wantVs: 1, wantEs: 0}, {line: "foo: bar", wantErr: nil, wantVs: 2, wantEs: 1}, {line: "foo: | bar", wantErr: nil, wantVs: 2, wantEs: 1}, {line: "foo: bar | baz", wantErr: nil, wantVs: 3, wantEs: 2}, {line: "foo: baz qux", wantErr: nil, wantVs: 3, wantEs: 2}, {line: "foo bar: baz qux", wantErr: nil, wantVs: 4, wantEs: 4}, {line: "foo", wantErr: ErrInvalidTarget}, } p := New() for _, tc := range testCases { name := fmt.Sprintf("line(%s), |V|=%d, |E|=%d", tc.line, tc.wantVs, tc.wantEs) t.Run(name, func(t *testing.T) { g := graph.New[string](graph.KindDirected) err := p.parseVertices(g, tc.line) if !errors.Is(err, tc.wantErr) { t.Errorf("want err %s, got err %s", tc.wantErr, err) } gotVs := len(g.GetVertices()) gotEs := len(g.GetEdges()) if gotVs != tc.wantVs { t.Errorf("want |V|=%d, got |V|=%d", tc.wantVs, gotVs) } if gotEs != tc.wantEs { t.Errorf("want |E|=%d, got |E|=%d", tc.wantEs, gotEs) } }) } } ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended" ], "postUpdateOptions": [ "gomodTidy", ] }