Skip to content

Building Vayu

This guide covers building Vayu on all platforms (macOS, Linux, Windows).

Quick Start

# Clone the repository
git clone https://github.com/athrvk/vayu.git
cd vayu

# Development build
python build.py --dev

# Start the app
cd app && pnpm run electron:dev

Prerequisites

All Platforms

  • Node.js v22+ with pnpm
  • CMake v3.25+
  • Ninja build system
  • vcpkg package manager

Platform-Specific

macOS:

xcode-select --install
brew install cmake ninja vcpkg pnpm

Linux (Ubuntu/Debian):

sudo apt update
sudo apt install build-essential cmake ninja-build git curl

# Install vcpkg
git clone https://github.com/Microsoft/vcpkg.git ~/vcpkg
cd ~/vcpkg
./bootstrap-vcpkg.sh
export VCPKG_ROOT=~/vcpkg

# Install pnpm
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install nodejs
npm install -g pnpm

Windows: - Visual Studio 2022 with "Desktop development with C++" workload - vcpkg:

git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg
cd C:\vcpkg
.\bootstrap-vcpkg.bat
- Node.js from nodejs.org - pnpm: npm install -g pnpm

Note: On Windows, the script auto-detects CMake and vcpkg bundled with Visual Studio.

Build Script Usage

The build.py script works identically on all platforms.

Basic Commands

# Show help
python build.py --help

# Development build (engine + app)
python build.py --dev

# Production build (engine + app, packaged)
python build.py

Build Options

Option Description
--dev Development build (Debug mode)
--prod Production build (Release mode, default)
-e, --engine-only Build only the C++ engine
-a, --app-only Build only the Electron app
-c, --clean Remove every generated directory before building, for whichever side is in scope: engine/build, build-release, build-test, out, and app/dist, dist-electron, release, build, plus the Vite cache and .tsbuildinfo. Dependencies (node_modules) and engine runtime data (engine/data) are left alone
-t, --tests Build and run unit tests
--test-only Run tests without rebuilding
-v, --verbose Show detailed build output
-h, --help Show help message

Examples

# Full development build
python build.py --dev

# Production build, app only (use existing engine)
python build.py -a

# Clean production build
python build.py -c

# Build engine with tests
python build.py -e -t

# Quick test iteration
python build.py --test-only

# Verbose build (for debugging)
python build.py -v

Development Workflow

1. Initial Build

python build.py --dev

This will: - Build the C++ engine in debug mode - Install app dependencies via pnpm - Set up the development environment

2. Running the App

cd app
pnpm run electron:dev

This starts: 1. Vite dev server (React app on http://localhost:5173) 2. TypeScript compiler (watches electron/ folder) 3. Electron (launches the app) 4. C++ Engine (auto-started by sidecar)

3. Making Changes

Frontend Changes (React/TypeScript): - Edit files in app/src/ - Vite will hot-reload automatically

Electron Main Process Changes: - Edit files in app/electron/ - TypeScript compiler will rebuild - Restart Electron manually

C++ Engine Changes: - Edit files in engine/src/ - Rebuild: python build.py --dev -e - Restart the Electron app

Engine rebuilds are cheaper than they look: build.py skips the CMake configure step when nothing configure-level changed, and picks up ccache and the mold/lld linkers on its own when they are installed. See Faster Rebuilds for what is detected and why.

Production Build

Building

python build.py

This will: 1. Build C++ engine in Release mode 2. Copy engine binary to app resources 3. Install app dependencies 4. Compile TypeScript for Electron 5. Build React app with Vite 6. Package with electron-builder

Output

Production builds are created in app/release/. artifactName in app/electron-builder.json is the source of truth for these names: the top-level pattern is ${productName}-${arch}.${ext}, and the mac and appImage blocks override it to insert ${version}, so the dmg, the zip and the AppImage carry a version while the exe and the deb do not.

  • macOS: Vayu-<version>-<arch>.dmg and Vayu-<version>-<arch>.zip. The zip carries the macOS install and update path - install.sh downloads it and latest-mac.yml names it - while the dmg is the drag-to-Applications route. <arch> is universal in a released build and the host arch (x64 or arm64) for a local python build.py, which has only the host-arch engine to package.
  • Windows: Vayu-<arch>.exe, the NSIS installer.
  • Linux: Vayu-<version>-x86_64.AppImage and Vayu-amd64.deb.

Testing Production Builds

macOS:

open app/release/Vayu-*.dmg
# Drag to Applications and launch

Linux:

# AppImage
chmod +x app/release/Vayu-*.AppImage
./app/release/Vayu-*.AppImage

# Debian package
sudo dpkg -i app/release/Vayu-*.deb

Windows:

.\app\release\Vayu-x64.exe

Advanced: Manual CMake Build

If you prefer to use CMake directly, the project uses CMakePresets.

Available Presets

Configure presets: - windows-dev, windows-prod - linux-dev, linux-prod - macos-dev, macos-prod

Usage

cd engine

# Configure
cmake --preset macos-prod    # or linux-prod, windows-prod

# Build
cmake --build --preset macos-prod

# Test
ctest --preset macos-prod

Every test preset runs the suite multi-process - ctest -j8 on Linux and macOS, -j4 on Windows, where the tests that open a scratch database are additionally held apart from each other because concurrent SQLite commits cost far more there. Pass -jN to override. See Engine build guide for the numbers.

Data Directories

Development

  • All platforms: engine/data/
  • Database: engine/data/db/vayu.db
  • Logs: engine/data/logs/ (engine_<stamp>.log, cli_<stamp>.log and the Electron app's own app_<stamp>.log, one per process start, newest 10 per prefix kept - see Engine Logging)
  • Lock file: engine/data/vayu.lock

Production

  • macOS: ~/Library/Application Support/vayu-client/
  • Linux: ~/.config/vayu-client/
  • Windows: %APPDATA%\vayu-client\

The directory name is app/package.json's name, which is what Electron gives app.getPath("userData") - not the product name.

Troubleshooting

Prerequisites Not Found

Problem: "Missing prerequisites" error

Solution:

macOS/Linux:

# Ensure VCPKG_ROOT is set
export VCPKG_ROOT=~/vcpkg
export PATH=$VCPKG_ROOT:$PATH

Windows: - Run from "Developer Command Prompt for VS" to use bundled tools - Or set VCPKG_ROOT environment variable - Script auto-detects Visual Studio bundled CMake and vcpkg

vcpkg Cannot Find the Pinned Baseline

Problem: every dependency fails during configure with one of

fatal: path 'versions/baseline.json' exists on disk, but not in '94a5411977...'
  while checking out baseline 94a5411977...
error: no version database entry for cpp-httplib at 0.53.0.
Available versions: 0.52.0, 0.51.0, ...

The first reads like a corrupt registry and the second like a deleted release. Both mean the same thing: your $VCPKG_ROOT clone is older than the builtin-baseline commit pinned in engine/vcpkg.json. Baseline bumps happen every release cycle, so any clone that has not been updated recently hits this.

vcpkg reads the baseline map out of the pinned commit but the per-port version database out of the worktree, which is why a bare git fetch cures the first error and lands you on the second.

Solution: build.py detects this and updates the clone itself, so the usual answer is to re-run the build. Where it cannot - a modified checkout, no network, or a vcpkg that is not a git clone - it prints the manual cure:

git -C "$VCPKG_ROOT" pull --ff-only origin master

CI is unaffected: the workflows check out the exact VCPKG_COMMIT.

Engine Fails to Start

Problem: "Failed to Start Engine" error

Solution: 1. Verify binary exists:

# Development
ls -la engine/build/vayu-engine           # macOS/Linux
dir engine\build\Debug\vayu-engine.exe    # Windows

  1. Rebuild if missing:
    python build.py --dev -e
    

Port 9876 Already in Use

Problem: "Address already in use"

Solution:

cd app
pnpm kill-ports

Build Fails

Problem: Compilation or linking errors

Solution:

# Clean rebuild with verbose output
python build.py -c -v

# Check for specific issues in the detailed output

Tests Not Found

Problem: --test-only says tests not found

Solution: Build with tests enabled first:

python build.py -e -t

CI/CD

The project uses GitHub Actions for automated builds:

  • PR Tests: .github/workflows/pr-tests.yml
  • Runs on every pull request
  • Tests engine and frontend on Linux/Windows/macOS
  • Lint, formatting and type-check run once, on their own Linux job, rather than in front of a suite they cannot affect. That job also runs ESLint over the JavaScript outside app/ (scripts/perf/*.{mjs,cjs} today), from the repository root and against its own config - it is where Node already is
  • Script lint does the same for the other two languages: shellcheck (v0.10.0) over every tracked shell script and ruff (0.15.8) over every tracked .py, both file lists taken from git ls-files so a script added later cannot escape them
  • The Windows frontend suite runs as two vitest shards on two runners; the CI gate job proves they covered every test file between them

  • Release Build: .github/workflows/release.yml

  • Triggers on version tags (v*)
  • Builds and publishes installers for all platforms
  • Uses CMakePresets and lukka actions for optimal caching

  • Sanitizers: .github/workflows/sanitizers.yml

  • Runs weekly on master (Monday 09:00 UTC) and on demand
  • ASan on Linux/Windows/macOS and TSan on Linux/macOS - five jobs, because ThreadSanitizer has no Windows implementation
  • A red leg files or updates a sanitizer-failure issue by itself, from the runner through GITHUB_TOKEN, with the run link and the sanitizer report
  • See Engine building - Sanitizers

  • Warm build cache: .github/workflows/cache-warm.yml

  • Runs on master when engine/vcpkg.json or app/pnpm-lock.yaml changes, weekly, or on demand
  • Resolves the vcpkg dependencies and populates the pnpm store for all three platforms, so both exist in the default-branch cache scope

These workflows use the same CMakePresets as local development for consistency.

Why a separate cache-warming workflow

GitHub Actions caches are scoped to the ref that created them. A run can restore caches from its own ref and from the default branch, but never from a different tag, and never from a pull request - a pull_request run writes to refs/pull/N/merge, which only re-runs of that same pull request can read.

So the caches pr-tests.yml writes are unreachable from anywhere else, and a tagged release build can only inherit from master. Nothing was building on master except CodeQL, which is Linux-only, so the macOS and Windows release legs had never had a warm dependency cache at all. cache-warm.yml exists to put the vcpkg archives in that scope for every platform.

All four caching workflows must key the cache identically or the warmed entry is unreachable from the build that needs it; cache-warm.yml has a guard job that fails if they drift - separately for the vcpkg key and for the cache-dependency-path that actions/setup-node hashes into the pnpm store key. For the same reason engine/vcpkg.json carries no version field - the key hashes that file, so bumping it per release minted a new key every time and guaranteed a cold cache exactly at release.

sccache is deliberately not warmed here. Its entries would be just as unreachable, but warming them means compiling the engine on every triggering push, and a release commit cannot use the result anyway: version.hpp reaches nearly every translation unit through core/constants.hpp, so a version bump changes the preprocessed text sccache keys on. Narrowing that include is the prerequisite; until then a warmed sccache would only help the first run of a pull request, at a recurring cost measured in tens of runner-minutes per merge.

Resources