BuildBro is a simple build tool, much like Make, that manages the dependencies between sets of executable commands called targets. Targets are specified using a symbolic expression-based language in a file called build.bro
. To illustrate, here is a simple brofile:
{project hello-world "Description goes here"
[target compile (depends "main.c") (creates "foo")
(! "gcc -o foo main.c")
]
[target run (depends compile) (creates)
(! "./foo")
]
}
Here within each target the !
command is used to execute a shell command. Note that [] and {} may also be used to delimit lists for improved readability.
Additionally, BuildBro provides basic support for Lisp-style macros.
The syntax for brofiles (modulo macros) is as follows:
<project> ::= (project <project-name> <project-description> <targets>)
<name> ::= <ATOM>
<project-description> ::= <STRING>
<targets> ::= <target> <targets> | ε
<target> ::= (target <target-name> <depends-clause> <creates-clause> <commands>)
<target-name> ::= <ATOM>
<depends-clause> ::= (depends <depends-list>)
<depends-list> ::= <depends-target> <depends-list> | <depends-file> <depends-list> | ε
<depends-target> ::= <ATOM>
<depends-file> ::= <STRING>
<creates-clause> ::= (creates <creates-list>)
<creates-list> ::= <creates-file> <creates-list> | ε
<creates-file> ::= <STRING>
<commands> ::= <command> <commands> | ε
<command> ::= (<command-name> <command-args>)
<command-name> ::= ! | mv
<command-args> ::= <command-arg> <command-args> | ε
<command-arg> ::= <STRING> | <ATOM>
Macros must be defined at the beginning of the brofile, immediately before any targets. The syntax for a macro directive is as follows:
<macro> ::= (macro (<macro-name> <macro-formals>) <macro-body>)
<macro-name> ::= <ATOM>
<macro-formals> ::= <macro-formal> <macro-formals> | ε
<macro-formal> ::= <ATOM>
<macro-body> ::= <EXPR>
Here is an example of a simple macro:
{project macro-sample "Example usage of a simple macro"
[macro (object-file name)
`(target ,(string->symbol (string-append "compile-" name))
(depends ,(string-append name ".c")) (creates ,(string-append name ".o"))
(! ,(string-append "gcc -c " name ".c")))
]
(object-file "main")
}
After macro expansion, the brofile looks like this:
{project macro-sample "Example usage of a simple macro"
(target compile-main
(depends "main.c") (creates "main.o")
(! "gcc -c main.c"))
}
A macro represents a set of rules for transforming some arguments into a symbolic expression that will be substituted for each instantiation of the macro within the brofile. In the grammar above, <EXPR>
stands for some expression that must evaluate to an atom, string, number, or list. Before BuildBro attempts to parse the project file, the macros are extracted and a process known as macro expansion occurs. Each instance of (<macro> <args>)
will be replaced by the result of evaluating that macro with those arguments. Note that arguments are not evaluated in BuildBro. That is, if you invoke (macro (string->append "hi"))
that macro will be passed the list (string->append "hi")
, not the result of applying string->append
to "hi"
.
Macro expressions in BuildBro consist of either self-evaluating expressions, standard forms, and function applications.
-
Self-evaluating expressions, such as strings and numbers, will always evaluate to themselves.
"A string" => "A string" 22.0 => 22.0
-
Standard forms, such as
let
andquasiquote
, allow for control flow, variable binding, and quotation (which prevents the evaluation of a list or other expression).(let ((x "Hi") (y "World")) (string-append x y)) => "HiWorld" (quasiquote (this is a list)) => (this is a list) (let ((the-answer 42)) (quasiquote (the answer is (unquote the-answer)) => (the answer is 42)
-
Function applications allow builtin functions to be invoked with some (evaluated) arguments.
(string-append "Hi" "World") => "HiWorld" (symbol->string "hi-world") => hi-world
-
(! cmd)
executes a shell command. -
(mv src dest)
moves a file fromsrc
todest
.
-
(quasiquote expr)
will prevent the evaluation of a list or other expression. Additionally, each instance of(unquote expr)
within a quasiquotation will be replaced with the result of evaluating the expression in the current environment. -
(let ((name1 value1) (name2 value2) ...) expr)
will bindname1
tovalue1
andname2
tovalue2
(et cetera) while evaluating the specified expression.
-
(string-append . args)
will append all of its string arguments together. -
(string->symbol str)
will convert a string into an atom.