Spread the love

How to write first makefile for c source code in linux - Tutorial

Prerequisites

  1. Should know how to compile program in Linux by using terminal
  2. Basic knowledge of compiler stages like compiler, assembler, linker, etc

Introduction

If you ever come across the manual installation of any software/Library in Linux than you will definitely heard about make and Makefile. The general procedure to install any software/Library in Linux is

Step 1: Run Configure or config script

Step 2: Do make

Step 3: and in last make install

Here, we only discuss about make and Makefile with theory and hands-on.

What is Makefile?

Makefile is a script written in a certain prescribed syntax which helps to build the target output (normally, one or more executables) from source files by compilation and linking. In simple words, makefile will compile your source code in simple & fast way.

Why we need Makefile?

=> Large projects can contain multiple source files which are dependent in one another or arranged in hierarchical manner for example, in order to compile file A, you have to first compile B; in order to compile B, you have to first compile C; and so on.

=> Make is a solution to these problems. It can be used to compile whole project in well arranged manner and generate your target according to your make rule(which we will discuss later) by entering single command that is make.

=> An important feature is that when a project is recompiled after a few changes, it will recompile only those files which are changed, and any other files that are dependent on it. This saves a lot of time.

=> For a large project, when a few changes are made to the source, manually recompiling the entire project each time is tedious, error-prone and time-consuming.

How to write Makefile ?

Now writing Makefiles, But before that i want to tell you that i have divided this course in different levels so you can start slow with level 1 and go on. And after the end of this small course you can write and understand Makefile easily.

Level 1 : Manual Compilation Process

Level 2 : Basics of Makefile

Level 3 : How Makefile Works ?

Level 4 : Advanced Makefile 

Manual Compilation Process

For example if you have three files .i.e. function.c function.h and main.c to compile and generate binary, this is the simple way to compile your code rather than write compilation line on terminal again and again.

function.h:

 

#include <stdio.h>

void sample_func();

function.c:

#include "function.h"

void func()
{
       printf("Hello world! \n");
}

main.c:

 

#include "function.h"

void func();

int main()
{
     func();

     return 0;
}

The following are the manual steps to compile the project and produce the target binary:

 

vishal@firmcodes:~$ gcc -c -I ./ main.c

vishal@firmcodes:~$ gcc -c -I ./ function.c

vishal@firmcodes:~$ gcc -o Binary main.o function.o
  • -I is used to include the current directory (./) where header file located.
  • -c is used to generate object file from source file
  • -o is used to link all required the objects and generate single executable file.

Now all these steps of compiling & linking are cumbersome if source and header files are increased in number, so we make a single Makefile to compile and generate binary by entering single command i.e. make.

 

Basics of Makefile

The general syntax of a Makefile rule is as follows:

target: dependency1 dependency2 ...

[TAB] command1

[TAB] command2

...

Note: here tab is must otherwise make will present error

makefile:<line_number>: *** missing separator. Stop.

# My First Makefile

all: main.o function.o
        gcc -o Binary main.o function.o

main.o: main.c
        gcc -c main.c -I ./

function.o: function.c
        gcc -c function.c -I ./

clean:
        rm -rf *.o
        rm -rf Binary
  • # is used to comment in Makefile as you seen in first line.
  • all is a special target which depends on main.o and function.o, and has the command (from the “manual” steps shown earlier) to make GCC link the two object files into the final executable binary.
  • main.o is a filename target that depends on main.c, and has the command to compile main.c to produce main.o.
  • function.o is a filename target that depends on function.c and function.h, it calls GCC to compile the function.c file to produce function.o.
  • clean is a special target that has no dependencies, but specifies the commands to clean the compilation outputs i.e object and binary files from the project directories.

 

How Makefile works ?

=> When the make command is executed on terminal, it looks for a file named makefile or Makefile in the current directory and constructs a dependency tree.

If you have several Makefiles, then you can execute specific with the command:

                               make -f MyMakefile

=> Based on make target specified in makefile, make checks if the dependency files of that target exist. And if they exist, whether they are newer than the target itself, by comparing file timestamps.

Here our first and default target is “all” which looks for main.o and function.o file dependencies. Second and third target is main.o and function.o respectively which have dependencies of main.c and function.c respectively.

=> Before executing the commands of corresponding target, its dependencies must be met, when they are not met, the targets of those dependencies are executed before the given make target, to supply the missing dependencies.

=> When a target is a file-name, make compares the time-stamps of the target file and its dependency files. If the dependency file is newer than target file, target execute otherwise not execute.

In our case, when first target “all” start executing it looks for main.o file dependency, if its not met. Then it goes to second target main.o which check for its dependency main.c and compare time-stamp with it. If target found main.c dependency is updated, then target execute else not. Same process is follow for next target function.o.

=> It thus winds up recursively checking all the way down the dependency tree, to the source code files. By this process, make saves time, by executing only commands that need to be executed, based on which of the source files (listed as dependencies) have been updated, and have a newer time-stamp than their target.

=> Now, when a target is not a file-name(which we called “special targets”), make obviously cannot compare time-stamps to check whether the target’s dependencies are newer. Therefore, such a target is always executed.

In our Makefile, special targets are “all” and “clean”. As we discussed target “all” earlier, but we not discuss target clean. Target clean removes the all object files created during compilation and binary executable files according to command.

For the execution of each target, make prints the actions while executing them. Note that each of the command are executed in a separate sub-shell environment because of secure execution so that they can not change current shell environment which may affect other target execution. For example, if one command contains cd newdir, the current directory will be changed only for that line command, for the next line command, the current directory will be unchanged.

 

Advanced Makefile

Makefile Macros/Variables

The makefile allows you to use macros, which are similar to variables and used as same as C macros. For example:

 

CC = gcc
CFLAGS = -O -Wall
LDFLAGS =
LIBS = -lmcpp -lm -lopenssl
OBJECTS = main.c function.c

Here, CC is variable or macro which assign value gcc and same as other variables too. There are many different techniques to assign value to variables like

  • = simply assign RHS to LHS which is re-evaluated every time at its occurrence in Makefile.
  • += assignment append the value to already assigned value, for example CC = gcc, CC += -W, now CC holds value gcc -W.
  • := assignment will assign RHS to LHS with the value is expanded and stored to all occurrences in the Makefile, For example, when a CC := ${GCC} ${CFLAGS} simple definition is first encountered, CC is expand and replaced with gcc -O -Wall at every occurrences in the Makefile

Note: In Makefile everything is treated as string.

There are mainly two types of macros, user defined like variables and standard macros.There are lots of standard macros (type “make -p” to print out the standard macros), some of those are CC, CFLAGS, AR, LDFLAGS, CPP, etc. The exapansion of macros is done with $ sign, for example $(CC) or ${CC}.

You can also define macros at the command line such as

 

make CC=arm-none-linux-gnueabi-gcc

Patterns/Directives of Makefile

  • % character can be used for wildcard pattern-matching, to provide generic targets. For example:

 

%.o: %.c           //When % appears in the dependency list, it is replaced with the same string that was used to perform substitution in the target.
[TAB] command
  • $@ represent full target name of the current target. For example:

 

Binary: main.o function.o
        gcc -o $@ main.o function.o       //$@ will replace with target name Binary
  • $? represent the dependencies that are newer than the current target. For example:

 

Binary: main.o function.o
        gcc -o $@ $? $(OBJECTS)       //$? will replaced with newer dependencies if required
  • $* represent the text that corresponds to % in the target. For example:

 

%.o: %.c
      gcc -c $*.c -I ./           //$* will replaced with target name
  • $< represent of the first dependency.
  • $^ represent of all the dependencies with space as the delimiter.
  • @ print output of command to standard-output(Monitor screen). For example: To print custome message to standard output while executing makefile we use like

 

@echo "Building.."

Executing shell command in Makefile

Sometimes we need to use the output of shell command from the Makefile. For example, checking versions/locations of installed libraries, or other files required for compilation. We can obtain the shell output using the shell command. For example:

 

@echo $(shell ls) # To print output of command 'ls'

//Note: We can also store output of this command in variable like this LIST=$(shell ls)

Advanced Makefile

 

CC = gcc # Compiler to use
INCLUDES = -I ./ # Directory for header file
OBJS = main.o function.o # List of objects to be build

all: ${OBJS}
     @echo "Building..."             # To print "Building.." message
     ${CC} ${OPTIONS} ${INCLUDES} ${OBJS} -o Binary

%.o: %.c                             # % pattern wildcard matching
     ${CC} ${OPTIONS} -c $*.c ${INCLUDES}

list:
     @echo $(shell ls)               # To print output of command 'ls'

clean:
     @echo "Cleaning..."
     rm -rf *.o
     rm Binary

With this brief introduction to Makefiles, you can create some very sophisticated mechanisms for compiling your projects efficiently. However, this is just the drop of the river. I don’t expect anyone to fully understand the example presented above without having consulted some Make documentation