Skip to content

Commit 380f563

Browse files
committed
feat: match herm container setup (version-tagged image, same-path mount, attachments/cache)
- Image derived from binary version: graycode/hawk:<version> (like herm's aduermael/herm:<tag>) - Mount project at same path (not /workspace) for path consistency - Add /attachments (read-only) and /cache mounts like herm - Auto-pull image if not available locally, fall back to ubuntu:24.04 - Add container/Dockerfile with dev tools (git, curl, ripgrep, node, python) - ContainerImageTag set via ldflags at build time
1 parent 2e92940 commit 380f563

5 files changed

Lines changed: 80 additions & 5 deletions

File tree

cmd/container_boot.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"context"
55
"fmt"
6+
"os/exec"
67
"time"
78

89
tea "github.com/charmbracelet/bubbletea"
@@ -44,6 +45,20 @@ func bootContainerCmd(projectDir string) tea.Cmd {
4445
}
4546
}
4647

48+
// Ensure image exists locally, pull if needed (like herm)
49+
image := cs.Image()
50+
pullCtx, pullCancel := context.WithTimeout(context.Background(), 300*time.Second)
51+
defer pullCancel()
52+
checkCmd := exec.CommandContext(pullCtx, "docker", "image", "inspect", image)
53+
if checkCmd.Run() != nil {
54+
// Image not available locally — pull it
55+
pullCmd := exec.CommandContext(pullCtx, "docker", "pull", image)
56+
if err := pullCmd.Run(); err != nil {
57+
// Fall back to ubuntu:24.04 if custom image can't be pulled
58+
cs.SetImage("ubuntu:24.04")
59+
}
60+
}
61+
4762
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
4863
defer cancel()
4964

container/Dockerfile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
FROM ubuntu:24.04
2+
3+
RUN apt-get update && apt-get install -y --no-install-recommends \
4+
git \
5+
curl \
6+
wget \
7+
jq \
8+
tree \
9+
ripgrep \
10+
fd-find \
11+
make \
12+
gcc \
13+
python3 \
14+
python3-pip \
15+
nodejs \
16+
npm \
17+
ca-certificates \
18+
openssh-client \
19+
&& rm -rf /var/lib/apt/lists/*
20+
21+
# Create symlinks for common tool names
22+
RUN ln -sf /usr/bin/fdfind /usr/bin/fd
23+
24+
# Set reasonable defaults
25+
ENV TERM=xterm-256color
26+
ENV LANG=C.UTF-8
27+
28+
WORKDIR /workspace

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66

77
"github.com/GrayCodeAI/hawk/cmd"
8+
"github.com/GrayCodeAI/hawk/sandbox"
89
)
910

1011
// Version is set at build time via ldflags.
@@ -17,6 +18,7 @@ var BuildDate = "unknown"
1718
func main() {
1819
cmd.SetVersion(Version)
1920
cmd.SetBuildDate(BuildDate)
21+
sandbox.ContainerImageTag = Version
2022
if err := cmd.Execute(); err != nil {
2123
fmt.Fprintln(os.Stderr, err)
2224
os.Exit(1)

sandbox/container.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,19 @@ func (c *ContainerSandbox) Start(ctx context.Context) error {
5757
// Remove any stale container with the same name from a previous session
5858
exec.CommandContext(ctx, "docker", "rm", "-f", name).Run()
5959

60+
// Create attachments and cache dirs (like herm)
61+
attachDir := filepath.Join(c.projectDir, ".hawk", "attachments")
62+
cacheDir := filepath.Join(c.projectDir, ".hawk", "cache")
63+
os.MkdirAll(attachDir, 0755)
64+
os.MkdirAll(cacheDir, 0755)
65+
6066
args := []string{
6167
"run", "-d", "--rm",
6268
"--name", name,
63-
"-v", c.projectDir + ":/workspace",
64-
"-w", "/workspace",
69+
"-v", c.projectDir + ":" + c.projectDir, // mount at same path (like herm)
70+
"-v", attachDir + ":/attachments:ro",
71+
"-v", cacheDir + ":/cache",
72+
"-w", c.projectDir,
6573
c.image,
6674
"sleep", "infinity",
6775
}
@@ -130,6 +138,20 @@ func (c *ContainerSandbox) ContainerID() string {
130138
return c.containerID
131139
}
132140

141+
// Image returns the current image name.
142+
func (c *ContainerSandbox) Image() string {
143+
c.mu.Lock()
144+
defer c.mu.Unlock()
145+
return c.image
146+
}
147+
148+
// SetImage updates the image to use for the next Start call.
149+
func (c *ContainerSandbox) SetImage(img string) {
150+
c.mu.Lock()
151+
defer c.mu.Unlock()
152+
c.image = img
153+
}
154+
133155
// BuildFromDockerfile builds a new image from a Dockerfile in the project.
134156
// Returns the image tag that can be used for subsequent Start calls.
135157
func (c *ContainerSandbox) BuildFromDockerfile(ctx context.Context, dockerfile string) (string, error) {
@@ -168,6 +190,13 @@ func (c *ContainerSandbox) containerName() string {
168190
return fmt.Sprintf("hawk-%s-%x", base, hash[:4])
169191
}
170192

193+
// ContainerImageTag is set at build time via ldflags. Falls back to "latest".
194+
var ContainerImageTag = "latest"
195+
196+
func defaultHawkImage() string {
197+
return "graycode/hawk:" + ContainerImageTag
198+
}
199+
171200
func resolveImage(projectDir string) string {
172201
dfPath := filepath.Join(projectDir, ".hawk", "Dockerfile")
173202
if _, err := os.Stat(dfPath); err == nil {
@@ -177,5 +206,5 @@ func resolveImage(projectDir string) string {
177206
return fmt.Sprintf("hawk-sandbox:%x", hash[:6])
178207
}
179208
}
180-
return "ubuntu:24.04"
209+
return defaultHawkImage()
181210
}

sandbox/container_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ func TestContainerSandbox_ContainerName(t *testing.T) {
3636

3737
func TestResolveImage_Default(t *testing.T) {
3838
img := resolveImage(t.TempDir())
39-
if img != "ubuntu:24.04" {
40-
t.Fatalf("expected default image ubuntu:24.04, got %s", img)
39+
expected := defaultHawkImage()
40+
if img != expected {
41+
t.Fatalf("expected default image %s, got %s", expected, img)
4142
}
4243
}
4344

0 commit comments

Comments
 (0)