Introduction

So, what is leafbuild?

leafbuild is an open-source C/C++ meta build system. It was designed as an alternative to cmake and meson.

Both the build system and the docs are WIP; keep this in mind.

What backends will it be able use?

ninja, make(not implemented yet).

Quick Start

Still a work in progress; as of now it’s not really useful unless you are a developer of the build system.

If you are interested in contributing, see the developer resources section of this book.

Supported Languages

The build system supports the following languages:

C

What compilers will it be able to use?

gcc, clang and msvc

C++

What compilers will it be able to use?

gcc, clang and msvc

Assembly

What assemblers will it be able to use?

gas and nasm

Syntax

The full syntax reference.

Comments

Same as in C/C++, a single line comment starts with // and goes on to the end of the line, and a block comment starts with /* and ends with */

Examples:

// a single line comment

// another single line comment

/*
 A block comment
 that can
 go on for
 multiple
 lines
 */

Values

Integer values

As of now, only 32-bit signed ints are supported. Int values work as they do in C/C++:

  • prefixed with 0 means it is an octal number
  • prefixed with 0x means it is a hex number

Examples of values:

0, 1, 2,
0777, // octal number
0x12349abcdef, // hex number

Booleans

true and false, just like in C++, or C with the <stdbool.h> header.

true, false

String values

A simple string begins and ends with ', and may have escaped apostrophes; should not contain newlines. You can also use multiline strings; those begin and end with '''.

Examples:

'A simple single line string',

'A simple string with \'escaped\' apostrophes'

'''A
multiline
string
example
'''

Vectors

{v0, v1, v2, ...}

Where v0, v1, v2, ... are of the same type.

Getting a value out of a vector

Same as in C/C++:

{1, 2, 3}[0] = 1
{1, 2, 3}[1] = 2
{1, 2, 3}[2] = 3

Maps

{k0: v0, k1: v1, k2: v2, ...}

Where v0, v1, v2, ... are of the same type, and k0, k1, k2 should be names. Example:

{
    a: 1,
    b: 2+3,
    c: '1',
    d: '''
        A
        B
        C
       '''
}

Getting a value out of a map

Same as with vectors, but pass a string with the key instead of the index.

{a: 1, b: 2+3, c: 9*10}['a'] = 1
{a: 1, b: 2+3, c: 9*10}['b'] = 5
{a: 1, b: 2+3, c: 9*10}['c'] = 90

The ternary conditional operator ?:

You can also use C/C++’s ternary conditional operator. Examples:

true ? 1 : 0

false ? 2 + 3 : 4 + 5

Variables

Variables, once assigned a value, will live for as long as the file they are declared in is processed.

Declaring

Variables are declared with the let keyword:

let variable_name = value;

Examples:

let a = 0;
let b = 1;
let c = 'a string';
let d = '''A
multiline
string''';
let e = a; // e = 0

Assigning a value to a variable

Like in C/C++:

let a = 0; // declare it
a = 1; // assign 1 to it
a += 1; // and add one to it; same as a = a + 1
a -= 1; // subtract one; same as a = a - 1
a *= 12; // multiply by 12; same as a = a * 12
a /= 2; // divide by 2; same as a = a / 2
a %= 3; // take modulo 3 and assign it back; same as a = a % 3

values cannot change their type, unless the type changes into the error type, in which case it can be assigned back to the type of the original value.

Accessing properties

You can access properties of values like so:

value.property_name

Calling functions

You can call functions like this:

function_name(positional_args, kwargs)

The positional_args is a list of comma-separated args, and kwargs is a list of comma-separated key-value arguments, the key and value being separated by =.

Examples:

f(a, b, c: d, e: f)
// note that you can also have only positional args or kwargs, without needing the extra comma between them
g(0, a)
h(a: b, c: d)

project()

You can also have a trailing comma and split function calls over multiple lines:

f(
  0,
  1,
)
g(
  a: b,
  c: d,
)

Calling methods

You can call methods like this:

base_value.method_name(positional_args, kwargs)

Note that method_name(positional_args, kwargs) works the same way functions do, so all the rules described in calling functions apply here as well.

If(conditionals)

Work the same as in C/C++, the only difference being you don’t need the parentheses after an if.

if condition {
statements
} else if condition2 {
statements2
} else if condition3 {
statements3
} /*...*/ {
/*...*/
} else {
// in case all conditions above failed
}

Foreach (and repetition)

foreach x in collection {
    do_something_with x
}

Where collection is either:

  • A vector and then x is the value of the current element
  • A map and then x is a map_pair; this type has 2 properties: key and value. The key property is always a string. The value could be anything.

Examples

Foreach over vector:

foreach x in {1, 2, 3, 4, 5} {
    print(x);
}
// prints:
/*
-- 1
-- 2
-- 3
-- 4
-- 5
*/

Foreach over map:

foreach x in {a: 1, b: 2, c: 3, d: 4, e: 5} {
    print(key: x.key, value: x.value);
}
// prints:
/*
-- key: 'a', value: 1
-- key: 'b', value: 2
-- key: 'c', value: 3
-- key: 'd', value: 4
-- key: 'e', value: 5
*/
// not necessarely in this order.

All statements end with ;

Please note that all statements(assignments, function calls, method calls) SHOULD end with a ;, like they do in C/C++.

Project Model

Let’s start with the simplest part of a leaf build system: a module.

Module

In a nutshell, a module is any folder that has a build.leaf file directly below it.

Project

A project is a module that contains some extra metadata. See the kwargs of the project() function to find out more.

The metadata present in the project should apply to all of its submodules.

The build.leaf file

Setup

Most developer actions are managed as make recipes by cargo-make; to install it, follow the guide in the readme.

They are described in the Makefile.toml file at the root of the repository. For a list of them all and what each does, see root makefile recipes.

Terminology

Some of the terminology used.

Build system boundary

Refers to when a child directory of a module / project is managed by a different build system.

Example:

.
└── outer
    ├── build.leaf
    └── inner
        └── CMakeLists.txt

And with inner subdir-ed from outer

// outer/build.leaf
project('outer')

subdir('inner')

Here outer is managed by leafbuild while inner is managed by cmake. We call outer the outer directory and inner the inner directory across the ("outer", "outer/inner") build system boundary.

Architecture overview

There are two main types of components in leafbuild:

Producers

The components that help produce the LfBuildsys are called producers. They all work together, and they are:

leafbuild-ast

Holds the ast structures produced by the leafbuild-parser. Further described in leafbuild-ast.

leafbuild-parser

Holds the logic to parse input build.leaf files and produces leafbuild-ast structures. Further described in leafbuild-parser

leafbuild-interpreter

Interprets the leafbuild-ast structures produced by leafbuild-parser and outputs the LfBuildsys. This is where most of the magic happens. Further described in leafbuild-interpreter

All the middle layers

Middle layers are quite a complicated thing to explain, so you can find more about them here

Consumers

The components that consume the LfBuildsys are called consumers.

leafbuild-ast

Holds ast structures of build.leaf files.

TBD

leafbuild-parser

Uses lalrpop(the grammar is available here). TBD

leafbuild-interpreter

Turns an ast defined in leafbuild-ast produced by leafbuild-parser into a LfBuildsys, by running the instructions in the AST to configure the buildsystem.

TBD

leafbuild-ninja-be

The ninja generator-backend. TBD

leafbuild-make-be

The make generator-backend. TBD

leafbuild-ml

What is a middle layer in leafbuild?

Most C/C++ build systems are isolated, in the sense that you cannot use multiple build systems in the same codebase, and when you have a dependency that you need to build which uses another build system than the one you use, it gets a lot harder to get them to talk to each other.

So a middle layer is a layer that goes between leafbuild and some other build systems. Read this document carefully if you want to write your own.

Conventions

  1. Across build system boundaries, the build directory at least somewhat resembles the directory structure of the source directory.Across build system boundaries, output directories of sibling projects & modules are always siblings, output directories of parent / child projects and modules have to at least somewhat resemble that parent / child relationship. More about this here.
  2. Across build system boundaries, the build system that manages the outer directory is responsible for creating a directory for the build system that manages the inner directory in it’s own designated space.
  3. If you recognize an added subdirectory as your own, you should handle it. Only and only otherwise should you invoke the middle layers to handle it.

Output directory resemblance

TBD

How leafbuild-ml works

It simply exposes a trait: MiddleLayer that other crates implement, and perform a little linker magic to make leafbuild-ml aware of those implementations.

The MiddleLayer trait is pretty simple:

use leafbuild_core::lf_buildsys::LfBuildsys;
use std::collections::HashMap;
use std::path::{Path, PathBuf};

/// A simple result type with all the possible errors a middle layer might throw
pub type Result<T> = std::result::Result<T, MiddleLayerError>;

/// A middle layer trait
pub trait MiddleLayer {
    /// Try to recognize a given `add_subdir()`-ed directory.
    /// This usually should just check that `path` contains a
    /// given file meaningful to the build system of this middle
    /// layer.
    ///
    /// For example, for cmake: check that `path/CMakeLists.txt` exists.
    /// For meson, check that `path/meson.build` exists, etc.
    fn recognize(&self, path: &Path) -> RecognizeResult;

    /// Invokes the inner build system and returns the changes that should be made
    /// to the [`LfBuildsys`]
    /// # Errors
    /// Anything that can go wrong. Though the build will fail if this returns `Err`.
    fn handle<'buildsys>(
        &self,
        buildsys: &'buildsys LfBuildsys<'buildsys>,
        boundary_details: BuildsysBoundaryDetails,
    ) -> Result<BuildsysChanges>;
}

/// Whether the middle layer recognizes the directory or not
#[derive(Debug, Copy, Clone)]
pub enum RecognizeResult {
    /// When it was recognized
    Recognized,
    /// When it was not recognized
    NotRecognized,
}

/// The data passed across a
/// [build system boundary](https://leafbuild.github.com/dev/terminology.html#build-system-boundary)
#[derive(Debug)]
pub struct BuildsysBoundaryDetails<'boundary> {
    /// The source root
    pub source_root: &'boundary Path,
    /// The output root
    pub output_root: &'boundary Path,

    /// The source folder that was `add_subdir()`-ed, and checked
    /// previously with [`MiddleLayer::recognize`]
    pub source_folder: PathBuf,
    /// The output folder assigned to this subdirectory for the
    /// inner build system to write files to.
    pub output_folder: PathBuf,

    /// The "arguments" passed to the inner build system.
    /// In cmake they are variables that should
    /// be `set()` before invoking the `CMakeLists.txt`,
    /// while in meson they are global variables.
    ///
    /// Since meson takes types *somewhat* seriously,
    /// they should be converted.
    pub arguments: HashMap<String, String>,
    // will maybe add more
}

/// The build system changes that are to be applied to
/// a [`LfBuildsys`] after [`MiddleLayer::handle`]
/// executed.
#[derive(Copy, Clone, Debug, Default)]
pub struct BuildsysChanges {}

/// Errors that can occur during a middle layer's execution.
#[derive(Error, Debug)]
pub enum MiddleLayerError {
    /// Any other error
    #[error("Other error: {0}")]
    Other(#[from] Box<dyn std::error::Error>),
}

leafbuild-cmakeml

CMake middle layer. TBD

leafbuild-mesonml

Meson middle layer.

The documentation repo

The leafbuild.github.io repo was created from the doc/book directory. The doc/book directory is generated from doc/src and doc/book.toml by mdbook.

If you want to see the changes after you made them, make sure you have mdbook installed.

Running makers doc-build from the root of the repo will automatically generate doc/book with all of its contents.

If you run makers doc-serve from the root of the repo, you will have a local instance of the doc site at http://localhost:3000.

After you are happy with the changes, submit a PR on the master branch, and mention you changed the documentation, so the site can be rebuilt.

Please use reference-style links for all links to the main repo.

You can use rust and leafbuild for syntax highlighting. Also the leafbuild language declaration for highlight.js can be found here.

The syntax highlighter

Syntax highlighting is available in the docs with:

```leafbuild
// ....
```

The highlighter also includes the rust and bash languages.

You can find the highlight.js definition for leafbuild in doc/leafbuild_highlight.js.

Root makefile recipes

They can be invoked via

cargo make <recipe_name>
# or
makers <recipe_name> 

format

Formats all the source files.

fmtcheck

Checks formatting of all the source files and exits with non-zero if they’re not formatted properly.

clean

Calls cargo clean, removing all built artifacts.

build

Builds the leafbuild executable.

lint

Calls our good friend clippy to help improve code.

check

Checks the database, by first formatting, then invoking clippy and testing. This should usually be invoked before a commit.

doc-build

Invokes mdbook to build doc/book from doc/src.

doc-serve

Invokes mdbook to serve the built files on localhost:3000.

doc-nuke

Cleans up the currently-built book in doc/book.

doc-push

Pushes the built doc to the docs repo.

doc-build-highlighter

Builds the syntax highlighter from the highlight.js repo with doc/leafbuild_highlight.js. More about the highlighter here.

Validation checks performed by leafbuild

All the referenced source files exist

Motivation

I decided to create leafbuild because I am not happy with what is already there:

  • cmake(I don’t like the docs at all)
  • meson:

Meson is an open source build system meant to be both extremely fast, and, even more importantly, as user friendly as possible.

Meson overview

Well it’s not really that fast. In fact it takes about 1.5 - 2 seconds to only print the version in a new session, which I find outrageous.

So in the end I ended up creating my own build system.

Roadmap

The current state of the implementation, along with future plans.

Syntax

  • Comments
  • Variable declaration
  • Variable assignment
  • Function calls
  • Method calls
  • If expressions
  • Ternary expressions
  • Loops