commit 9e390b8e4d6ad45e83c5ac5343d8d7d4b6161ecf Author: moony Date: Tue Sep 14 08:40:17 2021 -0700 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f0c128e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/bin +/.go +/.push-* +/.container-* +/.dockerfile-* diff --git a/Dockerfile.in b/Dockerfile.in new file mode 100644 index 0000000..e45aea3 --- /dev/null +++ b/Dockerfile.in @@ -0,0 +1,8 @@ +FROM {ARG_FROM} + +ADD bin/{ARG_OS}_{ARG_ARCH}/{ARG_BIN} /{ARG_BIN} + +# This would be nicer as `nobody:nobody` but distroless has no such entries. +USER 65535:65535 + +ENTRYPOINT ["/{ARG_BIN}"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..38e6c29 --- /dev/null +++ b/Makefile @@ -0,0 +1,188 @@ +# The binary to build (just the basename). +BIN := myapp + +# Where to push the docker image. +REGISTRY ?= thockin + +# This version-strategy uses git tags to set the version string +VERSION := $(shell git describe --tags --always --dirty) +# +# This version-strategy uses a manual value to set the version string +#VERSION := 1.2.3 + +### +### These variables should not need tweaking. +### + +SRC_DIRS := cmd pkg # directories which hold app source (not vendored) + +ALL_PLATFORMS := linux/amd64 linux/arm linux/arm64 linux/ppc64le linux/s390x + +# Used internally. Users should pass GOOS and/or GOARCH. +OS := $(if $(GOOS),$(GOOS),$(shell go env GOOS)) +ARCH := $(if $(GOARCH),$(GOARCH),$(shell go env GOARCH)) + +BASEIMAGE ?= gcr.io/distroless/static + +IMAGE := $(REGISTRY)/$(BIN) +TAG := $(VERSION)__$(OS)_$(ARCH) + +BUILD_IMAGE ?= golang:latest + +# If you want to build all binaries, see the 'all-build' rule. +# If you want to build all containers, see the 'all-container' rule. +# If you want to build AND push all containers, see the 'all-push' rule. +all: build + +# For the following OS/ARCH expansions, we transform OS/ARCH into OS_ARCH +# because make pattern rules don't match with embedded '/' characters. + +build-%: + @$(MAKE) build \ + --no-print-directory \ + GOOS=$(firstword $(subst _, ,$*)) \ + GOARCH=$(lastword $(subst _, ,$*)) + +container-%: + @$(MAKE) container \ + --no-print-directory \ + GOOS=$(firstword $(subst _, ,$*)) \ + GOARCH=$(lastword $(subst _, ,$*)) + +push-%: + @$(MAKE) push \ + --no-print-directory \ + GOOS=$(firstword $(subst _, ,$*)) \ + GOARCH=$(lastword $(subst _, ,$*)) + +all-build: $(addprefix build-, $(subst /,_, $(ALL_PLATFORMS))) + +all-container: $(addprefix container-, $(subst /,_, $(ALL_PLATFORMS))) + +all-push: $(addprefix push-, $(subst /,_, $(ALL_PLATFORMS))) + +build: bin/$(OS)_$(ARCH)/$(BIN) + +# Directories that we need created to build/test. +BUILD_DIRS := bin/$(OS)_$(ARCH) \ + .go/bin/$(OS)_$(ARCH) \ + .go/cache + +# The following structure defeats Go's (intentional) behavior to always touch +# result files, even if they have not changed. This will still run `go` but +# will not trigger further work if nothing has actually changed. +OUTBIN = bin/$(OS)_$(ARCH)/$(BIN) +$(OUTBIN): .go/$(OUTBIN).stamp + @true + +# This will build the binary under ./.go and update the real binary iff needed. +.PHONY: .go/$(OUTBIN).stamp +.go/$(OUTBIN).stamp: $(BUILD_DIRS) + @echo "making $(OUTBIN)" + @docker run \ + -i \ + --rm \ + -u $$(id -u):$$(id -g) \ + -v $$(pwd):/src \ + -w /src \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) \ + -v $$(pwd)/.go/cache:/.cache \ + --env HTTP_PROXY=$(HTTP_PROXY) \ + --env HTTPS_PROXY=$(HTTPS_PROXY) \ + $(BUILD_IMAGE) \ + /bin/sh -c " \ + ARCH=$(ARCH) \ + OS=$(OS) \ + VERSION=$(VERSION) \ + ./build/build.sh \ + " + @if ! cmp -s .go/$(OUTBIN) $(OUTBIN); then \ + mv .go/$(OUTBIN) $(OUTBIN); \ + date >$@; \ + fi + +# Example: make shell CMD="-c 'date > datefile'" +shell: $(BUILD_DIRS) + @echo "launching a shell in the containerized build environment" + @docker run \ + -ti \ + --rm \ + -u $$(id -u):$$(id -g) \ + -v $$(pwd):/src \ + -w /src \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) \ + -v $$(pwd)/.go/cache:/.cache \ + --env HTTP_PROXY=$(HTTP_PROXY) \ + --env HTTPS_PROXY=$(HTTPS_PROXY) \ + $(BUILD_IMAGE) \ + /bin/sh $(CMD) + +# Used to track state in hidden files. +DOTFILE_IMAGE = $(subst /,_,$(IMAGE))-$(TAG) + +container: .container-$(DOTFILE_IMAGE) say_container_name +.container-$(DOTFILE_IMAGE): bin/$(OS)_$(ARCH)/$(BIN) Dockerfile.in + @sed \ + -e 's|{ARG_BIN}|$(BIN)|g' \ + -e 's|{ARG_ARCH}|$(ARCH)|g' \ + -e 's|{ARG_OS}|$(OS)|g' \ + -e 's|{ARG_FROM}|$(BASEIMAGE)|g' \ + Dockerfile.in > .dockerfile-$(OS)_$(ARCH) + @docker build -t $(IMAGE):$(TAG) -f .dockerfile-$(OS)_$(ARCH) . + @docker images -q $(IMAGE):$(TAG) > $@ + +say_container_name: + @echo "container: $(IMAGE):$(TAG)" + +push: .push-$(DOTFILE_IMAGE) say_push_name +.push-$(DOTFILE_IMAGE): .container-$(DOTFILE_IMAGE) + @docker push $(IMAGE):$(TAG) + +say_push_name: + @echo "pushed: $(IMAGE):$(TAG)" + +manifest-list: all-push + platforms=$$(echo $(ALL_PLATFORMS) | sed 's/ /,/g'); \ + manifest-tool \ + --username=oauth2accesstoken \ + --password=$$(gcloud auth print-access-token) \ + push from-args \ + --platforms "$$platforms" \ + --template $(REGISTRY)/$(BIN):$(VERSION)__OS_ARCH \ + --target $(REGISTRY)/$(BIN):$(VERSION) + +version: + @echo $(VERSION) + +test: $(BUILD_DIRS) + @docker run \ + -i \ + --rm \ + -u $$(id -u):$$(id -g) \ + -v $$(pwd):/src \ + -w /src \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) \ + -v $$(pwd)/.go/cache:/.cache \ + --env HTTP_PROXY=$(HTTP_PROXY) \ + --env HTTPS_PROXY=$(HTTPS_PROXY) \ + $(BUILD_IMAGE) \ + /bin/sh -c " \ + ARCH=$(ARCH) \ + OS=$(OS) \ + VERSION=$(VERSION) \ + ./build/test.sh $(SRC_DIRS) \ + " + +$(BUILD_DIRS): + @mkdir -p $@ + +clean: container-clean bin-clean + +container-clean: + rm -rf .container-* .dockerfile-* .push-* + +bin-clean: + rm -rf .go bin diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c7349b --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Go app template build environment + +This is a skeleton project for a Go application, which captures the best build +techniques I have learned to date. It uses a Makefile to drive the build (the +universal API to software projects) and a Dockerfile to build a docker image. + +This has only been tested on Linux, and depends on Docker to build. + +## Customizing it + +To use this, simply copy these files and make the following changes: + +Makefile: + - change `BIN` to your binary name + - rename `cmd/myapp` to `cmd/$BIN` + - change `REGISTRY` to the Docker registry you want to use + - maybe change `SRC_DIRS` if you use some other layout + - choose a strategy for `VERSION` values - git tags or manual + +Dockerfile.in: + - maybe change or remove the `USER` if you need + +## Go Modules + +This assumes the use of go modules (which will be the default for all Go builds +as of Go 1.13) and vendoring (which reasonable minds might disagree about). +You will need to run `go mod vendor` to create a `vendor` directory when you +have dependencies. + +## Building + +Run `make` or `make build` to compile your app. This will use a Docker image +to build your app, with the current directory volume-mounted into place. This +will store incremental state for the fastest possible build. Run `make +all-build` to build for all architectures. + +Run `make container` to build the container image. It will calculate the image +tag based on the most recent git tag, and whether the repo is "dirty" since +that tag (see `make version`). Run `make all-container` to build containers +for all architectures. + +Run `make push` to push the container image to `REGISTRY`. Run `make all-push` +to push the container images for all architectures. + +Run `make clean` to clean up. diff --git a/build/' b/build/' new file mode 100644 index 0000000..356b8ce --- /dev/null +++ b/build/' @@ -0,0 +1,39 @@ +#!/bin/sh + +set -o errexit +set -o nounset +set -o pipefail + +export CGO_ENABLED=0 +export GO111MODULE=on +export GOFLAGS="-mod=vendor" + +TARGETS=$(for d in "$@"; do echo ./$d/...; done) + +echo "Running tests:" +go test -installsuffix "static" ${TARGETS} +echo + +echo -n "Checking gofmt: " +ERRS=$(find "$@" -type f -name \*.go | xargs gofmt -l 2>&1 || true) +if [ -n "${ERRS}" ]; then + echo "FAIL - the following files need to be gofmt'ed:" + for e in ${ERRS}; do + echo " $e" + done + echo + exit 1 +fi +echo "PASS" +echo + +echo -n "Checking go vet: " +ERRS=$(go vet ${TARGETS} 2>&1 || true) +if [ -n "${ERRS}" ]; then + echo "FAIL" + echo "${ERRS}" + echo + exit 1 +fi +echo "PASS" +echo diff --git a/build/build.sh b/build/build.sh new file mode 100755 index 0000000..a1bb1ae --- /dev/null +++ b/build/build.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +set -o errexit +set -o nounset +set -o pipefail + +if [ -z "${OS:-}" ]; then + echo "OS must be set" + exit 1 +fi +if [ -z "${ARCH:-}" ]; then + echo "ARCH must be set" + exit 1 +fi +if [ -z "${VERSION:-}" ]; then + echo "VERSION must be set" + exit 1 +fi + +export CGO_ENABLED=0 +export GOARCH="${ARCH}" +export GOOS="${OS}" +export GO111MODULE=on +export GOFLAGS="-mod=vendor" + +go install \ + -installsuffix "static" \ + -ldflags "-X $(go list -m)/pkg/version.VERSION=${VERSION}" \ + ./... diff --git a/build/test.sh b/build/test.sh new file mode 100755 index 0000000..356b8ce --- /dev/null +++ b/build/test.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +set -o errexit +set -o nounset +set -o pipefail + +export CGO_ENABLED=0 +export GO111MODULE=on +export GOFLAGS="-mod=vendor" + +TARGETS=$(for d in "$@"; do echo ./$d/...; done) + +echo "Running tests:" +go test -installsuffix "static" ${TARGETS} +echo + +echo -n "Checking gofmt: " +ERRS=$(find "$@" -type f -name \*.go | xargs gofmt -l 2>&1 || true) +if [ -n "${ERRS}" ]; then + echo "FAIL - the following files need to be gofmt'ed:" + for e in ${ERRS}; do + echo " $e" + done + echo + exit 1 +fi +echo "PASS" +echo + +echo -n "Checking go vet: " +ERRS=$(go vet ${TARGETS} 2>&1 || true) +if [ -n "${ERRS}" ]; then + echo "FAIL" + echo "${ERRS}" + echo + exit 1 +fi +echo "PASS" +echo diff --git a/cmd/myapp/main.go b/cmd/myapp/main.go new file mode 100644 index 0000000..ba27441 --- /dev/null +++ b/cmd/myapp/main.go @@ -0,0 +1,7 @@ +package main + +import "log" + +func main() { + log.Printf("hello, world!") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..010d1f7 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/thockin/go-build-template + +go 1.17 diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 0000000..de9b5be --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,5 @@ +package version + +// VERSION is the app-global version string, which should be substituted with a +// real value during build. +var VERSION = "UNKNOWN"