Skip to content

Development

This guide covers building and running BarkVisor from source for local development on macOS. For Linux multi-distro hosts, use getting-started-linux.md (./scripts/linux-dev.sh installs packages + Swift and runs smoke tests).

Website (landing + docs): unified Astro app in website/ syncs these Markdown files into /docs/* (cd website && bun install && bun run dev).

RequirementMinimum versionNotes
macOS26Apple Silicon required (HVF acceleration requires arm64 host for arm64 VMs)
Xcode / SwiftSwift 6.xProject pins Swift 6.2.3 via .swift-version
BunLatestJavaScript runtime for the frontend
HomebrewLatestFor installing build and runtime deps
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
  • qemuqemu-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).

In a release install, binaries live in /usr/local/libexec/barkvisor/ and QEMU resources in /usr/local/share/barkvisor/qemu/. During development these do not exist, so BundleResolver falls back through the following search order:

  1. Installed prefix: {prefix}/libexec/barkvisor/<name>
  2. /opt/homebrew/bin/<name> (Apple Silicon Homebrew)
  3. /usr/local/bin/<name> (Intel Homebrew)
  4. $PATH lookup via which

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

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

QEMU resources (-L data dir, firmware, keymaps) follow a similar pattern:

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

The project is organized as 5 Swift Package Manager targets:

Package.swift
Sources/
BarkVisorHelperProtocol/ # Shared XPC protocol between app and helper
BarkVisorHelper/ # Privileged helper (bridge/vmnet management)
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)
BarkVisorHelperProtocol
|
+-- BarkVisorHelper (executable -- privileged helper daemon)
|
+-- 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 using the Makefile:

Terminal window
make build
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:

vite.config.ts
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:7777',
changeOrigin: true,
ws: true,
},
},
}
VariableEffect
BARKVISOR_LOG_DIROverride the log output directory (default: <dataDir>/logs)
BARKVISOR_LOG_LEVELMinimum log level: debug, info, warn, error, fatal (default: info)
DISABLE_RATE_LIMITSet to 1 to disable login rate limiting (useful for testing)
Terminal window
make lint # Run SwiftLint

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
make format # Auto-format with SwiftFormat
make format-check # Check formatting without modifying files

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
make check # Runs lint + format-check (suitable for CI)
Terminal window
swift test

Or:

Terminal window
make test

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, registry, settings, navigation, and logs.

The XPC privileged helper (BarkVisorHelper) is used for operations that require root, such as configuring bridged networking via socket_vmnet.

In debug builds, kHelperTeamID is set to "DEVELOPMENT" (defined in Sources/BarkVisorHelperProtocol/HelperProtocol.swift). The helper skips code-signing verification in this mode, so you do not need a real Apple Developer Team ID during development.

For release builds, scripts/build-release.sh injects the real APPLE_TEAM_ID via sed before compiling:

Terminal window
sed -e 's/kHelperTeamID = "DEVELOPMENT"/kHelperTeamID = "<TEAM_ID>"/' ...