Skip to content

Commit b211e9d

Browse files
authored
Merge pull request #4 from NexusGPU/codex/apple-container-backend
fix: apple container backend
2 parents 5f96eea + 43e377a commit b211e9d

17 files changed

Lines changed: 1060 additions & 215 deletions

cmd/ggo/studio/studio.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func NewStudioCmd() *cobra.Command {
4747
Supported platforms:
4848
- wsl: Windows Subsystem for Linux (Windows only)
4949
- colima: Colima container runtime (macOS/Linux)
50+
- apple-container: Apple Container (macOS 26+)
5051
- docker: Native Docker
5152
- k8s: Kubernetes (kind, minikube, etc.)
5253
- auto: Auto-detect best available platform
@@ -58,6 +59,9 @@ Examples:
5859
# Create with specific mode
5960
ggo studio create my-studio --mode wsl -s "https://..."
6061
62+
# Create with Apple Container (macOS 26+)
63+
ggo studio create my-studio --mode apple-container -s "https://..."
64+
6165
# Create with specific Colima profile
6266
ggo studio create my-studio --mode colima --colima-profile myprofile
6367
@@ -179,7 +183,7 @@ Examples:
179183
RunE: runCreate,
180184
}
181185

182-
cmd.Flags().StringVarP(&mode, "mode", "m", "", "Container/VM mode (wsl, colima, docker, k8s, auto)")
186+
cmd.Flags().StringVarP(&mode, "mode", "m", "", "Container/VM mode (wsl, colima, apple-container, docker, k8s, auto)")
183187
cmd.Flags().StringVarP(&image, "image", "i", "tensorfusion/studio-torch:latest", "Container image")
184188
cmd.Flags().StringVarP(&shareLink, "share-link", "s", "", "Share link or share code to remote vGPU worker (required)")
185189
cmd.Flags().StringVar(&serverURL, "server", api.GetDefaultBaseURL(), "Server URL for resolving share links")
@@ -828,8 +832,10 @@ func (r *backendsResult) RenderTUI(out *tui.Output) {
828832
out.Println()
829833
out.Println(styles.Subtitle.Render("Install one of the following:"))
830834
out.Println()
835+
out.Println(" • " + styles.Bold.Render("Apple Container (macOS 26+):") + " " + tui.URL("https://github.com/apple/container/releases"))
831836
out.Println(" • " + styles.Bold.Render("Docker:") + " " + tui.URL("https://docs.docker.com/get-docker/"))
832837
out.Println(" • " + styles.Bold.Render("Colima (macOS):") + " " + tui.Code("brew install colima"))
838+
out.Println(" • " + styles.Bold.Render("OrbStack (macOS):") + " " + tui.Code("brew install orbstack"))
833839
out.Println(" • " + styles.Bold.Render("WSL (Windows):") + " " + tui.URL("https://docs.microsoft.com/en-us/windows/wsl/install"))
834840
out.Println()
835841
return
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Apple Container Backend Design
2+
3+
**Date:** 2026-02-06
4+
5+
## Goal
6+
Enable `ggo studio create` to use Apple Container on macOS 26+ as a first-choice fallback when no Docker socket is available, while preserving existing Docker/Colima/OrbStack flows and providing clear install/upgrade guidance.
7+
8+
## Key Behaviors
9+
- Replace the `apple` mode with `apple-container` across CLI flags, help text, and internal mode constants.
10+
- On macOS 26+:
11+
- If no Docker socket is found, prefer Apple Container for auto mode.
12+
- If `--mode apple-container` is specified but Apple Container is missing, prompt install.
13+
- If no Docker socket is found and Apple Container is missing, prompt install.
14+
- On macOS < 26:
15+
- If `--mode apple-container` is specified, error with OS upgrade requirement.
16+
- If no runtime is available, recommend installing Colima (plus other options) and note Apple Container needs macOS 26.
17+
18+
## Backend Strategy
19+
- Implement Apple Container backend using the `container` CLI (not Docker).
20+
- Detect availability with `container system status` and presence of the `container` CLI.
21+
- Implement list/get/start/stop/exec/logs/delete via `container` subcommands.
22+
- Parse JSON from `container list --format json` / `container inspect` to build `Environment` objects.
23+
24+
## Runtime Detection
25+
- Add helper for macOS major version detection using `runtime` + `internal/platform` helper.
26+
- Add Docker socket discovery for common paths:
27+
- `DOCKER_HOST` (unix socket)
28+
- `/var/run/docker.sock`
29+
- `~/.colima/*/docker.sock`
30+
- `~/.orbstack/run/docker.sock`
31+
- Use socket presence to decide preference order on macOS 26+.
32+
33+
## User-Facing Messaging
34+
- Update CLI help strings and examples to include `apple-container`.
35+
- Provide install guidance for:
36+
- Apple Container: download signed pkg from GitHub releases.
37+
- Colima/OrbStack: `brew install` suggestions.
38+
39+
## Testing
40+
- Add ginkgo tests covering backend selection rules and macOS version gating.
41+
- Run `container` CLI end-to-end tests on a common arm64 image (e.g., `alpine:latest`), ensuring create/list/exec/logs/stop/delete flows work.
42+
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Apple Container Backend Implementation Plan
2+
3+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4+
5+
**Goal:** Add Apple Container (`container` CLI) support for `ggo studio create` on macOS 26+, with correct auto-selection, install/upgrade messaging, and updated CLI/docs/UI strings.
6+
7+
**Architecture:** Introduce platform helpers for macOS version and Docker socket detection, then update backend selection to prefer Apple Container only when no Docker socket is present on macOS 26+. Rebuild the Apple backend around the `container` CLI with JSON parsing for list/inspect, and ensure install/runtime hints are precise.
8+
9+
**Tech Stack:** Go, Cobra, Ginkgo/Gomega, Apple `container` CLI.
10+
11+
---
12+
13+
### Task 1: Add Ginkgo suite + failing tests for macOS selection rules
14+
15+
**Files:**
16+
- Create: `internal/studio/studio_suite_test.go`
17+
- Create: `internal/studio/manager_apple_ginkgo_test.go`
18+
19+
**Step 1: Write the failing test**
20+
```go
21+
var _ = Describe("Apple container selection", func() {
22+
It("prefers apple-container on macOS 26+ when no Docker socket is present", func() {
23+
// stub platform helpers to darwin/26/no-socket
24+
// register apple + docker backends (both available)
25+
// expect ModeAppleContainer for ModeAuto
26+
})
27+
28+
It("prefers docker/colima when Docker socket exists on macOS 26+", func() {
29+
// stub platform helpers to darwin/26/has-socket
30+
// register apple + docker backends (both available)
31+
// expect ModeDocker for ModeAuto
32+
})
33+
34+
It("rejects apple-container on macOS < 26 when explicitly requested", func() {
35+
// stub platform helpers to darwin/25
36+
// expect error mentioning macOS 26 upgrade
37+
})
38+
})
39+
```
40+
41+
**Step 2: Run test to verify it fails**
42+
Run: `go test ./internal/studio -run Apple`
43+
Expected: FAIL with missing helper stubs / selection logic not implemented
44+
45+
**Step 3: Write minimal implementation**
46+
Implement platform helper stubs and selection logic hooks (function vars) to satisfy the tests.
47+
48+
**Step 4: Run test to verify it passes**
49+
Run: `go test ./internal/studio -run Apple`
50+
Expected: PASS
51+
52+
**Step 5: Commit**
53+
```bash
54+
git add internal/studio/studio_suite_test.go internal/studio/manager_apple_ginkgo_test.go
55+
GIT_OPTIONAL_LOCKS=0 git -c core.hooksPath=/dev/null commit -m "test: add apple-container selection tests"
56+
```
57+
58+
### Task 2: Implement macOS version + Docker socket helpers and update Manager selection/hints
59+
60+
**Files:**
61+
- Create: `internal/platform/macos_version_darwin.go`
62+
- Create: `internal/platform/macos_version_other.go`
63+
- Create: `internal/platform/docker_socket.go`
64+
- Modify: `internal/studio/manager.go`
65+
66+
**Step 1: Write the failing test**
67+
Extend existing Ginkgo tests (Task 1) to assert explicit error messaging for unsupported macOS version and missing Apple Container install guidance.
68+
69+
**Step 2: Run test to verify it fails**
70+
Run: `go test ./internal/studio -run Apple`
71+
Expected: FAIL with error text mismatch or missing helpers
72+
73+
**Step 3: Write minimal implementation**
74+
- Add `platform.MacOSMajorVersion()` using `syscall.Sysctl("kern.osproductversion")` on darwin; return `0` elsewhere.
75+
- Add `platform.HasDockerSocket()` to scan `DOCKER_HOST`, `/var/run/docker.sock`, `~/.colima/*/docker.sock`, `~/.orbstack/run/docker.sock`.
76+
- Update `Manager.detectBestBackend` to:
77+
- On darwin 26+: prefer apple-container only when no Docker socket is found.
78+
- On darwin < 26: exclude apple-container from auto preference list.
79+
- Update `platformBackendHint` to include install guidance:
80+
- Apple Container: download pkg from GitHub releases.
81+
- Colima/OrbStack: `brew install`.
82+
- Docker: link to Docker Desktop.
83+
- When `--mode apple-container` is requested on macOS < 26, return a clear upgrade error.
84+
85+
**Step 4: Run test to verify it passes**
86+
Run: `go test ./internal/studio -run Apple`
87+
Expected: PASS
88+
89+
**Step 5: Commit**
90+
```bash
91+
git add internal/platform/macos_version_darwin.go internal/platform/macos_version_other.go internal/platform/docker_socket.go internal/studio/manager.go
92+
GIT_OPTIONAL_LOCKS=0 git -c core.hooksPath=/dev/null commit -m "feat: add macOS version/socket helpers and selection rules"
93+
```
94+
95+
### Task 3: Rebuild Apple backend to use `container` CLI + parsing helpers
96+
97+
**Files:**
98+
- Modify: `internal/studio/backend_apple.go`
99+
- Create: `internal/studio/apple_container_parse_test.go`
100+
101+
**Step 1: Write the failing test**
102+
```go
103+
var _ = Describe("Apple container parsing", func() {
104+
It("maps container list JSON to Environment and SSH port", func() {
105+
// Provide sample JSON from `container list --format json`
106+
// Expect label filtering, SSH port extraction, GPU_WORKER_URL parsing
107+
})
108+
})
109+
```
110+
111+
**Step 2: Run test to verify it fails**
112+
Run: `go test ./internal/studio -run Apple`
113+
Expected: FAIL (parsing helpers not implemented)
114+
115+
**Step 3: Write minimal implementation**
116+
- Replace docker CLI usage with `container` CLI subcommands:
117+
- `container system status` for availability
118+
- `container system start` in `EnsureRunning`
119+
- `container run --detach` for create
120+
- `container list --format json` for list
121+
- `container inspect` for get
122+
- `container exec`, `container logs`, `container start`, `container stop`, `container delete --force`
123+
- Add JSON parsing helpers for list/inspect output.
124+
- Normalize memory suffixes for `container` CLI (`Gi`->`G`, `Mi`->`M`).
125+
- Use labels `ggo.managed=true`, `ggo.name`, `ggo.mode=apple-container`.
126+
127+
**Step 4: Run test to verify it passes**
128+
Run: `go test ./internal/studio -run Apple`
129+
Expected: PASS
130+
131+
**Step 5: Commit**
132+
```bash
133+
git add internal/studio/backend_apple.go internal/studio/apple_container_parse_test.go
134+
GIT_OPTIONAL_LOCKS=0 git -c core.hooksPath=/dev/null commit -m "feat: implement apple-container backend via container CLI"
135+
```
136+
137+
### Task 4: Update CLI/Docs/UI strings for `apple-container`
138+
139+
**Files:**
140+
- Modify: `internal/studio/types.go`
141+
- Modify: `cmd/ggo/studio/studio.go`
142+
- Modify: `docs/studio-guide.md`
143+
- Modify: `vscode-extension/src/views/createStudioPanel.ts`
144+
145+
**Step 1: Write the failing test**
146+
(Documentation/UI change; no automated test required)
147+
148+
**Step 2: Run test to verify it fails**
149+
Skip
150+
151+
**Step 3: Write minimal implementation**
152+
- Replace `apple` mode string with `apple-container`.
153+
- Update CLI help, examples, and backend listing output to include install guidance.
154+
- Update Studio guide table and VS Code UI mode labels.
155+
156+
**Step 4: Run test to verify it passes**
157+
Run: `go test ./cmd/ggo/studio ./internal/studio`
158+
Expected: PASS
159+
160+
**Step 5: Commit**
161+
```bash
162+
git add internal/studio/types.go cmd/ggo/studio/studio.go docs/studio-guide.md vscode-extension/src/views/createStudioPanel.ts
163+
GIT_OPTIONAL_LOCKS=0 git -c core.hooksPath=/dev/null commit -m "docs: update apple-container mode strings"
164+
```
165+
166+
### Task 5: Full functional Apple Container CLI test (manual)
167+
168+
**Files:**
169+
- None (manual verification)
170+
171+
**Step 1: Run container services**
172+
Run: `container system start`
173+
Expected: services start successfully
174+
175+
**Step 2: Run a common arm64 image**
176+
Run: `container run --name ggo-apple-test --detach --rm -p 18022:22/tcp alpine:latest sleep 600`
177+
Expected: container ID printed, `container list` shows it running
178+
179+
**Step 3: Exec/logs/stop/delete**
180+
Run:
181+
- `container exec ggo-apple-test sh -c "echo ok"`
182+
- `container logs ggo-apple-test`
183+
- `container stop ggo-apple-test`
184+
Expected: commands succeed
185+
186+
**Step 4: Record results**
187+
Note any failures or deviations.
188+
189+
---
190+
191+
**Global verification after each Go change:**
192+
- Run: `golangci-lint run --fix`
193+
- Run: `go test ./...` (at least once before finalization)
194+

docs/studio-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ ggo studio create my-studio -s abc123 --mode docker
9999
| `docker` | 原生 Docker | 所有 |
100100
| `colima` | Colima 容器运行时 | macOS/Linux |
101101
| `wsl` | Windows Subsystem for Linux | Windows |
102-
| `apple` | Apple Virtualization Framework | macOS |
102+
| `apple-container` | Apple Container(macOS 26+) | macOS |
103103

104104
### 卷挂载(Volume Mounts)
105105

internal/platform/docker_socket.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package platform
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
)
8+
9+
// HasDockerSocket checks common Docker socket locations for macOS runtimes.
10+
func HasDockerSocket() bool {
11+
if socketPath := dockerHostSocketPath(os.Getenv("DOCKER_HOST")); socketPath != "" {
12+
if fileExists(socketPath) {
13+
return true
14+
}
15+
}
16+
17+
if fileExists("/var/run/docker.sock") {
18+
return true
19+
}
20+
21+
homeDir, err := os.UserHomeDir()
22+
if err != nil {
23+
return false
24+
}
25+
26+
colimaGlob := filepath.Join(homeDir, ".colima", "*", "docker.sock")
27+
if matches, err := filepath.Glob(colimaGlob); err == nil {
28+
for _, match := range matches {
29+
if fileExists(match) {
30+
return true
31+
}
32+
}
33+
}
34+
35+
orbstackSock := filepath.Join(homeDir, ".orbstack", "run", "docker.sock")
36+
return fileExists(orbstackSock)
37+
}
38+
39+
func dockerHostSocketPath(dockerHost string) string {
40+
if dockerHost == "" {
41+
return ""
42+
}
43+
if !strings.HasPrefix(dockerHost, "unix://") {
44+
return ""
45+
}
46+
return strings.TrimPrefix(dockerHost, "unix://")
47+
}
48+
49+
func fileExists(path string) bool {
50+
if path == "" {
51+
return false
52+
}
53+
_, err := os.Stat(path)
54+
return err == nil
55+
}
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 platform
4+
5+
import (
6+
"strconv"
7+
"strings"
8+
"syscall"
9+
)
10+
11+
// MacOSMajorVersion returns the macOS major version (e.g., 26).
12+
// Returns 0 when the version cannot be determined.
13+
func MacOSMajorVersion() int {
14+
version, err := syscall.Sysctl("kern.osproductversion")
15+
if err != nil {
16+
return 0
17+
}
18+
version = strings.TrimSpace(version)
19+
if version == "" {
20+
return 0
21+
}
22+
parts := strings.Split(version, ".")
23+
if len(parts) == 0 {
24+
return 0
25+
}
26+
major, err := strconv.Atoi(parts[0])
27+
if err != nil {
28+
return 0
29+
}
30+
return major
31+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build !darwin
2+
3+
package platform
4+
5+
// MacOSMajorVersion returns 0 on non-macOS platforms.
6+
func MacOSMajorVersion() int {
7+
return 0
8+
}

0 commit comments

Comments
 (0)