From 6fedac6c64e64ef53063d1a94427d6898e8910a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Manso?= Date: Fri, 24 Jul 2026 12:08:22 +0100 Subject: [PATCH] fix(build): support make dev on native Windows (#1886) --- .github/workflows/backend-tests.yml | 9 +++++ Makefile | 8 ++++ apps/backend/Makefile | 62 +++++++++++++++++++++------- scripts/check-make-shells | 63 +++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 14 deletions(-) create mode 100755 scripts/check-make-shells diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index a2dd6da7c9..ade001d461 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -5,6 +5,7 @@ on: branches: [main] paths: - "apps/backend/**" + - "scripts/check-make-shells" - ".github/workflows/backend-tests.yml" - "!**/*.md" - "!**/*.mdx" @@ -13,6 +14,7 @@ on: branches: [main] paths: - "apps/backend/**" + - "scripts/check-make-shells" - ".github/workflows/backend-tests.yml" - "!**/*.md" - "!**/*.mdx" @@ -80,6 +82,13 @@ jobs: - name: Build run: go build -v ./cmd/kandev + # Assert the run/dev/start-debug targets expand to the right per-shell + # command line (native Windows / Git Bash / Unix). The windows-latest job + # below compiles Go but never exercises this Make-target dispatch, so a + # regression in the OS/MSYSTEM guards would otherwise ship unnoticed. + - name: Check Makefile shell dispatch + run: make check-make-shells + - name: Resolve base SHA shell: bash working-directory: ${{ github.workspace }} diff --git a/Makefile b/Makefile index 1078123093..c7c95d0469 100644 --- a/Makefile +++ b/Makefile @@ -150,7 +150,15 @@ bootstrap-e2e: .PHONY: doctor doctor: +# Native Windows (cmd/PowerShell) has no POSIX shell for the bash-only doctor +# script. Under Git Bash/MSYS ($(MSYSTEM) is set) it runs fine, so skip only +# when OS is Windows_NT AND MSYSTEM is empty — the concatenation equals +# "Windows_NT" only in that native case. +ifeq ($(OS)$(MSYSTEM),Windows_NT) + @echo pre-commit hooks skipped on native Windows - run scripts/doctor from Git Bash to enable +else @scripts/doctor +endif .PHONY: dev dev: doctor diff --git a/apps/backend/Makefile b/apps/backend/Makefile index eea6b6b9f2..5c1f61d61e 100644 --- a/apps/backend/Makefile +++ b/apps/backend/Makefile @@ -9,17 +9,30 @@ LDFLAGS := -s -w # Cross-platform commands # -# On Windows, GNU Make invokes commands through cmd.exe, which does not -# support Unix `NAME=VAL cmd` env-var-prefix syntax. Instead, `set NAME=VAL&` -# (single ampersand) chains a subsequent command in the same shell, scoping -# the env var to one recipe line — the closest cmd analog to the Unix form. -# `EXE` adds the .exe suffix to build outputs; Go does NOT auto-append it -# when `-o` is explicit. +# Two independent axes decide these variables; conflating them breaks Git +# Bash/MSYS, which sets OS=Windows_NT yet runs recipes through sh: +# +# 1. Executable suffix (EXE) — keyed on $(OS). Go emits a `.exe` for +# GOOS=windows regardless of the calling shell, so native cmd/PowerShell +# AND Git Bash both need it. Go does NOT auto-append it when `-o` is set. +# 2. Shell syntax — keyed on $(OS)$(MSYSTEM). Only native cmd/PowerShell needs +# `set NAME=VAL&` (single ampersand chains a command in the same shell, +# scoping the env var to one recipe line — the closest cmd analog to the +# Unix `NAME=VAL cmd` prefix), backslashed paths, and no `exec`. Under Git +# Bash/MSYS $(MSYSTEM) is set (e.g. MINGW64), so the concatenation differs +# from "Windows_NT" and recipes take the POSIX branch — matching the shell +# they run in. Mirrors the doctor guard in the root Makefile. # # Install GNU Make on Windows with `winget install ezwinports.make` (or via -# scoop/chocolatey). Targets that rely on Unix builtins (run, dev, -# start-debug, build-cross) remain Unix-only. +# scoop/chocolatey). build-cross remains Unix-only — it cross-compiles with +# CGO for other OSes, which needs Unix toolchains. ifeq ($(OS),Windows_NT) + EXE := .exe +else + EXE := +endif + +ifeq ($(OS)$(MSYSTEM),Windows_NT) RM = cmd /c del /s /q RMDIR = cmd /c rmdir /s /q # cmd's `mkdir` errors when the dir already exists, unlike Unix `mkdir -p`. @@ -34,7 +47,16 @@ ifeq ($(OS),Windows_NT) GO_LINUX_ARM64 = set CGO_ENABLED=0& set GOOS=linux& set GOARCH=arm64& GO_DARWIN_ARM64 = set CGO_ENABLED=0& set GOOS=darwin& set GOARCH=arm64& GO_DARWIN_AMD64 = set CGO_ENABLED=0& set GOOS=darwin& set GOARCH=amd64& - EXE = .exe + # Run-target env prefixes and binary invocation. cmd.exe has no `NAME=VAL cmd` + # syntax (use `set NAME=VAL&`) and no `exec`; the binary path needs backslashes. + DEV_ENV = set KANDEV_DEBUG_PPROF_ENABLED=true& + DEBUG_ENV = set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& + EXEC = + RUN_BIN = "$(subst /,\,$(BUILD_DIR)/$(BINARY_NAME)$(EXE))" + # check-make-shells is a bash script; native cmd.exe can't run its shebang, so + # print a skip note instead (mirrors the root Makefile's doctor guard). CI runs + # the real check on Linux, and Git Bash/WSL run it locally. + CHECK_SHELLS = @echo check-make-shells is bash-only - run it from Git Bash/WSL or the Linux CI # Windows recipes run under cmd.exe, which has no POSIX `if command -v ...; then` # syntax. Neither codesign nor rcodesign ships on Windows anyway, so ad-hoc # signing is a no-op here: darwin/arm64 stays Go-signed at link time, and @@ -51,7 +73,11 @@ else GO_LINUX_ARM64 = CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GO_DARWIN_ARM64 = CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 GO_DARWIN_AMD64 = CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 - EXE = + DEV_ENV = KANDEV_DEBUG_PPROF_ENABLED=true + DEBUG_ENV = KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug + EXEC = exec + RUN_BIN = ./$(BUILD_DIR)/$(BINARY_NAME)$(EXE) + CHECK_SHELLS = @../../scripts/check-make-shells # Ad-hoc sign a darwin Mach-O so Apple Silicon (AMFI) will run it; ad-hoc needs # no Apple identity. Go already ad-hoc-signs darwin/arm64 at link time, so this # is mainly to (a) sign darwin/amd64 too and (b) re-sign uniformly. Prefer @@ -75,7 +101,7 @@ BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || powershell -c LDFLAGS += -X main.Version=$(VERSION) -X main.Commit=$(COMMIT) -X main.BuildTime=$(BUILD_TIME) -.PHONY: all build build-all build-agentctl build-agentctl-remote build-agentctl-linux build-agentctl-linux-arm64 build-agentctl-darwin-arm64 build-agentctl-darwin-amd64 build-acpdbg acpdbg build-mock-agent build-mock-agent-linux build-preview build-winjob e2e-plugin-package clean run dev start-debug test test-e2e test-sprites-e2e test-lifecycle-goleak lint fmt vet help +.PHONY: all build build-all build-agentctl build-agentctl-remote build-agentctl-linux build-agentctl-linux-arm64 build-agentctl-darwin-arm64 build-agentctl-darwin-amd64 build-acpdbg acpdbg build-mock-agent build-mock-agent-linux build-preview build-winjob e2e-plugin-package clean run dev start-debug test test-e2e test-sprites-e2e test-lifecycle-goleak lint fmt vet check-make-shells help ## Default target all: build @@ -200,17 +226,17 @@ clean: ## Run the application run: build @echo "Running $(BINARY_NAME)..." - ./$(BUILD_DIR)/$(BINARY_NAME)$(EXE) __backend + $(RUN_BIN) __backend ## Run in development mode (with pprof enabled) dev: build @echo "Running $(BINARY_NAME) in development mode..." - @KANDEV_DEBUG_PPROF_ENABLED=true exec ./$(BUILD_DIR)/$(BINARY_NAME)$(EXE) __backend + @$(DEV_ENV) $(EXEC) $(RUN_BIN) __backend ## Run in debug mode (pprof + debug logging) start-debug: build @echo "Running $(BINARY_NAME) in debug mode (pprof + debug logging)..." - @KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug exec ./$(BUILD_DIR)/$(BINARY_NAME)$(EXE) __backend + @$(DEBUG_ENV) $(EXEC) $(RUN_BIN) __backend ## Run tests test: @@ -227,6 +253,13 @@ test-windows: @echo "Running Windows-sensitive packages..." $(CGO_PREFIX) $(GO) test -tags fts5 -v ./internal/agentctl/server/process/... ./internal/agent/runtime/agentctl/launcher/... +## Verify run/dev/start-debug expand to the right per-shell command line across +## native Windows (cmd), Git Bash/MSYS, and Unix — the Make-target dispatch the +## Go CI jobs don't exercise. The harness is a bash script, so it runs under Git +## Bash/WSL or the Linux CI; on native cmd/PowerShell it prints a skip note. +check-make-shells: + $(CHECK_SHELLS) + ## Run E2E tests (real agents, costs money — manual only) ## Set agent commands via env vars: E2E_CLAUDE_CODE_COMMAND, E2E_AMP_COMMAND ## Enable debug: KANDEV_DEBUG_AGENT_MESSAGES=true KANDEV_DEBUG_LOG_DIR=/tmp/e2e-debug @@ -331,6 +364,7 @@ help: @echo " start-debug Run in debug mode (pprof + debug logging)" @echo " test Run tests (full suite — fails on Windows pending fixture cleanup)" @echo " test-windows Run only the Windows-clean subset (matches CI windows-latest job)" + @echo " check-make-shells Assert run/dev/start-debug expand correctly per shell (make -n)" @echo " test-e2e Run E2E adapter tests (real agents, costs money)" @echo " e2e-plugin-package Package the plugin-fixture SDK plugin into .build/kandev-plugin-e2e-1.0.0.tar.gz" @echo " test-lifecycle-goleak Stress-run lifecycle goleak suite (override LIFECYCLE_GOLEAK_COUNT=N)" diff --git a/scripts/check-make-shells b/scripts/check-make-shells new file mode 100755 index 0000000000..aacb723bf6 --- /dev/null +++ b/scripts/check-make-shells @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# check-make-shells — assert that apps/backend/Makefile's run/dev/start-debug +# targets expand to the correct per-shell command line, without running 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. + +set -euo pipefail + +backend_dir="$(cd "$(dirname "$0")/../apps/backend" && pwd)" + +status=0 + +# expect