-
Notifications
You must be signed in to change notification settings - Fork 174
Expand file tree
/
Copy pathgotools.mk
More file actions
145 lines (128 loc) · 6.95 KB
/
gotools.mk
File metadata and controls
145 lines (128 loc) · 6.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# gotools.mk
# Simplified installation & usage of Go-based tools
#
# Input variables:
# GOTOOLS_PROJECT_ROOT: the project root directory; defaults to $(CURDIR)
# GOTOOLS_ROOT: the directory in which this file stores auxiliary data (should be .gitignore'd); defaults to
# $(GOTOOLS_PROJECT_ROOT)/.gotools
# GOTOOLS_BIN: the directory in which binaries are stored; defaults to $(GOTOOLS_ROOT)/bin.
#
# This file defines a single (user-facing) macro, `go-tool`, which can be invoked via
# $(call go-tool VARNAME, go-pkg [, module-root])
# where go-pkg can be:
# - an absolute Go import path with an explicit version, e.g.,
# github.com/golangci/golangci-lint/cmd/golangci-lint@v1.49.0. In this case, the tool is installed via `go install`,
# and module information from the local workspace is ignored, in accordance with the normal behavior of go install
# with an explicit version given.
# - an absolute Go import path WITHOUT a version, e.g., github.com/golangci/golangci-lint/cmd/golangci-lint. In this
# case, the tool is installed via `go install` from the module rooted at $(GOTOOLS_PROJECT_ROOT), or, if
# module-root is given, from the module rooted at that (relative) path. I.e., go-pkg must be provided by a module
# listed as a requirement in <module-root>/go.mod.
# - a relative Go import path (WITHOUT a version), e.g., ./tools/roxvet. In this case, the tool is installed via
# `go install` from the module rooted at $(GOTOOLS_PROJECT_ROOT).
#
# Invoking go-tool will set up Makefile rules to build the tools, using reasonable strategies for caching to avoid
# building a tool multiple times. In particular:
# - when using an absolute Go import path with a version, the rule is set up such that the `go install` command is only
# run once.
# - when using an absolute Go import path without a version, the rule is set up such that the `go install` command is
# re-run only when the respective go.mod file changes.
# - when using a relative Go import path, the rule is set up such that the `go install` command is re-run on every
# `make` invocation.
# Note that `go install` uses a pretty effective caching strategy under the hood, so even with relative import path,
# you should not expect noticeable latency.
#
# In addition to setting up the rules for building, invoking go-tool will also set the value of the variable `VARNAME`
# to the (canonical) location of the respective tool's binary, which is $(GOTOOLS_BIN)/<binary basename>. `$(VARNAME)`
# should be used as the only way of both invoking the tool in the Makefile as well as expressing a dependency on the
# installation of the tool.
# For use in non-Makefile scripts, a target `which-<tool>` is added, whhere <tool> is the basename of the tool binary.
# This target prints the canonical location of the binary and, if necessary, builds it. Note that invocations of
# `make which-tool` should be made with the flags `--quiet --no-print-directory` set, as otherwise the output gets
# clobbered.
#
# This file also defines two static, global targets:
# gotools-clean: this removes all gotools-related data
# gotools-all: this builds all gotools.
GOTOOLS_PROJECT_ROOT ?= $(CURDIR)
GOTOOLS_ROOT ?= $(GOTOOLS_PROJECT_ROOT)/.gotools
GOTOOLS_BIN ?= $(GOTOOLS_ROOT)/bin
# RUN_WITH_RETRY_FN provides a shell function wrapper for retrying commands.
# The function retries up to 8 times with exponential backoff.
#
# Note on escaping: $$$$ is used instead of $$ because this variable is expanded twice:
# - First by Make when defining the recipe ($$$$i becomes $$i)
# - Then by the shell when executing the recipe ($$i becomes $i)
# Without the quadruple $, the shell would receive empty variables.
RUN_WITH_RETRY_FN = run_with_retry() { \
attempts=8; \
for i in $$$$(seq $$$$attempts); do \
"$$$$@" && return 0; \
[[ $$$$i -eq $$$$attempts ]] && return 1; \
echo "Retry $$$$i/$$$$attempts failed. Retrying after $$$$((i**2)) seconds..."; \
sleep "$$$$((i**2))"; \
done \
}
_GOTOOLS_ALL_GOTOOLS :=
define go-tool-impl
# The variable via which the executable can be referenced
_gotools_var_name := $(strip $(1))
# The importable Go package path that contains the "main" package for the tool
_gotools_pkg := $(firstword $(subst @, ,$(strip $(2))))
# The version of the tool (if a version was explicitly specified)
_gotools_version := $(word 2,$(subst @, ,$(strip $(2))))
# The folder containing the go.mod file, if not the root folder
ifeq ($(strip $(3)),)
_gotools_mod_root := $(GOTOOLS_PROJECT_ROOT)
else
_gotools_mod_root := $(strip $(3))
endif
# We need to strip a `/v2` (etc.) suffix to derive the tool binary's basename.
_gotools_bin_name := $$(notdir $$(shell echo "$$(_gotools_pkg)" | sed -E 's@/v[[:digit:]]+$$$$@@g'))
_gotools_canonical_bin_path := $(GOTOOLS_BIN)/$$(_gotools_bin_name)
$$(_gotools_var_name) := $$(_gotools_canonical_bin_path)
.PHONY: which-$$(_gotools_bin_name)
which-$$(_gotools_bin_name):
@$(MAKE) $$($(strip $(1))) >&2
@echo $$($(strip $(1)))
ifneq ($(filter ./%,$(2)),)
# Tool is built from local files. We have to introduce a phony target and let the Go compiler
# do all the caching.
.PHONY: $$(_gotools_canonical_bin_path)
$$(_gotools_canonical_bin_path):
@echo "+ $$(notdir $$@)"
$$(SILENT)$(RUN_WITH_RETRY_FN); run_with_retry env GOBIN="$$(dir $$@)" go install "$(strip $(2))"
else
# Tool is specified with version, so we don't take any info from the go.mod file.
# We install the tool into a location that is version-dependent, and build it via this target. Since the name of
# the tool under that path is version-dependent, we never have to rebuild it, as it's either the correct version, or
# does not exist.
ifneq ($$(_gotools_version),)
_gotools_versioned_bin_path := $(GOTOOLS_ROOT)/versioned/$$(_gotools_pkg)/$$(_gotools_version)/$$(_gotools_bin_name)
$$(_gotools_versioned_bin_path):
@echo "+ $$(notdir $$@)"
$$(SILENT)$(RUN_WITH_RETRY_FN); run_with_retry env GOBIN="$$(dir $$@)" go install "$(strip $(2))"
# To make the tool accessible in the canonical location, we create a symlink. This only depends on the versioned path,
# i.e., only needs to be recreated when the version is bumped.
$$(_gotools_canonical_bin_path): $$(_gotools_versioned_bin_path)
@mkdir -p "$(GOTOOLS_BIN)"
$$(SILENT)ln -sf "$$<" "$$@"
else
# Tool is specified with an absolute path without a version. Take info from go.mod file in the respective directory.
$$(_gotools_canonical_bin_path): $$(_gotools_mod_root)/go.mod $$(_gotools_mod_root)/go.sum
@echo "+ $$(notdir $$@)"
$$(SILENT)cd "$$(dir $$<)" && $(RUN_WITH_RETRY_FN); run_with_retry env GOBIN="$$(dir $$@)" go install "$(strip $(2))"
endif
endif
_GOTOOLS_ALL_GOTOOLS += $$(_gotools_canonical_bin_path)
endef
go-tool = $(eval $(call go-tool-impl,$(1),$(2),$(3)))
.PHONY: gotools-clean
gotools-clean:
@echo "+ $@"
@git clean -dfX "$(GOTOOLS_ROOT)" # don't use rm -rf to avoid catastrophes
.PHONY: gotools-all
gotools-all:
@# these cannot be dependencies, as we need `$(_GOTOOLS_ALL_GOTOOLS)` to be
@# evaluated when the target is actually run.
$(MAKE) $(_GOTOOLS_ALL_GOTOOLS)