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.
[1] Something like http://mad-scientist.net/make/autodep.html
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):
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.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.
CFLAGS+=-MMD
-include $(DEPS)
That's it.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.
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.
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)
- 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 :)
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.
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.
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)!
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.
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.
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.
http://blog.factual.com/introducing-drake-a-kind-of-make-for...
This is yet another example of the JavaScript community being completely ignorant of what came before them.