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
andvalue
. Thekey
property is always a string. Thevalue
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
- 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.
- 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.
- 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.
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
|
|