Skip to content

Development

This guide is for contributors building BarkVisor from source. To use BarkVisor, install a package using the macOS, Linux, or Windows guide.

The main workflow below is for macOS. Linux and Windows commands are at the end.

Website (landing + docs): unified Astro app in website/ syncs these Markdown files into /docs/*. Run bun install, bun run sync, and bun run dev in website/. Repeat the sync after editing source Markdown.

RequirementMinimum versionNotes
macOS26Apple Silicon required (HVF acceleration requires arm64 host for arm64 VMs)
Xcode / SwiftSwift 6.3Local pin: .swift-version / mise.toml (currently 6.3.3). Linux CI/Docker package builds use the same 6.3.3 Ubuntu toolchains.
BunLatestJavaScript runtime for the frontend
HomebrewLatestFor installing build and runtime deps
mise (optional)LatestToolchain + tasks (`mise run build
Terminal window
brew install swiftlint swiftformat
  • SwiftLint — enforces code style rules (see .swiftlint.yml).
  • SwiftFormat — auto-formats Swift source (see .swiftformat).
Terminal window
brew install qemu swtpm socket_vmnet
  • qemu — qemu-system-aarch64 and associated firmware/resources.
  • swtpm — Software TPM emulator (required for Windows VMs with TPM enabled).
  • socket_vmnet — Bridged / vmnet-based networking (optional; NAT works without it).

macOS (PAS-287): Homebrew first. The pkg does not bundle QEMU or socket_vmnet.

  1. /opt/homebrew/bin/<name> (Apple Silicon Homebrew)
  2. /usr/local/bin/<name> (Intel Homebrew)
  3. Leftover {prefix}/libexec/barkvisor/<name> if an old pkg left one
  4. $PATH via which

For Homebrew opt-prefix packages (e.g. socket_vmnet):

  1. /opt/homebrew/opt/<package>/bin/<name>
  2. /usr/local/opt/<package>/bin/<name>
  3. Leftover {prefix}/libexec/barkvisor/<name>

QEMU resources (-L data dir, firmware, keymaps):

  1. /opt/homebrew/share/qemu/<name>
  2. /usr/local/share/qemu/<name>
  3. Leftover {prefix}/share/barkvisor/qemu/<name>

The pkg does not ship a privileged helper. Linux still uses distro QEMU.

The three main Swift targets are:

Package.swift
Sources/
BarkVisorCore/ # Core library: models, services, helpers (no Vapor)
BarkVisor/ # Vapor HTTP layer: controllers, middleware, routes
BarkVisorApp/ # Executable entry point (headless daemon)
Tests/
BarkVisorTests/ # Unit and integration tests
frontend/ # Vue 3 + TypeScript SPA (Vite)
BarkVisorCore (depends on: GRDB, JWTKit, Yams, NIO)
|
+-- BarkVisor (depends on: Vapor)
|
+-- BarkVisorApp (executable -- headless daemon)
PackagePurpose
Vapor 4.99+HTTP server, WebSocket, routing
GRDB 7.0+SQLite database (via DatabasePool)
JWTKit 5.0+JWT authentication
Yams 5.0+YAML parsing (cloud-init user data)
swift-nio 2.65+Async networking (VNC/console proxy)
Terminal window
swift build
# or: mise run build # release mode, see mise.toml
Terminal window
cd frontend
bun install
bun run build # production build (runs vue-tsc then vite build)

The production build output goes into frontend/dist/ and is served by the Vapor backend as a static SPA (with SPAFallbackMiddleware).

Terminal window
swift run BarkVisorApp

This starts the headless server daemon, which launches the Vapor HTTP server on 0.0.0.0:7777. Open http://localhost:7777 in a browser.

On first run the web UI presents a setup screen where you create the admin account. The data directory is at:

~/Library/Application Support/BarkVisor/

This contains the SQLite database (db.sqlite), disk images, firmware state, logs, and cloud-init data.

For frontend development with hot-reload:

Terminal window
cd frontend
bun install
bun run dev

Vite starts on http://localhost:5173 and proxies all /api requests (including WebSocket upgrades) to the backend at http://localhost:7777. Set VITE_API_TARGET to proxy to a different daemon port:

Terminal window
VITE_API_TARGET=http://127.0.0.1:50123 bun run dev

scripts/dev-instance.sh boots a detached BarkVisor daemon with a fresh, empty data directory on random free ports, provisions the admin account headlessly, and prints one JSON line an agent can consume directly (mise run instance-start works too):

Terminal window
scripts/dev-instance.sh start --seed
{"name":"default","url":"http://127.0.0.1:50190","port":50190,"pid":1234,
"dataDir":"/var/folders/…/barkvisor-dev-default.XXXX","adminUser":"admin",
"adminPass":"dev-instance-pass","seeded":true}
  • --data-dir PATH keeps state at a path you choose instead of a temp dir; custom paths are never deleted by stop.
  • --seed fills networks, disks, an API key, and an SSH key through the real API so pages have content (no QEMU involved).
  • Logs go to stderr; stdout stays pure JSON.

Drive the instance with the returned URL + admin credentials or the cached token (scripts/dev-instance.sh token), then clean up:

Terminal window
scripts/dev-instance.sh stop # kills daemon, removes temp data dir
scripts/dev-instance.sh list # list instances
scripts/dev-instance.sh clean # stop all registered throwaway instances
scripts/dev-instance.sh self-test # start → provision → seed → assert → stop
VariableEffect
BARKVISOR_PORTHTTP port, default 7777
BARKVISOR_DATA_DIRAbsolute path to an isolated data directory
BARKVISOR_FRONTEND_DIRAbsolute path to the built frontend directory
BARKVISOR_LOG_DIROverride the log output directory (default: <dataDir>/logs)
BARKVISOR_LOG_LEVELMinimum log level: debug, info, warn, error, fatal (default: info)
BARKVISOR_JOIN_CODEPairing offer on first boot only (console-local join; ignored after setup)
BARKVISOR_AUTH_MODEFront-door auth: secure (default), loopback (this computer), or disabled (whole network). Wins over Settings. loopback trusts direct local connections only — never put a reverse proxy in front of a loopback instance; use secure there.
DISABLE_RATE_LIMITSet to 1 to disable login rate limiting (useful for testing)
Terminal window
mise run lint # SwiftLint + SwiftFormat --lint
# or: swiftlint lint

SwiftLint is configured in .swiftlint.yml. Key settings:

  • Line length warning at 150, error at 200.
  • Function body length warning at 80 lines, error at 150.
  • Force unwrapping and implicitly unwrapped optionals are flagged.
  • VM is excluded from type name length rules. id, db, vm, ip, ci, fd, n, i, s are excluded from identifier name length rules.
Terminal window
swiftformat Sources/ Tests/ # apply formatting
swiftformat --lint Sources/ Tests/ # check only (also in mise run lint)

SwiftFormat is configured in .swiftformat. Key settings:

  • 4-space indentation, max line width 150.
  • Arguments and parameters wrap before-first.
  • Trailing commas are always added.
  • File headers are stripped.
Terminal window
mise run lint # suitable for CI (lint + format check)

features/ only contains Gherkin that a mapper script runs (guest-boot, api-contract, cross-device). Other behavior is covered by Swift tests or bun test.

Terminal window
swift test
# or: mise run test # full suite, same as CI Test
mise run linux-ci # Glibc compile in Docker (CI Linux Build)

The test suite includes unit tests for services, models, helpers, middleware, and controller logic. Tests are in Tests/BarkVisorTests/.

End-to-end tests use Cypress against a running BarkVisor instance:

Terminal window
cd frontend
bun run cy:open # Interactive Cypress runner
bun run cy:run # Headless Cypress run
bun run test:e2e # Alias for cy:run

E2E specs cover authentication, dashboard, VM lifecycle, disks, images, networks, settings, navigation, and logs.

Gherkin in features/guest-boot.feature maps onto the existing smoke scripts. A Device still boots a local Workload from SQLite if other Devices in the Home are unreachable.

Terminal window
mise run api-bdd # every documented API operation (fast; no QEMU)
mise run guest-smoke # blank disk → running (fast; no guest OS)
mise run guest-smoke-real # Ubuntu cloud image + cloud-init + SSH
mise run prepush-full # prepush + api-bdd + guest-smoke (operators who opt in)

mise run prepush runs lint, Swift tests, the Linux compile check, and frontend tests. Never add guest-boot to the default push gate.

ScenarioMapperRuntime
a blank-disk Workload reaches runningscripts/linux-guest-smoke.shseconds–minutes
a Linux Workload boots from a cloud image and answers SSHscripts/linux-real-guest-smoke.sh (REAL_GUEST=1)KVM/HVF: minutes; TCG: up to ~15 min (SSH_WAIT_SECS=900)

If qemu-system-aarch64 and qemu-system-x86_64 are both missing, the mapper prints SKIP: qemu-system-* is not on PATH and exits 0. Set ALLOW_NO_QEMU=1 to exercise API create-only instead of skipping.

Terminal window
DRY_RUN=1 ./scripts/guest-boot-bdd.sh # syntax + scenario inventory, no server

Out of scope here: Windows boot, Cypress.

Cross-Device Home proxy smoke (opt-in, not prepush)

Section titled “Cross-Device Home proxy smoke (opt-in, not prepush)”

Gherkin in features/cross-device.feature maps onto scripts/cross-device-smoke.sh. Two daemons on one host (two data dirs, two HTTP ports, two agent ports) pair with a real /api/pairing/codes + /api/pairing/join offer. Create + start a Workload on the member through /api/home/devices/:id/v1 and assert running from the Home proxy and on the member locally. Each Device still owns runtime in local SQLite if the peer is later unreachable.

Terminal window
mise run cross-device-smoke
DRY_RUN=1 ./scripts/cross-device-smoke.sh # syntax + endpoint inventory, no server

mise run prepush runs lint, Swift tests, the Linux compile check, and frontend tests. Never add this smoke to the default push gate.

Pairing redeem is LAN-only (not loopback). The host needs an RFC1918 address. After join the member daemon restarts so the agent plane presents the Home-issued Device certificate. Missing qemu-system-* SKIPs start after pair + create (exit 0). Set ALLOW_NO_QEMU=1 to treat create-only as the intended path.

Out of scope here: more than two Devices, auto-placement, template deploy via proxy, UI/Cypress, first-time join only.

Run mise run host-network-extra-ip for the opt-in Linux extra-IP add/remove check. On macOS, this uses Docker. It is not part of the default push gate.

BarkVisor does not ship a privileged helper. For bridged/vmnet on macOS:

Terminal window
brew install socket_vmnet

Do not sudo brew install. A root Device starts socket_vmnet via launchctl. Dev instances that are not root still need a running socket. NAT Workloads do not need that service. APPLE_TEAM_ID is still required when notarizing a release pkg, not for a helper.

From a source checkout:

Terminal window
./scripts/linux-dev.sh
source scripts/lib/linux-swift-compat.sh
barkvisor_export_swift_env
swift run BarkVisorApp

For an API-only source installation, sudo SKIP_FRONTEND=1 ./scripts/install-linux.sh skips copying the frontend and enables the agent service.

To run the development container:

Terminal window
docker build -t barkvisor:dev -f Dockerfile .
docker run --rm -it --device /dev/kvm -p 7777:7777 barkvisor:dev

Omit --device /dev/kvm to use software emulation.

Build and stage the Windows payload from the repository root:

Terminal window
cd frontend
bun install --frozen-lockfile
bun run build
cd ..
.\scripts\windows-swift.ps1 --% build -c release --product BarkVisorApp
.\scripts\stage-windows-payload.ps1 `
-SourceDir .build\release `
-FrontendDir frontend\dist `
-OutDir build\windows-payload

Install it using packaging\windows\install.ps1 -Source build\windows-payload from an administrator shell. See Building releases for the package workflows.