Skip to content

Commit 6a67453

Browse files
committed
Add ARM-compatible image handling
1 parent 0dab2c5 commit 6a67453

File tree

5 files changed

+177
-4
lines changed

5 files changed

+177
-4
lines changed

.github/workflows/docker.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [main]
66
paths:
77
- 'containers/**'
8+
- '.github/workflows/docker.yml'
89
workflow_dispatch:
910

1011
env:
@@ -30,11 +31,26 @@ jobs:
3031
- dockerfile: containers/Dockerfile-ts
3132
image: sanity-ts
3233
tag: latest
34+
- dockerfile: containers/Dockerfile-kotlin
35+
image: sanity-kotlin
36+
tag: latest
37+
- dockerfile: containers/Dockerfile-dart
38+
image: sanity-dart
39+
tag: latest
40+
- dockerfile: containers/Dockerfile-zig
41+
image: sanity-zig
42+
tag: latest
3343

3444
steps:
3545
- name: Checkout repository
3646
uses: actions/checkout@v6
3747

48+
- name: Set up QEMU
49+
uses: docker/setup-qemu-action@v3
50+
51+
- name: Set up Docker Buildx
52+
uses: docker/setup-buildx-action@v3
53+
3854
- name: Log in to Container Registry
3955
uses: docker/login-action@v3
4056
with:
@@ -53,6 +69,7 @@ jobs:
5369
with:
5470
context: .
5571
file: ${{ matrix.dockerfile }}
72+
platforms: linux/amd64,linux/arm64
5673
push: true
5774
tags: |
5875
${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.image }}:${{ matrix.tag }}

containers/Dockerfile-zig

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
FROM alpine:3.19
22

3+
ARG TARGETARCH
4+
35
# Install Zig 0.13.0
46
RUN apk add --no-cache wget xz && \
5-
wget -q https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz -O /tmp/zig.tar.xz && \
7+
case "${TARGETARCH}" in \
8+
amd64) ZIG_ARCH="x86_64" ;; \
9+
arm64) ZIG_ARCH="aarch64" ;; \
10+
*) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
11+
esac && \
12+
wget -q "https://ziglang.org/download/0.13.0/zig-linux-${ZIG_ARCH}-0.13.0.tar.xz" -O /tmp/zig.tar.xz && \
613
tar -xf /tmp/zig.tar.xz -C /opt && \
714
rm /tmp/zig.tar.xz && \
8-
ln -s /opt/zig-linux-x86_64-0.13.0/zig /usr/local/bin/zig
15+
ln -s "/opt/zig-linux-${ZIG_ARCH}-0.13.0/zig" /usr/local/bin/zig
916

1017
# Configure Zig cache location
1118
ENV ZIG_GLOBAL_CACHE_DIR=/tmp/.zig-cache

docs/ROAD-TO-V2-Overhaul.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ Multi-run directories use a `multi-run.json` config and `multi-run-state.json` f
8888

8989
Phase 5 (parallel runs with `--parallel-runs`) is deferred to a future release.
9090

91+
## What changed in v1.8.0-alpha.3 — ARM readiness
92+
93+
This pre-release focuses on making the harness reliable on `arm64` hosts while keeping behavior explicit when image architecture is mismatched.
94+
95+
- **Early image platform validation in the runner.** `EnsureImage` now inspects the local/pulled image platform and fails fast with a clear error if the image does not match the host architecture, instead of failing later at container create/run time.
96+
- **Zig container is now architecture-aware.** The Zig Dockerfile now selects the correct upstream tarball for `amd64` (`x86_64`) and `arm64` (`aarch64`) builds.
97+
- **Docker image publishing now includes both Linux architectures.** The Docker workflow now builds and pushes `linux/amd64` and `linux/arm64` images for all six runtime images (`go`, `rust`, `typescript`, `kotlin`, `dart`, `zig`) using Buildx + QEMU.
98+
99+
Operationally, this means ARM users get deterministic behavior:
100+
- either a matching image runs normally,
101+
- or the harness exits with an actionable platform mismatch message telling you to publish/build the needed architecture or override image config.
102+
91103
## Compatibility and comparing old runs
92104

93105
`1.7.x` is intentionally not identical to `v1.6.1` behavior. If you are comparing against historical leaderboard-era runs, use legacy mode:

internal/runner/docker.go

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,67 @@ func (d *DockerClient) EnsureImage(ctx context.Context, imageName string, autoPu
110110
}
111111

112112
if exists {
113-
return nil
113+
compatible, localPlatform, err := d.imageMatchesHostPlatform(ctx, imageName)
114+
if err != nil {
115+
return err
116+
}
117+
if compatible {
118+
return nil
119+
}
120+
121+
if !autoPull {
122+
return fmt.Errorf(
123+
"image %s is %s but host platform is %s and auto-pull is disabled",
124+
imageName,
125+
localPlatform,
126+
hostPlatformString(),
127+
)
128+
}
129+
130+
if err := d.PullImage(ctx, imageName); err != nil {
131+
return err
132+
}
133+
134+
compatible, localPlatform, err = d.imageMatchesHostPlatform(ctx, imageName)
135+
if err != nil {
136+
return err
137+
}
138+
if compatible {
139+
return nil
140+
}
141+
142+
return fmt.Errorf(
143+
"image %s is %s but host platform is %s; build or publish a %s image, or override this image in config",
144+
imageName,
145+
localPlatform,
146+
hostPlatformString(),
147+
hostPlatformString(),
148+
)
114149
}
115150

116151
if !autoPull {
117152
return fmt.Errorf("image %s not found locally and auto-pull is disabled", imageName)
118153
}
119154

120-
return d.PullImage(ctx, imageName)
155+
if err := d.PullImage(ctx, imageName); err != nil {
156+
return err
157+
}
158+
159+
compatible, localPlatform, err := d.imageMatchesHostPlatform(ctx, imageName)
160+
if err != nil {
161+
return err
162+
}
163+
if compatible {
164+
return nil
165+
}
166+
167+
return fmt.Errorf(
168+
"image %s resolved to %s but host platform is %s; build or publish a %s image, or override this image in config",
169+
imageName,
170+
localPlatform,
171+
hostPlatformString(),
172+
hostPlatformString(),
173+
)
121174
}
122175

123176
// ContainerConfig holds configuration for creating a container.
@@ -314,3 +367,23 @@ func hostPlatform() *ocispec.Platform {
314367
func hostPlatformString() string {
315368
return "linux/" + runtime.GOARCH
316369
}
370+
371+
func (d *DockerClient) imageMatchesHostPlatform(ctx context.Context, imageName string) (bool, string, error) {
372+
inspect, err := d.client.ImageInspect(ctx, imageName)
373+
if err != nil {
374+
return false, "", fmt.Errorf("inspecting image %s: %w", imageName, err)
375+
}
376+
377+
localPlatform := platformString(inspect.Os, inspect.Architecture)
378+
return localPlatform == hostPlatformString(), localPlatform, nil
379+
}
380+
381+
func platformString(osName, arch string) string {
382+
if osName == "" {
383+
osName = "unknown"
384+
}
385+
if arch == "" {
386+
arch = "unknown"
387+
}
388+
return osName + "/" + arch
389+
}

internal/runner/docker_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package runner
2+
3+
import (
4+
"runtime"
5+
"testing"
6+
)
7+
8+
func TestPlatformString(t *testing.T) {
9+
t.Parallel()
10+
11+
tests := []struct {
12+
name string
13+
os string
14+
arch string
15+
want string
16+
}{
17+
{
18+
name: "full platform",
19+
os: "linux",
20+
arch: "arm64",
21+
want: "linux/arm64",
22+
},
23+
{
24+
name: "missing os",
25+
os: "",
26+
arch: "amd64",
27+
want: "unknown/amd64",
28+
},
29+
{
30+
name: "missing arch",
31+
os: "linux",
32+
arch: "",
33+
want: "linux/unknown",
34+
},
35+
{
36+
name: "missing os and arch",
37+
os: "",
38+
arch: "",
39+
want: "unknown/unknown",
40+
},
41+
}
42+
43+
for _, tc := range tests {
44+
tc := tc
45+
t.Run(tc.name, func(t *testing.T) {
46+
t.Parallel()
47+
48+
got := platformString(tc.os, tc.arch)
49+
if got != tc.want {
50+
t.Fatalf("platformString(%q, %q) = %q, want %q", tc.os, tc.arch, got, tc.want)
51+
}
52+
})
53+
}
54+
}
55+
56+
func TestHostPlatformString(t *testing.T) {
57+
t.Parallel()
58+
59+
want := "linux/" + runtime.GOARCH
60+
got := hostPlatformString()
61+
if got != want {
62+
t.Fatalf("hostPlatformString() = %q, want %q", got, want)
63+
}
64+
}

0 commit comments

Comments
 (0)