From 6d12234476487079b4e1a6f0a48ee394086f7bc1 Mon Sep 17 00:00:00 2001 From: Eneko Galan Date: Wed, 8 Jul 2026 01:48:28 +0200 Subject: [PATCH 1/4] chore: update .gitignore and CHANGELOG for version 0.7.0 - Added binaries to .gitignore to prevent tracking of build artifacts. - Updated CHANGELOG to include new features and changes for version 0.7.0, highlighting cross-platform support, backend cross-compilation, and improvements in port conflict detection and UI build targets. - Enhanced documentation in CLAUDE.md and ROADMAP.md to reflect the new daemon integration and installation plans. - Updated Makefile to include new build targets for Linux and Windows. - Bumped version to 0.7.0 in backend and UI configurations. --- .github/workflows/ci.yml | 6 + .gitignore | 4 + CHANGELOG.md | 21 +++ CLAUDE.md | 9 +- Makefile | 59 +++++- ROADMAP.md | 2 +- backend/cmd/calf/main.go | 117 ++++++++++-- backend/internal/runtime/lima.go | 128 ++++++++++++- backend/internal/runtime/lima.yaml | 22 ++- backend/internal/runtime/nerdctl.go | 99 +++++++++- backend/internal/runtime/runtime.go | 1 + backend/version/version.go | 2 +- ui/lib/app_shell.dart | 10 - ui/lib/main.dart | 174 ++++++++++++++++-- ui/lib/platform/open_url.dart | 4 + .../screens/compose_group_detail_screen.dart | 11 +- ui/lib/screens/container_detail_screen.dart | 11 +- ui/lib/screens/containers_screen.dart | 13 +- ui/macos/Runner/Release.entitlements | 6 +- ui/pubspec.yaml | 2 +- 20 files changed, 611 insertions(+), 90 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be460f3..b188404 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,12 @@ jobs: working-directory: backend - run: CGO_ENABLED=0 go build -o /dev/null ./cmd/calf working-directory: backend + - name: Cross-compile for Linux + run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /dev/null ./cmd/calf + working-directory: backend + - name: Cross-compile for Windows + run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o /dev/null ./cmd/calf + working-directory: backend ui: runs-on: macos-latest diff --git a/.gitignore b/.gitignore index 94f1119..cece170 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ .DS_Store .vscode + +# Binaries +backend/calf +backend/calf-daemon diff --git a/CHANGELOG.md b/CHANGELOG.md index e810d79..5406bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +## [0.7.0] - 2026-07-08 + +### Added + +- **Cross-platform support** — daemon now builds on Linux, macOS, and Windows +- **Backend cross-compilation** verified in CI for linux/amd64 and windows/amd64 +- **Windows port conflict detection** via `netstat -ano` +- **Linux Flutter build target** (`make ui-linux`, `make release-linux`) +- **Windows Flutter build target** (`make ui-windows`, `make release-windows`) +- **Docker CLI proxy** replaced TCP-level proxy with HTTP reverse proxy (`httputil.ReverseProxy`) to fix compose commands hanging on completion + +### Changed + +- Port conflict detection uses `lsof` on Unix, `netstat` on Windows +- PATH setup on macOS prefers Homebrew paths without hardcoding on other OS +- UI port opening now uses the platform URL opener on all OS (open/xdg-open/rundll32) +- Daemon binary discovery works next to the Flutter executable on all platforms +- Socket dial failure during startup logged at Debug level instead of Warn when socket does not yet exist + ## [0.6.0] - 2026-07-06 ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index 11afa84..19092cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,8 @@ This file provides guidance to AI assistants when working with code in this repo - A **Go daemon** (`backend/`) that manages containers through `containerd` + `nerdctl`, running inside a **Lima** VM on macOS/Windows, or talking directly to the host runtime on Linux. - A **native Flutter GUI** (`ui/`) that drives the daemon over a local REST + WebSocket API. +The Go daemon binary is embedded inside the Flutter `.app` bundle (`Contents/MacOS/calf`). When the app launches, it spawns the daemon as a subprocess and kills it on close. No separate installation or terminal setup required. + The daemon also exposes a Docker-API-compatible socket (`~/.config/calf/docker.sock`), so the real `docker` / `docker compose` CLI can point at Calf via `DOCKER_HOST`. Non-goals (see `ROADMAP.md`): no built-in Kubernetes, no extensions marketplace, no Scout/AI/Cloud features. @@ -49,7 +51,7 @@ Non-goals (see `ROADMAP.md`): no built-in Kubernetes, no extensions marketplace, calf/ ├── backend/ Go daemon │ ├── cmd/calf/ -│ │ └── main.go Entrypoint: config/logger/runtime/server wiring, signal handling, PID file, stale-port takeover +│ │ └── main.go Entrypoint: config/logger/runtime/server wiring, signal handling, PID file, stale-port takeover (cross-platform: lsof/Unix, netstat/Windows) │ ├── internal/ │ │ ├── api/ │ │ │ ├── server.go Server struct, route registration for all /v1/... endpoints @@ -142,7 +144,7 @@ calf/ │ └── .dockerignore ├── scripts/verify-docker-cli.sh Verifies `docker` CLI works against Calf's socket ├── .github/workflows/ci.yml CI: backend (vet/test/build) + ui (analyze/test/build) jobs, macos-latest -├── Makefile dev-backend / dev-ui / ui / clean / verify-docker-cli targets +├── Makefile dev-backend / dev-ui / ui / clean / verify-docker-cli / release / ui-linux / ui-windows / release-linux / release-windows targets ├── README.md Project pitch + quick start ├── DEVELOPMENT.md Dev setup, config file example, Docker Desktop migration walkthrough ├── ROADMAP.md Phased plan, architecture diagram, non-goals, competitor comparison @@ -207,7 +209,7 @@ Docker Hub OAuth2 device-code flow client. Polls for a token, decodes JWT claims ## UI File Reference (`ui/lib/`) ### `main.dart` -Builds a Material `ThemeData` bridged from a `ShadThemeData` (shadcn_ui), with a hardcoded brand primary color (`#2496ED`). Light/dark `ShadThemeData` instances are built once as top-level finals. +App entrypoint. Calls `_startDaemon()` to spawn the Go daemon binary (found next to the Flutter executable — `.app` bundle on macOS, alongside the binary on Linux/Windows) and waits for `/v1/status` to respond before showing the UI. Kills the daemon on app close. Inserts common Homebrew paths into `PATH` on macOS (no-op on other platforms). Builds a Material `ThemeData` bridged from a `ShadThemeData` (shadcn_ui), with a hardcoded brand primary color (`#2496ED`). Light/dark `ShadThemeData` instances are built once as top-level finals. ### `app_shell.dart` Sidebar navigation (Containers / Images / Volumes / Builds) plus the settings screen, and a top bar showing Docker Hub registry sign-in status. `SettingsScreen` handles CPU/memory/swap slider configuration (bounded by host capacity from `/v1/config`), Docker Desktop migration trigger + polling, and theme mode switching. @@ -252,6 +254,7 @@ Simple JSON files under `~/.config/calf/ui/.json` (via `path_provider`'s a make dev-backend # cd backend && CGO_ENABLED=0 go run ./cmd/calf make dev-ui # flutter run -d macos make ui # flutter build macos (alias: make build) +make release # build Go daemon + Flutter .app and bundle together make clean # flutter clean make verify-docker-cli # runs scripts/verify-docker-cli.sh ``` diff --git a/Makefile b/Makefile index 9f4a387..364770f 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,57 @@ -.PHONY: help ui build clean dev dev-backend dev-ui verify-docker-cli +.PHONY: help ui-macos clean dev dev-backend dev-ui-macos verify-docker-cli release-macos \ + ui-linux ui-windows dev-ui-linux dev-ui-windows release-linux release-windows release ui help: @echo "Calf — common commands" @echo "" - @echo " make dev-backend API daemon on :8765 (terminal 1)" - @echo " make dev-ui Flutter app on macOS (terminal 2)" + @echo " make dev-backend API daemon on :8765 (terminal 1)" + + @echo " make dev-ui-macos Flutter app on macOS (terminal 2)" + @echo " make dev-ui-linux Flutter app on Linux (terminal 2)" + @echo " make dev-ui-windows Flutter app on Windows (terminal 2)" + @echo "" - @echo " make ui build macOS app" - @echo " make build build macOS app" - @echo " make clean remove build artifacts" + @echo " make ui-macos build macOS app" + @echo " make ui-linux build Linux app" + @echo " make ui-windows build Windows app" + + @echo "" + @echo " make release-macos build Go daemon + macOS app" + @echo " make release-linux build Go daemon + Linux app" + @echo " make release-windows build Go daemon + Windows app" + @echo " make release build for all platforms (macOS + Linux + Windows)" + @echo " make clean remove build artifacts" @echo " make verify-docker-cli smoke-test docker CLI against Calf" @echo "" @echo "Full guide: DEVELOPMENT.md" -ui: +ui-macos: cd ui && flutter build macos -build: ui +ui-linux: + cd ui && flutter build linux + +ui-windows: + cd ui && flutter build windows + +release: release-macos release-linux release-windows + +release-macos: + cd backend && CGO_ENABLED=0 go build -o calf-daemon ./cmd/calf + cd ui && flutter build macos + cp backend/calf-daemon ui/build/macos/Build/Products/Release/Calf.app/Contents/MacOS/calf-daemon + rm backend/calf-daemon + codesign --force --deep --sign - --entitlements ui/macos/Runner/Release.entitlements ui/build/macos/Build/Products/Release/Calf.app + +release-linux: ui-linux + cd backend && CGO_ENABLED=0 go build -o calf-daemon ./cmd/calf + cp backend/calf-daemon ui/build/linux/x64/release/bundle/calf-daemon + rm backend/calf-daemon + +release-windows: ui-windows + cd backend && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o calf-daemon.exe ./cmd/calf + cp backend/calf-daemon.exe ui/build/windows/x64/runner/Release/calf-daemon.exe + rm backend/calf-daemon.exe clean: cd ui && flutter clean @@ -24,9 +59,15 @@ clean: dev-backend: cd backend && CGO_ENABLED=0 go run ./cmd/calf -dev-ui: +dev-ui-macos: cd ui && flutter run -d macos +dev-ui-linux: + cd ui && flutter run -d linux + +dev-ui-windows: + cd ui && flutter run -d windows + verify-docker-cli: ./scripts/verify-docker-cli.sh diff --git a/ROADMAP.md b/ROADMAP.md index f831867..db9a7a3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -142,7 +142,7 @@ Be a **valid** Docker Desktop replacement for local development: same CLI (`dock ### 3.1 Installation and lifecycle - [ ] macOS installer (.dmg / .pkg) with signing and notarization -- [ ] Daemon as a user service (launchd / systemd) +- [x] Daemon embedded in .app bundle, spawned by Flutter app on launch, killed on close - [ ] In-app updates - [ ] Windows installer (WSL2 + integration) - [ ] Linux installer (.deb / .rpm / AppImage) diff --git a/backend/cmd/calf/main.go b/backend/cmd/calf/main.go index 1d021b6..9787887 100644 --- a/backend/cmd/calf/main.go +++ b/backend/cmd/calf/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "log/slog" "net" @@ -9,6 +10,7 @@ import ( "os/exec" "os/signal" "path/filepath" + goRuntime "runtime" "strconv" "strings" "syscall" @@ -24,6 +26,8 @@ func main() { } func run() int { + os.Setenv("PATH", ensurePath()) + cfg, err := config.Load() if err != nil { slog.Error("failed to load config", "error", err) @@ -57,6 +61,23 @@ func run() int { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() + go func() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if os.Getppid() == 1 { + logger.Warn("parent process died, shutting down") + stop() + return + } + } + } + }() + rtCtx, rtCancel := context.WithCancel(ctx) defer rtCancel() @@ -82,8 +103,8 @@ func run() int { if err != nil { logger.Error("server stopped", "error", err) rtCancel() - stopCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() + stopCtx, stopCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer stopCancel() if stopErr := rt.Stop(stopCtx); stopErr != nil { logger.Error("runtime stop failed", "error", stopErr) } @@ -91,16 +112,16 @@ func run() int { } case <-ctx.Done(): logger.Info("shutting down") - shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownCancel() if err := server.Shutdown(shutdownCtx); err != nil { logger.Error("shutdown failed", "error", err) } } rtCancel() - stopCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() + stopCtx, stopCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer stopCancel() if err := rt.Stop(stopCtx); err != nil { logger.Error("runtime stop failed", "error", err) } @@ -130,8 +151,8 @@ func ensurePort(addr string) error { return nil } - pid := findPidOnPort(port) - if pid == 0 { + pid, err := findPidOnPort(port) + if err != nil || pid == 0 { return fmt.Errorf("port %s is in use; run: pkill -f calf", addr) } @@ -140,8 +161,14 @@ func ensurePort(addr string) error { return fmt.Errorf("port %s is in use by pid %d", addr, pid) } - proc, _ := os.FindProcess(pid) - if proc != nil { + proc, err := os.FindProcess(pid) + if err != nil { + return fmt.Errorf("port %s is in use by pid %d, but process not found", addr, pid) + } + + if goRuntime.GOOS == "windows" { + proc.Kill() + } else { proc.Signal(syscall.SIGTERM) } @@ -157,10 +184,19 @@ func ensurePort(addr string) error { return fmt.Errorf("port %s is still in use after cleanup; run: pkill -f calf", addr) } -func findPidOnPort(port string) int { +func findPidOnPort(port string) (int, error) { + switch goRuntime.GOOS { + case "windows": + return findPidOnPortWindows(port) + default: + return findPidOnPortUnix(port) + } +} + +func findPidOnPortUnix(port string) (int, error) { out, err := exec.Command("lsof", "-ti", fmt.Sprintf(":%s", port), "-s", "TCP:LISTEN").Output() if err != nil { - return 0 + return 0, err } for _, raw := range strings.Fields(string(out)) { pid, err := strconv.Atoi(strings.TrimSpace(raw)) @@ -168,10 +204,31 @@ func findPidOnPort(port string) int { continue } if pid != os.Getpid() && pid != os.Getppid() { - return pid + return pid, nil } } - return 0 + return 0, errors.New("no pid found on port") +} + +func findPidOnPortWindows(port string) (int, error) { + out, err := exec.Command("netstat", "-ano").Output() + if err != nil { + return 0, err + } + target := fmt.Sprintf(":%s", port) + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if strings.Contains(line, "LISTENING") && strings.Contains(line, target) { + fields := strings.Fields(line) + if len(fields) > 4 { + pid, err := strconv.Atoi(strings.TrimSpace(fields[len(fields)-1])) + if err == nil && pid != os.Getpid() && pid != os.Getppid() { + return pid, nil + } + } + } + } + return 0, errors.New("no pid found on port") } func pidFilePath() string { @@ -179,6 +236,38 @@ func pidFilePath() string { return filepath.Join(home, ".config", "calf", "calf.pid") } +func ensurePath() string { + existing := os.Getenv("PATH") + if goRuntime.GOOS == "windows" { + return existing + } + + needed := false + for _, dir := range []string{"/opt/homebrew/bin", "/usr/local/bin"} { + if _, err := os.Stat(filepath.Join(dir, "limactl")); err == nil && !inPath(dir, existing) { + existing = dir + ":" + existing + needed = true + } + } + if !needed { + for _, dir := range []string{"/opt/homebrew/bin", "/opt/homebrew/sbin", "/usr/local/bin"} { + if !inPath(dir, existing) { + existing = dir + ":" + existing + } + } + } + return existing +} + +func inPath(dir, path string) bool { + for _, p := range strings.Split(path, ":") { + if p == dir { + return true + } + } + return false +} + func readPidFile() (int, error) { data, err := os.ReadFile(pidFilePath()) if err != nil { diff --git a/backend/internal/runtime/lima.go b/backend/internal/runtime/lima.go index 40636c5..127bc4a 100644 --- a/backend/internal/runtime/lima.go +++ b/backend/internal/runtime/lima.go @@ -4,16 +4,21 @@ import ( "context" _ "embed" "encoding/json" + "errors" "fmt" "io" "log/slog" "net" + "net/http" + "net/http/httputil" + "net/url" "os" "os/exec" "path/filepath" "strings" "sync" "sync/atomic" + "syscall" "time" ) @@ -38,6 +43,8 @@ type Lima struct { lastState State localhostProxy *localhostProxies watcherCancel context.CancelFunc + proxyListener net.Listener + dockerProxy *http.Server } func NewLima(vmName string, dockerSocket string, cpus int, memoryGB int, memorySwapGB int, diskGB int, apiListenPort int, proxy ProxyConfig) *Lima { @@ -102,6 +109,10 @@ func (l *Lima) Start(ctx context.Context) error { } } + if err := l.ensureDockerCLISocket(); err != nil { + limaLogger.Warn("failed to set up Docker CLI socket symlink (non-fatal)", "error", err) + } + l.started.Store(true) wCtx, wCancel := context.WithCancel(context.Background()) l.watcherCancel = wCancel @@ -129,6 +140,7 @@ func (l *Lima) Stop(ctx context.Context) error { } l.localhostProxy.stopAll() + l.removeDockerCLISocket() l.started.Store(false) _, err = runCommand(ctx, "limactl", "stop", l.vmName) @@ -144,29 +156,55 @@ func (l *Lima) Status(ctx context.Context) (Status, error) { } if _, err := exec.LookPath("limactl"); err != nil { + limaLogger.Warn("limactl not found", "error", err, "PATH", os.Getenv("PATH")) + status.Log = fmt.Sprintf("limactl not found: %v (PATH: %s)", err, os.Getenv("PATH")) return status, nil } output, err := runCommand(ctx, "limactl", "list", "--format", "{{.Name}}\t{{.Status}}") if err != nil { + limaLogger.Warn("limactl list failed", "error", err) + status.Log = fmt.Sprintf("limactl list failed: %v", err) return status, nil } + limaLogger.Debug("limactl list output", "output", string(output)) + found := false for _, line := range strings.Split(string(output), "\n") { fields := strings.Split(line, "\t") if len(fields) != 2 || fields[0] != l.vmName { continue } + found = true if strings.Contains(strings.ToLower(fields[1]), "running") { status.State = StateRunning + } else { + limaLogger.Warn("vm not running", "vm", l.vmName, "status", fields[1]) } } + if !found { + status.Log = "VM not found in limactl list" + } + if status.State == StateRunning && l.dockerSocket != "" { conn, err := net.DialTimeout("unix", l.dockerSocket, 100*time.Millisecond) if err != nil { - status.State = StateStopped + if errors.Is(err, syscall.ENOENT) { + limaLogger.Debug("socket not ready yet (expected during startup)", "socket", l.dockerSocket) + } else { + limaLogger.Warn("socket dial failed", "socket", l.dockerSocket, "error", err) + } + // Fall back to TCP on port 2375 (via Lima port forward + socat inside VM) + tcpConn, tcpErr := net.DialTimeout("tcp", "127.0.0.1:2375", 100*time.Millisecond) + if tcpErr != nil { + limaLogger.Warn("tcp fallback also failed", "error", tcpErr) + status.State = StateStopped + status.Log = fmt.Sprintf("socket dial failed: %v", err) + } else { + tcpConn.Close() + } } else { conn.Close() } @@ -447,7 +485,7 @@ func (l *Lima) StreamLogsFollow(ctx context.Context, id string, output func(stri } func (l *Lima) streamLogsFollow(ctx context.Context, id, since string, output func(string)) error { - command := exec.CommandContext(ctx, "limactl", append([]string{"shell", l.vmName, "--"}, NerdctlVMArgs("logs", "-f", "--since", since, id)...)...) + command := exec.CommandContext(ctx, "limactl", append([]string{"shell", l.vmName, "--", "sudo", "docker"}, "logs", "-f", "--since", since, id)...) command.Env = limaShellEnv() return streamCommandLogs(ctx, command, output) } @@ -494,7 +532,7 @@ func (l *Lima) AttachExec(ctx context.Context, id string, stdin io.Reader, onOut return err } - shellArgs := append([]string{"shell", l.vmName, "--"}, NerdctlVMArgs(interactiveExecArgs(id)...)...) + shellArgs := append([]string{"shell", l.vmName, "--"}, append([]string{"docker"}, interactiveExecArgs(id)...)...) command := exec.CommandContext(ctx, "limactl", shellArgs...) return attachExecInContainer(ctx, command, stdin, onOutput, resizeCh) } @@ -554,9 +592,12 @@ func (l *Lima) RegistryLogout(ctx context.Context, server string) error { return registryLogout(ctx, l.runInVM, server) } +// vmCommand routes commands for the Lima VM. "nerdctl" is transparently +// redirected to "docker" because the VM runs Docker Engine — containers +// live in its moby namespace, not containerd's default namespace. func vmCommand(command string, args ...string) []string { if command == "nerdctl" { - return NerdctlVMArgs(args...) + return append([]string{"sudo", "docker"}, args...) } return append([]string{command}, args...) @@ -658,3 +699,82 @@ func defaultDockerSocket() string { return filepath.Join(home, ".config", "calf", "docker.sock") } + +func dockerCLISocketPath() string { + home, err := os.UserHomeDir() + if err != nil { + return "" + } + + return filepath.Join(home, ".docker", "run", "docker.sock") +} + +func (l *Lima) ensureDockerCLISocket() error { + if l.dockerSocket == "" { + return nil + } + + dir := filepath.Dir(l.dockerSocket) + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("create %s: %w", dir, err) + } + + os.Remove(l.dockerSocket) + + listener, err := net.Listen("unix", l.dockerSocket) + if err != nil { + return fmt.Errorf("listen on %s: %w", l.dockerSocket, err) + } + l.proxyListener = listener + + dockerCliPath := dockerCLISocketPath() + if dockerCliPath != "" { + os.MkdirAll(filepath.Dir(dockerCliPath), 0o755) + os.Remove(dockerCliPath) + if err := os.Symlink(l.dockerSocket, dockerCliPath); err != nil { + limaLogger.Warn("failed to create Docker CLI symlink", "path", dockerCliPath, "error", err) + } + } + + go l.serveTCPProxy() + + return nil +} + +func (l *Lima) serveTCPProxy() { + targetURL, err := url.Parse("http://127.0.0.1:2375") + if err != nil { + limaLogger.Warn("docker HTTP proxy: invalid target URL", "error", err) + return + } + + proxy := httputil.NewSingleHostReverseProxy(targetURL) + proxy.ErrorLog = slog.NewLogLogger(slog.NewTextHandler(io.Discard, nil), slog.LevelError) + proxy.Transport = &http.Transport{ + DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext, + } + + l.dockerProxy = &http.Server{Handler: proxy} + + if err := l.dockerProxy.Serve(l.proxyListener); err != nil && err != http.ErrServerClosed { + limaLogger.Warn("docker HTTP proxy: serve error", "error", err) + } +} + +func (l *Lima) removeDockerCLISocket() { + if l.dockerProxy != nil { + l.dockerProxy.Close() + l.dockerProxy = nil + } + if l.proxyListener != nil { + l.proxyListener.Close() + l.proxyListener = nil + } + + os.Remove(l.dockerSocket) + + dockerCliPath := dockerCLISocketPath() + if dockerCliPath != "" { + os.Remove(dockerCliPath) + } +} diff --git a/backend/internal/runtime/lima.yaml b/backend/internal/runtime/lima.yaml index 3e63edb..be36a08 100644 --- a/backend/internal/runtime/lima.yaml +++ b/backend/internal/runtime/lima.yaml @@ -36,7 +36,7 @@ provision: #!/bin/bash set -eux -o pipefail apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y containerd docker.io curl ca-certificates + DEBIAN_FRONTEND=noninteractive apt-get install -y containerd docker.io curl ca-certificates socat systemctl enable --now containerd systemctl enable --now docker if [ -f /mnt/lima-cidata/lima.env ]; then @@ -83,6 +83,22 @@ provision: sudo systemctl daemon-reload sudo systemctl restart containerd docker + sudo tee /etc/systemd/system/docker-tcp-proxy.service >/dev/null <<'SERVICEEOF' + [Unit] + Description=Docker TCP Proxy + After=docker.service + Requires=docker.service + [Service] + Type=simple + ExecStart=/usr/bin/socat TCP-LISTEN:2375,reuseaddr,fork UNIX-CONNECT:/var/run/docker.sock + Restart=always + RestartSec=5 + [Install] + WantedBy=multi-user.target +SERVICEEOF + sudo systemctl enable docker-tcp-proxy + sudo systemctl start docker-tcp-proxy + portForwards: - - guestSocket: "/var/run/docker.sock" - hostSocket: "%[2]s/.config/calf/docker.sock" + - guestPort: 2375 + hostPort: 2375 diff --git a/backend/internal/runtime/nerdctl.go b/backend/internal/runtime/nerdctl.go index 8249acc..664264a 100644 --- a/backend/internal/runtime/nerdctl.go +++ b/backend/internal/runtime/nerdctl.go @@ -60,7 +60,93 @@ func listContainers(ctx context.Context, run commandRunner) ([]Container, error) return nil, err } - return ParseContainerLines(output) + return parseContainerLines(output) +} + +func parseContainerLines(output []byte) ([]Container, error) { + containers := make([]Container, 0) + seen := make(map[string]bool) + + scanner := bufio.NewScanner(bytes.NewReader(output)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + + var row struct { + ID string `json:"ID"` + Names string `json:"Names"` + Image string `json:"Image"` + State string `json:"State"` + Status string `json:"Status"` + Ports string `json:"Ports"` + CreatedAt string `json:"CreatedAt"` + Labels string `json:"Labels"` + } + if err := json.Unmarshal([]byte(line), &row); err != nil { + continue + } + + if row.ID == "" || seen[row.ID] { + continue + } + seen[row.ID] = true + + containerID := row.ID + if len(containerID) > 12 { + containerID = containerID[:12] + } + + containerName := row.Names + if containerName == "" { + containerName = containerID + } + + labels := parseCommaLabels(row.Labels) + project, service := composeFields(containerName, labels) + + containers = append(containers, Container{ + ID: containerID, + Name: containerName, + Image: row.Image, + State: containerState(row.Status, row.State), + Status: row.Status, + Ports: row.Ports, + Created: row.CreatedAt, + ComposeProject: project, + ComposeService: service, + }) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + inferComposeProjects(containers) + + return containers, nil +} + +func parseCommaLabels(raw string) map[string]string { + if raw == "" { + return nil + } + + labels := make(map[string]string) + for _, pair := range strings.Split(raw, ",") { + pair = strings.TrimSpace(pair) + if pair == "" { + continue + } + + parts := strings.SplitN(pair, "=", 2) + if len(parts) == 2 { + labels[parts[0]] = parts[1] + } + } + + return labels } func listImages(ctx context.Context, run commandRunner) ([]Image, error) { @@ -118,6 +204,8 @@ func pushImage(ctx context.Context, run commandRunner, ref string) error { return nil } + + func ParseContainerLines(output []byte) ([]Container, error) { containers := make([]Container, 0) scanner := bufio.NewScanner(bytes.NewReader(output)) @@ -133,11 +221,16 @@ func ParseContainerLines(output []byte) ([]Container, error) { continue } - project, service := composeFields(row.Names, row.Labels) + containerName := row.Names + if containerName == "" { + containerName = row.ID + } + + project, service := composeFields(containerName, row.Labels) containers = append(containers, Container{ ID: row.ID, - Name: row.Names, + Name: containerName, Image: row.Image, State: containerState(row.Status, row.State), Status: row.Status, diff --git a/backend/internal/runtime/runtime.go b/backend/internal/runtime/runtime.go index b424d37..34b5994 100644 --- a/backend/internal/runtime/runtime.go +++ b/backend/internal/runtime/runtime.go @@ -34,6 +34,7 @@ type Status struct { DockerSocket string `json:"docker_socket"` VMName string `json:"vm_name,omitempty"` PortConflicts []PortConflict `json:"port_conflicts,omitempty"` + Log string `json:"log,omitempty"` } type Container struct { diff --git a/backend/version/version.go b/backend/version/version.go index 6cc8872..2c0474f 100644 --- a/backend/version/version.go +++ b/backend/version/version.go @@ -1,3 +1,3 @@ package version -const Version = "0.6.0" +const Version = "0.7.0" diff --git a/ui/lib/app_shell.dart b/ui/lib/app_shell.dart index 77b3f75..77194b8 100644 --- a/ui/lib/app_shell.dart +++ b/ui/lib/app_shell.dart @@ -247,7 +247,6 @@ class SettingsScreen extends StatefulWidget { } class _SettingsScreenState extends State { - bool _startAtLogin = false; Config? _config; bool _configLoading = true; String? _configError; @@ -465,15 +464,6 @@ class _SettingsScreenState extends State { children: [ Text('Settings', style: theme.textTheme.h3), const SizedBox(height: 24), - _sectionHeader('General', theme), - const SizedBox(height: 12), - _settingRow( - 'Start Calf when you sign in to your computer', - ShadSwitch( - value: _startAtLogin, - onChanged: (value) => setState(() => _startAtLogin = value), - ), - ), const SizedBox(height: 12), _settingRow( 'Use Calf for Docker CLI', diff --git a/ui/lib/main.dart b/ui/lib/main.dart index 9428da3..4817e75 100644 --- a/ui/lib/main.dart +++ b/ui/lib/main.dart @@ -1,4 +1,8 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:ui/api/client.dart'; @@ -22,6 +26,78 @@ final _darkShadTheme = ShadThemeData( ), ); +Process? _daemonProcess; + +Future _startDaemon() async { + final daemonPath = _findDaemon(); + + if (daemonPath == null) { + return; + } + + try { + final env = Map.from(Platform.environment); + final path = env['PATH'] ?? ''; + final extras = _extraPaths(); + if (extras.isNotEmpty) { + env['PATH'] = '$extras:$path'; + } + _daemonProcess = await Process.start(daemonPath, [], runInShell: false, environment: env); + _daemonProcess!.stdout.listen((data) => stdout.add(data)); + _daemonProcess!.stderr.listen((data) => stderr.add(data)); + _daemonProcess!.exitCode.then((code) { + stderr.writeln('calf-daemon exited with code $code'); + if (_daemonProcess == null) return; + _daemonProcess = null; + Future.delayed(const Duration(seconds: 1), _startDaemon); + }); + } catch (e) { + stderr.writeln('failed to start calf-daemon: $e'); + } +} + +String? _findDaemon() { + final dir = File(Platform.resolvedExecutable).parent.path; + final daemonPath = '$dir/calf-daemon'; + if (File(daemonPath).existsSync()) { + return daemonPath; + } + // On macOS, daemon is next to the executable inside the .app bundle + // On Linux/Windows, also check next to executable + stderr.writeln('calf-daemon not found at $daemonPath'); + return null; +} + +String _extraPaths() { + if (Platform.isMacOS) { + final path = Platform.environment['PATH'] ?? ''; + final missing = []; + for (final dir in ['/opt/homebrew/bin', '/opt/homebrew/sbin', '/usr/local/bin']) { + if (!path.contains(dir)) { + missing.add(dir); + } + } + return missing.join(':'); + } + return ''; +} + +Future _stopDaemon() async { + final process = _daemonProcess; + if (process == null) return; + _daemonProcess = null; + process.kill(); + await process.exitCode.timeout(const Duration(seconds: 5), onTimeout: () { + process.kill(ProcessSignal.sigkill); + return -1; + }); +} + +void main() { + _startDaemon(); + runApp(const MainApp()); +} + ThemeData _materialTheme(ShadThemeData shadTheme) { return ThemeData( fontFamily: shadTheme.textTheme.family, @@ -46,10 +122,6 @@ ThemeData _materialTheme(ShadThemeData shadTheme) { ); } -void main() { - runApp(const MainApp()); -} - class MainApp extends StatefulWidget { const MainApp({super.key, this.apiClient}); @@ -59,8 +131,73 @@ class MainApp extends StatefulWidget { State createState() => _MainAppState(); } -class _MainAppState extends State { +class _MainAppState extends State with WidgetsBindingObserver { ThemeMode _themeMode = ThemeMode.system; + bool _daemonReady = false; + String? _error; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + if (widget.apiClient == null) { + _waitForDaemon(); + } else { + _daemonReady = true; + } + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.detached) { + _stopDaemon(); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _stopDaemon(); + super.dispose(); + } + + Future _waitForDaemon() async { + const url = 'http://127.0.0.1:8765/v1/status'; + final attempts = 120; + final client = http.Client(); + + for (var i = 0; i < attempts; i++) { + try { + final response = await client.get(Uri.parse(url)).timeout(const Duration(seconds: 2)); + if (response.statusCode == 200) { + final body = jsonDecode(response.body); + if (body is Map) { + final runtime = body['runtime']; + if (runtime is Map && runtime['state'] == 'running') { + client.close(); + if (mounted) { + setState(() => _daemonReady = true); + } + return; + } + if (runtime is Map && runtime['log'] != null && runtime['log'] != '') { + if (mounted) { + setState(() => _error = runtime['log']); + } + } + } + } + } catch (_) {} + await Future.delayed(const Duration(milliseconds: 500)); + } + client.close(); + if (mounted) { + setState(() { + _daemonReady = true; + _error = _error ?? 'Daemon did not become ready in time. Try restarting Calf.'; + }); + } + } @override Widget build(BuildContext context) { @@ -82,16 +219,31 @@ class _MainAppState extends State { ), child: ColoredBox( color: shadTheme.colorScheme.background, - child: child ?? const SizedBox.shrink(), + child: _daemonReady + ? (child ?? const SizedBox.shrink()) + : Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + if (_error != null) ...[ + const SizedBox(height: 16), + Text(_error!, style: TextStyle(color: shadTheme.colorScheme.destructive)), + ], + ], + ), + ), ), ), ); }, - home: AppShell( - apiClient: widget.apiClient, - themeMode: _themeMode, - onThemeModeChanged: (mode) => setState(() => _themeMode = mode), - ), + home: _daemonReady + ? AppShell( + apiClient: widget.apiClient, + themeMode: _themeMode, + onThemeModeChanged: (mode) => setState(() => _themeMode = mode), + ) + : const SizedBox.shrink(), ); } } diff --git a/ui/lib/platform/open_url.dart b/ui/lib/platform/open_url.dart index 6962fef..95b895f 100644 --- a/ui/lib/platform/open_url.dart +++ b/ui/lib/platform/open_url.dart @@ -1,5 +1,9 @@ import 'dart:io'; +void openPort(int port) { + openExternalUrl('http://localhost:$port'); +} + Future openExternalUrl(String url) async { if (url.isEmpty) { return false; diff --git a/ui/lib/screens/compose_group_detail_screen.dart b/ui/lib/screens/compose_group_detail_screen.dart index 73a37a4..c404743 100644 --- a/ui/lib/screens/compose_group_detail_screen.dart +++ b/ui/lib/screens/compose_group_detail_screen.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'dart:io' show Platform, Process; import 'package:flutter/material.dart' show Tooltip; import 'package:flutter/widgets.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:ui/api/client.dart'; +import 'package:ui/platform/open_url.dart'; import 'package:ui/widgets/calf_button.dart'; import 'package:ui/widgets/hover_list_row.dart'; import 'package:ui/widgets/logs_panel.dart'; @@ -205,13 +205,6 @@ class _ComposeGroupDetailViewState extends State { }); } - void _openPort(int port) { - if (!Platform.isMacOS) { - return; - } - Process.run('open', ['http://localhost:$port']); - } - @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); @@ -291,7 +284,7 @@ class _ComposeGroupDetailViewState extends State { onStart: (id) => _runAction(() => widget.apiClient.startContainer(id)), onStop: (id) => _runAction(() => widget.apiClient.stopContainer(id)), onRemove: (id) => _runAction(() => widget.apiClient.removeContainer(id)), - onOpenPort: _openPort, + onOpenPort: openPort, busy: _busy, ), ), diff --git a/ui/lib/screens/container_detail_screen.dart b/ui/lib/screens/container_detail_screen.dart index 1829f73..ea0d128 100644 --- a/ui/lib/screens/container_detail_screen.dart +++ b/ui/lib/screens/container_detail_screen.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io' show Platform, Process; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart' show Divider, SelectableText; @@ -11,6 +10,7 @@ import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:xterm/xterm.dart'; import 'package:ui/api/client.dart'; +import 'package:ui/platform/open_url.dart'; import 'package:ui/widgets/calf_button.dart'; import 'package:ui/widgets/files_panel.dart'; import 'package:ui/widgets/logs_panel.dart'; @@ -307,13 +307,6 @@ class _ContainerDetailViewState extends State { } } - void _openPort(int port) { - if (!Platform.isMacOS) { - return; - } - Process.run('open', ['http://localhost:$port']); - } - @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); @@ -361,7 +354,7 @@ class _ContainerDetailViewState extends State { Text(_container.displayImage, style: theme.textTheme.muted), if (port != null) GestureDetector( - onTap: () => _openPort(port), + onTap: () => openPort(port), child: Text( '$port:$port', style: theme.textTheme.small.copyWith(color: theme.colorScheme.primary), diff --git a/ui/lib/screens/containers_screen.dart b/ui/lib/screens/containers_screen.dart index 69f43b9..148f43c 100644 --- a/ui/lib/screens/containers_screen.dart +++ b/ui/lib/screens/containers_screen.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io' show Platform, Process; import 'dart:math' as math; import 'package:flutter/material.dart' show Tooltip; @@ -7,6 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:ui/api/client.dart'; +import 'package:ui/platform/open_url.dart'; import 'package:ui/screens/compose_group_detail_screen.dart'; import 'package:ui/screens/container_detail_screen.dart'; import 'package:ui/widgets/calf_button.dart'; @@ -190,13 +190,6 @@ class _ContainersScreenState extends State { }); } - void _openPort(int port) { - if (!Platform.isMacOS) { - return; - } - Process.run('open', ['http://localhost:$port']); - } - Future _loadGroupPreferences() async { final saved = await ContainerGroupPreferences.loadExpanded(); if (!mounted) { @@ -367,7 +360,7 @@ class _ContainersScreenState extends State { onStopAll: () => _runGroupAction(group.value, widget.apiClient.stopContainer, runningOnly: true), onRemoveAll: () => _runGroupAction(group.value, widget.apiClient.removeContainer), onOpen: _openContainer, - onOpenPort: _openPort, + onOpenPort: openPort, ), for (final container in layout.standalone) _ContainerTile( @@ -378,7 +371,7 @@ class _ContainersScreenState extends State { onStop: () => _runAction(() => widget.apiClient.stopContainer(container.id)), onRemove: () => _runAction(() => widget.apiClient.removeContainer(container.id)), onOpen: () => _openContainer(container), - onOpenPort: _openPort, + onOpenPort: openPort, ), ], ), diff --git a/ui/macos/Runner/Release.entitlements b/ui/macos/Runner/Release.entitlements index 38da9a9..8099d3c 100644 --- a/ui/macos/Runner/Release.entitlements +++ b/ui/macos/Runner/Release.entitlements @@ -2,11 +2,13 @@ - com.apple.security.app-sandbox - com.apple.security.network.client com.apple.security.files.user-selected.read-write + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + diff --git a/ui/pubspec.yaml b/ui/pubspec.yaml index 0c35917..4cc6729 100644 --- a/ui/pubspec.yaml +++ b/ui/pubspec.yaml @@ -1,7 +1,7 @@ name: ui description: "Calf UI App." publish_to: 'none' -version: 0.6.0+1 +version: 0.7.0+1 environment: sdk: ^3.12.2 From a181d8389e878276fee0c144880d80e1aa407672 Mon Sep 17 00:00:00 2001 From: Eneko Galan Date: Wed, 8 Jul 2026 02:41:51 +0200 Subject: [PATCH 2/4] chore: update CHANGELOG, Makefile, and entitlements for macOS release - Updated CHANGELOG to document changes for version 0.7.0, including fixes and enhancements. - Modified Makefile to include new build targets for macOS, Linux, and Windows. - Removed unnecessary entitlements from macOS release configuration to comply with app sandboxing requirements. - Refactored container parsing logic to unify handling of labels and improve error handling in the backend. - Enhanced UI logic to ensure proper state management during daemon startup. --- .cursor/rules/calf.mdc | 20 ++-- CHANGELOG.md | 10 +- CLAUDE.md | 19 ++-- Makefile | 13 ++- backend/api.test | Bin 10613520 -> 0 bytes backend/buildstore.test | Bin 4058800 -> 0 bytes backend/internal/runtime/lima.go | 4 +- backend/internal/runtime/nerdctl.go | 147 +++++++++++---------------- backend/runtime.test | Bin 4097184 -> 0 bytes ui/lib/main.dart | 13 ++- ui/macos/Runner/Release.entitlements | 6 +- 11 files changed, 114 insertions(+), 118 deletions(-) delete mode 100755 backend/api.test delete mode 100755 backend/buildstore.test delete mode 100755 backend/runtime.test diff --git a/.cursor/rules/calf.mdc b/.cursor/rules/calf.mdc index ce70d78..3b57e5c 100644 --- a/.cursor/rules/calf.mdc +++ b/.cursor/rules/calf.mdc @@ -144,7 +144,7 @@ calf/ │ └── .dockerignore ├── scripts/verify-docker-cli.sh Verifies `docker` CLI works against Calf's socket ├── .github/workflows/ci.yml CI: backend (vet/test/build) + ui (analyze/test/build) jobs, macos-latest -├── Makefile dev-backend / dev-ui / ui / clean / verify-docker-cli targets +├── Makefile dev-backend / dev-ui-macos / dev-ui-linux / dev-ui-windows / ui-macos / ui-linux / ui-windows / clean / verify-docker-cli / release / release-macos / release-linux / release-windows targets ├── README.md Project pitch + quick start ├── DEVELOPMENT.md Dev setup, config file example, Docker Desktop migration walkthrough ├── ROADMAP.md Phased plan, architecture diagram, non-goals, competitor comparison @@ -252,10 +252,18 @@ Simple JSON files under `~/.config/calf/ui/.json` (via `path_provider`'s a ``` make dev-backend # cd backend && CGO_ENABLED=0 go run ./cmd/calf -make dev-ui # flutter run -d macos -make ui # flutter build macos (alias: make build) -make clean # flutter clean -make verify-docker-cli # runs scripts/verify-docker-cli.sh +make dev-ui-macos # flutter run -d macos +make dev-ui-linux # flutter run -d linux +make dev-ui-windows # flutter run -d windows +make ui-macos # flutter build macos +make ui-linux # flutter build linux +make ui-windows # flutter build windows +make release # build Go daemon + Flutter app for all platforms +make release-macos # build Go daemon + macOS .app +make release-linux # build Go daemon + Linux bundle +make release-windows # build Go daemon + Windows bundle +make clean # flutter clean +make verify-docker-cli # runs scripts/verify-docker-cli.sh ``` CI (`.github/workflows/ci.yml`, both jobs on `macos-latest`): @@ -284,4 +292,4 @@ CI (`.github/workflows/ci.yml`, both jobs on `macos-latest`): ## Before making changes - Check `ROADMAP.md` before adding scope — respect the explicit non-goals. -- Run `make dev-backend` / `make dev-ui` to exercise changes locally. +- Run `make dev-backend` / `make dev-ui-macos` (or `make dev-ui-linux` / `make dev-ui-windows`) to exercise changes locally. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5406bdc..c5595d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - ## [0.7.0] - 2026-07-08 ### Added @@ -25,6 +23,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - UI port opening now uses the platform URL opener on all OS (open/xdg-open/rundll32) - Daemon binary discovery works next to the Flutter executable on all platforms - Socket dial failure during startup logged at Debug level instead of Warn when socket does not yet exist +- macOS release entitlements: removed `com.apple.security.cs.allow-unsigned-executable-memory` and `com.apple.security.cs.disable-library-validation`. App sandbox remains off because the backend must execute external `limactl`/`nerdctl`/`docker` binaries. + +### Fixed + +- Container list parsing now uses a single parser that handles both map-shaped and comma-separated labels. +- Lima streaming log and interactive exec commands consistently run through the `sudo docker` wrapper. +- macOS release build now produces a universal `calf-daemon` (x86_64 + arm64) and signs the helper without its own entitlements so the app can launch the backend successfully. +- UI startup spinner clears transient daemon errors once the runtime reaches the running state and no longer pretends the app is ready after the timeout expires. ## [0.6.0] - 2026-07-06 diff --git a/CLAUDE.md b/CLAUDE.md index 19092cf..1aeccf5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -144,7 +144,7 @@ calf/ │ └── .dockerignore ├── scripts/verify-docker-cli.sh Verifies `docker` CLI works against Calf's socket ├── .github/workflows/ci.yml CI: backend (vet/test/build) + ui (analyze/test/build) jobs, macos-latest -├── Makefile dev-backend / dev-ui / ui / clean / verify-docker-cli / release / ui-linux / ui-windows / release-linux / release-windows targets +├── Makefile dev-backend / dev-ui-macos / dev-ui-linux / dev-ui-windows / ui-macos / ui-linux / ui-windows / clean / verify-docker-cli / release / release-macos / release-linux / release-windows targets ├── README.md Project pitch + quick start ├── DEVELOPMENT.md Dev setup, config file example, Docker Desktop migration walkthrough ├── ROADMAP.md Phased plan, architecture diagram, non-goals, competitor comparison @@ -252,11 +252,18 @@ Simple JSON files under `~/.config/calf/ui/.json` (via `path_provider`'s a ``` make dev-backend # cd backend && CGO_ENABLED=0 go run ./cmd/calf -make dev-ui # flutter run -d macos -make ui # flutter build macos (alias: make build) -make release # build Go daemon + Flutter .app and bundle together -make clean # flutter clean -make verify-docker-cli # runs scripts/verify-docker-cli.sh +make dev-ui-macos # flutter run -d macos +make dev-ui-linux # flutter run -d linux +make dev-ui-windows # flutter run -d windows +make ui-macos # flutter build macos +make ui-linux # flutter build linux +make ui-windows # flutter build windows +make release # build Go daemon + Flutter app for all platforms +make release-macos # build Go daemon + macOS .app +make release-linux # build Go daemon + Linux bundle +make release-windows # build Go daemon + Windows bundle +make clean # flutter clean +make verify-docker-cli # runs scripts/verify-docker-cli.sh ``` CI (`.github/workflows/ci.yml`, both jobs on `macos-latest`): diff --git a/Makefile b/Makefile index 364770f..f5d1094 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .PHONY: help ui-macos clean dev dev-backend dev-ui-macos verify-docker-cli release-macos \ - ui-linux ui-windows dev-ui-linux dev-ui-windows release-linux release-windows release ui + ui-linux ui-windows dev-ui-linux dev-ui-windows release-linux release-windows release help: @echo "Calf — common commands" @@ -15,7 +15,7 @@ help: @echo " make ui-linux build Linux app" @echo " make ui-windows build Windows app" - @echo "" + @echo "" @echo " make release-macos build Go daemon + macOS app" @echo " make release-linux build Go daemon + Linux app" @echo " make release-windows build Go daemon + Windows app" @@ -37,11 +37,16 @@ ui-windows: release: release-macos release-linux release-windows release-macos: - cd backend && CGO_ENABLED=0 go build -o calf-daemon ./cmd/calf + cd backend && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o calf-daemon-amd64 ./cmd/calf + cd backend && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o calf-daemon-arm64 ./cmd/calf + lipo -create -output backend/calf-daemon backend/calf-daemon-amd64 backend/calf-daemon-arm64 + rm backend/calf-daemon-amd64 backend/calf-daemon-arm64 cd ui && flutter build macos cp backend/calf-daemon ui/build/macos/Build/Products/Release/Calf.app/Contents/MacOS/calf-daemon rm backend/calf-daemon - codesign --force --deep --sign - --entitlements ui/macos/Runner/Release.entitlements ui/build/macos/Build/Products/Release/Calf.app + codesign --force --sign - --identifier com.enegalan.calf.daemon ui/build/macos/Build/Products/Release/Calf.app/Contents/MacOS/calf-daemon + codesign --force --sign - --entitlements ui/macos/Runner/Release.entitlements ui/build/macos/Build/Products/Release/Calf.app + codesign --verify --deep --strict ui/build/macos/Build/Products/Release/Calf.app release-linux: ui-linux cd backend && CGO_ENABLED=0 go build -o calf-daemon ./cmd/calf diff --git a/backend/api.test b/backend/api.test deleted file mode 100755 index 652af862c7c6623df4536283bb356b007eebd114..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10613520 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