9 minutes read

Make is a utility in Linux which automatically identifies parts (files) of a project that have changed and issues a set of user-specified commands to recompile/modify them. The user-specified commands issued by the make utility are defined in a Makefile. make is an automation tool that is especially useful for automating the compilation process and the build workflow in larger projects written in compiled languages like C/C++. While the project may have hundreds of files, make only recompiles the files that have changed instead of recompiling them all, thus saving processing resources.

The use of make and Makefiles though is not restricted to compiling source files of programs. They can be used with other files and that's exactly what we are going to do in this topic. We will look at examples dealing with basic text files so no prior programming knowledge is required.

Makefile syntax

A Makefile is just a set of multiple rules.

The following is the generalized syntax of a Makefile rule:

target: dependency1 dependency2 ...
	recipe/commands
	...

In the code snippet above:

target is usually the name of the resulting file. But for intermediate rules which don't produce a target file, this target name can be any random name. Such a target is called a "phony/artificial target".

dependencies are the files(or targets) on which the target file of the current rule depends. If your target doesn't depend on any other files/targets, you can leave this blank.

recipe is nothing but a set of valid Linux commands which can be run on the Unix shell.

Notice that the commands in the recipe are indented by a tab character. This indentation by a tab character is compulsory.

First, let's consider an example of a Makefile where the target; greeting.txt, doesn't depend on any other files.

greeting.txt:
	echo "Happy holidays!" > greeting.txt

To run this file, save these contents in a file named Makefile, navigate to its corresponding directory through the command line, and run the make <rulename> command.

$ make greeting.txt
echo "Happy holidays!" > greeting.txt

$ make greeting.txt
make: 'greeting.txt' is up to date.

When you run make for the first time, the file greeting.txt is created with the text "Happy holidays!". When you run make for the second time, since there is no change in the file, you will only get a message saying that the file is up to date.

Multiple rules in the same Makefile

If there is only one rule in a Makefile, running make and make <rulename> will give the same result. But when the Makefile contains multiple rules, running make will run only the first rule. If you want to run any other rule, you will have to specify its name in the make <rulename> command.

Let's see this in action through the following example:

cache:
	echo "Cache"

ram:
	echo "RAM"

hard_disk:
	echo "Hard Disk"

Running only make on the above Makefile gives this output:

$ make
echo "Cache"
Cache

This shows that only the first rule was run. If you want to run the ram or hard_disk rule, you will have to supply the rule name as an argument to the make command as follows - make ram or make hard_disk.

If you want to run all the rules in the file in one command, there is a simple hack which we will see in the next section since it requires knowledge about dependencies.

Dependencies

Let's consider an example of a file called policy.txt. We have another file called policy_bkp.txt which is a backup for our original file. Whenever we make a change to policy.txt and run make, we want this change to be reflected in our backup file. To accomplish this, we just add the name of the file which our target depends on, after the colon as a dependency.

policy_bkp.txt: policy.txt
	cp policy.txt policy_bkp.txt
	echo "File backup completed."

If your target depends on more than one file, you can specify the file names of all the dependencies by separating each name with a space character.

When your target depends on a pre-existing file, that file doesn't have to be a Makefile rule. But when your target depends on a phony/artificial target, the artificial target has to have its own rule in the same Makefile.

house: kitchen bedroom
	echo "House is made up of various rooms."

kitchen: fridge
	echo "This is the kitchen."

bedroom:
	echo "This is the bedroom."

fridge:
	echo "This is the fridge."

The output of running make is:

$ make
echo "This is the fridge"
This is the fridge.
echo "This is the kitchen."
This is the kitchen.
echo "This is the bedroom."
This is the bedroom.
echo "House is made up of various rooms."
House is made up of various rooms.

Looking at the example above, a rule is not executed until the rules it is dependent on are executed. For a single rule, the dependencies are executed in the order in which they are specified in the Makefile. So, for house: kitchen bedroom, kitchen and all of its sub-dependencies are executed before bedroom.

The hack for running all the rules in a single command (as hinted in the previous section) makes use of this fact. You can create a phony rule at the top of the Makefile and add all the other rules as dependencies for this rule. This way, whenever make is run on the Makefile, all rules are executed in their respective order.

all: cache ram hard_disk

cache:
	echo "Cache"

ram:
	echo "RAM"

hard_disk:
	echo "Hard Disk"

The output of running make in the Makefile above is:

$ make
echo "Cache"
Cache
echo "RAM"
RAM
echo "Hard Disk"
Hard Disk

Use of Makefile variables and environment variables

All variables in a Makefile are string variables. The syntax of declaring a variable and assigning it a value is as follows:

var_name = var_value

To substitute a variable's value, write a dollar sign followed by the name of the variable in parentheses or braces: either $(var_name) or ${var_name} is a valid reference to the variable var_name.

Let's look at an example of a Makefile using variables:

fruit1 = Apple

phantom_target:
	echo ${fruit1} ${fruit2}

fruit2 = Banana

The output of executing make on the above Makefile rule is:

$ make
echo Apple Banana
Apple Banana

You can declare the variable anywhere in the Makefile. It is not compulsory to declare it before it is used. You can see this in action in the example above, where fruit1 has been declared before it is used while fruit2 is declared after the line which uses it.

You can also pass a variable as a command-line argument to make. The only thing you need to be careful about is that the name of the variable entered as a command-line argument should match the variable name in the Makefile.

Let's modify the Makefile in the previous example and add a fruit3 value to it.

fruit1 = Apple

phantom_target:
	echo ${fruit1} ${fruit2} ${fruit3}

fruit2 = Banana

The value of fruit3 will be passed as a command-line argument to make like this:

make fruit3=Cherry

The output of make phantom_target is:

$ make fruit3=Cherry 
echo Apple Banana Cherry
Apple Banana Cherry

The value of one variable can be used while defining the value of another variable.

greeting1 = Merry Christmas
greeting2 = ${greeting1} and a Happy New Year

phony_target:
	echo ${greeting2}

The above Makefile rule will give the following output:

$ make
Merry Christmas and a Happy New Year

You can also use environment variables in a Makefile. They can be referenced just like any other variable.

target:
	echo ${PATH}

Here, PATH is an environment variable.

Flags

If you want to see which commands will be executed by a Makefile but not actually run them, you can pass the -n flag to the make command. This will print/dry run all the commands for you.

$ make -n <phantom_target>

Running make -n on the Makefile in the example concerning fruit1, fruit2, and fruit3 variables from the previous section gives us this output:

$ make -n
echo Apple Banana

The -n flag is the same as --just-print and --dry-run flags. You can use either of them and get the same result.

The -s (same as --silent or --quiet) flag is the opposite of the -n flag. While -n suppresses execution and only prints the output of those particular commands, -s executes commands without printing their respective output.

Running make -s on the previous example will give the following output:

$ make -s
Apple Banana

Conclusion

Makefiles can greatly help you simplify your workflow. You will be able to execute multiple interdependent commands through just a single command. This is especially useful when compiling source code. It also saves a lot of processing power since it only recompiles the modified files. So make sure you start using make!

22 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo