fix(build): make Makefile $(shell) probes work under native Windows cmd (#1953)

This commit is contained in:
João Manso
2026-07-26 11:40:55 +01:00
committed by GitHub
parent 488d76edf4
commit cc340ae5f2
4 changed files with 86 additions and 7 deletions
+4
View File
@@ -5,6 +5,9 @@ on:
branches: [main]
paths:
- "apps/backend/**"
# check-make-shells also lints the root Makefile's $(shell ...) probes, so
# a change there has to be able to trip it.
- "Makefile"
- "scripts/check-make-shells"
- ".github/workflows/backend-tests.yml"
- "!**/*.md"
@@ -14,6 +17,7 @@ on:
branches: [main]
paths:
- "apps/backend/**"
- "Makefile"
- "scripts/check-make-shells"
- ".github/workflows/backend-tests.yml"
- "!**/*.md"
+14 -2
View File
@@ -22,6 +22,18 @@ else
RMDIR = rm -rf
endif
# stderr redirect for $(shell ...) probes. Keyed on $(OS)$(MSYSTEM) rather than
# $(OS) alone: Git Bash sets OS=Windows_NT yet runs commands through sh, so it
# needs the POSIX path. Under native cmd there is no /dev/null, and cmd resolves
# redirections before running the command — `2>/dev/null` would abort the probe
# and silently force its fallback. Holds the whole token so callers can blank it.
# Mirrors apps/backend/Makefile.
ifeq ($(OS)$(MSYSTEM),Windows_NT)
NULL_REDIR := 2>NUL
else
NULL_REDIR := 2>/dev/null
endif
# Colors for terminal output
RESET := \033[0m
BOLD := \033[1m
@@ -33,10 +45,10 @@ YELLOW := \033[33m
MAGENTA := \033[35m
VERBOSE ?= 0
NODE ?= $(shell command -v node 2>/dev/null || echo node)
NODE ?= $(shell command -v node $(NULL_REDIR) || echo node)
SERVICE_LAUNCHER := $(CURDIR)/dist/kandev/bin/kandev
SERVICE_BUNDLE_DIR := $(CURDIR)/dist/kandev
SERVICE_VERSION := $(shell git rev-parse --short HEAD 2>/dev/null || echo dev)
SERVICE_VERSION := $(shell git rev-parse --short HEAD $(NULL_REDIR) || echo dev)
SERVICE_ENV := KANDEV_BUNDLE_DIR="$(SERVICE_BUNDLE_DIR)" KANDEV_VERSION="$(SERVICE_VERSION)"
SERVICE_PORT_FLAG := $(if $(PORT),--port $(PORT),)
SERVICE_HOME_DIR_FLAG := $(if $(HOME_DIR),--home-dir "$(HOME_DIR)",)
+28 -3
View File
@@ -95,9 +95,34 @@ else
endif
# Build info
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || powershell -c "Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'" 2>/dev/null || echo "unknown")
#
# Keyed on $(OS)$(MSYSTEM) like the shell-syntax block above. cmd.exe has no
# /dev/null and resolves redirections *before* running the command, so under
# native cmd/PowerShell `2>/dev/null` aborts each command outright and hands
# every value to its fallback — stamping local Windows builds dev/unknown/unknown
# while printing "The system cannot find the path specified." per assignment.
# cmd's own `date` is no substitute: given arguments it prompts for input and
# still exits 0, so it would swallow the `||`. Git Bash sets MSYSTEM and runs
# recipes through sh, so it takes the POSIX branch.
#
# Fallbacks are unquoted: sh strips the quotes, cmd's `echo` would keep them.
#
# NULL_REDIR holds the whole redirect token, not just the device path, so a
# caller simulating another platform can blank it (`make NULL_REDIR=`) instead
# of naming a device this shell would misread — see scripts/check-make-shells.
ifeq ($(OS)$(MSYSTEM),Windows_NT)
NULL_REDIR := 2>NUL
# `Get-Date -Format` renders *local* time with a literal Z; UtcNow is real UTC.
# InvariantCulture is not optional: ToString uses the current culture's
# calendar, so a th-TH host stamps year 2569 and ar-SA stamps 1448.
BUILD_TIME := $(shell powershell -NoProfile -Command "[DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ',[Globalization.CultureInfo]::InvariantCulture)" $(NULL_REDIR) || echo unknown)
else
NULL_REDIR := 2>/dev/null
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ' $(NULL_REDIR) || echo unknown)
endif
VERSION ?= $(shell git describe --tags --always --dirty $(NULL_REDIR) || echo dev)
COMMIT := $(shell git rev-parse --short HEAD $(NULL_REDIR) || echo unknown)
LDFLAGS += -X main.Version=$(VERSION) -X main.Commit=$(COMMIT) -X main.BuildTime=$(BUILD_TIME)
+40 -2
View File
@@ -29,7 +29,15 @@ status=0
# expect <label> <OS> <MSYSTEM> <target> <expected recipe tail>
expect() {
local label=$1 os=$2 msys=$3 target=$4 want=$5 got
got=$(make -C "$backend_dir" OS="$os" MSYSTEM="$msys" -n "$target" 2>/dev/null \
# 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.
got=$(make -C "$backend_dir" OS="$os" MSYSTEM="$msys" NULL_REDIR= BUILD_TIME=simulated -n "$target" 2>/dev/null \
| grep -F __backend \
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/[[:space:]]\{1,\}/ /g' \
|| true)
@@ -57,7 +65,37 @@ expect git-bash Windows_NT MINGW64 run './bin/kandev.exe __backend'
expect git-bash Windows_NT MINGW64 dev 'KANDEV_DEBUG_PPROF_ENABLED=true exec ./bin/kandev.exe __backend'
expect git-bash Windows_NT MINGW64 start-debug 'KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug exec ./bin/kandev.exe __backend'
# 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
if [ "$status" -eq 0 ]; then
echo "All Makefile shell-dispatch checks passed (3 shells x 3 targets)."
echo "All Makefile shell-dispatch checks passed (3 shells x 3 targets, plus static guard)."
fi
exit "$status"