I think I'll just summarize my findings down here; it didn't all seem obvious and took some googling and tinkering, as some sources are a bit ambiguous on some of the points.
The best practice as endorsed by the Go team (golang/go#25922 (comment)).
//go:build tools
// +build tools
package yourpackage
import (
_ "golang.org/x/tools/cmd/stringer"
// ...
)
Change yourpackage
to the name of the package that you put this in, e.g., use qux
if you put it at the root of your module foo/bar/qux
. Only use tools
when you actually put it in a tools
package (directory).
If you're on Go 1.17+, you don't need the // +build tools
line.
To add the dependencies and the latest versions of these tools to your go.mod
and freshen the module cache, run
go mod tidy
If you use gopls
, configure it to include the tools
build tag, e.g., for VSCode, add the following to your settings.json
:
"gopls": {
"build.buildFlags": ["-tags=tools"],
}
You can install the go.mod
versions of all the tools to your $GOBIN
directory (probably not the best idea, expand for details)
go install $(go list -f '{{join .Imports " "}}' tools.go)
Note that this can break things for you well outside the module that you run this in, so you're likely going to be better off installing specific versions of tools manually, at your discretion.
If you still feel like you want to have the tools built (they really do start faster that way), either of the following sections (on Makefile
and bingo
) might be right up your alley.
Makefiles are very project-specific and everyone's setup is different, but I've found it useful to add this to my Makefile
:
toolsGo := tools.go
toolsDir := bin/tools
toolPkgs := $(shell go list -f '{{join .Imports " "}}' ${toolsGo})
toolCmds := $(foreach tool,$(notdir ${toolPkgs}),${toolsDir}/${tool})
$(foreach cmd,${toolCmds},$(eval $(notdir ${cmd})Cmd := ${cmd}))
go.mod: ${toolsGo}
go mod tidy
touch go.mod
${toolCmds}: go.mod
go build -o $@ $(filter %/$(@F),${toolPkgs})
tools: ${toolCmds}
.PHONY: tools
If you decided to put your tools.go
in a tools
package, you would have to change the toolsGo
variable to tools/tools.go
. It may also make sense to flip the toolsDir
variable to tools/bin
. And maybe put this snippet in a tools/tools.mk
, that you could include to reduce the clutter in your main Makefile
.
You'd probably want to add the toolsDir
directory to your .gitignore
.
The magic $(foreach ... $(eval ...
line defines <toolname>Cmd
variables (e.g. stringerCmd
, mockgenCmd
, etc.) to be used in other recipes in your Makefile
. For (a very rudimentary) example:
wire: ${wireCmd}
${wireCmd} ./...
.PHONY: wire
Running make wire
would now
- run
go mod tidy
to ensurego.mod
is up to date withtools.go
(if the latter is modified more recently than the former) - build
wire
at the version that's defined in yourgo.mod
(if it's not already built), and, finally, - recursively generate all your provider and injector wiring.
You can also execute
make tools
and, if go.mod
is older than tools.go
, it will run go mod tidy
, after which all the tools that you have defined in tools.go
will get built under the toolsDir
(as bin/tools/stringer
, for example).
You can use bingo
to install version-suffixed executables of your module's tools in the global $GOBIN
directory. This is an entirely different approach that avoids conflicts among tools and tool dependencies in larger projects, but it doesn't integrate with your module's go.mod
(which might also be a good thing).
The author introduced this tool in golang/go#25922 (comment) and further expanded on it in golang/go#25922 (comment)