# 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](https://blog.balthazar-rouberol.com/just-enough-makefile-to-be-dangerous#makefile-auto-documentation-as-the-default-step) 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](https://git.balthazar-rouberol.com/brouberol/5esheets/src/branch/main/Makefile#L189) of [hacks](https://gitlab.wikimedia.org/repos/data-engineering/airflow-dags/-/merge_requests/2084) 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

```bash
$ 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](https://devhints.io/makefile) 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

```python
@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:

```python
@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.

```python
@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 `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](https://en.wikipedia.org/wiki/Glob_(programming)) pattern

Take a look at [`example_c/Brakefile`](./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 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`.

```python
@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](https://graphviz.org/doc/info/lang.html), but [mermaid](https://mermaid.js.org/) is also supported by passing `--syntax=mermaid`.

```bash
$ brake graph  > brake.dot
$ dot -Tsvg brake.dot -o brake.svg
```
![brake tasks](https://f003.backblazeb2.com/file/brouberol-blog/public/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).

```bash
$ 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.

```bash
$ 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

```bash
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)
  ```

## Roadmap

- [ ] Defining variables
- [ ] Adding a `--explain` mode that wouldn't build the targets, but only explain what would get built and what wouldn't
- [ ] Release publicly
- [ ] 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
1. It sounds like `break`, which is the semantic opposite to `make`
1. Balthazar Rouberol's `make`.
