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?
> [...] it is just a worse version of whatever scripting language you are in it, if you use it this way [...]
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.
>> 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.
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.
The tabs and spaces thing is targeted at teams. At any point, there will be someone editing the makefile with a misconfigured editor. Depending on the team's growth rate, and their desire to use different tools, this happens a lot.
Avoiding tab vs spaces or tab width arguments is a good thing for any team to do. :)
Yet this has been settled in Makefiles for a long time: use tabs.
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.”
Tabs are obviously superior. If the tabs vs spaces argument must be had, and the correct option somehow loses, then we will have a much more annoying followup argument about how many spaces (I vote for three spaces and I will filibuster).
The cool thing about tabs is that you can do that 1-space indent without spreading your madness (but yeah, I think I got a preference for 3 spaces -- actually tabs with tabwidth set to 3 -- while using a little, now old, netbook).
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).
We've just gone the deterministic code formatter route on everything. Right off the top of my head:
- 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.
I prefer around 80 characters per line maximum even on very large displays. More columns of text are always better, and I also find shorter lines easier to read.
I mean, this is mostly joking, of course if I was on a team I'd put forward my argument once and then comply with the conclusion the group makes, it is not really a big deal anyway.
Hopefully somebody wouldn't just apply an auto-formatter, that'd be quite rude!
This is good, their builds will fail, notifying them of their misconfiguration in the most obvious way possible. Hopefully they will have to customize the Makefile a little to build on their system, and give it a build before they start programming, so they can correct their editor before they do any damage to the source code.
Sure, it also won't catch the misconfiguration for those who don't even open the Makefile. It is too optimistic to hope that this venerable piece of software will solve all out misconfiguration woes, but at least it provides a possible red flag sometimes.
And on the topic of "get a better editor", if you (quite reasonably) prefer something more visible than a tab... you can do something to make tabs more visible in your editor. That way it'll work for any Makefile you open, not just those you're responsible for.
> 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.
I think the `.RECIPEPREFIX = >` bit triggered a lot of people here in the comments, and I agree. That would make drafting newlines a huge pain in any editor. Just enable "Show Whitespace" in your editor if you want this.
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).
> I don't care if they've passed before and the files haven't changed.
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 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.
First, the engineer needs to understand how the makefile is written and that it doesn't rerun without changes. Is it not rerunning because of npm? because of jest? If the engineer isn't the one that wrote the makefile: they won't.
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.
> which means you think that your tests are not deterministic
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.
> [...] which means you think that your tests are not deterministic, otherwise it would be completely pointless to run them again without inputs changing.
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.
Keep in mind I'm about as ignorant of Make as one can be -- I use it to build things, but I've never put things into it
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
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 >.
Are you going to fix everyone else’s editor too? Software is a multiplayer game and eliminating a whole class of error that hinges on someone not noticing the difference between invisible characters in a diff is a huge win.
I'm not 100% certain, but I suspect a Makefile with this error will become unusable. So, no need to fix their editor, they will fix it themselves after their builds fail.
Are there any major editors that can't handle this? Given that python has a very similar kind of formatting I would assume any editor that a programmer is using today can handle it.
In Python, if you save your file with tabs as spaces, the code 1) still works, and 2) does the same thing it did before. Furthermore, it prohibits mixing tabs and spaces up in the same file, on the basis that it's impossible to reliably determine indentation levels then. So this isn't really a major issue with Python, unlike Make.
Sure, I'm happy to agree that the original design of make is kind of questionable. My point was that an editor which can handle telling you to not mix tabs and spaces in python should also be able to tell you to use tabs in a Makefile. Or, if you prefer, that if python can say "only tabs xor spaces" and be seen as reasonable, then make can say "only tabs" and be equally reasonable.
The editor doesn't need to do anything special to avoid mixing tabs and spaces in Python - quite the opposite, the simplest editors (the ones that either preserve or expand all tabs when you save, depending on the setting) will do.
Just about everyone I know uses VSCode, which handles Makefiles correctly out of the box, or I can trust to have a correct vim config, so honestly I think I'm okay with it. If something breaks, it'll break loud.
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).
Yeah, the two places I find myself still reaching for vim are Git commits and quick shell/Makefile tweaks and both are correctly formatted out of the box (shortened Git first lines on commits, etc).
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.
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.
The fastest shell is to not use shell special characters. For example, if you say `foo bar >/dev/null` then Make needs to launch your program as `sh -c 'foo bar >/dev/null`. But if you say just `foo bar` then Make can pass that directly to execve(), bypassing the shell entirely. Sometimes I actually do this:
SHELL := /bin/false
Just to make sure my Makefile doesn't use shell syntax. If you want a `.STRICT` mode, then try Landlock Make.
I completely forgot that the GNU Make source code has a check to see if the shell is bourne-compatible (it just strcmp's with sh, bash, and a hard coded list), and then only applies that optimization if it is. I deleted that code in my Landlock Make fork so I could use shells like /bin/false and still get the optimization. So sorry about that! Give it a try with Landlock Make https://github.com/jart/landlock-make and https://justine.lol/make/
> Bash is a much slower shell than Dash, which is why Debian and friends don't use it as /bin/sh
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 purpose of a title is both to summarize content and grab the readers attention. It's up the author which one they put for emphasis on.
You clicked so it worked, even if you don't like it.
The user only saw the headline and closed, isn't the author aiming visitors to read the content or clicks?
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.
In fairness, I did in fact read more or less the whole thing. I walked away with a fairly low opinion of the article and the author, but I did read it :)
This seems like a lot of work to not just consider ninja, meson, CMake, etc. I fully understand that the simplicity and portability of Make is alluring, but if you are actually using it to build C software it is a catastrophically poor choice and you can spend a ton of time and effort trying to come close to what you can get out of the box on a modern build system.
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.
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.
You should either trust the OS to package GL, or vendor it yourself.
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.
If you are targeting a GNU/Linux system and possibly other Unix like systems you can use pkg-config for this. All of the mainstream Linux distros support and use pkg-config, but I'm less sure about the BSD systems and MacOS since I've never used those before but from what I gather it should work for them too. Most Windows users probably don't have pkg-config installed but I also am not familiar with Windows.
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!
This is basically the best way I've seen to do this with pure Make, though I don't think it will work on macOS or even MSYS2 because even if you install pkg-config I don't think anything will provide definitions for OpenGL. (That's part of the reason I picked it; OpenGL is a really annoying edge case, since every platform went about it a little differently, and yet it is a common thing to need to link to.)
The trouble is not supporting all the different Operating systems/compilers, as make can do that. The issue is when the OpenGL library location varies, even on the same OS/compiler set.
It's kinda both. The problem is that it doesn't really make sense to do all of this work to configure the build directly in Make, so if you need any kind of expensive detection (like calling pkg-config or testing if some library function is available) it's best to do it in a configure script. Most projects don't have terribly special needs, so it would be both overkill (too much effort) and insufficient (not supporting enough use cases) to do a lot of custom configuration code. So most projects wind up using something like Autoconf, which has already done a ridiculous amount of work to work on almost everything, supporting important use cases like out-of-tree builds and cross-compilation, but even then it has some unfortunate limitations.
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.
The point isn't that CMake is good. I don't think anyone is arguing that CMake isn't an ugly, weird mess. I can point out several annoyances that are frankly disturbingly stupid.
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.
Oh I agree mostly. CMake is still a practical necessity for the time being, and I also agree that from 3.10 or so it goes from an NC-17 slasher flick to an R-rated horror film. It’s so ubiquitous that “I will do nothing, because I can do nothing”.
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.
Totally fair. I agree. It is a damn shame that things are the way they are with build systems.
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.)
That build server protocol is a great idea. There are folks who sell Bazel build as a service (e.g. BuildBuddy I think it's called), but while it's pretty easy to get a remote action cache, it's quite a bit more involved to get a true remote farm, and this obviously goes nonlinear in complexity as languages/platforms/toolchains start their combinatorics routine. If there was a standard and it were as successful as LSP (thank god for VSCode and whatever hero at Microsoft decided to keep LSP despite the JSON) I think it would create a whole new SaaS ecosystem and make everyone's life better.
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.
Make can do more than compile a bunch of C files. If you can express your goals and dependencies as files with meaningful creation timestamps, it can be a potent task executor that can also skip over steps if they are already done.
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.
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.
Out of the ones you list CMake is the only one I've authored and maintained and I still end up having to understand and troubleshoot Makefiles. Depending on the size of the project and the needs I try and use the fewest number of abstractions since I end up jumping down to troubleshoot anyway.
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.
This is bad advice. Changing the default prefix for recipes is worse than using tabs, whatever your feelings about tabs are. Just use make as it was designed, it will work better this way.
This results in a pretty un-portable Makefile. Portability is a desirable feature for a build system, which is supposed to help other people on other systems to build your software.
Given the explicit choice to use GNUisms and promptly overriding the SHELL to explicitly use bash, I'm quite confident that this author is not concerned with portability concerns.
Quite, but then the posture of "you're doing it wrong" is utterly incomprehensible. It's clear that the author is making rather strong assumptions about the runtime environment.
What do you mean by portability? Are you able to confidently write a Makefile that works on any Linux distro, BSDs, MacOS and Windows?
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).
You see, the very first point of the article not only has a really weird opinion (that'd be only half bad), but but this weird opinion is being justified/defended with obviously faulty reasoning: "in the shell spaces matter, [so don't use tabs]. Instead, ask make to use > as the block character". Yeah, because ">" doesn't matter in shell, and there is absolutely no confusion possible as a result. Imagine copypasting a recipe from such a Makefile into a shell? The results will be pretty hilarious.
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.
Oh I also disagree with the “>” thing. I just think we could have been like: “the > thing seems bad, good call on the pipefail thing. moving on…” rather than like ruin this guys day with 100+ negative comments because of the novel horror of a clickbait blog title. It’s not that big of a deal (unless you’re the guy who just got clobbered by every HN user awake on Sunday morning).
It's triggering for some people to be told that they are wrong, even if the person who said it doesn't actually know them specifically, wasn't thinking about them as individuals when they wrote the blog, and has no idea that they exist.
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?
> It's triggering for some people to be told that they are wrong, even if the person who said it doesn't actually know them specifically, wasn't thinking about them as individuals when they wrote the blog, and has no idea that they exist.
...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?
> 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.
> 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.
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...
Even beyond that I don't really understand the criticism. Like, I don't remember the last machine I had to do serious work on that didn't come with GNU Make either out of the box or added as part of some very basic bootstrapping scripts. (I don't know what a Mac comes with because my bootstrap for every new machine has coreutils in it and every project has a Brewfile with it too.)
I definitely don't remember the last machine I saw that didn't have bash on it.
If you write Makefiles for your own personal use — sure.
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.
Ehh. If I'm writing a Makefile, team context or no, it's almost certainly because I don't expect other people to be more familiar with Make than I am. If anything, I would expect ONESHELL to map to how most programmers think Make already works, even though it doesn't. I know I have to go look up the various Make-isms whenever I need to do anything complex.
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.
I guess I should've been more clear. My point is other people /using/ your Makefile, not editing it. You know what you're running. You don't know what everyone else is running. I've singled out ONESHELL because it will be silently ignored by non-GNU make, and it will behave ever so slightly different enough to cause extremely hard to find bugs.
btw. On the BSDs, GNU make is "gmake" while plain "make" is POSIX-compliant BSD make. Better hope you never make the easy mistake of forgetting that "g"…
(If you use any of these features — please rename your Makefile to GNUmakefile. Please. I beg you.)
> The article calls out GNU Make, so almost everything else in there is also non-portable.
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")
> Now I can't copy & paste a block anymore (into the shell, to run it)
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.
But then you can't copy a target and it's steps in one go. And the pasted code will only have a single space for indentation, if I'm understanding right.
Plus without word wrap you can't copy more than a screen width at a time on most terms.
> But then you can't copy a target and it's steps in one go.
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".
> Make leans heavily on the shell, and in the shell spaces matter. Hence, the default behavior in Make of using tabs forces you to mix tabs and spaces, and that leads to readability issues.
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.
The HN and the blogosphere generally are replete with unconvincing "you're doing it wrong" posts. This is one. I use make (and have used it off and on for a long long time: since the late 80s). I don't do any of these things. If the author is going to make a convincing case his way is "right" and other ways are "wrong", it's incumbent upon him to clearly state what failures I will avoid. Then I can evaluate how often I encounter them, and decide how important this advice is. As stated, it doesn't seem very important.
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.
> 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")
.DELETE_ON_ERROR is not sufficient. Some day your make process will be killed (due to a bug, OOM, kernel crash, power loss, whatever) while the recipe is running, thus having no chance to delete the broken output.
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.
> .DELETE_ON_ERROR is not sufficient. Some day your make process will be killed (due to a bug, OOM, kernel crash, power loss, whatever) while the recipe is running, thus having no chance to delete the broken output.
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.
I agree that the "you're doing it wrong" tone of the article title is off-putting and that if you're just using Make for some minor automation once in a while here and there, you probably shouldn't worry, but I found most of the tips genuinely helpful and the reasons for doing so are stated or obvious.
I feel the same way. I strongly believe that choice of language makes a huge difference in your material’s reception. Any time I am told that something I’m doing is wrong by an article, an inanimate piece of text that clearly has no cognition thus no idea what I’m doing or not doing, I think that the person who wrote it, is, in fact, communicating wrong.
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.
> I strongly believe that choice of language makes a huge difference in your material’s reception.
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.
> If the author is going to make a convincing case his way is "right" and other ways are "wrong", it's incumbent upon him to clearly state what failures I will avoid.
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.
> 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
And I think it’s important to read the article before writing comments based purely on the title, but here we are.
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.
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 think that this is one of actually nicer articles about using Make (and I think that developers should use it in more roles than a glorified task runner. It can do more, look at buildroot!), but the pontificating headline is quite off putting.
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.
The RECIPEPREFIX was my first clue that I should not go on reading. And yet, I did, and lo and behold, came to the comments page here to witness most people agreed.
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.
Can you explain this a bit further? I don't think I understand the point, but I've been using docker more lately and this sounds like it could possibly be something I could use. It sounds like I could do something like:
Please don't tell people "you are wrong / don't do X", especially if it's not objectively true. It's negative, rude, judgemental, and bossy. Find a kinder way to make your point and people might listen to you. (If you don't care if people listen to you, why are you speaking?)
> .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.
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've been using make for a while now, but have never seen the $(@D) variant for target. I didn't know about .RECIPEPREFIX, either. Does anyone know if these were additions in the recent versions of make?
I could only find release notes in the mailing list [0]. Initial searches didn't turn up anything.
I've heavily relied on GNUMake myself in a commercial project. But when I encounter a software that does require a GNUMake >= a specific version and on top bash instead of Posix SH, I must be quite desperate to install this just to build some software. My excuse was that I was building against 80 different target devices (it was j2me development) on some under-powered machines and that there was no budget for cutting down build times by several orders of magnitudes compared to ant. What's the author's excuse?
> GNUMake >= a specific version and on top bash instead of Posix SH
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
My development environment is FreeBSD, OpenBSD and Linux; I am deploying to non-linux unixes only. 99% of the patches I have to do to build systems are one-liners that explicitly spell out what syntactic sugar the newest feature of gmake/bash supported or that fixes what a linux-only developer considers to be "the standard" (and let's talk real, it's not only "linux-only" anymore, one has to say "this-specific-linux-distro-only"). And that is on top of the fact that it is not about "running the new versions" but actually installing non-standard software.
I don’t mind sticking to bash, but some of the other choices seem overly opinionated. In my experience the best Makefiles just invoke other commands, so you’re only using make for its dependency tracking. That way the Makefile remains fairly simple and you can use something more sensible (bash instead of bash-in-make, python, whatever) to write your actual build logic.
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.
A little trick to rebuild targets based on the build options: use .VARIABLES and pipe to sha256sum to create an option-dependent suffix for all built files:
> .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.
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.
I mostly use make like a package manager agnostic version of the "scripts" section in package.json, and some of the recommendations here are like a revelation for me. I didn't know you could change the tab character to a > for the commands, that's such a great improvement imo.
The trouble with makefiles is that they're supposed to specify dependency relationships, but they're used as a procedural scripting language because the dependency system isn't very smart.
Assuming that Makefiles aren't going away any time soon, I wonder if the situation here calls for a linter/formatter that can parse a makefile and optimize it toward "best practices"...
if you're building C please try to use the built-in rules. I often come across reimplementations that miss or disorder one of the flag sets and it becomes a pain when porting or integrating.
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%
This is really cool! I've always worked in interpreted code so this isn't that useful to me but I'm surprised that it exists.
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.
I've never written a non trivial Makefile, because all my non trivial Makefiles are generated by CMake. I'm interested in why my CMakes are wrong since it's a way more complicated tool.
For some reason RECIPEPREFIX has gotten really popular lately. Has OS X finally upgraded their version of Make, or are people no longer using the default Make on OS X at all?
Looking at bazel's complexity, and its popularity, one should see that Makefile and its dependents, which forces upon writer a centralized model (where one Makefile dictates how to build all code files scattered in subdirectories), cute but unreliable grammar (tab vs. space), unnecessarily convenience features (for god's sake, I never managed to learn any thing beyond simple target & deps, and things like PHONY target are just beyond my mental capacity).
I hate developers like this. This is the attitude of every weirdo developer I've had to work with who thought they were brilliant instead of just using the stupid tool as it was intended.
I don't know, I can't really argue with .SHELLFLAGS = -euo pipefail.
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.
More typically, the install instructions read like the bomb-defusal instructions in the classic MASH episode. "Cut red wire, then unscrew detonator subassembly. But first..."
> "Use a recent bash" seems like a matter of preference.
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 to read something good, check out the docs for this Landlock Make project I've been working on the past week. https://github.com/jart/landlock-make It has many more features since it was announced on August 7th.
Honestly his takes (mostly) aren't wrong or niche (at least not all of them; a few like the tabs thing are debatable). Some of these you only realize once you've wrestled with Make for a while and spent time thinking about the underlying problem, which many don't do. Knowing some of these
ahead of time can save quite a bit of a headache. They're worth considering even if you don't follow all of them.
It would have been classier if they'd replaced the tab character with an emoji. That's what someone on Lobsters recommended, once I told them about .RECIPEPREFIX.
That's a strange take on the post. The post only uses built-in functionality. Options exist for a reason, and defaults are _really_ difficult to change in software used by millions of people.
What about them? I don't use Python or Golang, but I install the requested versions of each if I need to do something that uses them. Folks who don't use GNU Make can do likewise. It's not a big deal.
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.
It is of course up to the coder, what platforms they want to support.
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).
They usually don't matter (and unseen-untested portability is simply wishful thinking), but particularly don't matter for projects you're responsible for and decided they don't matter. That doesn't mean you must randomly preempt portability, of course.
I felt the opposite, I (among many) was winging my makefiles due to unfinished learning. I always appreciate high precision posts like these (even if some of the points are above the top).
It's high precision of a flawed view (in my opinion, of course) that should only be taken for what they are, an opinion.
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.
People should be a little more forgiving. Even after reading emacs Manual thoroughly 3 times, I still managed to miss a ton of nuances and information. Same goes for make or other large old tools I guess.
Well, I think my point of learning from how people actually use make in the real world to be perfectly valid for learning, as is learning any kind of language/tool.
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?
When you come at people with the attitude the author has, you should expect it right back at you. That is why anyone who knows how to give a proper talk or write a professional paper does not do that. Leave it to popular culture sites to treat people like that, it should have no place in professional presentations.
> When you come at people with the attitude the author has, you should expect it right back at you.
The attitude being: a clickbait title immediately dismissed as "an opinion" -->
An opinionated approach to writing (GNU) Makefiles that I learned [..]
Use the above as guidelines, not dogma
But maybe you haven't read the article and was just offended by the title. Still not a good reason to insult people like you did. The author of the article never insulted anyone.
> it should have no place in professional presentations
Good thing then that this article is posted on a personal website. It's not even the author of the article who posted the link on HN.
Can't believe the number of people s**** on this article who obviously don't have enough experience with build systems or the problems tab and space mixing cause. Quality has really gone down on HN comments in the last few years.
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.
No company I have worked for in the past 10 years has used Make at all, other than when I'm using it, and that's because I'm just old and used to it. Developers use build tools designed for their language, Syseng use ci/cd and shell scripts, very large teams standardize on some "modern" overcomplicated monstrosity.
I agree the new systems are overly complex, but the reason they stick is enforcement of dependencies being accurate. If someone can bolt that into make it will be a huge improvement.
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?