I Like Makefiles
switowski.com
switowski.com
One of the worst things of Make is how deceptively simple it looks.
Make does exactly one thing: it takes input files, some dependencies and generates _exactly_one_ output file. To have rules which don't generate output (like `install` or `all` or `clean` or all targets in the article) we need to resort to a hack, a special magic target like `.PHONY` (which hasn't been part of POSIX up to the 2017 version - IEEE Std 1003.1-2017 - https://pubs.opengroup.org/onlinepubs/9699919799/utilities/m..., only the current one - IEEE Std 1003.1-2024 - https://pubs.opengroup.org/onlinepubs/9799919799/utilities/m... includes `.PHONY`). If you want to generate more than one file (like an object file and a module or a precompiled header or ...) you are on your own to build some brittle hack to get that working. Don't forget that not every Make is GNU Make, BSD and other nix like Solaris/Illumos still exist.
Don't get me wrong: Make has it's uses for sufficiently complex projects which aren't too complex yet to need some "better" build system. Problem is that such projects may get too complex when more code is added and they inevitably gain some sort of scripts/programs to generate Makefiles or parts of Makefiles (so, an ad hoc meta build system is created).
And the problem isn't that they use it, but that they are proposing it as a solution to "everybody". And that their Makefile stops working as soon as there is a directory (or file) `build` (or `dev` or ...) in the project root.
You name any technology and anyone can enumerate dozens of projects that use it wrong.
But seriously: can I take a look at it (Soource + Makefile)?
I like how the includes are separated and commented.
Also if you weren't already familiar with their work you might be interested in giving this a read: https://justine.lol/ape.html
% grep '^include' Makefile -c
159
includes with % wc --lines --total=only $(awk '/^include/{ print $2 }' Makefile) Makefile
22547
lines in total really count as having a single makefile? I have no dog in this fight, just wondering.* If you need portability, Makefiles are hard.
* The whitespace design of Makefiles is bad and has swallowed up countless debugging hours over the years. This design flaw isn’t intrinsic to the way Makefiles work, it’s just a lousy artifact from a superficial decision from decades ago: to change behavior based on distinctions invisible in source code. It’s mitigated by syntax highlighting but still bites people.
* Makefiles are dependent on the consistency of the build environment, for example the availability and behavior of command line switches. Even if your project doesn’t need OS platform portability, this is still a pain across time and requires external tooling to manage.
* There are certain subtleties to the way Makefiles behave that are addressed by `.PHONY`. I agree that these are manageable in the absence of other complexities, but they contribute towards Makefiles being more difficult than appears at first.
I’m sure you’re familiar with those critiques and others. They may not bother you, but you don’t speak for everybody.
Part of the premise of Make is its ubiquity, but if you can’t rely on that save as a simple bootstrap (as you seem to be doing) then why not forego it for something else?
Because blindly ditching a technology for no reason at all is not a way to fix problems.
Oh yes, in the good old tradition of "... as long as it's some Linux on x86".
[...] on Linux 2.6+ (or WSL) using GNU Make.
Sorry, it's actually AMD64 _and_ ARM64!Now show us the Makefile.
You may be biased.
He's not using C, though :-)
> And the problem isn't that they use it, but that they are proposing it as a solution to "everybody".
He's proposing it for the same reason I'm starting to like it, after many years in the industry: as a simple build wrapper.
> And that their Makefile stops working as soon as there is a directory (or file) `build` (or `dev` or ...) in the project root.
And they can fix that problem in 5 minutes, big deal :-)
> Don't forget that not every Make is GNU Make, BSD and other nix like Solaris/Illumos still exist.
This is a very bad reason in this day and age. 99.999999% of *NIX usage these days, probably 99.9999999999999999% for the average person, since most people won't ever get to those environments where BSD and Solaris are still used, is Linux.
And even for BSD and Solaris, guess what... you add an extra step in the build instructions asking them to... install GNU Make.
Heck, even back in 2005 (I think?) for Solaris one of the first things you'd do was to install the GNU userland wherever allowed because the Solaris one was so forlorn I swear I heard wooden planks creak and dust pouring down every time I had to use their version of ps.
And regarding POSIX, meh. If you're a C developer (C++, Rust, I guess), knock yourself out. Most of the stuff devs use are so far removed from POSIX... Actually, not removed, but has so many non-POSIX layers on top (I mean not standardized). Ruby bundler is not standardized like awk. Python pip is not standardized like make. Etc, etc. That's the reality we're in. POSIX is very useful but only as a very low level base most people don't need to chain themselves directly to. I'd definitely not avoid a tool because it's not in the latest POSIX standard (or only in the latest POSIX standard).
As said elsewhere, the use-case in the article is too simple to warrant a Makefile. So: if you aren't compiling some static language, you do not need - and certainly don't want to use - Make.
> you add an extra step in the build instructions asking them to... install GNU Make.
The main reason to use Make is that it is installed everywhere, as stated multiple times in other posts. If you must install something, you can also install a better alternative for your specific use-case to Make.
> one of the first things you'd do was to install the GNU userland
Yes, and the Unix vendors even shipped them on companion CDs or similar.
> is not standardized like awk
Same problem with awk (and sed and ...): some weeks ago I had problem with the SDK for some real-time Linux that works with mawk only, and not with GNU awk (most of the time it's the other way round, only working for some GNU program).
Don't get me wrong: I don't like Make, but I hate CMake and Autotools (and many other C++ build systems) too (and C and C++ and Fortran compilers and their vendors).
I've found that I prefer make as a command runner and most of the time I'm just running Python poetry commands or building Docker containers or running AWS infra commands. It's very useful to have a simple tool to run commands and have them depend on each other.
And regarding many of the alternatives to Make, they're either more complex or have other issues:
You have a lot of confidence. In reality, it's probably more like 30-60%, more now because of WSL. The rest is Mac OS, which uses a BSD userland and hence BSD make by default.
No. Just a really old version of GNU Make
make --version
GNU Make 3.81
Copyright (C) 2006Apple has restrictions about what software on the system you can modify as a user and how, in the name of security. GPL 3 is unfriendly to such restrictions. Whether what Apple is doing on the Mac specifically violates GPL is, well, a matter of debate that has never been tested in court, but Apple thinks there's at least some risk there, and that the risk isn't worth taking.
This is also why ZSH is now the default shell on the Mac. ZSH never switched to GPL V3, so it was either that, remaining on some god-awful old Bash version, or making their own.
And for MacOS you do the same thing, you get them to use their beloved homebrew to install GNU Make.
Honestly, a big issue I see is that people can somehow argue with a straight face (and successfully too!) to invest weeks of work introducing a pet project to avoid a 1 hour inconvenience that happens once every blue moon. Proportionality takes a backseat very quickly to motivated reasoning.
And talking about complex C and C++ (to be fair, the complex ones are almost always C++ ;) projects, I would not say that CMake (or Meson or ...) is less complex than Buck 2, it certainly has _way_ more magic than Buck 2. And getting Make & C++ & ccache(or whatever) & distcc (or whatever) to work _reliably_ isn't easy either ;)
Not true. Your dependency graph might culminate on a single final target, but nothing prevents you from adding as many targets that generate as many output files as you feel like adding and set them as dependencies of your final target.
Think about it for a second. If Make was only able to output a single file, how in the world do you think it's used extensively to compile all source files of a project, generate multiple libraries, link all libraries, generate executables, and even output installers and push them to a remote repository?
> To have rules which don't generate output (like `install` or `all` or `clean` or all targets in the article) we need to resort to a hack, a special magic target like `.PHONY`
I don't understand what point you thought you were making. So a feature that boils down to syntactic sugar was added many years ago. So what? As you showed some gross misconceptions on what the tool does and how to use it, this point seems terribly odd.
> And the problem isn't that they use it, but that they are proposing it as a solution to "everybody".
I think you're making stuff up. No one wants Make to rule the world. I don't know where you got that from.
I think the whole point is that Make excels at a very specific usecase: implement workflows comprised of interdependent steps that can be resumed and incrementally updated. Being oblivious of Make leads many among us to reinvent the wheel poorly, using scripting languages to do much of the same thing but requiring far more work. If you can do this with a dozen lines of code in a Makefile, why on earth would you be churning out hundreds of lines of any random scripting language?
Sorry, I did phrase that badly. A better version of that sentence would be
A single target (a single node in the dependency graph) of Make does exactly one thing: it takes input files, some dependencies and generates _exactly_one_ output file.
> I think the whole point is that Make excels at a very specific usecase [..]Excatly what I wanted to express with my post above. But the article isn't about such a case, but for something for which a single shell script (or, better, just adding the commands to the `scripts` stanza of `package.json`, which is the more common, expected way to do it) is actually better suited and way less error prone.
Yes, but this is not particularly relevant to the user. With pattern rules it's trivial to define a large number of targets automatically, such as in the example (from the manual):
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@The problem is a target with more than one output files, that target would look something like, which does work
foo bar: baz
compile $< -o foo -o bar
but as this is the same as writing foo: baz
compile $< -o foo -o bar
bar: baz
compile $< -o foo -o bar
to generate `foo` and `bar` the rule is run twice in a parallel build (`make -j 2`. Which may just be unnecessary or it may break the whole build. foo: baz
compile $< -o foo -o bar
bar: foo
(Can someone tell me how to do code tags on HN please? :) Edit: fixed now, thanks!) A_MESS_OF_FILES := foo bar zot
You now have to create dummy targets for bar, baz, and zot and not forget to add them. Or maybe break it into MAIN_FILE := foo
SUBORDINATE_FILES := bar zot
foo: baz
sudo make me a foo
$(SUBORDINATE_FILES): %: fooScroll down to "Grouped targets". I think these address the concern you raise.
The snippet you have shown is an example of the "independent targets" pattern which the first half of that page also covers.
I'm still not really following the point about one output file? That might be Make's stated purpose, but a Makefile rule can certainly create extra files as a side effect (or do pretty much anything a shell user could do, from creating directories and downloading files to launching applications)
One of my projects has a single makefile rule which downloads and unzips a tarball, applies a patch to it, then builds the application within, resulting in half a dozen binaries which are then used in building the rest of the project.
Edit: Ah - I see what you mean now, in your subsequent comment.
I've definitely been using .PHONY on various Linux and MacOS computers long before 2017.
Maybe it's just me, but I've never much cared for whether or not something is specified if it happens to be present everywhere I go.
Me too, and I've also used Makes which didn't (on e.g. Irix). What I wanted to express had been that you can't even rely on `.PHONY` existing, much less many other features.
Don't forget that not every Make is GNU Make, BSD and other nix like Solaris/Illumos still exist.
And I'm not even talking about Windows and Nmake.There is "make" for Windows too, but it's not as relevant - the windows commands are so different, it's unlikely you'll have a cross-platform file for Windows and BSD/Linux, unless you require user to install unix tools on Windows, in which case they will likely come with GNU make.
I think in most cases, using GNU make is the easiest way to provide compatibility with multiple OSes. There are certainly exception - if your daily driver is FreeBSD, use BSD make. But for Mac OS or Linux users, GNU make is a good default.
A pattern like
tgt:
generate_many_files
touch $@
is pretty common. What's the issue?Fully agree, and I would add that it's far better to adopt the right tool for the job, even if you are not an expert, than avoiding the criticisms from perfectionists by adopting the wrong tool for the job.
Everyone needs to start from somewhere, and once the ball is rolling then incremental changes are easy to add.
Great job!
Little tips here and there are nice, but that doesn't teach me the mentality of how to achitect a makefile
What exactly are you missing from the official manual?
Structure generally means most manuals are not the first resouece a learner should try to learn from. Great to have on hand, but manuals are not structured like a textbook that builds upon concepts needed to productively work with the subject.
Fully agree. Don't get discouraged, and keep it up!
This leads us to a neat rule of thumb for phony targets: any recipe that does not touch $@ and only $@ should have $@ marked as phony.
I find that keeping track of phony targets with a list makes things much easier.
phonies :=
phonies += something
something:
./do-something
phonies += something-else
something-else: something
./do-something-else
# touches $@ and thus does not need to be phony
create-file:
./generate-some-output > $@
.PHONY: $(phonies)What does it get you other than the ability to print the list of all phonys?
I use phony targets so much I wrote a shell script to parse the makefile database dump into some sort of help text. It doesn't depend on that variable at all.
https://github.com/matheusmoreira/.files/blob/master/~/.loca...
Prints output like:
phony1
phony2
dependency1
dependency2 .PHONY: something
something:
./do-something
.PHONY: something-else
something-else: something
./do-something-else
create-file:
./generate-some-output > $@I still wouldn’t say it’s that complicated - you do need to know your way around the syntax a bit but it’s less challenging than getting all the other tooling working in the first place. :)
“One file can be the target of several rules. All the prerequisites mentioned in all the rules are merged into one list of prerequisites for the target.”
It's simple, readable, editable, composable and already installed everywhere.
It does what it says on the tin and not much else.
FWIW, I also wrap up whatever fad (or nightmare) build system people use in other projects when I need to deal with them.
I'll eat crow if wrong, but I'm guessing I know more about GNU make than you do. It is none of the four things you claim. Also, people who say "on the tin" need a good ass-kicking.
Build systems tend to commit one or more of the following sins:
* Too basic: Once you try to build anything beyond a toy, it quickly becomes chaos.
* Too complicated: The upfront required knowledge, bureaucracy, synchronization and boilerplate is ridiculous. The build system itself takes an order of magnitude more data and memory than the build target.
* No standard library (or a substandard one that does things poorly or not at all): You must define everything yourself, leading to 10000 different incompatible implementations of the same build patterns. So now no one can just dive in and know what they're doing.
* Too constricting: The interface wasn't built as a simple layer upon an expert layer. So now as soon as your needs evolve, you have to migrate away.
* Too much magic: The hallmark of a poorly designed system. It doesn't have to be turtles all the way down, but it should be relatively close with few exceptions.
* Cryptic or inconsistent syntax.
Of course the chaos is not caused by, "very hypotheticaly" let's say, a compiler or maybe a language without modules.
How would you estimate that ? 20%, 40%, or 70%, true ?
Its just that in some languages and build systems (Node, Maven), we have abstracted this away by calling them plugins and they probably come from the same group that made the library you need.
No such pluginsystem exists, as far as I am aware, for makefiles.
There aren't many widely used build systems that can handle such dynamic dependencies without some special "magic" for these, the only one that I know of (with a significant number of users, so not Shake) is Buck 2 (Bazel and all C++ build systems use "special magic", you can't write in user rules).
So what's the problem exactly? https://mmottl.github.io/ocaml-makefile/
Oh look, it even builds a project faster than Dune: https://discuss.ocaml.org/t/dune-build-vs-makefile/11394
They all have the samé problem: that you don't know the name (or even the number) of modules (module files) being generated without reading the source. And as a bonus every compiler uses a sligthly different naming scheme for the generated module file (this is of course no problem for OCaml ;).
As an example (using Fortran). File `test.f90`:
module first
contains
subroutine hello ()
end subroutine hello
end module first
module second
contains
subroutine world ()
end subroutine world
end module second
`gfortran -c test.f90` yields the following files (2 of them are modules): -rw-r--r-- 1 roland staff 221 Sep 21 19:07 first.mod
-rw-r--r-- 1 roland staff 225 Sep 21 19:07 second.mod
-rw-r--r-- 1 roland staff 185 Sep 21 19:07 test.f90
-rw-r--r-- 1 roland staff 672 Sep 21 19:08 test.oI've successfully written Makefiles for Fortran and they worked with ifort/ifx and gfort. In my experiments I've also made GNU Cobol, GNU Modula-2 and Vishap Oberon fit within the Makefile paradigm without much trouble. You have failed to provide reasons as to why those languages in particular (or more likely any language that's not of C heritage) can't be used with Makefiles. For instance, you can definitely couple OCaml with Makefiles, just use ocamlopt and treat .cmx files as object files, generated beforehand by ocamlopt -c (like you'd do with GCC). I am not familiar with C++ modules and as such I didn't experiment with them.
Did the samé (I'm not sure if gfortran did exist at all at the time, I guess it had been g95), plus they worked with Absoft, PGI and Pathscale too (yes, that has been some time ago). And it was a great PITA. Not the least because at the time no Fortran compiler did generate the dependency description, so you either had to parse the Fortran sources by yourself or use makedepf90, which didn't work with all sources.
> You have failed to provide reasons as to why those languages in particular [...] can't be used with Makefiles.
I have obviously badly worded that. I didn't mean it is impossible, just that is a great PITA.
> I am not familiar with C++ modules and as such I didn't experiment with them.
They have the same problem, you don't know the name of the module that is going to be produced.
- Too basic: At least half of the software I use just uses plain makefiles and maybe a configure script. No autotools. I optionally run ./configure, and then make and make install, and it just works. I definitely wouldn't consider my setup to be a toy by any stretch of the imagination. It's built out of smaller programs that do one thing and one thing well.
- Too complicated: I don't know, I think make and how it works is really easy to understand to me at least. I guess everyone's had different experiences. Not necessarily your case, but I think usually it's because they had bad experiences that they probably blamed make for, when they were trying to build some complex project that either had a bad build setup itself (not make's fault), or without the requisite knowledge.
- No standard library: It's supposed to be tooling agnostic, which is what makes it universally applicable for a very wide range of tools, languages, and use cases. It's viewed as a feature, not a bug.
- Too constricting: I'm not sure what you mean here, it's designed to do one thing and one thing well. The simple layer is the dependency tracking.
- Too much magic: Cryptic or inconsistent syntax: See 'Too complicated'
The fact that Make can't even do subdirectories sanely is kind of ridiculous.
Does anyone know of anything better than Make? There's Ninja but it's not designed to be written by hand.
Xmake https://xmake.io/ for C and C++ (I haven't use that for anything serious yet) and Buck 2 https://buck2.build/ if you need a really complex build system. Both of these do caching of build artifacts and can do distributed builds (with less and more complex setup).
Xmake looks interesting too (even though I hate Lua). I wonder why it isn't more popular - I don't think I've seen a single project use it.
Though it's possible this goes beyond your "just do stuff"
The only general-purpose build system that spans all these languages is `make` or systems that target `make` (e.g., CMake). And this sucks because `make` sucks. And `make` sucks because:
- it's really difficult to use right
(think recursive vs. non-recursive make)
- so many incompatible variations:
- Unix/POSIX make
- BSD make
- GNU make
- `nmake` (Windows)
- it's rather ugly
But `make` used right is quite good. We're really lucky to have `make` for the lowest common denominator.https://pubs.opengroup.org/onlinepubs/9699919799/utilities/m...
As a smaller contender, my personal favorite is the Mill build tool (written in Scala), that is basically what a build tool should be, it’s as close to a theoretical perfect as possible. I really advise reading the blog post by its author Li Haoyi: https://www.lihaoyi.com/post/SoWhatsSoSpecialAboutTheMillSca...
(too basic) Makefiles are not. (too complicated) They can be, depends on what you make them to be. (standard library) Well, there is one, there are some builtin functions you can use in the makefile. (too constricting) Haven't noticed that, so I would say no. (too much magic) Hmmm I don't see it. It is very clear what is a target and a dependency and so on. Not so magical. (syntax) Yeah definitely could be better. Even a plain JSON file would be better here.
I will show how Make hits every one of your complaints:
(sarcasm on)
in file hello.c:
#include <stdio.h>
int main(int ac, char **av) { printf("hello\n"); return 0; }
How to compile and run this? We need a build system! Download and install GNU Make.When that step is complete:
Type in
make hello
and its done. Now, run via ./hello
See, Too much magic (didn't even have a makefile or Makefile), no standard library, Too constricting, cryptic, too basic. And, because you had to install Make, too complicated. Hits every one of your objections.
(sarcasm off)
No smarts. It's just a collection of snippets with a few variables. "make run", "make test", "make lint", that kind of thing.
"make recent" = lint then run the most recently modified script.
You could do the same thing with Bash or other shells, but then you get stuck into Developer Land. Things are so much more complicated, without giving extra value. Make is just a DSL saying "files like this, are made into files like that, by running this command or two". That's it.
This is incredibly powerful!
Nicely put.
Decades ago i wrote a testing framework in java where you could specify your tests and their dependent classes using make-like syntax. So you could have a set of test classes which define the "baseline suite", then another layer of test classes which is dependent on the above and only run if the above is successful and so on.
I really do not understand why folks today make everything so complicated. My advise has always been, stick to standard Unix tools and their way of doing things (tested and proven over time) unless you run into something which could absolutely not be done that way. Time is finite/limited and i prefer to spend it on System/Program Design/Modeling/Structure/Patterns etc. which are what is central to problem-solving; everything else is ancillary.
Running `make test` and knowing it will work, regardless of the stack, language, repo is a huge lifesaver.
The problem with make isn’t make - it’s that what makes calling usually doesn’t do that anymore. On my last project we had a makefile that had 4 main commands - build test frontend deploy. Build and test called through to maven, frontend called npm, and deploy called docker + aws.
All of those tools do their own internal state tracking, caching, incrementalness and don’t report what they’ve done, so it’s not possible to write a molecule that says “only deploy if build has been updated” because maven/cargo/dotnet/npm/go don’t expose that information.
.PHONY: $(MAKECMDGOALS)
> The author could have replaced it with a shell script that read $1Sure, but `./build.sh dev` is a bit less obvious than `make dev`.
Another reason to use Make even if you don’t have any non-phony steps is that you can add those later if needed. (I agree that the author should mark {dev,build,deploy} as phony though.)
> giving Make a bad name
How so?
Fine. Write a `make.sh` that parses the arguments; that would be better.
> How so?
Well, read the comments here. Do you sense that Make is a beloved tool? Most of the complaints are about some details about syntax, and those complaints are completely valid. If you use Make for its intended purpose, then it's still easily well-worth using, despite that. But if you use it as a glorified script, then all you see is the warts, without any upsides. And you then tell all your friends that "Make sux!" Which is a huge shame because Make is awesome.
The author just shared something they think is cool. That takes guts to show the world, and our critiques should respect that.
There are many reasonable scenarios why you might want to do this: determining at the point of calling make which targets to force or deactivate for safety, projects with nested or external makefiles not directly under your control, reuse of MAKECMDGOALS throughout the makefile (including propagation to submakefiles), ...
.PHONY: $(MAKECMDGOALS)
qux:
foo: qux
bar: foo
Now make bar and make foo bar will disagree on whether foo is phony, which may or may not be what one wants depending on both what foo and qux do, and how bar depends on foo and qux side effects.It also very much depends on what the intent is, notably such a "autophony" make foo is very different from make -B foo.
They could make technical improvements to their own Makefiles, sure. But it's more about being able to enter a project and have a consistent experience in "getting started".
I'd say putting the Makefile content in `package.json` would be more consistent, especially as they are already using Gulp as the build system.
But anyway, comments in a Makefile or `package.json` are not documentation anyway, that's what the `README` or `INSTALL` (or whatever) is there for (in projects like the one the Makefile is written for).
Or just with a simple command runner like just.
Maybe his Makefiles aren't complex, nor they seem to follow all the best practices invented by code gurus in sandals, but it works and, what's important, it works for him.
That it what a lot of SW developers forget: your code might be the best in the world, but , if someone is not able to build it, it is useless.
Make isn’t a silver bullet for builds. It isn’t even a bullet. Most software gets built from scratch and make’s deps graph makes little to zero sense in this mode. Make is a quirky poor dev tool footgun, jack of all trades master of none.
If you just want to run commands in the order written down, don't need the topological sorting feature of make and value ubiquity then a shell script is the answer.
If you are not stuck in the past and you truly live by the UNIX philosophy of doing one thing and doing it well, a command runner is the answer.
The command runner avoids the ton of foot guns both shell scripts (no matter which flavor) and make files have. just also brings a couple of features out of the box that would be very tedious and error prone that replicate in make and shell scripts.
I rewrote my makefile in PS and don't miss anything from make and have no regrets, as it is simpler now.
I don't particularly mind this use of make, but as an article on make it fails to exemplify what I think is its main purpose.
Make is from 1976. I don't think you can legitimately refer to it as that.
I don't have any of those concerns with GNU Make.
Just has quite a few features and it's at version 1.35.
That would've been a pretty short window of time since make first came out (according to wikipedia) in 1976.
I seem to recall it being praised very highly at the time as a great tool that saved many billable expensive CPU minutes and made a developer's job so much easier.
Until it doesn't. And then you really have to learn about PHONY targets, why and when there must be a tab and not spaces - good luck with an editor that doesn't treat Makefiles special and is configured to convert tabs to spaces.
Because it isn't fun checking if the whitespace at the beginning of the line is a tab or spaces. And as said, you must know when to use tabs and/or spaces in rules.
For doing such a simple thing as calling some commands, Make has way too many subtle footguns which _will_ bite somebody, someday. The problem (that's not a problém at all, that's a reason to celebrate!) is that most JS devs and users aren't used to Make, compared to e.g. C programmers. To rephrase: as someone writing C, you have to use something like a Makefile, as anything else (like scripts) gets unreadable and -usable quite fast. But if you can get away with a less complex solution, you should really use that instead of Make.
that's why https://editorconfig.org/ exists, so that neither you nor your teammates have to think about these things
You're better off using a Makefile linter. But you must know about the problem before being able to solve it. And error messages like
Makefile:2: *** missing separator. Stop.
aren't the most helpful.Is that Stockholm syndrome? Or an appeal to history/authority in action? What makes people believe that this is even remotely reasonable.
inb kids these days, I started in the '90s and wrote my share of makefiles. Tolerating make only made sense until 2010-ish, then both hw and sw advances rendered it useless.
Edit: just realized my reply to a wrong person, but let it stay here
If you're using make a glorified task runner, why don't you just create a scripts/ directory with shell scripts to do whatever you want. This is simpler, cleaner and works everywhere.
Make doesn't really add anything. I get the feeling that using make this way is an aesthetic preference that has somehow developed with time into a "this is the one true Unix way" cargo cult.
I wish there was a flag for make which set it to be a command runner by default for the current makefile.
The whole difference of Makefiles, their original idea was to not re-compile a target _file_ if it's already there. So if you need .PHONY -- you're using Makefiles wrongly.
This is a sect and global trend to reinvent and re-implement the wheel with the Rust :D
[macos]
# do a thing
And without any other checks, that section will only ever run on Mac. Yes, of course you can replicate this in Make, but it isn’t nearly that easy.1) It's already installed practically everywhere
2) It reduces your cognitive load for all sorts of tasks down to just remembering one verb which you can reuse across multiple projects, even if the implementation ends up differing a bit
3) In conjunction with the similarly ubiquitous SSH and git, you have everything you need to apply the basic principles of DevOps automation and IaC
There's something special about waking up one day with an idea, and being able to create a fresh git repository where the first commit is the Makefile you've had in your back pocket for years that scripts everything from environment setup to deployment to test automation to code reviews.
There's zero effort beyond just copying your single file "cookbook" into that new repo.
This always comes up, and is a sad chicken/egg problem.
We can all somehow agree that Make mostly sucks, but OS maintainers aren’t interested in providing a default alternative, due to choice overload or something.
Well, except Windows. But nobody uses that right?
2. nmake is with POSIX make, let alone GNU make. It doesn't even support .PHONY target, which is what you need to replace Just with make.
3. Installing Just with WinGet is simpler, faster and takes probably only 1% of the space of installing Visual Studio for nmake.
Neither is Make or GCC on Unix.
If it is a choice, it sort of shows an "I do not care" attitude to software development, or being seriously uninformed about proprietary software. Usually those are the types, for whom software engineering is merely a 9 to 5 job, and not a craft they take pride in. An activity they do not really care about at other times. Which is OK to do, not a crime. If I were hiring though, I would rather look for passionate software engineers/devs, who know a lot of stuff from tinkering and exploration. Ultimately using Windows means you are not truly in control of your productive system and are at the whim of MS. It is a risk no self-respecting software engineer should take.
To clarify, that is not to say, that there cannot be craftsmanship people using Windows. It is just way less likely. More likely they are "enterprise" software people. Even the choice to explore and use a GNU/Linux distribution betrays some kind of mentality of exploration. Wanting to know what is out there. Learning a new thing. Adapting it to ones needs. This kind of learning mindset in the long term is what sets engineers apart from others.
So I would claim, that not many good software engineers use Windows to be productive. If they have to, they will likely install some VM or some means of making things work as if they were on a GNU/Linux system. WSL or whatever, to circumvent the limitations and annoyances of a Windows system.
If there's craftmanship anywhere, its in game development, and they surely don't want to spend all their time working on a platform without proper tooling that their end users overwhelmingly do not use.
The choice of OS has nothing to do with craftmanship or "exploration". I "explored" linux many times and am not using it currently.
In fact, I'm happy to argue that most developers that care so much about the choice of OS that they are uninterested in using another one (and do not work in OS development) are probably somehow stuck in their ways and uninterested in exploration themselves.
Taken even further, currently the only important OS is the browser, and nobody cares who launches it.
I hope you are not somehow in charge of hiring.
But of course, if you want all the IP to leak via MS spyware phoning home, sure, let your devs work on Windows machines.
I think neither take is true, nor does it hold much value to claim it, unless your aim is to divide developers into arbitrary adverse tribes.
* All commands in one place, view them all with `just --list`
* Stupid-simple format
* Small standalone binary
* Configurable (with arguments, environment variables etc) but not _too_ configurable
When I see a git repo with a Makefile, I'm filled with dread. When I see a repo with a Justfile, I get warm fuzzies.
Some people say it just doesn't do enough to justify existing. These people are just wrong.
Another benefit is that justfile just cannot get too complex and tangled. Simplicity at its finest.
If anything, it's an argument for making better use of make's own features for configuration in the first place.
Make does dependency tracking relatively well (for 1976). But if you just want to run some commands, your shell already does that just as well, without any of the caveats that apply to make.
Why would anyone voluntarily subject themselves to that kind of insanity? :-))
Better to just use the hacksaw that is Make than all these Rube Goldberg contraptions :-)
That said, I used to write makefiles like this all the time, but have since switched to just and justfiles in recent years which make this the default behavior, and is generally simpler to use. Things like parameters are simpler.
i feel like we're due for some kind of newfangled coreutils distribution that packages up all the most common and useful newfangled utilities (just, ripgrep, and friends) and gets them everywhere you'd want them.
I notice just has some pre-built binaries that could be used for the same thing. I find it a little beyond rude what gradle normalized, but hey, it "works", and it removes the source of friction that's present any time you violate the principle of least surprise with your choice of build tool.
The properties file specifying the version and checksum is great, but we shouldn't need millions of identical copies of the binary itself checked into every repo.
This criticism might make sense for some non-vim editor because you might have to ssh into a remote location where you can’t install stuff. But if you should be able to build a project and thus install its required dependencies, then you might as well add one additional word to the install command.
Too late of course.
I agree fully with the OP, in particular I find it smart that he wraps anything in a top-level makefile, even if other, more sophisticated build tools are used. The advantage is standardization, not having to remember anything and to know that if you wrote it, you will just be able to type "make" and it will work.
Let's say a C person wants to compile a Rust project, they would not have to look up how cargo works, but could simply type "make" (or "gmake"; I don't use GNU specifics, but try to be POSIX compliant, even if it is certainly true that almost 100% of makes are gmakes).
Thanks for proposing the use of the timeless "make" as a sort of top-level build system driver; this will probably still work in 250 years.
Make on its own is great but most of the time I've worked with C projects it's been cmake/autotools + global pkg installs, which you Do have to frequently look up.
Except if you want to use some specific feature. Or specific log level. Or build a specific crate in a workspace. Or...
Except for the projects that would require something like "cargo build --feature=wayland" for example, in order to run.
So "cargo build" ends up not being universal, and adding make will make it just "make build" regardless of what flags people use with cargo, meaning it's more universal than "cargo build".
Avoids lots of weird makefileisims
I was not prepared to feel this old today.
Also the updated PDFs for my courses materials are automatically put into my websites source tree by another Makefile that I use to manage and build my teaching materials and which let me either build the PDFs I use from my LaTeX sources or build from the same sources alternate versions of the materials for my students (without solutions to the lab sessions exercises for example) and automatically publish those to my local website version to be uploaded whenever I want to deploy the updated website.
It's kind of Makefiles all the way down. I like Makefiles! =)
make as a build system is ok until you hit the warts.
- make/Makefiles aren't standardized, which is why automake exists. So now you're not writing Makefiles, but templates and generating the actual makefile. This doesn't matter if you own the whole toolchain, but most people don't, so this is what some folks do to guarantee their Makefiles are portable.
- make cannot do any kind of dependency resolution, it assumes that whatever you need is right there. That leads to configure scripts, which like makefiles, are not standard, so you use autoconf/autoreconf to generate the configure script that runs before you can even run a target with make.
- make (and adjacent tools like automake/autoconf/autorefconf) use mtime to determine if inputs are out of date. You can get into situations where building anything is impossible because inputs are out of date and running autoconf/autoreconf/automake/configure leaves them permanently out of date. (fwiw, many build systems can get away with using mtime if they can do proper dependency tracking)
All in all the fundamental design flaw with make is that it's built with the unix philosophy in mind: do one thing well, which is "rebuild targets if their inputs are out of date." However this is an extremely limited tool and modern build systems have to do a lot of work on top to make it useful as more than a basic task runner.
The ancient convention there is "make configure", which sets up whatever "make [build]" needs.
dependency:
...
target: dependency
...Ignorant's question: isn't dependency resolution the core of make? What are you referring to here?
It doesn’t directly interpret any source file though, if that’s what you mean.
Kudos to the author for writing this up and _not_ feeling the need to learn every last bit of make and do everything "properly" before sharing.
I've worked on a team where GitLab CI pipelines replaced Makefiles, and I was asked not to commit a makefile to the project because it's a customized developer workflow. They were allergic to local testing, but I thought it was a great way to just store and share knowledge about how to build, test, clean, etc. Far easier to read the GitLab CI files (which yes of course were also necessary and served a different porpoise).
The problem with the lack of a scripting language is that I either have to do horrible shell contortions to do simple things like using a temporary file in a recipe or write a standalone script that doesn't live in the Makefile which is needless indirection. This is exacerbated by Make interpreting newlines in the recipe as a separate shell invocation, which I consider a poor design choice. It also requires needless forking for many small tasks which could be done more efficiently in process.
The lack of proper recursion means that I either have to use recursive make, which is largely considered an anti-pattern, or I have to use a flat directory structure.
What Make does have going for it is ubiquity and good performance for small projects. It is the tool most projects should probably start with and only switch to something more advanced when its scalability issues become a genuine problem.
https://www.gnu.org/software/make/manual/html_node/One-Shell...
[1] https://pubs.opengroup.org/onlinepubs/9799919799/utilities/m...
As soon as I notice I'm reaching for anything more than `.PHONY` targets and dead-simple filename dependencies, I stop and do the real build work in something else (callable via a make target, of course!) I know how to do complicated stuff with make, which means that I know I will do it wrong. Repeatedly. Or possibly eventually do it right, but then have to maintain the resulting fire-breathing hairball.
(But to those complaining about not marking all the non-file targets `.PHONY`: lighten up. If the correctness matters so much that you're going to be messed up by a file named `all` or `build` or whatever, you've probably already gone too far down the rabbit hole and should switch to something else.)
That's not the problém. _We_ know what "dev is already up to dáte" means, but chances are people who don't know about `.PHONY` don't.
If you’re using a single entry point script (e.g. do.sh) and handling $1 you don’t get that for free.
And the moment you need to make your entry point script aware of “B requires A” then you’re going to half-bake something similar to make, anyway.
Here’s [1] my ~80 line version for Python projects (micromamba + uv) which I’ve been pretty happy with.
bin/ also perfectly tab-autocompletes, don't understand what you mean.
If you're having only one bin/do.sh script -- you're doing it wrong.
And if you know $1 can only be either "prod" or "dev", then create scripts build-prod.sh and build-dev.sh.
Simpler, installed on all systems, no quirky syntax with tabs and phonys.
In particular, I like:
* The ubiquity: it's easily available almost everywhere I touch and if not, it's usually a package install away. * The auto completion: I often define variables with default values at the top, but they can be both easily discoverable and their values can be changed just by typing `make VAR<tab>=foobar ...` * Chaining commands: make targets can be chained with `make target1 target2 target3 ...`. They will execute in the order specified. If I run this too often, I can usually create a 'ew make target that chains them all.
Make is definitely not perfect and could be simpler. My biggest griefs are:
* The abscons list of built-in variable. I can only remember a few of them ("dollar duck `$<` being my favorite), but they are not great to search in the docs and my brain is limited. * The "one line = one shell" is a bit of a PITA for the use case I have. I usually move more complicated script put of Makefiles.
I thought I would have been bitten by the M4 indentation more often, but it's not really a problem for me anymore at this point (my editor _knows_ Makefiles since it's so ubiquitous and does the right thing)
I have Just on my list of things to check, one day...
We probably all should. But it is valuable a few different ways to have everything in a single file.
Even though it's an ugly mess having a script within another script, with make variables mixed with shell variables mixed with make logic mixed with shell logic mixed with child proc output... the task at hand wants bits from both worlds at the same time, so the mess is also the value.
You can rig up alternative answers for each of those, but really it just means make should probably have a more generic scripting language itself so you don't have to leave it to get something done, like how you don't have to leave awk to do non-record-processing stuff. People don't, but you can basically write anything you want in awk just like in shell or python.
Or it should have less, be intentionally so limited that you can't embed any significant logic in the makefile and so it all lives somewhere else, either way, so long as it's all together, either all in the makefile or all in something the makefile runs.
Now I love Make, for non-C work.
The ability to create dependencies between targets is the cherry on top, but the main value is just the ability to create a cheat sheet of shell snippets accessible via `make <target>` from the root of the repo.
Such a makefile is always (1) version controlled as a secret github gist (though, as personal rule, i never hardcode secrets into it), (2) committed & pushed on `make` (3) git ignored via `.git/info/exclude`. This has worked quite well for me.
One downside with this approach is that the best syntax for passing parameters down to the target from the shell is to use environment variables, which is a little awkward. `NAME=value make target` is less pleasant than `make target --name=value` would have been.
NAME := foo
test:
echo "name is $(NAME)"
$ make test
echo name is foo
name is foo
$ make test NAME=bar
echo name is bar
name is barInspired by:
My latest gripe with it was its extremely poor support for filenames with spaces and special chars (;) in them: I just wanted to convert a flac library to MP3 files, it seemed well suited for the job at first glance.
I don't know what tool could be a proper replacement. scons perhaps? ninja files are too verbose. just doesn't do dependencies, job control, etc.
Any good tutorials or resources for learning that show a broader applicability for makefiles?
the philosophical "c" centredness is true, but doesn't get in the way of using other languages. (there's things like indirect rules for compiling .c files auromatically for instance, but even this can be turned off)
Well that's kind of wrong (it's used for that but that's an extremely limited viewpoint). Here's a short introduction to get started:
foo: bar
baz
This means "whenever bar has been updated, create or recreate foo by running baz". You run it with "make foo", and make will run "baz" by default in "sh".Here's an example in a totally different context:
.PHONY: build
build: node_modules
node_modules: package.json yarn.lock
yarn install
touch node_modules
With this, when you run "make build", it'll only do "yarn install" if node_modules's last-modified timestamp is older than both package.json and yarn.lock. The touch is there to mark it updated for the next time you run "make build", so it knows it doesn't have to do anything. Normally you wouldn't have to do that but make assumes the commands given will update the file, and "yarn install" won't necessarily update the directory's last-modified time.This example isn't terribly useful because "yarn install" is fast and doesn't do anything itself when it's up-to-date, but it should give ideas about how flexible make actually is.
One of the big criticisms of how people use "make", and why people recommend things like "just" instead, is they don't bother to use that functionality (or any of the piles of stuff built on top of it like pattern matching) and would have just done:
build:
yarn install
...which appears to be how OP uses it.A good detailed example; Using GNU Make to Manage the Workflow of Data Analysis Projects (pdf) here : https://www.jstatsoft.org/article/download/v094c01/1368
More generally, also take a look at how to use Unix Tools effectively, see; Unix : Concepts and Applications by Sumitabha Das and The Unix Programming Environment by Kernighan & Pike.
Here's a simple example to get you started. Create a file named "Makefile" with the following text and an empty file alongside it named foo.txt.
bar.txt: foo.txt
cp foo.txt bar.txt
When you run the "make" command in your shell it will check if the "bar.txt" file exists. If it does not exist OR if "foo.txt" has a newer timestamp, then it will rebuild "bar.txt" by executing the tab indented shell commands underneath. In this case, the only shell command used is the 'cp' command. In the linked article the author invokes npm, bundler, and netlify.When people use Make to compile their C code they are simply invoking the C compiler just like they would any other shell command. You might have seen something in a Makefile that looks like this:
foobar.o: foobar.c
gcc -c foobar.c
This is just saying: "The output file 'foobar.o' depends on the input file 'foobar.c' and to build 'foobar.o' run the shell command 'gcc -c foobar.c'" which is conceptually the same as my previous example were we built "bar.txt". Since explicitly listing every .o and .c file in a Makefile is tedious, many authors opt for wildcards.I hope this helped!
I got to this point and ran into an error: Makefile:2: *** missing separator. Stop.
Ok I'm just giving you a hard time, and you mention right after the existence of "tab indented" so whatever. Still it's one of the things I detest on an aesthetic level about make, even if my editor has special syntax support for makefiles to handle this archaic requirement of actual tabs without me ever having to worry about it in practice.
There's a little-known variable called .RECIPEPREFIX that lets you switch from tabs to anything else. Probably a bad idea to use it in anything shared with anyone else.
[1] https://pubs.opengroup.org/onlinepubs/9799919799/utilities/m...
Nah, who am I kidding ... no one loves yaml, it's just better than most of the alternatives.
It allows you to write something similar to a Makefile, but everything runs in Docker. This gets you isolated builds with parallelism and caching built-in.
I've found it to be great especially for small to medium projects. For some examples, I use it to publish my personal site/blog [1] and to build a C/C++/Fortran/Rust cross-compiler targeting macOS [2].
[0]: https://earthly.dev/
[1]: https://github.com/shepherdjerred/sjer.red/blob/main/Earthfi...
[2]: https://github.com/shepherdjerred/macos-cross-compiler/blob/...
You should always .PHONY any non-file targets though lest you want your build to break suddenly and confusingly when you add a folder named say "build" in the authors case.
Make checks file modification dates by default to see if it can eliminate steps. Setting a target as a .PHONY indicates it's a "fake target" as in a command set to be run and not a file to be written. Makes default assumption is it's being ran to "make a file" so "make foo.html" or such.
Here's a very simple example
The only thing is, this isn't what make is actually for. A number of commenters have recommended Just - the one I've been using on my personal projects is Task - https://taskfile.dev/ - which is pretty great. As other commenters have said, the problem is that make is installed everywhere already. I would love to see a task runner become standard to the same extent, and will have a look at Just if that's the one people are using.
As the article said, it's generally installed everywhere as soon as you install any dev-related stuff. So is bash, but it's a little clunkier for very basic usage. (`if [[ $1 = build ]]; then...`)
Put complex stuff in scripts and call script from makefile.
Even if your project needs a more sophisticated build system that requires caching builds and managing conditional dependencies, turborepo and the like offer even better support for javascript codebases out of the box. Turborepo can be a workspace dependency so technically you don't even have to manually and separately install a build system to build your project.
* Already installed
* Does not require some random runtime
* Does not require some random runtime version
* Same build system for (nearly) all languages
* The dependency tree can be constructed incrementally from simple, composable, verifiable building blocks
* Incremental builds are trivial
* Task bodies are (basically) shell scripts
* Isolated complexity can be moved to external shell scripts and called from a task body
* Unlike fad-language-build-systems learning is amortized across decades
* It's not CMake
.PHONY: targets
targets:
@make -qp | awk -F: '/^[a-zA-Z0-9][^$#\/\t=]*:([^=]|$$)/ {split($$1,A,/ /);for(i in A)print A[i]}' | sortThe problem is when people try to expand them to everything, and they end up some arcane file full of junk nobody understands. What is a phony target anyways? And even if you understand the concept, realize it makes no sense to passerby.
This is an entry level bar to the profession, one level below that will be an insult to anyone who calls themselves an engineer.
If you can't be bothered to learn the basics of the language, your criticism isn't going to be worth much.
Not sure how the two relate at all. One is a statement terminator, the other a rather complex system.
> If you can't be bothered to learn the basics of the language, your criticism isn't going to be worth much.
That's the entire point! Remember my complaint is not against Make as a build tool, but Make as a do everything(including deploy) system. There are countless options in the space. With most, you can generally open a config file or even bash script and figure out what's going on. Not so with Make, without learning as you call it. It's nonobvious. And I say that as someone who -does- understand it.
https://podcasters.spotify.com/pod/show/podgenai/episodes/Ma...
Will give it a listen tho!
Having said this, after having listened to 100+ episodes of this podcast, I have yet to spot a single lie, although they may still exist with a very small probability. The reason why any fact checking step has not been added to this podcast is because it hasn't proven to be too necessary in the first place.
I have a different project "newssurvey" which uses external data and has citations for claims, although I still need to add an extra citation verification step for it. Perhaps in time I will.
That said, I wish there would come a new tool that could make things easier, that would gain wider appeal and adoption.
And kudos for this article that gives a point of view, then fixes the main HN concern ;-)
Person puts short shell scripts "build", "deploy" and "dev" in a single makefile as targets, instead of putting each script in a separate well named file. Then concludes that make is a great build tool as long as you don't use it as a build tool ("nothing complicated").
Please do not do this. make is not a shell. Also, consider reading (and writing) build docs, instead of running make with random targets as soon as you see a makefile
That being said, the only issue preventing me from using shell scripts well is shared code. Like common functions etc. So far I've simply used a common.sh script and sourced/imported it.
Also, what do you do once you have 4-5 shell scripts at the root of your project? I find it gets messy quickly. I need a solution for this.
But yes, I agree shell scripts are generally better.
Put them in a folder! I see "tools" used commonly for dev/build scripts
Everyone has their favorite python modules they like to use in scripts, half of them are not built or wrong version for whatever current distro I'm in, so I install stuff with pip, then something with requirements.txt breaks because of wrong pytorch version, I start reading about envs, much crying.
At least with bash or make nobody expects you to provide external modules/libraries
A build script should work with vanilla out the box Python. No extra modules needed. I mean if you can assume vanilla bash tooling then vanilla Python is perfectly fine.
If there’s some Python library you really want then add it to repo. Easy peasy.
The benefit is Perl is robust, unchanging for decades at a time, and available on pretty much every system. Bash is not portable, Perl is.
I dream of a world where a "lessons learned make" is installed everywhere. How do we get that to happen?
That's the pitfall that so many would-be build system authors fall into: _Holy crap, why is it so complicated? Surely something simpler would do the job... [YEARS LATER]... Shit, my build system is so ugly because of all those damn real-world edge cases I've had to fold into it._
When even a simple one misbehaves, like it almost always does, I get to spend a solid chunk of time ignoring my actual goals to play with a bit of computing history.
And then I get to do it all over again a month later, when it breaks in yet another new way.
https://www.matheusmoreira.com/articles/managing-dotfiles-wi...
The sanest way I've found to write makefiles is to think of it as a tool that maps input paths to output paths.
When compiling a program, I want to map source/program.c to build/$(config)/objects/program.o so I write pattern rules that do just that.
$(directories.build.objects)/%.o: $(directories.source)/%.c | directories
$(strip $(CC) $(CFLAGS) -o $@ -c $<)
Then I write make functions to convert paths in some tree to paths in another tree, which makes it easy to create lists of files which then match the pattern rules. source_to_object = $(patsubst $(directories.source)/%.c,$(directories.build.objects)/%.o,$(1))
These functions are then augmented by lots and lots of project specific variables to organize things... directories.source := source
directories.build.root := build
directories.build := $(directories.build.root)/$(config)
directories.build.objects := $(directories.build)/objects
sources.program := $(shell find $(directories.source) -type f)
objects.program := $(call source_to_object,$(sources.program))
Then I add a real target which is supposed to get the makefile going. targets.program := $(directories.build)/program
$(targets.program): $(objects.program) | directories
$(strip $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^)
Then I add a phony target for it. targets.phony += program
program: $(targets.program)
.PHONY: $(targets.phony)
Then typing "make program" triggers the rule to make build/$(config)/program which depends on all the object files which will be linked into it which in turn is derived from a dynamically computed list of source files, and making those object files triggers the pattern rule which builds the entire thing.Using make without phony targets is insane. Without phony targets, I'd need to type things like "make build/aarch64/program" in order to get a build started. So I use phony targets for everything. Much easier to type "make program".
It got to the point I created a phony-targets shell script which parses make's database output and processes it into a sort of help text for any given makefile's phony targets interface:
https://github.com/matheusmoreira/.files/blob/master/~/.loca...
I love Make because it largely delivers on its promise -- and I am using it almost in _opposite_ to what the author describes. That is, I consider phony targets to be an "illegitimate" feature of Make, and avoid them like the plague. While convenient, targets in Make are heavily geared to be files, certainly most of the machinery in Make was written to assume so, and even the well-known (and documented) targets like "install" and "clean" leave a terrible taste in my mouth as of late, despite these being very conventional.
The problem with phony targets is that they're hard to reason with by Make (unless you actually turn "install" and "clean" into files) and break half of the rest of its assumptions on what targets should be and how they behave. The rest of the problem is the linguistical aspect of it -- if I `make install` am I making an install program/script or what? These kind of vagaries have led me firmly away from ever using phony targets.
As for the rest of it, Make is terribly archaic, but that also lends it strength since the archaic nature is very simple on the flip side.
The "hate" part is me taking a dislike to its bare-bones, in my opinion insufficient variables facility, where only truly global variables (certainly sharing one namespace) exist and juggling these for any non-trivial Makefile is always a problem of its own.
I am no novice with GNU Make, not any longer, but occasionally I need to look back into its manual to remember how e.g. the so-called double-colon rules work (when I suspect I might need one) and the real difference between the `=` and `?=` variable assignment, for when I want to provide default values and so on.
Lately I've been playing with the idea of _implementing_ a [GNU] Make compatible tool just to see if the somewhat _patchy_ scope of [GNU] Make can be given more coverage -- for instance to see if adding locally-scoped variables and more flexible target definition can improve Make? What I mean is to experiment with an implementation that retains fundamental principle and mandate of Make -- dependency graph resolution and reliance on [normally] UNiX shell -- but "upgrading" the syntax and extending the semantics. Because while Make is useful, it's also at times terribly hard to work with. To paraphrase Bjarne Stroustrup (the man behind C++), "inside Make there is a neat sound idea struggling to get out".
CMake is a different beast, really. While both have in common that they're build automation tools, you can say, save for some shared ideas, they aren't really that similar once you zoom in past some level of detail. Meaning I hardly can choose to adopt CMake _instead_ of writing a Make-derivative if my goal is to _extend_ [GNU] Make. And I have reasons to prefer Make over CMake, so I am absolutely not interested in extending CMake (or acknowledging it has fit my needs and/or is aligned with the way I like to solve problems I have used [GNU] Make for solving).
You could say that CMake is the same as [GNU] Make beyond their different syntax, which is true in a sense, but syntax does decide a lot for each respectively, I would say. The fundamental syntactical differences between the two become larger as one walks the abstraction ladder upwards, and looking at each from the perspective of a user (tasked with, say, building a large C++ program/library), one has to adopt slightly different set of concepts specific to each. To that end, I prefer Make's abstractions over CMake's.
Last, the value of my implementing a [GNU, henceforth implied] Make compatible tool, wasn't just for forking an improvement, but also in that when I have written a sufficiently capable fork, say, I can assess _how_ Make was made to work, in my experience one tends to learn a lot about what a piece of software writing a compatible "emulator". I _can_ read Make's source code, but I really don't want to because what I have seen suggests the kind of "organic development" that no longer ideally resembles something an outsider would find easy to grok, even a C expert. It's just the way of those things, unfortunately. Instead, I could pick up Python and write a very bare-bones Make-compatible implementation that would give me a lot of answers for "why does Make work like this?" questions.
Just not this one.
It's there, but last I checked, it only supports building with the Apple-supplied toolchains (Xcode). If you want to use anything else, do yourself a favor and install gmake(1) from MacPorts.
For C/C++ projects that are simple enough, I can reuse the same Makefile with only minor changes, and it works on both Linux and macOS, with support for sanitizers and valgrind(1). CLion eats it up like it were candy, although the gathering of sources is automated with find(1), and even respects my `--sysroot` setting.
CMake has good cross-platform capabilities, but GNU make gives me more control over the actual compiler and linker invocations, and its CLI is less horrible than that of CMake.
It is supposed to make the building process more straight forward and painless but what it brings to the table is
* Weird bugs: In one version of cmake would return the python version that I had to be 0.16 (????) and the only fix was to update the cmake version
* Messy Structure: Under the guise of giving freedom to the person designing their build with no real enforcement of any rules, in any way, people can write their own absolute craptastic version of cmake scripts which really really is going to make you lose your mind (mind you some people leverage it but it is the exception not the rule)
* Opaque process: Due to the fact that the structure can be messy you are never really quite sure of what it is doing and which compile flags are effectively fed to the compiler as well as sometimes totally ignoring the library you explicitly ask it to use and trying to find another one...
OK maybe this is a bit superlative, but unfortunately for me, reflects well my experience with using cmake and feels like an extra item between me and getting things working painlessly... But I might have been doing it wrong.
* “Weird bugs”: I suspect CMake wasn’t finding the version of Python you wanted because your find_package command was just finding a different version first. This is much easier to debug today by using —-debug-find-pkg=Python, which will print all the places it searches Python for and what it finds. You can then modify your find_package invocation as appropriate to find the Python you really want to use.
* Messy structure: yes, unfortunately I’ve also seen my fair share of nightmare-inducing CMake files.
* Opaque process: here I actually quite disagree. In my experience I’ve found it super easy to modify things like compiler flags (just use target_compile_options, or add_compile_options for directory wide options). And what made a big difference was using Ninja as the generator (I also use Ninja on Windows), which makes it super easy to view the final compiler commands that will actually be invoked. CMake is essentially a compiler that emits Ninja on the backend, and several times it’s been invaluable to confirm in the generated Ninja code what is actually being invoked.
CMake is definitely not perfect, but it’s much better than what it used to be! It’s ultimately a perfect match for C++ (both extremely powerful, configurable, hamstrung with decades of backwards compatibility, terrible ergonomics, etc.).
It's such an ubuiquitous gormat Llm are well trained on them.
So you don't have to read or edit it, just let the robot do it for you. It will do it well, and it's not something you do often anyway.
Then you can enjoy the other qualities of the tool, like the fact it's already there on unix, or that it's more than a task runner.
Although I prefer "just" or "doit", in 2024, make is fine again.