Skip to content

Commit da029b0

Browse files
authored
fix quickstyle: prefer repo tools and cache goimports (#116)
1 parent d040880 commit da029b0

1 file changed

Lines changed: 164 additions & 12 deletions

File tree

scripts/dev/quickstyle.sh

Lines changed: 164 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,160 @@ source "$(dirname "$SCRIPT")/../../setup/packages.sh"
1111

1212
check_dependencies
1313

14+
# quickstyle_repo_key returns a stable, filesystem-safe key for the current git repo root.
15+
# It is used for naming repo-scoped cache directories under the workflow workdir.
16+
function quickstyle_repo_key() {
17+
echo "${gitroot}" | tr '/' '_'
18+
}
19+
20+
# quickstyle_tools_cache_dir returns (and ensures) the directory where quickstyle may place
21+
# repo-scoped helper binaries (e.g. goimports) managed by workflow, not by the target repo.
22+
function quickstyle_tools_cache_dir() {
23+
local repo_key
24+
repo_key="$(quickstyle_repo_key)"
25+
local tools_dir="${ROX_WORKFLOW_WORKDIR}/quickstyle/tools/${repo_key}"
26+
mkdir -p "${tools_dir}"
27+
echo "${tools_dir}"
28+
}
29+
30+
# Try to resolve a tool path via the repo's Makefile gotools support (make/gotools.mk).
31+
# Many StackRox repos expose `which-<tool>` targets that print the tool's canonical binary; use those
32+
# to avoid relying on whatever happens to be in PATH/GOPATH.
33+
# Args:
34+
# - $1: tool basename (e.g. "roxvet", "golangci-lint", "goimports")
35+
# Output: prints an absolute path to stdout if found.
36+
# Returns: 0 if resolved; non-zero otherwise.
37+
function resolve_tool_via_make() {
38+
local tool="$1"
39+
[[ -n "${tool}" ]] || return 1
40+
[[ -f "${gitroot}/Makefile" ]] || return 1
41+
[[ -x "$(command -v make)" ]] || return 1
42+
local resolved
43+
resolved="$(make -C "${gitroot}" --quiet --no-print-directory "which-${tool}" 2>/dev/null)" || return 1
44+
[[ -n "${resolved}" ]] || return 1
45+
[[ -x "${resolved}" ]] || return 1
46+
printf '%s\n' "${resolved}"
47+
}
48+
49+
# resolve_tool_path attempts to find an executable tool by:
50+
# 1) repo-local Makefile ("make which-<tool>"), then
51+
# 2) PATH lookup.
52+
# Args:
53+
# - $1: tool basename
54+
# Output: prints resolved executable path to stdout.
55+
# Returns: 0 if resolved; non-zero otherwise.
56+
function resolve_tool_path() {
57+
local tool="$1"
58+
local resolved=""
59+
60+
resolved="$(resolve_tool_via_make "${tool}")" && { printf '%s\n' "${resolved}"; return 0; }
61+
62+
resolved="$(command -v "${tool}" 2>/dev/null)" || true
63+
[[ -n "${resolved}" && -x "${resolved}" ]] || return 1
64+
printf '%s\n' "${resolved}"
65+
}
66+
67+
# go_major_minor extracts "<major>.<minor>" from a Go version string.
68+
# Example: "go1.24.9" -> "1.24"
69+
# Args:
70+
# - $1: value like "$(go env GOVERSION)" or "go1.24.9"
71+
# Output: prints "<major>.<minor>" to stdout.
72+
function go_major_minor() {
73+
local goversion="$1"
74+
# Examples: go1.24.9 -> 1.24 ; go1.22.7 -> 1.22
75+
echo "${goversion}" | sed -E 's/^go([0-9]+\.[0-9]+).*/\1/'
76+
}
77+
78+
# tool_built_with_go_major_minor returns the Go "<major>.<minor>" used to build a given binary.
79+
# This uses `go version -m`, so it requires the binary to contain build info.
80+
# Args:
81+
# - $1: path to an executable binary
82+
# Output: prints "<major>.<minor>" to stdout.
83+
# Returns: 0 if build info is available; non-zero otherwise.
84+
function tool_built_with_go_major_minor() {
85+
local tool_path="$1"
86+
[[ -n "${tool_path}" && -x "${tool_path}" ]] || return 1
87+
local first_line
88+
first_line="$(go version -m "${tool_path}" 2>/dev/null | head -n 1)" || return 1
89+
# Format: /path/to/tool: go1.22.7
90+
local tool_go_version
91+
tool_go_version="$(echo "${first_line}" | awk '{print $2}')"
92+
[[ -n "${tool_go_version}" ]] || return 1
93+
go_major_minor "${tool_go_version}"
94+
}
95+
96+
# ensure_goimports resolves a usable goimports binary.
97+
# Resolution order:
98+
# 1) repo-local Makefile ("make which-goimports"), then
99+
# 2) PATH, then
100+
# 3) workflow-managed install into a repo-scoped cache dir under ${ROX_WORKFLOW_WORKDIR}.
101+
# Notes:
102+
# - When installing, we try to pin to the repo's `golang.org/x/tools` module version if present;
103+
# otherwise we fall back to @latest.
104+
# Output: prints the goimports executable path to stdout.
105+
# Returns: 0 if available; non-zero otherwise.
106+
function ensure_goimports() {
107+
local goimports_bin
108+
goimports_bin="$(resolve_tool_path goimports)" && { printf '%s\n' "${goimports_bin}"; return 0; }
109+
110+
local tools_dir
111+
tools_dir="$(quickstyle_tools_cache_dir)"
112+
goimports_bin="${tools_dir}/goimports"
113+
[[ -x "${goimports_bin}" ]] && { printf '%s\n' "${goimports_bin}"; return 0; }
114+
115+
local x_tools_version
116+
x_tools_version="$(cd "${gitroot}" && go list -m -f '{{.Version}}' golang.org/x/tools 2>/dev/null)" || true
117+
local goimports_pkg="golang.org/x/tools/cmd/goimports@latest"
118+
if [[ -n "${x_tools_version}" ]]; then
119+
goimports_pkg="golang.org/x/tools/cmd/goimports@${x_tools_version}"
120+
fi
121+
122+
einfo "goimports not found; installing ${goimports_pkg} into ${tools_dir}..."
123+
if ! env GOBIN="${tools_dir}" go install "${goimports_pkg}"; then
124+
efatal "Failed to install goimports (${goimports_pkg})."
125+
efatal "Either install goimports on PATH, or ensure the repo exposes a 'which-goimports' Make target."
126+
return 1
127+
fi
128+
129+
[[ -x "${goimports_bin}" ]] || return 1
130+
printf '%s\n' "${goimports_bin}"
131+
}
132+
133+
# ensure_roxvet resolves a usable roxvet binary.
134+
# Resolution order:
135+
# 1) repo-local Makefile ("make which-roxvet"), then
136+
# 2) GOPATH/bin/roxvet.
137+
# If GOPATH/bin/roxvet exists but was built with a different Go major/minor than the current toolchain,
138+
# we attempt to rebuild it from ${gitroot}/tools/roxvet (when present) to avoid export-data panics.
139+
# Output: prints the roxvet executable path to stdout.
140+
# Returns: 0 if available; non-zero otherwise.
141+
function ensure_roxvet() {
142+
local roxvet_bin=""
143+
roxvet_bin="$(resolve_tool_via_make roxvet)" && { printf '%s\n' "${roxvet_bin}"; return 0; }
144+
145+
roxvet_bin="$(go env GOPATH)/bin/roxvet"
146+
147+
if [[ -x "${roxvet_bin}" ]]; then
148+
local tool_go_mm
149+
tool_go_mm="$(tool_built_with_go_major_minor "${roxvet_bin}")" || tool_go_mm=""
150+
local cur_go_mm
151+
cur_go_mm="$(go_major_minor "$(go env GOVERSION)")"
152+
153+
# If roxvet is stale (built with different Go major.minor), it can panic on newer export data.
154+
if [[ -n "${tool_go_mm}" && "${tool_go_mm}" != "${cur_go_mm}" ]]; then
155+
ewarn "roxvet (${roxvet_bin}) was built with Go ${tool_go_mm}, but current Go is ${cur_go_mm}; rebuilding..."
156+
if [[ -d "${gitroot}/tools/roxvet" ]]; then
157+
if ! (cd "${gitroot}" && env GOBIN="$(dirname "${roxvet_bin}")" go install ./tools/roxvet); then
158+
ewarn "Failed to rebuild roxvet; continuing with existing binary (may fail)."
159+
fi
160+
fi
161+
fi
162+
fi
163+
164+
[[ -x "${roxvet_bin}" ]] || return 1
165+
printf '%s\n' "${roxvet_bin}"
166+
}
167+
14168
function newlinecheck() {
15169
local check_newlines
16170
check_newlines="$(
@@ -59,8 +213,10 @@ function gostyle() {
59213
einfo "Running go style checks..."
60214
if [[ -f "${gitroot}/.golangci.yml" ]]; then
61215
einfo "golangci-lint"
62-
if [[ -x "$(which golangci-lint)" ]]; then
63-
golangci-lint run --allow-parallel-runners "${godirs[@]}" --fix && (( status == 0 ))
216+
local golangci_lint_bin
217+
golangci_lint_bin="$(resolve_tool_path golangci-lint)" || golangci_lint_bin=""
218+
if [[ -n "${golangci_lint_bin}" && -x "${golangci_lint_bin}" ]]; then
219+
"${golangci_lint_bin}" run --allow-parallel-runners "${godirs[@]}" --fix && (( status == 0 ))
64220
status=$?
65221
else
66222
ewarn "No golangci-lint binary found, but the repo has a config file. Skipping..."
@@ -69,7 +225,9 @@ function gostyle() {
69225

70226
if ! golangci_linter_enabled 'goimports'; then
71227
einfo "imports"
72-
goimports -l -w "${gofiles[@]}" && (( status == 0 ))
228+
local goimports_bin
229+
goimports_bin="$(ensure_goimports)" || { status=1; return "${status}"; }
230+
"${goimports_bin}" -l -w "${gofiles[@]}" && (( status == 0 ))
73231
status=$?
74232
fi
75233

@@ -119,11 +277,9 @@ function gostyle() {
119277
roxvet_includes_validateimports=0
120278

121279
einfo "roxvet"
122-
local rox_vet="$(go env GOPATH)/bin/roxvet"
123-
if [[ ! -x "${rox_vet}" ]] && [[ -d "${gitroot}/tools/roxvet" ]]; then
124-
go install "${gitroot}/tools/roxvet"
125-
fi
126-
if [[ -x "${rox_vet}" ]]; then
280+
local rox_vet
281+
rox_vet="$(ensure_roxvet)" || rox_vet=""
282+
if [[ -n "${rox_vet}" && -x "${rox_vet}" ]]; then
127283
go vet -vettool "${rox_vet}" "${godirs[@]}" && (( status == 0 ))
128284
status=$?
129285
if "${rox_vet}" help | grep -q validateimports; then
@@ -152,10 +308,6 @@ function gostyle() {
152308
}
153309

154310
function golangci_linter_enabled() {
155-
if [[ ! -x "$(command -v "golangci-lint")" ]]; then
156-
return 1
157-
fi
158-
159311
local linter
160312
local enabled_linters
161313
linter="${1}"

0 commit comments

Comments
 (0)