Skip to content

Commit b730167

Browse files
rgarciaclaude
andcommitted
refactor: cross-platform foundation for macOS support
Split platform-specific code into _linux.go and _darwin.go files across resources, network, devices, ingress, vmm, and vm_metrics packages. Add hypervisor abstraction with registration pattern (RegisterSocketName, RegisterVsockDialerFactory, RegisterClientFactory) to decouple instance management from specific hypervisor implementations. Add "vz" to the OpenAPI hypervisor type enum, erofs disk format support, and insecure registry option for builds. No behavioral changes on Linux. macOS can now compile but has no VM functionality yet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f21c072 commit b730167

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+1460
-559
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,17 @@ cloud-hypervisor/**
2222
lib/system/exec_agent/exec-agent
2323
lib/system/guest_agent/guest-agent
2424
lib/system/init/init
25+
lib/hypervisor/vz/vz-shim/vz-shim
2526

2627
# Envoy binaries
2728
lib/ingress/binaries/**
2829
dist/**
2930

3031
# UTM VM - downloaded ISO files
3132
scripts/utm/images/
33+
34+
# IDE and editor
35+
.cursor/
36+
37+
# Build artifacts
38+
api

Makefile

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
SHELL := /bin/bash
2-
.PHONY: oapi-generate generate-vmm-client generate-wire generate-all dev build test install-tools gen-jwt download-ch-binaries download-ch-spec ensure-ch-binaries build-caddy-binaries build-caddy ensure-caddy-binaries release-prep clean build-embedded
2+
.PHONY: oapi-generate generate-vmm-client generate-wire generate-all dev build build-linux build-darwin test test-linux test-darwin install-tools gen-jwt download-ch-binaries download-ch-spec ensure-ch-binaries build-caddy-binaries build-caddy ensure-caddy-binaries release-prep clean build-embedded
33

44
# Directory where local binaries will be installed
55
BIN_DIR ?= $(CURDIR)/bin
@@ -174,33 +174,57 @@ ensure-caddy-binaries:
174174
fi
175175

176176
# Build guest-agent (guest binary) into its own directory for embedding
177+
# Cross-compile for Linux since it runs inside the VM
177178
lib/system/guest_agent/guest-agent: lib/system/guest_agent/*.go
178-
@echo "Building guest-agent..."
179-
cd lib/system/guest_agent && CGO_ENABLED=0 go build -ldflags="-s -w" -o guest-agent .
179+
@echo "Building guest-agent for Linux..."
180+
cd lib/system/guest_agent && CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o guest-agent .
180181

181182
# Build init binary (runs as PID 1 in guest VM) for embedding
183+
# Cross-compile for Linux since it runs inside the VM
182184
lib/system/init/init: lib/system/init/*.go
183-
@echo "Building init binary..."
184-
cd lib/system/init && CGO_ENABLED=0 go build -ldflags="-s -w" -o init .
185+
@echo "Building init binary for Linux..."
186+
cd lib/system/init && CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o init .
185187

186188
build-embedded: lib/system/guest_agent/guest-agent lib/system/init/init
187189

188190
# Build the binary
189-
build: ensure-ch-binaries ensure-caddy-binaries build-embedded | $(BIN_DIR)
191+
build:
192+
ifeq ($(shell uname -s),Darwin)
193+
$(MAKE) build-darwin
194+
else
195+
$(MAKE) build-linux
196+
endif
197+
198+
build-linux: ensure-ch-binaries ensure-caddy-binaries build-embedded | $(BIN_DIR)
199+
go build -tags containers_image_openpgp -o $(BIN_DIR)/hypeman ./cmd/api
200+
201+
# Build for macOS (no CH/Caddy needed; guest binaries cross-compiled for Linux)
202+
build-darwin: build-embedded | $(BIN_DIR)
190203
go build -tags containers_image_openpgp -o $(BIN_DIR)/hypeman ./cmd/api
191204

192205
# Build all binaries
193206
build-all: build
194207

195208
# Run in development mode with hot reload
196-
dev: ensure-ch-binaries ensure-caddy-binaries build-embedded $(AIR)
209+
dev: dev-linux
210+
211+
# Linux development mode with hot reload
212+
dev-linux: ensure-ch-binaries ensure-caddy-binaries build-embedded $(AIR)
197213
@rm -f ./tmp/main
198214
$(AIR) -c .air.toml
199215

200-
# Run tests (as root for network capabilities, enables caching and parallelism)
216+
# Run tests
201217
# Usage: make test - runs all tests
202218
# make test TEST=TestCreateInstanceWithNetwork - runs specific test
203-
test: ensure-ch-binaries ensure-caddy-binaries build-embedded
219+
test:
220+
ifeq ($(shell uname -s),Darwin)
221+
$(MAKE) test-darwin
222+
else
223+
$(MAKE) test-linux
224+
endif
225+
226+
# Linux tests (as root for network capabilities)
227+
test-linux: ensure-ch-binaries ensure-caddy-binaries build-embedded
204228
@VERBOSE_FLAG=""; \
205229
if [ -n "$(VERBOSE)" ]; then VERBOSE_FLAG="-v"; fi; \
206230
if [ -n "$(TEST)" ]; then \
@@ -210,6 +234,24 @@ test: ensure-ch-binaries ensure-caddy-binaries build-embedded
210234
sudo env "PATH=$$PATH" "DOCKER_CONFIG=$${DOCKER_CONFIG:-$$HOME/.docker}" go test -tags containers_image_openpgp $$VERBOSE_FLAG -timeout=180s ./...; \
211235
fi
212236

237+
# macOS tests (no sudo needed, adds e2fsprogs to PATH)
238+
# Uses 'go list' to discover compilable packages, then filters out packages
239+
# whose test files reference Linux-only symbols (network, devices, system/init).
240+
DARWIN_EXCLUDE_PKGS := /lib/network|/lib/devices|/lib/system/init
241+
test-darwin: build-embedded
242+
@VERBOSE_FLAG=""; \
243+
if [ -n "$(VERBOSE)" ]; then VERBOSE_FLAG="-v"; fi; \
244+
PKGS=$$(PATH="/opt/homebrew/opt/e2fsprogs/sbin:$(PATH)" \
245+
go list -tags containers_image_openpgp ./... 2>/dev/null | grep -Ev '$(DARWIN_EXCLUDE_PKGS)'); \
246+
if [ -n "$(TEST)" ]; then \
247+
echo "Running specific test: $(TEST)"; \
248+
PATH="/opt/homebrew/opt/e2fsprogs/sbin:$(PATH)" \
249+
go test -tags containers_image_openpgp -run=$(TEST) $$VERBOSE_FLAG -timeout=180s $$PKGS; \
250+
else \
251+
PATH="/opt/homebrew/opt/e2fsprogs/sbin:$(PATH)" \
252+
go test -tags containers_image_openpgp $$VERBOSE_FLAG -timeout=180s $$PKGS; \
253+
fi
254+
213255
# Generate JWT token for testing
214256
# Usage: make gen-jwt [USER_ID=test-user]
215257
gen-jwt: $(GODOTENV)
@@ -233,8 +275,10 @@ clean:
233275
rm -rf lib/ingress/binaries/
234276
rm -f lib/system/guest_agent/guest-agent
235277
rm -f lib/system/init/init
278+
rm -f lib/hypervisor/vz/vz-shim/vz-shim
236279

237280
# Prepare for release build (called by GoReleaser)
238281
# Downloads all embedded binaries and builds embedded components
239282
release-prep: download-ch-binaries build-caddy-binaries build-embedded
240283
go mod tidy
284+

cmd/api/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ type Config struct {
115115
RegistryCACertFile string // Path to CA certificate file for registry TLS verification
116116
BuildTimeout int // Default build timeout in seconds
117117
BuildSecretsDir string // Directory containing build secrets (optional)
118+
DockerSocket string // Path to Docker socket (for building builder image)
118119

119120
// Hypervisor configuration
120121
DefaultHypervisor string // Default hypervisor type: "cloud-hypervisor" or "qemu"
@@ -213,6 +214,7 @@ func Load() *Config {
213214
RegistryCACertFile: getEnv("REGISTRY_CA_CERT_FILE", ""), // Path to CA cert for registry TLS
214215
BuildTimeout: getEnvInt("BUILD_TIMEOUT", 600),
215216
BuildSecretsDir: getEnv("BUILD_SECRETS_DIR", ""), // Optional: path to directory with build secrets
217+
DockerSocket: getEnv("DOCKER_SOCKET", "/var/run/docker.sock"),
216218

217219
// Hypervisor configuration
218220
DefaultHypervisor: getEnv("DEFAULT_HYPERVISOR", "cloud-hypervisor"),

cmd/api/hypervisor_check_darwin.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//go:build darwin
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
"runtime"
8+
9+
"github.com/Code-Hex/vz/v3"
10+
)
11+
12+
// checkHypervisorAccess verifies Virtualization.framework is available on macOS
13+
func checkHypervisorAccess() error {
14+
if runtime.GOARCH != "arm64" {
15+
return fmt.Errorf("Virtualization.framework on macOS requires Apple Silicon (arm64), got %s", runtime.GOARCH)
16+
}
17+
18+
// Validate virtualization is usable by attempting to get max CPU count
19+
// This will fail if entitlements are missing or virtualization is not available
20+
maxCPU := vz.VirtualMachineConfigurationMaximumAllowedCPUCount()
21+
if maxCPU < 1 {
22+
return fmt.Errorf("Virtualization.framework reports 0 max CPUs - check entitlements")
23+
}
24+
25+
return nil
26+
}
27+
28+
// hypervisorAccessCheckName returns the name of the hypervisor access check for logging
29+
func hypervisorAccessCheckName() string {
30+
return "Virtualization.framework"
31+
}

cmd/api/hypervisor_check_linux.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//go:build linux
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
"os"
8+
)
9+
10+
// checkHypervisorAccess verifies KVM is available and the user has permission to use it
11+
func checkHypervisorAccess() error {
12+
f, err := os.OpenFile("/dev/kvm", os.O_RDWR, 0)
13+
if err != nil {
14+
if os.IsNotExist(err) {
15+
return fmt.Errorf("/dev/kvm not found - KVM not enabled or not supported")
16+
}
17+
if os.IsPermission(err) {
18+
return fmt.Errorf("permission denied accessing /dev/kvm - user not in 'kvm' group")
19+
}
20+
return fmt.Errorf("cannot access /dev/kvm: %w", err)
21+
}
22+
f.Close()
23+
return nil
24+
}
25+
26+
// hypervisorAccessCheckName returns the name of the hypervisor access check for logging
27+
func hypervisorAccessCheckName() string {
28+
return "KVM"
29+
}

cmd/api/main.go

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,11 @@ func run() error {
130130
logger.Warn("JWT_SECRET not configured - API authentication will fail")
131131
}
132132

133-
// Verify KVM access (required for VM creation)
134-
if err := checkKVMAccess(); err != nil {
135-
return fmt.Errorf("KVM access check failed: %w\n\nEnsure:\n 1. KVM is enabled (check /dev/kvm exists)\n 2. User is in 'kvm' group: sudo usermod -aG kvm $USER\n 3. Log out and back in, or use: newgrp kvm", err)
133+
// Verify hypervisor access (KVM on Linux, Virtualization.framework on macOS)
134+
if err := checkHypervisorAccess(); err != nil {
135+
return fmt.Errorf("hypervisor access check failed: %w", err)
136136
}
137-
logger.Info("KVM access verified")
137+
logger.Info("Hypervisor access verified", "type", hypervisorAccessCheckName())
138138

139139
// Check if QEMU is available (optional - only warn if not present)
140140
if _, err := (&qemu.Starter{}).GetBinaryPath(nil, ""); err != nil {
@@ -465,18 +465,3 @@ func run() error {
465465
return err
466466
}
467467

468-
// checkKVMAccess verifies KVM is available and the user has permission to use it
469-
func checkKVMAccess() error {
470-
f, err := os.OpenFile("/dev/kvm", os.O_RDWR, 0)
471-
if err != nil {
472-
if os.IsNotExist(err) {
473-
return fmt.Errorf("/dev/kvm not found - KVM not enabled or not supported")
474-
}
475-
if os.IsPermission(err) {
476-
return fmt.Errorf("permission denied accessing /dev/kvm - user not in 'kvm' group")
477-
}
478-
return fmt.Errorf("cannot access /dev/kvm: %w", err)
479-
}
480-
f.Close()
481-
return nil
482-
}

0 commit comments

Comments
 (0)