back
103 comments
Make is an absolutely wonderful, wonderful tool.

Most of the common criticisms of Make are actually criticisms of autoconf, which I agree is a hideous tool. (On the other hand, autoconf is a tool intended to address a hideous problem, so perhaps that's inevitable).

A lot of the more recent build tools I see are largely reinventions of Make. Make is very widely supported (most basic projects can make do[0] with portable Makefiles, though GNU Make is also available for most systems), and its syntax is actually very easy to grasp and manipulate[1].

[0] no pun intended

[1] Most projects only need a very small subset of what Make has to offer, anyway, and that can be learned in a matter of minutes.

Make is a great tool for the intended uses. Unfortunately, many uses of Make-like tools also require the unintended uses; my favorite example is the auto-dependencies, which is required by most compilation tasks and still tedious to get it right [1]. It also (mostly) lacks modern programmable interfaces, which led to Makefiles laden with hacks and mumbo-jumbos. At least for the compilation tasks, I see Make is clearly being outdated.

[1] Something like http://mad-scientist.net/make/autodep.html

The auto-dependency problem is a perfect example of how there is room for improvement in make-land. When something that basically everyone wants to do takes a long web page to describe a complicated method that sort of works, that is a compelling indicator that there must be a better way.

Incidentally, I am working on a make-replacement that is intended to address this and similar needs. It's still in very early stages, but I think I'm onto something.

The idea is to write a very low-level tool that leaves out most of Make's higher-level abstractions. In my tool there are no recipes, no implicit rules, no variables or variable substitutions, no conditionals, etc. The input to my tool is just the precise specifications of the commands you need to run, their inputs and outputs (so a precise dependency graph can be calculated), and with everything fully-expanded already.

The idea, then, is that whatever higher-level abstractions you want (if any) you build into a higher-level tool. The higher-level tool just spits out a file describing the list of tasks. Then the higher-level tool can worry about the higher-level structure, policy, configuration, etc. of your project. So instead of writing things like implicit rules in Makefiles, you just write some code that explicitly generates tasks.

For example, with Make, you might write an implicit rule like this:

     %.o : %.c
             $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
Then Make magically decides which output files match this implicit rule. My idea is that, instead of this, explicitly apply your rules to your inputs. Your build system could instead be a Ruby/Python/etc. script that looks something like this:

    for c_file, o_file in files:
      tasks.append(Task(
        target=o_file,
        source=c_file,
        command="gcc -c %s -o %s", c_file, o_file
      ))

    print(tasks)
I think this is much more convenient than having to program in Make and learn its quirky abstractions.

I have an elegant solution for the auto-dependency problem (unproven, but I think it's promising). The idea is that your dependency-calculating tasks are still just tasks, but the tool knows how to integrate the calculated dependencies back into the overall dependency graph. These dependency-generating tasks depend on the files they are generating dependencies for, so it is all part of the unified dependency graph.

If you're interested, star my project and follow my blog (where I will make any announcements about it):

https://github.com/haberman/taskforce

http://blog.reverberate.org/

Honestly I don't get why auto dependencies are even needed. IMO splitting up manually managed dependency rules into multiple, manually ordered makefiles and importing them in a master makefile, should work just fine. It is a lot of work to switch to from a managed environment, but if you start with this from the beginning it's not a big deal adding dependencies whenever they come up. As a developer I like having a nicely formatted dependency documentation, including comments, in one place instead of having to look at what autoconf & co are spitting out.
The GNU Make manual pages are really great. I use them all the time when I need to be remembered of some of the syntax or features. https://www.gnu.org/software/make/manual/make.html
I was surprised when I learned `make` and how simple it mostly is. Unfortunately it eschews man-pages in favor of info-pages which were not as pleasant to learn how to use, just to learn how to use `make`.
For me the problem is the argument formatting and dependency chasing for clang-osx and msvc/intelcc-windows
I write my own automation/build tools in Python

Envoy pynotify and fnmatch pretty much do the heavy lifting.

I do it this way because

    Since I know Python no extra syntax knowledge is required

    Python is going to be readable to me in 6 months when I've 
    forgotten the syntax of whatever build tool I just used

    *All* my machines desktop and servers already have Python 
    installed
    
    *It's fast, merging 20-30 largish text files takes around 
    10-15milliseconds

    * It's very very flexible (it's Python)
I used make extensively when I was at Uni and quite frankly if I never have to touch it again I would be a happy bunny.

It's an incredibly powerful and clever tool hiding behind an interface (that second to Git) is the worst I've ever had to use.

EDIT: I've actually been toying with the idea of writing a python library to simplify things as lots of the code I end up writing is very similar across projects, it's been one of my "think about in shower" projects for quite a while.

> a python library to simplify things

That's basically scons. Give it a try, I've been using it for years to manage a fairly complex, unusual build process with minimal effort (less than 200 lines of Python code).

http://www.scons.org/

Thanks for the link :).

Scons is awesome but for what I use Python for (minification of web assets, automatically running image optimisations) it's taking a sledgehammer to a walnut.

I'm not saying that it is a 'cure all', but actually make -n lets you dry run, and test out your rules before you actually use them (with "touch"), and in most cases I'm sure that that is faster than both reading the manual, and writing a python library for replicating the functionality (reinvent the wheel).

Now, this approach depends of course how well you know make to begin with. But there are a lot of examples out there, and the man pages of GNU make are quite good. It is quite true to the old make facility, so even older book examples should work with it.

I'm not knocking make, it's a good piece of software.

The reason I roll my own in Python is that I get exactly the functionality I want working in exactly the way I want and quite often I can replace a binary dependency which I use a tiny part of with a python function.

I find that reduces my cognitive load as I'm only working with one thing instead of others.

ymmv.

Make is a very under-appreciated tool. I think it gets a bad rap because many people's only exposure to it is in large projects (where Make has some issues) or when coupled with autotools (which is rather ugly).

If that has been your only exposure to Make, you should take another look at it, and consider using it for your next small- or medium-sized project.

If you read down the comment threads here, you'll notice something. Many many folks get frustrated with some aspect of Make, and then turn around and say "I can do better myself in [language]". And then they go off and do 30% of Make in their favorite idiom. Sorry, but as an old fart, these all read as "Wah, make is haaard, and inconvenient".

This isn't to say that I think Make hung the moon in radiant perfection, but I haven't seen any tool that has a clearly superior set of compromises and tradeoffs. Gorgeous example: automatic dependency generation. Does anyone really think the identical dep generation codebase will work on 20 year old C and node? It's not hard to glom whatever dependency mapping you want into your make workflow and drop it into separate included makefiles.

It feels easier to write your own. But what that really leads to is re-learning all the shit the make maintainers have learned in the last (look it up.. DAMN.) nearly 40 years.

Or maybe _not_ learning it, and duplicating mistakes which have been solved for decades.

The reason for all these alternate build tools isn't that Make is hard. The problem is that it's too generic and low-level. It comes with a very limited set of built in rules, after that you're on your own.

For example: If I'm working on a C++ project then I'd like my build tool to be aware of concepts like header files, shared libraries, include paths, compiler options. For me that tool is CMake.

Can I do that in Make? I'm sure I could, but I'd end up with the same problem you're complaining about. I'd essentially be writing my own build system, just implemented in Make. Life is too short for that.

The problems of make are in the scope of what it actually tries to do versus what we really want it to do. I explained a few weeks ago in another thread as to why all other replacements don't actually provide the solution.[https://news.ycombinator.com/item?id=7221515].

Almost every problem with makefiles is nothing to do with the tool itself - it's something to do with the environment, and the tool not being sufficiently intelligent enough (read: psychic) to figure stuff out. Most of the hacks built on top of make (eg, pkg-config) are attempts to fix some of these problems, but IMO, they're going about it from completely the wrong direction.

The Nix/Guix approach - declare your entire environment up front - will greatly simplify the requirements of any build system - there no longer any magic involved. You won't necessarily make the same mistakes, or even need to consider half of the problems Make has had to deal with over the years.

Make is still relevant and useful in combination with Guix/Nix, but it shouldn't need the hacks built on top of it, like pkg-config.

> The ugly side of Make is its syntax and complexity; the full manual is a whopping 183 pages. Fortunately, you can ignore most of this

I cannot take this seriously.

I like the concept of make but it doesn't make up for its own warts. Unfortunately, there is no single build tool I can blindly recommend to people without being extremely familiar with their project and how it builds. No solid and lightweight build tool that pleases more or less everyone without having 183 pages worth of manual and a repugnant syntax.

CMake/Lua gives me hope but we're not quite there yet. Make it pretty decent for small projects though... I see it as the HTTP of build tools, though. It has serious issues but when tools come up they are built on top of make because it's ubiquitous.

djb redo captures the concept of Make but is a lot simpler and no extra syntax (just uses shell)
POSIX make really does make auto dependencies hard, but GNU make actually solves this fairly cleanly by adding an include directive. As long as you have a script or program to process a file and spit out its dependencies in make style (e.g., GCC does this for C and C++ with its -M flags), you can just include the outputs of that process and make will do the rest.

For example, here's how I handle a simple C project.

    CSRC := [list .c files here]
    DEPS := $(CSRC:.c=.d)
    
    [standard statements for building go here]
    
    %.d: %.c
            gcc -MM -MF $@ $<
    
    -include $(DEPS)
Poof. Automatic dependency handling. You can see how any sort of dependency that can be detected by some sort of preprocessor can be plugged in here.
Some problems I have encountered with this approach:

1. dash before include. It will cause make to ignore any errors when generating the .d files. It can be pretty difficult to figure out what went wrong. Removing the dash will show a (harmless) error message for every .d file generated, which is pretty annoying.

2. At one time a.c includes b.h and you run make. a.d is created with "a.o: a.c b.h". If at this point you add to b.h an include to c.h, the new dependency of a.o on c.h will never be updated in a.d unless you manually delete it. This can cause some pretty nasty bugs! This can be solved by adding

    -MT $*.o -MT $*.d
to the gcc command line, which causes the dep file to regenerate when one of the dependencies changes (in fact the make manual suggests a similar solution using sed). However this creates another problem: if you remove a header a.h included by a.c (and the #include line to it), a.d still depends on a.h, so make will fail looking for it until you manually delete a.d (and any other dep file depending on the removed header).

tl;dr: this solution leaves much to be desired, and can be dangerous in some conditions.

You can even omit the extra dependency generation rule by using -MMD.

    CFLAGS+=-MMD
    -include $(DEPS)
That's it.
I completely agree with the sentiment of the article. Makefiles are a form of documentation. However, I no longer use make in new projects and instead use redux [1]: my implementation of djb redo. It manages the dependencies and you write your scripts in shell. Simple, straightforward and easy. No more 'make -B', no more make contortions.

[1] https://github.com/gyepisam/redux

I thought with make you also write scripts in shell.
This specific problem would be more simply solved with a script, listing the commands. Repeatable, testable and documents the process.

It doesn't require differentiating between identifically rendered tabs and spaces. It doesn't need dependency hand-holding, with explicit touch or checking Last-Modified. The simplest script just builds it all from scratch, like make clean.

If syntax is bad, why not build something with better syntax? Why we sticking with it?
I don't quite see the benefit of using make for arbitrary workflows (opposed to building software) over using plain old shell scripts.

But it sure isn't that much more complex nor requires a lot more of your system, so I'd say it's more a matter of preference.

Make has more natural built-in dependency management.

To write your workflow purely in shell scripts, you will invariably use lots of tests.

Also note that they are not necessarily mutually exclusive. You can write Makefiles that call shell scripts (and shell scripts that call make)

I've found these things to make Make useful for shell scripting:

- straightforward and obvious support for multiple named entry points

- by default, process terminates when a command returns a non-0 exit code

- often-adequate (though still crappy, especially for paths with spaces) set of built-in string processing functions

- scripts often somewhat portable between Unix-style shells and Windows

(I suppose there may be Unix-style shells available for Windows, but I've never found a low-dependency one that actually works. So combined with the first three advantages I've stuck with make.)

I've actually found Make much better as a shell script replacement than it is for its stated purpose of building software :)

If you can express your workflow as a set of dependencies (granted, not all workflows are easily expressed this way), make gives you parallel and incremental computation "for free".

Imagine that you needed to download a tar archive, unpack it, then run several simulations followed by regressions followed by figure plotting on the data. You could write a shell script to do this, but it would be hard to make the shell script simulate the capabilities of `make -j', and you'd have to do a lot of timestamping and file existence checking to simulate the incremental computation capabilities of make.

The advantage of make, or any other decent build tool, is the dependency management; what depends on what, what's changed, what needs to be (re)built, what order to do all this. In the absence of all that, everything gets rebuilt all the time. Not a big deal for small projects but an absolute killer as projects get larger.
how to you track directory and file changes with shell scripts?
I never understood why make has to use tabs. Even if that had sense in 1977, why it remained so until now. Why not let any whitespace, even more spaces, have the same effect of starting the command line.
Make wouldnt be the first build tool i would try. I would go with something like gradle first. Eventhough, in Mike's case make looks like a good match for his requirements.
Gradle looks to me like it's full of good intentions, of exactly the sort that in a few years will have led them straight to hell, at which point someone will reinvent ant or make with different syntax and we'll be back where we started.

I worked on a proprietary build system remarkably similar to gradle for many years. The cleverer a build system tries to be the more likely it will become a self sustaining beast that consumes ever more of your time and destroys productivity. They look great with relatively simple systems, but a few years into production with multiple deployment target configurations and you will want to murder people. Build systems should be stupid, simple, and trivially predictable.

I often convert makefiles to shell script (bash) when I outgrow my knowledge of make. However, it's where I always start, and the makefile never goes away, just the complex pieces get rewritten in bash!
I am glad, that make exists. Guess what would happen, if every open source tool would use its own build environment. Open Source Software would be much more difficult to build from sources (sometimes, I can't do without).

So it is rather straight forward. Type make, sometimes autoconf and most of the time you are done.

Also in my own projects I prefer make to many other build tools. In software development, command line still rules (if you want to be really productive)!

I've used make as the basis for a system that works out the dependencies for nightly batch processing in an ERP system and coordinates the execution of the required processes across multiple applications that make up the system.

There's some shell script to glue the pieces together, and a Python script for invoking processes within the ERP applications, but make is the secret sauce that figures out what order to run everything in.

https://github.com/ndmitchell/shake Shake is an awesome alternative to Make written in Haskell. It fixes a lot of the weird dependency tracking issues Make has ("my build broke and I don't know why!" "did you make clean?")
Surprised that there is no single comment here about other options like ant and gradle. I personally hate anything where tab has a different meaning from 4 spaces, but maybe it's just me. Anyway, my personal choice for any kind of automation would not involve make if I can use the tools I mentioned.
I've been using ansible for complex build environment setup. Inline documentation. Can Factor in software dependencies or environmental setup as required.

It's working pretty well so far. It wasn't an intensional thing. I had a fairly complex stack for a particular project that I needed to share.

I was looking at Ansible in the context of this thread:

https://news.ycombinator.com/item?id=7487202

exactly because I like the idea of capturing the setup (compared to a more haphazard approach). My initial reaction was that for the computer setup use case, it did too much to hide away complexity.

I guess I don't have a deeper point, but I wonder if it is overkill for capturing simple workflow like is discussed here.

You might be interested in Drake, a kind of ‘make for data’.

http://blog.factual.com/introducing-drake-a-kind-of-make-for...

I used to do this until I found drake. Drake is the truth and the light. Use it.
Make uses disk I/O, which is really slow, and for complex projects become difficult to maintain. Also, why not use a proper programming language to define the build process? Well, a modern tool like Gulp[1] does that. It's streaming/asynchronous and scripts are written in JavaScript. I don't see why one would prefer Make over it.

1. https://github.com/gulpjs/gulp

There are many complex projects that use Make to build source code. Grab just about any software release tarball and there will almost certainly be a Makefile in it.

This is yet another example of the JavaScript community being completely ignorant of what came before them.

And how exactly do you propose to compile a file without reading it?
I wish my compilations were merely I/O bound... everything would be so much faster then!
I might be wrong, but I think make does not work the way you think it does: First of all files you write to disk are not actually written to the harddrive immediately there are several caches in between, secondly if make has to process a file in multiple steps it will probably use unix pipes and won't write out intermediary files to disk.