0005. controller-gen, kustomize, setup-envtest and golangci-lint come from the Makefile
Status: Accepted · Date: 2026-10-25 · Design section: 0.2, 0.3 · Amends ADR 0002
Decided: Moved controller-gen, kustomize, setup-envtest and golangci-lint out of flake.nix and into Makefile-managed ./bin
Context
ADR 0002 put all four of kubebuilder's build tools in the devShell and
overrode CONTROLLER_GEN, KUSTOMIZE, ENVTEST and GOLANGCI_LINT with Nix
store paths, so that the generated Makefile would use the flake's binaries and
still not need editing. The reasoning was sound and the mechanism (?=) is
real, but running it showed the approach cannot work:
- The rule is
$(CONTROLLER_GEN): $(LOCALBIN), withbin/as a prerequisite.bin/is newer than any store path, so Make always runs the recipe. - The recipe is
go-install-tool, whose first act isrm -f "$(1)". When$(1)is a store path that isrm: cannot remove '/nix/store/…': Read-only file system, andset -eaborts. Every one ofmake build,make test,make manifestsandmake lintdies there, before reaching the tool. - Pre-creating
bin/<tool>-<version>symlinks to satisfy the macro's freshness check doesn't survive a second run either: the macro ends withln -sf "$$(realpath "$(1)-$(3)")" "$(1)", which repointsbin/<tool>at the resolved store path, so the next run'sreadlinkcomparison fails and it tries to reinstall. golangci-lintsettles it. Its target, when.custom-gcl.ymlexists — andkubebuilder initscaffolds one — runsgolangci-lint custom --destination $(LOCALBIN)to build a plugin-carrying binary and thenmvs it over the tool path. A read-only store binary can never serve that purpose.
Meanwhile CLAUDE.md says to never call controller-gen, kustomize or envtest
directly, and to build and generate through the Makefile targets. Tools nobody
is allowed to invoke directly don't need to be on PATH; putting them there
mostly creates the temptation the rule forbids.
Decision
Let the generated Makefile own its own build tools, and take them out of the
devShell: drop kubernetes-controller-tools, kustomize, setup-envtest and
golangci-lint from flake.nix, along with the four environment overrides.
make installs them into ./bin at the versions it pins
(CONTROLLER_TOOLS_VERSION, KUSTOMIZE_VERSION, GOLANGCI_LINT_VERSION, and
ENVTEST_VERSION derived from go.mod), which is how kubebuilder intends its
Makefile to work.
The devShell keeps what a person or an agent actually types: Go, kubebuilder, cobra-cli, talosctl, kubectl, helm, buf and the protoc plugins, goreleaser, govulncheck, QEMU, cni-plugins, Node, make, git.
Consequences
make build,make test,make manifestsandmake lintwork as kubebuilder shipped them, andkubebuilder alpha updatekeeps working because the Makefile stays unedited apart from theGOLANGCI_LINT ?=one-character fix (now unnecessary, but harmless and left in place).- The versions of these four tools are the ones kubebuilder 4.15.0 was tested against, rather than nixpkgs' newer ones. For build tooling that is the safer side to be on.
- This weakens the letter of the rule that every tool comes from
flake.nix: these four come from the Go module proxy instead. It keeps the rule's intent — one toolchain for people, agents and CI, pinned, with no ad-hoc or global installs — because the versions are pinned in the Makefile and resolved the same way everywhere. The rule still holds for everything else. makeneeds the module proxy the first time it runs in a fresh checkout, on top of the network it already needs forgo mod downloadand for envtest's assets. An offline build would have to populate./binand the envtest cache beforehand; see theshortcut:note inflake.nix.