After using GNU Make for automating the build step in my projects, I had this idea of making my own build automation tool like Make.
I originally wanted to use Makefile-like syntax for the config file but instead settled with TOML after thinking about it for a while. I also used Rust to write this since I am kinda familiar with the language.
Rust has a TOML crate for parsing TOML files. It also provides support for deserialization and serialization using serde.
It’s also extremely easy to use:
// we store custom and pre as HashMap<String, Struct> so that in the TOML file we can use dynamic table names.
// for environment variables, it's stored as HashMap<String, String> so we can specify as many env vars as we like.
#[derive(Debug, Deserialize)]
struct Recipe {
build: Build,
custom: Option<HashMap<String, Custom>>, // custom commands
pre: Option<HashMap<String, Pre>>, // pre-build commands
env: Option<HashMap<String, String>>, // for environment vars
}
/* <the build, custom and pre structs> */
let recipe: Recipe;
match toml::from_str(&recipe_str) {
Ok(r) => recipe = r,
Err(e) => {
printb!("Error: {}", e);
exit(1);
}
}
I didn’t include the other structs to make the snippet smaller. This was also the first time I used rust macros in a project, as you can see with the printb!
macro. All it does is print
Baker: <message>
as coloured output.
#[macro_export]
macro_rules! printb {
($($arg:tt)*) => {
println!("\x1b[32mBaker:\x1b[0m {}", format!($($arg)*));
};
}
Configuring baker
The configuration file is put in the root directory of the project, just like the Makefile.
If Baker is unable to find a config file (called recipe.toml
), it auto generates one.
Here’s an example config:
[env]
BIN_NAME="bake"
COMPILER_FLAGS="--release"
INSTALL_PREFIX="/usr/bin"
[build]
cmd = """
cargo build $COMPILER_FLAGS &&
cp -r ./target/release/$BIN_NAME ./bin/$BIN_NAME
"""
[custom.clean]
cmd = "cargo clean"
run = false
[pre.fmt]
cmd = "cargo fmt"
Baker is invoked using bake
and by default checks for the build
field in the recipe.toml
.
The cmd
value is then executed during build.
You can set environment variables using the env
field.
You can also add custom fields using custom
. It also checks for a run
value that is used to check whether the command should be run
after build or not. So if run = true
, then the command is run after build.
Baker also checks for an optional pre
field that contains commands which should be run before build. Like for example, if you wish to
format your code or something before compiling.
You may have noticed a name
value in the custom
and pre
fields ([custom.name]
). This is used to identify each field, and for the custom
field,
the name
is set as an argument when Baker is invoked.
For example, if I have a custom
field called setup
:
[custom.setup]
cmd = "mkdir -p bin && rustup install stable && rustup default stable"
run = false
You can run bake setup
to directly execute the command inside the field.
Baker is open source and you can find the repo here. I wrote it in like 2 days, so it may have bugs which I was not able to find, so all suggestions are welcome!
That’s it for this blog, see you soon!