Skip to content

Makefiles

Daniel Kofanov edited this page Nov 30, 2019 · 4 revisions
Note: MIPT-MIPS does not use Makefiles any longer, it uses CMake instead.
However, Make is still used as intermediate tool and as MIPS traces build tool

Why do we need makefiles?

As you've seen in C++ Build manual, simple project can be build in a few commands:

gcc funcsim.cpp –c –O2 –Wall –std=c++03 –Werror
gcc memory.cpp  –c –O2 –Wall –std=c++03 –Werror
gcc decoder.cpp –c –O2 –Wall –std=c++03 –Werror
gcc perfsim.cpp –c –O2 –Wall –std=c++03 –Werror
gcc funcsim.o memory.o decoder.o –o funcsim.out -larch
gcc perfsim.o memory.o decoder.o –o perfsim.out –larch

Nobody will type this commands by hands everytime, so it's obvious to create a bash script or something like this. But, for really big projects, writing and debugging of that script can be really complicated. That is the first reason to using special tool for building projects.

The second reason is about rebuilding. Imagine that you've touched only one small .cpp file, but if you're using script you have to rebuild everything. It can take even hours!

The solution has been suggested in 1970s and became a standard de-facto, it is called make utility.


make basics

make is an utility that runs according to rules written in Makefile. Makefile is a plain text file (it's always named Makefile without any extension) distributed with project sources and documentation.

Makefile is a set of rules of build called targets. Each target contains three main parts:

   <output>: <dependency1> <dependency2> ...
   -TAB-<command>
  • output is a name of file that should be created as a result of make output command;
  • dependencies are files required for correct command completion separated by spaces. There can be no dependencies.
  • command is a shell command that performs creation of output file. Usually it is compiler or linker, but you are free to use any shell command, including own scripts. Common used case is rm for cleanup.
Warning: -TAB- here is a symbol that is inserted by TAB key (\t). Spaces cannot be used here!

For example, the rules for making simple program will look like:

    func.o: func.cpp
        gcc func.cpp -c -o func.o

    main.o: main.cpp
        gcc main.cpp -c -o main.o

    program: func.o main.o
        gcc func.o main.o -o program

Each target is processed by following algorithm:

   function make(target) {
      foreach (dependency in target.get_dependecies()) {
          if (target_exists(dependency)) {
              make(dependency);
          }
          else if (!file_exists(dependency)) {
              error();
          }
      }
      if (!file_exists) {
          target.run_command();
      }
      else {
          foreach (dependency in target.get_dependecies()) {
              if (is_older(dependency, target)) {
                 target.run_command();
                 break;
              }
          }
      }
   }

For example, running of make program at first time is equal to following commands:

   gcc func.cpp -c -o func.o
   gcc main.cpp -c -o main.o
   gcc func.o main.o -o program

If you run make program second time, make will notice that everything is up-to-date and won't do anything else. Let's assume that we changed file func.cpp and run this command again. Make will understand that main.o is still up-tp-date and won't rebuild it:

   gcc func.cpp -c -o func.o
   gcc func.o main.o -o program
Note: All information above is enough for writing good makefiles in our project, but if you want to be experienced in Makefile, you may use tricks described below.

Automatic variables

To make your makefile more flexible, you may use automatic macrovariables. Here is list of the most popular ones:

  • $@ — target name.
  • $< — first dependency name.
  • $? — all dependencies more relevant than target (only changed dependencies)
  • $+ — all dependencies
  • $^ — all dependencies without repeats

Our example will look like:

    func.o: func.cpp
        gcc $+ -c -o $@

User variables

You may create own macrovariables and use them:

   SRC_DIR = source
   C_FILES = $(SRC_DIR)/func.cpp $(SRC_DIR)/main.cpp

Variables can be set from console:

   `make all DEBUG=1`

or set by directives:

ifeq ($(DEBUG), 1) 
	C_FLAGS = -O0 –g –DENABLE_TRACE=1
else
	C_FLAGS = -O3
endif

Some variables are set by default in Linux environment and can be changed locally:

  • CC — C compiler
  • CFLAGS — C compiler flags
  • LDFLAGS — Linker flags
  • CXX — C++ compiler
  • CXXFLAGS — C++ compiler flags

To make your project more platform-independent, you may use shell-generated variables:

  • $(shell uname -m) returns architecture of current PC (i686 or x86_64 )
  • $(shell uname -o) returns OS name (GNU/Linux, Cygwin …)

Substitutions, implicit targets

Finally, to avoid re-typing of filenames, you are free to use trick of implicit targets. Rule below describes making of any *.o file from OBJ_DIR:

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(CFLAGS) $< -c -o $@

This trick becomes even more powerful with substitution pattern. For example, if you want to get list of objective files from source files, you may write this code:

OBJS_FILES = ${C_FILES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o} 

Example of makefile

Let's unite all described tricks. The Makefile looks like:

CC := gcc
CFLAGS := -Wall

ifeq ($(DEBUG), 1)
	CFLAGS := $(CFLAGS) -O0 -g
else
	CFLAGS := $(CFLAGS) -O2
endif

SRC_DIR := source
BIN_DIR := bin
OBJ_DIR := obj

C_FILES := $(SRC_DIR)/func.c $(SRC_DIR)/main.c
OBJS_FILES := ${C_FILES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o}

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(CFLAGS) $< -c -o $@

$(BIN_DIR)/program: $(OBJS_FILES)
	$(CC) $(LDFLAGS) $^ -o $@

all: build_dirs $(BIN_DIR)/program

build_dirs:
	mkdir –p $(BIN_DIR)
	mkdir –p $(OBJ_DIR)

clean:
	rm -rf $(BIN_DIR)
	rm -rf $(OBJ_DIR)

It looks scary, doesn't it? :-)

Clone this wiki locally