Crank is a GNU Make source file that implements an object-oriented build system. It provides a number of commonly-used classes and allows makefiles to create their own subclasses.
A makefile using Crank is typically structured like this:
include path_to_crank/crank.min ...declarations... $(build)
The first line includes crank.min, which defines a number of functions and variables that the rest of the makefile can use. The last line triggers processing of all of the declarations in the makefile.
A class is a collection of items (zero or more). The items that belong to a class are stored in a Make variable of the same name as the class.
Compile = a.c b.c
Each class may have associated property definitions. Properties are represented by Make variables using a “<class>.<property>” naming convention, as in:
Compile.message = Compiling...
The combination of a class name and item name identifies an instance. Properties may be associated with instances using the following variable naming convention:
Compile[a.c].message = Compiling A!
In this document, we write simply “.prop” as shorthand for “the prop property”.
A class may inherit properties from a parent class. Inheritance is specified by .parent, as in:
Copy.parent = Gen
Instance properties override class properties. That is, if an instance property is defined, its value is used and class properties are ignored. Likewise, a class property will override any properties in its parent class.
When a property is evaluated, the current instance is available to it in the form of $C (the class name) and $I (the item name). Property definitions can refer to other properties, using the . function. Its general form is $(call .,<prop>). This triggers another property lookup on the instance (class name plus item name) that is currently being evaluated.
Copy.command = cp $I $(call .,out)
A generator is a class that generates rules. By rule, we mean a simple Make rule. Rules specify:
an output file
some number of prerequisites
the commands to be executed to generate the output file
Generators inherit (directly or indirectly) from the base class Gen.
These naming conventions introduced by Crank could be characterized as a language. This language has much in common with object-oriented languages: classes, instances, properties, and inheritance. At the same time, Crank is decidedly unlike object-oriented languages in two ways:
There is no mutable state associated with an object. The set of objects and properties is static.
There is no “object” data type — everything is a string.
Small makefiles will add items to pre-existing classes and optionally declare or override some properties. For example:
Exe += program Exe.in = program.c util.c Compile[util.c].opt-warn = -Wall
We could have used = instead of += in Exe += program, but using += is a good habit when adding items to a class. If there were multiple program descriptions in different sections of the makefile, += would allow them to coexist peacefully, whereas when using = the last assignment will override previous ones.
Note: The above example uses classes that are defined in the crank-c package which must be included by the makefile using, e.g., include $(crank-c).
Crank generator classes have an out property that evaluates to the name of the output file. Other class definitions can use this to name their own input files. This is enabled by the get function, which evaluates a property of an arbitrary instance. Its general form is:
$(call get,<property>,<classes>,<items>)
The <classes> value can be one or more classes. get returns the property value for all the listed items (if more than one, they are delimited by a space character, as are the results).
$(call get*,<property>,<classes>)
get* evaluates a property for all members of one or more classes.
Here is an example of chaining:
PrinceDoc += $(call get*,out,SmarkDoc)
The above line builds a PDF from every HTML file generated by SmarkDoc.
In a larger makefile, defining a new subclass and assigning it properties is preferable to modifying built-in classes, since your code will be more reusable and less likely to interfere unexpectedly with other parts of the makefile.
Creating a subclass is as simple as choosing a new name and defining .parent:
Snazzy.parent = SmarkDoc
The above line declares “Snazzy” as a subclass of “SmarkDoc”. A subclass is functionally equivalent to its parent until properties are attached to the subclass.
As mentioned above, the properties of a subclass will override the properties of the parent class. Often, however, when defining a subclass you would like to add something to what was defined by the parent class, perhaps to the end or the beginning, or perhaps you would like to remove something. The inherit function allows this: it returns the value that would have been inherited if the current property definition had not existed. Here is how it is used in one built-in class:
TSmarkDoc.exports = $(call inherit) CONTENT
Crank defines shorthands for some properties that are similar to Make automatic variables of the same name.
$@ is defined as $(call .,out)
$^ is defined as $(call .,^)
$< is defined as $(firstword $^).
Unlike the Make automatic variables, which are available only during the build phase, these variables are available whenever properties are being expanded. (Properties are expanded before Make's build phase.)
Note that $^ evaluates to the input files that should appear on the command line. This differs from the Make automatic variable $^, which includes all other pre-requisites (including implied dependencies).
A class can inherit from multiple parent classes by listing all of them in .parent, separated by spaces. The parents will be searched in the order listed — first the entire inheritance chain for the first parent, then the entire chain for the second, and so on. The first property definition found will be used.
$(call .?,PROP) returns 1 if .PROP is defined for the current instance, and the empty string otherwise.
$(call .-,PROP,DEFAULT) returns the value of .PROP if it is defined, or DEFAULT otherwise.
Unlike ., these functions do not throw an error when the property is not defined. They can be useful when dealing with computed property names, but when the property names are not computed it is better to avoid .? or .-, because the “property not defined” errors they bypass can be very useful in avoiding or diagnosing problems. When you want to make a property optional to subclasses or instances, use . and provide a default definition in the base class.
After including crank.min, a makefile can use the <require> function to include other makefiles. The <require> function accepts a file path as an argument and searches for a matching file relative to any of the directories listed in <requirePath>, which defaults to the directory of your Makefile and the Crank directory. If a file of the same name has already been included, <require> does nothing.
The Gen base class provides a lot of functionality that subclasses can rely upon. This section provides a high-level view of the meanings assigned to properties. If you have any further questions about how a particular property is used by Gen, the authoritative answer will be found in the source code for Gen in crank.min.
The out property specifies the output file name. There is always one output file per rule, and it is almost always based on the item name. Usually, the file extension is replaced with the value of .ext. For example, item “hello.c” generates an output file named “hello.o”.
All output files are written underneath a directory given by the current variant's buildDir property, which defaults to .crank/release. The path incorporates the name of the class that generated the rule. Any path elements in the item name are also preserved, with the exception of .crank/release.
For example:
Smark += foo.txt # --> .crank/release/Smark/foo.html Smark += ../x/bar.txt # --> .crank/release/Smark/__/x/bar.html Smark += .crank/release/Unzip/foo.txt # --> .crank/release/Smark/Unzip/foo.txt
This may seem gratuitous at first, but note that:
The object file's path tells you how it was built and what it was built from.
Multiple derivatives of a single input file can be generated within a single invocation of Make.
Subclasses or items can override .out. More commonly, they will specify a different file extension by overriding .ext, which is used by the base definition of .out.
Compile.ext = .o
Input files are listed in .in.
Its usage depends on what kind of rule is generated by the class.
Most rules have one primary input file the correlates with the output file. In these cases, .in will default to the item name, which is also used to compute the output file name.
For example, one .c file generates one .o file, so the item name is the name of the C file, as in Compile += foo.c.
Some rules generate one file from many input files. In this case, the input files are unrelated to the item name, and are specified separately by the project makefile.
For example:
Zip += src Zip[src].in = $(wildcard *.c *.h)
The ^ property is used within class implementations to describe input files that should appear on the command line, so it may differ between class implementations. In most cases it is equivalent to .in. On case in which it may differ is in a Compound generator like Exe that infers rules for intermediate files.
The command property evaluates to one or more shell commands that construct the output file. Newline characters — $(\n) — may be used to delimit multiple characters.
The Gen base class takes care of constructing the command section of the Make rule, prefixing lines with tabs (and optionally @, based on other properties). It also ensures that the output directory will be created.
If you are familiar with Make, be aware of the difference between Make command strings and shell commands. Make performs a round of expansion before executing commands. Crank escapes the results of .command so all $ characters in the command are passed through literally to the shell.
“Goals” are the target names specified on the Make command line. If no goal is specified, Make will build the default goal, which is the first target defined in the makefile. If the project makefile defines no targets, all will be defined by Crank as the default goal, and which will build all of the build items defined in the makefile (unless they specify otherwise).
Crank creates a phony target for each class and instance. So, for example, you can type make Exe to build all items in the Exe class, or Make Exe[prog] to build the prog items in the Exe class.
You may override .goal for a class or item to change the name of the phony target. When goal evaluates to the empty string, the build item will not be associated with any phony target (including all). (Note that it will still be built when it is named as a prerequisite of some other target that is being built.)
You can control the value of environment variables during execution of a rule. The exports property is a list of variable names. When the rule's command is executed, these variables will take on the value of the property by the same name.
For example:
MyClass.exports = PATH MyClass.PATH = /usr/local/bin/ghc
Makefile authors should be aware of the following names used by Crank:
Variables beginning with < or \ or consisting entirely of punctuation.
Single-character upper-case letters.
inherit build get v _v v.
Additionally, Crank and extension modules define a number of classes, all beginning with an uppercase letter.
Variants are different configurations of your project that are built from the same project description (your makefile contents) but which produce different build results. With Crank you can build different variants without having to “clean” the previously built variant. In fact, you can build multiple variants simultaneously in one invocation of make.
The variable V lists one or more variants to be built. V defaults to “_”. Project makefiles that make use of variants will usually assign some other value for V, which can be overridden on the command line:
$ make V=debug $ make V='debug release coverage'
For each variant being built, a complete set of rules will be generated. As the rules are generated, the variable v (lowercase, not upper) holds the name of the current variant. Class and item property definitions can inspect v to make decisions about, for example, what flags to pass to a compiler.
The value of v is incorporated into the build directory name (except when it is the default variant). This ensures that differently constructed target files will be cleanly segregated.
Crank provides functions that support a convention for structuring variant names as a _-delimited sequence of words. Each word may consist of a key-value pair delimited by . character.
v. evaluates a property for the variant currently being built. For example, to check for the presence of a “debug” flag:
$(if $(filter debug,$(call v.,flags)), ... )
When a variable is assigned using =, the right hand side is recorded as the definition of the variable, but it is not expanded (evaluated) until later, when the variable is expanded. When a variable is assigned using :=, the right hand side is expanded immediately ... when the assignment is first encountered.
The object-oriented operators — ., .?, .-, inherit — return values that depend upon the current context: class name, item name, and variant. These expressions should never appear on the right hand side of a ':=' assignment.
To define a new generator class, subclass Gen and provide a definition of .command.
Typically, generators will specify the extension to be used for the output file. For example:
Frob.parent = Gen Frob.command = $(call .,exe) -o $@ $^ Frob.exe = frob Frob.ext = .frobbed
The command name is typically defined in a separate property, exe, so that other makefiles can redefine it to include the complete path to an executable file. This separates configuration information — the location and name of the executable on a given system or in a given project tree — from your rule definition.
Since Frob inherits from the Gen base class, all of the following functionality is in effect:
A unique, descriptive output directory is automatically chosen.
The output directory will be automatically created.
make clean behavior is automatic.
make all and make Frob can be used to build the targets of the class.
make @= will select verbose builds.
The output file will be rebuilt if the command string changes.
Sub-classes can assign other Value Dependencies.
The Compile class and others use the Options mixin to construct command line arguments flexibly. This mixin computes a .options property that is included in the command line used to build an item.
These options are computed from a property called .flags, which is a list of words (or “flags”).
Flags beginning with “-” are passed through literally on the command line when the item is built.
Flags not beginning with “-” are treated as symbolic names, and the command line options to pass are given by another property named .flag-FLAGNAME. For example, when the flag “X” is enabled, the value of the property .flag-X will be added to the command line.
For each flag name there is an opposite flag name. The name “noX” and “X” are opposites. When two opposites appear in a set of flags, the one that appears last wins. The Options class defines .flags as $(call v.,flags), which defaults to all the underscore-delimited words in the variant name. Classes and items can append or prepend to the inherited value depending on whether they want the variant to be able to override.
Aliases are flag names that expand to one or more other flag names. This expansion is done before override processing. .flagAliases specifies the aliases and their definitions, in the form “ALIAS=F1;F2;F3 ALIAS2=F4...”.
Example:
Makefile excerpt: Class.flags = debug quiet $(call inherit) Class.flag-debug = -g Class.flag-quiet = -s Class.flag-warn = -Wall Class[X].flags = $(call inherit) -W $ make V=warn_nodebug Class[X].options Class[X].options = "-Wall -s"
Use make help to summarize the contents of your makefile.
$ make help "make [all]" builds: .crank/release/Smark/doc1.html ...
Crank works by generating GNU Make source and executing it using Make's eval function. You can ask Crank to display this auto-generated source by typing make help_debug.
make clean
Typing make clean will delete all output files. This removes the build directory and also any individual output files that lie outside the build directory.
Cleaning is parametrized by V in the same way that other targets are, so make clean will only clean build results for the default variant. Use make clean V=... to specify other variants to be cleaned.
GNU Make allows us to express dependencies in one way: in terms of relative ages of files. We can cause a file to be rebuilt when some other file is newer.
Crank supports expressing dependencies in terms of the values of properties. This can cause a file to be rebuilt whenever a computed value differs from the value that was computed when it was last built.
To specify a value dependency, set .valueDeps to a list of property names. If any of the listed properties change in value since the last time the output file was built, it will be rebuilt.
For example:
Compile = a.c Compile.includes = $(INCLUDES) Compile.valueDeps = in
This will cause a.o to be rebuilt if the value of $(INCLUDES), an environment variable, has changed since the last time a.o was built.
make open=<name> is a convenience for inspecting build results. It will look for an output file name ending in <name> or, if none are matched, an output file name containing <name> as a substring, and then issue a command to view the contents of the file. This command defaults to 'open' on MacOS or 'explorer' on Windows. Override .open to specify a different command to be used for an item or class.
Here are a set of use cases that illustrate different aspects of using a build system. These can be used as a checklist for comparing and contrasting build systems.
How can one write a simple “generator”/“rule” that can apply to many files? Specifically, consider a rule for generating HTML from plain text using Smark. In order to be complete, the generator must support explicit and implied dependencies and “make clean”.
In Crank, one answer looks like this:
Smark.parent = Gen Smark.command = $(call .,exe) -o $@ $(call .,options) --deps=$(call .,depFile) -- $^ Smark.options = $(foreach c,$(call .,css),--css=$c) Smark.depFile = $(call .,out).d Smark.ext = .html Smark.css =
How does one use a generator a project's makefile?
Crank example:
Smark += a.txt b.txt c.txt d.txt
How does one externalize configuration from a library of generators? What does the resulting makefile look like?
In Crank, reusable rules typically isolate configuration parameters as individual properties that can be overridden by the makefile that uses the rule, as in the following:
include $(smark)/crank-smark.min Smark.exe = ~/git/tooltree/smark/out/release/smark
Subclassing. Some makefiles want to use an existing rule but modify it in some way. Some want to use slightly different rules for different sets of files, or make exceptions for a few files out of a large set.
Example specialized class:
MyDocs.parent = Smark MyDocs.css = mystyles.css
Example per-items specialization:
Smark[foo.txt].css = foostyle.css
How does the build system answer these questions?
How to write rules that describe different variants?
In Crank, you write your declarations one time and the declarations will be expanded once for each variant being built. Object properties can use $v to obtain the name of the current variant, or invoke the function v. to query a property of the variant.
For example, the rules for compiling C files use $(call v.,flags) to obtain a set of flag names that can turn on or off features like debug symbols, optimization, and so on.
How to specify a variant?
In Crank, the makefile can define variant properties with this syntax:
V[variantName].prop = value
The .flags variant property defaults to $(subst _, ,$I). (Note that in the context of variant property expansion, $I is the name of the variant.)
For example, assigning V[dbg].flags = debug will set the flags property of the dbg variant to debug, which will cause object files generated for that variant to include symbols and use minimal optimization.
Where do build results go?
In Crank, the Gen base class defines .out in a way that segregates results based on variant name, class name, and item name. This can, of course, be overridden for particular subclasses or items.
How can one route the outputs of one generator into the inputs of another?
PrinceDoc += $(call get,out,SmarkDoc,foo.txt)
Changes to makefiles can result in changes to command lines or environment variables that affect the build results. Making all files depend on the Makefile can have costly consequences when only minor updates are made. How can one easily represent these “value dependencies” (dependencies that are not based on input files).
In Crank, one lists the set of properties to be tested at build time. It defaults to command, but other values can be assigned.
SmarkDoc[foo.txt].valueDeps = SMARK_PATH command
Auto-generated headers are an example of auto-generated source files that might be implied dependencies. We do not know, before compiling a C file or scanning its dependencies, whether an auto-generated header is an actual dependency of the generated object file. But we must generate the header before compiling any C files, since any one of them might include the header.
One approach is to treat all auto-generated headers as dependencies of all object files generated from C, but that creates false dependencies: changes to any auto-generated header will rebuild all object files. However, if we treat them as order-only dependencies, this will not happen. Order-only dependencies can guarantee that the headers are built before any C files are compiled. Ordinary dependency scanning will identify the ones that are real dependencies.
Crank recognizes the ooDeps property as listing order-only dependencies.
Compile.ooDeps = $(call get*,out,IDL)
A number of limitations stem from Crank's being built on GNU Make.
The set of characters that can be used in file names is limited. It does not support file names that contain whitespace, ':', or '=', or any shell special characters: !&|<$'"`\!
When performing parallel builds, all concurrent processes write to the same stdout and stderr pipes, producing potentially confusing output.