diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0919e9c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: '0 */12 * * *' + +jobs: + + build-test: + name: "Build & Test" + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + go: [ '1.18', '1.17' ] + + steps: + - uses: actions/checkout@v3 + + - name: Setup Go ${{ matrix.go }} + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go }} + + - name: Download Deps + run: go mod download + + - name: Check go.mod + run: | + go mod tidy -v + git diff --name-only --exit-code go.mod || ( git diff && echo "Run go tidy to update go.mod" && false ) + + - name: Build All + run: make build + + - name: Test All + run: make test diff --git a/.gitignore b/.gitignore index 989104a..f4e7b10 100644 --- a/.gitignore +++ b/.gitignore @@ -27,5 +27,5 @@ _testmain.go *.out # Builds -build/ +/.build/ go-callvis diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 97a5ed7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -dist: xenial - -language: go -go: - - "1.13.x" - - "1.12.x" - -install: true - -script: - - make test - - make install diff --git a/Makefile b/Makefile index b1e76d0..5240250 100644 --- a/Makefile +++ b/Makefile @@ -1,34 +1,56 @@ -SHELL = /bin/bash +SHELL := /usr/bin/env bash -o pipefail -GIT_VERSION ?= $(shell git describe --always --tags --always --dirty) +GIT_VERSION ?= $(shell git describe --always --tags --match 'v*' --dirty) +COMMIT ?= $(shell git rev-parse HEAD) +BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) +BUILD_DATE ?= $(shell date +%s) +BUILD_HOST ?= $(shell hostname) +BUILD_USER ?= $(shell id -un) + +PROJECT := go-callvis +BUILD_DIR ?= .build GOOS ?= $(shell go env GOOS) GOARCH = amd64 PLATFORMS := linux-$(GOARCH) darwin-$(GOARCH) -BUILD_DIR ?= ./build -ORG := github.com/ofabry -PROJECT := go-callvis -REPOPATH ?= $(ORG)/$(PROJECT) -BUILD_PACKAGE = $(REPOPATH) - GO_BUILD_TAGS ?= "" -GO_LDFLAGS := "-X main.commit=$(GIT_VERSION)" -GO_FILES := $(shell go list -f '{{join .Deps "\n"}}' $(BUILD_PACKAGE) | grep $(ORG) | xargs go list -f '{{ range $$file := .GoFiles }} {{$$.Dir}}/{{$$file}}{{"\n"}}{{end}}') +GO_LDFLAGS := \ + -X main.commit=$(GIT_VERSION) +GO_FILES := $(shell go list ./... | xargs go list -f '{{ range $$file := .GoFiles }} {{$$.Dir}}/{{$$file}}{{"\n"}}{{end}}') + +ifeq ($(NOSTRIP),) +GO_LDFLAGS += -w -s +endif + +ifeq ($(NOTRIM),) +GO_BUILD_ARGS += -trimpath +endif + +ifeq ($(V),1) +GO_BUILD_ARGS += -v +endif export GO111MODULE=on +export DOCKER_BUILDKIT=1 + +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -install: - go install -tags $(GO_BUILD_TAGS) -ldflags $(GO_LDFLAGS) +build: ## Build go-callvis + go build -tags $(GO_BUILD_TAGS) -ldflags "$(GO_LDFLAGS)" $(GO_BUILD_ARGS) -all: - go build -tags $(GO_BUILD_TAGS) -ldflags $(GO_LDFLAGS) +test: ## Run unit tests + go test -tags $(GO_BUILD_TAGS) -ldflags "$(GO_LDFLAGS)" $(GO_BUILD_ARGS) -short -race ./... + +install: ## Install go-callvis + go install -tags $(GO_BUILD_TAGS) -ldflags "$(GO_LDFLAGS)" $(GO_BUILD_ARGS) $(BUILD_DIR)/$(PROJECT): $(BUILD_DIR)/$(PROJECT)-$(GOOS)-$(GOARCH) cp $(BUILD_DIR)/$(PROJECT)-$(GOOS)-$(GOARCH) $@ $(BUILD_DIR)/$(PROJECT)-%-$(GOARCH): $(GO_FILES) $(BUILD_DIR) - GOOS=$* GOARCH=$(GOARCH) go build -tags $(GO_BUILD_TAGS) -ldflags $(GO_LDFLAGS) -o $@ $(BUILD_PACKAGE) + GOOS=$* GOARCH=$(GOARCH) go build -tags $(GO_BUILD_TAGS) -ldflags "$(GO_LDFLAGS)" -o $@ $(GO_BUILD_ARGS) %.sha256: % shasum -a 256 $< &> $@ @@ -40,13 +62,10 @@ $(BUILD_DIR): cross: $(foreach platform, $(PLATFORMS), $(BUILD_DIR)/$(PROJECT)-$(platform).sha256) -release: cross +release: cross ## Release go-callvis ls -hl $(BUILD_DIR) -test: $(BUILD_DIR)/$(PROJECT) - go test -v $(REPOPATH) - -clean: - rm -rf $(BUILD_DIR) +clean: ## Clean build directory + rm -vrf $(BUILD_DIR) -.PHONY: cross release install test clean +.PHONY: help build test install cross release clean diff --git a/dot.go b/dot.go index 0584b8e..43e8da4 100644 --- a/dot.go +++ b/dot.go @@ -1,85 +1,25 @@ package main import ( - "bytes" - "fmt" - "io" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - "text/template" - - "github.com/goccy/go-graphviz" + "bytes" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "text/template" ) var ( - minlen uint - nodesep float64 - nodeshape string - nodestyle string - rankdir string + minlen uint + nodesep float64 + nodeshape string + nodestyle string + rankdir string ) -// location of dot executable for converting from .dot to .svg -// it's usually at: /usr/bin/dot -var dotExe string - -// dotToImageGraphviz generates a SVG using the 'dot' utility, returning the filepath -func dotToImageGraphviz(outfname string, format string, dot []byte) (string, error) { - if dotExe == "" { - dot, err := exec.LookPath("dot") - if err != nil { - log.Fatalln("unable to find program 'dot', please install it or check your PATH") - } - dotExe = dot - } - - var img string - if outfname == "" { - img = filepath.Join(os.TempDir(), fmt.Sprintf("go-callvis_export.%s", format)) - } else { - img = fmt.Sprintf("%s.%s", outfname, format) - } - cmd := exec.Command(dotExe, fmt.Sprintf("-T%s", format), "-o", img) - cmd.Stdin = bytes.NewReader(dot) - var stderr bytes.Buffer - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("command '%v': %v\n%v", cmd, err, stderr.String()) - } - return img, nil -} - -func dotToImage(outfname string, format string, dot []byte) (string, error) { - if *graphvizFlag { - return dotToImageGraphviz(outfname, format, dot) - } - - g := graphviz.New() - graph, err := graphviz.ParseBytes(dot) - if err != nil { - return "", err - } - defer func() { - if err := graph.Close(); err != nil { - log.Fatal(err) - } - g.Close() - }() - var img string - if outfname == "" { - img = filepath.Join(os.TempDir(), fmt.Sprintf("go-callvis_export.%s", format)) - } else { - img = fmt.Sprintf("%s.%s", outfname, format) - } - if err := g.RenderFilename(graph, graphviz.Format(format), img); err != nil { - return "", err - } - return img, nil -} - const tmplCluster = `{{define "cluster" -}} {{printf "subgraph %q {" .}} {{printf "%s" .Attrs.Lines}} @@ -125,82 +65,120 @@ const tmplGraph = `digraph gocallvis { //==[ type def/func: dotCluster ]=============================================== type dotCluster struct { - ID string - Clusters map[string]*dotCluster - Nodes []*dotNode - Attrs dotAttrs + ID string + Clusters map[string]*dotCluster + Nodes []*dotNode + Attrs dotAttrs } func NewDotCluster(id string) *dotCluster { - return &dotCluster{ - ID: id, - Clusters: make(map[string]*dotCluster), - Attrs: make(dotAttrs), - } + return &dotCluster{ + ID: id, + Clusters: make(map[string]*dotCluster), + Attrs: make(dotAttrs), + } } func (c *dotCluster) String() string { - return fmt.Sprintf("cluster_%s", c.ID) + return fmt.Sprintf("cluster_%s", c.ID) } //==[ type def/func: dotNode ]=============================================== type dotNode struct { - ID string - Attrs dotAttrs + ID string + Attrs dotAttrs } func (n *dotNode) String() string { - return n.ID + return n.ID } //==[ type def/func: dotEdge ]=============================================== type dotEdge struct { - From *dotNode - To *dotNode - Attrs dotAttrs + From *dotNode + To *dotNode + Attrs dotAttrs } //==[ type def/func: dotAttrs ]=============================================== type dotAttrs map[string]string func (p dotAttrs) List() []string { - l := []string{} - for k, v := range p { - l = append(l, fmt.Sprintf("%s=%q", k, v)) - } - return l + l := []string{} + for k, v := range p { + l = append(l, fmt.Sprintf("%s=%q", k, v)) + } + return l } func (p dotAttrs) String() string { - return strings.Join(p.List(), " ") + return strings.Join(p.List(), " ") } func (p dotAttrs) Lines() string { - return fmt.Sprintf("%s;", strings.Join(p.List(), ";\n")) + return fmt.Sprintf("%s;", strings.Join(p.List(), ";\n")) } //==[ type def/func: dotGraph ]=============================================== type dotGraph struct { - Title string - Minlen uint - Attrs dotAttrs - Cluster *dotCluster - Nodes []*dotNode - Edges []*dotEdge - Options map[string]string + Title string + Minlen uint + Attrs dotAttrs + Cluster *dotCluster + Nodes []*dotNode + Edges []*dotEdge + Options map[string]string } func (g *dotGraph) WriteDot(w io.Writer) error { - t := template.New("dot") - for _, s := range []string{tmplCluster, tmplNode, tmplEdge, tmplGraph} { - if _, err := t.Parse(s); err != nil { - return err - } - } - var buf bytes.Buffer - if err := t.Execute(&buf, g); err != nil { - return err - } - _, err := buf.WriteTo(w) - return err + t := template.New("dot") + for _, s := range []string{tmplCluster, tmplNode, tmplEdge, tmplGraph} { + if _, err := t.Parse(s); err != nil { + return err + } + } + var buf bytes.Buffer + if err := t.Execute(&buf, g); err != nil { + return err + } + _, err := buf.WriteTo(w) + return err +} + +func dotToImage(outfname string, format string, dot []byte) (string, error) { + if *graphvizFlag { + return runDotToImageCallSystemGraphviz(outfname, format, dot) + } + + return runDotToImage(outfname, format, dot) +} + +// location of dot executable for converting from .dot to .svg +// it's usually at: /usr/bin/dot +var dotSystemBinary string + +// runDotToImageCallSystemGraphviz generates a SVG using the 'dot' utility, returning the filepath +func runDotToImageCallSystemGraphviz(outfname string, format string, dot []byte) (string, error) { + if dotSystemBinary == "" { + dot, err := exec.LookPath("dot") + if err != nil { + log.Fatalln("unable to find program 'dot', please install it or check your PATH") + } + dotSystemBinary = dot + } + + var img string + if outfname == "" { + img = filepath.Join(os.TempDir(), fmt.Sprintf("go-callvis_export.%s", format)) + } else { + img = fmt.Sprintf("%s.%s", outfname, format) + } + cmd := exec.Command(dotSystemBinary, fmt.Sprintf("-T%s", format), "-o", img) + cmd.Stdin = bytes.NewReader(dot) + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("command '%v': %v\n%v", cmd, err, stderr.String()) + } + return img, nil } diff --git a/dot_cgo.go b/dot_cgo.go new file mode 100644 index 0000000..a93435e --- /dev/null +++ b/dot_cgo.go @@ -0,0 +1,37 @@ +//go:build cgo +// +build cgo + +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "github.com/goccy/go-graphviz" +) + +func runDotToImage(outfname string, format string, dot []byte) (string, error) { + g := graphviz.New() + graph, err := graphviz.ParseBytes(dot) + if err != nil { + return "", err + } + defer func() { + if err := graph.Close(); err != nil { + log.Fatal(err) + } + g.Close() + }() + var img string + if outfname == "" { + img = filepath.Join(os.TempDir(), fmt.Sprintf("go-callvis_export.%s", format)) + } else { + img = fmt.Sprintf("%s.%s", outfname, format) + } + if err := g.RenderFilename(graph, graphviz.Format(format), img); err != nil { + return "", err + } + return img, nil +} diff --git a/dot_nocgo.go b/dot_nocgo.go new file mode 100644 index 0000000..bfde541 --- /dev/null +++ b/dot_nocgo.go @@ -0,0 +1,8 @@ +//go:build !cgo +// +build !cgo + +package main + +func runDotToImage(outfname string, format string, dot []byte) (string, error) { + return runDotToImageCallSystemGraphviz(outfname, format, dot) +}