Overview
Chapter 26 explored advanced std.Build techniques for coordinating workspaces and matrix builds. This project chapter puts those tools to work: we will assemble a three-package workspace featuring two reusable libraries, a vendored ANSI palette, and an application that renders a latency dashboard. Along the way, we capture metadata with named write-files and install the artefact into zig-out, demonstrating how vendor-first workflows coexist with registry-ready modules (see Build.zig and Dir.zig).
The example is intentionally compact yet realistic—libA performs statistical analysis, libB formats status lines, and the vendored palette keeps terminal colouring private to the workspace. The build graph registers only the contracts we want consumers to see, mirroring the hygiene rules from the previous concept chapters. 25
Learning Goals
- Wire multiple libraries and a vendored helper into a single workspace using a shared
deps.zigregistration function (see 26 and Module.zig). - Generate reproducible artefacts (a dependency map) with named write-files and install them into
zig-outfor CI inspection (see File.zig). - Validate component libraries through
zig build test, ensuring vendored code participates in the same test harness as registry packages (see testing.zig). - Apply Zig 0.15.2’s buffered writer API in an application that consumes the workspace modules (see #upgrading stdiogetstdoutwriterprint).
Workspace blueprint
The workspace lives under chapters-data/code/27__project-multi-package-workspace-and-vendor/. A minimal manifest declares the package name and the directories that should ship with any release, keeping vendored sources explicit (see build.zig.zon template).
Manifest and layout
.{
// Package identifier following Zig naming conventions
.name = .workspace_dashboard,
// Semantic version for this workspace (major.minor.patch)
.version = "0.1.0",
// Minimum Zig compiler version required to build this project
.minimum_zig_version = "0.15.2",
// Explicit list of paths included in the package for distribution and source tracking
// This controls what gets packaged when this project is published or vendored
.paths = .{
"build.zig", // Main build script
"build.zig.zon", // This manifest file
"deps.zig", // Centralized dependency configuration
"app", // Application entry point and executable code
"packages", // Local workspace packages (libA, libB)
"vendor", // Vendored third-party dependencies (palette)
},
// External dependencies fetched from remote sources
// Empty in this workspace as all dependencies are local/vendored
.dependencies = .{},
// Cryptographic hash for integrity verification of the package manifest
// Automatically computed by the Zig build system
.fingerprint = 0x88b8c5fe06a5c6a1,
}
$ zig build --build-file build.zig mapno outputRunning map installs zig-out/workspace-artifacts/dependency-map.txt, making the package surface auditable without combing through source trees.
Wiring packages with
deps.zig centralises module registration so every consumer—tests, executables, or future examples—receives the same wiring. We register libA and libB under public names, while the ANSI palette stays anonymous via b.createModule.
// Import the standard library for build system types and utilities
const std = @import("std");
// Container struct that holds references to project modules
// This allows centralized access to all workspace modules
pub const Modules = struct {
libA: *std.Build.Module,
libB: *std.Build.Module,
};
// Creates and configures all project modules with their dependencies
// This function sets up the module dependency graph for the workspace:
// - palette: vendored external dependency
// - libA: internal package with no dependencies
// - libB: internal package that depends on both libA and palette
//
// Parameters:
// b: Build instance used to create modules
// target: Compilation target (architecture, OS, ABI)
// optimize: Optimization mode (Debug, ReleaseSafe, ReleaseFast, ReleaseSmall)
//
// Returns: Modules struct containing references to libA and libB
pub fn addModules(
b: *std.Build,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
) Modules {
// Create module for the vendored palette library
// Located in vendor directory as an external dependency
const palette_mod = b.createModule(.{
.root_source_file = b.path("vendor/palette/palette.zig"),
.target = target,
.optimize = optimize,
});
// Create module for libA (analytics functionality)
// This is a standalone library with no external dependencies
const lib_a = b.addModule("libA", .{
.root_source_file = b.path("packages/libA/analytics.zig"),
.target = target,
.optimize = optimize,
});
// Create module for libB (report functionality)
// Depends on both libA and palette, establishing the dependency chain
const lib_b = b.addModule("libB", .{
.root_source_file = b.path("packages/libB/report.zig"),
.target = target,
.optimize = optimize,
// Import declarations allow libB to access libA and palette modules
.imports = &.{
.{ .name = "libA", .module = lib_a },
.{ .name = "palette", .module = palette_mod },
},
});
// Return configured modules for use in build scripts
return Modules{
.libA = lib_a,
.libB = lib_b,
};
}
$ zig build --build-file build.zig testno outputReturning module handles keeps callers honest—only build.zig decides which names become public imports, an approach that aligns with the namespace rules from Chapter 25. 25
Build graph orchestration
The build script installs the executable, exposes run, test, and map steps, and copies the generated dependency map into zig-out/workspace-artifacts/.
const std = @import("std");
const deps = @import("deps.zig");
/// Build script for a multi-package workspace demonstrating dependency management.
/// Orchestrates compilation of an executable that depends on local packages (libA, libB)
/// and a vendored dependency (palette), plus provides test and documentation steps.
pub fn build(b: *std.Build) void {
// Parse target platform and optimization level from command-line options
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Load all workspace modules (libA, libB, palette) via deps.zig
// This centralizes dependency configuration and makes modules available for import
const modules = deps.addModules(b, target, optimize);
// Create the root module for the main executable
// Explicitly declares dependencies on libA and libB, making them importable
const root_module = b.createModule(.{
.root_source_file = b.path("app/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
// Map import names to actual modules loaded from deps.zig
.{ .name = "libA", .module = modules.libA },
.{ .name = "libB", .module = modules.libB },
},
});
// Define the executable artifact using the configured root module
const exe = b.addExecutable(.{
.name = "workspace-dashboard",
.root_module = root_module,
});
// Register the executable for installation into zig-out/bin
b.installArtifact(exe);
// Create a command to run the built executable
const run_cmd = b.addRunArtifact(exe);
// Forward any extra command-line arguments to the executable
if (b.args) |args| run_cmd.addArgs(args);
// Register "zig build run" step to compile and execute the dashboard
const run_step = b.step("run", "Run the latency dashboard");
run_step.dependOn(&run_cmd.step);
// Create test executables for each library module
// These will run any tests defined in the respective library source files
const lib_a_tests = b.addTest(.{ .root_module = modules.libA });
const lib_b_tests = b.addTest(.{ .root_module = modules.libB });
// Register "zig build test" step to run all library test suites
const tests_step = b.step("test", "Run library test suites");
tests_step.dependOn(&b.addRunArtifact(lib_a_tests).step);
tests_step.dependOn(&b.addRunArtifact(lib_b_tests).step);
// Generate a text file documenting the workspace module structure
// This serves as human-readable documentation of the dependency graph
const mapping = b.addNamedWriteFiles("workspace-artifacts");
_ = mapping.add("dependency-map.txt",
\\Modules registered in build.zig:
\\ libA -> packages/libA/analytics.zig
\\ libB -> packages/libB/report.zig (imports libA, palette)
\\ palette -> vendor/palette/palette.zig (anonymous)
\\ executable -> app/main.zig
);
// Install the generated documentation into zig-out/workspace-artifacts
const install_map = b.addInstallDirectory(.{
.source_dir = mapping.getDirectory(),
.install_dir = .prefix,
.install_subdir = "workspace-artifacts",
});
// Register "zig build map" step to generate and install dependency documentation
const map_step = b.step("map", "Emit dependency map to zig-out");
map_step.dependOn(&install_map.step);
}
$ zig build --build-file build.zig rundataset status mean range samples
------------------------------------------------------
frontend stable 111.80 3.90 5
checkout stable 100.60 6.40 5
analytics alert 77.42 24.00 5The dependency map written by the map step renders as:
Modules registered in build.zig: libA -> packages/libA/analytics.zig libB -> packages/libB/report.zig (imports libA, palette) palette -> vendor/palette/palette.zig (anonymous) executable -> app/main.zig
Library modules
Two libraries share responsibility: libA performs numeric analysis, libB transforms those statistics into colour-coded rows. Tests live alongside each module so the build graph can execute them without additional glue.
Analytics core ()
libA implements Welford’s algorithm for stable variance computation and exposes convenience helpers such as relativeSpread and zScore. math.zig
/// Statistical summary of a numerical dataset.
/// Contains computed statistics including central tendency, spread, and sample size.
const std = @import("std");
pub const Stats = struct {
sample_count: usize,
min: f64,
max: f64,
mean: f64,
variance: f64,
/// Calculates the range (difference between maximum and minimum values).
pub fn range(self: Stats) f64 {
return self.max - self.min;
}
/// Calculates the coefficient of variation (range divided by mean).
/// Returns 0 if mean is 0 to avoid division by zero.
pub fn relativeSpread(self: Stats) f64 {
return if (self.mean == 0) 0 else self.range() / self.mean;
}
};
/// Computes descriptive statistics for a slice of floating-point values.
/// Uses Welford's online algorithm for numerically stable variance calculation.
/// Panics if the input slice is empty.
pub fn analyze(values: []const f64) Stats {
std.debug.assert(values.len > 0);
var min_value: f64 = values[0];
var max_value: f64 = values[0];
var mean_value: f64 = 0.0;
// M2 is the sum of squares of differences from the current mean (Welford's algorithm)
var m2: f64 = 0.0;
var index: usize = 0;
while (index < values.len) : (index += 1) {
const value = values[index];
// Track minimum and maximum values
if (value < min_value) min_value = value;
if (value > max_value) max_value = value;
// Welford's online algorithm for mean and variance
const count = index + 1;
const delta = value - mean_value;
mean_value += delta / @as(f64, @floatFromInt(count));
const delta2 = value - mean_value;
m2 += delta * delta2;
}
// Calculate sample variance using Bessel's correction (n-1)
const count_f = @as(f64, @floatFromInt(values.len));
const variance_value = if (values.len > 1)
m2 / (count_f - 1.0)
else
0.0;
return Stats{
.sample_count = values.len,
.min = min_value,
.max = max_value,
.mean = mean_value,
.variance = variance_value,
};
}
/// Computes the sample standard deviation from precomputed statistics.
/// Standard deviation is the square root of variance.
pub fn sampleStdDev(stats: Stats) f64 {
return std.math.sqrt(stats.variance);
}
/// Calculates the z-score (standard score) for a given value.
/// Measures how many standard deviations a value is from the mean.
/// Returns 0 if standard deviation is 0 to avoid division by zero.
pub fn zScore(value: f64, stats: Stats) f64 {
const dev = sampleStdDev(stats);
if (dev == 0.0) return 0.0;
return (value - stats.mean) / dev;
}
test "analyze returns correct statistics" {
const data = [_]f64{ 12.0, 13.5, 11.8, 12.2, 12.0 };
const stats = analyze(&data);
try std.testing.expectEqual(@as(usize, data.len), stats.sample_count);
try std.testing.expectApproxEqRel(12.3, stats.mean, 1e-6);
try std.testing.expectApproxEqAbs(1.7, stats.range(), 1e-6);
}
$ zig test packages/libA/analytics.zigAll 1 tests passed.Reporting surface ()
libB depends on libA for statistics and the vendored palette for styling. It computes a status label per dataset and renders a compact table suitable for dashboards or CI logs.
// Import standard library for testing utilities
const std = @import("std");
// Import analytics package (libA) for statistical analysis
const analytics = @import("libA");
// Import palette package for theming and styled output
const palette = @import("palette");
/// Represents a named collection of numerical data points for analysis
pub const Dataset = struct {
name: []const u8,
values: []const f64,
};
/// Re-export Theme from palette package for consistent theming across reports
pub const Theme = palette.Theme;
/// Defines threshold values that determine status classification
/// based on statistical spread of data
pub const Thresholds = struct {
watch: f64, // Threshold for watch status (lower severity)
alert: f64, // Threshold for alert status (higher severity)
};
/// Represents the health status of a dataset based on its statistical spread
pub const Status = enum { stable, watch, alert };
/// Determines the status of a dataset by comparing its relative spread
/// against defined thresholds
pub fn status(stats: analytics.Stats, thresholds: Thresholds) Status {
const spread = stats.relativeSpread();
// Check against alert threshold first (highest severity)
if (spread >= thresholds.alert) return .alert;
// Then check watch threshold (medium severity)
if (spread >= thresholds.watch) return .watch;
// Default to stable if below all thresholds
return .stable;
}
/// Returns the default theme from the palette package
pub fn defaultTheme() Theme {
return palette.defaultTheme();
}
/// Maps a Status value to its corresponding palette Tone for styling
pub fn tone(status_value: Status) palette.Tone {
return switch (status_value) {
.stable => .stable,
.watch => .watch,
.alert => .alert,
};
}
/// Converts a Status value to its string representation
pub fn label(status_value: Status) []const u8 {
return switch (status_value) {
.stable => "stable",
.watch => "watch",
.alert => "alert",
};
}
/// Renders a formatted table displaying statistical analysis of multiple datasets
/// with color-coded status indicators based on thresholds
pub fn renderTable(
writer: anytype,
data_sets: []const Dataset,
thresholds: Thresholds,
theme: Theme,
) !void {
// Print table header with column names
try writer.print("{s: <12} {s: <10} {s: <10} {s: <10} {s}\n", .{
"dataset", "status", "mean", "range", "samples",
});
// Print separator line
try writer.print("{s}\n", .{"-----------------------------------------------"});
// Process and display each dataset
for (data_sets) |data| {
// Compute statistics for current dataset
const stats = analytics.analyze(data.values);
const status_value = status(stats, thresholds);
// Print dataset name
try writer.print("{s: <12} ", .{data.name});
// Print styled status label with theme-appropriate color
try palette.writeStyled(theme, tone(status_value), writer, label(status_value));
// Print statistical values: mean, range, and sample count
try writer.print(
" {d: <10.2} {d: <10.2} {d: <10}\n",
.{ stats.mean, stats.range(), stats.sample_count },
);
}
}
// Verifies that status classification correctly responds to different
// levels of data spread relative to defined thresholds
test "status thresholds" {
const thresholds = Thresholds{ .watch = 0.05, .alert = 0.12 };
// Test with tightly clustered values (low spread) - should be stable
const tight = analytics.analyze(&.{ 99.8, 100.1, 100.0 });
try std.testing.expectEqual(Status.stable, status(tight, thresholds));
// Test with widely spread values (high spread) - should trigger alert
const drift = analytics.analyze(&.{ 100.0, 112.0, 96.0 });
try std.testing.expectEqual(Status.alert, status(drift, thresholds));
}
$ zig build --build-file build.zig testno outputTesting through zig build test ensures the module sees libA and the palette via the same imports the executable uses, eliminating discrepancies between direct zig test runs and build-orchestrated runs.
Vendored theme palette
The ANSI palette stays private to the workspace—deps.zig injects it where needed without registering a public name. This keeps colour codes stable even if the workspace later consumes a registry dependency with conflicting helpers.
// Import the standard library for testing utilities
const std = @import("std");
// Defines the three tonal categories for styled output
pub const Tone = enum { stable, watch, alert };
// Represents a color theme with ANSI escape codes for different tones
// Each tone has a start sequence and there's a shared reset sequence
pub const Theme = struct {
stable_start: []const u8,
watch_start: []const u8,
alert_start: []const u8,
reset: []const u8,
// Returns the appropriate ANSI start sequence for the given tone
pub fn start(self: Theme, tone: Tone) []const u8 {
return switch (tone) {
.stable => self.stable_start,
.watch => self.watch_start,
.alert => self.alert_start,
};
}
};
// Creates a default theme with standard terminal colors:
// stable (green), watch (yellow), alert (red)
pub fn defaultTheme() Theme {
return Theme{
.stable_start = "\x1b[32m", // green
.watch_start = "\x1b[33m", // yellow
.alert_start = "\x1b[31m", // red
.reset = "\x1b[0m",
};
}
// Writes styled text to the provided writer by wrapping it with
// ANSI color codes based on the theme and tone
pub fn writeStyled(theme: Theme, tone: Tone, writer: anytype, text: []const u8) !void {
try writer.print("{s}{s}{s}", .{ theme.start(tone), text, theme.reset });
}
// Verifies that the default theme returns correct ANSI escape codes
test "default theme colors" {
const theme = defaultTheme();
try std.testing.expectEqualStrings("\x1b[32m", theme.start(.stable));
try std.testing.expectEqualStrings("\x1b[0m", theme.reset);
}
$ zig test vendor/palette/palette.zigAll 1 tests passed.Application entry point
The executable imports only the public modules, builds datasets, and prints the table using the buffered writer API introduced in Zig 0.15.2.
// Main application entry point that demonstrates multi-package workspace usage
// by generating a performance report table with multiple datasets.
// Import the standard library for I/O operations
const std = @import("std");
// Import the reporting library (libB) from the workspace
const report = @import("libB");
/// Application entry point that creates and renders a performance monitoring report.
/// Demonstrates integration with the libB package for generating formatted tables
/// with threshold-based highlighting.
pub fn main() !void {
// Allocate a fixed buffer for stdout to avoid dynamic allocation
var stdout_buffer: [1024]u8 = undefined;
// Create a buffered writer for efficient stdout operations
var writer_state = std.fs.File.stdout().writer(&stdout_buffer);
// Get the generic writer interface for use with the report library
const out = &writer_state.interface;
// Define sample performance datasets for different system components
// Each dataset contains a component name and an array of performance values
const datasets = [_]report.Dataset{
.{ .name = "frontend", .values = &.{ 112.0, 109.5, 113.4, 112.2, 111.9 } },
.{ .name = "checkout", .values = &.{ 98.0, 101.0, 104.4, 99.1, 100.5 } },
.{ .name = "analytics", .values = &.{ 67.0, 89.4, 70.2, 91.0, 69.5 } },
};
// Configure monitoring thresholds: 8% variance triggers watch, 20% triggers alert
const thresholds = report.Thresholds{ .watch = 0.08, .alert = 0.2 };
// Use the default color theme provided by the report library
const theme = report.defaultTheme();
// Render the formatted report table to the buffered writer
try report.renderTable(out, &datasets, thresholds, theme);
// Flush the buffer to ensure all output is written to stdout
try out.flush();
}
$ zig build --build-file build.zig rundataset status mean range samples
------------------------------------------------------
frontend stable 111.80 3.90 5
checkout stable 100.60 6.40 5
analytics alert 77.42 24.00 5Notes & Caveats
- The workspace exposes only
libAandlibB; vendored modules remain anonymous thanks tob.createModule, preventing downstream consumers from relying on internal helpers. - Named write-files produce deterministic artefacts. Pair the
mapstep with CI to detect accidental namespace changes before they reach production. zig build testcomposes multiple module tests under a single command; if you add new packages, remember to thread their modules throughdeps.zigso they join the suite.
Exercises
- Extend the dependency map to emit JSON alongside the text file. Hint: add a second
mapping.add("dependency-map.json", …)and reusestd.jsonto serialise the structure. 26, json.zig - Add a registry dependency via
b.dependency("logger", .{}), re-export its module indeps.zig, and update the map to document the new namespace. 24 - Introduce a
-Dalert-spreadoption that overrides the default thresholds. Forward the option throughdeps.zigso both the executable and any tests see the same policy.
Caveats, alternatives, edge cases
- When the vendored palette eventually graduates to a standalone package, swap
b.createModuleforb.addModuleand list it inbuild.zig.zonto ensure consumers fetch it via hashes. - If your workspace grows beyond a handful of modules, consider grouping registries in
deps.zigby responsibility (observability,storage, etc.) so the build script stays navigable. 26 - Cross-compiling the dashboard requires ensuring each target supports ANSI escapes; gate palette usage behind
builtin.os.tagchecks if you ship to Windows consoles without VT processing. builtin.zig
Summary
deps.zigcentralises module registration, enabling repeatable workspaces that expose only sanctioned namespaces.- Named write-files and install directories turn build metadata into versionable artefacts ready for CI checks.
- A vendored helper can coexist with reusable libraries, keeping internal colour schemes private while the public API remains clean.
With this project you now have a concrete template for organising multi-package Zig workspaces, balancing vendored code with reusable libraries while keeping the build graph transparent and testable.