diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4219a59 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + pull_request: + push: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Zig + uses: mlugg/setup-zig@v2.2.1 + with: + version: 0.16.0 + + - name: Run test suite + run: zig build test --summary all diff --git a/README.md b/README.md index 4548e32..a52380a 100644 --- a/README.md +++ b/README.md @@ -1,255 +1,184 @@ # zbuild -An opinionated build tool for Zig projects. +Declarative `std.Build` graphs generated from `build.zig.zon` at comptime. -## Introduction +zbuild reads `@import("build.zig.zon")` as a typed value and turns it into normal `std.Build` calls. There is no runtime parser, and the graph is generated directly inside the build, not by an external codegen phase. It is a library that sits on top of Zig's native build graph. -`zbuild` is a command-line build tool designed to simplify and enhance the build process for Zig projects. It leverages a ZON-based configuration file (`zbuild.zon`) to define project builds declaratively, generating a corresponding `build.zig` and `build.zig.zon` file that integrates seamlessly with Zig’s native build system. This approach reduces the complexity of writing and maintaining Zig build scripts manually, offering a structured alternative for managing dependencies, modules, executables, libraries, tests, and more. +## Start Here -Note: `zbuild` is under active development. Some features are incomplete or subject to change. Check the `docs/TODO.md` file for planned enhancements. +- Want to try it immediately: use the quickstart below. +- Want the mental model first: read [Conceptual Model](docs/concepts.md). +- Want exact field-by-field details: read [Schema Reference](docs/schema.md). +- Want the rationale and tradeoffs: read [Why zbuild?](docs/motivation.md). -## Features +## Quickstart -- **ZON-based Configuration**: Define your build in a `zbuild.zon` file instead of writing Zig code directly. -- **Automatic build.zig Generation**: Create a `build.zig` and `build.zig.zon` file from your configuration. -- **Comprehensive Build Support**: Manage dependencies, modules, executables, libraries, objects, tests, formatting, and run commands. -- **Command-Line Interface**: Execute common build tasks like compiling executables, running tests, and formatting code. +### 1. Add zbuild as a dependency -## Installation +```bash +zig fetch --save=zbuild +``` -Currently, zbuild must be built from source: +That writes the `.dependencies.zbuild` entry for you. -1. Clone the repository: -```bash -git clone https://github.com/chainsafe/zbuild.git -cd zbuild +### 2. Create `build.zig` + +```zig +const zbuild = @import("zbuild"); +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + _ = try zbuild.configureBuild(b, @import("build.zig.zon"), .{}); +} ``` -2. Build the executable: -```bash -zig build -Doptimize=ReleaseFast +### 3. Add zbuild-owned fields to `build.zig.zon` + +```zig +.executables = .{ + .myapp = .{ + .root_module = .{ + .root_source_file = "src/main.zig", + }, + }, +}, ``` -3. (Optional) Install it globally: +Assume the rest of `build.zig.zon` is the normal Zig package metadata from your project or `zig init`. + +### 4. Build it + ```bash -zig build install --prefix ~/.local +zig build +zig build run:myapp +zig build help ``` -A pre-built binary distribution is planned for future releases once sufficient feature-completeness is achieved. +### 5. Expand from there -## Usage +Once the first executable works, add modules, tests, runs, fmts, options modules, or libraries as needed. The [simple example](examples/simple/) shows the minimal shape. The [full example](examples/full/) shows most of the library in one place. -`zbuild` provides a command-line interface with various commands to manage your Zig projects. Below is the general syntax and a list of available commands: +## What zbuild gives you -``` -Usage: zbuild [global_options] [command] [options] -``` +- `modules` for reusable Zig modules with imports, include paths, and dependency libraries +- `executables`, `libraries`, and `objects` +- `tests` with per-test steps and an aggregate `test` step +- `fmts` with per-target steps and an aggregate `fmt` step +- `runs` for arbitrary system commands +- `aliases` for named aggregate steps such as `check`, `ci`, or `release` +- `options_modules` that become importable Zig config modules and `-Dmodule.option` CLI flags +- `presets` for named bundles of `options_modules` defaults, selected with `-Dpreset=` +- comptime dependency args forwarded to `b.dependency(...)` +- a built-in help step (`help` by default, configurable via `Options.help_step`) +- two-phase validation so local graph mistakes fail early -### Commands - -- `init`: Initialize a new Zig project with a basic `zbuild.zon`, `build.zig`, `build.zig.zon`, and `src/main.zig` in the current directory. -- `fetch`: Copy a package into the global cache and optionally add it to `zbuild.zon` and `build.zig.zon` -- `install`: Install all artifacts defined in `zbuild.zon`. -- `uninstall`: Uninstall all artifacts. -- `sync`: Synchronize `build.zig` and `build.zig.zon` with `zbuild.zon`. -- `build`: Run `zig build` with the generated `build.zig`. -- `build-exe `: Build a specific executable defined in `zbuild.zon`. -- `build-lib `: Build a specific library. -- `build-obj `: Build a specific object file. -- `build-test `: Build a specific test into an executable. -- `run `: Run an executable or a custom run script. -- `test [name]`: Run all tests or a specific test. -- `fmt [name]`: Format code for all or a specific formatting target. -- `help`: Print the help message and exit. -- `version`: Print the version number and exit. - -### Global Options - -- `--project-dir `: Set the project directory (default: `.`). -- `--zbuild-file `: Specify the configuration file (default: `zbuild.zon`). -- `--no-sync`: Skip automatic synchronization of `build.zig` and `build.zig.zon`. - -### General Options - -- `-h, --help`: Print command-specific usage. - -For command-specific options (e.g., `fetch`), use zbuild --help. - -## Configuration - -The `zbuild.zon` file is the heart of the zbuild system. It defines your project’s structure and build settings. Below is an example configuration: - -```zon -.{ - .name = .example_project, - .version = "1.2.3", - .description = "A comprehensive example", - .fingerprint = 0x90797553773ca567, - .minimum_zig_version = "0.14.0", - .paths = .{ "build.zig", "build.zig.zon", "src" }, - .keywords = .{"example"}, - .dependencies = .{ - .mathlib = .{ - .path = "deps/mathlib", - }, - .network = .{ - .url = "https://github.com/example/network/archive/v1.0.0.tar.gz", - }, - }, - .options_modules = .{ - .build_options = .{ - .max_depth = .{ - .type = "usize", - .default = 100, - }, - }, - }, - .modules = .{ - .utils = .{ - .root_source_file = "src/utils/main.zig", - .imports = .{.mathlib, .build_options}, - .link_libc = true, - }, - .core = .{ - .root_source_file = "src/core/core.zig", - .imports = .{.utils}, - }, - }, - .executables = .{ - .main_app = .{ - .root_module = .{ - .root_source_file = "src/main.zig", - .imports = .{.core, .network}, - }, - }, - }, - .libraries = .{ - .libmath = .{ - .version = "0.1.0", - .root_module = .utils, - .linkage = .static, - }, - }, - .tests = .{ - .unit_tests = .{ - .root_module = .{ - .root_source_file = "tests/unit.zig", - .imports = .{.core, .utils}, - }, - }, - }, - .fmts = .{ - .source = .{ - .paths = .{"src", "tests"}, - .exclude_paths = .{"src/generated"}, - .check = true, - }, - }, - .runs = .{ - .start_server = "zig run src/server.zig", - .build_docs = "scripts/build_docs.sh", - }, -} -``` +## First Mental Model -### Key Sections +zbuild becomes easy to use once you keep three rules in your head: -- `name`, `version`, `fingerprint`, `minimum_zig_version`, `paths`: Project metadata (required). -- `dependencies`: External packages (path or URL). -- `options_modules`: Configurable build options bundled into modules. -- `modules`: Reusable code units with optional imports and build settings. -- `executables`, `libraries`, `objects`: Build targets with root modules. -- `tests`: Test targets with optional filters. -- `fmts`: Code formatting rules. -- `runs`: Custom shell commands. +1. `build.zig.zon` declares graph nodes. + `modules`, `executables`, `libraries`, `tests`, `runs`, `fmts`, and `aliases` each map to a different kind of `std.Build` node or step. -## Hello World Example +2. Ownership is encoded in syntax. + Enum literals like `.core`, `.config`, and `.myapp` mean "this belongs to the zbuild-owned graph". + Bare strings like `"shared"` and `"gen:prep"` mean "this is manual `build.zig` state registered before `configureBuild`". -Here’s a step-by-step example to create and build a simple Zig project with zbuild: +3. Validation happens in two phases. + Local manifest structure and manifest-owned refs fail at comptime. + Manual refs and dependency exports fail during configure, after zbuild can actually inspect them. -1. Initialize the project: +If you want the full model, including namespace rules and why those syntax splits exist, read [docs/concepts.md](docs/concepts.md). -```bash -mkdir myproject -cd myproject -zbuild init -``` +## Named Presets -2. (Optional) Inspect `zbuild.zon` - -```zon -.{ - .name = .myproject, - .version = "0.1.0", - .fingerprint = 0x, - .minimum_zig_version = "0.14.0", - .paths = .{ "build.zig", "build.zig.zon", "src" }, - .executables = .{ - .myproject = .{ - .root_module = .{ - .root_source_file = "src/main.zig", - }, - }, - }, -} -``` +If you have a recurring bundle of option values, keep the types in `options_modules` and add a named preset on top: -3. Update `src/main.zig` ```zig -const std = @import("std"); -pub fn main() !void { - const allocator = std.heap.page_allocator; - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); - - const arg = if (args.len >= 2) args[1] else "zbuild"; - std.debug.print("Hello {s}!\n", .{arg}); -} +.options_modules = .{ + .app = .{ + .log_level = .{ + .type = .@"enum", + .values = .{ .debug, .info, .warn }, + .default = .info, + }, + .asset_dir = .{ + .type = .string, + }, + }, +}, +.presets = .{ + .dev = .{ + .app = .{ + .log_level = .debug, + }, + }, + .prod = .{ + .app = .{ + .log_level = .warn, + .asset_dir = "dist/prod", + }, + }, +}, ``` -4. (Optional) Build the Executable: +Then select one explicitly: ```bash -zbuild build-exe myproject +zig build run:myapp -Dpreset=prod +zig build run:myapp -Dpreset=prod -Dapp.log_level=debug ``` -This builds the `myproject` executable into `zig-out/bin`. -5. Run the Executable: +Presets only override `options_modules`. They do not create a second config API, and per-option `-Dmodule.option=...` flags still win. -```bash -zbuild run myproject -- world -``` +## Working With Manual `build.zig` Code -Outputs: `Hello, world!` +zbuild does not replace `build.zig`. It owns the declarative 90%, and you keep Zig for the rest. -## Fetching Dependencies +Register manual modules or steps before `configureBuild`: -Add a dependency to your project: -```bash -zbuild fetch --save=example https://github.com/example/repo/archive/v1.0.0.tar.gz -``` +```zig +const zbuild = @import("zbuild"); +const std = @import("std"); -This updates `zbuild.zon` with: -```zon -.dependencies = .{ - .example = { - .url = "https://github.com/example/repo/archive/v1.0.0.tar.gz, - } +pub fn build(b: *std.Build) !void { + _ = b.addModule("shared", .{ + .root_source_file = b.path("src/shared.zig"), + .target = b.resolveTargetQuery(.{}), + .optimize = .Debug, + }); + _ = b.step("gen:prep", "manual prep step"); + + _ = try zbuild.configureBuild(b, @import("build.zig.zon"), .{}); } ``` -And synchronizes `build.zig.zon` with the fetched hash. +Then reference those manual nodes from the manifest with bare strings: +```zig +.executables = .{ + .app = .{ + .root_module = "shared", + }, +}, +.runs = .{ + .demo = .{ + .cmd = .{ "echo", "ok" }, + .depends_on = .{ "gen:prep" }, + }, +}, +``` -## Contributing +## Documentation Map -Contributions are welcome! To contribute: +- [Conceptual Model](docs/concepts.md): the bottom-up explanation of how the graph, namespaces, and validation fit together +- [Schema Reference](docs/schema.md): exact field types, syntax, and generated step names +- [Why zbuild?](docs/motivation.md): the problem it solves and the design constraints it follows +- [Simple Example](examples/simple/): the smallest useful project +- [Full Example](examples/full/): modules, tests, runs, fmts, and options modules together -1. Fork the repository on GitHub: https://github.com/chainsafe/zbuild. -2. Create a branch for your changes. -3. Submit a pull request with a clear description of your improvements. +## Requirements -Please open an issue first to discuss significant changes or report bugs. +Zig `0.16.0+` ## License diff --git a/build.zig b/build.zig index f229088..884837c 100644 --- a/build.zig +++ b/build.zig @@ -1,70 +1,203 @@ -// This file is generated by zbuild. Do not edit manually. - const std = @import("std"); +const build_runner = @import("src/build_runner.zig"); + +// Re-export the zbuild API so dependents can use @import("zbuild").configureBuild +pub const configureBuild = build_runner.configureBuild; +pub const Options = build_runner.Options; +pub const BuildResult = build_runner.BuildResult; + +const FixtureCommand = struct { + name: []const u8, + cwd: []const u8, + build_args: []const []const u8 = &.{}, + expect_exit: u8 = 0, + stdout_match: ?[]const u8 = null, + stderr_match: ?[]const u8 = null, +}; + +fn addFixtureCommand(b: *std.Build, aggregate: *std.Build.Step, command: FixtureCommand) void { + const run = b.addSystemCommand(&.{ b.graph.zig_exe, "build" }); + run.has_side_effects = true; + run.addArgs(command.build_args); + run.setCwd(b.path(command.cwd)); + run.expectExitCode(command.expect_exit); + if (command.stdout_match) |expected| run.expectStdOutMatch(expected); + if (command.stderr_match) |expected| run.expectStdErrMatch(expected); + + const step = b.step( + b.fmt("test:fixture:{s}", .{command.name}), + b.fmt("Run fixture check {s}", .{command.name}), + ); + step.dependOn(&run.step); + aggregate.dependOn(&run.step); +} pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const module_zbuild = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), + // zbuild library module (for use by zbuild-powered projects) + const zbuild_module = b.addModule("zbuild", .{ + .root_source_file = b.path("src/build_runner.zig"), .target = target, .optimize = optimize, }); - b.modules.put(b.dupe("zbuild"), module_zbuild) catch @panic("OOM"); - - const exe_zbuild = b.addExecutable(.{ - .name = "zbuild", - .root_module = module_zbuild, - }); - - const install_exe_zbuild = b.addInstallArtifact(exe_zbuild, .{}); - - const tls_install_exe_zbuild = b.step("build-exe:zbuild", "Install the zbuild executable"); - tls_install_exe_zbuild.dependOn(&install_exe_zbuild.step); - b.getInstallStep().dependOn(&install_exe_zbuild.step); - - const run_exe_zbuild = b.addRunArtifact(exe_zbuild); - if (b.args) |args| run_exe_zbuild.addArgs(args); - const tls_run_exe_zbuild = b.step("run:zbuild", "Run the zbuild executable"); - tls_run_exe_zbuild.dependOn(&run_exe_zbuild.step); + // Tests const tls_run_test = b.step("test", "Run all tests"); const test_zbuild = b.addTest(.{ .name = "zbuild", - .root_module = module_zbuild, - .filters = b.option([][]const u8, "zbuild.filters", "zbuild test filters") orelse &[_][]const u8{}, + .root_module = zbuild_module, + .filters = b.option([]const []const u8, "zbuild.filters", "zbuild test filters") orelse &.{}, }); - const install_test_zbuild = b.addInstallArtifact(test_zbuild, .{}); - const tls_install_test_zbuild = b.step("build-test:zbuild", "Install the zbuild test"); - tls_install_test_zbuild.dependOn(&install_test_zbuild.step); - const run_test_zbuild = b.addRunArtifact(test_zbuild); - const tls_run_test_zbuild = b.step("test:zbuild", "Run the zbuild test"); - tls_run_test_zbuild.dependOn(&run_test_zbuild.step); + const tls_test_zbuild = b.step("test:zbuild", "Run the zbuild tests"); + tls_test_zbuild.dependOn(&run_test_zbuild.step); tls_run_test.dependOn(&run_test_zbuild.step); - const module_sync = b.createModule(.{ - .root_source_file = b.path("test/sync.zig"), - .target = target, - .optimize = optimize, - }); - b.modules.put(b.dupe("sync"), module_sync) catch @panic("OOM"); - - const test_sync = b.addTest(.{ - .name = "sync", - .root_module = module_sync, - .filters = b.option([][]const u8, "sync.filters", "sync test filters") orelse &[_][]const u8{}, - }); - const install_test_sync = b.addInstallArtifact(test_sync, .{}); - const tls_install_test_sync = b.step("build-test:sync", "Install the sync test"); - tls_install_test_sync.dependOn(&install_test_sync.step); + const tls_test_fixtures = b.step("test:fixtures", "Run fixture integration tests"); + tls_run_test.dependOn(tls_test_fixtures); - const run_test_sync = b.addRunArtifact(test_sync); - const tls_run_test_sync = b.step("test:sync", "Run the sync test"); - tls_run_test_sync.dependOn(&run_test_sync.step); - tls_run_test.dependOn(&run_test_sync.step); - - module_sync.addImport("zbuild", module_zbuild); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "examples-full-build", + .cwd = "examples/full", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "examples-full-info", + .cwd = "examples/full", + .build_args = &.{"info"}, + .stdout_match = "full_example v1.0.0", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "manual-interop-build", + .cwd = "test/fixtures/manual_interop", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "manual-interop-run", + .cwd = "test/fixtures/manual_interop", + .build_args = &.{"run:myapp"}, + .stderr_match = "manual manual", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "manual-interop-cmd", + .cwd = "test/fixtures/manual_interop", + .build_args = &.{"cmd:demo"}, + .stdout_match = "manual interop ok", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "undeclared-external-module", + .cwd = "test/fixtures/undeclared_external_module", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "undeclared-external-step", + .cwd = "test/fixtures/undeclared_external_step", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "manual-step-named-test", + .cwd = "test/fixtures/manual_step_named_test", + .build_args = &.{"cmd:demo"}, + .stdout_match = "manual test step ok", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "aliases-nested", + .cwd = "test/fixtures/aliases_nested", + .build_args = &.{"check"}, + .stderr_match = "alias fixture ok", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "presets-prod", + .cwd = "test/fixtures/presets", + .build_args = &.{ "run:demo", "-Dpreset=prod" }, + .stderr_match = "level=warn tracing=false asset=dist/prod", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "presets-cli-override", + .cwd = "test/fixtures/presets", + .build_args = &.{ "run:demo", "-Dpreset=prod", "-Dconfig.log_level=debug" }, + .stderr_match = "level=debug tracing=false asset=dist/prod", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "presets-invalid-name", + .cwd = "test/fixtures/presets", + .build_args = &.{ "run:demo", "-Dpreset=missing" }, + .expect_exit = 1, + .stderr_match = "invalid value for -Dpreset: 'missing'", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "manual-module-collision", + .cwd = "test/fixtures/manual_module_collision", + .expect_exit = 1, + .stderr_match = "named module 'core' collides with an existing module registered before configureBuild", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "manual-step-collision", + .cwd = "test/fixtures/manual_step_collision", + .expect_exit = 1, + .stderr_match = "run command step 'cmd:demo' collides with an existing top-level step registered before configureBuild", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "import-namespace-named-options-collision", + .cwd = "test/fixtures/import_namespace_named_options_collision", + .expect_exit = 1, + .stderr_match = "options module 'config' collides with another zbuild-owned import name (named module)", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "import-namespace-dependency-default-collision", + .cwd = "test/fixtures/import_namespace_dependency_default_collision", + .expect_exit = 1, + .stderr_match = "dependency default module 'dep_pkg' collides with another zbuild-owned import name (options module)", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "inline-name-collision", + .cwd = "test/fixtures/inline_name_collision", + .expect_exit = 2, + .stderr_match = "collides with named module", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "dependency-bad-import", + .cwd = "test/fixtures/dependency_bad_import", + .expect_exit = 1, + .stderr_match = "could not resolve module 'missing' from dependency 'dep_pkg'", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "dependency-bad-lazy-path", + .cwd = "test/fixtures/dependency_bad_lazy_path", + .expect_exit = 1, + .stderr_match = "could not resolve named lazy path 'missing' from dependency 'dep_pkg'", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "root-module-string-options-module", + .cwd = "test/fixtures/root_module_string_options_module", + .expect_exit = 2, + .stderr_match = "options module 'config' is import-only and cannot be used as root_module", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "duplicate-installable-artifact-name", + .cwd = "test/fixtures/duplicate_installable_artifact_name", + .expect_exit = 2, + .stderr_match = "installable artifact names must be unique", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "alias-empty-depends-on", + .cwd = "test/fixtures/alias_empty_depends_on", + .expect_exit = 2, + .stderr_match = "aliases 'check': depends_on must not be empty", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "preset-unknown-option", + .cwd = "test/fixtures/preset_unknown_option", + .expect_exit = 2, + .stderr_match = "presets 'dev.config': unknown option 'missing'", + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "stdlib-passthrough-library", + .cwd = "test/fixtures/stdlib_passthrough", + .build_args = &.{"build-lib:mylib"}, + }); + addFixtureCommand(b, tls_test_fixtures, .{ + .name = "stdlib-passthrough-test", + .cwd = "test/fixtures/stdlib_passthrough", + .build_args = &.{"build-test:unit"}, + }); } diff --git a/build.zig.zon b/build.zig.zon index 5a55a88..ff9127d 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,10 +1,9 @@ -// This file is generated by zbuild. Do not edit manually. - .{ .name = .zbuild, - .version = "0.2.0", + .version = "0.4.0", .fingerprint = 0x60f98ac2bf5a915c, - .minimum_zig_version = "0.14.0", - .dependencies = .{}, + .minimum_zig_version = "0.16.0", .paths = .{ "build.zig", "build.zig.zon", "src" }, + .description = "Declarative build configuration for Zig projects", + .dependencies = .{}, } diff --git a/docs/AdvancedFeatures.md b/docs/AdvancedFeatures.md deleted file mode 100644 index a0fd193..0000000 --- a/docs/AdvancedFeatures.md +++ /dev/null @@ -1,102 +0,0 @@ -# Advanced Features - -## WriteFiles: Managing Generated Files - -The `write_files` feature in `zbuild` allows you to define sets of files or directories to be copied or generated during the build process. This is particularly useful for managing assets, configuration files, or generated code that your project depends on. The `write_files` section in `zbuild.zon` lets you specify these resources declaratively, and `zbuild` will generate the corresponding `build.zig` code to handle them. - -### Configuration -The `write_files` field is an object where each key is a named set of files, and the value defines the files or directories to include. Each entry can be marked as `private` (accessible only within the build script) or public (installable as part of the build output). The `items` field specifies the source paths and their destinations. -Here’s an example `zbuild.zon` snippet: - -```zon -.{ - .name = .myproject, - .version = "0.1.0", - .fingerprint = 0x90797553773ca567, - .minimum_zig_version = "0.14.0", - .paths = .{ "build.zig", "build.zig.zon", "src" }, - .write_files = .{ - .assets = .{ - .items = .{ - .@"logo.png" = .{ - .type = "file", - .path = "resources/logo.png", - }, - .config = .{ - .type = "dir", - .path = "resources/config", - .exclude_extensions = .{".tmp"}, - }, - }, - }, - }, - .executables = .{ - .myapp = .{ - .root_module = .{ - .root_source_file = "src/main.zig", - }, - }, - }, -} -``` - -#### How It Works -- `private`: If `true`, the files are added via `b.addWriteFiles()`, making them available only within the build script. If `false` or omitted, `b.addNamedWriteFiles()` is used, and the files are installable. -- `items`: A map of destination paths to source specifications: - - `file`: Copies a single file (e.g., `resources/logo.png` to `logo.png` in the write files directory). - - - `dir`: Copies a directory, with optional `exclude_extensions` or `include_extensions` filters. - -In the generated `build.zig`, this translates to: - -```zig -const write_files_assets = b.addNamedWriteFiles(); - -... - -_ = write_files_assets.addCopyFile(b.path("resources/logo.png"), "logo.png"); -_ = write_files_assets.addCopyDirectory(b.path("resources/config"), "config", .{ .exclude_extensions = &[_][]const u8{"tmp"} }); -``` - -## LazyPath Resolution: Flexible File References -`LazyPath` resolution enhances `zbuild` by allowing you to reference files dynamically across different sources (e.g., project files, `write_files`, `dependencies`, or `options`) using a colon-delimited syntax. This feature provides a flexible way to manage file paths without hardcoding them, making your build configuration more portable and maintainable. - -### Syntax -Paths in zbuild.zon can use the following format: -- Simple Path: `src/main.zig` – A direct filesystem path relative to the project root. -- Prefixed Path: `[:]` – Specifies the source of the path, such as the name of a write_files, dependency, or option. - -Supported sources include: -- writefiles: `:`: References a file or directory from a `write_files` set. -- dependency named lazy path: `:`: References a file from a dependency’s named lazy paths. -- dependency named writefiles: `::`: References a file from a dependency’s named write files. -- options: ``: References a `LazyPath` option value from the `options` section. - -### Configuration Example -Here’s an extended `zbuild.zon` showcasing `LazyPath` resolution: - -```zon -.{ - .name = .myproject, - .version = "0.1.0", - .fingerprint = 0x90797553773ca567, - .minimum_zig_version = "0.14.0", - .paths = .{ "build.zig", "build.zig.zon", "src" }, - .dependencies = .{ - .bar = .{ - .url = "git+https://github.com/org/bar", - }, - }, - .modules = .{ - .foo = .{ - .root_source_file = "bar:some_output", - }, - }, -} -``` - -#### How It Works -- `bar:some_output`: Resolves to the named lazy file `some_output` from the `bar` dependency. - -### Usage -- Extensibility: The `LazyPath` system supports additional sources like dependencies or options, making it adaptable to complex workflows. diff --git a/docs/MOTIVATION.md b/docs/MOTIVATION.md deleted file mode 100644 index e9b1d5f..0000000 --- a/docs/MOTIVATION.md +++ /dev/null @@ -1,145 +0,0 @@ -# Motivation for zbuild - -## Why zbuild? - -The Zig programming language offers a powerful and flexible build system integrated directly into its toolchain via `build.zig` files. This system, while highly customizable and programmatic, can become complex and verbose for larger projects or for developers who prefer a declarative approach to configuration. `zbuild` was created to address these challenges by providing an opinionated, ZON-based alternative to Zig's native build system, aiming to streamline the build process while retaining the power of Zig’s capabilities. - -The motivation behind `zbuild` stems from a desire to improve the developer experience for Zig projects, making it easier to define, manage, and share build configurations without sacrificing the language’s strengths. Here’s why `zbuild` exists and what it seeks to achieve: - ---- - -## Problems with the Status Quo - -### 1. Complexity of `build.zig` -Zig’s build system is a full-fledged programming environment written in Zig itself. While this offers unparalleled flexibility—allowing dynamic build logic, conditional compilation, and custom steps—it comes with a steep learning curve: -- Developers must write and maintain imperative Zig code for tasks like adding executables, libraries, or dependencies. -- Common build patterns (e.g., adding an executable with a test) require repetitive boilerplate code. -- Debugging build scripts can be challenging due to their programmatic nature. - -For small projects, this might be manageable, but as projects grow, the `build.zig` file can become a maintenance burden, especially for teams or newcomers unfamiliar with Zig’s build internals. - -### 2. Lack of Declarative Configuration -Many modern build tools (e.g., `Cargo` for Rust, `npm`/`package.json` for Node.js) offer declarative configuration files that define what to build rather than how to build it. Zig’s `build.zig` requires developers to specify both, which can feel overly hands-on for straightforward projects. There’s no built-in way to define a project’s structure in a simple, human-readable format without writing code. - -### 3. Inconsistent Build Patterns -Without a standardized structure, different Zig projects may adopt wildly different `build.zig` conventions. This inconsistency makes it harder for developers to: -- Quickly understand a new project’s build setup. -- Reuse build logic across projects. -- Share build configurations with the community. - -### 4. Limited Tooling Integration -While Zig’s build system is powerful, its programmatic nature doesn’t lend itself easily to integration with external tools like IDEs or CI/CD pipelines, which often expect structured configuration files (e.g., JSON or YAML) for validation, autocompletion, or automation. While ZON is still nascent, its smaller feature surface fits well. - ---- - -## Goals of zbuild - -`zbuild` aims to address these issues by introducing a layer of abstraction over Zig’s build system, guided by the following goals: - -### 1. Simplify Build Definition -By using a ZON-based configuration file (`zbuild.zon`), `zbuild` allows developers to declaratively specify their project’s components—dependencies, modules, executables, libraries, tests, and more—without writing Zig code. This reduces the cognitive load and makes build setup accessible to developers who may not be Zig experts. - -For example, instead of writing: - -```zig -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const module_myapp = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = b.standardTargetOptions(.{}), - }); - b.modules.put(b.dupe("myapp"), module_myapp) catch @panic("OOM"); - - const exe_myapp = b.addExecutable(.{ - .name = "myapp", - .root_module = module_myapp, - }); - - const install_exe_myapp = b.addInstallArtifact(exe_myapp, .{}); - const install_tls_exe_myapp = b.step("build-exe:myapp", "Install the myapp executable"); - install_tls_exe_myapp.dependOn(&install_exe_myapp.step); - b.getInstallStep().dependOn(&install_exe_myapp.step); - - const run_exe_myapp = b.addRunArtifact(exe_myapp); - if (b.args) |args| run_exe_myapp.addArgs(args); - const run_tls_exe_myapp = b.step("run:myapp", "Run the myapp executable"); - run_tls_exe_myapp.dependOn(&run_exe_myapp.step); - - const test_myapp = b.addTest(.{ - .name = "myapp", - .root_module = module_myapp, - }); - const install_test_myapp = b.addInstallArtifact(test_myapp, .{}); - const install_tls_test_myapp = b.step("build-test:myapp", "Install the myapp test"); - install_tls_test_myapp.dependOn(&install_test_myapp.step); - - const run_test_myapp = b.addRunArtifact(test_myapp); - const run_tls_test_myapp = b.step("test:myapp", "Run the myapp test"); - run_tls_test_myapp.dependOn(&run_test_myapp.step); -} -``` - - -You can define: -```zon -.{ - .name = .project, - .version = "0.1.0", - .fingerprint = 0xdeadbeefdeadbeef, - .minimum_zig_version = "0.14.0", - .paths = .{ "build.zig", "build.zig.zon", "src" }, - .executables = .{ - .myapp = .{ - .root_module = .{ - .root_source_file = "src/main.zig", - } - } - } -} -``` - -And let `zbuild` generate the necessary `build.zig`. - -### 2. Reduce Boilerplate -`zbuild` automates repetitive tasks like creating install steps, run commands, and tests for each target. It enforces a consistent structure, eliminating the need to manually replicate common patterns across projects. - -### 3. Enhance Readability and Maintainability -A ZON configuration is inherently more readable and diff-friendly than a programmatic script. It’s easier to see at a glance what a project builds, what it depends on, and how it’s configured. This also simplifies maintenance, as changes to the build setup are more predictable and less error-prone. - -### 4. Preserve Zig’s Power -While zbuild introduces a declarative layer, it doesn’t abandon Zig’s flexibility. It generates a build.zig file that can be customized further if needed, allowing developers to drop down to the native build system for advanced use cases. The generated file serves as a starting point, not a limitation. - -### 5. Foster a Standard Build Workflow -By providing an opinionated structure, zbuild encourages a consistent build workflow across Zig projects. This standardization can lower the barrier to entry for new contributors and make it easier to share build configurations or adopt best practices. - -## Usecases - -`zbuild` is particularly valuable for: -- **Beginners**: Developers new to Zig can define builds without learning the intricacies of build.zig. -- **Large Projects**: Teams managing multiple targets (executables, libraries, tests) benefit from a centralized, declarative configuration. -- **Open-Source Projects**: A readable `zbuild.zon` makes it easier for contributors to understand and modify the build setup. - -## Trade-Offs -Introducing `zbuild` comes with trade-offs: -- **Opinionation**: The tool enforces a specific structure, which may not suit every project’s needs. Developers requiring full control may prefer raw `build.zig`. -- **Maturity**: Incomplete features limit its scope, but the foundation is solid. - -For many, the benefits—simplicity, consistency, and tooling—outweigh these drawbacks. - -## Inspiration -`zbuild` draws inspiration from: -- **npm (Node.js)**: `package.json` provides a structured way to define scripts, dependencies, and metadata. -- **Cargo (Rust)**: A declarative Cargo.toml simplifies Rust project builds while retaining flexibility via `build.rs`. - -Unlike these tools, `zbuild` is tailored to Zig’s unique ecosystem, leveraging its native build system rather than replacing it. - -## Future Vision -`zbuild` aspires to become an indispensable companion in the Zig ecosystem, evolving beyond its current foundation to empower developers at every level. Its high-level ambitions include: - -- **Democratizing Zig Development**: Lower the barrier to entry with an intuitive, welcoming build experience, empowering newcomers and seasoned developers alike to embrace Zig’s potential. -- **Full Feature Parity**: Support all build.zig capabilities (e.g., depends_on, custom steps). -- **Seamless Build Excellence**: Evolve into a tool that effortlessly harnesses Zig’s full build capabilities, turning complexity into simplicity and fueling rapid project creation. -- **Thriving Zig Ecosystem**: Catalyze a flourishing community by streamlining dependency management and fostering collaboration, driving widespread adoption - -By alleviating the complexities of Zig’s build system while embracing its strengths, zbuild seeks to empower developers, making project setup and maintenance intuitive and efficient. diff --git a/docs/TODO.md b/docs/TODO.md deleted file mode 100644 index f3a48e9..0000000 --- a/docs/TODO.md +++ /dev/null @@ -1,22 +0,0 @@ -- [x] add test, fmt tls -- [x] add options to Config -- [x] single code path for Config processing - - always use codegen build.zig -- [x] compact union json parsing -- [x] fix how build_runner_path is set in cli - - will be resolved by only using build.zig -- [x] robust generate cli command - - write build.zig.zon - - read file / write directory flags -- [x] add init command -- [x] add fetch command -- [x] add named write files to Config -- [x] make ~~json~~zon parsing errors friendly - -- add named lazy paths to Config -- add fuzz to Config -- add additional module options, eg. addIncludePath -- conditionals? template strings? -- implement depends_on and test_runner -- lots of tests - diff --git a/docs/concepts.md b/docs/concepts.md new file mode 100644 index 0000000..8dac753 --- /dev/null +++ b/docs/concepts.md @@ -0,0 +1,258 @@ +# Conceptual Model + +This page answers the question the README does not try to answer in full: what is zbuild actually doing? + +If the README is the top-down path, this is the bottom-up path. The goal here is not to teach every field. The goal is to make the whole library feel coherent. + +## 1. zbuild is a translation layer + +zbuild is not a new build system. It is a compile-time translation layer on top of `std.Build`. + +The pipeline is: + +1. Zig evaluates `@import("build.zig.zon")` as a typed anonymous struct. +2. zbuild validates that value. +3. zbuild emits ordinary `std.Build` modules, artifacts, and steps from it. + +That has three consequences: + +- The compiler is the parser. +- The manifest type is the schema. +- The escape hatch is always just normal `build.zig` code. + +## 2. The manifest is a graph declaration + +Different sections create different kinds of graph nodes: + +| Section | Produces | Main public names | +|---|---|---| +| `modules` | reusable Zig modules | enum-literal import names like `.core` | +| `options_modules` | generated Zig config modules | enum-literal import names like `.config`; CLI flags like `-Dconfig.verbose` | +| `executables` | installable executable artifacts | `.myapp`, `build-exe:myapp`, `run:myapp` | +| `libraries` | installable library artifacts | `.mylib`, `build-lib:mylib` | +| `objects` | installable object artifacts | `.myobj`, `build-obj:myobj` | +| `tests` | test artifacts plus run/install steps | `test`, `test:unit`, `build-test:unit` | +| `fmts` | formatting steps | `fmt`, `fmt:src` | +| `runs` | arbitrary command steps | `cmd:deploy` | +| `aliases` | named aggregate top-level steps | `check`, `ci`, `release` | +| `dependencies` | loaded dependency build graphs | default module refs like `.zlib`, submodule refs like `"zlib:zlib"` | + +This is the first important idea: zbuild sections are not random manifest blobs. Each section exists because it corresponds to a specific class of `std.Build` node. + +`presets` are the exception on purpose: they do not create graph nodes. They are named bundles of `options_modules` overrides layered on top of the option schema. + +## 3. Ownership lives in syntax + +The cleanest part of zbuild's design is that reference syntax encodes ownership. + +### `root_module` + +- `.core` means a zbuild-owned named module +- `"shared"` means a manual module created with `b.addModule(...)` before `configureBuild` +- inline struct means define the module right here + +### `imports` + +- `.core` means a named module +- `.config` means an options module +- `.zlib` means a dependency default module +- `"shared"` means a manual module +- `"zlib:zlib"` means a dependency submodule + +### `depends_on` + +- `.myapp` means the install step for artifact `myapp` +- `"run:myapp"` means the exact generated step named `run:myapp` +- `"gen:prep"` means the exact manual top-level step named `gen:prep` + +That syntax split is the main reason the API can stay terse without becoming ambiguous everywhere. + +## 4. There are multiple namespaces, on purpose + +zbuild does not have one universal bag of names. It has several namespaces with different rules. + +### Import namespace + +Named modules, options modules, and dependency default modules all share one enum-literal import namespace. + +Examples: + +- `.core` +- `.config` +- `.zlib` + +Those names must be unique across those categories. zbuild rejects collisions instead of picking one by precedence. + +### Manual-module namespace + +Bare strings in `root_module` and `imports` are reserved for manual modules registered before `configureBuild`. + +Example: + +- `"shared"` + +This is a separate namespace on purpose. If a name belongs to zbuild, you should use the zbuild syntax for it. + +### Artifact shorthand namespace + +Executables, libraries, and objects share one shorthand namespace for `depends_on`. + +Example: + +- `.myapp` +- `.mylib` + +That shorthand maps to exactly one install step, so installable artifact names must be unique across those sections. + +### Top-level step namespace + +Generated step names and manual top-level steps live together in the top-level step namespace. + +Examples: + +- `build-exe:myapp` +- `run:myapp` +- `test` +- `cmd:deploy` +- `check` +- `gen:prep` + +Strings in `depends_on` target exact step names in this namespace. + +Some bare names are only reserved conditionally: `"test"` and `"fmt"` are zbuild-owned only when the manifest actually defines `tests` or `fmts`; otherwise those names remain available for manual top-level steps. + +## 5. Private modules are still zbuild modules + +`modules..private = true` means "do not export this module to `b.modules`", not "pretend it does not exist". + +Private named modules are still: + +- part of the zbuild-owned graph +- referenceable from other zbuild manifest entries +- subject to the same zbuild namespace rules + +The difference is visibility to outside `build.zig` consumers, not visibility inside the manifest. + +## 6. Validation splits by what is actually knowable + +zbuild validates in two phases because some facts are available at comptime and some are not. + +### Compile time + +These fail with `@compileError`: + +- unknown fields inside zbuild-owned sections +- bad local refs to zbuild-owned modules, artifacts, and generated steps +- namespace collisions within the zbuild-owned graph +- malformed reference syntax +- typed option schema mistakes + +### Configure time + +These fail while `configureBuild` runs, before the build graph executes: + +- missing manual modules or steps +- missing dependency exports +- missing dependency-backed lazy paths +- dependency artifact lookup failures + +This is not an arbitrary split. It follows ownership and knowability: + +- manifest-owned graph: knowable at comptime +- manual/dependency state: only knowable after `build.zig` and `b.dependency(...)` have run + +## 7. Options modules are generated config APIs + +`options_modules` are not just CLI flags. Each entry creates an importable Zig module. + +```zig +.options_modules = .{ + .config = .{ + .verbose = .{ + .type = .bool, + .default = false, + }, + .log_level = .{ + .type = .@"enum", + .values = .{ .debug, .info, .warn }, + .default = .info, + }, + }, +}, +``` + +Users set values with: + +```bash +zig build -Dconfig.verbose=true -Dconfig.log_level=warn +``` + +And Zig code imports: + +```zig +const config = @import("config"); +``` + +That is why `options_modules` live in the import namespace but are not valid `root_module` targets: they are config surfaces, not compilation roots. + +`presets` sit on top of that system, not beside it. A preset: + +- is selected explicitly with `-Dpreset=` +- only overrides `options_modules` +- does not redefine types +- does not create another imported module + +The precedence is: + +1. explicit `-Dmodule.option=...` +2. selected preset override +3. declared option default +4. `null` + +That keeps presets as a convenience layer for repeated option bundles, not a second configuration system. + +## 8. Interop is explicit, not magical + +Manual `build.zig` code still matters. The rule is simple: + +- register manual modules and manual top-level steps before `configureBuild` +- reference them from the manifest with bare strings + +Example: + +```zig +_ = b.addModule("shared", .{ + .root_source_file = b.path("src/shared.zig"), + .target = b.resolveTargetQuery(.{}), + .optimize = .Debug, +}); +_ = b.step("gen:prep", "manual prep step"); +``` + +Manifest side: + +```zig +.root_module = "shared" +.imports = .{"shared"} +.depends_on = .{"gen:prep"} +``` + +Aliases use that same `depends_on` model, but only create a named grouping step. They do not introduce a command or artifact of their own. + +That model stays understandable because the syntax tells you when you are leaving the zbuild-owned world. + +## 9. Why this feels coherent + +zbuild gets leverage from using Zig's own semantics instead of fighting them: + +- `build.zig.zon` is already typed +- `std.Build` is already the backend +- `build.zig` is already the escape hatch + +So the library only needs to add: + +- a graph-oriented manifest surface +- a consistent reference model +- early validation + +That is the whole design in one sentence: zbuild makes the static parts of a Zig build graph declarative without pretending the dynamic parts are declarative too. diff --git a/docs/motivation.md b/docs/motivation.md new file mode 100644 index 0000000..a522e8a --- /dev/null +++ b/docs/motivation.md @@ -0,0 +1,60 @@ +# Why zbuild? + +zbuild exists for a specific kind of Zig project: + +- the build graph is mostly static +- the same `std.Build` patterns repeat across many targets +- the team wants earlier, clearer failures than "some code in `build.zig` did the wrong thing" + +It is not trying to remove Zig from the build story. It is trying to remove repetitive graph wiring from the parts that are already declarative in practice. + +## The problem it attacks + +Zig's build system is powerful because it is just Zig. That is also the source of the friction: + +- a small executable often turns into a surprising amount of `build.zig` +- adding tests, runs, install steps, and module wiring scales linearly in boilerplate +- newcomers must learn a large API before they can express a simple graph + +For a lot of projects, that is the wrong abstraction level. The graph is static. The code is just encoding structure. + +## The design bet + +zbuild makes one strong bet: + +> `build.zig.zon` plus comptime is already enough structure to describe most static build graphs. + +That bet only works because Zig already gives zbuild the important pieces: + +- `@import("build.zig.zon")` returns a typed value at comptime +- `std.Build` is the real backend +- `build.zig` remains available for everything dynamic + +So zbuild does not need a runtime parser, a custom IR, or a replacement toolchain. It only needs a coherent manifest surface and a disciplined translation into `std.Build`. + +## What zbuild optimizes for + +zbuild is trying to maximize four things at once: + +- **Terseness** for the common static graph cases +- **Coherence** through explicit ownership and naming rules +- **Early failure** for manifest-owned mistakes +- **Interop** with manual `build.zig` code when the graph stops being static + +That is why the library uses syntax splits like enum literals vs strings instead of trying to infer intent from arbitrary names. The API is smaller and more learnable when "what kind of thing is this?" has a visible answer. + +## What it does not try to be + +zbuild is not: + +- a replacement for `build.zig` +- a universal abstraction over every `std.Build` feature +- a promise that all validation can happen at comptime + +If you need platform-conditional logic, generated inputs, custom discovery, or one-off graph surgery, write that in `build.zig`. zbuild is meant to coexist with that code, not ban it. + +## How to read the docs + +- Start with the [README](../README.md) if you want the fastest path from zero to a working build. +- Read [Conceptual Model](concepts.md) if you want the bottom-up explanation of namespaces, ownership, and validation phases. +- Keep [Schema Reference](schema.md) open when you need exact field types and generated step names. diff --git a/docs/schema.md b/docs/schema.md new file mode 100644 index 0000000..e96c501 --- /dev/null +++ b/docs/schema.md @@ -0,0 +1,477 @@ +# ZON Schema Reference + +This page is the bottom-most reference. If you are new to zbuild, read the [README](../README.md) first for the quickstart and [Conceptual Model](concepts.md) second for the mental model. + +This is the complete reference for zbuild's manifest fields. These fields are added to your standard `build.zig.zon` alongside Zig's own fields (`name`, `version`, `fingerprint`, `minimum_zig_version`, `paths`, `description`, `dependencies`). + +Unknown fields inside zbuild-owned sections are compile errors. Unknown top-level fields are left alone so Zig itself can evolve `build.zig.zon` without zbuild shadowing it. + +## `modules` + +Reusable code units registered with the build system. Modules can be referenced by name from executables, libraries, and tests via their `root_module` field. + +```zig +.modules = .{ + .core = .{ + .root_source_file = "src/core.zig", + .imports = .{ .utils, .zlib }, + .link_libc = true, + }, +}, +``` + +### Fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `root_source_file` | string | — | Path to the root source file | +| `target` | string | host | `"native"` or arch-os-abi triple (e.g. `"x86_64-linux-gnu"`) | +| `optimize` | enum literal | project default | `.Debug`, `.ReleaseSafe`, `.ReleaseFast`, `.ReleaseSmall` | +| `imports` | tuple | — | Modules, options_modules, or dependencies to import | +| `link_libraries` | tuple of strings | — | Dependency library artifacts to link (see below) | +| `include_paths` | tuple of strings | — | Include paths for C/C++ headers | +| `private` | bool | `false` | When `true`, not exported to `b.modules` | + +### Passthrough fields + +These map directly to `std.Build.Module.CreateOptions`: + +`link_libc`, `link_libcpp`, `single_threaded`, `strip`, `unwind_tables`, `dwarf_format`, `code_model`, `error_tracing`, `omit_frame_pointer`, `pic`, `red_zone`, `sanitize_c`, `sanitize_thread`, `stack_check`, `stack_protector`, `fuzz`, `valgrind`, `no_builtin` + +### `link_libraries` syntax + +Format: `"dep_name"` or `"dep_name:artifact_name"`. Resolves a library artifact from a declared dependency. This is distinct from LazyPath resolution. + +```zig +.link_libraries = .{ "zlib", "openssl:libssl" }, +``` + +### `imports` syntax + +Import entries can reference: +- **Named modules:** `.core` +- **Options modules:** `.config` +- **Dependency default modules:** `.zlib` +- **Dependency sub-modules:** `"zlib:zlib"` (imports a specific module from a dependency) +- **Manual modules:** `"shared"` resolved from `b.addModule(...)` before `configureBuild` + +Named modules, options modules, and dependency default modules share one enum-literal import namespace. Duplicate names across those categories are rejected during `configureBuild`. + +## `executables` + +Build targets that produce executable binaries. Each entry creates `build-exe:` (install) and `run:` (execute) steps. + +```zig +.executables = .{ + .myapp = .{ + .root_module = .core, // reference a named module + .version = "1.0.0", + }, +}, +``` + +### Root module forms + +The `root_module` field accepts three forms: + +1. **Enum literal** — references a named zbuild module: `.root_module = .core` +2. **String** — references a manual `b.addModule(...)` module registered before `configureBuild`: + ```zig + .root_module = "shared" + ``` +3. **Inline struct** — defines the module inline, with an optional `name` override: + ```zig + .root_module = .{ + .root_source_file = "src/main.zig", + .imports = .{ .core, .config, "shared" }, + .name = "custom_name", // optional: internal inline-module name used for import wiring + }, + ``` + +Inline root modules are not importable targets in the manifest. If provided, `root_module.name` must be unique across all inline root modules and must not collide with a named module. + +`root_module` string refs are reserved for manual modules only. Names that belong to zbuild-owned modules or `options_modules` are rejected. + +### Fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `root_module` | ref or struct | required | See root module forms above | +| `version` | string | — | Semantic version (e.g. `"1.0.0"`) | +| `linkage` | enum literal | — | `.static` or `.dynamic` | +| `dest_sub_path` | string | — | Custom install subdirectory | +| `depends_on` | tuple | — | Steps that must complete first (artifact install step or exact top-level step name) | +| `max_rss` | int | — | Maximum RSS for the build step | +| `use_llvm` | bool | — | Use LLVM backend | +| `use_lld` | bool | — | Use LLD linker | +| `zig_lib_dir` | string | — | Custom Zig lib directory (LazyPath resolved) | +| `win32_manifest` | string | — | Win32 manifest file (LazyPath resolved) | + +## `libraries` + +Same fields as executables, plus `win32_module_definition` and `linker_allow_shlib_undefined`. Each entry creates a `build-lib:` step. + +```zig +.libraries = .{ + .mylib = .{ + .root_module = .core, + .linkage = .static, + .version = "0.1.0", + }, +}, +``` + +| Additional Field | Type | Default | Description | +|------------------|------|---------|-------------| +| `win32_module_definition` | string | — | Win32 module definition file (LazyPath resolved) | +| `linker_allow_shlib_undefined` | bool | — | Allow undefined symbols in shared libs | + +## `objects` + +Compiled object files. Simpler subset — no `version`, `linkage`, `dest_sub_path`, or `win32_manifest`. Each entry creates a `build-obj:` step. + +```zig +.objects = .{ + .myobj = .{ + .root_module = .core, + }, +}, +``` + +Supported fields: `root_module`, `max_rss`, `use_llvm`, `use_lld`, `zig_lib_dir`. + +## `tests` + +Test targets. Each entry creates `test:` (run) and `build-test:` (install) steps. All tests also join the aggregate `test` step. + +```zig +.tests = .{ + .unit = .{ + .root_module = .{ + .root_source_file = "src/test.zig", + .imports = .{.core}, + }, + .filters = .{ "specific_test_name" }, + }, +}, +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `root_module` | ref or struct | required | See root module forms | +| `filters` | tuple of strings | — | Test name filters | +| `max_rss` | int | — | Maximum RSS | +| `use_llvm` | bool | — | Use LLVM backend | +| `use_lld` | bool | — | Use LLD linker | +| `zig_lib_dir` | string | — | Custom Zig lib directory | +| `test_runner` | struct | — | Custom test runner struct with `path` and `mode` (`.simple` or `.server`) | +| `emit_object` | bool | `false` | Emit a test object file instead of a runnable test binary | +| `linker_allow_shlib_undefined` | bool | — | Allow undefined symbols in shared libs the test links against (e.g. napi C symbols Node provides at dlopen time) | + +Filters can be overridden from the CLI: `-D.filters=specific_test`. + +## `fmts` + +Wraps `zig fmt`. Each entry creates `fmt:` and joins the aggregate `fmt` step. + +```zig +.fmts = .{ + .src = .{ + .paths = .{ "src", "tests" }, + .exclude_paths = .{ "src/generated" }, + .check = true, + }, +}, +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `paths` | tuple of strings | `&.{}` | Paths to format | +| `exclude_paths` | tuple of strings | `&.{}` | Paths to exclude | +| `check` | bool | `false` | Check formatting without modifying | + +## `runs` + +System commands. Each entry creates a `cmd:` step. Two syntax forms: + +### Short form + +Bare tuple of strings — becomes `addSystemCommand` args: + +```zig +.runs = .{ + .fmt = .{ "zig", "fmt", "src" }, +}, +``` + +### Long form + +Struct with `cmd` plus optional fields: + +```zig +.runs = .{ + .deploy = .{ + .cmd = .{ "./scripts/deploy.sh", "--env", "staging" }, + .cwd = "scripts", + .env = .{ .NODE_ENV = "production" }, + .inherit_stdio = true, + .depends_on = .{.myapp}, + }, +}, +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `cmd` | tuple of strings | required | Command and arguments | +| `cwd` | string | inherit | Working directory (LazyPath resolved) | +| `env` | struct | inherit | Environment variables (field name = key, value = value) | +| `inherit_stdio` | bool | `false` | Forward stdio to terminal | +| `stdin` | string | — | Bytes piped to stdin | +| `stdin_file` | string | — | File piped to stdin (LazyPath resolved) | +| `depends_on` | tuple | — | Steps that must complete first (artifact install step or exact top-level step name) | + +`stdin` and `stdin_file` are mutually exclusive. + +`depends_on` accepts both enum literals and strings: +- **Enum literals:** `.myapp` resolves to the install step for artifact `myapp` +- **Strings with zbuild-owned names:** `"run:myapp"`, `"test:unit"`, `"cmd:demo"`, `"test"`, or `"fmt"` resolve to exact manifest-owned top-level steps when zbuild generates those step names +- **Other strings:** `"gen:prep"` resolves to a manual top-level step created before `configureBuild` + +If the manifest has no `tests` or `fmts`, bare `"test"` and `"fmt"` remain available for manual top-level steps registered before `configureBuild`. + +Installable artifact names across `executables`, `libraries`, and `objects` must be unique. This keeps enum-literal shorthand unambiguous: `.myapp` always means exactly one artifact install step. + +Manual top-level steps must be created with `b.step(...)` before calling `configureBuild`. + +## `aliases` + +Named aggregate top-level steps. Aliases do not execute commands or create artifacts; they only group existing steps under a human-facing name such as `check`, `ci`, or `release`. + +```zig +.aliases = .{ + .check = .{ + .description = "Run the main verification steps", + .depends_on = .{ + "test", + "fmt", + }, + }, +}, +``` + +Each alias entry creates a top-level step with exactly that name. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `depends_on` | tuple | required | Non-empty list of artifact install-step shorthands or exact top-level step names | +| `description` | string | `"Run the alias"` | Top-level step description | + +`depends_on` uses the same syntax and resolution rules as `runs.depends_on`: +- **Enum literals:** `.myapp` resolves to the install step for artifact `myapp` +- **Strings with manifest-owned names:** `"test"`, `"fmt"`, `"run:myapp"`, `"cmd:deploy"`, or another alias name resolve to exact zbuild-owned top-level steps +- **Other strings:** `"gen:prep"` resolves to a manual top-level step created before `configureBuild` + +Alias names live in the top-level step namespace. They must not collide with generated zbuild steps, manual steps registered before `configureBuild`, or installable artifact shorthand names such as `.myapp`. + +## `options_modules` + +Configurable build options exposed as importable Zig modules. Users set values via `-D.