Build System
This document describes the HelixScreen prototype build system, including automatic patch application, multi-display support, and development workflows.
For common development tasks, see DEVELOPMENT.md - this document covers advanced build system internals.
Cross-Compilation (Embedded Targets)
Section titled “Cross-Compilation (Embedded Targets)”HelixScreen supports cross-compilation for embedded ARM targets using Docker-based toolchains. This allows building binaries for Raspberry Pi and other embedded displays directly from macOS or Linux development machines.
Quick Start
Section titled “Quick Start”# Build for Raspberry Pi 64-bit (aarch64/ARM64)make pi-docker
# Build for Raspberry Pi 32-bit (armhf/armv7l)make pi32-docker
# Build for Flashforge Adventurer 5M (armv7-a/ARM32)make ad5m-docker
# Build for Elegoo Centauri Carbon 1 (armv7-a/ARM32)make cc1-docker
# Build for Creality K1 (MIPS32, static/musl)make mips-docker # Or: make k1-docker (alias)
# Build for FlashForge AD5X (MIPS32r5, glibc)make ad5x-docker
# Build for Creality K1 series (MIPS32, dynamic/glibc)make k1-dynamic-docker
# Build for Creality K2 series (ARM, tested on K2 Max)make k2-docker
# Verify the binariesfile build/pi/bin/helix-screen # ELF 64-bit LSB, ARM aarch64file build/pi32/bin/helix-screen # ELF 32-bit LSB, ARM, EABI5file build/ad5m/bin/helix-screen # ELF 32-bit LSB, ARM, EABI5file build/cc1/bin/helix-screen # ELF 32-bit LSB, ARM, EABI5file build/mips/bin/helix-screen # ELF 32-bit LSB, MIPS32 (static)file build/ad5x/bin/helix-screen # ELF 32-bit LSB, MIPS32r5 (dynamic)file build/k1-dynamic/bin/helix-screen # ELF 32-bit LSB, MIPS32 (dynamic)file build/k2/bin/helix-screen # ELF 32-bit LSB, ARM, EABI5Docker images are automatically built on first use - no manual setup required!
Supported Targets
Section titled “Supported Targets”| Target | Command | Architecture | Display | Output Directory |
|---|---|---|---|---|
| Raspberry Pi (64-bit) | make pi-docker | aarch64 (ARM64) | DRM/fbdev | build/pi/ |
| Raspberry Pi (32-bit) | make pi32-docker | armv7-a (armhf) | DRM/fbdev | build/pi32/ |
| Adventurer 5M | make ad5m-docker | armv7-a (hard-float) | fbdev | build/ad5m/ |
| Centauri Carbon 1 | make cc1-docker | armv7-a (hard-float) | fbdev | build/cc1/ |
| Creality K1 | make mips-docker | MIPS32r2 (musl) | fbdev | build/mips/ |
| FlashForge AD5X | make ad5x-docker | MIPS32r5 (glibc) | fbdev | build/ad5x/ |
| Creality K1 (dynamic) | make k1-dynamic-docker | MIPS32r2 (glibc) | fbdev | build/k1-dynamic/ |
| Creality K2 | make k2-docker | armv7-a (musl) | fbdev | build/k2/ |
| Native (SDL) | make | Host architecture | SDL2 | build/ |
How It Works
Section titled “How It Works”-
Docker Toolchains: Each target has a Dockerfile (
docker/Dockerfile.pi,docker/Dockerfile.pi32,docker/Dockerfile.ad5m, etc.) that contains the cross-compiler, sysroot libraries, and build tools. -
Auto-Build Images: When you run
make pi-docker,make pi32-docker, ormake ad5m-docker, the build system automatically:- Checks if the Docker image exists
- Builds the image if missing (takes 2-5 minutes first time)
- Runs the cross-compilation inside the container
-
Volume Mounting: Your source code is mounted into the container, so compiled binaries appear directly in your
build/directory. -
Display Backend Selection: Cross-compilation automatically selects the appropriate display backend:
- Pi / Pi32: DRM (preferred) with fbdev fallback
- AD5M / CC1: fbdev (framebuffer)
Build Targets
Section titled “Build Targets”# Docker-based builds (recommended - no toolchain installation needed)make pi-docker # Raspberry Pi 64-bit via Docker (DRM only)make pi-all-docker # Raspberry Pi 64-bit — both DRM + fbdev (single pass)make pi32-docker # Raspberry Pi 32-bit via Docker (DRM only)make pi32-all-docker # Raspberry Pi 32-bit — both DRM + fbdev (single pass)make ad5m-docker # Adventurer 5M via Dockermake cc1-docker # Centauri Carbon 1 via Dockermake k1-docker # Creality K1 series via Docker (static/musl)make k1-dynamic-docker # Creality K1 series via Docker (dynamic/glibc)make k2-docker # Creality K2 series via Docker (tested on K2 Max)make docker-toolchains # Pre-build all Docker images
# Direct cross-compilation (requires toolchain installed on host)make pi # Raspberry Pi 64-bit (needs aarch64-linux-gnu-gcc)make pi32 # Raspberry Pi 32-bit (needs arm-linux-gnueabihf-gcc)make ad5m # Adventurer 5M (needs arm-linux-gnueabihf-gcc)make cc1 # Centauri Carbon 1 (needs arm-linux-gnueabihf-gcc)make k1 # Creality K1 static (needs Bootlin mips32el-musl toolchain)make k1-dynamic # Creality K1 dynamic (needs custom NaN2008 GCC 7.5 toolchain)make k2 # Creality K2 (needs Bootlin armv7-eabihf-musl toolchain)
# Informationmake cross-info # Show cross-compilation helpTarget Specifications
Section titled “Target Specifications”Raspberry Pi 64-bit (Mainsail OS)
Section titled “Raspberry Pi 64-bit (Mainsail OS)”- CPU: Cortex-A72/A76 (64-bit ARM)
- Toolchain:
aarch64-linux-gnu-gcc(GCC 10+) - Display: DRM preferred, fbdev fallback
- Input: libinput for touch
- Docker Image:
helixscreen/toolchain-pi(Debian Bullseye)
Raspberry Pi 32-bit (Mainsail OS)
Section titled “Raspberry Pi 32-bit (Mainsail OS)”- CPU: Cortex-A7/A53/A72 in 32-bit mode (armv7-a, hard-float + NEON)
- Toolchain:
arm-linux-gnueabihf-gcc(GCC 10+) - Display: DRM preferred, fbdev fallback
- Input: libinput for touch
- Docker Image:
helixscreen/toolchain-pi32(Debian Bullseye) - Coverage: Pi 2, 3, 4, 5 running 32-bit Raspberry Pi OS / MainsailOS
Flashforge Adventurer 5M
Section titled “Flashforge Adventurer 5M”- CPU: Cortex-A7 (32-bit ARM, hard-float)
- Toolchain:
arm-linux-gnueabihf-gcc(GCC 8.3) - Display: 800×480 framebuffer (
/dev/fb0) - Input: evdev for touch (
/dev/input/event4) - C Library: glibc 2.25 (requires older toolchain for compatibility)
- RAM: 110MB total (~36MB available with Klipper running)
- Docker Image:
helixscreen/toolchain-ad5m(Debian Buster)
Elegoo Centauri Carbon 1
Section titled “Elegoo Centauri Carbon 1”- SoC: Allwinner R528 / sun8iw20 (Cortex-A7 dual-core, armv7-a hard-float)
- Toolchain: ARM GCC 10.3 (
arm-none-linux-gnueabihf-gcc) - Display: 480×272 framebuffer (
/dev/fb0), 32bpp ARGB8888 - Input: evdev for touch (Goodix gt9xxnew_ts on
/dev/input/event1) - C Library: glibc 2.23 (static linking avoids version conflicts)
- RAM: 112MB total (~34MB available with Klipper running)
- Docker Image:
helixscreen/toolchain-cc1(Debian Bookworm)
Creality K1 Series — Static (K1C, K1 Max)
Section titled “Creality K1 Series — Static (K1C, K1 Max)”- CPU: Ingenic X2000E (MIPS32r2 dual-core @ 1.2 GHz)
- Toolchain: Bootlin
mips32el-musl(GCC 12, musl libc) - Display: 480×400 framebuffer
- Input: evdev for touch
- C Library: musl (fully static binary — no system library dependencies)
- RAM: 256MB
- Docker Image:
helixscreen/toolchain-k1(Debian Bookworm)
Creality K1 Series — Dynamic (K1C, K1 Max)
Section titled “Creality K1 Series — Dynamic (K1C, K1 Max)”- CPU: Ingenic X2000E (MIPS32r2 dual-core @ 1.2 GHz)
- Toolchain: Custom
mipsel-k1-linux-gnu-(GCC 7.5 built via crosstool-NG, NaN2008+FP64 ABI) - Display: 480×400 framebuffer
- Input: evdev for touch
- C Library: glibc 2.29 (links dynamically against K1’s native system libraries)
- Linking: Mixed — project libraries (libhv, libnl, wpa) static; system libraries (libc, libstdc++, libm, libpthread) dynamic
- RAM: 256MB
- Docker Image:
helixscreen/toolchain-k1-dynamic(custom, builds toolchain from source) - GCC 7.5 constraints: See GCC 7.5 Compatibility section above
- Why two K1 targets? Static/musl is simpler and more portable. Dynamic/glibc produces smaller binaries (shared system libs) and avoids musl edge cases, but requires the custom NaN2008 toolchain.
Creality K2 Series (K2, K2 Pro, K2 Plus, K2 Max) — Tested on K2 Max
Section titled “Creality K2 Series (K2, K2 Pro, K2 Plus, K2 Max) — Tested on K2 Max”- CPU: Allwinner sun8iw20p1 (ARM Cortex-A7, dual-core, 57 BogoMIPS)
- Toolchain: Bootlin
armv7-eabihf-musl(GCC 12, musl libc) - Display: 480x1600 fbdev on K2 Max (480x800 on other K2 models)
- Input: evdev for touch
- C Library: musl (static linking)
- RAM: ~488 MB
- Moonraker: Port 7125 (direct), port 4408 (nginx proxy)
- Docker Image:
helixscreen/toolchain-k2(Debian Bookworm) - OS: OpenWrt 21.02-SNAPSHOT, Linux 5.4.61 armv7l, procd init (NOT systemd)
- See
docs/devel/printers/CREALITY_K2_SUPPORT.mdfor full hardware details.
Dockerfile Architecture
Section titled “Dockerfile Architecture”docker/├── Dockerfile.pi # Pi 64-bit toolchain (Debian Bullseye, GCC 10)├── Dockerfile.pi32 # Pi 32-bit toolchain (Debian Bullseye, GCC 10)├── Dockerfile.ad5m # AD5M toolchain (Debian Buster, GCC 8)├── Dockerfile.cc1 # CC1 toolchain (Debian Bookworm, ARM GCC 10.3)├── Dockerfile.k1 # K1 static toolchain (Bootlin mips32el-musl, GCC 12)├── Dockerfile.k1-dynamic # K1 dynamic toolchain (crosstool-NG, GCC 7.5, glibc 2.29)└── Dockerfile.k2 # K2 toolchain (Bootlin armv7-eabihf-musl, GCC 12)The Dockerfiles handle:
- Cross-compiler installation (
crossbuild-essential-*) - Target architecture libraries (
:arm64/:armhfpackages) - SSL/crypto libraries for Moonraker WebSocket
- Environment variables for cross-compilation
Build System Integration
Section titled “Build System Integration”Cross-compilation is handled by mk/cross.mk, which defines:
# Set target platform (native, pi, pi32, ad5m, cc1, k1, k1-dynamic, k2)PLATFORM_TARGET ?= native
# Cross-compiler configurationCROSS_COMPILE := arm-linux-gnueabihf- # For AD5MCC := $(CROSS_COMPILE)gccCXX := $(CROSS_COMPILE)g++
# Target-specific flagsTARGET_CFLAGS := -march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hardTARGET_LDFLAGS := -lstdc++fs # GCC 8 requires this for std::filesystem
# Display backend selectionDISPLAY_BACKEND := fbdev # or drm, sdlGCC 7.5 Compatibility (K1 Dynamic Target)
Section titled “GCC 7.5 Compatibility (K1 Dynamic Target)”The K1 dynamic build uses a custom GCC 7.5 toolchain targeting the K1’s native glibc 2.29. GCC 7.5 only supports C++17 partially, so code must avoid certain features. This applies to all code in the codebase — even native builds should stay compatible.
What works:
- Most of C++17 (
std::optional,std::string_view, structured bindings,if constexpr, etc.) <filesystem>via the compat shim atinclude/compat/filesystem(aliasesstd::experimental::filesystem→std::filesystem)
Gotchas to avoid:
| Feature | GCC 7 Status | Workaround | Example |
|---|---|---|---|
std::from_chars (integers) | Not available | Use std::strtol / std::strtod | src/util/version.cpp |
std::atomic<time_point> | Doesn’t compile | Store as std::atomic<int64_t> (nanoseconds) | include/gcode_streaming_controller.h |
| C++20 designated initializers | Not supported ({.foo = 1}) | Initialize struct explicitly, then assign fields | src/ui/ui_fan_control_overlay.cpp |
directory_entry member functions | .is_regular_file(), .file_size(), .last_write_time() missing | Use free functions: std::filesystem::is_regular_file(entry.path()) | src/print/thumbnail_cache.cpp, src/plugin/plugin_manager.cpp |
-lstdc++fs | Required for <experimental/filesystem> | Added automatically for k1-dynamic in mk/cross.mk and mk/watchdog.mk | — |
LTO (-flto) | GCC 7.5 static toolchain lacks liblto_plugin.so | Disabled for k1-dynamic; uses plain ar/ranlib instead of gcc-ar/gcc-ranlib | mk/cross.mk |
Filesystem compat shim (include/compat/filesystem):
- For GCC < 8: includes
<experimental/filesystem>and aliases it intostd::filesystem - For GCC 8+/Clang/MSVC: passes through to the real
<filesystem>via#include_next - Activated by
-isystem include/compatin the K1 dynamic target flags
When adding new code: Always use std::filesystem::is_regular_file(path) (free function) rather than entry.is_regular_file() (member function). The free-function forms work on both GCC 7 and modern compilers.
Troubleshooting
Section titled “Troubleshooting”Docker not installed:
# macOS - Option 1: Docker Desktop (GUI)brew install --cask docker
# macOS - Option 2: Colima (lightweight, CLI-only, recommended)brew install colima dockercolima start --cpu 4 --memory 8 # Start VM with 4 cores, 8GB RAM
# Linuxsudo apt install docker.iosudo usermod -aG docker $USER # Logout/login after thisColima tips (macOS):
colima start # Start with defaultscolima start --cpu 4 --memory 8 # Custom resources (faster builds)colima stop # Stop VM when not neededcolima status # Check if runningDocker image build fails:
# Rebuild with no cachedocker build --no-cache -t helixscreen/toolchain-ad5m -f docker/Dockerfile.ad5m docker/“file format not recognized” linker error: This means a library was built for the wrong architecture. Clean and rebuild:
rm -rf build/ad5m lib/wpa_supplicant/wpa_supplicant/*.amake ad5m-dockerstd::filesystem undefined references (AD5M only):
GCC 8 requires -lstdc++fs for std::filesystem. This is already configured in mk/cross.mk for AD5M target.
Deploying to Target
Section titled “Deploying to Target”Using make targets (recommended for Pi):
# Full cycle: build + deploy + run on Pimake pi-test
# Deploy only (after building)make deploy-pi # Deploy binaries + assets, restart in backgroundmake deploy-pi-fg # Deploy and run in foreground (debug)
# Customize target (default PI_HOST is 192.168.1.113, NOT helixpi.local — which doesn't resolve)make deploy-pi PI_HOST=192.168.1.50 PI_USER=piUsing make targets for AD5M:
# Full cycle: remote build on thelio + deploy + runmake ad5m-test
# Remote build only (builds on thelio.local, fetches binaries)make remote-ad5m
# Deploy only (after building)make deploy-ad5m # Deploy binaries + assets, restart in backgroundmake deploy-ad5m-fg # Deploy and run in foreground (debug)make deploy-ad5m-bin # Deploy binaries only (fast iteration)
# Customize target (mDNS may not resolve - use IP instead)make deploy-ad5m AD5M_HOST=192.168.1.67Note: The AD5M’s mDNS (ad5m.local) may not resolve reliably. Use the IP address directly:
# Find your AD5M's IP from your router or the printer's network settingsAD5M_HOST=192.168.1.67 make deploy-ad5mManual deployment:
# Raspberry Piscp build/pi/bin/helix-screen pi@mainsailos.local:~/
# Adventurer 5M (via SSH or SD card)scp build/ad5m/bin/helix-screen root@192.168.1.x:/usr/data/Logging on Target
Section titled “Logging on Target”HelixScreen automatically detects the best logging backend:
| Platform | Default Backend | View Logs |
|---|---|---|
| Linux + systemd | journal | journalctl -t helix -f |
| Linux (no systemd) | syslog | tail -f /var/log/syslog | grep helix |
| File fallback | rotating file | tail -f /var/log/helix-screen.log |
Override via CLI:
./helix-screen --log-dest=journal # Force systemd journal./helix-screen --log-dest=file --log-file=/tmp/debug.logsystemd service: The included config/helixscreen.service automatically logs to journal. View with:
sudo journalctl -u helixscreen -fDisplay Backend Selection
Section titled “Display Backend Selection”The build system automatically selects display backends:
| Backend | Define | Libraries | Use Case |
|---|---|---|---|
| SDL | HELIX_DISPLAY_SDL | SDL2 | Desktop development |
| DRM | HELIX_DISPLAY_DRM | libdrm, libinput | Pi with KMS |
| fbdev | HELIX_DISPLAY_FBDEV | (none) | Embedded framebuffer |
Display backend is selected via DISPLAY_BACKEND in mk/cross.mk and controls:
- LVGL driver compilation (
lv_conf.hconditionals) - Display initialization in
display_backend.cpp - Input driver selection (SDL mouse, evdev touch, libinput)
Pi Dual-Link Build (Compile Once, Link Twice)
Section titled “Pi Dual-Link Build (Compile Once, Link Twice)”Pi release builds produce two binaries: DRM (GPU-accelerated) and fbdev (framebuffer fallback). Instead of compiling all ~900 source files twice, the dual-link build compiles everything once with DRM superset defines, then links two binaries with different display libraries and link flags.
This cuts Pi CI build time roughly in half (~40 min instead of 80+).
# Dual-link build (produces both DRM + fbdev in one pass)make PLATFORM_TARGET=pi-both -j # Direct (requires toolchain)make pi-all-docker # Docker (recommended)
# Individual builds still work (for development/debugging)make PLATFORM_TARGET=pi -j # DRM onlymake PLATFORM_TARGET=pi-fbdev -j # fbdev onlyHow It Works
Section titled “How It Works”-
Compile phase: All source files compile once using DRM superset defines (
-DHELIX_DISPLAY_DRM -DHELIX_DISPLAY_FBDEV -DHELIX_ENABLE_OPENGLES). Objects go tobuild/pi/obj/. -
Variant-specific compilation (only 4 files):
display_backend.cpp,display_backend_fbdev.cpp,touch_calibration.cpp→ compiled intobuild/pi/display-fbdev/without DRM defines, archived aslibhelix-display-fbdev.acrash_reporter.cpp→ compiled intobuild/pi/fbdev-variant/with-DHELIX_BINARY_VARIANT="fbdev"
-
DRM link: All objects + LVGL DRM drivers + OpenGLES objects +
-ldrm -linput -lEGL -lGLESv2 -lgbm→build/pi/bin/helix-screen -
fbdev link: Common objects (minus DRM drivers, OpenGLES objects, DRM display backend) +
libhelix-display-fbdev.a+ fbdev crash_reporter.o →build/pi-fbdev/bin/helix-screen -
Verification:
verify-fbdevautomatically checks the fbdev binary has no DRM/GLES undefined symbols.
Build Output Layout
Section titled “Build Output Layout”build/ pi/ obj/ # ALL objects (shared between both links) lib/ libhelix-display.a # DRM display library (used by splash/watchdog too) libhelix-display-fbdev.a # fbdev display library display-fbdev/ # fbdev display backend objects fbdev-variant/ # fbdev crash_reporter.o bin/ helix-screen # DRM binary helix-splash # Splash (DRM only) helix-watchdog # Watchdog (DRM only) pi-fbdev/ bin/ helix-screen # fbdev binary (linked from pi/ objects)Important Caveats for Developers
Section titled “Important Caveats for Developers”#ifdef HELIX_DISPLAY_DRMin non-display files: The shared objects are compiled with DRM defines enabled. If you add#ifdef HELIX_DISPLAY_DRMto a non-display source file, that code path will execute in the fbdev binary too. Onlydisplay_backend*.cppis recompiled for fbdev. Use runtime detection (DisplayBackend::get_type()) instead of compile-time guards for behavior that should differ between DRM and fbdev.- Adding new DRM-only sources: If you add a new source file that references DRM/GLES symbols (e.g.,
drmModeGetResources), you must add its object toLVGL_DRM_DRIVER_OBJSinmk/pi-dual-link.mkto exclude it from the fbdev link. Theverify-fbdevtarget will catch this if you forget. - The
piandpi-bothtargets sharebuild/pi/: Switching between them doesn’t trigger a clean rebuild — the arch-change detection maps both to the same build directory.
| File | Purpose |
|---|---|
mk/pi-dual-link.mk | Fbdev display lib, crash reporter variant, fbdev link rule, verify/strip targets |
mk/cross.mk | pi-both / pi32-both platform target definitions |
mk/rules.mk | Conditional all target (uses strip-both in dual-link mode) |
Git Worktrees
Section titled “Git Worktrees”Git worktrees allow parallel development on multiple branches without switching contexts. HelixScreen uses worktrees for feature development.
Creating a Worktree
Section titled “Creating a Worktree”Use setup-worktree.sh to create and configure worktrees with fast builds:
# Create worktree with new branch (one command does everything)./scripts/setup-worktree.sh feature/my-feature
# Creates at .worktrees/my-feature, builds automaticallycd .worktrees/my-feature./build/bin/helix-screen --test -vvScript Options
Section titled “Script Options”# Create at custom path./scripts/setup-worktree.sh feature/foo /tmp/helixscreen-foo
# Set up existing worktree without creating./scripts/setup-worktree.sh --setup-only feature/i18n
# Skip the initial build./scripts/setup-worktree.sh --no-build feature/quick-testWhat setup-worktree.sh Does
Section titled “What setup-worktree.sh Does”The script optimizes for fast builds by sharing artifacts from the main tree:
- Symlinks lib/ — all submodules symlinked (no clone/configure time)
- Symlinks compiled libraries —
libhv.a,libwpa_client.afrom main tree - Symlinks precompiled header —
lvgl_pch.h.gch(22MB saved) - Symlinks tools —
node_modules/,.venv/ - Clones build objects — copies
build/obj/from the main tree (APFS clonefile on macOS; plain copy on Linux) - Configures ccache for cross-worktree reuse — so the worktree builds against the same ccache the main tree populated (see below)
- Validates architecture — wrong-arch
.o/.afiles (left by a prior cross-compile) are detected and cleared somakerebuilds them correctly - Configures git —
.git/info/exclude+--skip-worktreekeepgit statusclean despite the symlinks
Trade-off: If you need to modify library code (lib/), un-symlink that specific directory first (rm lib/<name> && cp -a $MAIN/lib/<name> lib/).
Why worktree builds are fast (and the ccache config the script sets)
Section titled “Why worktree builds are fast (and the ccache config the script sets)”A fresh git worktree add stamps every source file with the current mtime, so make sees all sources as newer than the cloned objects and wants to recompile the whole tree. The cloned build/obj/ therefore does not, by itself, save you on Linux — the real speedup comes from ccache: those “recompiles” become near-instant cache hits instead of cold compiles.
But ccache only helps across worktrees if it’s configured for it. The native build compiles with -g (debug info), and ccache’s default hash_dir=true folds the absolute working directory into the cache key — so an object cached while building in the main tree never matches the same source compiled under .worktrees/<name>/. Every worktree would start stone cold.
setup-worktree.sh fixes this once, by writing global ccache config (only when unset, so it never clobbers a value you chose):
| ccache setting | Set to | Why |
|---|---|---|
base_dir | $HOME | Rewrites absolute paths under $HOME to relative before hashing, so main-tree and worktree paths collapse to the same key |
hash_dir | false | Stops folding the cwd (the -g debug-path component) into the key |
max_size | 25G (raised, never lowered) | The default 5 GiB thrashes once several worktrees + cross-compile caches share it, re-causing cold misses |
For the script’s own initial build it also exports CCACHE_BASEDIR (the longest common ancestor of the main tree and the worktree, so it works even for out-of-tree paths like /tmp/foo) and CCACHE_NOHASHDIR=1.
Caveat:
hash_dir=falseis global, so cached objects carry whicheverDW_AT_comp_dir(debug source path) compiled them first. For throwaway dev worktrees this is cosmetic, but gdb inside a worktree may point at the main-tree paths. If you do serious in-worktree debugging, build that target withCCACHE_DISABLE=1.
Verify the cache is actually being shared after a build:
ccache -s # "Hits" should climb sharply on the 2nd+ worktree buildccache -p | grep -E 'base_dir|hash_dir|max_size'Typical worktree workflow
Section titled “Typical worktree workflow”# 1. Spin up an isolated workspace for a feature (builds automatically)./scripts/setup-worktree.sh feature/my-featurecd .worktrees/my-feature
# 2. Iterate — XML-only changes need no rebuild (loaded at runtime)HELIX_HOT_RELOAD=1 ./build/bin/helix-screen --test -vv# ...C++ changes:make -j && ./build/bin/helix-screen --test -vv
# 3. Run the relevant tests before committingmake test-run
# 4. Commit in the worktree (it's a normal checkout on its own branch)git add -A && git commit -m "feat(scope): ..."
# 5. When done, merge/push from the worktree, then tear it down (see Cleanup)If a worktree already exists but its symlinks/objects drifted (e.g. after a git submodule update in the main tree), re-run setup in place — it’s idempotent:
cd .worktrees/my-feature && ../../scripts/setup-worktree.sh --setup-only --no-build .# (or just `../../scripts/setup-worktree.sh` with no args from inside the worktree —# it auto-detects the branch and path)Managing Worktrees
Section titled “Managing Worktrees”# List existing worktreesgit worktree list
# Example output:# /Users/you/code/helixscreen abc1234 [main]# /Users/you/code/helixscreen/.worktrees/i18n def5678 [feature/i18n]Cleanup
Section titled “Cleanup”# Remove a worktreegit worktree remove .worktrees/my-feature
# Or force remove if dirtygit worktree remove --force .worktrees/my-featureBuild System Overview
Section titled “Build System Overview”The project uses GNU Make with a modular architecture:
- Modular design: ~4,300 lines split across 14 files for maintainability
- Color-coded output for easy visual parsing
- Verbosity control to show/hide full compiler commands
- Automatic dependency checking before builds with smart canvas detection
- Interactive installation of missing dependencies (
make install-deps) - Automatic code formatting for C/C++ and XML files
- Fail-fast error handling with clear diagnostics
- Parallel build support with output synchronization
- Build timing for performance tracking
Modular Makefile Structure
Section titled “Modular Makefile Structure”The build system is organized into focused modules:
| File | Lines | Purpose |
|---|---|---|
Makefile | ~630 | Configuration, variables, platform detection, module includes |
mk/tests.mk | ~880 | All test targets (unit, integration, by-feature) |
mk/cross.mk | ~750 | Cross-compilation, toolchain setup, display backends |
mk/deps.mk | ~500 | Dependency checking, installation, libhv/wpa_supplicant |
mk/rules.mk | ~340 | Compilation rules, linking, main build targets |
mk/remote.mk | ~280 | Remote deployment (Pi, AD5M) |
mk/images.mk | ~200 | Image conversion (PNG, SVG) |
mk/patches.mk | ~130 | LVGL patch application |
mk/fonts.mk | ~120 | Font/icon generation, Material icons |
mk/watchdog.mk | ~120 | Hardware watchdog support |
mk/format.mk | ~110 | Code and XML formatting |
mk/splash.mk | ~110 | Splash screen generation |
mk/tools.mk | ~110 | Development tool targets |
mk/display-lib.mk | ~60 | Display library configuration |
mk/pi-dual-link.mk | ~200 | Pi dual-link build (compile once, link DRM + fbdev) |
Each module is self-contained with GPL-3 copyright headers and clear separation of concerns.
Quick Start
Section titled “Quick Start”# Parallel build (auto-detects CPU cores)make -j
# Fast development build (-O0, ~2x faster compilation)make dev
# Clean parallel build with progress/timingmake build
# Verbose mode (shows full commands)make V=1
# Code formatting (clang-format for C/C++, xmllint for XML)make format # Format all filesmake format-staged # Format only staged files
# Dependency checking (comprehensive)make check-deps
# Auto-install missing dependencies (interactive)make install-deps
# Help (shows all targets and options)make help
# Apply patches manually (usually automatic)make apply-patches
# IDE/LSP support (auto-generated after builds, or manually)make compile_commands # Merge existing fragments (~1-2s)Build Configuration Options
Section titled “Build Configuration Options”The build system supports several configuration flags to customize the build:
Verbosity Control (default: quiet)
# Quiet mode (default) - shows progressmake -j
# Verbose mode - shows full compiler commandsmake -j V=1Dependency Management
Section titled “Dependency Management”The build system includes comprehensive dependency checking and automatic installation.
Checking Dependencies
Section titled “Checking Dependencies”make check-depsThis checks for:
- System tools: C/C++ compiler, cmake, make, python3, npm
- Code formatters: clang-format (C/C++), xmllint (XML validation/formatting)
- Libraries: pkg-config
- Canvas dependencies: cairo, pango, libpng, libjpeg, librsvg (for lv_img_conv)
- npm packages: lv_font_conv, lv_img_conv
- Optional libraries: SDL2, spdlog, libhv (uses system if available, otherwise builds from submodules)
- Git submodules: LVGL (always built from submodule)
The checker is platform-aware and shows the correct install commands for:
- macOS (Homebrew)
- Debian/Ubuntu (apt)
- Fedora/RHEL (dnf)
Installing Dependencies
Section titled “Installing Dependencies”make install-depsThis interactively installs missing dependencies:
- Detects your platform
- Lists packages to be installed
- Shows the command it will run
- Asks for confirmation before proceeding
- Installs system packages via brew/apt/dnf
- Runs
npm installfor lv_font_conv/lv_img_conv - Initializes git submodules if needed
Smart Canvas Detection: Uses pkg-config to detect exactly which canvas libraries are missing and only installs what’s needed.
Automatic Builds: Git submodules (libhv, wpa_supplicant, spdlog) are built automatically by the main build system when missing - no manual intervention needed.
Library Clean Targets
Section titled “Library Clean Targets”Individual clean targets are available for forcing rebuilds of specific libraries without a full make clean. This is useful when:
- Switching between native and cross-compilation
- Build flags have changed
- Debugging library-specific issues
make libhv-clean # Clean libhv WebSocket library artifactsmake sdl2-clean # Clean SDL2 CMake build directorymake lvgl-clean # Clean LVGL compiled objectsmake libs-clean # Clean all library artifacts at onceCross-Compilation Note: When cross-compiling (e.g., make ad5m-docker), libhv is automatically cleaned before each build to prevent architecture mixing. This adds ~5 seconds but ensures correct builds.
Test Harness
Section titled “Test Harness”The dependency system includes a comprehensive test suite:
./tests/test_deps.shTests 9 scenarios with 22 assertions covering dependency detection, platform-specific commands, and auto-installation workflow.
Build Options
Section titled “Build Options”V=1- Verbose mode: shows full compiler commands instead of short[CC]/[CXX]tagsOPT=0|1|2- Optimization level (default: 2). UseOPT=0for fastest compilation,OPT=2for release.make devis shorthand forOPT=0 -j.JOBS=N- Set parallel job count (default: auto-detects CPU cores)NO_COLOR=1- Disable colored output (useful for CI/CD)-j<N>- Enable parallel builds with N jobs (NOT auto-enabled by default)
Build Output
Section titled “Build Output”The build system uses color-coded tags:
[CC](cyan) - Compiling C sources (LVGL)[CXX](blue) - Compiling C++ sources (app code)[FONT](green) - Compiling font assets[ICON](green) - Compiling icon assets[LD](magenta) - Linking binary✓(green) - Success messages✗(red) - Error messages⚠(yellow) - Warning messages
Error Handling
Section titled “Error Handling”When compilation fails, the build system:
- Shows the failed file with a red
✗marker - Displays the full compiler command for debugging
- Exits immediately (fail-fast behavior)
Example:
[CXX] src/ui_panel_home.cpp✗ Compilation failed: src/ui_panel_home.cppCommand: clang++ -std=c++17 -Wall -Wextra -O2 -g -I. -Iinclude ...Code Formatting
Section titled “Code Formatting”The build system includes automatic code formatting for C/C++ and XML files, integrated with pre-commit hooks.
Formatters
Section titled “Formatters”- clang-format - Formats C, C++, and Objective-C files according to
.clang-formatconfig - xmllint - Formats and validates XML layout files with consistent indentation
Configuration
Section titled “Configuration”.clang-format (LLVM-based with project customizations):
- Indentation: 4 spaces, no tabs
- Line length: 100 characters
- Braces: K&R style (same line)
- Pointers: Left-aligned (
int* ptr) - Includes: Auto-sorted with grouping (project → external → system)
Formatting Commands
Section titled “Formatting Commands”# Format all C/C++ and XML filesmake format
# Format only staged files (useful before commit)make format-staged
# Check formatting without modifying files./scripts/quality-checks.shPre-commit Integration
Section titled “Pre-commit Integration”Formatting is automatically checked by the pre-commit hook (.git/hooks/pre-commit), which calls scripts/quality-checks.sh --staged-only:
- Checks staged files for formatting issues
- Reports files that need formatting
- Prevents commit if formatting issues are found
- Suggests fix: Run
make format-stagedorclang-format -i <file>
To bypass (not recommended):
git commit --no-verifyQuality Checks
Section titled “Quality Checks”The scripts/quality-checks.sh script runs multiple checks:
- Code formatting (clang-format)
- XML formatting (xmllint)
- XML validation (xmllint —noout)
- Copyright headers (GPL v3 SPDX identifiers)
- Merge conflict markers
- Trailing whitespace
- Build verification (pre-commit only)
Used by both:
- Pre-commit hook (staged files only)
- CI/CD (all files)
Automatic Patch Application
Section titled “Automatic Patch Application”The build system automatically applies patches to git submodules before compilation.
How It Works
Section titled “How It Works”- Patch Storage: All submodule patches are stored in
patches/(in the repository root) - Auto-Detection: Makefile checks if patches are already applied before each build
- Idempotent: Safe to run multiple times - patches are only applied once
- Transparent: No manual intervention needed for normal development
Patch: LVGL SDL Window Position
Section titled “Patch: LVGL SDL Window Position”File: patches/lvgl_sdl_window_position.patch
Purpose: Adds multi-display support to LVGL 9’s SDL driver by reading environment variables.
Environment Variables:
HELIX_SDL_DISPLAY- Display number (0, 1, 2…) to center window onHELIX_SDL_XPOS- X coordinate for exact window positionHELIX_SDL_YPOS- Y coordinate for exact window position
Application Logic (in Makefile):
apply-patches: @echo "Checking LVGL patches..." @if git -C $(LVGL_DIR) diff --quiet src/drivers/sdl/lv_sdl_window.c; then \ # File is clean, apply patch git -C $(LVGL_DIR) apply ../patches/lvgl_sdl_window_position.patch else \ # File already modified (patch applied) echo "✓ LVGL SDL window position patch already applied" fiStatus Messages:
✓ Patch applied successfully- Patch was applied during this build✓ LVGL SDL window position patch already applied- Patch was already present⚠ Cannot apply patch (already applied or conflicts)- Manual intervention needed
Adding New Patches
Section titled “Adding New Patches”To add a new submodule patch:
- Make changes in the submodule directory
- Generate patch:
Terminal window cd lib/lvglgit diff > ../../patches/my-new-patch.patch - Update Makefile to apply the patch in the
apply-patchestarget - Document in
patches/README.md
Patch Gotchas (hard-won)
Section titled “Patch Gotchas (hard-won)”Two traps cost a full debugging session on 2026-06-14 when the libhv DNS resolver
fallback patch silently stopped reaching the binary on the AD5M (on-machine
update checks failed with “Connection failed”). Both are now regression-tested in
tests/shell/test_libhv_dns_resolver_patch.bats.
-
Guard on the actual change, not on a side effect. A patch that adds NEW files and edits an existing one must not gate re-application on the new file’s existence. The old guard used
[ ! -f base/dns_resolv.c ]; a submodule reset reverted the trackedbase/hsocket.c(the wiring) but left the untrackeddns_resolv.corphaned, so the guard declared “already applied” and never re-wiredhsocket.c. Result: resolver compiled but never called. Guard on a marker string inside the edited file and self-heal:if ! grep -q "dns_resolv_resolve" "$(LIBHV_DIR)/base/hsocket.c"; thenrm -f .../base/dns_resolv.c .../base/dns_resolv.h; # drop orphansgit -C $(LIBHV_DIR) checkout -- base/hsocket.c; # pristinegit -C $(LIBHV_DIR) apply .../libhv-dns-resolver-fallback.patchfi -
A patched file compiled into a static
.amust invalidate that.a.$(LIBHV_LIB)(build//lib/libhv.a) originally had no prerequisites → built once, never rebuilt when a patch changed the libhv source. Because the resolver call site lives only in hsocket.c(→ insidelibhv.a) whiledns_resolv.cis compiled separately into the app, a stale archive kept a pristinehsocket.o(puregetaddrinfo→EAI_SYSTEM/ret=-11on static glibc) across every rebuild. This is dev-only — a fresh CI build has nolibhv.ayet — but the fix is to depend on the stamp:$(LIBHV_LIB): $(PATCHES_STAMP)When in doubt,
rm build/<plat>/lib/libhv.ato force a clean archive, and confirm a patch’s marker actually made it in:strings <binary> | grep <sym>.
Multi-Display Support (macOS)
Section titled “Multi-Display Support (macOS)”The prototype supports multi-monitor development workflows with automatic window positioning.
Command Line Arguments
Section titled “Command Line Arguments”# Display-based positioning (centered)./build/bin/helix-screen --display 0 # Main display./build/bin/helix-screen --display 1 # Secondary display./build/bin/helix-screen -d 2 # Third display (short form)
# Exact pixel coordinates./build/bin/helix-screen --x-pos 100 --y-pos 200./build/bin/helix-screen -x 1500 -y -500 # Works with negative Y (display above)
# Combined with other options./build/bin/helix-screen -d 1 -s small --panel homeImplementation Details
Section titled “Implementation Details”Flow:
main.cppparses command line arguments- Sets environment variables before LVGL initialization:
setenv("HELIX_SDL_DISPLAY", "1", 1); // For --display 1// orsetenv("HELIX_SDL_XPOS", "100", 1); // For --x-pos 100setenv("HELIX_SDL_YPOS", "200", 1); // For --y-pos 200
- LVGL SDL driver reads environment variables during window creation
- Uses
SDL_GetDisplayBounds()to query display geometry - Calculates center position:
display_x + (display_w - window_w) / 2 - Calls
SDL_SetWindowPosition()after window creation (fixes macOS quirks)
Source Files:
src/main.cpp- Argument parsing and environment setup (lines 218-220, 385-401)lvgl/src/drivers/sdl/lv_sdl_window.c- Window positioning logic (patch)
Screenshot Script Integration
Section titled “Screenshot Script Integration”The scripts/screenshot.sh script automatically uses display positioning:
# Default: opens on display 1 (keeps terminal visible on display 0)./scripts/screenshot.sh helix-screen output-name panel
# Override displayHELIX_SCREENSHOT_DISPLAY=0 ./scripts/screenshot.sh helix-screen output panelHow it works:
# In screenshot.shHELIX_SCREENSHOT_DISPLAY=${HELIX_SCREENSHOT_DISPLAY:-1} # Default to display 1EXTRA_ARGS="--display $HELIX_SCREENSHOT_DISPLAY $EXTRA_ARGS"This ensures the UI window appears on a different display from the terminal, making it easier to monitor build output and screenshots simultaneously.
Parallel Compilation
Section titled “Parallel Compilation”Important: Parallel builds are NOT enabled by default. Use -j flag explicitly.
Platform Detection
Section titled “Platform Detection”UNAME_S := $(shell uname -s)ifeq ($(UNAME_S),Darwin) # macOS NPROC := $(shell sysctl -n hw.ncpu 2>/dev/null || echo 4) PLATFORM := macOSelse # Linux NPROC := $(shell nproc 2>/dev/null || echo 4) PLATFORM := Linuxendifmake -j # Auto-detect CPU cores and parallelize (recommended)make -j16 # Explicit job count (current system has 16 cores)make JOBS=16 # Set job count via variablemake build # Clean parallel build (auto-detects cores)The build system uses --output-sync=target to prevent interleaved output during parallel builds.
Font Generation
Section titled “Font Generation”The build system uses lv_font_conv to convert TrueType fonts into LVGL-compatible C arrays.
Font Types
Section titled “Font Types”Material Design Icons (MDI):
- Source:
scripts/regen_mdi_fonts.sh(single source of truth) - Font:
assets/fonts/materialdesignicons-webfont.ttf - Output:
mdi_icons_16.c,mdi_icons_24.c,mdi_icons_32.c,mdi_icons_48.c,mdi_icons_64.c - Codepoint mapping:
include/ui_icon_codepoints.h
Noto Sans Text Fonts:
- Source:
package.jsonnpm scripts - Font:
assets/fonts/NotoSans-Regular.ttf,NotoSans-Bold.ttf - Output:
noto_sans_*.c,noto_sans_bold_*.c
How It Works
Section titled “How It Works”MDI icon fonts are regenerated when scripts/regen_mdi_fonts.sh changes. The build system uses Make’s dependency tracking to only regenerate when needed.
Automatic regeneration:
make # Checks fonts and regenerates if regen script is newerManual regeneration:
make regen-fonts # Regenerate MDI icon fonts from regen scriptmake generate-fonts # Explicit font regenerationAdding New Icon Glyphs
Section titled “Adding New Icon Glyphs”To add new Material Design Icons:
- Find the icon at https://pictogrammers.com/library/mdi/
- Get the codepoint (e.g.,
wifi-strength-4=0xF0928) - Edit
scripts/regen_mdi_fonts.shand add the codepoint:Terminal window MDI_ICONS+=",0xF0928" # wifi-strength-4 - Add to codepoints header (
include/ui_icon_codepoints.h):{"wifi_strength_4", "\xF3\xB0\xA4\xA8"}, // F0928 wifi-strength-4 - Regenerate fonts:
Terminal window make regen-fontsmake -j
Requirements
Section titled “Requirements”- Node.js and npm - Required for font generation
- macOS:
brew install node - Ubuntu/Debian:
sudo apt install npm - Fedora/RHEL:
sudo dnf install npm
- macOS:
- lv_font_conv - Installed automatically via
npm install(seepackage.jsondevDependencies)
Troubleshooting
Section titled “Troubleshooting”npm not found:
# macOSbrew install node
# Linuxsudo apt install npm # Debian/Ubuntusudo dnf install npm # Fedora/RHEL
# Verifynpm --versionFonts not regenerating:
# Force regeneration by touching the regen scripttouch scripts/regen_mdi_fonts.shmake generate-fontsMissing icons:
# Validate all icons in codepoints.h are in the fontmake validate-fontsManual font generation:
# Generate specific sizenpm run convert-font-24
# Generate all fontsnpm run convert-all-fontsIcon Generation
Section titled “Icon Generation”The build system includes automated icon generation with platform-specific output formats.
Quick Start
Section titled “Quick Start”# Generate/regenerate icon from source logomake iconOutput:
- macOS:
helix-icon.icns(multi-resolution bundle) +helix-icon.png(650x650) - Linux:
helix-icon.png(650x650 for application use)
Requirements
Section titled “Requirements”Required:
imagemagick- Image processing (magickcommand)- macOS:
brew install imagemagick - Ubuntu/Debian:
sudo apt install imagemagick - Fedora/RHEL:
sudo dnf install ImageMagick
- macOS:
macOS only:
iconutil- macOS icon bundle creator (built-in on macOS)
What It Does
Section titled “What It Does”The make icon target performs the following steps:
All platforms:
- Crops source logo (
assets/images/helixscreen-logo.png) to just the circular helix - Creates square icon at 650x650px with transparent background →
helix-icon.png
macOS only (additional steps): 3. Generates 12 resolutions:
- Standard: 16x16, 32x32, 64x64, 128x128, 256x256, 512x512
- Retina (@2x): 32x32, 64x64, 128x128, 256x256, 512x512, 1024x1024
- Bundles into .icns file using
iconutil→helix-icon.icns - Cleans up temporary iconset directory
Generated Files
Section titled “Generated Files”All platforms:
assets/images/helix-icon.png- Cropped square logo (650x650px, ~245KB)
macOS only:
assets/images/helix-icon.icns- macOS icon bundle (~1.3MB with all resolutions)assets/images/icon.iconset/- Temporary directory (auto-deleted after .icns creation)
Cross-platform:
- SDL window icons (programmatically via
SDL_SetWindowIcon()with PNG) - Linux
.desktopfiles (Icon= field pointing to PNG)
macOS specific:
- macOS
.appbundles withInfo.plist(CFBundleIconFile pointing to .icns) - Dock/Finder display when bundled as application
Troubleshooting
Section titled “Troubleshooting”ImageMagick not found (macOS):
brew install imagemagickmake iconImageMagick not found (Linux):
# Ubuntu/Debiansudo apt install imagemagick
# Fedora/RHELsudo dnf install ImageMagickLinux output:
- Linux builds generate PNG only (not .icns)
- This is expected -
.icnsis macOS-specific - PNG icons work with SDL and Linux desktop environments
Regenerating after logo changes:
# Update assets/images/helixscreen-logo.pngmake icon # Regenerates all icon files (platform-specific)SVG to PNG Conversion
Section titled “SVG to PNG Conversion”When converting SVG files to PNG for use in the project, always use rsvg-convert from the librsvg library.
Why Not ImageMagick?
Section titled “Why Not ImageMagick?”ImageMagick’s SVG renderer doesn’t correctly handle certain SVG features (transforms, filters, complex paths). This produces corrupted output—often solid white/black rectangles instead of the intended graphics.
Using rsvg-convert
Section titled “Using rsvg-convert”# Single file at specific size:rsvg-convert logo.svg -w 64 -h 64 -o logo_64.png
# Batch convert all SVGs in a directory:for svg in *.svg; do name="${svg%.svg}" rsvg-convert "$svg" -w 64 -h 64 -o "${name}_64.png"done
# Common size options:rsvg-convert input.svg -w 128 -h 128 -o output.png # By pixel dimensionsrsvg-convert input.svg --dpi-x 192 --dpi-y 192 -o output.png # By DPIInstallation
Section titled “Installation”The librsvg package is already tracked as a dependency for lv_img_conv. Install with:
# macOSbrew install librsvg
# Debian/Ubuntusudo apt install librsvg2-bin
# Fedora/RHELsudo dnf install librsvg2-toolsCurrent Usage
Section titled “Current Usage”- AMS logos (
assets/images/ams/) - Multi-material system icons converted from SVG sources
Make Target Reference
Section titled “Make Target Reference”The Makefile is self-documenting — these help targets are the authoritative, always-current list (the tables below are a curated tour of the typical ones):
| Command | Shows |
|---|---|
make help | The common build/dependency/quality targets |
make help-build | Build, dependency, and patch targets |
make help-test | Test targets and test discovery |
make help-cross | Cross-compilation + per-device deployment targets and options |
make help-remote | Remote build system (build on a fast host, fetch binaries) |
make help-images / help-splash / help-watchdog | Asset/splash/watchdog targets |
make help-all | Everything, all topics combined |
make cross-info | Current cross-compile configuration (platform, backend) |
Native build & run
Section titled “Native build & run”| Target | What it does |
|---|---|
make -j (all) | Build the main binary (default), -O2, auto-parallel |
make dev | Fast build at -O0 (~2× faster compile; larger/slower binary) |
make OPT=1 -j | -O1 middle ground |
make build | Clean build with progress + timing |
make run | Build and run the UI |
make clean | Remove build artifacts (keeps deps) |
make distclean | Deep clean to fresh-checkout state |
make V=1 … | Verbose (show full compiler commands) |
make JOBS=N … | Cap parallel job count |
Run flags worth knowing: ./build/bin/helix-screen --test -vv (mock printer + DEBUG), HELIX_HOT_RELOAD=1 … (live XML reload).
The build system has 30+ test targets by feature area; see TESTING.md for the tag taxonomy. Most-used:
| Target | What it does |
|---|---|
make test | Build tests (does not run them) |
make test-run | Build and run tests in parallel (recommended, ~4–8× faster) |
make test-serial | Run sequentially (debugging thread issues) |
make test-smoke | Quick smoke subset (~30s) for rapid iteration |
make test-all | All tests incl. [slow] |
make test-asan / test-tsan | Run under Address/Thread sanitizer |
make test-list-tags | List available tags |
./build/bin/helix-tests "[tag]" | Run a specific tag (e.g. [ams], [gcode]) |
Code quality & IDE
Section titled “Code quality & IDE”| Target | What it does |
|---|---|
make format | clang-format all C/C++ + xmllint XML |
make format-staged | Format only staged files (pre-commit) |
make quality | All quality checks (formatting, headers, conflicts) |
make setup-hooks | Enable the git pre-commit hook |
make compile_commands | Merge compile_commands.json for clangd (~1–2s) |
make compile_commands_full | Full regen via bear/compiledb (slow; use if fragments corrupt) |
Dependencies & patches
Section titled “Dependencies & patches”| Target | What it does |
|---|---|
make check-deps | Verify build dependencies (see below) |
make install-deps | Interactively install missing deps |
make venv-setup | Create .venv with Python asset/telemetry deps |
make libs-clean | Clean all built library artifacts |
make apply-patches | Apply LVGL/libhv patches (idempotent; auto-run before builds) |
make reapply-patches | Force re-apply (repair manually-edited patched files) |
Asset regeneration
Section titled “Asset regeneration”Usually invoked after editing icons/images. See Font Generation and Icon Generation for the full pipeline.
| Target | What it does |
|---|---|
make regen-fonts | Regenerate MDI icon fonts from codepoints.h |
make regen-text-fonts | Regenerate Noto Sans text fonts (incl. CJK) |
make regen-icon-consts | Regenerate icon string constants in globals.xml |
make validate-fonts | Verify every codepoint is present in the compiled fonts |
make regen-images | Regenerate pre-rendered splash images (all sizes) |
make gen-printer-images | Pre-render printer DB images |
make translations | Regenerate translation tables from YAML |
make translation-coverage | Show per-language translation coverage |
Cross-compile, deploy & remote build
Section titled “Cross-compile, deploy & remote build”These get their own deep section above — see Cross-Compilation. Shape of it:
- Build:
make <target>-docker(recommended, no local toolchain) ormake <target>(needs host toolchain). Targets:pi,pi32,ad5m,ad5x,cc1,k1,k1-dynamic,k2,snapmaker-u1,x86. - Deploy + run on device:
make <target>-test(build + deploy + run fg),make deploy-<target>(background),deploy-<target>-fg(foreground),deploy-<target>-bin(binaries only, fast iteration),<target>-ssh. - Host override:
make deploy-pi PI_HOST=192.168.1.50. Defaults live inmk/cross.mk— notePI_HOSTactually defaults to192.168.1.113(themake help-crosstext sayinghelixpi.localis stale, andhelixpi.localdoes not resolve).K2_HOSThas no default and must be supplied. - Remote build:
make remote-pi/remote-ad5m/remote-nativebuild on a fast Linux host (REMOTE_HOST, defaultthelio.local) and fetch the binaries back.make remote-statuschecks readiness.
Utilities
Section titled “Utilities”| Target | What it does |
|---|---|
make demo | Build LVGL demo widgets (LVGL API testing) |
make symbols | Extract .sym + .debug (crash-backtrace resolution) |
make strip | Strip the binary for release |
make screenshots | Generate documentation screenshots |
make print-cxxflags / print-ldflags | Dump resolved flags (build debugging) |
Dependency Checking
Section titled “Dependency Checking”Before building, the system automatically checks for required dependencies:
Required:
clang/clang++- C/C++ compiler with C++17 supportcmake- Build system for SDL2 when building from submodule (version 3.16+)- Git submodules:
lvgl,wpa_supplicant(auto-built by build system)
Optional (uses system if available, otherwise builds from submodules):
sdl2,spdlog,libhv- Auto-detected and built only if not system-installed
Optional:
compiledborbear- Only forcompile_commands_full(normal builds auto-generate)imagemagick- For screenshot conversion and icon generationiconutil- For macOS .icns icon generation (macOS only, built-in)
Manual Dependency Check
Section titled “Manual Dependency Check”make check-depsExample output:
Checking build dependencies...✓ clang found: Apple clang version 17.0.0✓ clang++ found: Apple clang version 17.0.0✓ SDL2: Using system version 2.32.10✓ cmake found: cmake version 3.30.5✓ libhv: Using submodule version✓ spdlog: Using submodule version (header-only)✓ LVGL found: lvgl
All dependencies satisfied!If dependencies are missing, the check provides installation instructions.
IDE/LSP Support (compile_commands.json)
Section titled “IDE/LSP Support (compile_commands.json)”The build system uses incremental compile command generation for fast IDE integration.
How It Works
Section titled “How It Works”- During compilation: Each
.ofile generates a.ccj(compile command JSON) fragment alongside it - After build: Fragments are automatically merged into
compile_commands.json - Adding new files: Just compile them - fragments are created automatically
This replaces the slow compiledb make -n -B approach (which did a full dry-run) with instant merges.
# Normal workflow - compile_commands.json is auto-updated after every buildmake -j
# Manual merge (if you want to update without building)make compile_commands # ~1-2 seconds for ~1000 files
# Full regeneration (slow, use only if fragments are corrupted)make compile_commands_fullFragment Storage
Section titled “Fragment Storage”- Fragments are stored as
.ccjfiles next to.ofiles inbuild/obj/ - They’re automatically cleaned with
make clean - They’re gitignored (inside
build/)
Troubleshooting
Section titled “Troubleshooting”compile_commands.json has missing entries:
# Ensure all targets are builtmake -j && make test-buildmake compile_commandsJSON validation errors:
# Check if JSON is validpython3 -m json.tool compile_commands.json > /dev/null
# If corrupted, do a full regenerationmake compile_commands_fullDependency Management
Section titled “Dependency Management”Git Submodules
Section titled “Git Submodules”The project uses git submodules for external dependencies:
lvgl- LVGL 9.5 graphics library (with automatic patches)libhv- HTTP/WebSocket client library (auto-built)spdlog- Logging librarywpa_supplicant- WiFi control (Linux only, auto-built)
Additionally, lib/helix-xml/ contains the extracted XML engine (originally from LVGL 9.4, MIT licensed). This is not a submodule — it lives directly in the repository with XML patches baked in permanently.
Automatic handling: Submodule dependencies are built automatically when missing. Patches are applied automatically before builds. Never commit changes directly to submodules - always create patches instead.
SDL2 is a system dependency installed via package manager:
# macOSbrew install sdl2
# Debian/Ubuntusudo apt install libsdl2-dev
# Fedora/RHELsudo dnf install SDL2-develThe Makefile uses sdl2-config to auto-detect paths:
SDL2_CFLAGS := $(shell sdl2-config --cflags)SDL2_LIBS := $(shell sdl2-config --libs)Troubleshooting
Section titled “Troubleshooting”Patch Application Fails
Section titled “Patch Application Fails”Symptom: ⚠ Cannot apply patch (already applied or conflicts)
Causes:
- Submodule was manually modified (expected if patch is working)
- Patch conflicts with newer LVGL version
- Patch file is corrupted
Solutions:
# Check if file is modified (expected)git -C lvgl diff src/drivers/sdl/lv_sdl_window.c
# Revert to original (re-applies patch on next build)git -C lvgl checkout src/drivers/sdl/lv_sdl_window.cmake apply-patches
# Force re-applygit -C lvgl checkout src/drivers/sdl/lv_sdl_window.cgit -C lvgl apply ../patches/lvgl_sdl_window_position.patchBuild Performance
Section titled “Build Performance”Symptom: Slow compilation
The build compiles ~566 app source files. The dominant cost is template instantiation and optimization passes (not header parsing — preprocessing takes <1s even for the worst files). At -O2, individual files take 8–15s to compile.
Speed tiers (full app rebuild, 32-core machine):
| Method | Wall time | Notes |
|---|---|---|
make -j (cold, no ccache) | ~4.5 min | Baseline |
make dev (cold, no ccache) | ~2.5 min | -O0 skips optimizer passes |
make -j (ccache populated) | ~38 sec | 98% cache hit rate |
make dev (ccache populated) | ~20 sec | -O0 + ccache |
Touch one .cpp, rebuild | ~7 sec | Recompile + relink |
Touch widely-included .h | ~8 sec | ccache direct hit (content unchanged) |
Solutions (most impactful first):
-
Install ccache — by far the biggest win. The Makefile auto-detects and wraps the compiler. Gives ~7x speedup for rebuilds where source content hasn’t actually changed (e.g., switching branches back and forth, touching headers without real edits).
Terminal window # Ubuntu/Debiansudo apt install ccache# macOSbrew install ccache -
Use
make devfor daily development — builds at-O0, cutting per-file compile time roughly in half. Library code still builds at-O2since it rarely changes. The binary is larger and slower at runtime, but compilation is ~2x faster.Terminal window make dev # -O0, auto-parallelmake OPT=1 -j # -O1 (middle ground: some optimization, faster than -O2)make -j # -O2 (default, for release/CI) -
Use parallel builds:
make -j(auto-detects all cores) -
Use incremental builds:
make -jinstead ofmake clean && make
Header fan-out — changing these headers triggers the most recompilation:
| Header | Files affected |
|---|---|
theme_manager.h | ~144 |
ui_update_queue.h | ~144 |
moonraker_api.h | ~122 |
app_globals.h | ~121 |
printer_state.h | ~104 |
With ccache installed, touching these headers without content changes costs ~8s (direct cache hit). Actual content changes recompile all dependents (~2 min at -O2, ~1 min at -O0).
Precompiled header (include/lvgl_pch.h): Covers LVGL, helix-xml, spdlog, nlohmann JSON, and common STL headers. These are precompiled once and reused across all translation units. Don’t add project headers to the PCH — only stable external libraries.
ccache across worktrees and Docker cross-builds
Section titled “ccache across worktrees and Docker cross-builds”ccache (~/.ccache) is shared per-user, but two things stop it from being reused as widely as you’d expect:
1. Worktree path mismatch (native builds). Because the native build compiles with -g and ccache defaults to hash_dir=true, the absolute working directory is part of the cache key — so the same source in .worktrees/foo/ misses everything the main tree cached. setup-worktree.sh configures ccache (base_dir=$HOME, hash_dir=false, max_size=25G) so worktree builds reuse the main tree’s objects. See Git Worktrees → Why worktree builds are fast for the full rationale and caveats. If you build outside $HOME (e.g. /tmp), set CCACHE_BASEDIR to a common ancestor yourself.
2. Docker cross-builds use a separate, per-target cache. Containers can’t see ~/.ccache, so each *-docker target bind-mounts its own persistent cache directory (mk/cross.mk):
DOCKER_CCACHE_BASE ?= $(HOME)/.cache/helixscreen-ccachedocker-ccache-args = -v "$(DOCKER_CCACHE_BASE)/$(1)":/ccache -e CCACHE_DIR=/ccacheSo make pi-docker caches into ~/.cache/helixscreen-ccache/pi/, make ad5m-docker into .../ad5m/, etc. — one cache per architecture (they must stay separate; a Pi aarch64 object is meaningless to an AD5M armv7-a build). First cross-build of a target is cold; subsequent ones hit ~98%. Override the base location with DOCKER_CCACHE_BASE=/path make pi-docker. To wipe a single target’s cache, rm -rf ~/.cache/helixscreen-ccache/<target>.
Concurrent Docker cross-builds are serialized by scripts/cross-compile-lock.sh to avoid thrashing the machine — this is automatic.
Clang Standard Library Issues (Arch Linux)
Section titled “Clang Standard Library Issues (Arch Linux)”Symptom: fatal error: 'stdlib.h' file not found at #include_next <stdlib.h>
Cause: Clang can’t find GCC’s libstdc++ headers on bleeding-edge distros (Arch with GCC 15+).
Automatic Fix: The build system detects this and auto-falls back to g++. You’ll see:
Note: clang++ has stdlib issues on this system, using g++ insteadManual Override: Force a specific compiler:
CXX=g++ CC=gcc make -j # Use GCCCXX=clang++ make -j # Force Clang (may fail)SDL2 Not Found
Section titled “SDL2 Not Found”Symptom: sdl2-config: command not found
Solutions:
# macOSbrew install sdl2
# Debian/Ubuntusudo apt install libsdl2-dev
# Verify installationwhich sdl2-configsdl2-config --versionBest Practices
Section titled “Best Practices”Development Workflow
Section titled “Development Workflow”- Edit code in
src/orinclude/ - Run
make dev- fast build at-O0with auto-patching (ormake -jfor optimized build) - Test with
./build/bin/helix-screen - Screenshot with
./scripts/screenshot.sh(auto-opens on display 1) - Commit with working incremental changes
For debugging build issues:
make cleanmake V=1 # Verbose sequential buildClean Builds
Section titled “Clean Builds”Only use make clean && make when:
- Switching branches with significant changes
- Build artifacts are corrupted
- Troubleshooting mysterious build errors
Avoid clean rebuilds for normal development (wastes time).
Submodule Management
Section titled “Submodule Management”Never:
- Commit changes directly to submodules
- Update submodule commits without testing
- Modify submodule files without creating patches
Always:
- Create patches for submodule changes
- Document patches in
patches/README.md - Test patch application on clean checkouts
See Also
Section titled “See Also”- README.md - Project overview and quick start
- DEVELOPMENT.md - Development environment, workflow, and contributing
- ARCHITECTURE.md - System design and technical patterns
- CLAUDE.md - Development context and AI assistant guidelines
- patches/README.md - Patch documentation