IMAGE_NAME := opencode-sandbox IMAGE_TAG := local IMAGE := $(IMAGE_NAME):$(IMAGE_TAG) NETWORK_NAME := opencode-sandbox-net WORKDIR_NAME := $(notdir $(patsubst %/,%,$(CURDIR))) # OpenCode follows the XDG Base Directory spec with three distinct dirs: # config (settings/plugins), data (auth.json, opencode.db, snapshots), and # state (session/model history, locks) -- data and state are NOT the same # directory, despite the similar names. CONFIG_DIR := $(HOME)/.config/opencode DATA_DIR := $(HOME)/.local/share/opencode STATE_DIR := $(HOME)/.local/state/opencode # No :Z/:z SELinux suffixes: on macOS the host side (APFS) has no SELinux # concept, so the labeling flag would be a meaningless no-op here. # # Known virtiofs caveat (default podman-machine file share on macOS/applehv): # inotify file-change events don't propagate into the container. Not a # blocker for this sandbox, just a rough edge if a tool relies on watching # /workspace for live changes. # # --memory/--memory-swap/--cpus: on macOS, `podman machine`'s Linux VM has a # FIXED resource allocation (check with `podman machine inspect`; currently # 4 GiB / 6 CPUs, bumped up from the 2 GiB default -- see `podman machine # set --memory podman-machine-default`, which requires the machine to # be stopped first). Without a cgroup cap here, a memory-hungry build tool # (Gradle's daemon and its separate Kotlin-compiler daemon are the worst # offenders -- both size their heap off "available" memory with no idea # they're sharing a small VM) can consume the VM's *entire* RAM. That # doesn't just OOM-kill the container -- it starves the VM's own guest # kernel, which stalls the very processes Podman needs to stay responsive # (its API socket, gvproxy, SSH), making the whole machine appear to hang. # Capping the container forces a contained OOM kill instead. --memory-swap # equal to --memory disables extra swap for the container specifically, so # it fails fast rather than thrashing. Sized to leave ~1 GiB / 2 CPUs of # headroom below the VM's own allocation for the VM/podman/gvproxy itself -- # adjust alongside `podman machine set` if you resize the machine again. # Override any of these per-invocation by exporting OPENCODE_SANDBOX_MEMORY, # OPENCODE_SANDBOX_MEMORY_SWAP, and/or OPENCODE_SANDBOX_CPUS before running # `make opencode`/`make shell`. OPENCODE_SANDBOX_MEMORY ?= 3072m OPENCODE_SANDBOX_MEMORY_SWAP ?= 3072m OPENCODE_SANDBOX_CPUS ?= 4 RUN_FLAGS := \ --rm -it \ --userns=keep-id \ --memory=$(OPENCODE_SANDBOX_MEMORY) \ --memory-swap=$(OPENCODE_SANDBOX_MEMORY_SWAP) \ --cpus=$(OPENCODE_SANDBOX_CPUS) \ --network $(NETWORK_NAME) \ --network-alias $(WORKDIR_NAME) \ -v $(CURDIR):/workspace \ -v $(CONFIG_DIR):/home/dev/.config/opencode \ -v $(DATA_DIR):/home/dev/.local/share/opencode \ -v $(STATE_DIR):/home/dev/.local/state/opencode \ -w /workspace .PHONY: build opencode shell install # Only relevant on macOS, where podman requires a running VM ("podman # machine"). Handles "machine exists but stopped" -> start it. Does NOT # handle "no machine exists at all" -- that needs a one-time manual # `podman machine init`, left as a documented prerequisite since it's a # heavier, resource-allocating operation. # # Written as a single shell line deliberately: macOS ships GNU Make 3.81 # (frozen pre-GPLv3), which predates `.ONESHELL:` (added in 3.82). Without # it, Make runs a separate shell per newline in the *expanded* recipe text, # which would split an if/then/fi block from a multi-line variable across # unrelated shell invocations and break it. A single logical line sidesteps # the issue entirely and works on any Make version. define ENSURE_MACHINE [ "$$(uname -s)" != "Darwin" ] || podman machine list --format '{{.Running}}' 2>/dev/null | grep -q '^true$$' || { echo "==> No running podman machine found; starting default machine..."; podman machine start; } endef # Idempotent: creates the shared cross-container network once, reused on # every subsequent invocation. Any other Podman dev container joined to this # same network name can resolve and reach this sandbox by its workdir alias. define ENSURE_NETWORK podman network exists $(NETWORK_NAME) || podman network create $(NETWORK_NAME) endef build: @$(ENSURE_MACHINE) podman build -t $(IMAGE) -f Containerfile . opencode: @$(ENSURE_MACHINE) @$(ENSURE_NETWORK) podman run $(RUN_FLAGS) $(IMAGE) opencode shell: @$(ENSURE_MACHINE) @$(ENSURE_NETWORK) podman run $(RUN_FLAGS) $(IMAGE) bash # Builds the image, then installs a global `opencode-sandbox` shortcut on # the host (see scripts/install-shortcut.sh for what "installs" means -- a # shim in ~/.local/bin plus a PATH edit for the user's login shell). Kept out # of the recipe itself: multi-line shell-with-conditionals in a Make recipe # means doubling every $ as $$ and fighting Make's per-line shell splitting # (see the ENSURE_MACHINE comment above) -- not worth it for something this # size. install: build sh scripts/install-shortcut.sh "$(CURDIR)"