Hacker News new | ask | show | jobs
by woodrowbarlow 765 days ago
people often turn to make as a taskrunner, but i've never understood why. i've heard people say "i don't want to add another tool/language/dependency to my project, so i'll just use make" but, usually, `make` is a new dependency for the project. `make` (realistically) excludes Windows users, most Linux distros don't ship it by default, it is a unique syntax (no, it's not shell). even in a C project, it's not a given that Make is already a dependency. in languages like python or javascript, with rich package ecosystem, `make` makes even less sense; why limit your users to one OS, and why not write your tasks in your project's native language? as a taskrunner, `make` has so many limitations -- because that's not what it's designed for. `make` is good for tracking dependencies between files that are generated from other files, and that's about it.

a good taskrunner makes it easy to run a task while still exposing the tool's underlying flexibility. a good taskrunner lets me invoke the entire test suite with a short command, but also allows me to add custom options and arguments to, say, run a specific test case in an alternate environment.

`make` fails to expose the tools' underlying flexibility. sure, you can write a .PHONY target to run the full test suite, but `make` can't handle passing options or arguments (besides cumbersome Makefile variables).

a makefile tends to obscure the underlying tool, enshrining its launch arguments. anyone who has tried to cross-compile a makefile codebase authored by someone who didn't consider cross-compilation understands what i mean (you'll end up re-writing the Makefile 90% of the time).

4 comments

I think the thing is that most people do have make installed, and if not it's on every package manager ever. It's a language agnostic tool that can bootstrap the language/environment. For most use cases as a task runner, the differences between various flavors of make don't matter.

I agree with _everything_ else you've said though.

i agree that it's common to encounter developers who already have `make` installed, and developers who feel comfortable enough to read/edit a Makefile. however, i think most of those are merely leveraging their knowledge of shell scripting rather than knowledge of Makefile language (which is similar enough to have scary pitfalls) -- so i'm not sure i agree with "language agnostic". it's an extra language. i wouldn't necessarily expect (say) a frontend developer to have `make` installed, or even be comfortable with shell syntax.
I think you’re right about leveraging their shell knowledge, but I think that’s the reason make works. It is just enough shell that it’s grokkable.

I think it’s fairly likely that a FE dev would have make installed, but also that it’s likely easier to install make than it is to install go or dotnet on a specific version, and then use a makefile to install the dependencies to run the backend locally.

make is part of POSIX; if you assume already POSIX environment for your software then make is not an additional dependency. And that also indicates that make is in no way Linux-only, it is widely used on BSDs and UNIXes of all flavours.

I don't like make for various reasons, but portability is not one of them.

ahh -- a fellow C coder, i assume. :)

i admit my tirade is a little less applicable to C projects; many C projects are already using `make` for legitimate reasons, and at that point are already making some POSIX-y assumptions. so if that project's taskrunner needs are light, it really might make sense to just cram them in the Makefile and mark them .PHONY, despite make's shortcomings as a taskrunner.

my portability comments were really written with windows in mind.

You make valid points. I would, however say that I primarily use make as a task runner for simple projects. If I have a "larger" project, at some point I would also probably opt for something closer to that language to squeeze out as much as possible.

And no tool is perfect. I think it's all about balance. Example: I love Zig build tool, but I think it would scare some of the people who would just want to add something to the build pipeline and have no idea about Zig. Projects are rarely just the code they have. They have scripts and require installing additonal software.

But yeah, I agree with your points here.

Then what is a good task runner?
Not OP but whatever your language/ecosystem is written in. If you're in python use python, maybe with just. If you're in ruby use rake. If you're in a polyglot environment, pick the lowest common denominator.
For Python projects, I like to use Taskipy as the UX entry point. If any task itself is more than a one liner but doesn't need Python, I'll have it invoke a shell script from a mk folder. If any task itself requires an activated venv, I'll define it in a noxfile.

What it might look like in a pyproject.toml:

  [tool.taskipy.tasks]
  fmt = "nox -s fmt"
  lock = "nox -s lock"
  install = "if [ $VIRTUAL_ENV ]; then pip install -r local-requirements.txt; else printf '%s\n' 'Please activate a venv first'; return 1; fi"
  test = "nox -s test test_without_toml typecheck -p 3.12"
  docs = "nox -s render_readme render_api_docs"
For file generation tasks (the kind of stuff make is good at), I've been using Tup more and more. But recent versions aren't always packaged for distros, so I'm hoping it gets an asdf/mise plugin.
`just` is great, especially if you're using rust. if you're in python or javascript, it's well worth it to use a language-native runner (like pydoit / grunt). that way your "task" is just a function, it can take arguments (and you can define default values), you can even use something like `*args, **kwargs` to take any options/arguments, and then just pass them verbatim onto the subprocess.
I'm also a big fan of mise for managing development runtimes, and while I haven't tried its own task runner functionality, it does feature some: https://mise.jdx.dev/tasks/