Crank

Introduction

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.

Crank Concepts

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:

  1. an output file

  2. some number of prerequisites

  3. 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:

  1. There is no mutable state associated with an object. The set of objects and properties is static.

  2. There is no “object” data type — everything is a string.

Using Generators

Simple Case

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

Chaining

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.

Subclassing

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

Property Shorthands

Crank defines shorthands for some properties that are similar to Make automatic variables of the same name.

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

Multiple Inheritance

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.

Undefined Properties

$(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.

Modules

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.

Base Class Conventions

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.

Output Files

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:

  1. The object file's path tells you how it was built and what it was built from.

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

Input files are listed in .in.

Its usage depends on what kind of rule is generated by the class.

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

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

Command Strings

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.

Make Goals

“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.)

Environment Variables

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

Variable Naming

Makefile authors should be aware of the following names used by Crank:

Additionally, Crank and extension modules define a number of classes, all beginning with an uppercase letter.

Variants

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.

Assignments With “:=” vs. “=”

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.

Defining Your Own Classes

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:

Flags and Options

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"

Other Features

Help

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.

Value Dependencies

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.

open

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.

Examples and Use Cases

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.

Writing a Generator

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 =

Using a Generator

How does one use a generator a project's makefile?

Crank example:

Smark += a.txt b.txt c.txt d.txt

Configuration

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

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

Multiple Variants

How does the build system answer these questions?

Chained Generators

How can one route the outputs of one generator into the inputs of another?

PrinceDoc += $(call get,out,SmarkDoc,foo.txt)

Value Dependencies

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

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)

Problems with Crank

A number of limitations stem from Crank's being built on GNU Make.