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 AUR | laze-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
.
If you are interested in making modifications to laze itself, check out the Contributing Guide for more information.
Getting Started
Running Laze
To build an application for a builder, run
laze build -b <builder> -a <application>
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:
-
"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".
-
"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.
-
"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.
-
"conflicts" -> the modules cannot be used together in the same build.
-
"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.
-
"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.
-
"if this than that" -> if "this" is part of the dependency tree, "depend" on "that"
-
"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 filesGIT_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:
Name | Purpose |
---|---|
defaults | a 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.