Skip to content

Instantly share code, notes, and snippets.

@aprice
Last active January 8, 2023 16:53
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aprice/7471d17d2dbab7c5d4ea634c4cb5f678 to your computer and use it in GitHub Desktop.
Save aprice/7471d17d2dbab7c5d4ea634c4cb5f678 to your computer and use it in GitHub Desktop.
Full base Golang makefile for developers and Jenkins. Handles installing tools, formatting, linting, cross-compilation, installation, and packaging for distribution.
# This file is intended as a starting point for a customized makefile for a Go project.
#
# Targets:
# all: Format, check, build, and test the code
# setup: Install build/test toolchain dependencies (e.g. gox)
# lint: Run linters against source code
# format: Format the source files
# build: Build the command(s) for target OS/arch combinations
# install: Install the command(s)
# clean: Clean the build/test artifacts
# report: Generate build/test reports
# check: Run tests
# bench: Run benchmarks
# dist: zip/tar binaries & documentation
# debug: print parameters
#
# Parameters:
# VERSION: release version in semver format
# BUILD_TAGS: additional build tags to pass to go build
# DISTDIR: path to save distribution files
# RPTDIR: path to save build/test reports
#
# Assumptions:
# - Your package contains a cmd/ package, containing a directory for each produced binary.
# - You have cloc installed and accessible in the PATH.
# - Your GOPATH and GOROOT are set correctly.
# - Your makefile is in the root of your package and does not have a space in its file name.
# - Your root package contains global string variables Version and Build, to receive the bild version number and commit ID, respectively.
#
# Features:
# - report generates files that can be consumed by Jenkins, as well as a list of external dependencies.
# - setup installs all the tools aside from cloc.
# - Works on Windows and with paths containing spaces.
# - Works when executing from outside the makefile directory using -f.
# - Targets are useful both in CI and developer workstations.
# - Handles cross-compiation for multiple OSes and architectures.
# - Bundles binaries and documentation into compressed archives, using tar/gz for Linux and Darwin, and zip for Windows.
# Parameters
PKG = github.com/me/myapp
NAME = myapp
DOC = README LICENSE
# Replace backslashes with forward slashes for use on Windows.
# Make is !@#$ing weird.
E :=
BSLASH := \$E
FSLASH := /
# Directories
WD := $(subst $(BSLASH),$(FSLASH),$(shell pwd))
MD := $(subst $(BSLASH),$(FSLASH),$(shell dirname "$(realpath $(lastword $(MAKEFILE_LIST)))"))
PKGDIR = $(MD)
CMDDIR = $(PKGDIR)/cmd
DISTDIR ?= $(WD)/dist
RPTDIR ?= $(WD)/reports
GP = $(subst $(BSLASH),$(FSLASH),$(GOPATH))
# Parameters
VERSION ?= $(shell git -C "$(MD)" describe --tags --dirty=-dev)
COMMIT_ID := $(shell git -C "$(MD)" rev-parse HEAD | head -c8)
BUILD_TAGS ?= release
CMDPKG = $(PKG)/cmd
CMDS := $(shell find "$(CMDDIR)/" -mindepth 1 -maxdepth 1 -type d | sed 's/ /\\ /g' | xargs -n1 basename)
BENCHCPUS ?= 1,2,4
# Commands
GOCMD = go
ARCHES ?= 386 amd64
OSES ?= windows linux darwin
OUTTPL = $(DISTDIR)/$(NAME)-$(VERSION)-{{.OS}}_{{.Arch}}/{{.Dir}}
LDFLAGS = -X $(PKG).Version=$(VERSION) -X $(PKG).Build=$(COMMIT_ID)
GOBUILD = gox -rebuild -gocmd="$(GOCMD)" -arch="$(ARCHES)" -os="$(OSES)" -output="$(OUTTPL)" -tags "$(BUILD_TAGS)" -ldflags "$(LDFLAGS)"
GOCLEAN = $(GOCMD) clean
GOINSTALL = $(GOCMD) install -a -tags "$(BUILD_TAGS)" -ldflags "$(LDFLAGS)"
GOTEST = $(GOCMD) test -v -tags "$(BUILD_TAGS)"
GOLINT = gometalinter --deadline=30s --tests --disable=aligncheck --disable=gocyclo --disable=gotype
GODEP = $(GOCMD) get -d -t
GOFMT = goreturns -w
GOBENCH = $(GOCMD) test -v -tags "$(BUILD_TAGS)" -cpu=$(BENCHCPUS) -run=NOTHING -bench=. -benchmem -outputdir "$(RPTDIR)"
GZCMD = tar -czf
ZIPCMD = zip
SHACMD = sha256sum
SLOCCMD = cloc --by-file --xml --exclude-dir="vendor" --include-lang="Go"
XUCMD = go2xunit
# Dynamic Targets
INSTALL_TARGETS := $(addprefix install-,$(CMDS))
.PHONY: all
all: debug setup dep format lint test bench build dist
setup: setup-dirs setup-build setup-format setup-lint setup-reports
setup-reports: setup-dirs
go get github.com/tebeka/go2xunit
setup-build: setup-dirs
go get github.com/mitchellh/gox
setup-format: setup-dirs
go get github.com/sqs/goreturns
setup-lint: setup-dirs
go get github.com/alecthomas/gometalinter
gometalinter --install
setup-dirs:
mkdir -p "$(RPTDIR)"
mkdir -p "$(DISTDIR)"
clean:
$(GOCLEAN) $(PKG)
rm -rf "$(DISTDIR)"/*
rm -f "$(RPTDIR)"/*
format:
$(GOFMT) "$(PKGDIR)"
dep:
$(GODEP) $(PKG)/...
lint: setup-dirs dep
$(GOLINT) "$(PKGDIR)" | tee "$(RPTDIR)/lint.out"
check: setup-dirs clean dep
$(GOTEST) $$(go list "$(PKG)/..." | grep -v /vendor/) | tee "$(RPTDIR)/test.out"
bench: setup-dirs clean dep
$(GOBENCH) $$(go list "$(PKG)/..." | grep -v /vendor/) | tee "$(RPTDIR)/bench.out"
report: check
cd "$(PKGDIR)";$(SLOCCMD) --out="$(RPTDIR)/cloc.xml" . | tee "$(RPTDIR)/cloc.out"
cat "$(RPTDIR)/test.out" | $(XUCMD) -output "$(RPTDIR)/tests.xml"
go list -f '{{join .Deps "\n"}}' "$(CMDPKG)/..." | sort | uniq | xargs -I {} sh -c "go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}' {} | tee -a '$(RPTDIR)/deps.out'"
build: $(CMDS)
$(CMDS): setup-dirs dep
$(GOBUILD) "$(CMDPKG)/$@" | tee "$(RPTDIR)/build-$@.out"
install: $(INSTALL_TARGETS)
$(INSTALL_TARGETS):
$(GOINSTALL) "$(CMDPKG)/$(subst install-,,$@)"
dist: clean build
for docfile in $(DOC); do \
for dir in "$(DISTDIR)"/*; do \
cp "$(PKGDIR)/$$docfile" "$$dir/"; \
done; \
done
cd "$(DISTDIR)"; for dir in ./*linux*; do $(GZCMD) "$(basename "$$dir").tar.gz" "$$dir"; done
cd "$(DISTDIR)"; for dir in ./*windows*; do $(ZIPCMD) "$(basename "$$dir").zip" "$$dir"; done
cd "$(DISTDIR)"; for dir in ./*darwin*; do $(GZCMD) "$(basename "$$dir").tar.gz" "$$dir"; done
cd "$(DISTDIR)"; find . -maxdepth 1 -type f -printf "$(SHACMD) %P | tee \"./%P.sha\"\n" | sh
$(info "Built v$(VERSION), build $(COMMIT_ID)")
debug:
$(info MD=$(MD))
$(info WD=$(WD))
$(info PKG=$(PKG))
$(info PKGDIR=$(PKGDIR))
$(info DISTDIR=$(DISTDIR))
$(info VERSION=$(VERSION))
$(info COMMIT_ID=$(COMMIT_ID))
$(info BUILD_TAGS=$(BUILD_TAGS))
$(info CMDS=$(CMDS))
$(info BUILD_TARGETS=$(BUILD_TARGETS))
$(info INSTALL_TARGETS=$(INSTALL_TARGETS))
@chrplr
Copy link

chrplr commented Dec 20, 2022

Hello,

Thanks you for this great Makefile that will save me a lot of time!

I just wanted to report tiny issues that I encountered while using it.

On a first run, make signaled that gometalinter was not installed:

gometalinter --install
make: gometalinter: Command not found
make: *** [Makefile:106: setup-lint] Error 127

So I grabbed the appropriate binaries from gometalinter repo, and now gometalinter -install spits:

gometalinter: error: could not find vendored linters in GOPATH="/home/cp983411/go"

I will have to find a solution to this, but in order to advance, I disabled the lint and setup-lint targets in the Makefile.

Then there was an issue with goreturn:

/bin/sh: 1: goreturns: not found
make: *** [Makefile:115: format] Error 127

which I solved with go install github.com/sqs/goreturns

After that, make runs smoothly. I still have to solve the issue with gometalinter.

Note: I am a beginner in Go, and I do not fully grasp the difference between go get vs. go install . It seems that when you want to import and compile go code that will result in executables in $GOBIN, you need to use go install. Yet I see that you use go get in the setup-* targets (?)

@chrplr
Copy link

chrplr commented Dec 20, 2022

Suggestion: Might be a good idea to add a target to push the binaries to github using ghr
(see https://www.process-one.net/blog/distributing-prebuilt-go-binaries-on-github-with-gox/)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment