Make and Makefile: The ultimate tool to manage your projects

What is the difference between a make and makefile?

A Makefile is a specific file format used by the "make" utility program to determine how to compile and link a group of source code files. Sometimes, it is commonly believed that make is used only in software development. In contrast, "make" is a general-purpose utility that can be used to build anything, not just software programs.

What are Makefiles?

Makefiles are files that contain instructions on how to compile and link a program. They are typically used in Unix-like operating systems.

The make program reads a file named "Makefile" and then determines which targets need to be rebuilt. It then invokes the appropriate compiler or linker to do the work. To start using makefiles, all you need to do is create a file named "Makefile" at the root of your project.

Makefiles can be created by hand or generated by a tool such as CMake.

Prerequisites

This article assumes you have a working understanding of the Terminal and how to run commands. It also assumes you understand how and have installed dev tools that contain gcc gdb make etc

Why use Makefiles?

Makefiles provide a way to build a program from source code. They can be used to automate the build process.

Makefiles can also be used to specify dependencies between files. For example, if a source file includes another file, the Makefile can ensure that the included file is up to date before building the source file.

How do I create a Makefile?

There are two ways to create a Makefile. The first is to write one by hand. The second is to use a tool such as CMake.

To write a Makefile by hand, you will need to know the syntax of the makefile language. A simple Makefile might look like this:

all:
    gcc -o myprogram myprogram.c

This Makefile has one target, all, of which is the default target. The all target depends on myprogram.c, which is the source file that will be compiled.

The gcc command is used to compile the source file. The -ooption tells gcc to output the compiled program to a file named myprogram.

To build the program, you would type make at the command line. This would cause gcc to compile myprogram.c and output the compiled program to myprogram.

If you change myprogram.c, you can type make again to rebuild the program. Make will check the timestamp of myprogram.c and, since it is newer than myprogram, will recompile it.

You can also type make all to rebuild all targets in the Makefile.

Makefiles can be much more complex than the one shown above. They can have multiple targets and dependencies. They can also include variables and macros.

To learn more about Makefiles, consult a book on Unix programming or read the documentation for the make program.

How to Write a Makefile

The syntax of a Makefile is very simple. Each rule must start with a target, followed by a colon, followed by a list of dependencies. The dependencies are separated by spaces. Optionally, the rule may also specify a list of commands to be executed to build the target. If a rule does not specify any commands, the make program will use a default set of commands. For most targets, the default commands will suffice. In some cases, however, it may be necessary to specify custom commands.

The commands associated with a rule must be indented with tabs or spaces. The commands will be executed in the order in which they appear in the Makefile. Here is a simple example of a Makefile:

foo: foo.c bar.c    
     gcc -o foo foo.c bar.c

This Makefile contains a single rule. The target is "foo" and the dependencies are "foo.c" and "bar.c". The commands to build "foo" are just the standard gcc commands. Note that the commands in the Makefile are executed in a shell. This means that shell features, such as wildcards and variables, can be used in the commands.

Variables can be used in Makefiles to avoid repetition. For example, the CFLAGSvariable can be used to specify the flags that should be passed to the C compiler. All rules that build C programs can then use the $(CFLAGS) variable instead of repeating the flags in each rule. Here is an example of a Makefile that uses the CFLAGS variable:

CFLAGS = -g -O2
foo: foo.c bar.c
     gcc $(CFLAGS) -o foo foo.c bar.c

In this example, the CFLAGSvariable is set to "-g -O2". This causes the gcc command to be executed with the "-g" and "-O2" flags. The $(CFLAGS) variable can be used in any rule, not just rules that build C programs. It is common to set other variables, such as CC (the C compiler) and LDFLAGS (linker flags), in a Makefile.

Macros

Macros can also be used in Makefiles. Macros are similar to variables, but they are expanded before the Makefile is read. This means that macros can be used to conditionally include or exclude parts of the Makefile.

Here is an example of a Makefile that uses macros:

ifeq ($(DEBUG),1)
     CFLAGS = -g -O0
else
      CFLAGS = -O2
endif
foo: foo.c bar.c
     gcc $(CFLAGS) -o foo foo.c bar.c

In this example, the DEBUG macro is used to control the value of the CFLAGS variable. If DEBUG is set to 1, then CFLAGS will be set to "-g -O0". If DEBUG is not set, or is set to any other value, then CFLAGS will be set to "-O2".

Macros can also be used to create abbreviations for long strings. For example, the following Makefile uses the FOO macro to abbreviate the "foo.c bar.c" string:

FOO = foo.c bar.c
foo: $(FOO)
    gcc -o foo $(FOO)

In this example, the FOO macro is expanded to "foo.c bar.c" when the rule is read. This is equivalent to writing "foo: foo.c bar.c" in the Makefile.

Macros can also be used to conditionally include or exclude parts of the Makefile. Here is an example of a Makefile that uses macros to conditionally include or exclude parts of the Makefile:

ifeq ($(OS),Windows)
    SOURCES = foo.c bar.c
else
    SOURCES = foo.c baz.c
endif
foo: $(SOURCES)
     gcc -o foo $(SOURCES)

In this example, the OSmacro is used to control which files are included in the SOURCES variable. If OS is set to "Windows", then the SOURCES variable will be set to "foo.c bar.c". If OS is set to anything else, then the SOURCES variable will be set to "foo.c baz.c".

The Makefile will then build "foo" using the files in the SOURCES variable.

Building multiple targets

Makefiles can also be used to build multiple targets. Each rule in a Makefile can have multiple targets. The targets must be separated by spaces.

Here is an example of a Makefile that builds two targets:

foo: foo.c bar.c
      gcc -o foo foo.c bar.c
baz: baz.c
      gcc -o baz baz.c

In this example, the first rule builds the "foo" target. The second rule builds the "baz" target.

The commands associated with each target can be different. In the example above, the "foo" target is built with the gcc command, while the "baz" target is built with the cc command.

Makefiles can also be used to build multiple versions of a target. For example, a Makefile can be used to build a debug version and a release version of a program.

Here is an example of a Makefile that builds two versions of a target:

DEBUG = 1
ifeq ($(DEBUG),1)
      CFLAGS = -g
else
      CFLAGS = -O2
endif
foo: foo.c
      gcc $(CFLAGS) -o foo foo.c

In this example, the DEBUG macro is used to control the value of the CFLAGS variable. If DEBUG is set to 1, then the CFLAGS variable will be set to "-g". If DEBUG is set to 0, then the CFLAGS variable will be set to "-O2".

The "foo" target will be built twice, once with the "-g" flag and once with the "-O2" flag.

Building libraries and linking files

Makefiles can also be used to build libraries. A library is a collection of object files that can be linked together to form a larger program.

Here is an example of a Makefile that builds a library:

libfoo.a: foo.o bar.o
        ar rcs libfoo.a foo.o bar.o

In this example, the "libfoo.a" target is a library. The "foo.o" and "bar.o" files are the object files that are included in the library. The "ar" command is used to create the library.

Makefiles can also be used to build programs that use libraries. Here is an example of a Makefile that builds a program that uses a library:

program: program.o libfoo.a
         gcc -o program program.o -L. -lfoo

In this example, the "program" target is a program. The "program.o" file is the object file that is included in the program. The "libfoo.a" file is the library that the program uses. The "-L." flag tells the linker to search the current directory for libraries. The "-lfoo" flag tells the linker to use the "libfoo.a" library.

Using path in makefile

Makefiles can also be used to build programs that use libraries that are not in the default search path. Here is an example of a Makefile that builds a program that uses a library that is not in the default search path:

program: program.o libfoo.a
        gcc -o program program.o -L/path/to/libfoo -lfoo

In this example, the "program" target is a program. The "program.o" file is the object file that is included in the program. The "libfoo.a" file is the library that the program uses. The "-L/path/to/libfoo" flag tells the linker to search the "/path/to/libfoo" directory for libraries. The "-lfoo" flag tells the linker to use the "libfoo.a" library.

Running makefiles with flags

The make program can be invoked with the "-n" flag to print the commands that would be executed without actually executing them. This can be useful for debugging Makefiles. Here is an example of using the "-n" flag:

$ make -n
gcc -o foo foo.c bar.c
gcc -o baz baz.c
ar rcs libfoo.a foo.o bar.o
gcc -o program program.o -L. -lfoo

In this example, the make program is invoked with the "-n" flag. The output shows the commands that would be executed if the "-n" flag was not used.

The make program can also be invoked with the "-t" flag to touch the targets. This causes the targets to be updated with the current timestamp. This can be useful when the dependencies of the targets have changed and the targets need to be rebuilt.

Here is an example of using the "-t" flag:

$ make -t
touch foo
touch baz
touch libfoo.a
touch program

In this example, the make program is invoked with the "-t" flag. The output shows the targets that were updated.

The make program can also be invoked with the "-j" flag to run multiple commands in parallel. This can be useful when the build process is slow.

Here is an example of using the "-j" flag:

$ make -j4
gcc -o foo foo.c bar.c &
gcc -o baz baz.c &
ar rcs libfoo.a foo.o bar.o &
gcc -o program program.o -L. -lfoo &
wait

In this example, the make program is invoked with the "-j4" flag. This causes the four commands to be executed in parallel. The "wait" command is used to wait for all of the commands to finish before exiting.

The make program can also be invoked with the "-w" flag to print the working directory before executing each command. This can be useful for debugging Makefiles.

Here is an example of using the "-w" flag:

$ make -w
/path/to/project/src
gcc -o foo foo.c bar.c
/path/to/project/src
gcc -o baz baz.c
/path/to/project/src
ar rcs libfoo.a foo.o bar.o
/path/to/project/src
gcc -o program program.o -L. -lfoo

In this example, the make program is invoked with the "-w" flag. The output shows the working directory before each command is executed.

The make program can also be invoked with the "-C" flag to change to a different directory before executing the commands. This can be useful when the build process needs to be run in a different directory.

Here is an example of using the "-C" flag:

$ make -C /path/to/project/src
gcc -o foo foo.c bar.c
gcc -o baz baz.c
ar rcs libfoo.a foo.o bar.o
gcc -o program program.o -L. -lfoo

In this example, the make program is invoked with the "-C /path/to/project/src" flag. This causes the make program to change to the "/path/to/project/src" directory before executing the commands.

Using makefiles for Github Workflows

If you're anything like me, you love automating away the tedious parts of your workflow. Makefiles are a great way to do this, and they're especially useful when working with GitHub repositories. I'll show you how to use makefiles to automate your GitHub workflow, from creating new repositories to submitting pull requests.

Creating a New Repository

Let's say you want to create a new repository on GitHub. You could do this manually, of course, but why not automate it? With a makefile, you can create a new repository with a single command.

First, you'll need to create a file called Makefile in the root directory of your project. Then, add the following lines to the file:

repo:
    curl -u 'your-username' https://api.github.com/user/repos -d '{"name":"$(REPO_NAME)"}'

In this makefile, we've defined a repo target. When we run make repo, the curl command will be executed, which will create a new repository on GitHub with the name specified in the "REPO_NAME" variable.

To use this makefile, you'll first need to set the "REPO_NAME" variable to the name of your new repository. You can do this on the command line like this:

REPO_NAME=my-new-repo make repo

You can also add the REPO_NAME variable to your environment so you don't have to specify it every time you run the makefile. To do this, you can add the following line to your .bashrc or .zshrc file:

export REPO_NAME=my-new-repo

Now, you can simply run make repo and the repository will be created.

Submitting a Pull Request

Now that you know how to create a new repository with a makefile, let's look at how to submit a pull request.

First, you'll need to create a new branch for your changes. You can do this with the git checkout command:

git checkout -b my-new-branch

Once you've made your changes and committed them, you can push the branch to GitHub with the git push command:

git push origin my-new-branch

Finally, you can submit a pull request with the following curl command:

curl -u 'your-username' https://api.github.com/repos/your-username/your-repo/pulls -d '{"title":"My pull request","head":"my-new-branch","base":"master"}'

As you can see, there's a lot of boilerplate involved in submitting a pull request. With a makefile, we can automate this process.

Add the following lines to your Makefile :

pull-request:
    git checkout -b my-new-branch
    git push origin my-new-branch
    curl -u 'your-username' https://api.github.com/repos/your-username/your-repo/pulls -d '{"title":"My pull request","head":"my-new-branch","base":"master"}'

Now, you can submit a pull request with a single command:

make pull-request

This makefile will create a new branch, push it to GitHub, and submit a pull request. All you have to do is wait for your code to be reviewed and merged!

Conclusion

Makefiles are a powerful tool for automating your workflow. In this article, we've looked at the basic commands and how to use makefiles to automate two common GitHub tasks: creating new repositories and submitting pull requests. With makefiles, you can save time and streamline your workflow. However, there are several other uses of makefiles. I hope to cover them in future articles in this series.