First, I grok make. I'm saying this from a position of familiarity, not of ignorance and fear.
Make is great at compiling code in languages that don't have bespoke build systems. If you want to compile a bunch of C, awesome. For building a Rust or JavaScript project, no way. Those have better tooling of their own.
So for the last 15 years or so, I've used make as a task runner (like "make test" shelling out to "cargo test", or "make build" calling "cargo build", etc.). As a task runner... it kinda sucks. Of course it's perfectly capable of running anything a shell script can run, but it was designed for compiling large software projects and has a lot of implicit structure around doing that.
Just doesn't try to be a build system. It's optimized for running tasks. Here, that means it provides a whole lot of convenient functions for path manipulation and other common scripty things. It also adds dozens of quality-of-life features that devs might not even realize they wanted.
For example, consider this trivial justfile:
# Delete old docs
clean:
rm -rf public
# This takes arguments
hello name:
@echo Hello, {{name}}
If you're in a directory with it and run `just --list`, it'll show you the list of targets in that file:
$ just --list
Available recipes:
clean # Delete old docs
hello name # This takes arguments
That second recipe takes a required command line argument:
$ just hello
error: Recipe `hello` got 0 arguments but takes 1
usage:
just hello name
$ just hello underdeserver
Hello, underdeserver
You can do these things in make! I've seen it! Just doesn't add things that were impossible before. But I guarantee you it's a lot harder to implement them in make than it is in just, where they're happy native features.
There are a zillion little niceties like this. Just doesn't try to do everything make does. It just concentrates on the smaller subset of things you'd put in .PHONY targets, and makes them really, really ergonomic to use.
You wouldn't use just to replace make in a large, complicated build. I would unhesitatingly recommend it for wrapping common targets in repos of newer languages, so that `just clean build test` does the same things whether you're in Python or TS or Rust or whatever, and you don't want to hack around all of make's quirks just to build a few simple entry points.
I don't really like Make as a build system, especially since today every language I've worked with has a much better one - cargo, uv, cmake, bazel, others.
My thing is that it's ubiquitous and if you stay simple, the syntax is perfectly readable; if you're doing anything complicated, I'll argue you don't want your logic in a Makefile anyway, you want it in a shell script or a Python script.
I get that if you want to do really complicated things, Just can be more ergonomic. But I haven't seen any real argument that it's more ergonomic, or understandable, or worth the learning curve, when your Makefiles are simple or have actual scripts doing the complicated stuff.
`just --list` is indeed missing (though I hear make is adding a --print-targets flag), but I usually need to run the same commands - make run, make test, make typecheck, make lint, make format. Note how none of these take an argument - that's also something I've never found myself needing.
That’s the thing: just is a lot simpler and cleaner for the simple things, too. Like not requiring .PHONY, for instance. And if you already have to install cargo/uv/cmake, add just to that list of dependencies.
The argument stuff is nice when you realize you’re no longer constrained by not having easy access to it. I used just to build my blog, and added a target to create a template entry whenever I updated to a new release of certain software. I could write “just newversion 1.2.3” to publish an announcement using the same mechanisms that did everything else. Without that feature, I could have scripted something up to do the same. With it, I didn’t have to. I wouldn’t have tried that with make.
Make is great at compiling code in languages that don't have bespoke build systems. If you want to compile a bunch of C, awesome. For building a Rust or JavaScript project, no way. Those have better tooling of their own.
So for the last 15 years or so, I've used make as a task runner (like "make test" shelling out to "cargo test", or "make build" calling "cargo build", etc.). As a task runner... it kinda sucks. Of course it's perfectly capable of running anything a shell script can run, but it was designed for compiling large software projects and has a lot of implicit structure around doing that.
Just doesn't try to be a build system. It's optimized for running tasks. Here, that means it provides a whole lot of convenient functions for path manipulation and other common scripty things. It also adds dozens of quality-of-life features that devs might not even realize they wanted.
For example, consider this trivial justfile:
If you're in a directory with it and run `just --list`, it'll show you the list of targets in that file: That second recipe takes a required command line argument: You can do these things in make! I've seen it! Just doesn't add things that were impossible before. But I guarantee you it's a lot harder to implement them in make than it is in just, where they're happy native features.There are a zillion little niceties like this. Just doesn't try to do everything make does. It just concentrates on the smaller subset of things you'd put in .PHONY targets, and makes them really, really ergonomic to use.
You wouldn't use just to replace make in a large, complicated build. I would unhesitatingly recommend it for wrapping common targets in repos of newer languages, so that `just clean build test` does the same things whether you're in Python or TS or Rust or whatever, and you don't want to hack around all of make's quirks just to build a few simple entry points.