Initial commit.

This commit is contained in:
moony 2021-09-14 08:40:17 -07:00
commit 9e390b8e4d
10 changed files with 368 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/bin
/.go
/.push-*
/.container-*
/.dockerfile-*

8
Dockerfile.in Normal file
View File

@ -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}"]

188
Makefile Normal file
View File

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

45
README.md Normal file
View File

@ -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.

39
build/' Normal file
View File

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

29
build/build.sh Executable file
View File

@ -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}" \
./...

39
build/test.sh Executable file
View File

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

7
cmd/myapp/main.go Normal file
View File

@ -0,0 +1,7 @@
package main
import "log"
func main() {
log.Printf("hello, world!")
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/thockin/go-build-template
go 1.17

5
pkg/version/version.go Normal file
View File

@ -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"