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>
This commit is contained in:
João Manso
2026-08-18 16:53:35 +00:00
committed by GitHub
parent 69fc5d720a
commit 263cbab560
3 changed files with 90 additions and 13 deletions
+36 -11
View File
@@ -111,6 +111,29 @@ else
endef endef
endif 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 # Build info
# #
# Keyed on $(OS)$(MSYSTEM) like the shell-syntax block above. cmd.exe has no # 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: build-kandev:
@echo "Building $(BINARY_NAME)..." @echo "Building $(BINARY_NAME)..."
@$(call MKDIR,$(BUILD_DIR)) @$(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 binaries (unified + individual services + agentctl)
build-all: build build-agentctl build-all: build build-agentctl
@echo "Building individual services..." @echo "Building individual services..."
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/agent-manager$(EXE) ./cmd/agent-manager $(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: build-agentctl:
@echo "Building agentctl..." @echo "Building agentctl..."
@$(call MKDIR,$(BUILD_DIR)) @$(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 only the agentctl helpers needed by the development profile.
build-agentctl-dev: $(DEV_AGENTCTL_TARGETS) build-agentctl-dev: $(DEV_AGENTCTL_TARGETS)
@@ -321,7 +346,7 @@ start-debug: build
## Run tests ## Run tests
test: test:
@echo "Running tests..." @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 ## Run only the Windows-sensitive packages — mirrors the test-windows job in
## .github/workflows/backend-tests.yml. The broader suite uses Unix-only ## .github/workflows/backend-tests.yml. The broader suite uses Unix-only
@@ -331,11 +356,11 @@ test:
## runtime.GOOS guards in source. ## runtime.GOOS guards in source.
test-windows: test-windows:
@echo "Running Windows-sensitive packages..." @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..." @echo "Running Windows port-collision tests..."
$(CGO_PREFIX) $(GO) test -tags fts5 -v ./internal/common/netutil -run TestIsAddrInUse $(CGO_LOCALE) $(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_LOCALE) $(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/gateway/websocket -run TestResolveAndBindReportsOccupiedPort
## Verify run/dev/start-debug expand to the right per-shell command line across ## 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 ## 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 LIFECYCLE_GOLEAK_COUNT ?= 20
test-lifecycle-goleak: test-lifecycle-goleak:
@echo "Stress-running lifecycle goleak suite ($(LIFECYCLE_GOLEAK_COUNT)x)..." @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 ## Run tests with coverage
test-coverage: test-coverage:
@echo "Running tests with 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 $(GO) tool cover -html=coverage.out -o coverage.html
## Regenerate Go stubs from proto/kandev/plugin/v1/plugin.proto (§3 of ## 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. # supported target that could reach a finding before removing reported code.
deadcode: 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)..." @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 ## Format code
fmt: fmt:
+10
View File
@@ -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. 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\<name>\`), 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\<name>` 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: 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 ```powershell
+44 -2
View File
@@ -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 # 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. # 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 \ expect unix '' '' deadcode \
'GOTOOLCHAIN=go1.26.0 go run golang.org/x/tools/cmd/deadcode@v0.48.0 -tags fts5 ./...' \ 'GOTOOLCHAIN=go1.26.0 go run golang.org/x/tools/cmd/deadcode@v0.48.0 -tags fts5 ./...' \
'golang.org/x/tools/cmd/deadcode' '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 ./...' \ 'set GOTOOLCHAIN=go1.26.0& go run golang.org/x/tools/cmd/deadcode@v0.48.0 -tags fts5 ./...' \
'golang.org/x/tools/cmd/deadcode' 'golang.org/x/tools/cmd/deadcode'
expect git-bash Windows_NT MINGW64 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' '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 — 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 # 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. # 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 ...)' printf 'ok %-16s %s\n' "static" 'no literal /dev/null inside $(shell ...)'
fi 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 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 fi
exit "$status" exit "$status"