Learn Makefiles
108 comments
·June 20, 2025bsenftner
Way back in the dark ages of 1985, I encountered a guy at the Boston University Graphics lab that was using Makefiles to drive the generation of a 3D renderer for animation. He was a Lisp guy, doing early procedural generation and 3D actor systems. His Makefile was extremely elegant, about 10 lines total. It generated hundreds of animations, all based on the simple file date dependency. He had Lisp generating the 3d form for each frame, and them Make would generate the frames. This being '85, pre pretty much everything we take for granted with 3D and animation, the guy was blowing everyone's mind. He went on to write the 3D renderer for Iron Giant, and was key in Caroline too, I seem to remember. Brian Gardner.
agumonkey
This guy http://3d-consultant.com/bio.html ?
stabbles
A couple make flags that are useful and probably not very well known:
Output synchronization which makes `make` print stdout/stderr only once a target finishes. Otherwise it's typically interleaved and hard to follow:
make --output-sync=recurse -j10
On busy / multi-user systems, the `-j` flag for jobs may not be best. Instead you can also limit parallelism based on load average: make -j10 --load-average=10
Randomizing the order in which targets are scheduled. This is useful for your CI to harden your Makefiles and see if you're missing dependencies between targets: make --shuffle # or --shuffle=seed/reverse
blueflow
> and probably not very well known
Maybe the make authors could compile a list of options somewhere and ship it with their program, so users could read them? Something like a text file or using some typesetting language. This would make that knowledge much more accessible.
moefh
Not sure if you're being snarky, but the manual has a list of all options accepted by make: https://www.gnu.org/software/make/manual/html_node/Options-S...
(`make --help` will only print the most common options)
tux1968
make --help
Will give you the command line options. And GNU make has decent documentation online for everything else:
https://www.gnu.org/software/make/manual/html_node/index.htm...
monkeyelite
> On busy / multi-user systems
Can’t the OS scheduler handle it?
f1shy
The one that I most use is -B for unconditional build all
davemp
I’ve seen and had ‘make -j’ dos machines enough times that I consider it a bug.
bayindirh
If "make -j" successfully drowns a machine, I can argue that the machine has no serious bottlenecks for the job. Because, make is generally I/O bound when run with high parallelism, and if you can't saturate your I/O bandwidth, that's a good thing in general.
However, if "make -j" is saturates a machine, and this is unintentional, I'd assume PEBKAC, or "holding it wrong", in general.
davemp
The problem is ‘make -j’ spinning up 100s of C++ compilation jobs, using up all of the systems RAM+swap, and causing major instability.
I get that the OS could mitigate this, but that’s often not an option in professional settings. The reality is that most of the time users are expecting ‘make -j $(N_PROC)’, get bit in the ass, and then the GNU maintainers say PEBKAC—wasting hundreds of hours of junior dev time.
holsta
> A couple make flags that are useful [..]
But not portable. Please don't use them outside of your own non-distributable toy projects.
deng
I will not restrict myself to an arcane subset of Make just because you refuse to type 'gmake' instead of 'make'. Parallel execution, pattern rules, order-only prerequisites, includes, not to mention the dozens of useful function like (not)dir, (pat)subst, info... There's a reason why most POSIX Makefiles nowadays are generated. It's not GNU's fault that POSIX is stale.
EDIT: There's one exception, and that would be using Guile as an extension language, as that is often not available. However, thanks to conditionals (also not in POSIX, of course), it can be used optionally. I once sped up a Windows build by an order of magnitude by implementing certain things in Guile instead of calling shell (which is notoriously slow on Windows).
Tor3
Agreed. My company decided on using GNU Make on every platform we supported, which back then (last century) was a bunch of Unix variants, and Linux. That made it possible to write a simple and portable build system which could be used for everything we did, no hassle. And not difficult, because gmake was available basically everywhere, then just as now.
matheusmoreira
Completely agree. POSIX is irrelevant anyway. Every single unixlike has unique features that are vastly superior to whatever legacy happens to be standardized by POSIX. Avoiding their use leads to nothing but misery.
stabbles
The guide is basically about GNU Make, and the flags are obviously just for end users to invoke make.
f1shy
Not every project has to be a multi-platform, multi-os, multi-language monster. It is perfectly fine to target a specific set of architecture, os, etc. And I find insulting and silly calling it a “toy project”
nrclark
Agreed if you're looking at it through the lens of portable software that you plan to distribute. Automake generates portable Makefiles for a reason.
But there's another huge category: people who are automating something that's not open-source. Maybe it stays within the walls of their company, where it's totally fine to say "build machines will always be Ubuntu" or whatever other environment their company prefers.
GNU Make has a ton of powerful features, and it makes sense to take advantage of them if you know that GNU Make will always be the one you use.
matheusmoreira
Portability is overrated. Better to make full use of one's tools. Restricting oneself to some "portable" subset of all features is pure masochism.
GNU Make is feature rich and is itself portable. It's also free software, as in freedom. Just use it.
Tor3
And it's available everywhere. All Unix platforms had it back then, and the still existing ones (AIX is alive, at least) have it available. Which made it easy for our company to base our build system on GNU Make for everything, back in the day.
f1shy
Not only overrated, but also the source of extreme complex and gigantic pieces of software, which end being a nightmare to keep updated.
Just like optimization, it has its place and time.
signa11
exactly ! instead of writing portable Makefiles, use portable make !
leetrout
The article says most people don’t mark recipes as .PHONY and seems to use that as a reason to not bother in the tutorial. I think that is a weak excuse and we should teach the right way to use a tool.
My teammates gave me a hard time for adding and maintaining .PHONY on all our recipes since we use make as a task runner.
Clark Grubb has a great page explaining a style guide for make files:
https://clarkgrubb.com/makefile-style-guide
Does anyone else use this style guide? Or for phony recipes marking phony at the recipe declaration vs a giant list at the top of the file?
I would love to have a linter that enforced this…
stabbles
Another thing that's interesting lately is that CMake has decided that Makefiles are unfit for projects that use C++20 modules, and ninja is the way to go. [1]
Basically it's considered too hard if not impossible to statically define the target's dependencies. This is now done dynamically with tools like `clang-scan-deps` [2]
[1] https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules....
[2] https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...
nrclark
Modules are a disaster tbh.
dgan
can you expand on that?
alextingle
If you can't easily reason about dependencies, then your builds will just get more and more bloated.
People who care about build systems are a special kind of nerd. Programmers are often blissfully ignorant of what it takes to build large projects - their experience is based around building toy projects, which is so easy it doesn't really matter what you do.
In my experience, once a project has reached a certain size, you need to lay down simple rules that programmers can understand and follow, to help them from exploding the build times. Modules make that extra hard.
monkeyelite
Nobody implemented them except Msft
danw1979
Make has its place as a build tool for large C codebases.
People sometimes treat it as a generic “project specific job runner”, which it’s not a good fit for. Even simple conditionals are difficult.
I’ve seen several well-intentioned attempts at wrapping Terraform with it, for example, which have ended terribly.
monkeyelite
It’s not a generic job runner. It’s a generic way to transform linear shell scripts into declarative dependencies. It’s a general tool for the shell.
creata
Is there a good generic job runner?
Edit: Sorry, it looks like I totally misunderstood what you meant by "job runner".
homebrewer
Sure, a bash script.
People keep writing and using other alternatives (like just), which provide a very slight improvement on pure shell at the cost of installing yet another tool everywhere.
I stick with bash, write every task as a separate function, and multiplex between them with a case statement (which supports globs et al. and is very readable).
EPWN3D
Years ago, I discovered git-rev-parse's option parsing, and it completely removed any excuse I had not to write my own personal bash scripts to a professional standard.
Now when I need a tool, I can knock it out in bash with proper option parsing, usage, etc.
bash is awful on a lot of fronts, but if you're writing code that's primarily calling a bunch of tools and mucking with their output, it's still the best thing out there I've found just due to piping syntax.
dakom
Taskfile and Justfile are pretty solid.
llukas
This is excellent modern replacement for part where Makefiles get messy: https://github.com/casey/just
thristian
It replaces the "list of short shell-scripts" aspect of Make, but it doesn't replace the "only execute rules that need to be re-executed" part, which is the actually useful bit.
ajross
This is the most frustrating bit of this weird recursive ecosystem of build tools. No one really uses all of make, so they only clone the bits they need, so their tool is simple and clean and beautiful to a subset of the community that has their same problem. But it can't replace make, so seven months later someone with a slightly different problem shows up with a make replacement, and the circle of life continues.
And you see this on the other side of the problem area too, where large and ugly tools like cmake are trying to do what older large and ugly software like autotools did, and trying to replace make. And they suck too.
I continue to believe the GNU make in the late 80's was and remains a better generic tool than everything in the modern world in all ways but syntax (and in many cases, again c.f. cmake, it had better syntax too). Had the original v7 syntax used something other than tabs, and understood that variable names longer than 1 byte were a good thing, we might never have found ourselves in this mess.
amelius
Sounds good. If it isn't broken, don't fix it.
izabera
they do place themselves as an alternative to make, but imho they're entirely different and not at all comparable. make is centered around creating artefacts and not rebuilding what is already built. just is a command runner.
PhilippGille
Or:
- Task (Go): https://github.com/go-task/task
- Cake (C#): https://github.com/cake-build/cake
- Rake (Ruby): https://github.com/ruby/rake
Or an entirely different concept: Makedown, as discussed on HN 8 months ago: https://news.ycombinator.com/item?id=41825344
izoow
The main benefit I see with using Make as a command runner is that it's a standard tool that's installed "everywhere". Even though these replacements seem nicer to use, I never felt like they bring enough to the table to warrant having to install an extra tool.
syklemil
I also use just as a command runner, but I gotta agree with the others here that it should be described accurately as a command runner, while make is a build system.
There are some uses of make, especially by people who have never used it to build C/C++ projects, which makes more sense to replace with just. It doesn't have the baggage that make does, and they're not using it to actually make files. They also quite likely don't know the conventions (e.g. what a lot of us expect "make install" to do), and I support them in not learning the conventions of make—as long as they use something else. :)
Other uses of make will need other modern replacements, e.g. Cmake or Bazel.
It is possible that Kids These Days can say "no thanks" when someone tries to teach them make, and that the future of make is more along the lines of something us greybeards complain about. Back in _my_ day, etc.
Lyngbakr
Task* is another alternative, although I admittedly only use it with simple hobby projects in C so I can't speak to whether it scales well or not.
jekwoooooe
In 2025 makefiles are once again only for C projects at best. For task running, use just or mise.
pards
> Note: Makefiles must be indented using TABs and not spaces or make will fail.
Oh no. I have never worked with Makefiles but I bet that causes pain and suffering.
I've lost so many hours to missing/extraneous spaces in YAML files that my team recently agreed to get rid of YAML from our Spring Boot codebase.
PhilipRoman
Do people not have different symbols for spaces/tabs in IDEs? I see people committing mixed or trailing whitespace but every editor I've used shows spaces and tabs clearly.
Agree about yaml though. I still have to look up how to align a multiline string every single time.
ho_schi
Aside from the editor thing. I indent usually with Tab :)
I learned Makefiles a bit, using it in one tiny project. Than checked Autotools and everything in my brain refused to learn this awkward workaround-engine. At the same time Meson[1] appeared and the thing with Builds, Dependencies and Testing is solved :)
PS: Dependency handling with Meson is awesome.
f1shy
A no-issue if you use a half decent editor, same as python is not a problem today.
izabera
this is literally never an issue because every editor automatically uses tabs for makefiles
thesnide
actually requiring tabs is a godsend. no more off-by-one-space error
bitwize
An editor that groks Makefiles will help immensely, as it will ensure that the TAB key does the right thing. Emacs is good at this.
Of course the real solution is: just use CMake, you dweeb.
Joker_vD
You know, for small-ish C projects I found that the easiest way to handle "which .h files do the .c files depend on" question is to just say "on all of them".
SOURCE_FILES := $(wildcard $(SRC_DIR)/*.c)
HEADER_FILES := $(wildcard $(SRC_DIR)/*.h)
OBJ_FILES := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SOURCE_FILES))
.PHONY: build clean
build: $(BUILD_DIR)/$(TARGET)
clean:
rm -rf $(BUILD_DIR)
$(BUILD_DIR):
mkdir $(BUILD_DIR)
$(BUILD_DIR)/$(TARGET): $(OBJ_FILES) | $(BUILD_DIR)
$(LINK.o) $^ $(LDLIBS) -o $@
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(HEADER_FILES) | $(BUILD_DIR)
$(COMPILE.c) $< -o $@
So when you don't fiddle with inter-file/shared interfaces, you get an incremental rebuild. When you do — you get a full rebuild. Not ideal, but mostly fine, in my experience.P.S. I just love the way that Make names its built-in variables. The output is obviously $@, but can you quickly tell which of $^ and $< give you only the first of the inputs? What about $> and $∨, do you remember what they do?
EPWN3D
You can have gcc and clang output dependency files that your Makefile can include. Those are targets which will tell you which headers a source file depends on.
Joker_vD
I know I can do that. But it's fiddly, and doesn't really save time for small to medium projects: on small projects, full recompilation is fast enough that the time to regularly re-run "gcc -MMD" is actually noticeable and wasted — it simply is faster to not bother with it. And for medium projects, in my experience, the headers tend to not change all that often, and when they do it means you need to rebuild about 30-50% of all the sources so might as well rebuild 100% just to be on the safe side. I've had enough pitiful debugging experiences where the executable code does not match the source files that are fixed by doing "make clean build".
And when you change flags/compiler versions/system header versions you still need to a clean rebuild, so unless you write your makefiles the way that e.g. CMake generates them (I am willing to bet nobody does that)...
zwp
I used to like having a "depend" target to make the dependencies explicit and so minimize build time, although that fiddles with the contents of the Makefile (some discussion at https://wiki.c2.com/?MakeDepend).
The standalone makedepend(1) that does the work is available in package xutils-dev on Ubuntu.
andreynering
I'm the creator and one of the maintainers of an alternative to Make: Task.
It has existed for 8+ years and still evolving. Give it a try if you're looking for something fresh, and don't hesitate to ask any questions.
ulbu
and another alternative: just
TickleSteve
Stop with the alternatives... just use make for this task.
Seriously. :o)
null
dakom
Funny coincidence, I use this often and just opened an issue earlier today: https://github.com/go-task/task/issues/2303 :)
hahn-kev
Thank you for making it! We love it
amelius
Does anyone have experience with tup?
https://gittup.org/tup/ex_dependencies.html
It is a build system that automatically determines dependencies based on file system access, so it can work with any kind of compiler/tool.
I was a little surprised by this bullet point for when make would be an appropriate build tool:
> The build system does not need to be highly portable.
I know "highly" is a vague qualifier here, but I pretty much always default to a Makefile in Go projects and have used it to build Electron apps on Linux, macOS, and Windows (without WSL, just Make for Windows). You have to do a little extra finagling to get the executable paths right, but it works well enough for my purposes.
To some extent, I get why Make gets a lot of hate. But if you keep them simple, they provide a great way to get around some of the limitations of package.json scripts (e.g., adding comments).