A minimalistic yet powerful build tool
Project description
Brake
A minimalistic build system.
Why another build system?
I've been using make for years, and probably use 10% of what it can really do. Over time, I have established patterns that I'm reusing in all (or most) projects:
- autodocumenting the public facing targets
- providing a target in charge of visually rendering the make graph, to help debug dependencies
These patterns rely on a number of hacks that I've been cargo culting in different projetcs, because make does not provide me with the level of annotation and introspection capabilities required to implement these features simply.
I've also grown tired about some make behaviors over the years:
- the implicitness of whether a target runs a task or builds a file
$ cat Makefile
test:
echo "testing"
$ make test
echo "testing"
testing
$ touch test
$ make test
make: `test' is up to date.
- more generally, the sheer amount of implict behavior (run
make -pand stare into the horizon) - the lack of builtin way to publicy document targets
- the crazy syntax that looks like bash but really isn't
I set out to write my own built system that would be based on the following principles:
- no implicit behavior
- builtin target introspection and documentation
- automatic parallel builds
- heavily tested
How does it work
All targets are defined in a file, called Brakefile by default.
TLDR: brake itself is built with itself, so have a look at the Brakefile in this project to see what features it has (or not).
Defining a target
The simplest break task you can define is
@task
test:
pytest .
This defines a target of type task, that runs pytest . when executed.
Commands are assumed to be bash, and are executed line by line, instead of in a single go. Although this may change in the future, the design goal is to only have the simplest commands be part of the Brakefile. Anything more than a oneliner should go into a script (whether python, bash or anything else) and be called from the Brakefile.
Target inter-dependencies
You can define interdependant targets using the deps target argument:
@task
test:
pytest .
@task
check:
ruff check .
@task(deps=[test, check])
ci:
This way, when running break run ci, both test and check tasks will be executed. As the ci target has no associated command, it only acts as a dependency placeholder.
Documenting targets
You can document each target by annotating them with a description argument.
@task(description="Run unit tests")
test:
pytest .
@task(description="Run linter checks")
check:
ruff check .
@task(deps=[test, check], description="Run all tests and linters")
ci:
You can then get the help for your targets by running brake help
check Run linter checks
ci Run all tests and linters
test Run unit tests
@task vs @file
brake can deal with 2 types of targets:
task: defines what a task does (ex: running tests, formatting the codebase, applying database migrations, etc)file: defines how a file gets built (ex: compiling source code, running some codegen script, etc)
The following rules apply:
- A
tasktarget can depend on bothfileortasktargets - A
filetarget can only depend on otherfiletarget(s) - A
tasktarget will always be rebuilt - A
filetarget will be rebuilt if it does not exist on disk, or if any of itsfiledependencis was modified after the file itself. - A
filetarget name can be composed of*, which will be expanded as a simple glob pattern
Take a look at example_c/Brakefile to see an example of a Brakefile mixing both task and file targets, aiming at building a very simple C program.
Defining variables
You can define variables in your Brakefile that can be reused in your target commands.
ruff = poetry run ruff
@task(description="Run linters")
lint:
{ruff} check .
@task(description="Format the codebase")
check:
{ruff} format .
Variables can be interdependant and are recursively resolved.
Example:
run = poetry run
ruff = {run} ruff
Defining a default target
In the same way that make lets you define a default targt with .DEFAULT_GOAL, you can define which target will be built by default if no argument is provided to brake run.
@task(description="Run unit tests")
test:
pytest .
@task(description="Run linter checks")
check:
ruff check .
@task(deps=[test, check], description="Run all tests and linters", default=true)
ci:
Visualizing the target graph
You can use the brake graph command to export the target graph into a format that can itself be exported to an image. The default format is dot, but mermaid is also supported by passing --syntax=mermaid.
$ brake graph > brake.dot
$ dot -Tsvg brake.dot -o brake.svg
Parallel target builds
Looking at the target graph form the previous section, we can see that running the lint task would run both the lint.check and lint.format dependency tasks. As each of these tasks are independant, they are run in parallel, through a process pool of available number of CPUs by default (configurable via the -j argument).
$ brake run lint
[task:lint.check] poetry run ruff check .
[task:lint.format] poetry run ruff format --check .
11 files already formatted
All checks passed!
By setting -j1, you can ensure that each task gets executed serially instead.
$ brake -j1 run lint
[task:lint.check] poetry run ruff check .
All checks passed!
[task:lint.format] poetry run ruff format --check .
11 files already formatted
Usage
brake --help
usage: brake [-h] [-j MAX_JOBS] [-f FILE] {run,help,graph} ...
A minimalistic yet powerful build tool
positional arguments:
{run,help,graph}
run Run a task
help Display the targets help
graph Display the targets as a graph
options:
-h, --help show this help message and exit
-j, --max-jobs MAX_JOBS
The maximum number of jobs to run in parallel (default: 10)
-f, --file FILE Path to the file containing the brake targets (default: Brakefile)
-n, --dry-run Print what targets would have been built, as well as the commands, without running anything (default: False)
-p, --plain Don't colorize output (default: False)
Installation
$ pip install brake
Roadmap
- Release publicly
- Defining variables
- Adding a
--dry-modemode that wouldn't build the targets, but only explain what would get built and what wouldn't - Colorize outputs when building several targets in parallel
- Write some syntax highlighters for the Brake grammar
Why the name brake?
There are at least 3 reasons. Use the one you prefer.
- So I can be able to say "this is a make-or-break" tool and sound smart
- It sounds like
break, which is the semantic opposite tomake - Balthazar Rouberol's
make.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file brake-0.5.0.tar.gz.
File metadata
- Download URL: brake-0.5.0.tar.gz
- Upload date:
- Size: 13.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.2.1 CPython/3.14.3 Darwin/25.3.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0274b34a86b66729e325dfc3d1f6b2bbb89ab405ae7e7c9406e80db8c774088c
|
|
| MD5 |
99b0ce341652a137cc8c3e76b79fae2e
|
|
| BLAKE2b-256 |
1cf9f314d658094c3e49e11f768fccf0baf5ea23aa1c730289c9954a381ddda9
|
File details
Details for the file brake-0.5.0-py3-none-any.whl.
File metadata
- Download URL: brake-0.5.0-py3-none-any.whl
- Upload date:
- Size: 13.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.2.1 CPython/3.14.3 Darwin/25.3.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee8515a264a8e9066bc8f6e48c9b77abedc253e54432ddc3da2feb3b58f85970
|
|
| MD5 |
24c378618c23bc66347782b99e1ddb40
|
|
| BLAKE2b-256 |
5130de8430538d3b3646e0787cf3cf7355b91269ccb45f20b1cebd16251921f7
|