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:
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:
- 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¶
This will: - Build the C++ engine in debug mode - Install app dependencies via pnpm - Set up the development environment
2. Running the App¶
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¶
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>.dmgandVayu-<version>-<arch>.zip. The zip carries the macOS install and update path -install.shdownloads it andlatest-mac.ymlnames it - while the dmg is the drag-to-Applications route.<arch>isuniversalin a released build and the host arch (x64orarm64) for a localpython build.py, which has only the host-arch engine to package. - Windows:
Vayu-<arch>.exe, the NSIS installer. - Linux:
Vayu-<version>-x86_64.AppImageandVayu-amd64.deb.
Testing Production Builds¶
macOS:
Linux:
# AppImage
chmod +x app/release/Vayu-*.AppImage
./app/release/Vayu-*.AppImage
# Debian package
sudo dpkg -i app/release/Vayu-*.deb
Windows:
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>.logand the Electron app's ownapp_<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:
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...
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:
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
- Rebuild if missing:
Port 9876 Already in Use¶
Problem: "Address already in use"
Solution:
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:
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 lintdoes 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 fromgit ls-filesso a script added later cannot escape them-
The Windows frontend suite runs as two vitest shards on two runners; the
CI gatejob 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-failureissue by itself, from the runner throughGITHUB_TOKEN, with the run link and the sanitizer report -
Warm build cache:
.github/workflows/cache-warm.yml - Runs on
masterwhenengine/vcpkg.jsonorapp/pnpm-lock.yamlchanges, 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.