Skip to content

applecontainer: native runtime backend#62

Merged
bilby91 merged 11 commits into
mainfrom
m6/pr-i-integration-bucket-a
May 15, 2026
Merged

applecontainer: native runtime backend#62
bilby91 merged 11 commits into
mainfrom
m6/pr-i-integration-bucket-a

Conversation

@bilby91
Copy link
Copy Markdown
Member

@bilby91 bilby91 commented May 15, 2026

Summary

Adds support for Apple's native container runtime 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/)

  • New ACBridge library exposing a C ABI (include/ac_bridge.h) over Apple's ContainerClient.
  • Per-capability sources: lifecycle.swift, exec.swift, inspect.swift, logs.swift, pull.swift, build.swift, plus shared Helpers.swift.
  • Built as a dylib, embedded into the Go binary and dlopen'd at runtime so the rest of the toolchain stays portable.

Go runtime (runtime/applecontainer/, darwin/arm64)

  • RunContainer / StartContainer / StopContainer / RemoveContainer with bind / tmpfs / volume mount mapping.
  • InspectContainer + FindContainerByLabel returning a typed Container snapshot (state, mounts, labels).
  • ExecContainer with stdin, TTY, and cancellation support.
  • ContainerLogs streaming.
  • PullImage against the local content store.
  • BuildImage via the full BuildKit gRPC client, with a builder probe that surfaces a typed error when BuildKit isn't available.
  • Typed errors (ImageNotFoundError, ContainerNotFoundError, builder-unavailable) translated from Apple's error shapes.
  • cgo shim (shim.c / shim.h) resolves bridge entry points via dlsym; partial-failure path resets state cleanly.

Integration tests (test/integration/)

  • Full image lifecycle (pull / inspect / metadata / build from source).
  • Feature contributions, user-env probe, UID reconciliation, shutdown action.

Test plan

  • `make bridge && go test ./runtime/applecontainer/...` — green on darwin/arm64
  • `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build ./runtime/applecontainer/...` — stub still compiles
  • `go vet ./...` clean
  • Integration tests on a darwin/arm64 host with Apple `container` installed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Full container lifecycle: create, start, stop, delete (idempotent semantics)
    • Exec support: start/wait/signal/release with stdin/stdout/stderr streaming and cancellation
    • Image ops: pull images and BuildKit-based Dockerfile builds (probe + export)
    • Logs: open and stream container logs with follow/non-follow modes
    • Improved builder-unavailable error hints
  • Tests

    • Expanded integration suite covering lifecycle, exec, build, pull, logs, image metadata, features, shutdown, UID and env-probe behaviors

Review Change Stack

bilby91 and others added 9 commits May 15, 2026 00:18
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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2cff2939-b2a3-4fe9-a89f-711c762d5026

📥 Commits

Reviewing files that changed from the base of the PR and between 297dc9c and c6e4e14.

📒 Files selected for processing (3)
  • applecontainer-bridge/Sources/ACBridge/Helpers.swift
  • applecontainer-bridge/Sources/ACBridge/build.swift
  • test/integration/applecontainer_features_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/integration/applecontainer_features_test.go
  • applecontainer-bridge/Sources/ACBridge/build.swift

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Apple-container runtime bridge and Go implementation

Layer / File(s) Summary
Bridge API contract and package dependency
applecontainer-bridge/Package.swift, applecontainer-bridge/include/ac_bridge.h
Adds ContainerImagesService dependency and declares expanded C bridge functions (ac_run, ac_start, ac_stop, ac_delete, exec APIs, ac_logs_open, ac_pull_image, ac_build_probe, ac_build) with JSON envelope contracts.
Bridge error helpers
applecontainer-bridge/Sources/ACBridge/Helpers.swift
Introduces BridgeCodedError, json escaping, and encodeErrWithCode helpers to emit typed {ok:false, code, err} envelopes.
Container lifecycle bridge
applecontainer-bridge/Sources/ACBridge/lifecycle.swift
Implements ac_run, ac_start, ac_stop, ac_delete with RunSpec wire types, validation, mount conversion, process configuration, and shared lifecycle timeout behavior.
Process execution bridge & registry
applecontainer-bridge/Sources/ACBridge/exec.swift
Implements ac_exec_start, ac_exec_wait, ac_exec_signal, ac_exec_release with ExecRegistry, fd wrappers, user parsing, and JSON envelopes.
Logs bridge
applecontainer-bridge/Sources/ACBridge/logs.swift
Implements ac_logs_open returning duplicated FD for Go to stream logs independently.
Pull bridge
applecontainer-bridge/Sources/ACBridge/pull.swift
Implements ac_pull_image as a synchronous pull with a 30-minute timeout and PullResult envelope.
BuildKit bridge
applecontainer-bridge/Sources/ACBridge/build.swift
Implements ac_build_probe and ac_build: BuildKit vsock dialing, builder verification, export to OCI tar, tagging, and BuildResult envelope.
C shim loading and wrapper functions
runtime/applecontainer/shim.c, runtime/applecontainer/shim.h
Extends dlsym loading for new bridge symbols and adds _p wrappers that dispatch to loaded function pointers or return null.
Go BuildImage & PullImage (darwin/arm64)
runtime/applecontainer/build_darwin_arm64.go, runtime/applecontainer/pull_darwin_arm64.go
Adds BuildImage and PullImage implementations that call the bridge, probe for builder availability (mapped to BuilderUnavailableError), emit build/pull events, and decode bridge results.
Go lifecycle & exec & logs implementations
runtime/applecontainer/lifecycle_darwin_arm64.go, runtime/applecontainer/exec_darwin_arm64.go, runtime/applecontainer/logs_darwin_arm64.go
Adds RunContainer/Start/Stop/Remove, ExecContainer with pipe and streaming management and cancellation semantics, and ContainerLogs with follow/non-follow streaming and cancellation semantics.
Inspect mount model updates
runtime/applecontainer/inspect_darwin_arm64.go
Refactors mount wire model to accept variant or object forms, maps Apple mount kinds to runtime types, and computes ReadOnly from mount options.
Runtime cleanup and error types
runtime/applecontainer/runtime_darwin_arm64.go, runtime/errors.go
Removes placeholder stubs, adds BuilderUnavailableError type (Hint + wrapped Err).
Integration & unit tests (darwin/arm64)
test/integration/*, runtime/applecontainer/*_test.go
Adds many integration and unit tests validating build-source, image-source, lifecycle, exec, logs, pull, build caching, builder-down behavior, feature installation, image metadata/remoteUser, UID reconciliation, userEnvProbe, shutdown actions, mounts, and related helpers.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

"🐰 Bridging worlds, one byte at a time,
Swift meets Go in a dance so fine,
Containers now run on Apple's bright arm,
Building and executing without alarm."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'applecontainer: native runtime backend' is concise, specific, and clearly summarizes the main change: adding support for Apple's native container runtime as a new backend.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch m6/pr-i-integration-bucket-a

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c212bc8 and 4e57718.

📒 Files selected for processing (29)
  • applecontainer-bridge/Package.swift
  • applecontainer-bridge/Sources/ACBridge/build.swift
  • applecontainer-bridge/Sources/ACBridge/exec.swift
  • applecontainer-bridge/Sources/ACBridge/lifecycle.swift
  • applecontainer-bridge/Sources/ACBridge/logs.swift
  • applecontainer-bridge/Sources/ACBridge/pull.swift
  • applecontainer-bridge/include/ac_bridge.h
  • runtime/applecontainer/build_darwin_arm64.go
  • runtime/applecontainer/build_darwin_arm64_test.go
  • runtime/applecontainer/exec_darwin_arm64.go
  • runtime/applecontainer/exec_darwin_arm64_test.go
  • runtime/applecontainer/inspect_darwin_arm64.go
  • runtime/applecontainer/lifecycle_darwin_arm64.go
  • runtime/applecontainer/lifecycle_darwin_arm64_test.go
  • runtime/applecontainer/logs_darwin_arm64.go
  • runtime/applecontainer/logs_darwin_arm64_test.go
  • runtime/applecontainer/pull_darwin_arm64.go
  • runtime/applecontainer/pull_darwin_arm64_test.go
  • runtime/applecontainer/runtime_darwin_arm64.go
  • runtime/applecontainer/shim.c
  • runtime/applecontainer/shim.h
  • runtime/errors.go
  • test/integration/applecontainer_build_source_test.go
  • test/integration/applecontainer_features_test.go
  • test/integration/applecontainer_image_metadata_test.go
  • test/integration/applecontainer_image_source_test.go
  • test/integration/applecontainer_shutdown_action_test.go
  • test/integration/applecontainer_uid_reconcile_test.go
  • test/integration/applecontainer_userenvprobe_test.go

Comment thread applecontainer-bridge/include/ac_bridge.h Outdated
Comment thread applecontainer-bridge/include/ac_bridge.h
Comment thread runtime/applecontainer/exec_darwin_arm64.go Outdated
Comment thread runtime/applecontainer/logs_darwin_arm64_test.go Outdated
Comment thread runtime/applecontainer/logs_darwin_arm64.go
Comment thread runtime/applecontainer/logs_darwin_arm64.go
Comment thread test/integration/applecontainer_features_test.go
Comment thread test/integration/applecontainer_image_source_test.go
Comment thread test/integration/applecontainer_uid_reconcile_test.go
Comment thread test/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>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4e57718 and 297dc9c.

📒 Files selected for processing (15)
  • applecontainer-bridge/Sources/ACBridge/Helpers.swift
  • applecontainer-bridge/Sources/ACBridge/build.swift
  • applecontainer-bridge/include/ac_bridge.h
  • runtime/applecontainer/build_darwin_arm64.go
  • runtime/applecontainer/envelope_test.go
  • runtime/applecontainer/exec_darwin_arm64.go
  • runtime/applecontainer/inspect_darwin_arm64.go
  • runtime/applecontainer/lifecycle_darwin_arm64.go
  • runtime/applecontainer/logs_darwin_arm64.go
  • runtime/applecontainer/logs_darwin_arm64_test.go
  • runtime/errors.go
  • test/integration/applecontainer_features_test.go
  • test/integration/applecontainer_image_source_test.go
  • test/integration/applecontainer_uid_reconcile_test.go
  • test/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

Comment thread applecontainer-bridge/Sources/ACBridge/build.swift
Comment thread applecontainer-bridge/Sources/ACBridge/Helpers.swift Outdated
Comment thread test/integration/applecontainer_features_test.go Outdated
- 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>
@bilby91 bilby91 merged commit 9aebace into main May 15, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant