My series of comparisons between SCons and GNU make sparked a lot of discussion, not just about SCons and gmake, but about many other build tools. That was to expected, but what surprised me was several comments specifically criticizing the syntax of make — the semicolons, colons, ats and dollars that we all know so well. One reader actually said that make syntax has a 1970’s feel, as if the age of the language is somehow an indicator of unsuitability for the task. Then my friend John Graham-Cumming posted an article in defense of make syntax, and I figured I would add my thoughts.
Make syntax is the worst… except for all the alternatives
Criticisms of make syntax strike me as a bit absurd. Take a look around the build tool space: you’ll see that many of these “improved” tools use syntax that ranges from “pretty much the same” to “ridiculously verbose”. Let’s look at the syntax used for the two core functions of a build system: specifying the graph of depdencies between files, and specifying the commands to generate a file from a set of inputs.
Dependency graph syntax
The syntax for describing the relationship between input and output files in make is concise, if oblique:
foo: foo.in
To me this is elegant in its simplicity. You may argue that the choice of a colon is arbitrary, and you’d be right — but then, what would be significantly better? I would say there is nothing that is better, but plenty of things worse. For comparison, look at the same relationship, expressed in the syntax of some other build tools:
CMake |
add_custom_command( OUTPUT foo COMMAND update -o foo foo.in DEPENDS foo.in) |
Cook |
foo: foo.in ; |
Jam |
MyCompile foo : foo.in ; |
Rake |
file foo => [ 'foo.in' ] |
SCons |
env.MyCompile('foo', 'foo.in') |
tup |
foo.in |> update -o %o %f |> foo |
Waf |
bld( rule = 'update -o ${TGT} ${SRC} source = 'foo.in', target = 'foo') |
Some of these, like Cook and Jam, are nearly identical to make. Others, like Waf, are certainly more verbose, but not obviously better. That verbosity may seem great when there’s only a handful of targets, but with hundreds of targets, it will be an irritation.
The truth is that there just isn’t any particular syntax that naturally lends itself to expressing a dependency graph. The reason make syntax hasn’t changed in over 30 years is because target: prereq works, and it’s just as good as anything else you might choose.
Command syntax
True to form, the syntax for specifying the commands to run to generate a file in make is just as terse:
update -o $@ $^
This minimalist syntax naturally puts the emphasis on the important stuff: the command to run and its flags. Here’s the same command in some other build tools (nota bene: some of these are the same as what’s shown above; in those cases I could not easily determine a syntax for specifying dependencies separately from commands, or whether that is even possible with that tool):
CMake |
add_custom_command ( OUTPUT foo COMMAND update -o foo foo.in DEPENDS foo.in ) |
Cook |
{ update -o [target] [need]; } |
Jam |
rule MyRule { MyCompile $(1) $(2) ; } actions MyCompile { update -o $(1) $(2) } |
Rake |
sh "update -o #{t.name} #{t.prerequisites.join(' ')}" |
SCons |
env.Append(BUILDERS = {'MyCompile': Builder(action = 'update -o $TARGET $SOURCE', src_suffix='.in')}) |
tup |
foo.in |> update -o %o %f |> foo |
Waf |
bld( rule = 'update -o ${TGT} ${SRC} source = 'foo.in', target = 'foo') |
Most of these are more verbose than make, and for me the extra text just makes it harder to see what’s really going on. The SCons example is particularly ugly: 6 times the characters to express the same simple command!
Did you mean TAB instead of 8 spaces?
I suspect that at the heart of complaints about make syntax is a single unfortunate confluence of facts. First, make uses a literal TAB character to mark the beginning of a command in a recipe. Second, most code editors automatically replace TAB with spaces. Together these facts conspire to confound even the most experienced makefile writer, resulting in this slightly condescending, always irritating error message:
*** missing separator (did you mean TAB instead of 8 spaces?)
.
I won’t argue with you, this is a real nuisance. But there’s good news: GNU make 3.82 introduced a new special variable called .RECIPEPREFIX. Set this variable to any character you like, and GNU make will use that instead of TAB to mark commands in the makefile. For example:
.RECIPEPREFIX=! all: !@echo Who says make syntax is bad?
Conclusion
Don’t get me wrong: as with any tool, there is room for improvement in make. I agree with John’s suggestion to optionally include command-lines and input file checksums in the up-to-date decisions (some of that is available now in ElectricAccelerator). Beyond that I think it would be great to add support for non-pattern rules with multiple outputs — there’s no way to do that now, although there are a variety of hacks to emulate non-pattern rules with multiple outputs. The interesting thing about these ideas is that all of them can be added to make, without requiring the creation of a completely new build tool.
Yes, make syntax is terse, but the lack of extraneous noise makes it easier to see what’s going on in a makefile than in a comparable build file from another tool. Likewise, make syntax is old, but rather than being a weakness, I see that as a testament to it’s fitness. Surely it’s telling that in 30 years, nothing else has come along that is obviously better, or sufficiently better to justify the cost of migration.
One thought on “Make syntax is the worst… except for all the alternatives”