Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -413,16 +413,16 @@ jobs:
go build -tags fts5 -ldflags "${LDFLAGS}" -o bin/kandev${EXT} ./cmd/kandev
go build -ldflags "${LDFLAGS}" -o bin/agentctl${EXT} ./cmd/agentctl

# Cross-compile a linux/amd64 agentctl regardless of the host platform.
# Remote executors (local Docker, remote Docker, Sprites) bind-mount this
# binary into the container — without it, Docker sessions fail with
# "agentctl linux binary not found" on Mac/Windows installs and even on
# Linux installs (the resolver looks for the -linux-amd64 suffix).
# CGO is off so this works from any host without extra toolchain setup.
- name: Build linux/amd64 agentctl helper
# Cross-compile remote agentctl helpers regardless of the host platform.
# Remote executors upload or bind-mount these sidecars into Linux and
# macOS SSH hosts. CGO is off so this works from any host without extra
# toolchain setup.
- name: Build remote agentctl helpers
run: |
cd apps/backend
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 CC= go build -ldflags "${LDFLAGS}" -o bin/agentctl-linux-amd64 ./cmd/agentctl
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 CC= go build -ldflags "${LDFLAGS}" -o bin/agentctl-darwin-arm64 ./cmd/agentctl
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 CC= go build -ldflags "${LDFLAGS}" -o bin/agentctl-darwin-amd64 ./cmd/agentctl

- name: Package bundle
run: |
Expand All @@ -432,6 +432,8 @@ jobs:
cp apps/backend/bin/kandev${EXT} dist/kandev/bin/
cp apps/backend/bin/agentctl${EXT} dist/kandev/bin/
cp apps/backend/bin/agentctl-linux-amd64 dist/kandev/bin/
cp apps/backend/bin/agentctl-darwin-arm64 dist/kandev/bin/
cp apps/backend/bin/agentctl-darwin-amd64 dist/kandev/bin/
Comment thread
carlosflorencio marked this conversation as resolved.
Comment thread
carlosflorencio marked this conversation as resolved.
cd dist
tar -czf "kandev-${{ matrix.platform }}.tar.gz" kandev
shasum -a 256 "kandev-${{ matrix.platform }}.tar.gz" > "kandev-${{ matrix.platform }}.tar.gz.sha256"
Expand Down
39 changes: 14 additions & 25 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -147,31 +147,14 @@ bootstrap:
bootstrap-e2e:
@scripts/bootstrap-dev-env --with-e2e

# Build the linux/amd64 agentctl on every host except Linux/x86_64 (where the
# host build IS already linux/amd64). Computed up here, OS-conditionally, so
# the recipe doesn't shell out to `uname` — that fails on Windows under cmd
# and produces spurious "CreateProcess(NULL, uname -s, ...) failed" warnings
# on every Make invocation.
ifeq ($(OS),Windows_NT)
HOST_IS_LINUX_AMD64 := 0
else
ifeq ($(shell uname -s)/$(shell uname -m),Linux/x86_64)
HOST_IS_LINUX_AMD64 := 1
else
HOST_IS_LINUX_AMD64 := 0
endif
endif

.PHONY: doctor
doctor:
@scripts/doctor

.PHONY: dev
dev: doctor
ifneq ($(HOST_IS_LINUX_AMD64),1)
@echo "Building Linux agentctl for remote executors..."
@$(MAKE) -C $(BACKEND_DIR) build-agentctl-linux
endif
@echo "Building remote agentctl helpers..."
@$(MAKE) -C $(BACKEND_DIR) build-agentctl-remote
ifeq ($(OS),Windows_NT)
@echo "Building winjob (Ctrl-C-safe wrapper for Windows)..."
@$(MAKE) -C $(BACKEND_DIR) build-winjob
Expand Down Expand Up @@ -278,10 +261,14 @@ service-bundle: install build
$(call phase,Packaging Service Bundle)
@test -n "$(SERVICE_BUNDLE_DIR)" || { echo "SERVICE_BUNDLE_DIR is empty; aborting."; exit 1; }
@test "$(SERVICE_BUNDLE_DIR)" != "/" || { echo "SERVICE_BUNDLE_DIR must not be /; aborting."; exit 1; }
@test -f "$(BACKEND_DIR)/bin/agentctl-linux-amd64" || $(MAKE) -C $(BACKEND_DIR) build-agentctl-linux
@$(MAKE) -C $(BACKEND_DIR) build-agentctl-remote
@$(RMDIR) "$(SERVICE_BUNDLE_DIR)/bin"
@mkdir -p "$(SERVICE_BUNDLE_DIR)/bin"
@cp "$(BACKEND_DIR)/bin/kandev" "$(BACKEND_DIR)/bin/agentctl" "$(BACKEND_DIR)/bin/agentctl-linux-amd64" "$(SERVICE_BUNDLE_DIR)/bin/"
@cp "$(BACKEND_DIR)/bin/kandev" "$(BACKEND_DIR)/bin/agentctl" \
"$(BACKEND_DIR)/bin/agentctl-linux-amd64" \
"$(BACKEND_DIR)/bin/agentctl-darwin-arm64" \
"$(BACKEND_DIR)/bin/agentctl-darwin-amd64" \
"$(SERVICE_BUNDLE_DIR)/bin/"
@scripts/release/package-bundle.sh
$(call success,Service bundle packaged at $(SERVICE_BUNDLE_DIR))

Expand Down Expand Up @@ -327,10 +314,12 @@ build-backend:
@printf "$(CYAN)Building backend...$(RESET)\n"
@$(MAKE) -C $(BACKEND_DIR) build

.PHONY: build-backend-linux-helpers
build-backend-linux-helpers:
@printf "$(CYAN)Building linux/amd64 helper binaries (agentctl + mock-agent) for Docker E2E...$(RESET)\n"
@$(MAKE) -C $(BACKEND_DIR) build-agentctl-linux build-mock-agent-linux
.PHONY: build-backend-remote-helpers build-backend-linux-helpers
build-backend-remote-helpers:
@printf "$(CYAN)Building remote helper binaries (agentctl helpers + mock-agent) for executor E2E...$(RESET)\n"
@$(MAKE) -C $(BACKEND_DIR) build-agentctl-remote build-mock-agent-linux

build-backend-linux-helpers: build-backend-remote-helpers

.PHONY: acpdbg
acpdbg:
Expand Down
40 changes: 29 additions & 11 deletions apps/backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ LDFLAGS := -s -w
#
# 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, build-agentctl-linux) remain Unix-only.
# start-debug, build-cross) remain Unix-only.
ifeq ($(OS),Windows_NT)
RM = cmd /c del /s /q
RMDIR = cmd /c rmdir /s /q
Expand All @@ -27,17 +27,21 @@ ifeq ($(OS),Windows_NT)
# "already exists" failure is tolerated; on Unix the prefix is a no-op.
MKDIR = cmd /c mkdir
CGO_PREFIX = set CGO_ENABLED=1&
# Cross-compile prefix for the linux/amd64 agentctl bundled with the host
# build (used by remote-Sprites executors). cmd has no Unix env-var-prefix
# syntax; `set X=Y&` chains `set` and the next command in the same shell.
# Cross-compile prefixes for agentctl helpers bundled with the host build.
# cmd has no Unix env-var-prefix syntax; `set X=Y&` chains `set` and the next
# command in the same shell.
GO_LINUX_AMD64 = set CGO_ENABLED=0& set GOOS=linux& set GOARCH=amd64&
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
else
RM = rm -f
RMDIR = rm -rf
MKDIR = mkdir -p
CGO_PREFIX = CGO_ENABLED=1
GO_LINUX_AMD64 = CGO_ENABLED=0 GOOS=linux GOARCH=amd64
GO_DARWIN_ARM64 = CGO_ENABLED=0 GOOS=darwin GOARCH=arm64
GO_DARWIN_AMD64 = CGO_ENABLED=0 GOOS=darwin GOARCH=amd64
EXE =
endif

Expand All @@ -48,7 +52,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-linux build-acpdbg acpdbg build-mock-agent build-mock-agent-linux build-preview build-winjob 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-darwin-arm64 build-agentctl-darwin-amd64 build-acpdbg acpdbg build-mock-agent build-mock-agent-linux build-preview build-winjob clean run dev start-debug test test-e2e test-sprites-e2e test-lifecycle-goleak lint fmt vet help

## Default target
all: build
Expand Down Expand Up @@ -84,11 +88,10 @@ build-acpdbg:
-@$(MKDIR) $(BUILD_DIR)
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/acpdbg$(EXE) ./cmd/acpdbg

## Build the unified kandev binary. Always includes the linux/amd64 agentctl
## variant because remote executors (local Docker, remote Docker, Sprites)
## bind-mount it into containers — without it, the first Docker session on a
## non-Linux host fails with "agentctl linux binary not found".
build: build-agentctl build-agentctl-linux build-mock-agent build-acpdbg build-winjob
## Build the unified kandev binary. Always includes remote agentctl helpers
## because SSH/Docker/Sprites executors upload or bind-mount platform-specific
## sidecars into remote environments.
build: build-agentctl build-agentctl-remote build-mock-agent build-acpdbg build-winjob
@echo "Building $(BINARY_NAME)..."
-@$(MKDIR) $(BUILD_DIR)
$(CGO_PREFIX) $(GO) build $(GOFLAGS) -tags fts5 -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)$(EXE) ./cmd/kandev
Expand All @@ -104,12 +107,27 @@ build-agentctl:
-@$(MKDIR) $(BUILD_DIR)
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/agentctl$(EXE) ./cmd/agentctl

## Build agentctl for linux/amd64 (used by Sprites remote executor)
## Build all remote agentctl helpers used by SSH/Docker/Sprites executors.
build-agentctl-remote: build-agentctl-linux build-agentctl-darwin-arm64 build-agentctl-darwin-amd64

## Build agentctl for linux/amd64 (used by Docker/Sprites and Linux SSH hosts)
build-agentctl-linux:
@echo "Building agentctl for linux/amd64..."
-@$(MKDIR) $(BUILD_DIR)
$(GO_LINUX_AMD64) $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/agentctl-linux-amd64 ./cmd/agentctl

## Build agentctl for darwin/arm64 (used by Apple Silicon SSH hosts)
build-agentctl-darwin-arm64:
@echo "Building agentctl for darwin/arm64..."
-@$(MKDIR) $(BUILD_DIR)
$(GO_DARWIN_ARM64) $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/agentctl-darwin-arm64 ./cmd/agentctl

## Build agentctl for darwin/amd64 (used by Intel Mac SSH hosts)
build-agentctl-darwin-amd64:
@echo "Building agentctl for darwin/amd64..."
-@$(MKDIR) $(BUILD_DIR)
$(GO_DARWIN_AMD64) $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/agentctl-darwin-amd64 ./cmd/agentctl

## Build mock-agent for linux/amd64 (used by Docker E2E tests)
build-mock-agent-linux:
@echo "Building mock-agent for linux/amd64..."
Expand Down
87 changes: 69 additions & 18 deletions apps/backend/internal/agent/runtime/lifecycle/agentctl_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import (
"os"
"path/filepath"
"runtime"
"strings"

"go.uber.org/zap"

"github.com/kandev/kandev/internal/common/logger"
)

// AgentctlResolver finds the path to a linux/amd64 agentctl binary for remote executors.
// AgentctlResolver finds the path to platform-specific agentctl binaries for
// remote executors.
// Resolution order:
// 1. KANDEV_AGENTCTL_LINUX_BINARY env var
// 2. build/agentctl-linux-amd64 relative to the running binary (dev mode)
// 3. bin/agentctl-linux-amd64 relative to the running binary
// 1. KANDEV_AGENTCTL_<OS>_<ARCH>_BINARY env var
// 2. KANDEV_AGENTCTL_LINUX_BINARY legacy env var for linux/amd64
// 3. platform-suffixed helper relative to the running binary
// 4. native agentctl when the remote platform matches the control plane
type AgentctlResolver struct {
logger *logger.Logger
}
Expand All @@ -29,36 +32,84 @@ func NewAgentctlResolver(log *logger.Logger) *AgentctlResolver {

// ResolveLinuxBinary returns the path to a linux/amd64 agentctl binary.
func (r *AgentctlResolver) ResolveLinuxBinary() (string, error) {
// 1. Env var override
if envPath := os.Getenv("KANDEV_AGENTCTL_LINUX_BINARY"); envPath != "" {
return r.ResolveRemoteBinary(SSHRemotePlatform{GOOS: sshRemoteGOOSLinux, GOARCH: sshRemoteGOARCHAMD64})
}

// ResolveRemoteBinary returns the path to an agentctl binary for the given
// remote platform.
func (r *AgentctlResolver) ResolveRemoteBinary(platform SSHRemotePlatform) (string, error) {
if err := requireSupportedRemotePlatform(platform); err != nil {
return "", err
}
for _, envName := range agentctlBinaryEnvNames(platform) {
envPath := os.Getenv(envName)
if envPath == "" {
continue
}
if _, err := os.Stat(envPath); err == nil {
Comment thread
carlosflorencio marked this conversation as resolved.
r.logger.Debug("using agentctl from env var", zap.String("path", envPath))
r.logger.Debug("using agentctl from env var",
zap.String("env", envName),
zap.String("path", envPath),
zap.String("remote_platform", platform.String()))
return envPath, nil
}
return "", fmt.Errorf("KANDEV_AGENTCTL_LINUX_BINARY=%q does not exist", envPath)
return "", fmt.Errorf("%s=%q does not exist", envName, envPath)
}

// 2. Relative to the running binary (dev mode: build/agentctl-linux-amd64)
exePath, err := os.Executable()
if err == nil {
exeDir := filepath.Dir(exePath)
candidates := []string{
filepath.Join(exeDir, "agentctl-linux-amd64"),
filepath.Join(exeDir, "..", "build", "agentctl-linux-amd64"),
filepath.Join(exeDir, "..", "bin", "agentctl-linux-amd64"),
}
candidates := agentctlBinaryCandidates(exeDir, platform)
for _, candidate := range candidates {
if _, statErr := os.Stat(candidate); statErr == nil {
abs, _ := filepath.Abs(candidate)
r.logger.Debug("found agentctl binary", zap.String("path", abs))
r.logger.Debug("found agentctl binary",
zap.String("path", abs),
zap.String("remote_platform", platform.String()))
return abs, nil
}
}
}

return "", fmt.Errorf(
"agentctl linux binary not found; build it with 'make build-agentctl-linux' "+
"or set KANDEV_AGENTCTL_LINUX_BINARY (os=%s arch=%s)",
runtime.GOOS, runtime.GOARCH,
"agentctl helper for %s not found; build it with 'make build-agentctl-remote' "+
"or set %s (control plane os=%s arch=%s)",
platform.String(), agentctlBinaryEnvNames(platform)[0], runtime.GOOS, runtime.GOARCH,
)
}

func agentctlBinaryName(platform SSHRemotePlatform) string {
return fmt.Sprintf("agentctl-%s-%s", platform.GOOS, platform.GOARCH)
}

func agentctlBinaryEnvNames(platform SSHRemotePlatform) []string {
primary := fmt.Sprintf(
"KANDEV_AGENTCTL_%s_%s_BINARY",
strings.ToUpper(platform.GOOS),
strings.ToUpper(platform.GOARCH),
)
if platform.GOOS == sshRemoteGOOSLinux && platform.GOARCH == sshRemoteGOARCHAMD64 {
return []string{primary, "KANDEV_AGENTCTL_LINUX_BINARY"}
}
return []string{primary}
}

func agentctlBinaryCandidates(exeDir string, platform SSHRemotePlatform) []string {
name := agentctlBinaryName(platform)
candidates := []string{
filepath.Join(exeDir, name),
filepath.Join(exeDir, "..", "build", name),
filepath.Join(exeDir, "..", "bin", name),
}
if platform.GOOS == runtime.GOOS && platform.GOARCH == runtime.GOARCH {
Comment thread
carlosflorencio marked this conversation as resolved.
Comment thread
carlosflorencio marked this conversation as resolved.
// Same-platform remotes can use the native binary in development even
// when the suffixed helper has not been built yet. Released bundles are
// still validated separately and must include every supported helper.
candidates = append(candidates,
filepath.Join(exeDir, "agentctl"),
filepath.Join(exeDir, "..", "build", "agentctl"),
filepath.Join(exeDir, "..", "bin", "agentctl"),
)
}
return candidates
}
Loading
Loading