Tuesday, November 8, 2011

Introduction to Make

<Thanks to  Jennifer Vesperman>

Make originated as a system for building compiled code. It is now used as a system for making changes across many files and directories. It is useful whenever a change in one file requires changes or actions elsewhere.
Make is useful for system administrators as well as developers. This article primarily discusses Make as a compilation tool, but it can be effective for program installation or system configuration changes.

Running Make

Make requires a configuration file. Once this file is constructed for your project, you usually type make to build the changed files.
The usual name for this file is Makefile; the capitalization lists the makefile with README and other special files.
When run with no arguments, GNU Make looks for the configuration files named GNUmakefileMakefile, and makefile in the current working directory. If using other names, call Make with make -f filename.
The makefile in this example displays make complete and does nothing else. By default, Make lists the commands it runs. To have it run quietly, call Make with make -s.
$ls
makefile    renamed_makefile

$make
echo make complete
make complete

$make -f renamed_makefile
echo make complete
make complete

$make -s
make complete

Simple Makefiles

The examples in this article are written for C, and produce the sample target file. Make can produce any target file, use any shell commands, and work from any source files. Make works best with languages where the compiler does not itself try to resolve dependencies.
Experienced users of Make will see redundant lines in the example makefile. Make already knows how to compile some types of files, and the rules for those files can be left out. (These are called implicit rules.) For clarity, the examples show these rules.
# Linking object files
sample: main.o example.o
  cc -o sample main.o example.o
  echo sample: make complete

# Compiling source files
main.o: main.c main.h
  cc -c main.c
example.o: example.c defs.h
  cc -c example.c

A Make rule is composed of:
target: prerequisites
 commands

A target is considered "up to date" if it exists and is newer than its prerequisites.
Make works backwards, starting with the target of the first rule in the file. In our example, that's sample. Make checks the prerequisites for sample -- main.o and example.o -- to see if they have rules. If they do, it recursively checks their rules.
Make walks down the recursion chain until it finds a target that has no prerequisites, or whose prerequisites have no rules. Once it hits one of those, it walks back up its recursion chain and runs commands as necessary. It creates a recursion chain for every prerequisite it encounters that has a rule.
Once all of the prerequisite rules have been run, it eventually returns to sample's rule. If the file doesn't exist, or is older than its prerequisites now are (after their rules have been recursively tested), it runs the commands to generate sample.
In the example makefile, Make:
  1. Runs the first rule it sees -- sample.
  2. Checks to see whether sample's prerequisites have rules. They do.
  3. Runs the rule for the first prerequisite -- main.o.
  4. Checks to see whether main.o's prerequisites have rules. They don't.
  5. Checks whether main.o is up to date. If not, it runs the commands for main.o.
  6. Runs the rule for the second prerequisite -- example.o.
  7. Checks to see whether example.o's prerequisites have rules. They don't.
  8. Checks whether or not example.o is up to date. If not, it runs the commands for example.o.
  9. Returns to sample's rule
  10. Checks whether or not sample is up to date. If not, it runs the commands to update it.
Make can run the prerequisites in any order. The important part of this sequence is that it runs recursively backwards from the first target (or the target named in the command parameters), and tests only the rules that it encounters in the prerequisites chain.
Make aborts compilation if it receives an error. This is usually useful behavior -- it lets you correct compiler-detected problems during a compile-and-test cycle. The option -i tells Make to ignore errors.

Phony Targets

The special rule .PHONY tells Make which targets are not files. This avoids conflict with files of the same name, and improves performance.
In software development, it's very convenient to create a script to remove old compiled code so that the next build recompiles everything. It's also convenient to have a script for installing the code. Make allows scripts like this to be included in the makefile, as phony targets. Phony targets may have prerequisites, and may themselves be prerequisites.
If a phony target is included as a prerequisite for another target, it will be run every time that other target is required. Phony targets are never up-to-date.
To run a phony target from the command line, call Make with the name of the phony target, e.g.: make clean.

# Naming our phony targets
.PHONY: clean install

# Removing the executable and the object files
clean: 
  rm sample main.o example.o
  echo clean: make complete

# Installing the final product
install:
  cp sample /usr/local
  echo install: make complete

Makefile Variables

As a project gets larger, more files are usually added. If you repeat a list of files, you can accidentally leave files out of the list. It's simpler to make use of a variable that expands into the list.
The syntax for declaring and setting a makefile variable is varname = variable contents. To call the variable, use $(varname).
# Defining the object files
objects = main.o example.o

# Linking object files
sample: $(objects) 
  cc -o sample $(objects)
  echo sample: make complete

# Compiling source files
main.o: main.c main.h
  cc -c main.c
example.o: example.c defs.h
  cc -c example.c

# Removing the executable and the object files
clean: 
  rm sample $(objects)
  echo clean: make complete

Final Touches

There are a few touches which make the difference between a usable and a professional makefile. The next example adds those extra touches.
# 1
# Defining the compiler:
CC=gcc

# Defining the object files:
objects = main.o example.o

# 2
# The default rule - compiling our main program:
all: sample
  echo all: make complete

# 3
sample: $(objects)
 # If we get here, all the dependencies are now built.
 # Link it:
 $(CC) -o $@ $+

# 4
# Tell make how to build .o files from .c files:
%.o:%.c
 $(CC) -c $+
 
# 5
#Now make sure that make rebuilds files if included headers change:
main.o: main.h defs.h
example.o: example.h defs.h
  1. Use a variable for the compiler, in case you want to use the same makefile with a different compiler.
  2. When called without a rule parameter, Make runs the first rule it encounters. It is more human-readable to explicitly state your first rule. all is a common name for a first rule.
  3. The automatic variable $@ means "the name of the target." The automatic variable $+ means "all prerequisites, space-separated." Automatic variables are pre-defined in Make.
  4. A pattern rule tells make how to convert the prerequisite to the target. The % in the pattern means "one or more characters," and refers to the same string in the prerequisite and the target. This particular pattern tells make how to convert a *.c file to a *.o file of the same name.
    The automatic variable $+ means "all prerequisites, space-separated."
  5. These rules are relying on "implicit rules." Make has built-in patterns for converting a *.h file to the dependent *.o. These rules are included to define the prerequisites for the relevant *.o files.

Caveats and Gotchas

  • This article is written about GNU Make. Other Unix systems use different versions of Make, which may have minor variations.
  • Make rules require a tab at the beginning of each command. A series of spaces doesn't work.
  • Make works backwards from the target to the prerequisites.

Final Words

Make has many features I haven't mentioned, and is useful in any situation where a file is dependent on files that may change. Experiment, and develop your own interesting variations on makefiles.

Further Reading

No comments: