So far in this series we’ve written super simple programs, and we’ve compiled those programs directly using cc
, gcc
, or clang
(some of which are just legacy aliases that point at the same system compiler)
$ cc hello-world.c
$ gcc hello-world.c
$ clang hello-world.c
In the real world it’s unommon that a C project will be written or distributed as a single file. SQLite is a rare exception. Most C projects will require you to become familiar with their build systems if you want to compile the project yourself.
If you’re coming from a programming language like Java or PHP you may be familiar with build systems like ant
or phing
. While there’s far from one universal build tool for C programs, chances are your project will use the grandparent of all build tools — Make. The PHP source is no exception.
This is the first of two articles that will cover the basics of Makefile
s. If you want to learn more, the GNU Make documentation is surprisingly cogent.
Make Makes Files
You can test that the make
program is installed on your computer in the usual ways
$ which make
/usr/bin/make
$ make --version
GNU Make 3.81
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
This program built for i386-apple-darwin11.3.0
The first thing to know about Make is it’s a file oriented tool, not a task oriented tool. Not clear on what, exactly, that means? Let’s start with a quick demonstration. Create the following two line file in a folder on your computer
# File: Makefile
hello-world.txt:
touch hello-world.txt
This is the world’s simplest Makefile
. The first line (hello-world.txt:
) defines a file-to-create, known as a target. The second line (touch hello-world.txt
) is the shell command we want to run in order to create the target file
IMPORTANT: The second line of this file must be indented using tabs. Make is strict in this way — it will not recognize plain spaces as block nesting.
With the above file created, run the following command while cd
ed into the same folder as your Makefile
$ make hello-world.txt
touch hello-world.txt
Make will run, and then output the commands it ran for you. If you take a look at your directory contents, you should see a hello-world.txt
file.
$ ls -1
Makefile
hello-world.txt
Now — try running the same command again
$ make hello-world.txt
make: `hello-world.txt' is up to date.
Instead of blindly re-running our touch
command, make
saw that the file already existed, and skipped actually doing the work of creating it.
This is what we mean when we say make is a file based tool. This may seem a little foreign, but (as we’ll see) it does have its advantages.
Hello Make
Let’s create a make file that actually builds a C program. First, we’ll create the ubiquitous hello world
//File: hello-world.c
#include <stdio.h>
int main()
{
printf("Hello, World!\n");
return 0;
}
Then, we’ll create a new Makefile
in the same directory
#File: Makefile
hello-world:
cc hello-world.c -o hello-world
Our target name is hello-world
, and the command to create our hello-world
file is cc hello-world.c -o hello-world
. If we run make
, we’ll have a compiled binary of our program.
$ make hello-world
cc hello-world.c -o hello-world
$ ./hello-world
Hello, World!
Also, as we saw before, if we re-run our command, make
knows that the hello-world
binary is already built
$ make hello-world
make: `hello-world' is up to date.
This presents a small problem — let’s try editing our hello world to say Hello Make.
#include <stdio.h>
int main()
{
printf("Hello, Make!\n");
return 0;
}
and then, we’ll run our Make command again.
$ make hello-world
make: `hello-world' is up to date.
Huh — that’s no good. We’ve edited our program file, we want to rebuild it, but make
doesn’t think it needs to recompile the source because the hello-world
binary is already there.
We have two solutions. The first is a bit of a sledge hammer: We could just delete the compiled hello-world
binary file. While this wouldn’t be a big deal for this simple hello world example, for a Makefile
with dozens, or possibly hundreds, of targets, it’s not a solution that scales.
Fortunately, make
offers us a solution. For each target, we can create a list of files that the target itself depends on. Give the following a try
#File: Makefile
hello-world: hello-world.c
cc hello-world.c -o hello-world
By listing a file after the hello-world
target, we’re telling make that the hello-world
target depends on hello-world.c
being present. This, in turn, tells make
Hey, if
hello-world.c
(the thing the current target is dependent on) has an edited date later than our target file (hello-world
) then we need to rebuild.
Give it a try!
$ make hello-world
cc hello-world.c -o hello-world
$ ./hello-world
Hello, Make!
$ make
make: `hello-world' is up to date.
As you can see, make
realizes it needed to rebuild the hello-world.c
program.
Target dependencies also serve another purpose. Let’s try deleting our hello-world.c
file.
$ rm hello-world.c
and then re-running make.
$ make hello-world
make: *** No rule to make target `hello-world.c',
needed by `hello-world'. Stop.
Our build failed because there was no file to compile. However — the error message is interesting. Instead of a compiler error
$ cc hello-world.c -o hello-world
clang: error: no such file or directory: 'hello-world.c'
clang: error: no input files
We have an error from Make itself.
make: *** No rule to make target `hello-world.c',
For every target dependency, Make will check if the file exists. If the file doesn’t exist, Make will look for another target in order to create the file.
This means we could do something like this.
#File: Makefile
hello-world: hello-world.c
cc hello-world.c -o hello-world
hello-world.c:
printf "#include <stdio.h>\n" > hello-world.c
printf "int main()\n" >> hello-world.c
printf "{\n" >> hello-world.c
printf " printf(\"Hello, Make!\");\n" >> hello-world.c
printf " return 0;\n" >> hello-world.c
printf "}\n" >> hello-world.c
Here, we’ve added a new target to our Makefile
named hello-world.c
, and the multiline target creates our hello world program for us. This actually gives us a Makefile
with a self-contained hello world program
$ make hello-world
printf "#include <stdio.h>\n" > hello-world.c
printf "int main()\n" >> hello-world.c
printf "{\n" >> hello-world.c
printf " printf(\"Hello, Make! \\\\n\");\n" >> hello-world.c
printf " return 0;\n" >> hello-world.c
printf "}\n" >> hello-world.c
cc hello-world.c -o hello-world
$ ./hello-world
Hello Make
Like most tutorial examples, this last one is a bit silly — but it’s not that uncommon for some c files in a project to be pre-generated. Just a reminder for the pre-generated javascript crowd that all this has happened before, and all this will happen again.
Wrap Up
We’re going to wrap things up here for today. If you’re used to thinking about build systems as tools that help you organize the scripts that perform your builds, Make’s file/artifact based approach can take a little getting used to. Regardless of what you think about it, Make and C grew up together, and the things that Make makes possible have ended up becoming de-facto standards for how C programs are built.
There’s still a few Make basics to cover, but before we can do that, we’ll need to talk about multi-file C programs. That’s coming next time!