Remake has mostly declarative language. You describe what depends on what, what do you want to receive and it decides how to get there. The source file is composed of variable assignments, function declarations and class declarations.

The language is case-sensitive (life, after all, too ;-).

Basic principles

First thing to note is that order of definitions has no semantic meaning — it makes no difference, if you set variable value below or above its use. When a variable has some value, it has the same value at other places as well. Order of evaluation (or if something will be evaluated or not) is not defined. The only exception to this are functions.

The second important thing is, everything in remake is an array (list, or however you call data structure where more than one value live, in given order). It means, when you have a variable, result of an expression, etc., can have one value, no values or multiple values.

This allows handling multiple similar things at once, without writing the same code many times. However, there are places, where it makes no sense to work with multiple items at once (if you concatenate string A and B, it does not much help, if A is 3 strings instead). In such cases, remake makes Cartesian product of all the parameters, pass them one each time to the function (parameter, or whatever it is) and puts all the results together. (So the result would be A1B A2B A3B. If B had 2 values and not one, it would be A1B1 A1B2 A2B1…)

Another principle is, rules how to create something is divided from the thinks it creates or is created from. It means, how to compile a .c file into .o file is described at one place, and at another is only said that such rule should be used to create this list of files.

Low-level markup

There are two things which work even below syntax. The first one is line breaks. Usually, end of line means end of expression. However, this can be changed both ways. You can use semicolon (;) to end current expression and start new one without going to the next line. On the other hand, when your expression gets too long, if you put backslash (\) before the end of line, the expression continues on the next line.

Next one are comments. Everything from a hash character (#) to the nearest end of line is ignored. However, hashes inside strings are not considered to be start of comment. The end of line terminates both the comment and the expression, if there is any before the comment.

Values

Each value has its type. It is not strictly required for all values of single variable to be of the same type, however, mixing them does not much help readability or correctness of the description.

When you list more items, just separate them with spaces. You can specify empty lists (just write no items). If it makes problems because of operator priorities, use parenthesis.

variable := 'value' 'another value' 'third value'
empty_variable :=

Each value can have one of the five types.

String

Is just sequence of characters, with no special meaning to remake. Some functions return strings, take strings and you can write them in two ways.

First is literal string. It is enclosed between single quotes ('). What you write inside is exactly what it will contain. You can not write single quote character in the string this way. The string can spawn across multiple lines.

'String with lots of very odd symbols: "#$ @@\ ☠

Next line'

The second way is using double quotes ("). This way you can use backslash sequences to write all sorts of characters (including double quotes, newlines). Furthermore, it allows variable substitution. You can substitute in simple concatenation form, using a dollar sign. It replaces the occurrence with the variable content, and it produces as many strings, as there are values in the variable. These two expressions have the same meaning.

"String start $variable string end"
'String start '.variable.' string end'

If you use the at-sign form, it formats the values for shell. It puts them into single quotes, and, if a variable has more values, puts them one after another, separated by spaces.

variable := 'value1' 'value2' 'value with spaces' "It's my apostrophe"
empty_variable
"String @variable, @empty_variable"
# It will produce:
"String 'value1' 'value2' 'value with spaces' 'It'\''s my apostrophe"

In both cases, if the variable contains something that is not string or object, it fails.

Here is the list of the backlash sequences:

\n

newline

\r

carriage return

\t

tab character

\e

escape

\\

\

\'

' (even when you can write it literally)

\"

"

\$

$ (not taken as variable substitution)

\@

@ (not taken as variable substitution)

Function

Function definition creates a variable with single function value. Then it can be called, assigned to other variable, passed as an parameter to some other function, etc.

A function definition looks like this:

fun hello($parameter, @multiparameter) {
  var := 'hello'
  date := !'date'
  RESULT := "$var $parameter, it's $date now"
}

The fun keyword says a function definition starts. Then there is name of the function. Then there is list of parameters in parenthesis. The dollar sign means the function takes exactly one value in this parameter (if more values are passed, it is run multiple times), the at sign means any number of values can be passed trough this parameter.

Then there is body of the function, ended by }. Unlike the rest of the script, this part is procedural — it takes lines, one by one, from top to bottom, and evaluates them. Value of a variable can change during the execution, order of evaluation is defined.

Unfortunately, lambda functions (function defined inside function, remembering state of variables in the time of its creation in the parent function) is not yet implemented. However, they seem to fit nicely into the syntax, so they may appear some day.

Class

Class represents the rule how to create something from something else. It looks like this:

class rule(@inputs, $output) :~ parent {
  variable := 'value'
  fun method() {
    var := variable
  }
}

The start is similar to the one of function. The :~ is optional and specifies inheritance. The part behind it must be expression evaluated to a list of objects. They will be parents to the object created from this class. Look at the [blocks] to know what it means.

The body of the class has the same syntax, as the outside of the class — declarative, with no semantic meaning of order. It can contain functions local to the class (methods).

Class can be called with parameters the same way as functions are.

Object

When you call a class with parameters, the result is object (or multiple objects). It represents one real result (file) to be created by remake.

Object is somehow class that goes alive — class is the way to compile .c to .o, object is the .o file that will get created.

Method

Method is a function inside an object — it carries the object with itself, to know where it belongs.

Expressions and operators

Expressions are built in a similar way as mathematical expressions, or expressions in the C language — from operators and functions. The operators are either unary or binary and have their priorities. You can use parenthesis, if the priorities do not suit your needs.

Some expressions work on all values at once, some on only one and use the Cartesian product trick.

The following table contains the operators. Signature specifies, what parameters and where it takes. Dollar means it is run multiple times, each time with single value, at means it is run with all values at once. L means it must be L-value (expression into which one can assign). C means it is allowed only in class definition. -> L at the end means it return L-value.

Operator

Function

Priority

Arity

Parameters

. (dot)

Concatenation

3

2

$ . $

! (bang)

Shell execution

1

1

! @

:=, +=

Assignment

1

2

L := @, L += @

:~, :~

Inheritance

1

2

C :~ @, C +~ @

inherited

Parent context

4

1

inherited L

export

Scope change

4

1

export L -> L

global

Scope change

4

1

global L -> L

(space)

Multiple values

0

2

@ @

Concatenation

It takes two strings or objects and concatenates them together. It takes filename of the object, if the parameter is object. Returns string.

Shell execution

Takes the parameter and runs it in shell (/bin/sh is used). It returns the standard output of it. If the result is unused, the standard output is not captured and appears on standard output of remake. Must terminate with zero exit status, or remake fails. If you want to detect something by the script, let it output it to standard output (like do_test || echo "failed").

Assignment

Assigns to the variable on the left side (list of variables can be provided, variables with modified scope, etc.) the right side. The := does a direct assignment. There can be only one into a given variable, or remake fails. There can be multiple +=, they add values to the variable. You can have one := and some += ones. The order of evaluation of the assignments is not defined.

However, in function, the assignments acts a little different. As the expressions in functions are evaluated one by one, multiple assignments of any type can be present trough the lifetime of variable. Each one modifies the current content of it — := replaces it, while += adds to it.

Inheritance

It is allowed only in class definition. Takes the objects on right side of it and makes them parents of the object created from the current class. The expression is evaluated in the context of the newly created object, however, not all other parents need to be available in the time. It means, you can use variable defined in the body of the class, but you should not use variables set in some of the parents to compute the other parents.

The +~ version may be removed in future, since it has no practical use.

Parent context

If you prefix a variable name with inherited, the variable will be searched in the first parent context of this context. For details about inheritance, see [blocks]

Scope change

These modify, where the variable lives. The export says this variable is the same one as variable of the same name in parent contexts. Its common use is to export functions from modules or sub-REM-files. When used inside a method, the current value the variable will have at the end of the function is copied to the context of the object and remembered (even across runs of remake).

The global operator specifies the variable is the same as variable of the same name in any of the context’s child context.

They both work for lists of variables too, and can be directly assigned to.

Multiple values

Just takes values and groups them together. This operator has an exception — it is evaluated before assignments and inheritance, even when they have higher priority. So this will assign 2 values into the variable:

variable := 'val' . 'ue' 'another value'

Space is taken as an operator only if there is no other operator. It means, the spaces around := and . are not operators, but the one before 'another value' is.

Functions

Anything that is a function value list (with any number of the values) can be called using parenthesis. The thing before opening parenthesis is the list, inside are parameters. If more than one function value is provided, each function is called with the given parameters and the results are put together into a list.

Each parameter can be an empty list. So you can write this:

function('first nonempty parameter', )

If you have function with single parameter and you want to pass an empty list to it, you need to put the empty list into parenthesis, so it can be recognized from call without parameters:

function(())

Blocks and contexts

You noticed that each class or function has its body, block of code enclosed between { and }. You can split code into blocks too, just like this:

variable := 'a'

{
  variable2 := 'b'
}

The blocks modify scope, or context, where variables live. When a variable is read, remake tries to read it in current context. If it is not there, it tries parent context. If it is not there either, it continues up.

If a variable is assigned, one is created in current context.

This has few consequences. While variable in the example can be read outside the block as well as inside it, but variable2 only inside. Further, any assignment (even the one with +=) creates a new variable, independent of the one outside. So if we assigned to variable inside the block, it would be a different one than the outside.

However, you can search in the parent context if you use the inherited operator. In this example, variable inside the block will contain two values:

variable := 'a'

{
  variable2 := 'b'
  variable := inherited variable variable2
}

The global and export merge variables in multiple contexts together. The export one merges the one in current context with the one in parent context. The global operator merges it in all child contexts (even transitively — children of children, etc.).

It is more complicated with object contexts. An object context has multiple parents. There are the contexts of parent objects (inherited ones), then there is the context where the object was created (the class was called) and the last one is the parent context of the class.

Limitations

Hierarchy

Usually, you want to split the definition into multiple files or use some provided modules (libraries) for common tasks. There are statements to do this.

import

Searches for a global module and loads it. The module has it’s own context, which is plugged into current one as child. The name is specified without any quotes, it can not be computed.

import core
descend

Takes list of strings. For each string, if it ends with a slash, it is considered to be a directory. A REM file in the directory (unless configured otherwise) is loaded. If it does not end with a slash, it is considered to be full filename and given file is loaded. The loaded file has its own context and is plugged into the current one as child.

The list of strings must be computable without any subfiles (loaded by other descend). But you can use information (functions, etc.) from modules loaded by import and from parent files (but without their `descend`s too).

descend "subdir/" "another_subdir/file"
parent

This one is optional, inside files loaded from others by descend. If it is present, it must lead to the file which loads it (or to the directory, where it resides and the parent one must be REM).

It is used when remake is run from a subdirectory, so it can find the top-level script.

The value of the string must be computable even without the modules loaded by import, or any other script files.

parent "../"

Besides these contexts, there is one builtin context that is parent to the one of top-level script. It contains variables defined on command line.

All relative paths are relative to the directory, where the top-level script lives (script that has no parent).

Special variables

Some of the behaviour is defined trough special variables. Syntactically, they are common variables, but they have special meaning.

RESULT

Value of this variable inside a function context at the end of its execution is considered to be result of the function. It means, if the function returns something, assign it to this variable.

WORKDIR

If it is set and you call some shell (by the ! operator), it is run with this working directory.

ECHO_MODE

If this variable is set to SILENT (single string value) and you call some shell, the command is not printed. Otherwise, it is.

DEPS

Inside an object, this variable holds list of all dependencies. If a dependency is a string, it is considered to be an input file and it only checks, if it changed. If it is other object, it is checked (and possibly compiled) before this one. If it is recompiled (or changes itself), this one is recompiled too.

TARGET

Inside an object, it specifies the list of files this object creates. If you compiled a .c file into an .o file, you would assign the .o filename here. This variable must be defined, but it may be empty — then the object does not produce anything on disk, but represents some kind of action.

DEFAULT

Inside an object, it holds the method used to perform compilation. This must be defined inside each object.

TARGETS

The list of objects you want to get by default compilation (if you call remake without parameters). You can ask it to use other variable instead. This variable is global trough all the script files.

ACTIONS

List of method names (strings) considered actions. You can ask remake to perform a different method instead of the DEFAULT one. It will be run on all objects. But if the method is an action, DEFAULT is run before it (if some dependencies changed).

Example of action would be install — you need to have the thing compiled before you can install it. Example of method that is not an action is clean — you do not need to perform compilation before you delete the results.

This variable is global trough all the script files.

Tips

Since one very needed feature is still missing, setting variables in specified objects from outside (like, CFLAGS are set to some value for all objects, but there is one object that needs to be compiled differently from the others), you can use the block syntax to solve it.

import C

TARGETS := clink(objects, 'hello')
CFLAGS := '-ggdb -O0'
objects := ccompile(sources) world_object
sources := 'hello'

{
  # No-one should debug the world and it needs to run real-time
  CFLAGS := '-O3'
  export world_object := ccompile('world')
}