This is the repository for the new build system for UVA Rocketry. It's "just a build.zig script"
It's a very reasonable question why a repo with 3k+ lines of zig just to build some c++ even needs to exist. So, why?
There are numerous reasons, most of them boil down to the fact that platformio's build system is... pretty bad:
- building for desktop targets or python modules (like we need for RocketPy testing and generic testing) is (afaik) impossible on platformio
- due to the above reason, easily sharing build flags across platformio and whatever other build system we use is basically impossible
- allowing transitive dependencies on our repos is needlessly difficult because it requires us to maintain a separate library.json file (which adds a third source of truth to builds?)
- integrating zig code into our code is very sus right now, even though it would be very beneficial in some places
- LSP information generation is NOT possible to do not manually right now (at least with a fancy build.zig script, cmake could probably be done easily though, but I don't want to EVER touch cmake)
The solution is this monstrosity of a build script. It autogenerates:
- platformio.ini
- library.json (for dependencies to be usable from platformio)
- LSP Information
Since this script is just a build.zig script, users can modify the returned modules to add all their own special information, making this super powerful. Also, since it's a "just" build.zig script it will ALSO build zig code, which enables using zig's testing system, which is LEAGUES better than C++ testing
With Chicot, ALL information comes from the build.zig.zon file. This makes it so that there is ONLY one source of truth about the build
There are a few commands to use different aspects of chicot:
Fortunately, getting up and running writing code inside your editor is super easy. First, you must build everything with zig build (or zig build -Dmode=... if you want to specify a specific mode)
Then, all you have to do is zig build lsp -p . (or zig build lsp -p . -Dmode=...)
This command will always modify the compile_flags.txt in the same directory as the build.zig.zon file
Unfortunately, chicot does not currently support lsp information generation for Microsoft's C/C++ Extension. If you want autocomplete and all those nice features inside vscode, install the clangd extension. Vscode should prompt you to disable intellisense after the installation of clangd, which you should accept.
If you have updated the lsp information (with zig build lsp -p .) but clangd is still showing errors then save the file and everything should start working again.
If a project is setup to use platformio for the embedded build system, then you will need to run zig build pio -p . to autogenerate the platformio.ini and accompanying checkpio.py build files. If the platformio.ini file is out of sync with Chicot's current build info, then platformio will prompt you to rerun zig build pio -p . when you next run pio run ...
If you want to package up your current project as a platformio library, then run zig build libraryjson to generate the library.json file that platformio needs. Note that this will NOT package up zig exported symbols, so use this with care
The build.zig file is the entry point for the Zig build system. When using Chicot, your build.zig should be minimal and delegate all work to the Chicot build function.
const std = @import("std");
pub fn build(b: *std.Build) !void {
const chicotDep = b.lazyDependency("chicot", .{}) orelse return;
const chicot = @import("chicot");
_ = try chicot.chicotBuild(b, chicotDep, @import("build.zig.zon"), .{});
}If you need to add external Zig dependencies (e.g. dependencies that aren't also built with chicot), you can extend the build.zig:
const builtin = @import("builtin");
const std = @import("std");
pub fn build(b: *std.Build) !void {
const chicotDep = b.lazyDependency("chicot", .{}) orelse return;
const chicot = @import("chicot");
const modules = try chicot.chicotBuild(b, chicotDep, @import("build.zig.zon"), .{});
// Add external Zig dependencies to the build
const dep = b.dependency("websocket", .{});
// make it publicly available to the main src/root.zig module
modules.libzigMod.addImport("websocket", dep.module("websocket"));
if (modules.exeMod) |exe| {
// make it publicly available to the executable module as well
// if we are building on desktop
exe.addImport("websocket", dep.module("websocket"));
}
}You can add custom include paths to the Zig module after the Chicot build:
const std = @import("std");
pub fn build(b: *std.Build) !void {
const chicotDep = b.lazyDependency("chicot", .{}) orelse return;
const chicot = @import("chicot");
const mods = try chicot.chicotBuild(b, chicotDep, @import("build.zig.zon"), .{});
mods.libzigMod.addIncludePath(b.path("./.pio/libdeps/teensy41/llcc68_driver/src/"));
}pub fn build(
b: *std.Build,
chicot: *std.Build.Dependency,
zon: anytype,
options: BuildOptions,
) !ModulesParameters:
b: The Zig build contextchicot: The Chicot dependency (obtained viab.lazyDependency("chicot", .{}))zon: The parsed build.zig.zon file (passed as@import("build.zig.zon"))options: Build options (currently unused, pass.{})
Returns: Modules struct containing all the built modules and libraries:
libzig: The static library containing Zig codelibzigMod: The Zig module (can be used to add imports)libcpp: The static library containing C++ codelibcppForDeps: Library for linking C++ dependenciesdepLibcpps: Array of dependency C++ librariesdepLibzigs: Array of dependency Zig librariesrootTests: Test executable (if applicable)cppMod: The C++ modulezigobject: Object file for Zig codecompatHeadersDir: Path to compatibility headersdepHeadersDir: Path to dependency headersplatformioClangdCompatHeaders: PlatformIO clangd compatibility headerslib: Combined libraryheaderLib: Library containing headersdepHeaderLib: Library containing dependency headerspythonMod: Python module (if building Python bindings)python: Python library (if building Python bindings)exeMod: Desktop executable module (if building exe)exe: Desktop executable (if building exe)check: Check step for compilation verification
The build.zig.zon file is the single source of truth for your project configuration. It defines dependencies, build modes, compiler flags, and output targets.
.{
// Standard Zig package fields
.name = .your_project_name,
.version = "0.0.1",
.fingerprint = 0x..., // Unique package fingerprint
.minimum_zig_version = "0.15.1",
// Dependencies
.dependencies = .{
.chicot = .{
.url = "git+https://github.com/UvaRocketry/chicot#commit_hash",
.hash = "chicot-0.0.1-...",
},
.other_dep = .{
.path = "../other_dep",
},
},
// Files to include in the package
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"desktop", // Optional: for desktop executables
},
// Chicot-specific configuration
.chicotversion = "0.0.0",
.builddefaults = .{
.mode = "desktop",
.targets = .{
.teensy = .{
.arch_os_abi = "thumb-freestanding-eabihf",
.cpu_features = "cortex_m7+fp_armv8d16",
},
},
},
.buildmodes = .{
// Define your build modes here
},
}The package name as an enum literal (e.g., .myproject).
Semantic version string (e.g., "0.0.1").
Unique package fingerprint for Zig's package manager.
Minimum required Zig version (e.g., "0.15.1").
Map of dependencies. Must include at least chicot.
Dependency format:
- Git dependency:
{ .url = "git+https://github.com/...#commit_hash", .hash = "..." } - Local path dependency:
{ .path = "../relative/path" }
Array of paths to include in the package:
"build.zig"- The build script"build.zig.zon"- This configuration file"src"- Source code directory"desktop"- Desktop application code (optional, for building executables)"python"- Python bindings (optional, for building Python modules)
Chicot build system version (currently "0.0.0").
Default build configuration:
.builddefaults = .{
// Default build mode if not specified
.mode = "desktop",
// Target definitions
.targets = .{
.teensy = .{
.arch_os_abi = "thumb-freestanding-eabihf",
.cpu_features = "cortex_m7+fp_armv8d16",
},
},
}Map of build mode configurations. Each mode defines how the project should be built.
Required build modes:
desktop- For building desktop executables and teststeensy41- For building Teensy 4.1 embedded targets
Each build mode is a struct with the following fields:
Human-readable description of the build mode.
Name of another build mode to inherit settings from. Use null for root modes.
.inherit = "shared", // Inherit from "shared" modeTarget identifier (references a key from builddefaults.targets).
.target = "teensy",Optimization mode. One of:
.Debug- Debug builds (default for desktop).ReleaseSafe- Safe release builds.ReleaseFast- Fast release builds.ReleaseSmall- Small binary size
Array of output types to generate:
.libzig- Static library with Zig code.liball- Static library with all code (Zig + C++).pythonmodule- Python extension module.exe- Desktop executable.platformioini- PlatformIO configuration
.outputTypes = .{
.libzig,
.exe,
},Array of dependency specifications:
.dependencies = .{
.{ .dependencyName = "stl" },
.{ .dependencyName = "vec", .importName = "vector" }, // With custom import name
},Fields:
dependencyName(required): Name of the dependency as declared in the dependencies sectionimportName(optional): Custom name for importing the dependency in Zig (defaults to dependencyName)
C++ compiler configuration:
.cpp = .{
// Compiler flags (e.g., -std=c++23)
.otherFlags = .{
"-std=c++23",
"-Wall",
"-Wextra",
},
// Include directories (will be prefixed with -I)
.include = .{
"include",
"third_party",
},
// Link library names (will be prefixed with -l)
.linkPath = .{
"m", // math library
},
// Preprocessor definitions
.define = .{
.FLOAT = "float",
.DEBUG = "1",
.RELEASE = null, // Defined with no value
},
// Flags that must be present (for validation)
.requiredFlags = .{
"-DFLOAT=",
},
// Flags that can be overridden by parent projects
.overrideableFlags = .{
"-O",
},
}PlatformIO configuration for embedded builds:
.platformio = .{
.platform = "teensy",
.board = "teensy41",
.framework = "arduino",
.build_type = "release",
// Additional PlatformIO library dependencies
.lib_deps = .{
"Wire",
"SPI",
"sandeepmistry/LoRa@^0.8.0",
},
// Extra Python scripts to run during build
.extra_scripts = .{
"pre:idk.py",
},
}Array of additional header directories to install:
.installHeaders = .{
.{ .fromDir = "include", .toDir = "" },
.{ .fromDir = "third_party/headers", .toDir = "third_party" },
},Configuration for C header generation from Zig code:
.headergen = .{
.{ .ignoreType = "InternalStruct" }, // Exclude types from header
.{ .addInclude = "extra_header.h" }, // Add additional includes
.{ .ignoreDecl = "internal_func" }, // Exclude declarations
},The Chicot build system provides several build steps that can be invoked with zig build <step>:
Builds and installs all configured outputs.
Options:
-Dmode=<mode>- Build in specific mode (e.g.,-Dmode=teensy41)-Dtarget=<target>- Override target triple-Doptimize=<mode>- Override optimization (Debug, ReleaseSafe, ReleaseFast, ReleaseSmall)
Runs Zig tests from src/root.zig.
Runs the desktop executable (if exe output type is configured).
Passes additional arguments to the executable:
zig build run -- arg1 arg2Verifies that everything compiles without errors (does not produce output).
Generates LSP configuration files for clangd.
Options:
-p <path>- Installation prefix path (usually-p .)-Dmode=<mode>- Build mode for LSP configuration
Generates platformio.ini and supporting files for PlatformIO builds.
Options:
-p <path>- Installation prefix path-Dmode=<mode>- Build mode (usuallyteensy41)-Ddiff=<bool>- Whether to diff with existing platformio.ini
Generates library.json for publishing as a PlatformIO library.
Generates a C/C++ header file that exports all public symbols from src/root.zig.
Installs the Python module to the canonical location (python/<name>.so or python/<name>.pyd).
Options:
-DforceBuildPy=<bool>- Force building Python modules even when cross-compiling
Chicot expects the following directory structure:
project/
├── build.zig # Build script
├── build.zig.zon # Project configuration
├── src/ # Source code
│ ├── root.zig # Main Zig module (optional)
│ ├── *.cpp # C++ source files (optional)
│ └── *.h # C++ headers (optional)
├── desktop/ # Desktop application code
│ └── main.zig # Desktop entry point (optional)
├── python/ # Python bindings
│ └── python.zig # Python module entry (optional)
└── ...
Build modes support inheritance to reduce duplication. Use the inherit field to specify a parent mode:
.buildmodes = .{
.shared = .{
.description = "Common settings",
.cpp = .{ .otherFlags = .{"-std=c++23"} },
.inherit = null,
},
.desktop = .{
.description = "Desktop build",
.inherit = "shared", // Inherits cpp flags from "shared"
.cpp = .{
.otherFlags = .{"-Wall"}, // Merged with inherited flags
},
},
}Inheritable fields:
cpp- Merged (flags are appended)target- Overridden if specifiedplatformio- Mergeddependencies- Merged (dependencies are combined)outputTypes- Overridden if specifiedinstallHeaders- Mergedheadergen- Merged
Chicot recognizes the following environment variables:
HOME/USERPROFILE(Windows) - Used to find PlatformIO installation- Standard Zig environment variables for target selection
To cross-compile for different targets:
# Build for Teensy 4.1
zig build -Dmode=teensy41
# Build for desktop with specific target
zig build -Dtarget=aarch64-linux-gnu
# Build for Windows from Linux/macOS
zig build -Dtarget=x86_64-windows-gnuTo build Python extension modules:
- Create
python/python.zigwith your module code - Add
pythonmoduletooutputTypesin your build mode - Build:
zig build -Dmode=desktop - Install:
zig build py
Note: Python modules are automatically skipped when cross-compiling unless -DforceBuildPy=true is set.
Chicot generates all necessary files for PlatformIO:
- Generate configuration:
zig build pio -p . -Dmode=teensy41 - Build with PlatformIO:
pio run
The build system will:
- Generate
platformio.iniwith all settings - Generate
checkpio.pyto validate configuration - Build
libziglibrary for linking - Set up all include paths and dependencies
// build.zig.zon
.{
.name = .myproject,
.version = "0.0.1",
.fingerprint = 0x1234567890abcdef,
.minimum_zig_version = "0.15.1",
.dependencies = .{
.chicot = .{
.url = "git+https://github.com/UvaRocketry/chicot#a832f0a",
.hash = "chicot-0.0.1-...",
},
},
.paths = .{"build.zig", "build.zig.zon", "src"},
.chicotversion = "0.0.0",
.buildmodes = .{
.desktop = .{
.description = "Desktop build",
.outputTypes = .{.libzig},
.cpp = .{ .otherFlags = .{"-std=c++23"} },
},
.teensy41 = .{
.description = "Teensy build",
.outputTypes = .{.libzig},
.target = "teensy",
.platformio = .{
.platform = "teensy",
.board = "teensy41",
.framework = "arduino",
},
},
},
}// build.zig.zon
.{
// ... standard fields ...
.paths = .{"build.zig", "build.zig.zon", "src", "python"},
.buildmodes = .{
.desktop = .{
.description = "Desktop with Python",
.outputTypes = .{.libzig, .pythonmodule},
.dependencies = .{
.{ .dependencyName = "stl" },
},
},
},
}// build.zig.zon
.{
// ... standard fields ...
.paths = .{"build.zig", "build.zig.zon", "src", "desktop"},
.buildmodes = .{
.desktop = .{
.description = "Desktop app",
.outputTypes = .{.libzig, .exe},
.dependencies = .{
.{ .dependencyName = "stl" },
.{ .dependencyName = "debugger" },
},
},
},
}// build.zig.zon
.{
// ... standard fields ...
.buildmodes = .{
.shared = .{
.description = "Common settings",
.cpp = .{
.otherFlags = .{"-std=c++23"},
.define = .{.FLOAT = "float"},
},
.dependencies = .{
.{ .dependencyName = "stl" },
.{ .dependencyName = "vec" },
},
.inherit = null,
.outputTypes = .{},
},
.desktopshared = .{
.description = "Desktop shared settings",
.inherit = "shared",
.cpp = .{
.otherFlags = .{"-Wall", "-Wextra"},
.define = .{.DESKTOP = ""},
},
},
.desktop = .{
.description = "Desktop executable",
.inherit = "desktopshared",
.outputTypes = .{.libzig, .exe, .pythonmodule},
.dependencies = .{
.{ .dependencyName = "debugger" },
},
},
.teensy41 = .{
.description = "Teensy embedded",
.inherit = "shared",
.outputTypes = .{.libzig},
.target = "teensy",
.platformio = .{
.platform = "teensy",
.board = "teensy41",
.framework = "arduino",
},
},
},
}