Reading time: 30 minutes | Coding time: 5 minutes
In this article, we have explained the concepts behind Make tool and Makefile which forms the basics of compiling and building executables for a C/ C++ project codebase. We have taken a look at any example as well to clear the concepts.
An automation tool predominantly used to compile and construct the binary executable files from a complex source code involving multiple imports and libraries.
- The “Make” command uses this “Makefile” to know the steps to build the binaries from the source code.
- By default, the “Make” command searches for a file named “Makefile” if the “Makefile” is not explicitly specified. At most places, the “Makefile” will be given the same name.
Why use make?
- Not just a tool to build large binaries.
- Essential in compiling the code written in “C” or “C++” language.
- A built-in tool natively available in all the distributions of ‘Linux’ and ‘Unix’ Operating Systems except the ones that need to be small in size (Like alpine linux, etc.) as much as possible.
- It is a multi-purpose tool and not specific to any languages. It can be used to compile java files, run a couple of shell commands, etc.
It can be said that the “Makefile” is a machine-readable documentation that captures the desired workflow in the form of certain syntaxes. Since building a binary involves a combination of multiple processes, they are recorded in the form of “Makefiles” and can be repeatedly very easily.
Though they are generally used in compiling C and C++ code to executable, they can also be used to automate some regular system administration tasks. Because the compiler needs all the dependencies in their arguments and it becomes a tedious and error-prone task. A “Makefile” can orchestrate and work in accordance with the system commands.
This makes “Make” a powerful tool to do more generic activities and so it can be used to compile and build any code with their respective compilers. Also, tasks such as archiving logs, cleaning temporary files, renaming files, or anything of this kind can be done using the Makefiles.
Makefile v/s BashScript
- Functionality that the “Make” offers is far superior to a regular Bash script.
- It can find out and map the dependencies using topological sorting with the given “Makefile” whereas Bash script lacks this feature.
- Bash script follows a procedural paradigm, “Makefile” follows declarative paradigm.
- “Make” can even decide if we need to rebuild the file.
Comparing “Bash” and “Make”, both of them are not full-fledged general purpose programming languages. Yet both of them have their own advantages in some areas.
Bash is a procedural way of running the code in which the bash interpreter reads and executes the code line by line."Make"is a declarative way of running the code in which the code (i.e. Makefile) is read as a whole and the parts (i.e. rules/statements) are topologically sorted with the dependency graph and executed.
Unlike conventional code which needs the logic to be fed explicitly, “Make” can come up with its own decision only with the given dependencies.
"Make" checks if a work is done and if it is done, it doesn’t run the code responsible for it. The bash script doesn’t have this intelligence. Though it is possible to use multiple IF conditions and chaining them, it is not as convenient as “Makefile” in analyzing and generating the dependency map.
Minimal rebuilds are possible in “Makefile” whereas the bash script requires a lot of time and effort to achieve the same.
Features of Make
- Topological ordering of the generated dependency graph.
- Recompiling only if the source files are changed.`
- Executes system commands as its action that can even be a script.
- Available as native tool. Hence no additional installation is required.
- Supports Macros and macros aren’t much different from bash variables.
- Supports Suffix rules and Pattern rules.
- Phony targets are allowed.
- Simple and elegant syntax.
"Make" has a unique way of executing code. It generates the dependency graph and sorts it before execution. It doesn’t unnecessarily rebuild the code all the time. Instead, if the file to be built already exists and its timestamp is not newer than the make’s last build, then skips the execution of the rules for that particular target. The recipe of the rules (i.e. actions) is nothing but the plain system commands. So any system commands or even custom scripts of an interpreter can be executed. Since most of the files are dependent on "Make" for building, it is natively available in most of the distributions of Linux Operating Systems."Make" has several special built-in directives to handle several edge cases in the process. Targets that are not files can be just a set of actions which can be used for housekeeping activities.
- Specify a target, which is commonly a file generated by building the source files.
- Each target should be specified with its dependencies so that the “make” command can generate the topological order of the dependency tree.
- Provide the set of commands to be executed that generates the target files.
- If a target A is used as a dependency in more than one target (say target B, target C, etc.,), then the commands in target A will be executed only once unless it is a phony target.
"Make" follows very simple steps. These steps are explained below.
- Get the list of declarations i.e. the rules specified in the makefile.
Loop through the rules and acquire the targets, dependencies and the recipes to execute.
- Check whether the target exists and are valid. The generated target file can be pointed as a dependency to some other targets. If not, check whether the dependencies exist.
- If dependencies don’t exist or are invalid, run the dependency rules.
- If dependencies are valid but the target is invalid, "Make" executes the recipe of the rule and the rule forms the target file.
- In next immediate rebuild, the target will be valid and therefore it is straightaway skipped. The rebuild process will not run the target commands unless the time stamp of source files or other dependencies get changed. Housekeeping targets can be added in Makefiles and can be called by passing the target name as the first argument.
A rule can be defined as a block with a defined structure that forms the “Makefile”. Each rule should be separated by an empty line from other rules. Rules are also referred to as statements in few communities of “Make” though the GNU and BSD stick to the name “Rule” to refer to them. Most often, the rules will not have more than one target. Each rule lists other files as the prerequisites or dependencies of the target. The order of the rules is not significant, but the order formed by resolving the dependencies is used on building the source. The target names are generally the name of the files and the name of the target accepts wildcard patterns just like Bash in order to have multiple files as targets.
Structure of Makefile
- Target : A target is usually the name of a file, which should be there. If it is not there or does have a newer timestamp, it is considered invalid and some steps are taken to recreate the file.
- Dependency : A dependency is the name of a target. Say we have two rules A and B If target A is having the target B as the dependency, the target B is evaluated first before entering the process of creating the target A.
- Recipe : Recipe is the steps performed in order to create the specified target name. The actions i.e. the commands to be executed should be prefixed by a “tab” character and should be placed on the next line of the target name.
target: dependency (list) recipe (command)
mycalculator: main.c mymath.h add.c subtract.c multiply.c divide.c gcc -o mycalculator main.c mymath.h add.c subtract.c multiply.c divide.c
Following makefile will create a compiled output of the name mycalculator will all the dependencies combined in that single file.
mycalculator is the output executable that has dependencies on main.c, mymath.h, add.c, subtract.c, multiply.c and divide.c.
gcc -o mycalculator main.c mymath.h add.c subtract.c multiply.c divide.c
The above command in the actual command to compile all the files and create the executable from main.c.
For the output refer to :
With this, you have the complete knowledge of makefile. Enjoy.