Skip to main content

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 -p and 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 task target can depend on both file or task targets
  • A file target can only depend on other file target(s)
  • A task target will always be rebuilt
  • A file target will be rebuilt if it does not exist on disk, or if any of its file dependencis was modified after the file itself.
  • A file target 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

brake tasks

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-mode mode 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.

  1. So I can be able to say "this is a make-or-break" tool and sound smart
  2. It sounds like break, which is the semantic opposite to make
  3. 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

brake-0.5.0.tar.gz (13.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

brake-0.5.0-py3-none-any.whl (13.0 kB view details)

Uploaded Python 3

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

Hashes for brake-0.5.0.tar.gz
Algorithm Hash digest
SHA256 0274b34a86b66729e325dfc3d1f6b2bbb89ab405ae7e7c9406e80db8c774088c
MD5 99b0ce341652a137cc8c3e76b79fae2e
BLAKE2b-256 1cf9f314d658094c3e49e11f768fccf0baf5ea23aa1c730289c9954a381ddda9

See more details on using hashes here.

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

Hashes for brake-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ee8515a264a8e9066bc8f6e48c9b77abedc253e54432ddc3da2feb3b58f85970
MD5 24c378618c23bc66347782b99e1ddb40
BLAKE2b-256 5130de8430538d3b3646e0787cf3cf7355b91269ccb45f20b1cebd16251921f7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page