#!/usr/bin/env bash set -euo pipefail export GOTOOLCHAIN=local repo_root="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" repo_root="$(realpath "${repo_root}")" cd "${repo_root}" usage() { echo "usage: go.sh check [read|write] [PATH...] | race [PATH...] | staticcheck [PATH...] | vuln [PATH...]" >&2 exit 2 } path_is_excluded() { local path="$1" relative path="$(realpath -m -- "${path}")" relative="${path#"${repo_root}"/}" [[ "${path}" != "${repo_root}" && "${relative}" == "${path}" ]] && return 1 [[ "/${relative}/" =~ /(\.git|\.terraform|\.go-cache|\.go-modcache|target|generated|vendor)/ ]] && return 0 git -C "${repo_root}" check-ignore -q --no-index -- "${relative}" 2>/dev/null } add_module() { local module="$1" [[ " ${selected_modules[*]} " == *" ${module} "* ]] || selected_modules+=("${module}") } discover_modules() { local mod while IFS= read -r mod; do mod="$(dirname "${mod}")" path_is_excluded "${mod}" || modules+=("${mod}") done < <( find "${repo_root}" \ \( -type d \( -name .git -o -name .terraform -o -name .go-cache -o -name .go-modcache -o -name target -o -name generated -o -name vendor \) -prune \) -o \ \( -type f -name go.mod -print \) | sort ) } select_modules() { local path absolute mod selected longest if [[ "$#" -eq 0 ]]; then selected_modules=("${modules[@]}") return fi for path in "$@"; do absolute="$(realpath -m -- "${path}")" [[ "${absolute}" == "${repo_root}" || "${absolute}" == "${repo_root}/"* ]] || { echo "path outside repository: ${path}" >&2 exit 2 } path_is_excluded "${absolute}" && continue case "${absolute#"${repo_root}"/}" in scripts/local/go.sh | scripts/local/check.sh | scripts/local/check-staged.sh | scripts/local/verify.sh | scripts/local/test-go-tooling.sh | lefthook.yml | justfile) selected_modules=("${modules[@]}") continue ;; .ci/*) for mod in "${modules[@]}"; do [[ "${mod}" == "${repo_root}/services/luci" ]] && add_module "${mod}" done continue ;; esac if [[ -d "${absolute}" ]]; then for mod in "${modules[@]}"; do if [[ "${absolute}" == "${mod}" || "${absolute}" == "${mod}/"* || "${mod}" == "${absolute}/"* ]]; then add_module "${mod}" fi done continue fi selected="" longest=0 for mod in "${modules[@]}"; do if [[ "${absolute}" == "${mod}/go.mod" || "${absolute}" == "${mod}/"* ]] && [[ "${#mod}" -gt "${longest}" ]]; then selected="${mod}" longest="${#mod}" fi done [[ -z "${selected}" ]] || add_module "${selected}" done } module_packages() { local import_path dir listing listing="$(go list -e -f '{{if .Dir}}{{.ImportPath}}{{"\t"}}{{.Dir}}{{end}}' ./...)" || return 1 while IFS=$'\t' read -r import_path dir; do [[ -n "${import_path}" ]] || continue path_is_excluded "${dir}" || printf '%s\n' "${import_path}" done <<<"${listing}" } module_files() { local file owner longest mod while IFS= read -r file; do path_is_excluded "${file}" && continue owner="" longest=0 for mod in "${modules[@]}"; do if [[ "${file}" == "${mod}/"* ]] && [[ "${#mod}" -gt "${longest}" ]]; then owner="${mod}" longest="${#mod}" fi done [[ "${owner}" == "$1" ]] && printf '%s\n' "${file}" done < <( find "$1" \ \( -type d \( -name .git -o -name .terraform -o -name .go-cache -o -name .go-modcache -o -name target -o -name generated -o -name vendor \) -prune \) -o \ \( -type f -name '*.go' -print \) | sort ) } run_module() { local action="$1" module="$2" mode="${3:-}" format_diff package_output local -a files packages mapfile -t files < <(module_files "${module}") case "${action}" in check) if [[ "${mode}" == "read" ]]; then if [[ "${#files[@]}" -gt 0 ]]; then format_diff="$(gofmt -d "${files[@]}")" if [[ -n "${format_diff}" ]]; then printf '%s\n' "${format_diff}" return 1 fi fi else [[ "${#files[@]}" -eq 0 ]] || gofmt -w "${files[@]}" fi ;; esac [[ "${#files[@]}" -gt 0 ]] || return 0 ( cd "${module}" package_output="$(module_packages)" || exit 1 [[ -n "${package_output}" ]] || exit 0 mapfile -t packages <<<"${package_output}" case "${action}" in check) go vet "${packages[@]}" go test "${packages[@]}" ;; race) go test -race "${packages[@]}" ;; staticcheck) go tool staticcheck "${packages[@]}" ;; vuln) go tool govulncheck "${packages[@]}" ;; esac ) } action="${1:-}" [[ -n "${action}" ]] || usage shift mode="" case "${action}" in check) mode="write" if [[ "${1:-}" == "read" || "${1:-}" == "write" ]]; then mode="$1" shift fi ;; race | staticcheck | vuln) ;; *) usage ;; esac modules=() selected_modules=() discover_modules select_modules "$@" if [[ "${#selected_modules[@]}" -eq 0 ]]; then echo "no Go modules selected" exit 0 fi if [[ "${action}" == "race" ]]; then [[ "$(go env CGO_ENABLED)" == "1" ]] || { echo "race tests require CGO_ENABLED=1" >&2 exit 1 } compiler="${CC:-$(go env CC)}" command -v "${compiler%% *}" >/dev/null 2>&1 || { echo "race tests require C compiler: ${compiler}" >&2 exit 1 } fi printf 'go modules:' printf ' %s' "${selected_modules[@]#"${repo_root}"/}" printf '\n' for module in "${selected_modules[@]}"; do run_module "${action}" "${module}" "${mode}" done