Files
kandev/scripts/check-make-shells
T
João Manso 263cbab560 fix(backend): set LC_ALL=C for CGO Make targets under Git Bash on Windows (#2720)
Co-authored-by: Joao Manso <X363081@bcpcorp.net>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
2026-08-18 16:53:35 +00:00

225 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# check-make-shells — assert that apps/backend/Makefile's run/dev/start-debug
# and deadcode targets expand to the correct per-shell command line, without
# building them.
#
# The Makefile dispatches on two independent axes:
# * EXE suffix — keyed on $(OS): any Windows build (native cmd/PowerShell
# OR Git Bash) produces kandev.exe.
# * shell syntax — keyed on $(OS)$(MSYSTEM): only native cmd/PowerShell uses
# `set VAR=VAL&` + backslash path + no exec; Unix and Git
# Bash/MSYS use POSIX `VAR=val exec`.
# Git Bash is the trap — it sets OS=Windows_NT yet runs recipes through sh, so
# it must take the POSIX branch while keeping the .exe suffix. Conflating the
# two axes (the pre-fix bug) made Git Bash emit `set VAR=VAL&` into bash, where
# the env var is never exported.
#
# Each shell is simulated deterministically with make's OS=/MSYSTEM=
# command-line overrides, so one host checks the dispatch for all three shells
# without having to run on each. The harness itself is bash (grep, sed), so it
# needs a POSIX shell — it runs in the Linux CI job and under Git Bash/WSL
# locally, not from native cmd.exe/PowerShell. Covers the Make-target dispatch
# that the backend Go CI jobs don't exercise. When cmd.exe is available, the
# script also runs an isolated mkdir probe without building any binaries.
set -euo pipefail
backend_dir="$(cd "$(dirname "$0")/../apps/backend" && pwd)"
status=0
windows_mkdir_recipe=
# expect <label> <OS> <MSYSTEM> <target> <expected recipe tail>
expect() {
local label=$1 os=$2 msys=$3 target=$4 want=$5 marker=${6:-__backend} title_prefix=${7:-} got
# The OS=/MSYSTEM= overrides only steer which branch the Makefile picks;
# $(shell ...) still runs under this harness's POSIX shell, and it expands
# even under `make -n`. So neutralise the build-stamp probes rather than let
# the windows-native case run them: NULL_REDIR=NUL would have sh create a
# junk ./NUL, and BUILD_TIME would spawn PowerShell per case. Blanking
# NULL_REDIR (rather than passing /dev/null) also dodges Git Bash rewriting
# a /dev/null argument to `nul` on its way to native make. None of this
# affects the run/dev/start-debug recipes under assertion.
if [ -n "$title_prefix" ]; then
got=$(KANDEV_WEB_TITLE_PREFIX="$title_prefix" make -C "$backend_dir" OS="$os" MSYSTEM="$msys" NULL_REDIR= BUILD_TIME=simulated -n "$target" 2>/dev/null \
| grep -F "$marker" \
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/[[:space:]]\{1,\}/ /g' \
|| true)
else
got=$(env -u KANDEV_WEB_TITLE_PREFIX make -C "$backend_dir" OS="$os" MSYSTEM="$msys" NULL_REDIR= BUILD_TIME=simulated -n "$target" 2>/dev/null \
| grep -F "$marker" \
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/[[:space:]]\{1,\}/ /g' \
|| true)
fi
if [ "$got" = "$want" ]; then
printf 'ok %-16s %-12s %s\n' "$label" "$target" "$got"
if [ "$label" = windows-native ] && [ "$target" = build-kandev ]; then
windows_mkdir_recipe="$got"
fi
else
printf 'FAIL %-16s %-12s\n want: %s\n got: %s\n' \
"$label" "$target" "$want" "$got"
status=1
fi
}
# Unix — no .exe, POSIX env-prefix + exec.
expect unix '' '' run './bin/kandev __backend'
expect unix '' '' dev 'KANDEV_DEBUG_DEV_MODE=true exec ./bin/kandev __backend'
expect unix '' '' start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='Debug' exec ./bin/kandev __backend"
expect unix '' '' start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='Preview' exec ./bin/kandev __backend" __backend Preview
expect unix '' '' start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='QA Debug' exec ./bin/kandev __backend" __backend "QA Debug"
# Native Windows (cmd/PowerShell) — .exe, `set VAR=VAL&`, backslash path, no exec.
expect windows-native Windows_NT '' run '"bin\kandev.exe" __backend'
expect windows-native Windows_NT '' dev 'set KANDEV_DEBUG_DEV_MODE=true& "bin\kandev.exe" __backend'
expect windows-native Windows_NT '' start-debug 'set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& set "KANDEV_WEB_TITLE_PREFIX=Debug"& "bin\kandev.exe" __backend'
expect windows-native Windows_NT '' start-debug 'set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& set "KANDEV_WEB_TITLE_PREFIX=Preview"& "bin\kandev.exe" __backend' __backend Preview
expect windows-native Windows_NT '' start-debug 'set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& set "KANDEV_WEB_TITLE_PREFIX=QA Debug"& "bin\kandev.exe" __backend' __backend "QA Debug"
# Git Bash/MSYS — .exe suffix but POSIX syntax (recipes run through sh).
expect git-bash Windows_NT MINGW64 run './bin/kandev.exe __backend'
expect git-bash Windows_NT MINGW64 dev 'KANDEV_DEBUG_DEV_MODE=true exec ./bin/kandev.exe __backend'
expect git-bash Windows_NT MINGW64 start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='Debug' exec ./bin/kandev.exe __backend"
expect git-bash Windows_NT MINGW64 start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='Preview' exec ./bin/kandev.exe __backend" __backend Preview
expect git-bash Windows_NT MINGW64 start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='QA Debug' exec ./bin/kandev.exe __backend" __backend "QA Debug"
# deadcode — the analyzer is invoked through the same shell-specific env-prefix
# mechanism, but its command has no __backend marker, so match its module path.
# Git Bash additionally carries the CGO_LOCALE prefix (LC_ALL=C): deadcode compiles
# the -tags fts5 cgo packages, and this mingw gcc mis-decodes its own non-ASCII
# install path under a UTF-8 locale (see docs/public/windows-support.md). LC_ALL=C
# (not LANG=C) is used so it overrides an inherited LC_ALL/LC_CTYPE. Unix and
# native cmd stay locale-free.
expect unix '' '' deadcode \
'GOTOOLCHAIN=go1.26.0 go run golang.org/x/tools/cmd/deadcode@v0.48.0 -tags fts5 ./...' \
'golang.org/x/tools/cmd/deadcode'
expect windows-native Windows_NT '' deadcode \
'set GOTOOLCHAIN=go1.26.0& go run golang.org/x/tools/cmd/deadcode@v0.48.0 -tags fts5 ./...' \
'golang.org/x/tools/cmd/deadcode'
expect git-bash Windows_NT MINGW64 deadcode \
'LC_ALL=C GOTOOLCHAIN=go1.26.0 go run golang.org/x/tools/cmd/deadcode@v0.48.0 -tags fts5 ./...' \
'golang.org/x/tools/cmd/deadcode'
# test — a second CGO_LOCALE consumer, so the prefix is not proven for deadcode
# alone. The `test` recipe is deterministic (no version/timestamp ldflags), so
# assert it in full across all three shells: Git Bash carries LC_ALL=C, Unix and
# native cmd do not.
expect git-bash Windows_NT MINGW64 test \
'LC_ALL=C CGO_ENABLED=1 go test -tags fts5 ./...' \
'go test -tags fts5'
expect unix '' '' test \
'CGO_ENABLED=1 go test -tags fts5 ./...' \
'go test -tags fts5'
expect windows-native Windows_NT '' test \
'set CGO_ENABLED=1& go test -tags fts5 ./...' \
'go test -tags fts5'
# mkdir — the Windows branch must check for the directory before calling cmd's
# mkdir, so rebuilding over an existing bin/ stays quiet while other errors stay
# visible. Unix and Git Bash use idempotent `mkdir -p`, which is silent by design.
expect unix '' '' build-kandev 'mkdir -p "bin"' mkdir
expect windows-native Windows_NT '' build-kandev 'cmd /c if not exist "bin" mkdir "bin"' mkdir
expect git-bash Windows_NT MINGW64 build-kandev 'mkdir -p "bin"' mkdir
# When cmd.exe is available, execute the captured native-Windows recipe against
# a temporary directory. This checks that an existing directory stays quiet.
check_native_mkdir_behavior() {
local recipe=$1 cmd_bin inner probe_name probe_dir stderr_file first_status
if ! cmd_bin=$(command -v cmd.exe 2>/dev/null); then
printf 'skip %-16s cmd.exe is not available\n' "native-mkdir"
return
fi
probe_name=".make-shell-check-$$"
probe_dir="$backend_dir/$probe_name"
stderr_file=$(mktemp)
inner=${recipe#cmd /c }
inner=${inner//bin/$probe_name}
rm -rf "$probe_dir"
first_status=0
(cd "$backend_dir" && "$cmd_bin" /c "$inner") 2>"$stderr_file" || first_status=$?
if [ "$first_status" -ne 0 ] || [ ! -d "$probe_dir" ] || [ -s "$stderr_file" ]; then
printf 'FAIL %-16s first mkdir did not create a quiet directory\n' "native-mkdir"
status=1
rm -rf "$probe_dir"
rm -f "$stderr_file"
return
fi
: >"$stderr_file"
(cd "$backend_dir" && "$cmd_bin" /c "$inner") 2>"$stderr_file" || true
if [ -s "$stderr_file" ]; then
printf 'FAIL %-16s existing directory wrote to stderr\n' "native-mkdir"
status=1
else
printf 'ok %-16s existing directory stayed quiet\n' "native-mkdir"
fi
rm -rf "$probe_dir"
rm -f "$stderr_file"
}
if [ -n "$windows_mkdir_recipe" ]; then
check_native_mkdir_behavior "$windows_mkdir_recipe"
fi
# Static guard: no literal /dev/null inside $(shell ...).
#
# Unlike a recipe, which only ever runs on a platform its target supports,
# $(shell ...) expands at parse time on EVERY invocation, through whatever shell
# make picked. Under native cmd there is no /dev/null: cmd resolves the
# redirection first, fails on the missing \dev directory, and hands the value to
# the fallback — so the probe silently returns its default on Windows while
# every POSIX host looks fine. Use $(NULL_REDIR), which carries the right token
# per shell. This runs on the Linux CI, where the Windows branch is otherwise
# never exercised.
# Fail closed: grep exits 1 for "no matches" but >1 for a real scan error, and
# swallowing the difference would let an unreadable or moved Makefile report ok.
grep_status=0
# `#` starts a comment anywhere in a Makefile line, so the match must not span
# one: [^#]* keeps a prose mention like `FOO = bar # $(shell x 2>/dev/null)`
# from failing CI, and cannot reach past a whole-line comment either. It matches
# empty so a bare `$(shell ...)` in column 0 still counts.
devnull_hits=$(grep -nE '^[[:space:]]*[^#]*\$\(shell[^#]*/dev/null' \
"$backend_dir/../../Makefile" "$backend_dir/Makefile") || grep_status=$?
if [ "$grep_status" -gt 1 ]; then
printf 'FAIL %-16s unable to scan Makefiles (grep exit %s)\n' "static" "$grep_status"
status=1
elif [ -n "$devnull_hits" ]; then
printf 'FAIL %-16s literal /dev/null inside $(shell ...) — use $(NULL_REDIR)\n' "static"
printf ' %s\n' "$devnull_hits"
status=1
else
printf 'ok %-16s %s\n' "static" 'no literal /dev/null inside $(shell ...)'
fi
# Static guard: native cgo host builds carry the $(CGO_LOCALE) prefix.
#
# kandev (-tags fts5) and agentctl both import cgo (go list -deps reports CgoFiles),
# so their native `go build` invokes gcc and fails on a non-ASCII toolchain path
# under Git Bash unless prefixed with $(CGO_LOCALE) — exactly the miss that first
# shipped build-agentctl without it. Cross builds set CGO_ENABLED=0 / GOOS= and do
# not invoke gcc, so they are excluded. Extend the cmd list here if another host
# binary starts importing cgo.
cgo_status=0
cgo_unprefixed=$(grep -nE '\$\(GO\) build' "$backend_dir/Makefile" \
| grep -E '\./cmd/(kandev|agentctl)$' \
| grep -vE 'CGO_LOCALE|GOOS=|\$\(GO_(LINUX|DARWIN)') || cgo_status=$?
if [ "$cgo_status" -gt 1 ]; then
printf 'FAIL %-16s unable to scan Makefile (grep exit %s)\n' "static-cgo" "$cgo_status"
status=1
elif [ -n "$cgo_unprefixed" ]; then
printf 'FAIL %-16s native cgo build without $(CGO_LOCALE):\n' "static-cgo"
printf ' %s\n' "$cgo_unprefixed"
status=1
else
printf 'ok %-16s %s\n' "static-cgo" 'native cgo host builds carry $(CGO_LOCALE)'
fi
if [ "$status" -eq 0 ]; then
echo "All Makefile shell-dispatch checks passed (run/dev/start-debug/deadcode/test/build-kandev across 3 shells, plus static guards)."
fi
exit "$status"