I have always been attracted to minimalism. The idea that one thing should perform one function, but at the same time perform it as best as possible, resulted in the creation of UNIX. And although UNIX has long been a simple system, and minimalism in it is not so easy to see, it can be considered a vivid example of the quantity-quality transformation of many simple and understandable things into one very complex and not transparent. In its development, make was about the same way: simplicity and clarity, with an increase in scale, turned into a terrible monster (remember your feelings when you first opened the makefile).
My persistent disregard of make for a long time was due to the convenience of the IDE used, and the reluctance to understand this 'remnant of the past' (in fact, laziness). However, all these annoying buttons, menus, and so on. attributes of various studios made me look for an alternative to the method of work that I have practiced until now. No, I didn’t become a guru of make, but the knowledge I received is quite enough for my small projects. This article is intended for those who, like me just recently, are willing to break out of the cozy window slavery into the ascetic but free world of the shell.
Make - basic information
make is a utility designed to automate file conversion from one form to another. Conversion rules are specified in a script named Makefile, which must be located in the root of the project working directory. The script itself consists of a set of rules, which in turn are described:
1) objectives (what this rule does);
2) details (that is necessary to fulfill the rules and get goals);
3) commands (performing these transformations).
')
In general, the makefile syntax can be represented as follows:
That is, the make rule is the answer to three questions:
{ ? ()} ---> [ ? ()] ---> { ? ()}
It is easy to see that the processes of translation and compilation fall very nicely on this scheme:
{ } ---> [] ---> { }
{ } ---> [] ---> { }
Simplest Makefile
Suppose we have a program consisting of only one file:
#include <stdio.h> int main() { printf("Hello World!\n"); return 0; }
To compile it, a very simple makefile is enough:
hello: main.c gcc -o hello main.c
This Makefile consists of one rule, which in turn consists of a target — hello, a prop — main.c, and a command — gcc -o hello main.c. Now, to compile, just give the make command in the working directory. By default, make will execute the very first rule, if the execution target was not explicitly specified in the call:
$ make <>
Compiling from multiple sources
Suppose we have a program consisting of 2 files:
main.c
int main() { hello(); return 0; }
and hello.c
#include <stdio.h> void hello() { printf("Hello World!\n"); }
The makefile compiling this program might look like this:
hello: main.c hello.c gcc -o hello main.c hello.c
It is quite efficient, but it has one significant drawback: which one we will reveal further.
Incremental compilation
Imagine that our program consists of a dozen different source files. We make changes to one of them, and we want to rebuild it. Using the approach described in the previous example will lead to the fact that all the source files without exception will be compiled again, which will have a negative effect on the recompilation time. The solution is to divide the compilation into two stages: the translation stage and the linking stage.
Now, after changing one of the source files, it is enough to translate it and link all object files. At the same time, we skip the broadcast stage of the details not affected by the changes, which reduces the compilation time as a whole. This approach is called incremental compilation. To support it, make compares the time for changing goals and their details (using the file system data), so that it can decide for itself which rules to follow and which ones can be simply ignored:
main.o: main.c gcc -c -o main.o main.c hello.o: hello.c gcc -c -o hello.o hello.c hello: main.o hello.o gcc -o hello main.o hello.o
Try to build this project. For its assembly, it is necessary to clearly indicate the purpose give the command make hello.
After- edit any of the source files and compile it again. Note that during the second compilation, only the modified file will be broadcast.
After running, make will immediately try to get the hello target, but to create it you need the main.o and hello.o files, which are not yet available. Therefore, the execution of the rule will be delayed and make will search for rules describing the obtaining of missing details. Once all the details have been received, make will return to the pending goal. It follows that make executes the rules recursively.
Dummy targets
In fact, not only real files can serve as make targets. All those who had to build programs from source codes should be familiar with two standard commands in the UNIX world:
$ make $ make install
The make command compiles the program, and the make install command installs. This approach is very convenient, since everything necessary for building and deploying an application on the target system is included in one file (let's forget about the configure script). Please note that in the first case we do not indicate the goal, and in the second goal, it is not the creation of the install file at all, but the process of installing the application into the system. To do such tricks we are allowed by so-called fictitious (phony) goals. Here is a short list of standard targets:
- all is the default default target. When you call make, you can not explicitly specify it.
- clean - clear the directory of all files received as a result of compilation.
- install - install
- uninstall - and uninstall respectively.
In order for make not to look for files with such names, they should be defined in the Makefile using the .PHONY directive. The following is an example of a Makefile with targets for all, clean, install, and uninstall:
.PHONY: all clean install uninstall all: hello clean: rm -rf hello *.o main.o: main.c gcc -c -o main.o main.c hello.o: hello.c gcc -c -o hello.o hello.c hello: main.o hello.o gcc -o hello main.o hello.o install: install ./hello /usr/local/bin uninstall: rm -rf /usr/local/bin/hello
Now we can build our program, install it / uninstall it, and also clean the working directory using standard make targets.
Note that the all target has no commands specified; all she needs is to get the hello prop. Being aware of the recursive nature of make, it is not difficult to guess how this script will work. You should also pay special attention to the fact that if the hello file already exists (left after the previous compilation) and its details have not been changed, then the make command
will not rebuild anything . This is a classic make rake. For example, by changing the header file, not accidentally included in the list of details, you can get long hours of headache. Therefore, in order to guarantee a complete rebuild of the project, you must first clear the working directory:
$ make clean $ make
For install / uninstall purposes, you will need to use sudo.
Variables
All those who are familiar with the DRY (Don't repeat yourself) rule have probably already noticed something wrong, namely, our Makefile contains a large number of duplicate fragments, which can lead to confusion in subsequent attempts to expand or change it. In imperative languages for these purposes we have variables and constants; make also has similar tools. Variables in make are named strings and are defined very simply:
<VAR_NAME> = <value string>
There is an unspoken rule that variables should be named in uppercase, for example:
SRC = main.c hello.c
So we defined the list of source files. To use the value of a variable, it should be renamed using the $ (<VAR_NAME>) construct; like this:
gcc -o hello $(SRC)
Below is a makefile that uses two variables: TARGET - to determine the name of the target program and PREFIX - to determine the path to install the program into the system.
TARGET = hello PREFIX = /usr/local/bin .PHONY: all clean install uninstall all: $(TARGET) clean: rm -rf $(TARGET) *.o main.o: main.c gcc -c -o main.o main.c hello.o: hello.c gcc -c -o hello.o hello.c $(TARGET): main.o hello.o gcc -o $(TARGET) main.o hello.o install: install $(TARGET) $(PREFIX) uninstall: rm -rf $(PREFIX)/$(TARGET)
This is prettier. I think now the above example does not need any special comments for you.
Automatic variables
Automatic variables are designed to simplify makefiles, but in my opinion have a negative impact on their readability. Anyway, I will give here some of the most frequently used variables, and what to do with them (and whether to do at all) you decide:
- $ @ The target name of the rule being processed.
- $ <First dependency name of the rule being processed
- $ ^ List of all dependencies of the rule being processed
If anyone wants to make a complete obfuscation of their scripts, you can take inspiration here:
Automatic variablesConclusion
In this article I tried to explain in detail the basics of writing and the work of makefiles. I hope that it will help you gain an understanding of the essence of make and master this time-tested tool as soon as possible.
All examples on GitHubFor those who have tasted:
Makefile mini HOWTO on OpenNETGNU Make Richard M. Stallman and Roland McGrath, translation © Vladimir Ignatov, 2000Effective use of GNU Make