applecontainer: native runtime backend#62
Conversation
Fourth PR of M6. Replaces the RunContainer / StartContainer /
StopContainer / RemoveContainer stubs with real implementations.
Bridge (Swift):
- lifecycle.swift: ac_run wraps ContainerClient.create with a full
ContainerConfiguration built from a RunSpec JSON payload, kernel
fetched via ClientKernel.getDefaultKernel. ac_start does
bootstrap + process.start in detached mode with idempotent
short-circuit when the snapshot already reports running. ac_stop
wraps ContainerClient.stop with a Go-supplied grace period.
ac_delete wraps ContainerClient.delete with the force flag.
- ac_bridge.h: documented per the PR-B style guide. Notes Apple's
60s sync timeout for these calls (cold first-run needs to fetch
the kernel and init image).
- Mount mapping: bind → Filesystem.virtiofs (matches the design §11.1
finding — virtiofs is identity-permissive). Tmpfs → Filesystem.tmpfs.
Named volumes treated as virtiofs binds for PR-C; real volume
lifecycle is a later PR.
cgo shim (Go side):
- shim.h / shim.c: four new function-pointer slots and wrappers.
Runtime methods (lifecycle_darwin_arm64.go):
- runSpecToWire marshals runtime.RunSpec into the bridge JSON shape.
Drops RunArgs, Privileged, SecurityOpt (intentionally absent on
the wire type so a future caller depending on them fails the
build instead of silently losing the field). Documented in
design §8.
- RunContainer: marshal → ac_run → decode → returns
runtime.Container in Created state.
- StartContainer: idempotent via bridge.
- StopContainer: clamps Timeout to int32 seconds with overflow guard.
- RemoveContainer: RemoveVolumes silently dropped (volume support
deferred).
- mapRunErr / mapLifecycleErr translate Apple's `notFound`
errors into the runtime.{Container,Image}NotFoundError contract.
Inspect path tightened (inspect_darwin_arm64.go):
- containerMount.Type now decodes Apple's enum-with-associated-values
shape (`{"virtiofs":{}}` etc) via a custom mountTypeWire decoder.
Surfaces as runtime.MountBind / MountTmpfs / MountVolume so callers
reason about mounts in Docker-style terms.
- ReadOnly is now derived from the `options` array containing "ro"
(Apple's wire shape, replacing the synthetic boolean field that
doesn't exist in ContainerSnapshot).
Tests (lifecycle_darwin_arm64_test.go):
- TestLifecycle_EndToEnd: Run → Start → Inspect(running) → Start
(idempotent) → Stop → Inspect(stopped) → Remove →
Inspect(notfound). 5s state-poll loop absorbs the apiserver's
async status transitions.
- TestRunContainer_MissingImage: asserts ImageNotFoundError contract
when the caller hasn't pulled the image first.
- TestRunContainer_BindMount: creates a container with a virtiofs
bind and verifies the inspect path round-trips the mount; absorbs
Apple's trailing-slash path normalization via TrimRight.
Test plan:
- `make bridge && go test ./runtime/applecontainer/...` — full
package green
- `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build ./runtime/applecontainer/...`
— stub still compiles
- `go vet ./...` clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (PR-D)
Fifth PR of M6. Replaces the ExecContainer stub with a real
implementation that wires Go ctx → SIGTERM → in-VM process,
answering the design §11.3 open question (cancellation does
propagate end-to-end).
Bridge (Swift, exec.swift):
- ExecRegistry: NSLock-guarded handle table mapping uint64 → (process,
retained FileHandles). The retained FileHandles aren't strictly
required (closeOnDealloc:false) but keep ARC's behavior boring
during the in-flight exec.
- ac_exec_start: builds a ProcessConfiguration from the container's
init process (inherits PATH etc) overridden by caller cmd/env/cwd/
user/tty. Wraps caller-supplied fds in FileHandles for stdio[0..2].
Calls createProcess + start; registers and returns a handle.
- ac_exec_wait: blocking wait via Swift Task; returns exitCode.
- ac_exec_signal: process.kill(signal) — the cancellation primitive.
- ac_exec_release: clears the registry slot.
cgo shim (Go side):
- shim.h / shim.c: four new dlsym slots + wrappers.
Runtime method (exec_darwin_arm64.go):
- execPipes: three os.Pipe pairs (stdin optional; stdout always;
stderr suppressed in TTY mode per Apple). Apiserver-facing ends
passed to the bridge as fds; Go closes its local copies after
XPC dup's them in ac_exec_start.
- ExecContainer:
* Marshals ExecOptions into execOptsJSON (drops Tty/User/etc that
don't have an Apple analog by being absent from the wire type).
* Spawns goroutines for stdin write / stdout read / stderr read.
* Spawns ctx-watcher goroutine; on ctx.Done() it calls
ac_exec_signal(SIGTERM).
* Calls ac_exec_wait; closes Go-facing read ends to release the
copy goroutines; drains them.
* Returns ctx.Err() if we triggered the cancel path, else the real
exit code.
- isBrokenPipe: swallows EPIPE / "file already closed" from copy
goroutines so a process exiting mid-write doesn't surface a
spurious error.
Tests (exec_darwin_arm64_test.go):
- TestExec_CaptureStdoutAndExit: echo + non-zero exit; verifies
stdout/stderr capture + exit code propagation.
- TestExec_StdinRoundTrip: `cat` round-trip — bidirectional pipes work.
- TestExec_StreamingWriters: io.Writer path (what DAP readiness
probes use).
- TestExec_ContextCancelKillsProcess: spawns `sleep 60`, cancels
after 500ms, asserts return within 15s. Locally completes in ~2.5s
— the design §11.3 cancellation chain works.
- TestExec_EnvAndCwd: per-call env + workingDir overrides land in
the in-VM process.
Test plan:
- `make bridge && go test ./runtime/applecontainer/...` — full
package green
- `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build` — stub OK
- `go vet ./...` clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sixth PR of M6. Streams the container's stdio log to a Go io.Writer with follow / non-follow modes. Boot log (index 1 from Apple's logs API) is not surfaced — out of runtime.Runtime contract. Bridge (Swift, logs.swift): - ac_logs_open: calls ContainerClient.logs(id), dup's the stdio FileHandle's fd (so Apple's FileHandle deinit can't yank the fd out from under the Go reader), returns the dup'd fd via JSON. cgo shim: one new dlsym slot + wrapper, fail-cleanup branch updated. Runtime method (logs_darwin_arm64.go): - ContainerLogs: opens via the bridge, wraps the fd in os.NewFile, copies to the caller's writer. Ctx-watcher goroutine closes the file on ctx.Done() so a blocked Read unblocks immediately. In follow mode, polls on EOF (200ms cadence) until ctx fires. Why poll vs inotify: the log file is a regular file on the host; inotify isn't macOS-native and Apple's logs API doesn't expose an event stream. A bounded sleep keeps the Go runtime cheap and ctx-cancel responsive. Tests: - TestLogs_NonFollow: emits two known lines, asserts both arrive. - TestLogs_FollowBlocksUntilCancel: emits markers on a 1s loop, waits for first marker, cancels ctx, asserts return < 5s with context.Canceled. Test plan: - `make bridge && go test ./runtime/applecontainer/...` — all tests pass (5 envelope + 6 inspect + 3 lifecycle + 5 exec + 2 logs + 2 ping = 23) - `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build` — stub OK Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seventh PR of M6. Synchronous pull via ClientImage.pull; coarse BuildEvent emission (log + completed). Progress streaming deferred to a future PR — engine treats coarse events as acceptable for v1. Bridge (Swift, pull.swift): - ac_pull_image: ClientImage.pull(reference:platform:.current). Returns reference + digest. 30-minute bridge timeout covers cold multi-GB pulls; in-flight cancellation isn't yet wired (Apple's pull API doesn't expose a cancellation token). cgo shim: one new dlsym slot + wrapper. Runtime method (pull_darwin_arm64.go): - PullImage: ctx check at entry; emit "pulling" BuildEventLog; call bridge; emit BuildEventCompleted with the digest; return ImageRef with ID=digest, Tags=[reference]. - emitBuildEvent: non-blocking send (slow consumer doesn't gate the pull); nil channel is a no-op. Tests: - TestPullImage_SmallPublic: pulls alpine, asserts ImageRef + at least one BuildEventCompleted with a digest. - TestPullImage_NoSuchImage: typed-error assertion is best-effort — Docker Hub returns 401 (auth) for non-existent images, not 404, so the test logs the actual error type and only fails if the string looks like a missing-image error that wasn't translated. Test plan: - `make bridge && go test ./runtime/applecontainer/...` — full package green (25 tests) - Cross-build clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eighth PR of M6. Lands the builder-liveness probe + typed
*runtime.BuilderUnavailableError contract; defers the full BuildKit
gRPC integration to a follow-up PR.
Scope rationale: Apple's build path requires (a) dialing the
"buildkit" container over vsock, (b) auto-starting it if absent
via BuilderStart.start, (c) constructing Builder(socket:) with a
SwiftNIO MultiThreadedEventLoopGroup, (d) building a 17-field
Builder.BuildConfig from our RunSpec, (e) translating BuildKit's
streaming-progress output into our BuildEvent channel. Each step is
nontrivial; doing them in a single PR alongside everything else in
M6 would either ship sloppy code or block the rest of the stack
indefinitely. The value-bearing slice we can land cleanly today is
the typed error contract — callers (engine, DAP) can now distinguish
"this backend doesn't yet support Dockerfile builds" from "this
backend permanently rejects builds" and surface the right hint to
users.
New error type (runtime/errors.go):
- BuilderUnavailableError{Hint, Err}: separate from
DaemonUnavailableError because the build component (Apple's
buildkit VM, Docker's BuildKit daemon) is typically a different
process that can be started/stopped independently of the main
container daemon.
Bridge (Swift, build.swift):
- ac_build_probe: ContainerClient.get(id: "buildkit") + status
check. The real CLI does a vsock dial + Builder.info() ping
round-trip; this stub stops short of that because Builder
requires SwiftNIO + nontrivial wiring we'd just throw away once
the full implementation lands.
cgo shim: one new dlsym slot + wrapper.
Runtime method (build_darwin_arm64.go):
- BuildImage: probe builder via bridge; if not running, return
typed BuilderUnavailableError with the "container builder start"
hint; if running, return a plain error explaining the build path
isn't implemented yet on this backend and pointing callers at
`image:` source devcontainers as a workaround.
Tests:
- TestBuildImage_PartialContract: accepts either outcome
(typed unavail or plain not-implemented) and asserts the relevant
diagnostic strings; documents what the next PR must change about
this contract.
Test plan:
- `make bridge && go test ./runtime/applecontainer/...` — full
package green
- Cross-build clean
Follow-up PR (PR-G2): wire ContainerBuild.Builder over vsock,
construct BuildConfig from RunSpec, translate BuildKit progress
events into runtime.BuildEvent, add a real end-to-end Dockerfile
build smoke test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final PR of M6. Validates the apple-container backend works end-to- end through the engine. New tests (test/integration/applecontainer_image_source_test.go, build tag: integration && darwin && arm64): - TestAppleContainer_ImageSource_FullLifecycle: mirrors the M2 docker test (image_source_test.go) against the apple-container backend. eng.Up an `image:` devcontainer with containerEnv, eng.Exec a /bin/sh -c command, assert stdout + containerEnv injection landed, eng.Down with Remove. Locally ~7s — covers PullImage + RunContainer + StartContainer + InspectContainer + ExecContainer + StopContainer + RemoveContainer through the engine's high-level API. - TestAppleContainer_BuildSource_DocumentsLimitation: contract test for PR-G's partial state. A build-source devcontainer must fail with either BuilderUnavailableError (typed) or the "not yet implemented" error. Once PR-G2 lands the real build path, this test should be removed or flipped to assert success. Out of scope for PR-H (mapped to follow-up PRs): - Dockerfile builds and feature install (needs PR-G2) - Compose-source devcontainers (apple-container doesn't support compose; see design §9) - updateRemoteUserUID reconciliation (skipped on this backend per design §13.8 — virtiofs is identity-permissive) - The full M2/M3 fixture suite — the existing tests are tightly coupled to *docker.Runtime and would need refactoring to be runtime-agnostic. The one new test demonstrates the engine integration shape; broader parity is a future task once the remaining Runtime methods land. Test plan: - `make bridge && go test -tags=integration ./test/integration/...` — 2 new apple-container tests pass; existing docker tests still pass - daemon prerequisite: `brew install container && container system start` Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds four engine-integration scenarios that exercise the deeper runtime surface beyond the basic Up/Exec/Down lifecycle: - TestAppleContainer_ReattachStopped: Up → Down(no remove) → Up. Validates FindContainerByLabel + StartContainer-idempotency through the engine's restart-existing path. Asserts the same container ID survives the cycle. - TestAppleContainer_LifecycleAndIdempotency: postCreate / postStart / postAttach commands run through ExecContainer; marker-based idempotency holds across a restart (create stays at 1, start and attach increment to 2). ~12 exec round-trips per run — effectively a stress test for the cgo pipe-fd plumbing under realistic engine workload. - TestAppleContainer_WorkspaceMount_Writable: validates design §13.8 end-to-end. Engine binds the host workspace into the container; test writes a marker from inside the VM and reads it back from the host. Proves virtiofs identity-permissive mounting works without updateRemoteUserUID — the M6 design's load-bearing assumption. - TestAppleContainer_ComposeSource_DocumentsLimitation: confirms the design §9 contract — compose-source devcontainers fail with runtime.ErrNotImplemented through the engine. Test runtime: ~42s for all six apple-container integration tests (including the two from the initial PR-H commit). Existing docker integration tests are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces PR-G's partial typed-error stub with a working BuildKit integration. eng.Build now produces real images from Dockerfiles on the apple-container backend. Bridge (Swift, build.swift): - ac_build wraps the full flow: dial buildkit container over vsock, construct SwiftNIO MultiThreadedEventLoopGroup + Builder, build via gRPC, then load + unpack + tag the resulting OCI tarball through the images service. - Export destination lives under <apiserver.appRoot>/builder/<buildID>/ per Apple convention; this directory is the host-side mount the apiserver shares into the buildkit VM as /var/lib/container-builder-shim/exports/<buildID>/. Writing anywhere else fails inside the VM with "no such file or directory". - BuildConfig defaults: terminal=nil + quiet=false (matches CLI's `progress=plain` mode). Empirically, quiet=true causes BuildKit to stall before sending the Solve request — observed in test iterations; documented inline. - Single-platform builds only; multi-platform is out of scope. - Auto-start of the buildkit container is deliberately NOT implemented (BuilderStart.start is internal to ContainerCommands and reimplementing it duplicates lifecycle logic). Users must `container builder start` once per machine; we surface a typed BuilderUnavailableError otherwise via the existing PR-G probe. Bridge dep additions in Package.swift: - ContainerBuild (Builder, BuildConfig, BuildExport, BuildPipeline) - ContainerImagesService (RemoteContentStoreClient — BuildKit's ContentStore implementation) Plus implicit transitive deps via apple/container: NIOPosix / NIOCore / GRPCCore / swift-log. cgo shim: one new dlsym slot (ac_build_p) + wrapper. Runtime method (build_darwin_arm64.go): - BuildImage probes builder liveness first; surfaces typed *runtime.BuilderUnavailableError if not running. - Marshals RunSpec into the bridge JSON shape; emits BuildEventLog + BuildEventCompleted (no fine-grained progress yet — see below). - mapRunErr remaps any post-probe "buildkit not found" / "builder not running" string back to BuilderUnavailableError so callers get a single typed-error contract regardless of which dial point fails. Known limitations / follow-up work: - BuildKit progress output goes to the bridge's stderr (which the Go process inherits). Streaming typed BuildEvents from BuildKit's progress channel into our events channel is a future PR — needs a custom Terminal substitute that captures and parses BuildKit's output lines. - SwiftNIO emits "Cannot schedule tasks on an EventLoop that has already shut down" warnings on completion, because the gRPC client's internal graceful-shutdown has straggler work after our event loop tear-down. Warnings are upstream deprecation notices rather than functional failures; tests pass. A polish PR can fix the sequencing when grpc-swift exposes the right hook. - In-flight cancellation isn't wired. Build is long-running and cancelling Go's ctx doesn't currently interrupt the gRPC stream. Tests (build_darwin_arm64_test.go): - TestBuildImage_DockerfileSmoke: 2-line Dockerfile, BuildImage, InspectImage afterwards verifies the tag landed in the local content store. Asserts a BuildEventCompleted with a digest is emitted. - TestBuildImage_NoCache_FreshLayers: same shape with NoCache=true, verifies the flag plumbs through (build output shows RUN executes instead of CACHED). - TestBuildImage_BuilderDownTypedError: kills the builder via CLI then asserts the typed-error path. Skips if the builder is restored mid-test. Test plan: - `make bridge && go test ./runtime/applecontainer/...` — full package green; both build tests pass in ~2-3s each on a warm builder - `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build` — stub OK - Daemon prerequisites: `container system start` AND `container builder start` Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backfills the apple-container integration suite to ~70% parity with docker's M2-M4 coverage. Each new file mirrors its docker counterpart in shape so future cross-runtime helpers (deferred per design) can extract shared assertion bodies cleanly. New test files: - applecontainer_image_metadata_test.go: RemoteUserHonored + UserOverrideWins. Builds a labeled image via PR-G2 BuildImage, exercises the engine's InspectImage-driven config-merge path. - applecontainer_shutdown_action_test.go: NoneLeavesRunning + StopContainerStops. Exercises eng.Shutdown semantics through the runtime's StopContainer. - applecontainer_userenvprobe_test.go: PathFromBashrc, None, LifecycleSeesBashrcPath (the 3 most representative of docker's 6 variants). Pulls bash:5.2-alpine3.20 for shells that source rc files. - applecontainer_build_source_test.go: FullLifecycle + BuildArgs. Newly enabled by PR-G2; replaces the PR-H DocumentsLimitation stub (removed alongside). - applecontainer_uid_reconcile_test.go: pins design §13.8 contract — we do NOT reconcile UID, and the workspace mount remains writable as a non-root user across virtiofs. Companion to the WorkspaceMount_Writable test from PR-H. - applecontainer_features_test.go: A2 probe. Currently SKIPs with a TODO because the engine's feature dockerfile generation emits COPY semantics that work on docker classic builder but mis-place the feature dirs under BuildKit (PR-G2). The runtime is fine — the gap is in feature/dockerfile.go's COPY paths. Tracked as a follow-up; non-blocking for the rest of M6. Removed: - TestAppleContainer_BuildSource_DocumentsLimitation (PR-H stub). Replaced by TestAppleContainer_BuildSource_FullLifecycle in applecontainer_build_source_test.go. Builder-down typed-error contract is still pinned by TestBuildImage_BuilderDownTypedError in the runtime package. Helpers added to applecontainer_image_source_test.go: - writeFile, runtimeBuildSpec — small DRY wrappers used across the new files. Test plan: - `container system start && container builder start` first - `go test -tags=integration ./test/integration/...` — 16 apple tests (15 pass + 1 probe skip with TODO), 22 docker tests still pass. Full apple suite runtime ~112s on a warm builder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds a Swift C-bridge exposing lifecycle, exec, logs, image pull, and BuildKit build operations; extends the C shim to load new symbols; implements darwin/arm64 Go runtime methods (build, pull, lifecycle, exec, logs); adds typed builder-unavailable error and broad integration tests. ChangesApple-container runtime bridge and Go implementation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@applecontainer-bridge/include/ac_bridge.h`:
- Around line 298-309: The ac_build_probe ABI currently returns only a free-form
{ "ok": bool, "err": "..." } JSON which makes Go-side typed mapping brittle;
update the contract for ac_build_probe (and the other similar probe function at
lines 335-337) to include a stable machine-readable field such as "code" or
"kind" (e.g. { "ok": true } or { "ok": false, "code": "BUILDER_UNAVAILABLE",
"err": "..." }) and document this in ac_bridge.h's comment block and the
function's return encoding; ensure callers still free the returned char* with
ac_free and that existing consumers are updated to inspect the new "code"/"kind"
field for typed error handling.
- Around line 139-146: The bridge currently documents that RunSpec.RunArgs,
RunSpec.Privileged, and RunSpec.SecurityOpt are silently dropped; instead, add
validation early in the request handling path (the code that consumes the
canonical RunSpec wire shape and produces the { "ok": ..., "data"/"err": ... }
response) to detect non-empty RunArgs or true Privileged or non-empty
SecurityOpt and immediately return a stable unsupported-capability error (e.g.
"unsupported: RunArgs/Privileged/SecurityOpt") rather than proceeding; update
the handler that parses RunSpec (the function called when decoding the incoming
RunSpec message in this header/bridge implementation) to perform these checks
and return {ok:false, err:...} consistently, and apply the same validation logic
to the other bridge entry point referenced in the file (the block around the
same RunSpec handling later).
In `@runtime/applecontainer/exec_darwin_arm64.go`:
- Around line 189-203: The stdout/stderr Go-facing readers are currently closed
via pipes.closeGoFacingReaders() before waiting for the copy goroutines, which
can truncate buffered output; move the call to pipes.closeGoFacingReaders() so
it runs after wg.Wait() (i.e., after the copy goroutines have drained) and
before close(copyErrCh), leaving the defer
C.ac_exec_release_p(C.uint64_t(handle)) unchanged.
In `@runtime/applecontainer/logs_darwin_arm64_test.go`:
- Around line 58-63: Replace the fixed time.Sleep and unbounded context usage by
creating a bounded context (via context.WithTimeout) and retrying reads until
the expected log appears or the timeout elapses; specifically, remove the 500ms
sleep and instead loop with a short interval, calling rt.ContainerLogs(ctx, id,
&buf, false) inside that loop using the timeout-backed ctx, and break the loop
when the buffer contains the expected output (or fail when ctx.Done() triggers);
this uses the existing rt.ContainerLogs call and the test's id/bytes.Buffer
while ensuring the test does not hang indefinitely.
In `@runtime/applecontainer/logs_darwin_arm64.go`:
- Around line 83-86: The current forward logic calls dst.Write(buf[:n]) and
treats any returned error as fatal, but it ignores short writes (wrote < n) and
thereby silently drops the remaining bytes; change the write handling in the
loop that processes read chunk `buf[:n]` so you repeatedly call dst.Write until
totalWritten == n (or an error occurs), update the slice/offset accordingly, and
on error wrap and return it the same way as `fmt.Errorf("applecontainer: log
writer: %w", err)`; reference the existing dst.Write call and the buf[:n] usage
to locate where to implement this write-until-complete loop.
- Around line 32-35: The ContainerLogs method should guard against a nil writer
to avoid copyLogs panics: at the start of Runtime.ContainerLogs, after the
context check, add a nil check for the w parameter and if w == nil set w =
io.Discard (or otherwise ensure a non-nil io.Writer) before proceeding to call
copyLogs so copyLogs never receives a nil writer.
In `@test/integration/applecontainer_features_test.go`:
- Around line 81-91: The current probe swallows setup failures by calling t.Logf
and t.Skip when err != nil (after the eng.Up/setup step in
applecontainer_features_test.go); change this to fail the test instead—replace
the t.Logf/t.Skip path with a failing assertion (e.g., t.Fatalf or t.Errorf
followed by return) so that errors from eng.Up (or the setup producing err)
surface as CI failures while keeping the existing error message text for
context.
In `@test/integration/applecontainer_image_source_test.go`:
- Around line 148-159: The test may leak the container created by the second
call to eng.Up if reattach fails; after successfully obtaining second (i.e.,
right after second, err := eng.Up(... ) returns nil) immediately schedule
cleanup for it (e.g., defer eng.Down(ctx, second) or the appropriate
teardown/close method on the returned object) so that regardless of subsequent
assertions (including the ID mismatch branch) the second container is always
torn down; reference eng.Up, the returned variable second (and its
Container.ID), and eng.Down/teardown method to locate where to add the
defer/cleanup.
In `@test/integration/applecontainer_uid_reconcile_test.go`:
- Around line 47-49: The test currently skips on any error from rt.BuildImage
(called with runtimeBuildSpec) which hides genuine build failures; change the
logic so you detect and only skip when the error clearly indicates the builder
is unavailable (e.g., connection/refused/unavailable/rpc Unavailable or other
known sentinel returned by the builder client) and otherwise fail the test
(t.Fatalf or t.Errorf) with the full error; update the conditional around
rt.BuildImage(ctx, runtimeBuildSpec(dir, tag), nil) so t.Skipf is only invoked
for the builder-unavailable error case and non-builder errors surface as test
failures.
In `@test/integration/applecontainer_userenvprobe_test.go`:
- Around line 124-126: The test currently skips on any error from rt.BuildImage,
which hides real failures; update the check around
BuildImage(runtimeBuildSpec(...)) so it only calls t.Skipf when the error is
specifically a BuilderUnavailableError (use errors.Is(err,
BuilderUnavailableError) or errors.As to match *BuilderUnavailableError as
appropriate), and call t.Fatalf or t.Fatalf-like assertion for all other errors
so true build failures surface; keep the same callsite (rt.BuildImage and
runtimeBuildSpec) and only change the error-handling branch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3d43bd4c-dec8-400c-b52f-36918e23f35a
📒 Files selected for processing (29)
applecontainer-bridge/Package.swiftapplecontainer-bridge/Sources/ACBridge/build.swiftapplecontainer-bridge/Sources/ACBridge/exec.swiftapplecontainer-bridge/Sources/ACBridge/lifecycle.swiftapplecontainer-bridge/Sources/ACBridge/logs.swiftapplecontainer-bridge/Sources/ACBridge/pull.swiftapplecontainer-bridge/include/ac_bridge.hruntime/applecontainer/build_darwin_arm64.goruntime/applecontainer/build_darwin_arm64_test.goruntime/applecontainer/exec_darwin_arm64.goruntime/applecontainer/exec_darwin_arm64_test.goruntime/applecontainer/inspect_darwin_arm64.goruntime/applecontainer/lifecycle_darwin_arm64.goruntime/applecontainer/lifecycle_darwin_arm64_test.goruntime/applecontainer/logs_darwin_arm64.goruntime/applecontainer/logs_darwin_arm64_test.goruntime/applecontainer/pull_darwin_arm64.goruntime/applecontainer/pull_darwin_arm64_test.goruntime/applecontainer/runtime_darwin_arm64.goruntime/applecontainer/shim.cruntime/applecontainer/shim.hruntime/errors.gotest/integration/applecontainer_build_source_test.gotest/integration/applecontainer_features_test.gotest/integration/applecontainer_image_metadata_test.gotest/integration/applecontainer_image_source_test.gotest/integration/applecontainer_shutdown_action_test.gotest/integration/applecontainer_uid_reconcile_test.gotest/integration/applecontainer_userenvprobe_test.go
- Reject RunArgs / Privileged / SecurityOpt at the Go boundary with a typed UnsupportedOptionError instead of silently dropping them - Tag builder-unavailable failures with a stable `code` field in the bridge envelope; Go side keys typed errors off `code` rather than message text - exec: drop the pre-drain close of stdout/stderr readers; rely on the apiserver's natural EOF so buffered output is never truncated - logs: guard ContainerLogs against a nil writer; loop on writes so a short-write never silently drops bytes - logs test: replace fixed sleep + unbounded context with a bounded retry under context.WithTimeout - features test: surface eng.Up failures as t.Fatalf instead of skipping the contract assertion - uid_reconcile / userenvprobe tests: only skip when BuildImage returns BuilderUnavailableError; fail otherwise - image_source test: defer teardown of the second Up's container so an ID-changed regression doesn't leak it Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@applecontainer-bridge/Sources/ACBridge/build.swift`:
- Around line 121-139: Ensure eventLoopGroup is always shut down if Builder(...)
or builder.info() throws: wrap the Builder(socket:socketHandle,
group:eventLoopGroup, logger:logger) construction and the subsequent _ = try
await builder.info() probe in a do/catch (or equivalent) that calls try?
eventLoopGroup.shutdownGracefully() in the error path and rethrows a
BridgeCodedError with code "BUILDER_UNAVAILABLE" so failures mirror the dial
pattern; reference the symbols eventLoopGroup, Builder, builder.info(),
eventLoopGroup.shutdownGracefully(), and BridgeCodedError("BUILDER_UNAVAILABLE")
when making the change.
In `@applecontainer-bridge/Sources/ACBridge/Helpers.swift`:
- Around line 91-95: The jsonEscape(_ s: String) function currently only handles
backslash, quote and newline, which fails for other control characters
(U+0000–U+001F) per JSON/RFC 7159; update jsonEscape to iterate the input
characters and emit proper escapes for backspace (\b), form feed (\f), carriage
return (\r), tab (\t), newline (\n), backslash (\\) and quote (") and encode any
remaining control codepoints (<= 0x1F) as Unicode escapes like \u00XX so all
control characters are validly escaped for downstream Go decoding.
In `@test/integration/applecontainer_features_test.go`:
- Around line 9-13: Update the stale header comment in the apple-container probe
test to reflect the current fail-fast behavior: change the paragraph that states
failures are "expected-to-fail and skipped" to state that the single probe test
now fails the suite immediately (fail-fast) and that failures should be handled
as explicit failures (referencing the probe at Line 82 that now fails). Edit the
top-of-file comment in test/integration/applecontainer_features_test.go to
describe this contract change so readers know the test is authoritative and will
fail the run rather than being marked expected-to-fail.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f835a855-da5e-45b2-8c17-13af75167589
📒 Files selected for processing (15)
applecontainer-bridge/Sources/ACBridge/Helpers.swiftapplecontainer-bridge/Sources/ACBridge/build.swiftapplecontainer-bridge/include/ac_bridge.hruntime/applecontainer/build_darwin_arm64.goruntime/applecontainer/envelope_test.goruntime/applecontainer/exec_darwin_arm64.goruntime/applecontainer/inspect_darwin_arm64.goruntime/applecontainer/lifecycle_darwin_arm64.goruntime/applecontainer/logs_darwin_arm64.goruntime/applecontainer/logs_darwin_arm64_test.goruntime/errors.gotest/integration/applecontainer_features_test.gotest/integration/applecontainer_image_source_test.gotest/integration/applecontainer_uid_reconcile_test.gotest/integration/applecontainer_userenvprobe_test.go
🚧 Files skipped from review as they are similar to previous changes (10)
- runtime/errors.go
- runtime/applecontainer/inspect_darwin_arm64.go
- runtime/applecontainer/build_darwin_arm64.go
- runtime/applecontainer/logs_darwin_arm64_test.go
- test/integration/applecontainer_uid_reconcile_test.go
- runtime/applecontainer/lifecycle_darwin_arm64.go
- runtime/applecontainer/exec_darwin_arm64.go
- test/integration/applecontainer_image_source_test.go
- runtime/applecontainer/logs_darwin_arm64.go
- applecontainer-bridge/include/ac_bridge.h
- jsonEscape: handle all RFC 7159 control characters (\r, \t, \b,
\f, and \u00XX for the remainder of U+0000–U+001F). Previously
only \\, \", and \n were escaped, so an error containing e.g. a
tab would produce invalid JSON and break Go-side decoding
- build.swift: wrap Builder construction + info() probe in a
do/catch that shuts down the event loop on failure and rethrows
as BridgeCodedError("BUILDER_UNAVAILABLE"), mirroring the dial
path. Previously a throw from either call leaked NIO threads
- features_test: refresh stale top-of-file comment to reflect the
current fail-fast contract (test failures are real regressions,
not expected-to-fail TODOs)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds support for Apple's native
containerruntime as a backend for devcontainers on darwin/arm64. Containers are launched directly against Apple's runtime via a Swift bridge instead of routing through Docker.What lands
Swift bridge (
applecontainer-bridge/)ACBridgelibrary exposing a C ABI (include/ac_bridge.h) over Apple'sContainerClient.lifecycle.swift,exec.swift,inspect.swift,logs.swift,pull.swift,build.swift, plus sharedHelpers.swift.dlopen'd at runtime so the rest of the toolchain stays portable.Go runtime (
runtime/applecontainer/, darwin/arm64)RunContainer/StartContainer/StopContainer/RemoveContainerwith bind / tmpfs / volume mount mapping.InspectContainer+FindContainerByLabelreturning a typedContainersnapshot (state, mounts, labels).ExecContainerwith stdin, TTY, and cancellation support.ContainerLogsstreaming.PullImageagainst the local content store.BuildImagevia the full BuildKit gRPC client, with a builder probe that surfaces a typed error when BuildKit isn't available.ImageNotFoundError,ContainerNotFoundError, builder-unavailable) translated from Apple's error shapes.shim.c/shim.h) resolves bridge entry points viadlsym; partial-failure path resets state cleanly.Integration tests (
test/integration/)Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests