An opinionated approach to GNU Make
tech.davis-hansson.com
tech.davis-hansson.com
The tabs vs spaces thing seems pretty silly to me. If your editor is randomly swapping tabs and spaces, get a better editor. Tab is the default in a makefile and that seems fine. The suggestion to use "> " instead of tab just looks noisier.
The observation about the filesystem is good and hopefully well known. The way to think about makefiles is as a tool for creating files (it is very oriented toward this), not as a general purpose scripting language (it is just a worse version of whatever scripting language you are in it, if you use it this way). I do wonder if he could structure his tests to have them actually generate output files which make could track, and also have his tests track dependencies.
Point taken about the magic variables. Sometimes they can get obscure (although they are pretty easy to look up). IMO one he's missing, though, is the pattern matching % operator. If make isn't generating at least some of your recipes for you, then why not just make a "build.sh" script?
Probably a preference for explicit over implicit, just like setting `MAKEFLAGS += --no-builtin-rules`. Of course, promptly using a couple of magic variables because they're "common enough that you quickly learn to recognize what they mean" is... a bit amusing, to me.
If I may quote this little part: Oh no, it is actually much better, than what a huge part of developers in web development do: They use package.json of their project, where they add under the "scripts" attribute calls to commands, which contain again calls of "npm run", which again reference other "scripts" ... Of course there is no way to actually specify dependencies (previous steps a step depends on) as actual dependencies and the whole thing becomes a procedural thing, instead of a more declarative thing.
There is also no good way other than writing whole shell conditions in there, in a JSON file inside a mere string, if the command relies on a file existing. So in effect the logic will always run the step, which creates that file.
So this considered, I don't think Makefiles are really doing badly. You can do much worse.
EDIT:
> If make isn't generating at least some of your recipes for you, then why not just make a "build.sh" script?
Because then you don't get what Make brings to the table: tab completion, declarative dependency specification between steps, declarative definition of targets.
You would have to write code for these things yourself in that "build.sh" script.
> Because then you don't get what Make brings to the table: tab completion, declarative dependency specification between steps, declarative definition of targets.
Yeah, that part was a little bit flip. :)
I think neglecting this feature ignores too much of Make's power, but it definitely does have other things going for it as well.
Avoiding tab vs spaces or tab width arguments is a good thing for any team to do. :)
OP only seeks to sit on a high horse about how tabs are bad and to divide an ecosystem which is already pretty consistent. I’m so tired of people insisting tabs are evil and that using them somehow makes you “wrong.”
I’m actually coming around on really narrow conventions since tiny laptops started getting really good.
I used to think 80-wide columns and 1-2 space indent was silly retro stuff in an era of modern displays.
But on an M2 Air you can just barely get 80 + 80 side by side if you full screen SFMono at like 12-14pt.
I’ll never sell my colleagues on it, but I’ve been playing around with setting the formatters to 1-space indent. It’s not as heinous as it sounds.
Bring it on! :P
One nice advantage of 3 space tabs is that if somebody mixes tabs and spaces in Python, leading to mysterious IDE-dependent bugs, it immediately sticks out (my example is from helping student in an intro to python class, hopefully this doesn't occur much in the real world).
- C/C++: `clang-format` is great - Rust: `rustfmt` is very good - Python: `yapf` is very good - Java: `google-java-formatter` is pretty good - Haskell: `fourmolu` is OK, and if you apply it first and then `stylish-haskell` it's good enough - Starlark - `buildifier` is great - Shell: `shfmt` is pretty good - Nix: `nixfmt` is pretty good
I haven't done any JS or TypeScript or golang in awhile, but I'm sure there are great options there too.
By having all the auto-formatters, we can have one golden config that's fully deterministic for upstream, but everyone can have their own if they want and it just gets blasted into the "golden" format before code review.
Finally, a world without brace wars! Everyone tabs and spaces and whatever to their hear's content. Everyone's happy!
* you do eat the blame getting fucked up the one time. it's worth it.
Hopefully somebody wouldn't just apply an auto-formatter, that'd be quite rude!
I think it's very funny that you think a build is guaranteed to fail in an obvious way due to a tab vs space problem.
That said, I'm more concerned about the guidance to not use .PHONY and instead do this:
# Tests - re-ran if any file under src has been changed since tmp/.tests-passed.sentinel was last touched
tmp/.tests-passed.sentinel: $(shell find src -type f)
> mkdir -p $(@D)
> node run test
> touch $@
The author is right, that does use make in a more more idiomatic way by relying on a real file, but I see 2 major problems:* That's a lot of logic for something that should just be super simple.
* When I say `make test` I want it to run the tests. I don't care if they've passed before and the files haven't changed.
Really though, make just isn't a great tool for build scripts. The syntax is horrific and it's hard to scale it into something readable.
If I started a new project I'd probably consider Just: https://github.com/casey/just (though I haven't had a chance to use it myself yet).
This is a common interjection from some people... which means you think that your tests are not deterministic, otherwise it would be completely pointless to run them again without inputs changing.
I actually admit that from end-to-end tests, this is usually true despite our best efforts to the contrary... but for unit tests, I really think tests should be 100% deterministic and only ever run when something they rely on changes. The Unison programming language goes even further[1] and it NEVER executes a test again once the code under test has been "committed" into its image.
What if I installed some dependency outside of my project?
What if I'm trying to test performance with `time make test`?
What if it failed due to a transitory error—or I'm simply trying to discover if it is transitory?
What if I made a change to the way the tests are run in the Makefile or a different file not in ./src? Like running `npm install`?
What if I want to set an environment variable or pass an argument to the test runner?
make -B test
to force a rerun?Next, you're assuming a JS engineer would know what the flag is (or even that such a flag exists) to force reruns. I've literally never used this flag in my (JS) career so I wouldn't expect anyone to know this.
Don't send engineers down some debugging rabbit hole just to save a few seconds when no changes happen. If you want this functionality, just use `jest --onlyChanged` in your own workflow and don't screw with everyone else's.
The engineer will know because it is their job to know.
(Yes I'm a huge fan of deterministic tests and running only what's necessary).
> What if I made a change to the way the tests are run in the Makefile or a different file not in ./src? Like running `npm install`?
> What if I want to set an environment variable or pass an argument to the test runner?
Those are all inputs. Unfortunately, Make isn't very good at tracking inputs (or outputs, for that matter: e.g. if we change an input back to some previous value, make will still rebuild rather than using the old result).
These days I find Nix to be a much more sane alternative to Make.
I have a feeling meeting this may scope creep the thing into being more environment aware. Interpretation of the things it's sourcing [and their paths], and less-than-obvious things like environment variables
This just feels like one of those things trying to be too helpful that inevitably gets in my way
This would be relevant if the command was saving some test report somewhere. But then the target would just depend on the report, and there would be no need to add guard files.
Looks like that `make test` just does the normal thing that is run the tests and print the results to the screen. If so, people would want to repeat it any number of times.
Re-running seems reasonable to me. It's make, not Bazel.
In the article I'm looking at targets with a "find" shell command on the right hand side. I don't think the dependencies of those files are carefully mapped somewhere else in the makefile.
That seems like a terrible idea. You change the basic syntax of the entire Makefile, forcing anybody reading it to get used to your custom indentation, where almost every line starts with an unnecessary >.
> And you will never again pull your hair out because some editor swapped a tab for four spaces and made Make do insane things.
Which... I guess that would be annoying, but maybe fix your horribly broken editor rather than mutilating the Makefile?
There's a lot of other really good advice in this article but this one feels unfounded.
And this isn't some obscure thing that people need to have got around to configuring; vim works properly with Makefiles (marking non-tab leading whitespace as an error) out of the box on at least Ubuntu and I'd guess much more widely than that (all distros and Mac and Windows and also when building from source wouldn't surprise me, although it also wouldn't surprise me if there were a couple of exceptions where things would need to be massaged).
I do not think this is actually a problem.
SHELL := bash
Bash is a much slower shell than Dash, which is why Debian and friends don't use it as /bin/sh. .ONESHELL mitigates the speed problem, but you could also just use the default shell and leave ONESHELL turned off. Use bash strict mode
....
.SHELLFLAGS := -eu -o pipefail -c
I wish people would stop cargo-culting the so-called "strict mode".The -e flag is only useful because the author likes .ONESHELL mode. If you leave ONESHELL turned off, then you don't need it.
The -u flag is useful sometimes, depending on coding style. I use it on complex scripts. Individual Makefile recipes maybe don't want that much complexity though. Also the -u flag makes the shell's variable-handling behavior inconsistent with Make's.
The pipefail option is Bash-specific, and only works because the author likes to set SHELL to Bash in their Makefiles. It's also not a good default in my opinion. There are times when it's useful, and other times when it's the opposite of what you want. Just depends on the pipeline that you're writing.
SHELL := /bin/false
Just to make sure my Makefile doesn't use shell syntax. If you want a `.STRICT` mode, then try Landlock Make.https://github.com/NetBSD/src/blob/netbsd-9/usr.bin/make/com...
$ make -v | head -n1
GNU Make 4.3
$ printf 'SHELL := /bin/false\nall:\n\tls\n' > Makefile
$ make
ls
make: *** [Makefile:3: all] Error 1Not really. The most common class of bugs I see in makefiles is something like this:
for x in foo bar baz; do frobnicate $x; done
This ignores errors from frobnicate, unless you set -e.Their advice is mostly about using a known interpreter, rather than 'vague POSIX hand-waving'; e.g. they end that section with:
> The key message here, of course, is to choose a specific shell. If you’d rather use ZSH, or Python or Node for that matter, set it to that. Pick a specific language so you can stop targeting a lowest common denominator
Your suggestion of Dash is compatible with that (as is any other particular shell interpreter).
An analogy would be running Selenium tests with particular browsers, rather than using /usr/bin/www-browser and trying to accomodate lynx, dillo, netsurf, ...
Content: A lot of subjective preferences, with the only thing people are probably doing "wrong" being not properly mapping files as inputs and outputs (which could be a correctness problem but is probably either a mere inefficiency or complete non-issue).
If this had been titled, say, "An opinionated approach to writing Makefiles", or perhaps "How to use GNU Make in a completely unorthodox way that I really like", I wouldn't mind it so much.
The same analogy could be made to a baker luring customers in their bakery with an attractive facade but the very same "customers" just give a quick glimpse and leave.
What is the win?
If the tradeoff was that Make was easier to use and debug, then maybe it could be justified, but in general my experience is that it's worse.
There are probably some use cases for Make where it remains difficult to replace for one reason or another, but most people using it anymore are not in that position. Now, it's usually more work to keep using it.
For toy stuff I'll just compile on the commandline (maybe write a bash script). I'll write a Makefile if I need to start wrangling too many things. CMake usually comes in if I need to go cross-platform or incorporate another build system or dependency that needs it. I think most places probably need CMake, but quite a few don't. If Make, as is, works then it makes sense to come up with opinions and standards that streamline authoring and maintenance.
However, it also offers practical answers to real problems that even Autoconf don't do a good job with, and that makes it valuable. CMake can handle out of tree builds, vendoring, cross-compilation, packaging RPMs/debs/even DMGs, library and platform detection, test suites, abstracting build systems, MSVC/Windows... If you are going to tell me about the "best" way to compile software and the answer to this is either "shrug" or "Here's a shitty 1000 line Bash script you can copy" then I'm going to continue to discard the advice, because frankly it sucks.
CMake, also, for all its faults, has improved a fair bit from the 2.x days. I'm not saying it will ever not be ugly, at least as far as the language itself goes, but they're definitely cleaning up a lot of the worst mistakes over time and it's making CMake a lot less of a bad proposition. You do have to opt-in to some of the new practices, but it is nonetheless worthwhile improvements.
Personally I’ve taken the plunge on Bazel and whoo, the first time you run clang-format, save, and hit in the cache on the .o, I mean sex is cool but have you tried building C++ fast?
But Bazel will probably never be the standard or even common, le sigh.
What we might get is something sane that generates CMake, so that it can generate Ninja, so that we can be bitching about CMakeGen in 10 years.
I like Bazel, but I wish it didn't inherit all of the issues that come with large Java software. Also, to be honest, I'm less thrilled with how Bazel works outside Google than Blaze works within Google; they took out some of it's advantages in exchange for better ecosystem interoperability, which totally makes sense and yet also is a bummer. I wish they could've somehow given the rest of the world a generalized taste of how they do it.
Sorta related: I like Bazel's concept of the build server. I can't help but think the programming community could invent a "build server protocol" not unlike the language server protocol, and somehow integrate it with LSPs and IDEs. (Obviously it would still be complex, but the premise of having a somewhat general way to swap build systems in a project and have e.g. clangd or tsserver know what flags it would get where seems amazing.)
I've never worked at Google, so I'm very curious about what Blaze gets you that Bazel doesn't. I have no trouble imagining it's a lot: you can do great stuff when you control the whole stack and have lots of computers, but I'd be intrigued by any details you're at liberty to share.
Also, your CMake generates Makefiles, so...
CMake and meson/ninja, though, seem to be pretty much tuned to compiling C-shaped things, although I’d like to see them (ab)used for other things.
Of course Make + ssh and a distributed FS gets you there, but you don't always have a distributed FS especially across continents.
Trigger warning: the thing is mature enough to speak about rsh(1) seriously.
IllumOS has a "dmake" command, but as far as I've read its man page, it's not distributed across hosts and likely exists for compatibility.
It is said that dmake is licensed under CDDL, but it will take a more patient soul to get to the sources. Oracle Developer Studio can be downloaded and run, so I presume you can toy with it: https://www.oracle.com/tools/developerstudio/downloads/devel...
CloudBees build a thing to vendor-lock your development workflow in, and they have a thing called ElectricAccelerator. It's vastly more complicated, and from I gather, hosted. But they say it can ingest "most" GNU Makefiles and distribute tasks across worker nodes.
I strongly suggest spending an afternoon with "recursive make considered harmful" and the gnu make manual.
I've never encountered a cmake proponent that can add trivial functionality to a cmake build in less time than it took me to learn make.
I can usually port cmake builds to make in less time than such people can debug the cmake version of the build I ported.
How do I do this in Make:
find_package(OpenGL)
target_link_library(app OpenGL::OpenGL)
Goals:- Support Windows, macOS, *NIX like
- Compile with either MinGW or MSVC on Windows
- Decent error output if GL headers are not found
This SHOULD be possible. All we are doing here is calling the compiler with a fairly easy to derive set of options. Yet, in Make, there is no ideal way to abstract this. Even detecting platforms can be annoying, less trying to abstract the differences between them.
I do not find Make hard to use. I do not lack knowledge on how to use Make, or not understand the "zen" of Makefiles.
I just know that `-lGL` is a terrible answer to this question. And I find Make to be generally bad for compiling software.
Out of tree builds and reasonable platform detection generally require configure scripts, at which point we've thoroughly left any semblence of elegance.
If you trust your build environment, then -lGL is the right answer. If you do not, then vendoring libGL is the right answer. Both approaches are easy to achieve with make.
find_package semantics are frankly weird: "maybe use the OS version, or override in nonstandard ways, or maybe download some version from somewhere and build it using some compiler flags that came from somewhere mysterious. If you succeed, have package-dependent side effects on the set of global variables in my cmake script".
Does it have a higher chance of producing a binary in dodgy environments? Sure. Are those binaries actually reproducible or what the developer / distribution tested with? Absolutely not.
As for windows with visual studio: Either point make at the visual studio compiler, or hand maintain a separate .SLN file. The impedance mismatch between Unix and Windows builds is too great, and the auto-generated .SLN files that tools like cmake produce are low quality.
The question I asked is simple. CMake has an answer. Make has more questions.
Anyways if you are targeting platforms that support it, pkg-config will automatically generate the required flags for you, you can append them to a variable however you like, and later pass that variable to the compiler and linker.
You can try this out in a shell session if you are curious, try running:
pkg-config gl --cflags --libs
You can see what libraries it knows about by running: pkg-config --list-all
Even when not using make directly a lot of build systems will be using pkg-config "under the hood"! Cmake is an exception here and has it's own way of doing this, but you can configure it to use what you want.Just to be clear I am not saying makefiles are good or bad or what you should use, I just wanted to mention pkg-config because it's pretty cool and useful to know about.
The real answer to your question is to just use cmake I think!
As much as I don't think CMake is elegant or perfect by any means, CMake is nice because it does work, and there generally is a proper way to do things even if it's not always intuitive. And for how bad it can be, honestly, it's much less of a pain in my opinion than dealing with autotools, for many reasons. Just to be concrete about it, one thing I find really painful with autotools is trying to find the exact combination of versions that will actually work with a given project... With CMake, usually newer versions are always OK, and there's a decent focus on both backwards compatibility as well as explicitly declaring the required CMake version. There's more, but it's probably not worth getting into, as I don't think most people defend autotools as being particularly nice to use.
Some projects, like the Linux kernel, can definitely get away with Make and custom configuration, since a lot of higher-level general purpose tools are not really well-suited for their needs anyways. I feel like most projects (somewhere on the order of >99% of them) are not really in this boat.
Or for you, portability means Linux systems only?
Honestly curious, because I've gone to the trouble of writing my own build system just so I can use the same build file on any OS whatsoever (which to me, means using a cross platform language for everything, not relying on bash or any other shell).
It’s an opinionated blog post about relatively minor Makefile conventions with a clickbait title, which doesn’t look obviously self-submitted.
And yet the thread is #1 and has 96 comments, of which like 92 are trashing the poor guy.
Surely there is someone more worthy of an HN gang-tackle than this? It can’t be that slow of a news day.
The idea that someone could be so presumptuous to assume that they had more make knowledge than everyone on the planet makes them very angry, because they are someone on the planet, so their first instinct is to lash out and prove that person wrong. A better instinct would be to humor the idea that the author doesn't think they know make better than anyone else in the world and isn't trying to hurt their feelings or their careers, but instead is trying to give people who don't know make as well as the author does a few tips.
edit: basically a gathering of the people who reply to things on the internet that upset them with: "That's just your opinion." No shit, buddy, I wrote it, who else's opinion would it be?
...and, of course, when they're not actually wrong.
> No shit, buddy, I wrote it, who else's opinion would it be?
Okay, if OP can have an opinion that "Your Makefiles are wrong", then I can have an opinion that " Your article is wrong". Fair?
Which is a shame because the rest of the post is mostly reasonable: turning recipes from a collection of one-liners into an actual piece of shell script, deleting output files on build errors, using -e and -o pipefail, etc.
It’s just weird.
> However, for the sentinel file pattern, the magic variable $(@D), which refers to the directory the target should go in, and $@, which refers to the target, are common enough that you quickly learn to recognize what they mean:
So, avoid using the magic variables, but actually you should use them because they're useful and common. Got it.
> You really just need the .RECIPEPREFIX = >
Now I can't copy & paste a block anymore (into the shell, to run it), and all my editor indentation settings are broken.
> SHELL := bash
And the Makefile is now non-portable.
> .SHELLFLAGS := -eu -o pipefail -c
If this matters, it's likely you're wedging too complicated things into one recipe. But less bad than the other suggestions.
> .ONESHELL
Funnily enough using this is the primary reason the previous item becomes important. The subtly changed behavior also turns multi-line recipes into a giant footgun if you end up with a non-GNU make.
(skipping a few that are not as bad)
> out/image-id: $(shell find src -type f)
Might be OK in a single rule. Otherwise, it's calling find more... and more...
> Sentinel files
Actual good practice to end it on.
The article calls out GNU Make, so almost everything else in there is also non-portable.
I definitely don't remember the last machine I saw that didn't have bash on it.
If you're working in a team with other people, or are publishing things for a broader public to use… no. Especially since things like .ONESHELL don't just flat-out cause errors, but rather introduce subtle and insidious distinctions in behavior.
And a Make supergenius, should I ever meet one, surely will look at the top of the file or derive from "incorrect" code that something is nonstandard.
I'd love better tools that take the good parts of Make (around file transformations, mostly) and expose them more effectively in shells (because doing so in a more fully featured programming language often results in different and worse hacks--hi, Rake) but using Make to do things more people will find predictable seems fine to me.
Example:
.ONESHELL:
sometarget: someinput
␉BASEDIR=other_expression
␉SHELLVAR=complicated_expression
␉rm -rf $$BASEDIR/$$SHELLVAR
(obviously extreme to illustrate the point, but you get the idea.)(If you use any of these features — please rename your Makefile to GNUmakefile. Please. I beg you.)
I can only assume that's the last of the GPLv2 licensed ones. Classic Apple.
My brain honestly didn't process that, and it's still refusing to. I think it's the braces around (GNU) — seems like I have some neurons wired to push (bracketed) pieces of text down into a "detail" stack and eliminate them from high-level processing…
P.S./ed.: GNU make looks for GNUmakefile before Makefile, and arguably if the Makefile is GNU specific, that feature should be used — and that includes the title of the article ("Your GNUmakefiles Are Wrong")
You likely can, actually. Most terminal emulators have a key you can hold (ctrl in gnome-terminal, alt in urxvt) to select a block of text that doesn't start at the beginning of the line. Doesn't work if your lines wrap, of course.
Plus without word wrap you can't copy more than a screen width at a time on most terms.
The parent said they wanted to paste it into the shell to run it. In that case, you aren't going to want to copy the target, only the steps. It will have a single space indentation if you start there, or no indentation if you start one character over; either could be what you want, depending on your shell settings and whether you want the commands in your history.
If you want to copy and paste the target and all of its steps in one go, you need to ask yourself where you want to paste it, but probably including the > and copying normally is the right thing, to be reformatted on the other side as desired. In fact, I expect that to break a little less often than pasting things with leading tabs.
> Plus without word wrap you can't copy more than a screen width at a time on most terms.
Yeah, I was imprecise; "doesn't work if your lines wrap" should probably have been "doesn't work if your lines are long enough that they would need to wrap".
I have written a great many makefiles, simple and complex. I can’t recall a single time I’ve needed to mix tabs and spaces in one (though I have had to mix them multiple times in both YAML and HTML).
(As for anything like accidental mixing, for my part I have a sanely-configured text editor and so don’t need to worry about anything silly like tabs being turned into spaces. Tabs are superior to spaces anyway. ⸺But I do use spaces for Rust and Python where that is customary, I’m not completely antisocial.)
> .ONESHELL ensures each Make recipe is ran as one single shell session, rather than one new shell per line. This both - in my opinion - is more intuitive, and it lets you do things like loops, variable assignments and so on in bash.
.ONESHELL also means that your makefile will behave differently from how anyone that’s familiar with makefiles will expect it to. But I guess this does explain why you went enabling strict mode, since you’ve basically turned off the near-equivalent default functionality from Make.
Note also that you can do loops and such already—you just need to use line continuations (put backslashes at the end of each line, which Make will consume).
Yeah, the default behaviour is idiosyncratic and will lead to surprises in the unwary (though they’ll normally observe it immediately, when the cd is ineffective on the next line, or when the if/for causes a syntax error), but I think Make has generally become niche enough that I’d prefer to pander to people that know Make than normal people. :-)
> .DELETE_ON_ERROR
Two-edged sword: it also means you can’t inspect what went wrong by looking at the file. You’re also making the very dubious assumption that merely deleting this one file will fix everything. A few times when I’ve known something to be fallible but want to be able to inspect what it created, I’ve put in something like a `… || { touch --date=@0 $@; exit 1; }` suffix so it still fails, but first zeroes its mtime so that subsequent runs will see that it’s out of date, though the file still exists.
I’m not saying it’s wrong or a bad idea, just that it’s worth considering the implications fully rather than blindly applying it.
I frequently run into similar situations with more junior engineers at work. One will insist on dogmatically adopting some "best" practice advocated somewhere, and when I ask what failures or bad situations we'll avoid, or what good situations we'll encourage, they can't answer. In their minds, someone (outside our team or the company) said it's better and so it must be.
I think it's important to avoid invoking incantations, and to understand the reasons for each choice you make. In this article, I don't see that.
I was on the fence about posting this reply; after all the guidelines tell us to stay relevant to the material, but I do believe you have done that.
Not with me, unless you're talking about the difference between clear and unclear language.
I'm not bothered by language that some people seem to class as patronizing or like you know everything or whatever. When somebody is explaining something to me, I don't want them to explain it to me like I know it already. I want them to explain it to me like someone I've hired to explain things to me i.e. like they're the expert and I'm not.
If I think I know everything about make, there's no reason for me to have clicked on this other than an urge to seek out things that confirm my sense of self-worth, or to get upset about things that threaten it.
> it's incumbent upon him to clearly state what failures I will avoid
He does exactly that though? Here's a list of some of them:
Rule: "Use a strict Bash mode"
Failure(s) avoided: "your build may keep executing even if there was a failure in one of the targets."
Rule: .ONESHELL
Failure(s) avoided: assignments failing to take effect on subsequent lines ("it lets you do things like loops, variable assignments and so on in bash")
Rule: .DELETE_ON_ERROR
Failure(s) avoided: "ensures the next time you run Make, it’ll properly re-run the failed rule, and guards against broken files"
Rule: MAKEFLAGS += --warn-undefined-variables
Failure(s) avoided: avoids silent misbehavior when a variable doesn't exist ("if you are referring to Make variables that don’t exist, that’s probably wrong and it’s good to get a warning")
I had this happen often enough (in a quite large system that farmed out compile jobs to a cluster) that now I always make my recipes write to a temporary file, and then rename the temp file to the actual target, e.g.
test.o: test.c > cc -o $@.tmp $< > mv $@.tmp $@
Once you adopt this strategy, .DELETE_ON_ERROR is irrelevant.
I agree, it's one reason Make itself sucks.
> I always make my recipes write to a temporary file, and then rename the temp file to the actual target
Then hopefully set the timestamp if you used some tool to generate it so it doesn't look out of date to Make. (Again, another deficiency of Make. I could list more.)
> Once you adopt this strategy, .DELETE_ON_ERROR is irrelevant.
Sure (well actually no, but that's next paragraph), but that strategy is only worth it for "serious" Makefiles. Ones you use in your work environment and all. For personal projects etc. it's not always worth the hassle of polluting the Makefile with boilerplate like that; it's much handier to put that one line.
But actually no, there's still a benefit: it saves disk space to delete incomplete output files. If your files are 4KiB you might not care, but if they're 4GiB then you might. And sure you can get around that by manually adding 'rm' in the beginning of every rule too, but why not use this instead while it's already there. It's one line and doesn't harm anything.
The author makes several convincing arguments and specifically lays out what failure modes are avoided for each recommendation. Honestly the title is completely out of step with the tone of the argument, which is a critique I can support.
I agree, any claim that you should do XYZ should give a strong argument.
The article here does try to give very short arguments, to be fair. I leave unconvinced by many of them. For example, requiring bash means you can't use dash; dash is less capable but much faster.
I prefer arguments that walk through the key pros and cons. Longer, but in long run more useful.
Seems like dash would be fine for the author.
.SUFFIXES:
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/m....SUFFIXES
Prerequisites of .SUFFIXES shall be appended to the list of known suffixes and are used in conjunction with the inference rules (see Inference Rules). If .SUFFIXES does not have any prerequisites, the list of known suffixes shall be cleared.
I know that Twitter and Medium popularized this style a lot. I wish we used it less.
The tips in the article are interesting, and the text is humbler than one would expect from a preamble like this, though. Go read it, give it a thought.
Seriously now I love Makefiles and use them extensively (nothing like make serve and make deploy to simplify my day), but there is a limit where being too opinionated (rather than just simple, easy to understand conventions) just ruins the tool and adds too much cognitive overhead.
Don't generate some random id and a tag (as in the post). Use docker's "-iidfile" flag when building to write the actual id of the image to a file which can then be used in a "docker run".
Likewise, you can use "--cidfile" in a "docker run" to output the id to a file and use that later for accessing it.
docker build --iidfile .dockerid && \
docker run -it /bin/bash --cidfile .dockerid
Without needing to copy and paste the image ID? Is that right? Why wouldn't I just tag the image? docker build -t myimage && \
docker run -it myimage /bin/bash--iidfile writes the image id of the build to a file.
--cidfile writes the container id of the container to a file.
To use the output of --iidfile in a "docker run", instead of specifying an image name call "$(cat <iidfile>)". As an example:
docker build --iidfile out/imageid ...
docker run ... $(cat out/imageid)
--- edit for formatting ---
https://www.reddit.com/r/ProgrammerHumor/comments/uosex4/no_...
You can do things like loops and variable assignments, you just have to keep it on one line. You can put that one line on multiple lines using newline escaping by ending a line with \
You could argue that making it clumsy to do those things enforces that makefile scripts be simpler, while still providing enough of an escape hatch should it be necessary.
But probably ONESHELL performs better. The default sounds like a lot of wasted repeated fork/execs.
I could only find release notes in the mailing list [0]. Initial searches didn't turn up anything.
[0]: https://lists.gnu.org/archive/cgi-bin/namazu.cgi?query=gnu+m...
It's such a shame that this is the attitude though. We're stuck with a make and shell frozen in time. Why even add new features to make/bash if nobody will run the new versions?
Make is cool, but it's stagnated because people don't think we should rely on anything beyond POSIX
Why must the build system stick to POSIX when your libraries don't?
The Makefile should probably be listed as a dependency for all the rules too, otherwise you’re gonna end up dealing with stale results and adding a “clean” target.
https://bnikolic.co.uk/blog/sh/make/unix/2021/07/08/makefile
In this way a meaningful change to the makefile triggers rebuild automatically like it should
This will at some point cause a bug that will be a pain to debug. Even worse when it depends on execution order/thread grouping with -j8. I would not consider introducing state is a good thing.
like nested header files, or forgetting to update them when the code is changed
(so everyone runs make clean all instead every time...)
Makedep creates additinal dependency targets, so that source files depend upon the header files they include.
Build systems are hard so I’m not gonna point fingers too hard. But reading/writing makefiles is one of my least favorite parts of the job.
General reminder: you don't even necessarily need a make file to build C:
~ % echo 'void main(){printf("hello");}' > example.c
~ % make example
cc example.c -o example
...
2 warnings generated.
~ % ./example
hello%I tried it myself though and it complains about not including stdio.h and wanting `int main` instead of `void main`. Though of course I still get your point.
What this does allow you to do though, is also do things like:
make CFLAGS=-DFOO LDFLAGS=-lfoo exampleThe advice might be sensible in some way, but if Make isn’t anal enough for your tastes, why not just use a different build tool?
For me, every Mac I touch gets a GNU userland by default (and Homebrew comes with shell helpers to switch to GNU userland aliases temporarily, it isn't a hard switch unless, like me, you lock it on all the time) because it's really not worth the time and effort to remember what bits are GNU and what bits are BSD. So I picked the one that has the most stuff that makes my life easier.
There's also just the more basic thing of "the author doesn't care about portability to environments they consider out of scope," and that's totally fine, too. In 2022, the barriers to entry to installing the necessary bits of one flavor or another are very low, and if you choose to make them harder for yourself that's mostly on you.
But it isn't always easy to install new software. GNU make is so widespread that it probably doesn't matter. But for example, if you are on a shared machine where you might not necessarily have root. Or, if somebody requires some obscure build system that isn't available in your distro repos (of course you can build from source, but the need to check the source of the build system is going to increase the barrier to entry).
"Use a recent bash" seems like a matter of preference.
"Make is all about files (paraphrasing)" seems a little obvious hopefully, but definitely the way Make is intended to be used.
"Magic variables" eh... I dunno, I guess sometimes they can get obscure but it isn't super hard to look this sort of thing up.
The omission of % seems weird.
> .CPU = SECONDS
> .MEMORY = SIZE
I wish the cluster I use sometimes had this built into their make system, to prevent people from make-ing on the headnode, haha.
Unfortunately it's not merely a preference; old Bash versions (in particular the one that shipped on macOS for a long time) have had at least one nasty behavior (I think multiple, but I recall at least definitely one) that have been fixed since then, but which cause grief in Makefiles. I don't recall what it was or have a link handy, but if anybody knows, please leave it here; it's a well-known issue.
If you want someone to really hate, pick me. I see a Makefile and think "welp, strap in for a wild ride that isn't going to get me a working binary". If someone has ever made a good Makefile, I certainly haven't seen it.
mosh is a good example: https://mosh.org/#build-instructions
For any readers that want to learn make, look at the makefiles of big projects and see how they set things up. There's a huge difference of one person's opinions vs. a working system for a real world project.
https://www.gnu.org/software/make/manual/make.html
It's a peeve of mine that folks refuse to read the actual documentation for tools these days, preferring to wing it by copying other people's code.
And being forgiving of not understanding the docs requires someone to read the docs in the first place.
Most of the time I needed two or three passes in different context to start to really grasp a tool domain.
But here's my criticisms:
> Instead, ask make to use > as the block character, by adding this at the top of your makefile:
Changing this is like replacing java curly braces with square brackets because it's easier to type. A good developer should be able to cope with different stylistic choices to match the existing code without strain IMO
Maybe if it was a greenfield project it's ok, but the article doesn't say either way.
> The key message here, of course, is to choose a specific shell. If you’d rather use ZSH, or Python or Node for that matter, set it to that.
I disagree with using ZSH as it adds another dependency and shell to learn to build your project. This introduces more ramp up in knowledge, but also something build systems will have to pull in.
The vast majority of steps end up as simple unix commands and program calling with specific flags in practice.
Python is a maybe if that's the only language you're working with. Otherwise anything but simple function calls is better served by an external file. Even a simple module function call becomes clunky on one line.
This is an interesting idea worth exploring in general, though.
> Make has a bunch of cryptic magic variables that refer to things like the targets and prerequisites of rules. I mostly think these should be avoided, because they are hard to read.
See point 1. This is like choosing to not use regex because the symbols are confusing.
~
The rest of the article has good stuff in it though, esp. the file structure and bash flags. The rest is just questionable opinion and has little impact to a good makefile.
Can you give some arguments against what I typed above?
Most of these choices are not really subjective. They are the most robust methods that make provides for trying to eliminate errors in your make file. If you think "set -e" isn't a good idea you're either a rookie or brain damaged. The alternative is your scripts silently look like they're working when they actually fail and you spend hours trying to debug where things went wrong because the system isn't pointing it out to you.
My only fault with the article is that it's putting lipstick on a pig. None of this does anything to address the fundamental flaw with make that there is no protection against you incorrectly specifying building inputs. Every make file I've ever seen at a company at scale is buggy. These tips would definitely help but would not cure that fundamental issue.