diff --git a/apps/backend/Makefile b/apps/backend/Makefile index 0bfba0616c..b88104a8cd 100644 --- a/apps/backend/Makefile +++ b/apps/backend/Makefile @@ -111,6 +111,29 @@ else endef endif +# CGO locale for Git Bash/MSYS on Windows. +# +# This mingw (WinLibs) gcc resolves its own install path, then passes it to ld/ +# collect2 across a locale-sensitive boundary. Under a UTF-8 locale, ld mis-decodes +# the ANSI-encoded path, so a non-ASCII segment (e.g. an accented Windows +# username under C:\Users\...) collapses to U+FFFD and the link cannot find +# crt2.o / crtbegin.o / -lm. Forcing the C locale keeps the raw path bytes, so the +# CGO link resolves. See docs/public/windows-support.md. +# +# Use LC_ALL=C, not LANG=C: LANG is the lowest-precedence locale variable +# (LC_ALL > LC_CTYPE > LANG), so LANG=C is silently ignored when the shell already +# exports LC_ALL or LC_CTYPE as UTF-8. LC_ALL=C overrides them all. +# +# Scope mirrors the shell-syntax block above: Windows but NOT native cmd, i.e. +# $(MSYSTEM) is set, so this is Git Bash/MSYS. It is empty only on Unix and native +# cmd. On Git Bash/MSYS the prefix is always applied: a harmless no-op on an ASCII +# toolchain path, and the actual fix on a non-ASCII one. +ifeq ($(OS),Windows_NT) + ifneq ($(OS)$(MSYSTEM),Windows_NT) + CGO_LOCALE := LC_ALL=C + endif +endif + # Build info # # Keyed on $(OS)$(MSYSTEM) like the shell-syntax block above. cmd.exe has no @@ -220,18 +243,20 @@ build-runtime: build-agentctl build-agentctl-remote build-kandev build-kandev: @echo "Building $(BINARY_NAME)..." @$(call MKDIR,$(BUILD_DIR)) - $(CGO_PREFIX) $(GO) build $(GOFLAGS) -tags fts5 -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)$(EXE) ./cmd/kandev + $(CGO_LOCALE) $(CGO_PREFIX) $(GO) build $(GOFLAGS) -tags fts5 -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)$(EXE) ./cmd/kandev ## Build all binaries (unified + individual services + agentctl) build-all: build build-agentctl @echo "Building individual services..." $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/agent-manager$(EXE) ./cmd/agent-manager -## Build the agentctl sidecar binary (includes embedded MCP server) +## Build the agentctl sidecar binary (includes embedded MCP server). Carries +## $(CGO_LOCALE) because agentctl imports cgo (unlike mock-agent/acpdbg/winjob), +## so its native `go build` invokes gcc; see the CGO_LOCALE note above. build-agentctl: @echo "Building agentctl..." @$(call MKDIR,$(BUILD_DIR)) - $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/agentctl$(EXE) ./cmd/agentctl + $(CGO_LOCALE) $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/agentctl$(EXE) ./cmd/agentctl ## Build only the agentctl helpers needed by the development profile. build-agentctl-dev: $(DEV_AGENTCTL_TARGETS) @@ -321,7 +346,7 @@ start-debug: build ## Run tests test: @echo "Running tests..." - $(CGO_PREFIX) $(GO) test -tags fts5 ./... + $(CGO_LOCALE) $(CGO_PREFIX) $(GO) test -tags fts5 ./... ## Run only the Windows-sensitive packages — mirrors the test-windows job in ## .github/workflows/backend-tests.yml. The broader suite uses Unix-only @@ -331,11 +356,11 @@ test: ## runtime.GOOS guards in source. test-windows: @echo "Running Windows-sensitive packages..." - $(CGO_PREFIX) $(GO) test -tags fts5 -v ./internal/agentctl/server/process/... ./internal/agent/runtime/agentctl/launcher/... ./internal/backendapp/ownershiplock/... + $(CGO_LOCALE) $(CGO_PREFIX) $(GO) test -tags fts5 -v ./internal/agentctl/server/process/... ./internal/agent/runtime/agentctl/launcher/... ./internal/backendapp/ownershiplock/... @echo "Running Windows port-collision tests..." - $(CGO_PREFIX) $(GO) test -tags fts5 -v ./internal/common/netutil -run TestIsAddrInUse - $(CGO_PREFIX) $(GO) test -tags fts5 -v ./internal/agentctl/server/instance -run TestAllocatePortAndListenerRetriesAddressInUse - $(CGO_PREFIX) $(GO) test -tags fts5 -v ./internal/gateway/websocket -run TestResolveAndBindReportsOccupiedPort + $(CGO_LOCALE) $(CGO_PREFIX) $(GO) test -tags fts5 -v ./internal/common/netutil -run TestIsAddrInUse + $(CGO_LOCALE) $(CGO_PREFIX) $(GO) test -tags fts5 -v ./internal/agentctl/server/instance -run TestAllocatePortAndListenerRetriesAddressInUse + $(CGO_LOCALE) $(CGO_PREFIX) $(GO) test -tags fts5 -v ./internal/gateway/websocket -run TestResolveAndBindReportsOccupiedPort ## 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 @@ -368,12 +393,12 @@ test-sprites-e2e: build-agentctl-linux LIFECYCLE_GOLEAK_COUNT ?= 20 test-lifecycle-goleak: @echo "Stress-running lifecycle goleak suite ($(LIFECYCLE_GOLEAK_COUNT)x)..." - $(CGO_PREFIX) $(GO) test -tags fts5 -race -count=$(LIFECYCLE_GOLEAK_COUNT) -timeout 600s ./internal/agent/runtime/lifecycle/ ./internal/agent/runtime/agentctl/ + $(CGO_LOCALE) $(CGO_PREFIX) $(GO) test -tags fts5 -race -count=$(LIFECYCLE_GOLEAK_COUNT) -timeout 600s ./internal/agent/runtime/lifecycle/ ./internal/agent/runtime/agentctl/ ## Run tests with coverage test-coverage: @echo "Running tests with coverage..." - $(CGO_PREFIX) $(GO) test -tags fts5 -v -coverprofile=coverage.out ./... + $(CGO_LOCALE) $(CGO_PREFIX) $(GO) test -tags fts5 -v -coverprofile=coverage.out ./... $(GO) tool cover -html=coverage.out -o coverage.html ## Regenerate Go stubs from proto/kandev/plugin/v1/plugin.proto (§3 of @@ -406,7 +431,7 @@ lint: # supported target that could reach a finding before removing reported code. deadcode: @echo "Scanning for unreachable Go functions (host configuration only; verify other targets before deleting findings; toolchain=$(DEADCODE_GO_TOOLCHAIN), override with DEADCODE_GO_TOOLCHAIN=go1.XY.Z)..." - $(DEADCODE_ENV) $(GO) run golang.org/x/tools/cmd/deadcode@$(DEADCODE_VERSION) -tags fts5 ./... + $(CGO_LOCALE) $(DEADCODE_ENV) $(GO) run golang.org/x/tools/cmd/deadcode@$(DEADCODE_VERSION) -tags fts5 ./... ## Format code fmt: diff --git a/docs/public/windows-support.md b/docs/public/windows-support.md index b922e040b2..83bc97d8f2 100644 --- a/docs/public/windows-support.md +++ b/docs/public/windows-support.md @@ -202,6 +202,16 @@ winget install ezwinports.make The repository bootstrap script is Bash/package-manager oriented and does not provision a native Windows toolchain automatically. Use Git Bash or another compatible shell for Unix-oriented root recipes, install the pinned tools manually, and follow the contributor guide. `make dev` builds a `winjob.exe` helper so `Ctrl+C` can close a native development process tree. +### CGO link failure with a non-ASCII toolchain path + +The `kandev` binary and the backend tests build with CGO and SQLite FTS5. If the MinGW/GCC toolchain resolves to a path with non-ASCII characters (most commonly an accented Windows username, which puts the toolchain under `C:\Users\\`), a UTF-8 locale in Git Bash makes GCC mis-decode its own install path when it hands it to `ld`. The link then fails with `cannot find crt2.o`, `crtbegin.o`, and `-lm`, even though the files exist. + +The CGO Make targets (`build-kandev`, `build-agentctl`, the `-tags fts5` test targets, and `deadcode`) set `LC_ALL=C` automatically when they run under Git Bash/MSYS on Windows. This prevents the CGO link error in `make build` and `make dev`. Use `make test-windows` for the supported Windows-clean test suite. The complete `make test-backend` suite still contains tests with Unix-only fixtures. `LC_ALL=C` (rather than `LANG=C`) is used so the fix still applies when the shell already exports `LC_ALL` or `LC_CTYPE` as a UTF-8 locale, which would otherwise override `LANG`. The prefix is empty on Unix and native `cmd`/PowerShell`; on Git Bash/MSYS it is always applied, which is a harmless no-op on an ASCII toolchain path and the actual fix on a non-ASCII one. + +When invoking `go build`/`go test` directly, prefix the command with `LC_ALL=C` (for example `LC_ALL=C go build -tags fts5 ./cmd/kandev`). To remove the cause entirely, install the toolchain at an ASCII path outside `C:\Users\` and point `CC`/`CXX` at it. Do not force the C locale globally (for example `export LC_ALL=C`) in your shell profile; that disables the UTF-8 locale for all of Git Bash. + +A third option removes the cause at the OS level. Enable Windows' **Beta: Use Unicode UTF-8 for worldwide language support** (Region control panel, **Administrative** tab, **Change system locale**, then reboot). This sets the system ANSI code page to UTF-8, so GCC and `ld` agree on the path encoding and the accented toolchain path links directly under a UTF-8 locale, with no `LC_ALL=C` prefix and no toolchain relocation. It is a **beta**, system-wide setting: it requires a reboot and can change how other non-UTF-8 applications interpret text, so treat it as opt-in. + Backend Windows CI runs `go build ./...`, `go vet ./...`, and focused race tests for Windows-sensitive process, agent-launcher, instance-port, and websocket-tunnel packages. It does not run every backend package or the full product E2E suite. The repository's broader Windows-clean target adds web and CLI unit tests: ```powershell diff --git a/scripts/check-make-shells b/scripts/check-make-shells index 16c08f6e80..a6351256d7 100755 --- a/scripts/check-make-shells +++ b/scripts/check-make-shells @@ -86,6 +86,11 @@ expect git-bash Windows_NT MINGW64 start-debug "KANDEV_DEBUG_PPROF_ENABLED=true # 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' @@ -93,9 +98,23 @@ 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 \ - 'GOTOOLCHAIN=go1.26.0 go run golang.org/x/tools/cmd/deadcode@v0.48.0 -tags fts5 ./...' \ + '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. @@ -176,7 +195,30 @@ 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/build-kandev across 3 shells, plus static guard)." + echo "All Makefile shell-dispatch checks passed (run/dev/start-debug/deadcode/test/build-kandev across 3 shells, plus static guards)." fi exit "$status"