from Hacker News

Learn Makefiles

by dsego on 6/20/25, 8:05 AM with 118 comments

  • by bsenftner on 6/20/25, 11:07 AM

    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.
  • by stabbles on 6/20/25, 9:24 AM

    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
  • by leetrout on 6/20/25, 12:13 PM

    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…

  • by danw1979 on 6/20/25, 11:01 AM

    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.

  • by llukas on 6/20/25, 10:36 AM

    This is excellent modern replacement for part where Makefiles get messy: https://github.com/casey/just
  • by amelius on 6/20/25, 12:21 PM

    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.

  • by stabbles on 6/20/25, 9:36 AM

    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...

  • by o11c on 6/20/25, 4:02 PM

    There are some dangerous and subtle problems with this tutorial.

    In order to handle long options and empty short options, when searching MAKEFLAGS you really need to do:

      ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))
    
    If you need compatibility with the ancient version of `make` shipped by default on OS X, not that it is missing a lot of functions and features, some subtly.

    Most other problems are either obvious typos or due to violating best-practice style, so I won't go into them.

    Note also that `load` is actually more portable than `guile`, but make sure you use the correct compiler in case you're cross-compiling the main project.

    ***

    Really you should just read Paul's Rules of Makefiles [1], then the GNU make manual [2]. Some of the other manuals related to GNU build tools are also relevant even if you aren't using the particular tool. I also have a demo project [3] if you just want something that works and isn't complicated.

    [1]: https://make.mad-scientist.net/papers/rules-of-makefiles/

    [2]: https://www.gnu.org/software/make/manual/

    [3]: https://github.com/o11c/makefile-demo

  • by andreynering on 6/20/25, 12:03 PM

    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.

    https://taskfile.dev/

    https://github.com/go-task/task

  • by articsputnik on 6/20/25, 12:13 PM

    I love my Makefiles. All my github repos contain a Makefile as I always forget the commands for every repo. Like this I have it stored nicely, but I can also add complex steps and run on each of my projects `make` and it will do what I'd expect without remembering any cmds.
  • by pards on 6/20/25, 10:50 AM

    > 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.

  • by jekwoooooe on 6/20/25, 1:28 PM

    In 2025 makefiles are once again only for C projects at best. For task running, use just or mise.
  • by codelikeawolf on 6/20/25, 2:57 PM

    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).

  • by Joker_vD on 6/20/25, 11:57 AM

    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?

  • by circadian on 6/20/25, 12:40 PM

    Very glad to see a tutorial like this. Make is something I've used relentlessly because it just works, but I know I'm missing out a lot more that it can help with because of my feeling that the docs are inaccessible. Knowing that this is here waiting for the day when a project calls for something just a little more means I won't bloat out my development workflow. Something a little more friendly than the make docs themselves lowers the barrier for me, nice one! :)
  • by buserror on 6/20/25, 9:37 AM

    Very nice article, seems to mention all the modern bits that helps making makefile so, SO much easier than in decades past...

    The interesting bits are for example the -MMD flag to gcc, which outputs a .d file you can -include ${wildcard *.d} and you get free, up to date dependencies for your headers etc.

    That and 'vpath' to tell it where to find the source files for % rules, and really, all the hard work is done and your 1/2 page Makefile will stay the same 'forever' and wills still work in 20 years...

  • by donatj on 6/20/25, 10:46 AM

    Generally speaking, it would be nice if the examples had simple execution examples like

        $ make foo
        Hello foo
        This ran too!
    
    That's a contrived example, but some of these take a bit too much thought parsing the example Makefile alone to understand the execution order and rule selection.

    It would just be very helpful to have clear examples of when I run this, I get this.

  • by globular-toast on 6/20/25, 10:59 AM

    Make is one of those things that I'm really glad I learnt at the beginning of my career. Not because I use it much any more, but because it showed me the power of a declarative system over an imperative one.

    I also realised at one point how naturally the idea extends to other tasks that I do. Going by the picture at the top of this site, it seems the author realised a similar thing to me: you can understand food recipes better if you think about them declaratively like makefiles, rather than imperatively like scripts, which is how recipes are traditionally written down.

    I wrote about it here: https://blog.gpkb.org/posts/cooking-with-make/

    I always scribble down recipes in a way that I can read like a Makefile and take that into the kitchen with me. I'm curious if anyone has tried typesetting or displaying recipes in this way as I feel like it would save a lot of time when reading new recipes as I wouldn't have to convert from a script to a makefile myself.

  • by torcete on 6/20/25, 1:03 PM

    In bioinformatics I used to use snakemake or perhaps nextflow. I wonder if makefiles could be use with the same effectiveness.
  • by uncircle on 6/20/25, 11:13 AM

    > it's specifically written for GNU Make, which is the standard implementation on Linux and MacOS

    Is this true? Doesn't macOS ship with BSD make?

  • by donatj on 6/20/25, 10:37 AM

    I have been working with Makefiles for over a decade, though never with C nor C++

    I knew there was a lot of weirdness and baggage but I am frankly kind of horrified to learn about these "implicit rules" that seemingly automatically activate the C compiler due to the mere presence of a rule that ends in ".c" or ".o"

  • by matheusmoreira on 6/20/25, 9:29 AM

    Makefiles are great but do try not to get carried away. Years ago I tried to create a pure GNU Make framework, only to realize I was effectively reinventing autoconf. That was the moment I finally understood what the GNU autotools had been made for.

    Makefiles are eerily lisplike turing tarpits. GNU Make even has metaprogramming capabilities. Resisting the urge to metaprogram some unholy system inside the makefile can be difficult. The ubiquitousness of GNU Make makes it quite tempting.

  • by signa11 on 6/20/25, 9:19 AM

    why single meson out ? infinitely better than most alternatives mentioned in the article.
  • by kjgkjhfkjf on 6/20/25, 10:33 AM

    Why would you use make for a C or C++ project when bazel exists?
  • by claytonaalves on 6/20/25, 11:09 AM

    I work with Makefiles on Delphi/FreePascal projects.
  • by akoboldfrying on 6/20/25, 12:10 PM

    Looks beautiful, but I got as far as the first dependency graph illustration (yellow and light green) and noticed that the final target is a source file, main.cpp, when it should almost certainly be a binary ("main.exe" or simply "main"). Similarly, one.cpp does not depend on one.h in the way that make cares about.

    Make cares only about how to generate files from other files.

    I point this out because this is one of the classic misunderstandings about dependencies that beginners (and sometimes old hands) make. The code inside main.cpp might well depend on code in one.cpp to work, but you don't generate main.cpp from one.cpp et al. You generate the final binary from those other files, and that's what make cares about.

    One right way to show it would be to have one.o depend on both one.cpp and on one.h (yes, this is confusing at first), likewise for two.o, and main.exe depend on one.o, two.o, libc and libm. Another way would be to omit the object files completely (as now), and just have main.exe depend directly on all other targets, but this makes for a less helpful example.

    ETA: I'd also appreciate it if you would mention in the "Recursive use of make" section that calling make recursively is a Bad Idea [0]. (Why? In short, because no dependency information can cross that process barrier, so there's always a risk that you don't build things in the right order, forget to build something you should, etc. If you have a hierarchy of projects in a subdir hierarchy, it's much better to use a separate "Makefile fragment" file in each subdir, and then "include" them all into a single top-level Makefile, so that make has a chance to ensure that, ya know, things get built before other things that need them.) I realise the GNU docs themselves don't say so, and GNU make has a ton of hacks to accommodate recursive make (suggesting that this is a blessed way), but that is simply unfortunate.

    [0]: "Recursive Make Considered Harmful", https://accu.org/journals/overload/14/71/miller_2004/

  • by 90s_dev on 6/20/25, 1:02 PM

    I remember learning how simple and pure Makefiles were back in about 2010, and then running into so many bumps that a friend recommended CMake. But usually CMake just uses Make under the hood anyway. I really do like this idea of "build a small, simple, and robust layer, and if it's not good enough, build another layer on top of it." But I do wonder what life would be like if we started from scratch by merging all these layers. Would it be Cargo? Probably not, I doubt it's as flexible as all these layers combined.
  • by revskill on 6/20/25, 11:58 AM

    Makefile is bad like python due to space sensitivity.