-
Notifications
You must be signed in to change notification settings - Fork 139
Makefiles
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 |
---|
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
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 ofmake 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 isrm
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. |
---|
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 $@
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 …)
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}
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? :-)
MIPT-V / MIPT-MIPS — Cycle-accurate pre-silicon simulation.