Linux Fu: The Great Power Of Make

Over the years, Linux (well, the operating system that is commonly known as Linux which is the Linux kernel and the GNU tools) has become much more complicated than its Unix roots. That’s inevitable, of course. However, it means old-timers get to slowly grow into new features while new people have to learn all in one gulp. A good example of this is how software is typically built on a Linux system. Fundamentally, most projects use make — a program that tries to be smart about running compiles. This was especially important when your 100 MHz CPU connected to a very slow disk drive would take a day to build a significant piece of software. On the face of it, make is pretty simple. But today, looking at a typical makefile will give you a headache, and many projects use an abstraction over make that further obscures things.

In this article, I want to show you how simple a makefile can be. If you can build a simple makefile, you’ll find they have more uses than you might think. I’m going to focus on C, but only because that’s sort of the least common denominator. A make file can build just about anything from a shell prompt.


You may scoff and say that you don’t need a makefile. Your IDE builds your software for you. That may be true, but a lot of IDEs actually use makefiles under the covers. Even IDEs that do not often have a way to export a makefile. This allows you to more easily write build scripts or integrate with continuous release tools which usually expect a makefile.

One exception to this is Java. Java has its own set of build tools that people use and so make isn’t as common. However, it is certainly possible to use it along with command line tools like javac or gcj.

The Simplest Makefile

Here’s a simple makefile:

hello: hello.c

That’s it. Really. This is a rule that says there is a file called hello and it depends on the file hello.c. If hello.c is newer than hello, then build hello. Wait, what? How does it know what to do? Usually, you’ll add instructions about how to build a target (hello) from its dependencies. However, there are default rules.

If you create a file called Makefile along with hello.c in a directory and then issue the make command in that directory you’ll see the command run will be:

cc hello.c -o hello

That’s perfectly fine since most distributions will have cc pointing to the default C compiler. If you run make again, you’ll see it doesn’t build anything. Instead, it will report something like this:

make: 'hello' is up to date.

To get it to build again, you’ll need to make a change to hello.c, use the touch command on hello.c, or delete hello. By the way, the -n command line option will tell make to tell you what it will do without actually doing it. That’s handy when you are trying to figure out what a particular makefile is doing.

If you like, you can customize the defaults using variables. The variables can come from the command line, the environment, or be set in the makefile itself. For example:

hello : hello.c

Now you’ll get a command like:

gcc -g hello.c -o hello

In a complex makefile you might want to add options to ones you already have and you can do that too (the number sign, #, is a comment):

# Comment next line to turn off optimization
hello : hello.c

In fact, the implicit rule being used is:

$(CC) $(CFLAGS) $(CPPFLAGS) hello.c -o hello

Note the variables always use the $() syntax by convention. In reality, if you have a single character variable name you could omit it (e.g., $X) but that may not be portable, so the best thing to do is always use the parenthesis.

Custom Rules

Maybe you don’t want to use the default rule. That’s no problem. The make program is a little bit of stickler for file format, though. If you start subsequent lines with a tab character (not just a few spaces; a real tab) then that line (along with others) will run like a script instead of the default rules. So while this isn’t very elegant, here’s a perfectly fine way to write our makefile:

hello : hello.c
    gcc -g -O hello.c

In fact, you ought to use variables to make your rules more flexible just like the default ones do. You can also use wildcards. Consider this:

% : %.c
    $(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@

This is more or less the same as the default rule. The percent signs match anything and the $< gets the name of the first (and in this case, only) prerequisite which is hello.c. The $@ variable gives you the name of the target (hello, for this example). There are many more variables available, but these will get you started.

You can have multiple script lines (processed by the default system shell, although you can change that) as long as they all start with a tab. You can also add more prerequisites. For example:

hello : hello.c hello.h mylocallib.h

This gets tedious to maintain, though. For C and C++, most compilers (including gcc) have a way to create .d files that can automatically tell make what files an object depends on. That’s beyond the scope of this post, but look up the -MMD option for gcc, if you want to know more.

I Object

Normally, in a significant project, you won’t just compile C files (or whatever you have). You’ll compile the source files to object files and then link the object files at the end. The default rules understand this and, of course, you can write your own:

hello : hello.o mylib.o
hello.o : hello.c hello.h mylib.h
mylib.o : mylib.c mylib.h

Thanks to the default rules, that’s all you need. The make program is smart enough to see that it needs hello.o, it will go find the rule for hello.o and so on. Of course, you can add one or more script lines after any of these if you want to control what happens for the build process.

By default, make only tries to build the first thing it finds. Sometimes you’ll see a fake target as the first thing like this:

all : hello libtest config-editor

Presumably, then, you’ll have rules for those three programs elsewhere and make will build all three. This works as long as you never have a file name “all” in the working directory. To prevent that being a problem, you can declare that target as phony by using this statement in the makefile:

.PHONY: all

You can attach actions to phony targets, too. For example, you’ll often see something like:

.PHONY: clean
     rm *.o

Then you can issue make clean from the command line to remove all the object files. You probably won’t make that the first target or add it to something like all. Another common thing to do is create a phony target that can burn your code to a microcontroller so you can issue a command like “make program” to download code to the chip.

Stupid Make Tricks

Usually, you use make to build software. But like any other Unix/Linux tool, you’ll find people using it for all sorts of things. For example, it would be easy to build a makefile that uses pandoc to convert a document to multiple formats any time it changes.

The key is to realize that make is going to build a dependency tree, figure out what pieces need attention, and run the scripts for those pieces. The scripts don’t have to be build steps. They can copy files (even remotely using something like scp), delete files, or do anything else a Linux shell script could do.

But Wait, There’s More

There’s a lot more to learn about make, but with what you already know you can do a surprising amount. You can read the manual, of course, to learn more. If you look at a significant makefile like the one for Arduino projects, you’ll see there’s a lot there we didn’t talk about. But you still should be able to pick through a lot of it.

Next time, I’ll talk a little about schemes that take a different input format and generate a makefile. There are a few reasons people want to do this. One is the abstraction software might determine dependencies or make other changes to the generated makefile. In addition, most of these tools can generate build scripts for different platforms. As an example, if you’ve seenautomakeorcmake, these are the kinds of tools I’m talking about.

However, you don’t need anything that fancy for a lot of small projects like we often do on a Raspberry Pi or other embedded system. Basic make functionality will take you a long way. Especially for a project targeting a single kind of computer with a small number of files and dependencies.

76 thoughts on “Linux Fu: The Great Power Of Make

    1. Eh. I’ve used CMake and for some things it is fine. There are also lots of other build systems out there I’ve used (and I even wrote one for Space Station Freedom). But the problem with CMake is it can be overly complex. Case in point. Last week I needed a piece of software that wasn’t in the Ubuntu repos. So I download the source and go to build it. Well, CMake was the wrong version so that stops it dead. I go to find the newest version and you can’t get there without downloading it and rebuilding it. But that breaks CMake-Curses-GUI or something like that. I don’t use that, so I just nuked it and let the new version of CMake install and then built the stuff I wanted.

      I don’t have that problem with Make and simple Makefiles. I’m sure there is some cutting edge feature of Make version 99.253 that would break old versions of Make but the kind of things I’m talking about here are dead simple and will work on versions of Make going back beyond this century.

      My intent here was to say “Hey, you know if you are walking to the store, you could be riding a bike which is easier and will let you carry more.” And you are saying, “Why not drive this tank that requires a fuel depot and a support crew?” If you need it, great. But if you can make do with the bike, you are going to have it easier.

      I hate to sound like an old guy, but to quote Mr. Scott: The more they overthink the plumbing, the easier it is to stop up the drain.

      Make is simple and for simple things is works.

      1. I have to agree on this one. Simple make systems are the best, there are CMake, QMake, and what not, but even without all the bells and whistles, make still does the job just as good. Some things are the best when they’re simple, if you’ve ever dealt with Android development, you might have seen what a monstrosity Gradle is.

        1. Unless, you are actually developing on a project. Then Make is a pain the ass.

          Having C files depend on their header? So they rebuild if a header changes. Requires all kinds of tricks, with flaws. Rename a header file, and your build system comes crashing down and takes your house with it.

          Note that I think autoconf is worse, it’s taking the worste of Make and the worst of shell scripting combined.

          CMake is ok, but you can still make a horror show with it. However, the basic standard things don’t require ugly hacks and works then cross platform reasonably well. It generally handles cross-compiling better then other build systems.

          (Gradle is stupid IMHO, it’s another layer on top of already broken layers, it just adds more things to break, without means to debug it)

      2. CMake is far from perfect, one thing is their script language which is weird and not very complete.
        But you can use it very simply even in cross-compiling setup and create eclipse project or frontend for make or ninja.
        SCons is also a contender, having python on hand, but I have very limited experience with it.

  1. Nowadays I would not recommend anyone to use Makefiles if avoidable. The need of tabs vs. spaces is already ridiculous enough (and yes I do hate Python for sematics through indentiation). Use CMake, type less, require and find libraries, pull their header/libs in and build on different platforms. (Caveat: Yes you need to have CMake installed).

        1. No, I’m sorry but I’m going to disagree. If I have, say, 4 C files that relate in some way and I don’t want to build them all every build, then a shell script is not easier. Can you do it? Sure, of course. But it just isn’t easier. Yet, I don’t need CMake or any of the others just to do a basic dependency build on 4 files.

          Sure if your shell script is:

          gcc -o foo foo.c

          Yes (i usually name this go so that ./go builds) but doing a test to see which files are out of date and doing a minimal build is trivial in make and not that simple in shell.

          1. Oh come on! If you have a simple project, there is not much time saved omitting a few source files from rebuild. You’ll probably do “make rebuild all” each time, for guaranteed repeatability.

          2. Since I can’t reply directly to salec (too deep in the reply chain). No, you come on. I probably don’t brace my code like you do either. We don’t all follow your workflow (or mine either and that’s fine). But yeah for more than a few files, I’m going to use make. If you know how to use it, it takes about 10 seconds to put the file together. And then you have a consistent way to build stuff on all your projects. Guess what? I’ll probably use git, too, even though it is just a simple project. And, no, I hate rebuilding all because if I have objects that work, I really don’t want them to change unless I know they need changing.

            I have to say, in general, I’m surprised at how much vitriol make is generating. Sure there are some great tools if you need them. But if someone is editing a 5 line file with nano, I’m hardly going to tell them they should use Eclipse. Or even emacs (the One True Editor; sorry Elliot). Or even VIM. Those tools all have their place, too. This wasn’t a rant about using make to build your 50,000 source file multilanguage build.

          3. Any non-trivial project is going to have generated intermediate files, such as fonts, images, icons, etc. encoded into C constants. Any non-trivial project is going to have multiple targets. There is no way you are going to rebuild all of these things every time, and there is no way you are going to be successful with manually determining the dependencies. That shell script is a loaded gun pointed right at your foot. For goodness sake, take 10 minutes to write a makefile, you will save hours of frustration.

          4. And see, that shell script is *already fucked*, because on FreeBSD I can’t run it. My C compiler is not named ‘gcc’. It’s ‘cc’ and is based on clang.


            THAT’S WHAT IT IS FOR

  2. You used to be able to type:
    $ make love
    Make: Don’t know how to make love. Stop.

    That does’t work in more recent versions (e.g. GNU Make 4.1 that’s installed on my FrankenMac) though:
    $ make love
    make: *** No rule to make target ‘love’. Stop.

    1. This reminds me of how disappointed I was, the first time I typed “cat food” in Linux. It just said something lame. In REAL Unix, the response was, “cat: can’t open food”.

      1. OpenBSD 6.3 (GENERIC) #3: Thu May 17 23:54:13 CEST 2018

        Welcome to OpenBSD: The proactively secure Unix-like operating system.

        Please use the sendbug(1) utility to report bugs in the system.
        Before reporting a bug, please try to reproduce it with the latest
        version of the code. With bug reports, please try to ensure that
        enough information to reproduce the problem is enclosed, and if a
        known fix for it exists, include that as well.

        sjl-router$ cat food
        cat: food: No such file or directory

  3. Makefiles are really nice and elegant and all, but the fact that C doesn’t have it’s own build system and dependency management or module system like python does is possibly my least favorite part of the language.

      1. And the same goes for C: you can find lots of different IDEs that were made for C/C++. But none of them are compatible with each other. Make works everywhere. It even appears to be (mostly) compatible with Microsoft’s nmake.

    1. It’s a darned good thing that C does not have its own build system, because building an executable program requires a lot more than compiling C files. In many cases you must generate those C files before you can compile them. In some cases you will also need to compile files that are not C code, such as assembler or Ada or even Fortran (yes people still use Fortran). You are much better served with a language-agnostic build system (for instance, a makefile).

      1. Don’t let them get big and complex. Break them up into smaller pieces. “make -f mymakefile” is your friend. Use common files for common code. Do not feed the complexity beast.

  4. I’ve been wondering this about Linux or the Linux community for a long time.

    So.. the point of a build system such as make is that it is smart about knowing which files need to be re-compiled/re-linked after a change and which ones do not right? I mean if you want to re-compile every source file and re-link every object file each and every time you do a build you might as well just write a simple script right? This is what I was taught about make in college anyway.

    So why is it that whenever I read directions for building things for example… directions for building the Linux kernel… the authors always like to instruct the user to run “make clean” each time. Doesn’t that completely break the point of having make in the first place? Why do I want to wait for a bunch of files to build that have neither changed nor have their dependencies changed since the last build? What is the point of that?

    1. Yes. For the kernel, it’s no longer needed.

      But it used to be, for Make is a simple tool, and the setup of the kernel didn’t track everything that you could change in configuration. So if configuration was changed, it was better to force rebuild everything, then end up with a broken kernel because some part of the dependency tracking failed.

      Header files, defines, compiler configuration, it all effects the result, and thus things need to be rebuild if those change. And Make, without specific configuration for those changes, doesn’t know and thus doesn’t rebuild.

    2. This was exactly what I thought, the first time I tried to configure a kernel – “why is it rebuilding EVERYTHING?” What it really comes down to is, if the complexity of a project exceeds the abilities of the creator of the Makefile, then it’s really only useful as a place to keep track of all of the command line switches needed to build each module. It’s still useful for that, though. Arguably no better than a shell script, but I’d rather type “make” than go looking for the build script file.
      It’s more common these days, though, for FOSS projects to require “./configure; make”, where “configure” is a script generated by some other tool that generates a Makefile specific to the machine it’s running on and what options are to be compiled. Sometimes this works, sometimes you have to go digging to find a directory containing files like “makefile.linux”. Again, only slightly better than shell scripts. These almost always require not just “make clean”, but maybe “make distclean”, or, “oh, to hell with it”, and re-unzipping the source tarball to rebuild with a different configuration.

  5. To all those objecting that Make is old and things, a little anecdote: around twelve years ago, I wrote a little program for a customer. Gtk GUI (it was Gtk2 back then), SQL database, some config files — those things. 4K lines of C according to sloccount, plus a bit of SQL here, you get the idea.

    The thing runs on Linux, originally for an i386.

    Back then, I bit the bullet and did the build in configury (automake, autoconf, etc., and yes, pkg-config for the dependencies).

    A couple of weeks ago, the customer asked me whether I could port it to a Raspberry Pi. “Sure” I said (with some trepidation on my part).

    Know what? The thing cross-compiled out of the box. On my AMD64.

    This Autoconf stuff (which is built around Make) knows portability. I’m not sure CMake or QMake or something would have been so easy.

    1. I think CMake actually does this pretty good, too. And at the end, CMake also uses make. I’m not trying to blast CMake here, but I think if you are a guy with a handful of C files on a Raspberry Pi, me asking you to think about using Make is an easier sell than trying to convince you to spend 3 hours futzing with CMake to get it installed and all the version issues ironed out. If you need the complexity, sure… (I do like autoconf), CMake, etc. Great.

    1. That’s a good point. I know when I was doing a lot of AVR stuff pre-Arduino, I would always start with some guy’s make file (can’t remember who now) that had targets like “flash” “debug” etc. It was set up so you just had to set a few variables at the head of the fiel and that was usually it.

  6. Make, CMake are all as old as ASSembler. Custom build systems are where it’s at. The simpler the project, the more complex the build system should be!

    If you don’t need to install and configure frooble, to compile drooble, to make drabble, to configure frabble to interface with drizzle to frizzle the schizzle, to compile snuffle to enable druffle to fluffle the socksifier, then your project is crap.

    Take a look at java, that requires TONS of stuff to compile hello world. If you don’t need at least 2Gb of ram to compile hello world, your programming language is nothing more than a toy.

      1. 512Mb of RAM, and that’s just for the IDE, now you actually need to run something, java will automatically use the rest of your ram.

        “Hello world” needs at least 2gb to run within a few minutes.

        with 8GB of ram , hello world can compile and run within as little as 30 seconds, on a quad core cpu, overclocked 10%.

  7. I have been using make since back when my buddies and I were building the pyramids. I use make for every project. I never want to type “cc -o pig pig.c” more than once, and as a project grows, having a makefile is all the more valuable. So if I go back to an old project I haven’t touched in years, I can just edit a file, type make and on to the races.

    I have no idea what Cmake is all about, but my view with make is that if it ain’t broke, don’t fix it.

    But I have seen many Makefiles from hell and have no idea what people are thinking when they craft huge complex make scenarios. If you have been turned off by these, take a second look at what clean and simple makefiles can do.

    I use make to hold a variety of things that other people might use shell scripts for, since I don’t want a clutter of shell scripts in my directory. So for an ESP8266 project, I may have targets like “make flash” to run esptool for me, and “make flash” to run picocom with proper arguments. It is all about making my life easy, not proving something.

    Avoid other peoples complex, nasty, and horrible Makefile templates. They give you the whole wrong view of the world. Make is simple and can be your friend.

  8. I hate it when makefiles try to be clever and hide the command line and do something ‘clever’ like
    compiling foo.c.
    compiling bar.c

    Makes debugging a failed build so much harder.

  9. I’d suggest scons instead, a more modern build system written in python that you can actually single step and debug.
    CMake has become quite popular but its always been a turn off for me due to its inability to debug / single step and that some of its scripts you have to just trust as working like black magic,
    Also it has no api to allow it’s functionality to be controlled or used by another language externally as the cmake devs want to force you to use it’s syntax and only it’s syntax alone

  10. > In fact, the implicit rule being used is:
    > $(CC) $(CFLAGS) $(CPPFLAGS) hello.c -o hello

    No, it’s
    $(LINK.c) hello.c $(LOADLIBES) $(LDLIBS) -o hello
    Just execute “make -p” in an empty directory to get a list of all build-in rules.

    And the real fun with make starts when you generate rules on the fly using $(eval).

  11. “[…] old-timers get to slowly grow into new features while new people have to learn all in one gulp.”
    This. This phrase is gold. For me it sumarize modern development world.

    Thank you for the article, I loved it. It’s being a really long time since I wrote a make file when I was studying. Doesn’t seems too difficult after this ;)

  12. I’ve always regarded make as one of those nifty utilities that some Unix boffin came up with to speed up compilation on their PDP-11, and over the years it just got out of hand. There is one thing I learned from it – never use invisible characters as delimiters.

  13. No, the simplest Makefile is no Makefile at all. If you’ve got a monolithic program, like your “hello.c”, just do “make hello” and it’ll happily make it without the hello target.

    The only reason you need a target at all is if it’s not a monolithic program (more than one file to combine into an executable). *Then* you need a target.

  14. I used to be all about make 5-15 years ago. Now I generally try to avoid build systems that expect me to create a makefile myself. I also try to avoid C & C++ when I can. If i have to to use them, I try to find an automated build system / IDE (mBed, Arduino, Qt/Qmake, CMake, scons e.t.c) that takes care of the build details for me so I can get on with the actual programming task.

    Still I must say I really do like make and I’m still very proud of the makefiles that I wrote to build code for AVR and ARM Micros back in the day.

  15. Nice clean usable explanations. I use make the way I use my screwdriver set… it’s easy to get, use, and achieve results. I have a large toolchest with specialized and more powerful tools which I use from time to time, but the description of keeping Cmake up-to-date is the story of my decades long developer life. This depends on a version. That depends on a library. The other thing depends on whether you have done something no-one documented. Make is small and easy. I even have enhancements for it, such as my kv (key value) bash script which simplifies many makefile complexities, offering value retention between steps specialized for the rule such as $(basename $@) used over and over again is replaced with `kv $@ target $(basename $@)` at the beginning of the rule and `kv $@ target` where it is needed. In other words, a per-rule dictionary (much like a stack frame). As you can see… I support this Fu.

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.