# A quick introduction to GNU Make
(Make Series pt.1)

*When GNU Make was first introduced to the world in* [*1976*](https://en.wikipedia.org/wiki/Make_(software))*, it was named as such because it would* ***Make*** *you hate build systems. Today, Make remains a powerful yet hard-to-use build tool. In this first article in a series on GNU Make, we will dive into the basics and attempt to demystify this jackhammer of a tool.*

---

## The basics

Make is a [build tool](https://stackoverflow.com/questions/7249871/what-is-a-build-tool), that is to say, a program that is used to automate the creation of executables based on source code.

When you build a c application using `gcc`

```bash
$ gcc main.c -o main
```

you are *manually compiling* your source code. While this is fine for small projects, it gets tedious for anything beyond a simple `hello world` application.

A **Makefile** is a file containing a set of **rules** that specify how to build your project. This makes it simpler to compile large projects on the fly.

```bash
# uses the makefile in the current directory to compile your project
$ make all
```

---

## Pitfalls of manual compilation

Manual compiling isn't just tedious: it's slow. When you recompile a project using `gcc`, you recompile *all* source files. But what if you didn't modify each file? Imagine you have the following project structure:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1680353401193/1aef4694-10b3-4efc-996b-b116911c2480.png align="center")

Calling `gcc` will recompile *all* files individually

```bash
# a.c is recompiled even if it hasn't changed!
$ gcc src/a.c src/b.c -Iinclude -o main
```

On large projects with hundreds, if not thousands of source files, this can mean the difference between waiting *hours* for your project to build, or just *minutes*. So how do we fix this? Well, hold on a second! First, we need to learn a bit more about Make.

---

## Make syntax

Here is a simple Makefile that we can use to generate an executable for our toy project:

```makefile
all: src/a.o src/b.o
    gcc src/a.o src/b.o -Iinclude -o main

src/a.o: src/a.c
    gcc -c src/a.c -o src/a.o -Iinclude

src/b.o: src/b.c
    gcc -c src/b.c -o src/b.o -Iinclude
```

A Makefile is made up of **targets**, **prerequisites,** and **recipes**.

* **target:** these are the files we want to generate. They are the keywords to the *left* of the ':' symbol.
    
* **prerequisites:** these are the dependencies needed to generate a file. They are the keywords to the *right* of the ':' symbol.
    
* **recipe:** these are the commands used to generate a file. They are the lines *under* the target and the prerequisites.
    

Together, a **target, prerequisite** and **recipe** form a **rule**.

```makefile
target: prerequisite1 prerequisite2 ...
    recipe 🍰 
    ...
```

for example:

```makefile
all: a.c b.c
    gcc src/a.o src/b.o -Iinclude -o main
```

* `all` is a **target**
    
* `src/a.o` and `src/b.o` are **prerequisites**
    
* `gcc src/a.o src/b.o -Iinclude -o main` is a **recipe**
    

> **gcc options:**
> 
> * **\-I:** the directory in which header files are located
>     
> * **\-o:** the name of the output binary
>     
> * **\-c:** compilation will be done without linking (creates an object file instead of an executable)
>     

---

## Make execution

When Make is executing a Makefile, it will recursively try and resolve any **prerequisites** a rule has. What does this mean?

Let's break this down, one rule at a time:

```makefile
all: src/a.o src/b.o
    # this won't be executed until src/a.o and src/b.o exist
    gcc src/a.o src/b.o -Iinclude -o main
```

> Make will always execute the first **target** in a Makefile. By convention, the first **target** should be called `all.`

`src/a.o` is a dependency for `all`. However, `src/a.o` does not exist already, so Make will try and generate it. For this, it will have to find a rule that specifies how to generate `src/a.o`

```makefile
src/a.o: src/a.c
    gcc -c src/a.c -o src/a.o -Iinclude
```

Ah, we found it! Make will use this rule to generate `src/a.o`. This rule depends on `src/a.c`, which already exists, so Make will not try and generate it.

> Note that if Make cannot find `a.c` in the current working directory, the build will fail.

Now that Make has generated `src/a.o`, it will do the same for `src/b.o`. Once all prerequisites for `all` have been met (that is to say, `src/a.o` and `src/b.o` now exist), Make will finally execute the **recipe** associated with `all`.

```makefile
all: src/a.o src/b.o
    # Now that src/a.o and src/b.o exist, this can be executed
    # and the main binary compiled
    gcc src/a.o src/b.o -Iinclude -o main
```

Let's run this code and see what happens!

```bash
$ make # runs the code in our Makefile
gcc -c src/a.c -o src/a.o -Iinclude # builds src/a.o
gcc -c src/b.c -o src/b.o -Iinclude # builds src/b.o
gcc src/a.o src/b.o -Iinclude -o main # builds the main binary
```

Hurray! But what is so special about this? Couldn't I just have written these commands myself? Let's run the build again to see Make's greatest advantage.

```bash
$ make
gcc src/a.o src/b.o -Iinclude -o main
```

Well... As you can see Make only recompiles `main`, but why is it doing this? Let's break down exactly what is happening and why this common mistake isn't quite the behavior we want.

---

## Dependency tracking

Make is a *dependency-tracking* build tool. This means that Make follows a set of rules to *minimize the number of recompilations* required to build your project. For example:

* If a **target** does not exist, Make will try and build it.
    
* If a target's **prerequisites** have been modified, Make will execute that target's **recipe**.
    

These rules are applied **recursively**. So, for example, if `src/a.c` is modified then `src/a.o` will be rebuilt, which will trigger the **task** `all` to have its recipe executed.

![dependency tree](https://cdn.hashnode.com/res/hashnode/image/upload/v1680525975603/aacba41d-a5c8-4056-9a59-48ea616c9131.png align="center")

*Dependency tree for project Makefile. If you start from the right, you can see which files will have to be recompiled upon change.*

Notice how I used the term **task** and not **executable**? That is because, by default, Make does not have any way to know what files you are generating.

> Make is only aware of the **tasks** and **dependencies** specified to it. If you want to minimize recompilation, make sure these correspond to the files you are generating so Make can keep track of them!

Remember our previous Makefile? In particular, let us focus on the first rule:

```makefile
all: src/a.o src/b.o
    gcc src/a.o src/b.o -Iinclude -o main
```

* **task** *all* (make will try and build this)
    
* **dependencies** *src/a.o src/b.o* (make will look for these before building the task)
    

By specifying `all` as a task, we are telling Make that this file should be generated. When we call Make again, it will look for `all`. Since `all` does not exist, it will try and generate it, by calling its recipe, *triggering recompilation even if no file has been modified*.

Here is an updated version of the Makefile which avoids this pitfall.

```makefile
all: main

main: src/a.o src/b.o
	gcc src/a.o src/b.o -Iinclude -o main

src/a.o: src/a.c
	gcc -c src/a.c -o src/a.o -Iinclude

src/b.o: src/b.c
	gcc -c src/b.c -o src/b.o -Iinclude
```

Notice that `all` does not have any **recipe** associated with it! This way, when Make runs again, it will check if any dependencies for `all` have changed, but it will not run any **recipe** or recompile any files unnecessarily.

```plaintext
$ make
make: Nothing to be done for 'all'.
```

Just to be sure, let's change the first rule to:

```makefile
all: main
    echo "If a rule is present in all, it will always run 😱"
```

If we run Make again, the output is:

```plaintext
$ make
echo "If a rule is present in all, it will always run 😱"
If a rule is present in all, it will always run 😱
```

---

## The power of Make

Perhaps now you can begin to see the superpower which Make provides: by automatically checking for changes in files, Make allows us to keep our recompilation fast and to a minimum. But that isn't all!

We've only just scratched the surface of what Make allows us to do. Right now, our makefile is still very inflexible, and we have to write a rule for every file we want to compile.

Check out our [**next episode**](https://born2code.hashnode.dev/variables-in-makefiles) to find out how to use **variables** and **pattern matching** to make your code more reusable and easier to maintain!
