tart

package
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package tart implements the cell engine for macOS VMs on Apple Silicon via the tart CLI.

Index

Constants

View Source
const (
	NixVolumeFileName = "nix.img"
	NixVolumeSizeGB   = 100
	NixVolumeLabel    = "DevcellNix"
)
View Source
const BaseTemplateName = "devcell-tart-base"

BaseTemplateName is the tart VM name for a stack-agnostic base template (nix installed, store on external disk, but no nix-darwin activation).

View Source
const DarwinVMUser = "devcell"

DarwinVMUser is the fixed nix-darwin/home-manager user inside the macOS VM (devcell-sh/home's darwinVMUser). Agent binaries are installed into its per-user profile, regardless of which session user runs the cell.

View Source
const ProvisionedMarkerPath = "/private/var/devcell-provisioned"

ProvisionedMarkerPath is on the boot disk's writable Data volume. /private/var persists across tart clone. We use /private/var (not /var) to avoid any symlink indirection during early boot.

View Source
const S6EnvDir = s6.EnvDir

S6EnvDir is where session env vars are written for s6 service scripts. Deprecated: use s6.EnvDir from internal/s6 instead.

View Source
const S6ServicesDir = s6.SourceDir

S6ServicesDir is the s6-rc source directory. Deprecated: use s6.SourceDir from internal/s6 instead.

Variables

This section is empty.

Functions

func BuildExecCommand

func BuildExecCommand(spec ExecSpec) string

BuildExecCommand constructs a shell command string for `tart exec <vm> bash -l -c <cmd>`. Sources the nix daemon profile, cds into the project dir, sets env vars, and runs the binary.

When RunAsUser is set, the entire command is wrapped with `sudo -u <user> -i bash -l -c '...'` so the session runs as the specified user (matching Docker's HOST_USER model).

func CellHome

func CellHome(home, cellName string) string

CellHome returns the per-cell persistent home directory. Layout: ~/.devcell/<cellName>/ Same path on both Linux (bind-mount → /home/<user>) and macOS (VirtioFS → /Users/<user>).

func CollectSSHPubKeys

func CollectSSHPubKeys(sshDir string) string

CollectSSHPubKeys reads all *.pub files from sshDir and returns their contents concatenated (one key per line). Returns "" if the directory doesn't exist or contains no .pub files.

func DetectHostNixStore

func DetectHostNixStore() string

DetectHostNixStore returns "/nix" if the host has a functional Nix store, empty string otherwise.

func EnsureHomeDir

func EnsureHomeDir(home, cellName string) (string, error)

EnsureHomeDir creates the CellHome directory if it doesn't exist. Returns the path to the (existing or newly created) directory. Replaces EnsureHomeVolume — home is now a VirtioFS directory mount, not a disk image.

func EnsureNixVolume

func EnsureNixVolume(home string) (string, error)

EnsureNixVolume creates a sparse nix store disk image if it doesn't exist. Returns the path to the (existing or newly created) image. Callers should log the returned path and whether it was created or reused.

func GenerateCreateSessionUserScript

func GenerateCreateSessionUserScript(username string) string

GenerateCreateSessionUserScript returns a script that creates a macOS user matching the host's $USER, mirroring Docker's HOST_USER entrypoint model. The user gets admin group membership (for sudo) and a home directory. Idempotent: skips if the user already exists.

func GenerateHomeMountScript

func GenerateHomeMountScript(cellName, username string) string

GenerateHomeMountScript returns a guest-side script that mounts the CellHome VirtioFS share at /Users/<username>. The host passes --dir home:<cellHome> to tart run.

func GenerateHostNixSubstituterScript

func GenerateHostNixSubstituterScript() string

GenerateHostNixSubstituterScript returns a guest-side script that configures the host's shared /nix store as a local substituter. The host's /nix is shared read-only via VirtioFS at /Volumes/My Shared Files/hostnix.

To avoid SQLite-over-VirtioFS locking issues, the script copies the nix database locally and symlinks the store paths from the VirtioFS mount.

func GenerateNixDarwinActivateScript

func GenerateNixDarwinActivateScript(stack, flakeDir string) string

GenerateNixDarwinActivateScript returns the nix-darwin activation command. nix-darwin manages system-level config (LaunchDaemons, /etc/) and user packages — a superset of home-manager. The flakeDir must contain a flake with darwinConfigurations.<stack>.

func GenerateNixDiskPrepScript

func GenerateNixDiskPrepScript(cellName string) string

GenerateNixDiskPrepScript returns a script that reformats the external VirtIO disk as JHFS+ with the "DevcellNix" label BEFORE the Nix installer runs. This prevents a label collision: the Determinate installer creates an APFS volume named "Nix Store" and then encrypts it by name. If a pre-existing HFS+ volume with the same label exists (from a previous build), diskutil finds the wrong volume and the encrypt step fails.

func GenerateNixInstallScript

func GenerateNixInstallScript() string

GenerateNixInstallScript returns a script that installs Nix using the official multi-user installer. The installer creates an APFS volume named "Nix Store" on the boot disk — GenerateNixStoreSwapScript runs afterward to migrate the nix store to our external JHFS+ disk.

We use the official installer (not Determinate) because nix-darwin's activation checks for /usr/local/bin/determinate-nixd and aborts if found — the two daemon management layers conflict.

func GenerateNixShadowRepairScript

func GenerateNixShadowRepairScript() string

GenerateNixShadowRepairScript returns a best-effort script that detects and repairs the installer-APFS-over-JHFS+ shadow mount on /nix.

Templates built before the swap-script fix keep the installer's org.nixos.darwin-store LaunchDaemon, which re-mounts the tiny APFS "Nix Store" volume (initial install only, ~71 store paths) over the JHFS+ DevcellNix disk at every clone boot. That hides the nix-darwin system profile, s6 binaries, and per-user profiles. This runs at VM start, before the s6/nix-darwin preflight. Deliberately not `set -e`: on a healthy VM every step is a no-op and boot must proceed.

func GenerateNixStoreSwapScript

func GenerateNixStoreSwapScript(cellName string) string

GenerateNixStoreSwapScript returns a script that migrates the nix store from the Determinate installer's APFS volume to our external JHFS+ disk. Assumes GenerateNixDiskPrepScript already formatted the disk as JHFS+ "DevcellNix" and unmounted it. This script runs after "Install Nix":

  1. Mounts the pre-formatted "DevcellNix" volume
  2. Copies the initial nix store (small — daemon + profiles only)
  3. Swaps the /nix mount from installer's APFS to our JHFS+
  4. Installs a boot-time LaunchDaemon for clone VMs
  5. Restarts the nix daemon on the new mount

func GeneratePreflightDiagScript

func GeneratePreflightDiagScript() string

GeneratePreflightDiagScript returns a diagnostic script that checks the VM environment before Nix installation. Output goes to the debug log via io.MultiWriter so failures are diagnosable without re-running.

func GenerateProjectMountScript

func GenerateProjectMountScript(tag, username, mountPoint string) string

GenerateProjectMountScript returns a script to symlink the project VirtioFS share at mountPoint (an absolute in-VM path from ProjectPathInVM), mirroring Docker's bind-mount behavior. Parent directories are created as the cell user so they stay writable.

func GenerateProvisionedMarkerScript

func GenerateProvisionedMarkerScript() string

GenerateProvisionedMarkerScript returns a script that stamps a marker file indicating provisioning completed successfully. Written to /private/var (boot disk) rather than ~ because VirtioFS may shadow the home directory at runtime.

func GenerateS6SessionActivateScript

func GenerateS6SessionActivateScript(cellUser string) string

GenerateS6SessionActivateScript returns a script that activates s6 session services for a newly created session user via s6-rc. Delegates to the shared s6.ActivateScript for the platform-agnostic parts.

func GenerateSSHEnablementScript

func GenerateSSHEnablementScript() string

GenerateSSHEnablementScript returns a shell script to enable SSH on macOS.

func GenerateSSHKeyPair

func GenerateSSHKeyPair(dir string) (string, error)

GenerateSSHKeyPair creates an ed25519 key pair and writes them to the artifact dir. Returns the public key in OpenSSH authorized_keys format.

func GenerateSSHKeyScript

func GenerateSSHKeyScript(pubKey string) string

GenerateSSHKeyScript returns a script to inject an SSH public key.

func GenerateSetHostnameScript

func GenerateSetHostnameScript(cellName string) string

GenerateSetHostnameScript returns a script that sets the macOS hostname to cellName via scutil, so `hostname -s` returns the cell name. This lets the nix home-manager set-wallpaper activation read the cell name from the OS.

func GenerateSetupSessionHomeScript

func GenerateSetupSessionHomeScript(username string) string

GenerateSetupSessionHomeScript returns a script that symlinks the CellHome VirtioFS share into the cell user's home directory, mirroring how the build-time "Mount home volume" step sets up /Users/admin.

func GenerateSudoersScript

func GenerateSudoersScript(username string) string

GenerateSudoersScript returns a script to enable passwordless sudo.

func GenerateVerifySSHdFDAScript

func GenerateVerifySSHdFDAScript() string

GenerateVerifySSHdFDAScript returns a script that checks whether sshd has Full Disk Access in the TCC database. Run over SSH before Nix installation to confirm the boot-time grant succeeded. Exits 0 if granted, 1 if not.

func GenerateVirtioFSMountScript

func GenerateVirtioFSMountScript(tag, mountPoint string) string

GenerateVirtioFSMountScript returns a script to mount a VirtioFS share.

Tart 2.30+ bundles --dir shares under Apple's automount VirtioFS device (tag "com.apple.virtio-fs.automount") instead of creating individual VirtioFS devices per share. macOS automounts these at /Volumes/My Shared Files/<tag>/. The script tries mount_virtiofs first (individual device), then falls back to symlinking from the automount path.

func InstanceVMName

func InstanceVMName(cellName string) string

InstanceVMName returns the tart VM name for a per-cell running instance.

func IsNixStore

func IsNixStore(nixPath string) bool

IsNixStore checks whether the given path contains a functional Nix store (has store/ directory and var/nix/db/db.sqlite database).

func NixVolumePath

func NixVolumePath(home string) string

NixVolumePath returns the path to the global nix store disk image. Layout: ~/.devcell/darwin/nix-store.img Shared across all cells — mirrors Docker's single devcell-nix-store volume.

func PrepareArtifactDir

func PrepareArtifactDir(dir string) error

PrepareArtifactDir creates the artifact directory if it doesn't exist.

func ProjectPathInVM

func ProjectPathInVM(hostHome, baseDir, cellUser string) string

ProjectPathInVM maps the host project path to its in-VM location. When the project lives under the host home, the relative path is mirrored into the cell user's VM home — so /Users/warden/dev/acme/proj on the host appears at the same path inside the VM (cell user == host $USER). Projects outside the host home fall back to ~/<basename>.

func TartClone

func TartClone(ctx context.Context, ref, localName string) error

TartClone runs `tart clone <ref> <localName>`. Parallel to Docker: exec.Command("docker", "pull", tag).

func TartDelete

func TartDelete(ctx context.Context, name string) error

TartDelete runs `tart delete <name>`. Parallel to Docker: exec.Command("docker", "rm", name).

func TartExec

func TartExec(ctx context.Context, name string, command []string, stdout, stderr *os.File) error

TartExec runs a command inside a running VM via `tart exec`.

func TartIP

func TartIP(ctx context.Context, name string) (string, error)

TartIP runs `tart ip <name>` and returns the guest IP.

func TartSet

func TartSet(ctx context.Context, name string, cpus uint, memoryMB uint64) error

TartSet configures VM resources via `tart set`.

func TartStop

func TartStop(ctx context.Context, name string) error

TartStop runs `tart stop <name>`.

func TartVersion

func TartVersion(ctx context.Context) string

TartVersion returns the tart CLI version string.

func TemplateVMName

func TemplateVMName(stack string, modules []string) string

TemplateVMName returns the tart VM name for a built template image.

func WaitForSSH

func WaitForSSH(host string, port uint16, timeout, interval time.Duration) error

WaitForSSH polls a TCP connection to host:port until it succeeds or the timeout elapses. Returns nil on success, or an error describing the failure. Pure network check — no authentication, just verifies the port is accepting.

func WaitForSSHObserved

func WaitForSSHObserved(host string, port uint16, timeout, interval time.Duration, obs Observer, stateFunc ...VMStateFunc) error

WaitForSSHObserved is like WaitForSSH but reports each poll attempt to obs. An optional VMStateFunc checks VM liveness each iteration — if the VM enters "stopped" or "error" state, polling aborts immediately instead of waiting the full timeout.

Types

type AcquireInputs

type AcquireInputs struct {
	VMName       string            // tart VM name (instance, e.g. "CELL-tart")
	TemplateName string            // template VM to clone from if instance doesn't exist
	SharedDirs   map[string]string // tag -> host path (VirtioFS via --dir)
	Disks        []string          // raw disk image paths (VirtIO block devices via --disk)
	SSHHost      string
	SSHPort      uint16
	NoGraphics   bool // pass --no-graphics to tart run (headless); false shows native display
	ExternalVM   bool // user explicitly configured SSH target — skip lifecycle
	SSHTimeout   time.Duration
	InitFunc     func() error // called to auto-init when VM doesn't exist; nil = error instead
}

AcquireInputs are the parameters for AcquireDarwinVM.

func (*AcquireInputs) ApplyDefaults

func (a *AcquireInputs) ApplyDefaults()

ApplyDefaults fills zero values.

func (*AcquireInputs) Validate

func (a *AcquireInputs) Validate() error

Validate checks required fields.

type AcquireResult

type AcquireResult struct {
	VM      *VM    // non-nil only if we started a managed VM (caller must shut it down)
	SSHHost string // resolved SSH host (guest IP or external)
	SSHPort uint16 // resolved SSH port
	Managed bool   // true if we started the VM (vs. external/already-running)
}

AcquireResult holds the outcome of AcquireDarwinVM.

func AcquireDarwinVM

func AcquireDarwinVM(_ context.Context, _ AcquireInputs) (*AcquireResult, error)

AcquireDarwinVM is not available on this platform.

type BuildConfig

type BuildConfig struct {
	CellName string
	HomeDir  string
	Stack    string
	Modules  []string
	CPUs     uint
	MemoryGB uint64
	SSHPort  uint16
	Username string
}

BuildConfig holds the parameters for building a macOS VM image.

func (*BuildConfig) ApplyDefaults

func (c *BuildConfig) ApplyDefaults()

ApplyDefaults fills zero-value fields.

func (*BuildConfig) Validate

func (c *BuildConfig) Validate() error

Validate checks required fields.

type CellSSHPaths

type CellSSHPaths struct {
	Dir        string // ~/.devcell/<cellName>/.ssh/
	PrivateKey string // id_ed25519
	PublicKey  string // id_ed25519.pub
}

CellSSHPaths holds paths to per-cell SSH keys. Layout: ~/.devcell/<cellName>/.ssh/

func NewCellSSHPaths

func NewCellSSHPaths(home, cellName string) CellSSHPaths

NewCellSSHPaths returns paths for a cell's SSH keys.

type DiscoveredVM

type DiscoveredVM struct {
	CellName string
	IP       string
	VNCPort  string // macOS Screen Sharing on port 5900
}

DiscoveredVM is a running Tart macOS VM discovered via `tart list`.

func DiscoverRunningVMs

func DiscoverRunningVMs(ctx context.Context) []DiscoveredVM

DiscoverRunningVMs lists running Tart instances that match the devcell naming convention (<cellName>-tart) and resolves each VM's guest IP.

type Engine

type Engine struct{}

Engine runs a cell in a macOS VM on Apple Silicon through the tart CLI. It registers itself as engine.Tart.

Init and Build need darwin/arm64 and fail elsewhere. Run fails elsewhere too unless opts.Debug or opts.DryRun is set: with --debug on another host it simulates every step and prints each with a [MOCK] prefix.

func (Engine) Build

func (Engine) Build(ctx context.Context, opts engine.BuildOpts) error

Build creates the macOS template VM the cell's instance is cloned from: it clones the OCI image (or the local base template when one exists), boots it, provisions nix, nix-darwin and the stack, and saves it as a template. opts.Stage "base" builds the stack-agnostic base template instead. opts.Stack overrides opts.Cell.Stack, and opts.Update implies opts.Force. Templates are keyed on the stack only; opts.Cell.Modules is not applied.

func (Engine) Init

Init needs darwin/arm64.

func (Engine) Run

func (Engine) Run(ctx context.Context, opts engine.RunOpts) error

Run is the tart equivalent of the docker agent path. With a managed VM:

  1. Acquire the VM (clone the template, or build it first if missing)
  2. Boot it and wait for the guest agent
  3. Mount the project directory via VirtioFS
  4. Run the agent through `tart exec` (like docker exec)

Two args belong to tart and are taken out of opts.Args: --force deletes the instance VM and the template first, so both are rebuilt, and --stack=<name> overrides opts.Cell.Stack. opts.Env, opts.Rebuild, opts.Update and opts.Background are not honored yet; the VM this call started is shut down when the agent exits.

type ExecSpec

type ExecSpec struct {
	Binary     string   // binary to run (e.g. "zsh", "claude")
	Flags      []string // default flags for the binary
	UserArgs   []string // user-provided args
	EnvVars    []string // KEY=VAL pairs to set in the environment
	ProjectDir string   // host project path — basename fallback for cd ~/basename
	WorkDir    string   // absolute in-VM project path (ProjectPathInVM); wins over ProjectDir
	RunAsUser  string   // if set, wrap command with sudo -u <user> -i
}

ExecSpec describes a command to run inside a tart VM via `tart exec`.

type InitConfig

type InitConfig struct {
	CellName   string
	HomeDir    string
	Stack      string
	Username   string
	Password   string
	CPUs       uint
	MemoryGB   uint64
	DiskGB     uint64
	SSHPort    uint16
	HasHostNix bool
}

InitConfig holds the parameters for a full VM initialization.

func (*InitConfig) ApplyDefaults

func (c *InitConfig) ApplyDefaults()

ApplyDefaults fills zero-value fields with sensible defaults.

func (*InitConfig) Validate

func (c *InitConfig) Validate() error

Validate checks that all required fields are set.

type NopObserver

type NopObserver struct{}

NopObserver silently discards all events.

func (NopObserver) Logf

func (NopObserver) Logf(string, ...any)

func (NopObserver) Progress

func (NopObserver) Progress(float64, string)

type Observer

type Observer interface {
	// Logf emits a debug-level message.
	Logf(format string, args ...any)
	// Progress reports a step's completion fraction (0.0–1.0) with a message.
	Progress(fraction float64, message string)
}

Observer receives progress events from long-running tart operations. Methods are called synchronously on the caller's goroutine.

type ProvisionStep

type ProvisionStep struct {
	Name          string
	Command       string
	NeedsPassword bool // true = use password auth (before SSH key is injected)
}

ProvisionStep pairs a human-readable phase name with the SSH command to run.

func BaseProvisionSteps

func BaseProvisionSteps(cfg InitConfig, pubKey string) []ProvisionStep

BaseProvisionSteps returns the stack-agnostic provisioning steps: SSH setup, sudo, home mount, nix disk prep, nix install, and store swap. These produce a base template that any stack can build on.

func ProvisionSteps

func ProvisionSteps(cfg InitConfig, pubKey string, offlineProvisioned bool) []ProvisionStep

ProvisionSteps returns the SSH provisioning commands with human-readable names. Steps before key injection use NeedsPassword=true since the generated SSH key isn't in authorized_keys yet.

When offlineProvisioned is true, SSH enablement, key injection, and sudo configuration are skipped — they were written directly to the disk image by ApplyDiskPatch. Only Nix install and home-manager activation remain.

func StackProvisionSteps

func StackProvisionSteps(cfg InitConfig) []ProvisionStep

StackProvisionSteps returns the stack-specific provisioning steps: mount nixhome and activate nix-darwin for the configured stack.

type TartGetInfo

type TartGetInfo struct {
	OS         string `json:"OS"`
	CPU        int    `json:"CPU"`
	Memory     uint64 `json:"Memory"`
	Disk       int    `json:"Disk"`
	DiskFormat string `json:"DiskFormat"`
	Size       string `json:"Size"`
	Display    string `json:"Display"`
	Running    bool   `json:"Running"`
	State      string `json:"State"`
}

TartGetInfo is the JSON output of `tart get <name> --format json`. Parallel to Docker: `docker image inspect <tag>`.

func TartGet

func TartGet(ctx context.Context, name string) (TartGetInfo, error)

TartGet runs `tart get <name> --format json` and returns the parsed info. Parallel to Docker: exec.Command("docker", "image", "inspect", tag).

type VM

type VM struct {
	Name string
	// contains filtered or unexported fields
}

VM wraps a tart VM managed via the tart CLI.

func TartRun

func TartRun(ctx context.Context, name string, dirs map[string]string, disks []string, noGraphics bool) (*VM, error)

TartRun starts a VM via `tart run`. When noGraphics is true, --no-graphics is passed (headless). When false, the native macOS display window is shown. dirs are VirtioFS shared directories (tag -> host path). disks are raw disk image paths attached as VirtIO block devices. The returned VM holds the background process; the caller must call Stop().

func (*VM) ForceStop

func (vm *VM) ForceStop() error

ForceStop kills the tart run process immediately.

func (*VM) Stderr

func (vm *VM) Stderr() string

Stderr returns any stderr output captured from the tart run process.

func (*VM) Stop

func (vm *VM) Stop() error

Stop gracefully shuts down the VM via `tart stop`.

func (*VM) WaitForIP

func (vm *VM) WaitForIP(ctx context.Context, timeout, interval time.Duration) (string, error)

WaitForIP polls `tart ip` until it returns an IP or the timeout expires.

type VMStateFunc

type VMStateFunc func() string

VMStateFunc returns the current VM state (e.g. "running", "stopped", "error"). Passed to polling functions so they can fail fast if the VM crashes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL