-
Notifications
You must be signed in to change notification settings - Fork 137
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 –larchNobody 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>-
outputis a name of file that should be created as a result ofmake outputcommand; -
dependenciesare files required for correct command completion separated by spaces. There can be no dependencies. -
commandis 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 isrmfor 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 programEach 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 programIf 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.cppVariables 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
endifSome 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.