Introduction

laze is a build system for C/C++ projects. It can be used in many cases instead of Make, CMake or Meson.

It's main differentiators are:

  • declarative, easy to use yaml-based build descriptions
  • designed to handle massively modular projects
    • main driver: RIOT OS, which builds > 100k configurations as part of its CI testing
    • powerful module system
    • first-class cross compilation
    • first-class multi-repository support
  • optimized for fast build configuration
    • written in Rust
    • extensive caching
    • multithreaded build configuration
  • optimized for fast building
    • automatically re-uses objects that are built identically for different build configurations
  • actual building is done transparently by Ninja

This book contains a guide on how to install and use laze, and a reference for its build description format.

How does it look

Consider a simple application consisting of a single hello.c source file.

This laze file, saved to laze-project.yml next to hello.c, would build it:

# import default context and builder
imports:
  - laze: defaults

# define an application named "hello"
apps:
  - name: hello
    sources:
        - hello.c

The application can now be built:

laze build

The resulting executable ends up in build/out/host/hello/hello.elf.

Alternatively,

laze build run

Would run the executable, (re-)building it if necessary.

Contributing

laze is free and open source. You can find the source code on GitHub and issues and feature requests can be posted on the GitHub issue tracker. laze relies on the community to fix bugs and add features: if you'd like to contribute, please read the CONTRIBUTING guide and consider opening a pull request.

License

laze source code is released under the Apache 2.0 license.

This book has been heavily inspired by and is based on the mdBook user guide, and is thus licenced under the Mozilla Public License v2.0.

Installation

There are multiple ways to install the laze CLI tool. Choose any one of the methods below that best suit your needs. If you are installing laze for automatic deployment, check out the continuous integration chapter for more examples on how to install.

Distribution packages

The easiest way to install laze is probably by using a package for your distribution.

Available packages:

Distribution                      Package name
Arch Linux AURlaze-bin

Dependencies

laze requires Ninja. You can download the Ninja binary or find it in your system's package manager.

Pre-compiled binaries

Executable binaries are available for download on the GitHub Releases page. Download the binary for your platform (Windows, macOS, or Linux) and extract the archive. The archive contains laze executable.

To make it easier to run, put the path to the binary into your PATH.

Build from source using Rust

To build the laze executable from source, you will first need to install Rust and Cargo. Follow the instructions on the Rust installation page. laze currently requires at least Rust version 1.54.

Once you have installed Rust, the following command can be used to build and install laze:

cargo install laze

This will automatically download laze from crates.io, build it, and install it in Cargo's global binary directory (~/.cargo/bin/ by default).

Installing the latest master version

The version published to crates.io will ever so slightly be behind the version hosted on GitHub. If you need the latest version you can build the git version of laze yourself. Cargo makes this super easy!

cargo install --git https://github.com/kaspar030.git laze

Again, make sure to add the Cargo bin directory to your PATH.

Configuration

Currently, laze has no configuratin itself. Only its <TAB> (shell-) completions need some set-up.

Shell completions

laze provides dynamic shell completions (TAB completions in the shell). In order to use them, they need to be set up.

If you've installed laze using your package manager, chances are that this is already working out of the box.

Use the snippet that corresponds to your shell to automatically load the correct completions on shell start (if in doubt, you're probably running bash):

Bash

echo "source <(COMPLETE=bash laze)" >> ~/.bashrc

Elvish

echo "eval (E:COMPLETE=elvish laze | slurp)" >> ~/.elvish/rc.elv

Fish

echo "source (COMPLETE=fish laze | psub)" >> ~/.config/fish/config.fish

Powershell

echo "COMPLETE=powershell laze | Invoke-Expression" >> $PROFILE

Zsh

echo "source <(COMPLETE=zsh laze)" >> ~/.zshrc

The shell will need to be restarted for this to take effect. In order to just set up completions for the currently running shell session, issue just the source ... command, e.g., source <(COMPLETE=bash laze).

Getting Started

Running Laze

To build an application for a builder, run

laze build -b <builder> -a <application>

Contributing

If you are interested in making modifications to laze itself, check out the Contributing Guide for more information.

Build file structure

laze uses yaml files to describe a project.

Each laze project consists of a file named laze-project.yml in the project's root folder, and any number of laze.yml files in subdirectories.

Please see the reference for a detailed description of the file format.

laze will always read laze-project.yml and all referenced laze.yml files of a project. Don't worry, that's fairly fast, e.g., reading and parsing ~650 build files of RIOT takes ~35ms on a Thinkpad T480s. And it's cached, if no buildfile has been changed since it was last read, loading the cache takes less than 10ms.

Once laze has read the build files, it will configure builds as requested when executing it. This will resolve all dependencies for the requested builds, configure the environments and write a Ninja build file.

Once done configuring, laze will automatically call Ninja with the changed build configuration. Ninja will then do the actual building.

Builds, Apps, Builders, Contexts, Modules, Tasks

Conceptionally laze builds apps for builders. Each app/builder combination that laze configures is called a build.

An app represents a binary, and a builder would be e.g., the toolchain configuration for the host system.

Builders are contexts. All contexts (and thus builders) have a parent, up to the root context which is called "default". A builder is just a context that apps can be built for. Any context with buildable: true is a builder.

A context could be "linux", or "ubuntu" with parent context "linux". A builder could then be "amd64" with parent context "ubuntu". Builders can have other builders as parent context.

Contexts (and thus builders) inherit modules and variables from their parents.

Apps can depend on modules, which can have dependencies themselves. Technically, an app is just a special kind of module that can be built for a builder.

Apps and modules consist of zero or more source files.

Tasks are operations that can be run on binaries of a build configuration. They can be used to execute a compiled binary, run it through a debugger, install some files, update/reset an embedded target, ...

Tasks are executed by laze (not ninja), after building possible dependencies.

Module Dependencies

Dependency types

Apps/modules can depend on other modules. There are multiple dependency types:

  1. "depends" -> hard depend on another module, import env

    This module won't work unless the dependency is usable (not disabled, not conflicted by another module and its own dependencies can be resolved). It is pulled in and the dependency's exported environment will be imported.

    "depends" is equivalent to specifying both "selects" "uses".

  2. "selects" -> hard depend on another module, don't import env

    like 1.), the dependency will be pulled in, but the dependency's exported environment will not be imported.

  3. "uses" -> don't depend on module, but import env if module is part of build

    The dependency will not be pulled in by this module, but if it is part of the build through other means (e.g., another module depends on it), this module will import the dependency's exported environment.

  4. "conflicts" -> the modules cannot be used together in the same build.

  5. "optionally depends" -> soft depend on another module

    If the dependency is usable (not disabled, not conflicted by another module and its own dependencies can be resolved), it will be pulled in and its exported environment will be imported. If the dependency is not usable, the build continues as if the dependency was not specified at all.

  6. "optionally select" -> soft depend on another module (without importing env)

    If the dependency is usable (not disabled, not conflicted by another module and its own dependencies can be resolved), it will be pulled in, but its exported environment will not be imported.

  7. "if this than that" -> if "this" is part of the dependency tree, "depend" on "that"

  8. "optional if this than that" -> same as 6.) but as soft dependency.

Object Sharing

Object Sharing in laze

Object Sharing is a useful feature of laze that enables the sharing of identical compiled objects between builds. This means that if two different builds are using the same module and are compiled for the same builder, laze will avoid compiling the module multiple times.

When generating a build plan, laze would normally place the objects of a module in a folder like build/out/<builder>/<app>/src/<module>. With Object Sharing enabled, laze will instead place objects in build/objects/<source_path>/filename.<hash>.o, where hash represents the hash of the ninja build statement used to build the file. In addition, laze makes sure that it doesn't include duplicate build statements.

This scheme effectively and automatically makes laze apps share common objects, which eliminates the need for users to take any action to achieve this. In testing with the RIOT OS, this feature has reduced the number of built objects by approximately 30-40%.

In summary, Object Sharing in laze is a powerful feature that streamlines the build process by avoiding the redundant compilation of identical objects. This can lead to significant time and resource savings for users, particularly in large-scale projects.

Laze File Reference

This chapter contains the reference of laze's YAML build file format.

TODO: it is currently incomplete ...

laze_required_version

Expects a semver version string (a.b.c). Laze will refuse to read the file if its own version is smaller.

Example:

laze_required_version: 1.0.0

app

apps contains a list of app entries.

An app represents a binary that can be built for builders.

Example:

apps:
 - name: hello-world
   # ... possible other fields
 - name: foobar
   # ... possible other fields

app fields

As an app is just a special kind of module, they share all fields.

builders

builders contains a list of builder entries.

A builder represents a configuration and set of available modules that an app can be built for.

Example:

builders:
 - name: name_of_this_builder.
   # ... possible other fields
 - name: name_of_other_builder
   # ... possible other fields
contexts:
 - name: name_of_another_builder
   buildable: true
   # ... possible other fields

builder fields

As a builder is just a context that has buildable: true set, they share all fields.

contexts

contexts contains a list of context entries.

Example:

contexts:
 - name: name_of_this_context.
   # ... possible other fields
 - name: name_of_other_context
   # ... possible other fields

context fields

name

Name of this context. Any UTF8 string is valid. This will be used as part of file- and foldernames, so better keep it simple.

Context names must be unique.

Each context must have a name field, all other fields are optional.

Example:

contexts:
 - name: name_of_this_context.
   # ... possible other fields
 - name: name_of_other_context
   # ... possible other fields

parent

The parent of this context. If unset, defaults to default.

Example:

context:
 - name: some_name
   parent: other_context_name

env

A map of variables. Each map key correspondents to the variable name. Each value must be either a single value or a list of values.

Example:

context:
 - name: some_name
   env:
     CFLAGS:
       - -D_this_is_a_list
       - -D_of_multiple_values
     LINKFLAGS: -Ithis_is_a_single_value

selects

List of modules that are always selected for builds in this context or any of its children.

Example:

context:
 - name: birthday_party
   selects:
     - cake
     - music

disable

List of modules that are disabled for builds in this context or any of its children.

Example:

context:
 - name: kids_birthday_party
   parent: birthday_party
   disables:
     - beer

rules

This fiels contains a list of laze build rules.

A laze build rule mostly correspondents to Ninja rules.

Example:

contexts:
  - name: default
    rules:
      - name: CC
        in: c
        out: o
        cmd: ${CC} ${in} -o ${out}

rule fields

name

This field contains the name for this rule.

It will be used as rule name (or rule name prefix) in the generated Ninja build file, so use short, capital names/numbers/underscores like "CC", "CXX", ...

Some names have special meaning in laze:

  • LINK is the rule used to combine compiled source files for a given application.
  • GIT_DOWNLOAD will be used for downloading source files
  • GIT_PATCH will be used for applying patches on git repositories.

Example:

context:
 - # ...
   rules:
   - name: CC
   # ... possible other fields

cmd

This field contains the command to be run when the output needs to be rebuilt. Will end up in Ninja's "command" field for this rule.

Example:

    rules:
      - name: CC
        cmd: "${CC} -c ${in} -o ${out}""
        # ... other fields ...

description

Human readable description, will end up in Ninja's "description" field for this rule.

Example:

    rules:
      - name: CC
        description: "Compiling ${in}..."
        # ... other fields ...

in

in is used to specify the extension of input files for this rule.

laze will look up a rule for each source file depending on this field.

Example:

    rules:
      - name: CC
        description: CC ${out}
        in: "c"
        # ... other fields ...

out

out is used to specify the extension of output files for this rule.

laze will use this to generate output file names.

Example:

    rules:
      - name: CC
        description: CC ${out}
        in: "c"
        out: "o"
        # ... other fields ...

options

Currently unused.

gcc_deps

This field is used to enable Ninja's automatic header dependency tracking.

It takes a filename (usually containing $out) and will make Ninja read that file as Makefile expecting to contain extra dependencies of the source file.

laze will use this to set depfile = ... combined with deps = gcc in the generated Ninja build file.

See the Ninja Manual for more information.

Example:

    rules:
      - name: CC
        description: CC ${out}
        in: "c"
        out: "o"
        gcc_deps: "$out.d"
        cmd: "${CC} -MD -MF $out.d ${CFLAGS} -c ${in} -o ${out}"

rspfile

Use this to specify a file containing additional command line arguments. Supposed to be used in combination with rspfile_content.

This can be used if resulting command lines get too long, and the used tool supports reading arguments from file.

Example:

    rules:
      - name: LINK
        in: 'o'
        rspfile: $out.rsp
        rspfile_content: $in
        cmd: ${LINK} @${out}.rsp -o ${out}

rspfile_content

Use this to specify the contents of an rspfile.

Example:

    rules:
      - name: LINK
        in: 'o'
        rspfile: $out.rsp
        rspfile_content: $in
        cmd: ${LINK} @${out}.rsp -o ${out}

pool

This allows limiting execution of this rule's to named concurrency pools. See the Ninja Manual for more information.

Currently, the only supported pool is Ninja's predefined console pool.

From the Ninja manual:

It has the special property that any task in the pool has direct access to the
standard input, output and error streams provided to Ninja, which are normally
connected to the user’s console (hence the name) but could be redirected. This
can be useful for interactive tasks or long-running tasks which produce status
updates on the console (such as test suites).

While a task in the console pool is running, Ninja’s regular output (such as
progress status and output from concurrent tasks) is buffered until it
completes.

Example:

    rules:
      - name: FOO
        pool: console
        cmd: some_command_that_needs_console_input

always

This boolean flag makes the rule always run. It basically makes the resulting ninja build entry "phony".

Currently, this only has any effect for the special LINK rule.

modules

'modules' contains a list of module entries.

Example:

modules:
 - name: name_of_this_module.
   # ... possible other fields
 - name: name_of_other_context
   # ... possible other fields

module fields

name

Name of this module. Any UTF8 string is valid. This will be used as part of file- and foldernames, so better keep it simple.

Within each context, module names must be unique.

Each module must have a name. If the field is ommitted, ${relpath} is used.

Example:

modules:
 - name: name_of_this_module.
   # ... possible other fields
 - name: name_of_other_module
   # ... possible other fields

sources

List of source files used to build this module.

This field is optional.

The file path is relative to the directory of the yaml file this module is described in.

Example:

modules:
 - name: cake
   sources:
     - sugar.c
     - flour.c
     - dressings/cream.c
   # ... possible other fields

depends

List of modules this module depends on.

If a module depends on another module, it will pull that module into the build, and import that module's exported environment.

Note: depending a module is equivalent to both selecting and using it.

If a dependency name is prefixed with "?", the dependency turns into an optional dependency. That means, if the dependency is available, it will be depended on, otherwise it will be ignored.

Example:

modules:
 - name: datetime
   depends:
     - date
     - time
   # ... possible other fields
 - name: party
   depends:
     - people
     - ?music
     - ?alcohol
   # ... possible other fields

selects

List of modules this module selects.

If a module depends on another module, it will pull that module into the build.

If a dependency name is prefixed with "?", the dependency turns into an optional dependency. That means, if the dependency is available, it will be selected, otherwise it will be ignored.

Example:

modules:
 - name: release_build
   selects:
     - optimize_speed
     - disable_debug
   # ... possible other fields

uses

List of modules this module uses.

If a module uses another module, it will import that module's exported environment, if that module is part of the build.

Example:

modules:
 - name: datetime
   uses:
     - timezone_support
   # ... possible other fields

conflicts

List of modules that this module conflicts. Two conflicting modules cannot both be part of a build.

A conflicting B implies B conflicting A.

It is possible to conflict a provided feature to ensure a module is the only selected module providing a feature, but provides_unique should be used for this.

Example:

modules:
 - name: gnu_libc
   conflicts:
     - musl_libc
   # ... possible other fields

provides

List of features that this module provides.

If a module depends on or selects something provided by another module, it works like an alias.

A feature can be provided by multiple modules. In that case, all providing modules will be considered. Unless the dependency is optional, it fails if not at least one module can be resolved. All modules that resolve will be used.

See also provides_unique

Example:

modules:
 - name: amazon_s3
   provides:
     - s3_api
   # ... possible other fields
 - name: backblaze_s3
   provides:
     - s3_api
   # ... possible other fields

 - name: s3_storage
   depends:
     # both "amazon_s3" and "backblaze_s3" will be added to dependencies
     - s3_api

provides_unique

List of features that this module provides, but needs to be the only provider.

provides_unique can be used for this.

Adding to provides_unique is equivalent to adding to both provides and conflicts.

Example:

modules:
 - name: gnu_libc
   # ensure only one "libc" will be chosen
   provides_unique:
     - libc
 - name: musl_libc
   provides_unique:
     - libc
   # ... possible other fields

subdirs

subdirs contains a list of foldernames. Laze will parse <foldername>/laze.yml.

Example:

subdirs:
  - drivers
  - utils

imports

imports contains a list of import entries, each representing a local or remote source for additional laze projects.

Example:

imports:
 - git:
    - url: https://github.com/kaspar030/laze
      tag: 0.1.17

import types

git

A git import makes laze clone additional laze files from a git repository.

git requires a url. Additionally, commit, tag or branch can be set. If neither is specified, the default branch will be checked out.

Example:

imports:
 - git:
    url: https://example.com/foo
    commit: 890c1e8e02b0a872d73b1e49b3da45a5c8170016
    # or
    # tag: v1.2.3
    # or
    # branch: main

laze

A laze import allows using laze files that are bundled inside the laze binary.

laze needs the name of the desired file.

Currently, the following are defined:

NamePurpose
defaultsa default context and host builder for simple C projects

Example:

imports:
  - laze: defaults

path

A path import allows using a local directory as import source.

If path is not absolute, it is considered relative to the project root.

Optionally, path can be symlinked into the imports folder inside the laze build directory by setting symlink: true in the import. The symlink name within $build_dir/imports defaults to the last path component of path. This can be changed by setting name. Using a symlink helps turning absolute pathnames into relative ones. This might be desirable for privacy reasons, or help with reproducible builds.

Example:

imports:
  - path: /path/to/local/folder
  - path: /path/to/another/local/folder
    symlink: true
  - path: /path/to/a/third/local/folder
    symlink: true
    name: folder3

Variables defined by laze

appdir

This variable contains the path in which the app of a build was defined.

Useful mostly for tasks, as ${relpath} would evaluate to the folder in which the task is defined.

Examples:

# in apps/foo/laze.yml
apps:
  - name: foo_app


# in modules/foo/laze.yml
modules:
  - name: foo
    env:
      export:
        CFLAGS:
          - -DAPPDIR=\"${appdir}\"

builder

This variable contains the name of the builder.

Examples:

modules:
  - name: foo
    env:
      export:
        CFLAGS:
          - -DBUILDER=\"${builder}\"

modules

This variable evaluates to a list of all modules used in a build.

relpath

This variable is evaluated early and will be replaced with the relative (to the project root) path of the laze yaml file.

Example:

modules:
  - name: foo
    env:
      export:
        CFLAGS:
          - -I${relpath}/include

root

This variable will be replaced with the relative path to the root of the main project. This can be used for specifying root-relative path names. Usually (for laze projects that were not imported), this contains . If a laze file is part of an import of another laze project, ${root} contains the relative path to the location where the import has been downloaded.

Example:

# in some project:
imports:
  - git:
     url: .../foo.git
     commit: ...


# in .../foo.git/some/subdir/laze.yml:

modules:
  - name: foo
    env:
      export:
        CFLAGS:
          - -I${root}/include
          # this will evaluate to `-Ibuild/imports/foo-<hash>/include`

srcdir

Contains the relative (to project root) base path of a module's source files. If a module has not been downloaded, this is usually identical to ${relpath}. For downloaded modules, it points to the module's download folder.