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 <dnaeon@gmail.com>
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
[](https://github.com/dnaeon/makefile-graph/actions/workflows/test.yaml/badge.svg)
[](https://pkg.go.dev/github.com/dnaeon/makefile-graph)
[](https://goreportcard.com/report/github.com/dnaeon/makefile-graph)
[](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.

## 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)`.

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.

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.

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
```

A different theme can be specified using the `--theme` option. This example
renders the same graph using `--theme=white`.

## 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 <dnaeon@gmail.com>
// 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 <dnaeon@gmail.com>
// 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
<D = $(patsubst %/,%,$(dir $<))
# automatic
?F = $(notdir $?)
# environment
LC_CTYPE = UTF-8
# default
CWEAVE = cweave
# automatic
?D = $(patsubst %/,%,$(dir $?))
# automatic
@D = $(patsubst %/,%,$(dir $@))
# automatic
@F = $(notdir $@)
# default
PC = pc
# default
MAKE_VERSION := 3.81
# environment
EDITOR = emacsclient
# default
FC = f77
# makefile (from `Makefile', line 1)
.DEFAULT_GOAL := build
# automatic
%D = $(patsubst %/,%,$(dir $%))
# default
WEAVE = weave
# default
LINK.cpp = $(LINK.cc)
# default
F77 = $(FC)
# default
.VARIABLES :=
# automatic
*F = $(notdir $*)
# default
COMPILE.def = $(M2C) $(M2FLAGS) $(DEFFLAGS) $(TARGET_ARCH)
# default
LEX = lex
# makefile
MAKEFLAGS = rpnB
# environment
MFLAGS = -rpnB
# automatic
*D = $(patsubst %/,%,$(dir $*))
# default
LEX.l = $(LEX) $(LFLAGS) -t
# environment
XPC_SERVICE_NAME = 0
# environment
LC_TERMINAL_VERSION = 3.4.23
# automatic
+D = $(patsubst %/,%,$(dir $+))
# default
COMPILE.r = $(FC) $(FFLAGS) $(RFLAGS) $(TARGET_ARCH) -c
# automatic
+F = $(notdir $+)
# default
GNUMAKE = YES
# variable set hash-table stats:
# Load=138/1024=13%, Rehash=0, Collisions=13/187=7%
# Pattern-specific Variable Values
# No pattern-specific variable values.
# Directories
# . (device 16777233, inode 7066412): 22 files, no impossibilities.
# 22 files, no impossibilities in 1 directories.
# Implicit Rules
# No implicit rules.
# Files
test-cover:
# Phony target (prerequisite of .PHONY).
# Implicit rule search has not been done.
# File does not exist.
# File has not been updated.
# commands to execute (from `Makefile', line 20):
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
.PHONY: get test test-cover build
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# Not a target:
.SUFFIXES:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# Not a target:
Makefile:
# Implicit rule search has been done.
# Last modified 2024-04-19 14:02:58
# File has been updated.
# Successfully updated.
# variable set hash-table stats:
# Load=0/32=0%, Rehash=0, Collisions=0/0=0%
test:
# Phony target (prerequisite of .PHONY).
# Implicit rule search has not been done.
# File does not exist.
# File has not been updated.
# commands to execute (from `Makefile', line 17):
go test -v -race ./...
build: /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph
# Phony target (prerequisite of .PHONY).
# Implicit rule search has not been done.
# File does not exist.
# File has been updated.
# Successfully updated.
# variable set hash-table stats:
# Load=0/32=0%, Rehash=0, Collisions=0/11=0%
/Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph: /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin
# Implicit rule search has not been done.
# Implicit/static pattern stem: `'
# Last modified 1970-01-01 01:59:56
# File has been updated.
# Successfully updated.
# automatic
# @ := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph
# automatic
# % :=
# automatic
# * :=
# automatic
# + := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin
# automatic
# | :=
# automatic
# < := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin
# automatic
# ^ := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin
# automatic
# ? := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin
# variable set hash-table stats:
# Load=8/32=25%, Rehash=0, Collisions=2/19=11%
# commands to execute (from `Makefile', line 9):
go build -o $(BINARY) cmd/main.go
# Not a target:
.DEFAULT:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
/Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin:
# Implicit rule search has not been done.
# Implicit/static pattern stem: `'
# Last modified 1970-01-01 01:59:56
# File has been updated.
# Successfully updated.
# automatic
# @ := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin
# automatic
# % :=
# automatic
# * :=
# automatic
# + :=
# automatic
# | :=
# automatic
# < :=
# automatic
# ^ :=
# automatic
# ? :=
# variable set hash-table stats:
# Load=8/32=25%, Rehash=0, Collisions=1/13=8%
# commands to execute (from `Makefile', line 6):
mkdir -p $(LOCAL_BIN)
get:
# Phony target (prerequisite of .PHONY).
# Implicit rule search has not been done.
# File does not exist.
# File has not been updated.
# commands to execute (from `Makefile', line 14):
go get -v -t -d ./...
# files hash-table stats:
# Load=10/1024=1%, Rehash=0, Collisions=0/30=0%
# VPATH Search Paths
# No `vpath' search paths.
# No general (`VPATH' variable) search path.
# # of strings in strcache: 1
# # of strcache buffers: 1
# strcache size: total = 4096 / max = 4096 / min = 4096 / avg = 4096
# strcache free: total = 4087 / max = 4087 / min = 4087 / avg = 4087
# Finished Make data base on Fri Apr 20 15:08:33 2024
================================================
FILE: pkg/fixtures/sample_gnu_make_db_4.4.1.txt
================================================
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
# GNU Make 4.4.1
# Built for aarch64-apple-darwin23.0.0
# Copyright (C) 1988-2023 Free Software Foundation, Inc.
# License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
# 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
<D = $(patsubst %/,%,$(dir $<))
# environment
ITERM_PROFILE = Default
# default
COMPILE.C = $(COMPILE.cc)
# default
YACC.m = $(YACC) $(YFLAGS)
# default
LINK.C = $(LINK.cc)
# default
MAKE_HOST := aarch64-apple-darwin23.0.0
# default
LINK.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# makefile
SHELL = /bin/sh
# default
LINK.F = $(FC) $(FFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# environment
SHLVL = 2
# environment
MAKELEVEL := 0
# default
MAKE = $(MAKE_COMMAND)
# default
FC = f77
# default
LINT = lint
# default
PC = pc
# default
MAKEFILES :=
# automatic
^F = $(notdir $^)
# default
LEX.m = $(LEX) $(LFLAGS) -t
# default
.LIBPATTERNS = lib%.dylib lib%.a
# default
CPP = $(CC) -E
# default
LINK.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# default
CHECKOUT,v = +$(if $(wildcard $@),,$(CO) $(COFLAGS) $< $@)
# default
COMPILE.f = $(FC) $(FFLAGS) $(TARGET_ARCH) -c
# default
COMPILE.r = $(FC) $(FFLAGS) $(RFLAGS) $(TARGET_ARCH) -c
# default
COMPILE.S = $(CC) $(ASFLAGS) $(CPPFLAGS) $(TARGET_MACH) -c
# automatic
?F = $(notdir $?)
# default
GET = get
# default
LINK.r = $(FC) $(FFLAGS) $(RFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# automatic
+F = $(notdir $+)
# default
MAKEINFO = makeinfo
# 'override' directive
GNUMAKEFLAGS :=
# default
PREPROCESS.r = $(FC) $(FFLAGS) $(RFLAGS) $(TARGET_ARCH) -F
# default
LINK.m = $(OBJC) $(OBJCFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# environment
LOGNAME = dnaeon
# default
LINK.p = $(PC) $(PFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# default
YACC = yacc
# environment
XPC_FLAGS = 0x0
# makefile (from 'Makefile', line 1)
.DEFAULT_GOAL := build
# default
RM = rm -f
# environment
SAVEHIST = 1000
# default
WEAVE = weave
# environment
EDITOR = emacsclient
# environment
USER = dnaeon
# default
MAKE_VERSION := 4.4.1
# 'override' directive
.SHELLSTATUS := 0
# default
F77 = $(FC)
# default
CWEAVE = cweave
# default
YACC.y = $(YACC) $(YFLAGS)
# default
LINK.cpp = $(LINK.cc)
# default
CO = co
# environment
COLORTERM = truecolor
# environment
LC_CTYPE = UTF-8
# default
OUTPUT_OPTION = -o $@
# default
COMPILE.s = $(AS) $(ASFLAGS) $(TARGET_MACH)
# default
MAKE_TERMERR := /dev/ttys002
# environment
HOME = /Users/dnaeon
# default
LEX = lex
# environment
TERM = tmux-256color
# default
LINT.c = $(LINT) $(LINTFLAGS) $(CPPFLAGS) $(TARGET_ARCH)
# default
COMPILE.F = $(FC) $(FFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
# default
.RECIPEPREFIX :=
# automatic
<F = $(notdir $<)
# default
SUFFIXES :=
# default
LD = ld
# default
.FEATURES := target-specific order-only second-expansion else-if shortest-stem undefine oneshell nocomment grouped-target extra-prereqs notintermediate shell-export archives jobserver jobserver-fifo output-sync check-symlink load
# default
CXX = clang++
# default
CC = cc
# makefile (from 'Makefile', line 2)
LOCAL_BIN = $(shell pwd)/bin
# default
COMPILE.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
# variable set hash-table stats:
# Load=145/1024=14%, Rehash=0, Collisions=14/211=7%
# Pattern-specific Variable Values
# No pattern-specific variable values.
# Directories
# . (device 16777231, inode 7066412): 14 files, no impossibilities.
# 14 files, no impossibilities in 1 directories.
# Implicit Rules
# No implicit rules.
# Files
# 'override' directive
/Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin: .SHELLSTATUS := 0
/Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin:
# Implicit rule search has not been done.
# Implicit/static pattern stem: ''
# Last modified 2514-05-30 04:53:03.107374182
# File has been updated.
# Successfully updated.
# automatic
# @ := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin
# automatic
# * :=
# automatic
# < :=
# automatic
# + :=
# automatic
# % :=
# automatic
# ^ :=
# automatic
# ? :=
# automatic
# | :=
# variable set hash-table stats:
# Load=9/32=28%, Rehash=0, Collisions=1/16=6%
# recipe to execute (from 'Makefile', line 6):
mkdir -p $(LOCAL_BIN)
# Not a target:
Makefile:
# Implicit rule search has been done.
# File is secondary (prerequisite of .SECONDARY).
# Last modified 2024-04-19 15:25:48.106252418
# File has been updated.
# Successfully updated.
test-cover:
# Phony target (prerequisite of .PHONY).
# Implicit rule search has not been done.
# File does not exist.
# File has not been updated.
# recipe to execute (from 'Makefile', line 20):
go test -v -race -coverprofile=coverage.txt -covermode=atomic $(shell go list ./... | grep -v cmd)
# 'override' directive
/Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph: .SHELLSTATUS := 0
/Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph: /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin
# Implicit rule search has not been done.
# Implicit/static pattern stem: ''
# Last modified 2514-05-30 04:53:03.107374182
# File has been updated.
# Successfully updated.
# automatic
# @ := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph
# automatic
# * :=
# automatic
# < := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin
# automatic
# + := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin
# automatic
# % :=
# automatic
# ^ := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin
# automatic
# ? := /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin
# automatic
# | :=
# variable set hash-table stats:
# Load=9/32=28%, Rehash=0, Collisions=1/24=4%
# recipe to execute (from 'Makefile', line 9):
go build -o $(BINARY) cmd/main.go
# Not a target:
.DEFAULT:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
build: /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin/makefile-graph
# Phony target (prerequisite of .PHONY).
# Implicit rule search has not been done.
# File does not exist.
# File has been updated.
# Successfully updated.
# variable set hash-table stats:
# Load=0/32=0%, Rehash=0, Collisions=0/15=0%
test:
# Phony target (prerequisite of .PHONY).
# Implicit rule search has not been done.
# File does not exist.
# File has not been updated.
# recipe to execute (from 'Makefile', line 17):
go test -v -race $(shell go list ./... | grep -v cmd)
get:
# Phony target (prerequisite of .PHONY).
# Implicit rule search has not been done.
# File does not exist.
# File has not been updated.
# recipe to execute (from 'Makefile', line 14):
go get -v -t -d ./...
# Not a target:
.SUFFIXES:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
.PHONY: get test test-cover build
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# files hash-table stats:
# Load=10/1024=1%, Rehash=0, Collisions=0/30=0%
# VPATH Search Paths
# No 'vpath' search paths.
# No general ('VPATH' variable) search path.
# strcache buffers: 1 (0) / strings = 24 / storage = 362 B / avg = 15 B
# current buf: size = 8162 B / used = 362 B / count = 24 / avg = 15 B
# strcache performance: lookups = 41 / hit rate = 41%
# hash-table stats:
# Load=24/8192=0%, Rehash=0, Collisions=0/41=0%
# Finished Make data base on Sat Apr 20 14:59:39 2024
================================================
FILE: pkg/parser/parser.go
================================================
// Copyright (c) 2024 Marin Atanasov Nikolov <dnaeon@gmail.com>
// 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 <dnaeon@gmail.com>
// 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",
]
}
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
SYMBOL INDEX (18 symbols across 3 files)
FILE: cmd/makefile-graph/main.go
constant formatDot (line 53) | formatDot = "dot"
constant formatTopoSort (line 54) | formatTopoSort = "tsort"
constant formatEcharts (line 55) | formatEcharts = "echarts"
function main (line 58) | func main() {
function keepRelatedVerticesOnly (line 194) | func keepRelatedVerticesOnly(g graph.Graph[string], source string) error {
function highlightVertices (line 220) | func highlightVertices(g graph.Graph[string], source string, color strin...
function printErrAndExit (line 238) | func printErrAndExit(err error) {
function dumpMakeDb (line 244) | func dumpMakeDb(file string) (io.Reader, error) {
function writeEchartsTree (line 299) | func writeEchartsTree(g graph.Graph[string], w io.Writer, globalOpts []c...
FILE: pkg/parser/parser.go
constant phonyTarget (line 44) | phonyTarget = ".PHONY"
constant filesSectionMarker (line 47) | filesSectionMarker = "# Files"
type Parser (line 52) | type Parser struct
method readLine (line 62) | func (p *Parser) readLine(r *bufio.Reader) (string, error) {
method Parse (line 80) | func (p *Parser) Parse(r io.Reader) (graph.Graph[string], error) {
method parseVertices (line 142) | func (p *Parser) parseVertices(g graph.Graph[string], line string) err...
function New (line 55) | func New() *Parser {
FILE: pkg/parser/parser_test.go
function TestWithSampleDatabases (line 38) | func TestWithSampleDatabases(t *testing.T) {
function TestParseVerticesLine (line 86) | func TestParseVerticesLine(t *testing.T) {
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (51K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 21,
"preview": "---\ngithub: [dnaeon]\n"
},
{
"path": ".github/dependabot.yaml",
"chars": 187,
"preview": "---\nversion: 2\nupdates:\n # Create PRs for dependency updates\n - package-ecosystem: gomod\n directory: /\n schedule"
},
{
"path": ".github/workflows/test.yaml",
"chars": 543,
"preview": "# .github/workflows/test.yaml\non: [push, pull_request]\nname: test\njobs:\n test:\n strategy:\n matrix:\n go-v"
},
{
"path": ".gitignore",
"chars": 49,
"preview": "# Ignore Emacs backup files\n*~\nbin/\ncoverage.txt\n"
},
{
"path": "LICENSE",
"chars": 1337,
"preview": "Copyright (c) 2024 Marin Atanasov Nikolov <dnaeon@gmail.com>\nAll rights reserved.\n\nRedistribution and use in source and "
},
{
"path": "Makefile",
"chars": 552,
"preview": ".DEFAULT_GOAL := build\nLOCAL_BIN ?= $(shell pwd)/bin\nBINARY ?= $(LOCAL_BIN)/makefile-graph\nSRC_DIRS := $(shell go list -"
},
{
"path": "README.md",
"chars": 6074,
"preview": "# makefile-graph\n\n[](http"
},
{
"path": "cmd/makefile-graph/main.go",
"chars": 10297,
"preview": "// Copyright (c) 2024 Marin Atanasov Nikolov <dnaeon@gmail.com>\n// All rights reserved.\n//\n// Redistribution and use in "
},
{
"path": "examples/Makefile",
"chars": 765,
"preview": "# Example Makefile from [1].\n#\n# [1]: https://www.gnu.org/software/make/manual/html_node/Simple-Makefile.html\nedit : mai"
},
{
"path": "go.mod",
"chars": 294,
"preview": "module github.com/dnaeon/makefile-graph\n\ngo 1.23\n\nrequire gopkg.in/dnaeon/go-graph.v1 v1.0.2\n\nrequire (\n\tgithub.com/go-e"
},
{
"path": "go.sum",
"chars": 1452,
"preview": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.m"
},
{
"path": "pkg/fixtures/fixtures.go",
"chars": 1576,
"preview": "// Copyright (c) 2024 Marin Atanasov Nikolov <dnaeon@gmail.com>\n// All rights reserved.\n//\n// Redistribution and use in "
},
{
"path": "pkg/fixtures/sample_gnu_make_db_3.81.txt",
"chars": 5819,
"preview": "# GNU Make 3.81\n# Copyright (C) 2006 Free Software Foundation, Inc.\n# This is free software; see the source for copying"
},
{
"path": "pkg/fixtures/sample_gnu_make_db_4.4.1.txt",
"chars": 10008,
"preview": "mkdir -p /Users/dnaeon/workspace/Projects/golang/src/github.com/dnaeon/makefile-graph/bin\ngo build -o /Users/dnaeon/work"
},
{
"path": "pkg/parser/parser.go",
"chars": 4388,
"preview": "// Copyright (c) 2024 Marin Atanasov Nikolov <dnaeon@gmail.com>\n// All rights reserved.\n//\n// Redistribution and use in "
},
{
"path": "pkg/parser/parser_test.go",
"chars": 3761,
"preview": "// Copyright (c) 2024 Marin Atanasov Nikolov <dnaeon@gmail.com>\n// All rights reserved.\n//\n// Redistribution and use in "
},
{
"path": "renovate.json",
"chars": 163,
"preview": "{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\n \"config:recommended\"\n ],\n \"po"
}
]
About this extraction
This page contains the full source code of the dnaeon/makefile-graph GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (46.2 KB), approximately 14.4k tokens, and a symbol index with 18 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.