Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions .justfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,21 @@ export LIMEN_LINT_GO_BIN := LIMEN_BUILD / 'tools/limen-lint-go'
default:
@just --list

lint: build-lint-go do::lint::default do::lint::go::default do::lint::go::deadcode lint-lint-go
lint: build-lint-go do::lint::default do::lint::go::default do::lint::go::deadcode
fix: build-lint-go do::fix::default do::fix::go::default
test: do::test::go::default test-lint-go
security: do::security::default

# limen-lint-go is the second binary of limen's release (see .release-go.yaml),
# a nested module so its YAML parser never enters limen's own go.mod. The
# shared Go lanes stop at the root module, so this repository builds, lints
# and tests it itself: the lint runs on the baseline rendered for that module
# with its own .lint-go.yaml, using the golangci-lint the lint go lane built,
# and the tests through the gotestsum the test go lane built.
# shared Go lint lanes enter it as they enter every command that is a module
# of its own, on the baseline rendered with its own .lint-go.yaml; this
# repository builds it first, since the lanes render through it, and runs its
# tests itself, through the gotestsum the test go lane built, which stops at
# the root module.
build-lint-go:
mkdir -p "$LIMEN_BUILD/tools" && GOOS='' GOARCH='' go -C cmd/limen-lint-go build -o "$LIMEN_BUILD/tools/" .

lint-lint-go:
go -C cmd/limen-lint-go mod tidy -diff
go -C cmd/limen-lint-go vet ./...
"$LIMEN_BUILD/tools/limen-lint-go" -C cmd/limen-lint-go render -o "$LIMEN_BUILD/golangci-lint-go.yml"
cd cmd/limen-lint-go && "$LIMEN_BUILD/tools/golangci-lint" run -c "$LIMEN_BUILD/golangci-lint-go.yml" ./...
cd cmd/limen-lint-go && "$LIMEN_BUILD/tools/golangci-lint" fmt --diff -c "$LIMEN_BUILD/golangci-lint-go.yml"

test-lint-go:
cd cmd/limen-lint-go && "$LIMEN_BUILD/tools/gotestsum" -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" ./...

Expand Down
49 changes: 30 additions & 19 deletions .limen/just/fix-go.just
Original file line number Diff line number Diff line change
Expand Up @@ -39,36 +39,47 @@ go_fixers := "wsl_v5,godot,tagalign,nolintlint"
# runs and no other, since `go vet` honours neither the rendered lint
# configuration nor `//nolint` and a fix must not fail on a finding lint
# never reports (book/linting-go.md).
code: (_banner "fix go" "go") (_go-tool "golangci-lint" "github.com/golangci/golangci-lint/v2/cmd/golangci-lint") _golangci-config (_per-platform '"$LIMEN_BUILD/tools/golangci-lint" run --fix --issues-exit-code 0 --enable-only ' + go_fixers + ' -c "$LIMEN_BUILD/golangci.yml"')
code: (_banner "fix go" "go") (_go-tool "golangci-lint" "github.com/golangci/golangci-lint/v2/cmd/golangci-lint") _golangci-config (_per-platform go_modules_init + '; for module in "${modules[@]}"; do (cd "$module" && "$LIMEN_BUILD/tools/golangci-lint" run --fix --issues-exit-code 0 --enable-only ' + go_fixers + ' -c ' + golangci_config + ' ./...); done')
#!/usr/bin/env bash
set -euo pipefail
max=5
pass=0
# Check first: an already-formatted tree costs one --diff and no rewrite.
while ! "$LIMEN_BUILD/tools/golangci-lint" fmt --diff -c "$LIMEN_BUILD/golangci.yml" >/dev/null 2>&1; do
pass=$((pass + 1))
if [ "${pass}" -gt "${max}" ]; then
echo "formatters did not converge after ${max} passes β€” they are fighting each other." >&2
echo "The residual diff below reverses on the next pass; fix the formatter config." >&2
"$LIMEN_BUILD/tools/golangci-lint" fmt --diff -c "$LIMEN_BUILD/golangci.yml" >&2 || true
# Every module the lint lane enters (go_modules_init in lib.just), each on its
# own rendered configuration, as `lint go` checks it.
modules=(.); for d in cmd/*/; do [ -f "${d}go.mod" ] || continue; modules+=("${d%/}"); done
for module in "${modules[@]}"; do
config={{ golangci_config }}
max=5
pass=0
# Check first: an already-formatted tree costs one --diff and no rewrite.
while ! (cd "$module" && "$LIMEN_BUILD/tools/golangci-lint" fmt --diff -c "$config" >/dev/null 2>&1); do
pass=$((pass + 1))
if [ "${pass}" -gt "${max}" ]; then
echo "formatters did not converge after ${max} passes in ${module} β€” they are fighting each other." >&2
echo "The residual diff below reverses on the next pass; fix the formatter config." >&2
(cd "$module" && "$LIMEN_BUILD/tools/golangci-lint" fmt --diff -c "$config" >&2) || true
exit 1
fi
(cd "$module" && "$LIMEN_BUILD/tools/golangci-lint" fmt -c "$config")
done
if ! go -C "$module" vet -asmdecl -buildtag ./...; then
echo "the fixers left ${module} not compiling: repair what they broke above, then run again." >&2
exit 1
fi
"$LIMEN_BUILD/tools/golangci-lint" fmt -c "$LIMEN_BUILD/golangci.yml"
done
if ! go vet -asmdecl -buildtag ./...; then
echo "the fixers left a tree that does not compile: repair what they broke above, then run again." >&2
exit 1
fi

# Tidy go.mod/go.sum, for the module, for .limen/tools/ (the analyzers' module) and
# for each isolated tool module (.limen/tools/<name>/go.mod, see _go-tool).
# Tidy go.mod/go.sum, for every module (go_modules_init in lib.just), for
# .limen/tools/ (the analyzers' module) and for each isolated tool module
# (.limen/tools/<name>/go.mod, see _go-tool).
mod: (_banner "fix go" "mod")
#!/usr/bin/env bash
set -euo pipefail
go mod tidy
modules=(.); for d in cmd/*/; do [ -f "${d}go.mod" ] || continue; modules+=("${d%/}"); done
for module in "${modules[@]}"; do
go -C "$module" mod tidy
done
go -C .limen/tools mod tidy
for m in .limen/tools/*/go.mod; do
[ -f "$m" ] && go -C "${m%/go.mod}" mod tidy
[ -f "$m" ] || continue
go -C "${m%/go.mod}" mod tidy
done

# Update every dependency to its latest version, then tidy.
Expand Down
29 changes: 27 additions & 2 deletions .limen/just/lib.just
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,27 @@ _per-platform +cmd:
platforms=(darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64)
for platform in "${platforms[@]}"; do
echo "β†’ GOOS=${platform%/*} GOARCH=${platform#*/}"
CGO_ENABLED=0 GOOS="${platform%/*}" GOARCH="${platform#*/}" {{ cmd }}
# Exported, not prefixed: a prefix binds to a simple command alone,
# and a leg's command may be a loop over modules. The recipe is its
# own process, so nothing outlives the loop.
export CGO_ENABLED=0 GOOS="${platform%/*}" GOARCH="${platform#*/}"
{{ cmd }}
done
fi

# The Go modules the Go lanes analyze, built into the array `modules` by this
# statement: the root, then every command that is a module of its own
# (cmd/<name>/go.mod, the shape build-go.just builds from inside). A tool
# module under .limen/tools/ is not project code and is not on it. For a
# command a lane hands _per-platform; a recipe body writes the statement
# out instead, since the shell lane lints a body with its interpolations
# opaque and must see the assignment the loop reads.
go_modules_init := 'modules=(.); for d in cmd/*/; do [ -f "${d}go.mod" ] || continue; modules+=("${d%/}"); done'

# The rendered golangci configuration of the module `$module` names in such a
# loop: the root's at golangci.yml, as before; a command's beside it by name.
golangci_config := '"$LIMEN_BUILD/golangci$([ "$module" = . ] || echo "-${module##*/}").yml"'

# The native build of one .limen/tools/go.mod tool into .build/tools/, shared by
# _go-tool and the tools recipes (`{{ go_tool_build }} <pkg>`). GOOS/GOARCH
# cleared: a per-platform leg would cross-compile the tool itself (exec format
Expand Down Expand Up @@ -172,5 +189,13 @@ _vulndb:
# LIMEN_LINT_GO_BIN is for the limen repository itself, which builds it from
# its own tree (same idea as LIMEN_BIN in lint.just).
_golangci-config:
#!/usr/bin/env bash
set -euo pipefail
${LIMEN_LINT_GO_BIN:-limen-lint-go} check "$LIMEN_BUILD/tools/golangci-lint"
${LIMEN_LINT_GO_BIN:-limen-lint-go} render -o "$LIMEN_BUILD/golangci.yml"
# One configuration per module: the baseline with that module's own
# .lint-go.yaml, so a command that is a module of its own carries its
# own carve-outs.
modules=(.); for d in cmd/*/; do [ -f "${d}go.mod" ] || continue; modules+=("${d%/}"); done
for module in "${modules[@]}"; do
${LIMEN_LINT_GO_BIN:-limen-lint-go} -C "$module" render -o {{ golangci_config }}
done
111 changes: 71 additions & 40 deletions .limen/just/lint-go.just
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,21 @@ default: code vet mod licenses nilaway
# .golangci.yml), then once per supported platform (see _per-platform:
# platform-specific files are only analyzed for the GOOS/GOARCH that builds
# them). Built with the toolchain it analyzes, it cannot skew from it β€” the
# reason the prebuilt binary and its lockstep check are gone.
# The formatter is build-graph-independent and runs once. The last step
# enforces the suppression doctrine (book/linting-go.md, "silencing a
# finding"): a finding is silenced by its rule, never by its linter.
code: (_banner "lint go" "go") (_go-tool "golangci-lint" "github.com/golangci/golangci-lint/v2/cmd/golangci-lint") _golangci-config (_per-platform '"$LIMEN_BUILD/tools/golangci-lint" run -c "$LIMEN_BUILD/golangci.yml"') _suppression-check
"$LIMEN_BUILD/tools/golangci-lint" fmt --diff -c "$LIMEN_BUILD/golangci.yml"
# reason the prebuilt binary and its lockstep check are gone. Every module
# (see go_modules_init in lib.just: the root and each command that is one of its
# own), from inside it, on its own rendered configuration: golangci stops at
# a nested go.mod, so a module not entered is a module not linted.
# The formatter is build-graph-independent and runs once per module. The
# last step enforces the suppression doctrine (book/linting-go.md,
# "silencing a finding"): a finding is silenced by its rule, never by its
# linter.
code: (_banner "lint go" "go") (_go-tool "golangci-lint" "github.com/golangci/golangci-lint/v2/cmd/golangci-lint") _golangci-config (_per-platform go_modules_init + '; for module in "${modules[@]}"; do [ "$module" = . ] || echo "β†’ $module"; (cd "$module" && "$LIMEN_BUILD/tools/golangci-lint" run -c ' + golangci_config + ' ./...); done') _suppression-check
#!/usr/bin/env bash
set -euo pipefail
modules=(.); for d in cmd/*/; do [ -f "${d}go.mod" ] || continue; modules+=("${d%/}"); done
for module in "${modules[@]}"; do
(cd "$module" && "$LIMEN_BUILD/tools/golangci-lint" fmt --diff -c {{ golangci_config }})
done

# Polices the shape the book requires (linting-go.md, "silencing a
# finding"): nolintlint checks a directive's form, not which linter it names,
Expand Down Expand Up @@ -95,23 +104,30 @@ _suppression-check:
# neither the rendered configuration nor //nolint, so any analyzer that overlaps with
# golangci's govet would re-report findings a project deliberately silenced,
# and the same exception would then have to be written twice. Every
# supported platform, like the rest (see _per-platform in lib.just); vet
# type-checks without an assembler or C toolchain, so each leg is a few
# seconds. Note `go test` is not a backstop here: the vet subset it runs
# excludes both analyzers.
vet: (_banner "lint go" "vet") (_per-platform "go vet -asmdecl -buildtag ./...")
# supported platform, like the rest (see _per-platform in lib.just), and
# every module (go_modules_init); vet type-checks without an assembler or C
# toolchain, so each leg is a few seconds. Note `go test` is not a backstop
# here: the vet subset it runs excludes both analyzers.
vet: (_banner "lint go" "vet") (_per-platform go_modules_init + '; for module in "${modules[@]}"; do [ "$module" = . ] || echo "β†’ $module"; go -C "$module" vet -asmdecl -buildtag ./...; done')

# go.mod/go.sum tidiness, for the module and for .limen/tools/ (the Go-built tools'
# own module β€” see _go-tool in lib.just). Fails printing the diff; `just do fix
# go mod` repairs.
# go.mod/go.sum tidiness, for every module (go_modules_init) and for .limen/tools/
# (the Go-built tools' own module β€” see _go-tool in lib.just). Fails printing
# the diff; `just do fix go mod` repairs.
mod: (_banner "lint go" "mod")
#!/usr/bin/env bash
set -euo pipefail
go mod tidy -diff
modules=(.); for d in cmd/*/; do [ -f "${d}go.mod" ] || continue; modules+=("${d%/}"); done
for module in "${modules[@]}"; do
[ "$module" = . ] || echo "β†’ $module"
go -C "$module" mod tidy -diff
done
go -C .limen/tools mod tidy -diff
# The isolated tool modules too (.limen/tools/<name>/go.mod, see _go-tool).
# `continue`, not `&&`: the loop's status is the recipe's, and a
# repository with no isolated tool module would otherwise end on 1.
for m in .limen/tools/*/go.mod; do
[ -f "$m" ] && go -C "${m%/go.mod}" mod tidy -diff
[ -f "$m" ] || continue
go -C "${m%/go.mod}" mod tidy -diff
done

# NilAway (go.uber.org/nilaway), Uber's nil-panic analyzer: not a golangci-lint
Expand All @@ -127,29 +143,38 @@ mod: (_banner "lint go" "mod")
nilaway: (_banner "lint go" "nilaway") (_go-tool "nilaway" "go.uber.org/nilaway/cmd/nilaway")
#!/usr/bin/env bash
set -euo pipefail
flags=$(${LIMEN_LINT_GO_BIN:-limen-lint-go} flags nilaway)
mode=$(${LIMEN_LINT_GO_BIN:-limen-lint-go} mode nilaway)
fail=0
# The platform loop of _per-platform, inline: a leg's failure must not
# stop the others, and the verdict depends on the mode.
if [ "{{ go_cgo }}" = "1" ]; then
echo "CGO_ENABLED=1: cross-platform runs are impossible (foreign-platform package" >&2
echo "loading needs that platform's C toolchain) β€” native platform only." >&2
# shellcheck disable=SC2086 # the flags are a list by contract
"$LIMEN_BUILD/tools/nilaway" $flags ./... || fail=1
else
platforms=(darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64)
for platform in "${platforms[@]}"; do
echo "β†’ GOOS=${platform%/*} GOARCH=${platform#*/}"
verdict=0
# Every module (go_modules_init), each with the flags and the mode of its own
# overlay; the platform loop of _per-platform, inline: a leg's failure
# must not stop the others, and the verdict depends on the mode.
modules=(.); for d in cmd/*/; do [ -f "${d}go.mod" ] || continue; modules+=("${d%/}"); done
for module in "${modules[@]}"; do
flags=$(${LIMEN_LINT_GO_BIN:-limen-lint-go} -C "$module" flags nilaway)
mode=$(${LIMEN_LINT_GO_BIN:-limen-lint-go} -C "$module" mode nilaway)
fail=0
[ "$module" = . ] || echo "β†’ $module"
if [ "{{ go_cgo }}" = "1" ]; then
echo "CGO_ENABLED=1: cross-platform runs are impossible (foreign-platform package" >&2
echo "loading needs that platform's C toolchain) β€” native platform only." >&2
# shellcheck disable=SC2086 # the flags are a list by contract
CGO_ENABLED=0 GOOS="${platform%/*}" GOARCH="${platform#*/}" "$LIMEN_BUILD/tools/nilaway" $flags ./... || fail=1
done
fi
if [ "$fail" = 1 ] && [ "$mode" != blocking ]; then
echo "nilaway: findings above are informational for this project (nilaway.blocking is false in .lint-go.yaml)" >&2
exit 0
fi
exit "$fail"
(cd "$module" && "$LIMEN_BUILD/tools/nilaway" $flags ./...) || fail=1
else
platforms=(darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64)
for platform in "${platforms[@]}"; do
echo "β†’ GOOS=${platform%/*} GOARCH=${platform#*/}"
# shellcheck disable=SC2086 # the flags are a list by contract
(cd "$module" && CGO_ENABLED=0 GOOS="${platform%/*}" GOARCH="${platform#*/}" "$LIMEN_BUILD/tools/nilaway" $flags ./...) || fail=1
done
fi
if [ "$fail" = 1 ]; then
if [ "$mode" = blocking ]; then
verdict=1
else
echo "nilaway: findings above are informational for ${module} (nilaway.blocking is false in its .lint-go.yaml)" >&2
fi
fi
done
exit "$verdict"

# Dependency license compliance, once per supported platform (see _per-platform in
# lib.just: each platform pulls its own dependencies). The allowed licenses
Expand All @@ -167,11 +192,17 @@ nilaway: (_banner "lint go" "nilaway") (_go-tool "nilaway" "go.uber.org/nilaway/
# graph, the repository's own LICENSE is the `license` rule's, and go-licenses
# classifies a license by its text, so the Closed-source notice limen writes
# failed every package of such a module.
licenses: (_banner "lint go" "licenses") (_go-tool "go-licenses" "github.com/forkcloser/go-licenses/cmd/go-licenses") (_per-platform 'GOROOT="$(go env GOROOT)" "$LIMEN_BUILD/tools/go-licenses" check --include_tests --ignore "$(go list -m)" $(${LIMEN_LINT_GO_BIN:-limen-lint-go} flags licenses) ./...')
licenses: (_banner "lint go" "licenses") (_go-tool "go-licenses" "github.com/forkcloser/go-licenses/cmd/go-licenses") (_per-platform go_modules_init + '; for module in "${modules[@]}"; do [ "$module" = . ] || echo "β†’ $module"; (cd "$module" && GOROOT="$(go env GOROOT)" "$LIMEN_BUILD/tools/go-licenses" check --include_tests --ignore "$(go list -m)" $(${LIMEN_LINT_GO_BIN:-limen-lint-go} -C . flags licenses) ./...); done')

# Unreachable functions, by whole-program call-graph analysis (deeper than the
# per-package `unused` linter in golangci). -test keeps library-only repos
# analyzable (test binaries serve as roots) and stops dead code from hiding
# behind test-only callers. Informational: native only, not in the default set.
deadcode: (_banner "lint go" "deadcode") (_go-tool "deadcode" "golang.org/x/tools/cmd/deadcode")
"$LIMEN_BUILD/tools/deadcode" -test ./...
#!/usr/bin/env bash
set -euo pipefail
modules=(.); for d in cmd/*/; do [ -f "${d}go.mod" ] || continue; modules+=("${d%/}"); done
for module in "${modules[@]}"; do
[ "$module" = . ] || echo "β†’ $module"
(cd "$module" && "$LIMEN_BUILD/tools/deadcode" -test ./...)
done
10 changes: 6 additions & 4 deletions .limen/just/security-go.just
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import 'lib.just'

default: vuln

# Known-vulnerability scan of the module's dependency graph against the Go
# Known-vulnerability scan of each module's dependency graph against the Go
# vulnerability database (fetched once, see _vulndb in lib.just), once per
# supported platform (see _per-platform in lib.just: call paths and
# dependencies differ by platform). Reports what the module reaches, not what
# it merely imports: a finding is a call path to cut or a bump to take. No
# dependencies differ by platform) and for every module the lint lanes enter
# (go_modules_init: the root and each command that is a module of its own),
# since govulncheck stops at a nested go.mod. Reports what a module reaches,
# not what it merely imports: a finding is a call path to cut or a bump to take. No
# go.mod at all: says so and passes, so `just do security` is safe in every
# repository β€” the guard is a recipe of its own because a dependency cannot
# short-circuit the ones after it, and the tool build would fail first.
Expand All @@ -29,4 +31,4 @@ vuln: (_banner "security go" "vuln")
fi
just do security go _vuln

_vuln: (_go-tool "govulncheck" "golang.org/x/vuln/cmd/govulncheck") _vulndb (_per-platform '"$LIMEN_BUILD/tools/govulncheck" -db "$(cat "$LIMEN_BUILD/vulndb.url")" ./...')
_vuln: (_go-tool "govulncheck" "golang.org/x/vuln/cmd/govulncheck") _vulndb (_per-platform go_modules_init + '; for module in "${modules[@]}"; do [ "$module" = . ] || echo "β†’ $module"; (cd "$module" && "$LIMEN_BUILD/tools/govulncheck" -db "$(cat "$LIMEN_BUILD/vulndb.url")" ./...); done')
Loading
Loading