On the search for a Make replacement
Make is great, but there are some issues with it that are probably impossible to fix now. So I’ve been looking for a replacement that I can use for simple task automation; surely in the 40+ years of Make’s lifetime someone has written something better, right?
These are the notes I made while evaluating the different options that I explored. I’m interested to see if anyone has comments, corrections, or other suggestions.
Make
- Implemented in: Various programming languages (probably mainly C).
- Script language: Make (various dialects).
- Metaprogramming: Yes (major implementations).
- Biggest issue: Stringly typed.
Good old Make. Very nice for all sorts of tasks, until you need to deal with files containing space and/or quote characters, where things start to go downhill.
Ninja
- Implemented in: C++.
- Script language: Ninja.
- Metaprogramming: No.
- Biggest issue: Not programmable.
This is only mentioned for completion because Ninja is in a completely different ballpark. It’s meant to be a target language for buildfile generators like CMake and Meson, so by itself it has zero programmability, and you wouldn’t really want to write it by hand.
Just
- Implemented in: Rust.
- Script language: Just (Make-like).
- Metaprogramming: No.
- Biggest issue: Stringly typed.
This feels like a Make variant with fewer features than most Make implementations. I don’t see this as a practical choice for any project, at least right now.
Task
- Implemented in: Go.
- Script language: YAML + Go template.
- Metaprogramming: No.
- Biggest issue: Stringly typed.
I actually really like the idea of Task. For very simple use cases it’s very elegant, because its whole syntax is
just 100% valid YAML with a bit of string templating. The templating sometimes gets in the way, though, because the
use of {{.VAR}}
for variables
conflicts with YAML’s
{a: b}
map syntax, forcing you to waste one level of string quoting on it.
A bigger flaw is that there’s no easy way to override variables from the command line. I think you can work around this by jumping through scripting hoops, but then you lose a ton of elegance points.
And the biggest flaw: it’s still stringly typed just like Make, so you’ll have trouble separating strings from lists.
Gulp
- Implemented in: JavaScript.
- Script language: JavaScript.
- Metaprogramming: Yes.
- Biggest issue: Doesn’t track outputs?
This looked promising until I noticed that outputs didn’t seem to be tracked anywhere, which means everything gets rebuilt all the time. Is this really the case or am I missing something?
Grunt
- Implemented in: JavaScript.
- Script language: JavaScript.
- Metaprogramming: Yes.
- Biggest issue: Convoluted just to get started.
Grunt’s documentation is rather bad, and the examples they have all throw you in the deep, ugly end. Skimming these introductory materials, I couldn’t figure out how to write the simplest build file, which seems a bad sign.
Rake
- Implemented in: Ruby.
- Script language: Ruby.
- Metaprogramming: Yes.
- Biggest issue: Ruby’s
shell
module is broken on Windows.
This one looked very promising. I’m not a fan of Ruby, but was willing to put up with it because Rake
seemed to do all I wanted. But then I discovered that Ruby’s most reasonable subprocess handler, the
shell
module, breaks on Windows. Without it, you’re back to various
ugly half-baked APIs, each
with their own limitations.
SCons and Waf
- Implemented in: Python.
- Script language: Python.
- Metaprogramming: Yes.
- Biggest issue: Not designed for simple tasks.
Two competing Python-based build systems. These seem too complicated for my use cases. I think making them suit simple tasks would be a significant undertaking. Or perhaps I’m just missing a documentation that is not mainly targeted at people trying to create a build pipeline for their C projects.
doit
- Implemented in: Python.
- Script language: Python.
- Metaprogramming: Yes.
- Biggest issue: Verbose.
With this one you end up with lots of boilerplate because rather than writing tasks, you’re writing task creators. It makes sense, but it feels like doing things at a too-low level when you want it to be a simple Make alternative.
The author of doit suggests several high-level interfaces that can be implemented on top of doit. They do limit what you can do, but you can always write normal doit task creators in addition to the simplified versions. I think this is a reasonable compromise and I particularly like the decorator version.
The only remaining problem, then, is that Python’s subprocess handling is very cumbersome. There are two libraries
I know of that can rectify this: sh and Plumbum. sh, in my opinion, is
not suitable for use in a Make replacement use case. The way it does piping by default is not in line with what we
expect, coming from Make. Plumbum is not perfect but better (you still
have to end everything with .run_fg()
or the magical & FG
).
A quirk of doit is that it creates a cache file (or files) alongside your build file. Depending on the exact database backend used, it can create up to three files, which I’d say is not ideal.
Conclusion
I have for now settled on doit + Plumbum with around 100 lines of support code. I’m not fully happy with this, and I’m not sure it can cover all my use cases, but I think it’s time for me to put my ideas and investigations out there and seek comments.
Rake is almost what I need, if not for what I believe is a bug in Ruby’s standard library. But even if it’s fixed, I’d prefer to stick with a Python-based solution if possible.