Just vs GNU Make — A Detailed Comparison
Overview
Just and GNU Make are often mentioned in the same breath, but they solve fundamentally different problems. Make is a build system designed around dependency graphs and file timestamps, originally created for compiling C programs. Just is a command runner designed for executing project-specific recipes and tasks.
This distinction matters: Make excels when you need to track which files have changed and conditionally rebuild only what's necessary. Just excels when you want to run commands reliably and ergonomically, without fighting against implicit behavior and cryptic syntax.
Philosophy and Design Goals
GNU Make
- Purpose: Dependency-based build system for incremental compilation
- Model: Declare targets as depending on prerequisites; Make rebuilds only if prerequisites are newer
- History: Created 1976 for managing C compilation pipelines
- Design: Maximizes efficiency by avoiding unnecessary work
- Paradigm: Declarative (you describe dependencies, Make figures out the order)
Just
- Purpose: Command runner for project automation and task execution
- Model: Recipes that explicitly run when invoked or when dependencies change
- History: Created 2016 as a spiritual successor to Make for modern projects
- Design: Maximizes usability and clarity
- Paradigm: Imperative (you describe what commands to run and in what order)
Syntax Comparison
Basic Recipe/Target
Makefile:
build:
cargo build
test:
cargo test
justfile:
build:
cargo build
test:
cargo test
Key difference: Make requires literal tabs for indentation (a historical quirk). Just uses spaces.
Variables
Makefile:
RUST_VERSION := 1.70
BIN_NAME = myapp
build:
rustc --version $(RUST_VERSION)
cargo build --out-dir bin
justfile:
RUST_VERSION := "1.70"
BIN_NAME := "myapp"
build:
rustc --version {{RUST_VERSION}}
cargo build --out-dir bin
Key difference: Just uses {{}} for interpolation (Jinja-inspired). Make uses $() or ${}.
Arguments/Parameters
Makefile:
deploy:
@echo "Deploying to $(ENVIRONMENT)"
./deploy.sh $(ENVIRONMENT)
# Invoked as: make deploy ENVIRONMENT=production
justfile:
deploy ENV:
echo "Deploying to {{ENV}}"
./deploy.sh {{ENV}}
# Invoked as: just deploy production
Key difference: Just has first-class parameter support. Make requires passing as environment variables.
Dependencies
Makefile:
build/app: src/main.rs src/lib.rs
cargo build --release
dist: build/app
mkdir -p dist
cp build/app dist/
justfile:
build_app: src_files
cargo build --release
src_files:
@test -f src/main.rs && test -f src/lib.rs
dist: build_app
mkdir -p dist
cp build/app dist/
Key difference: Make tracks file timestamps automatically. Just requires explicit dependency recipes or external dependency management.
Shell Integration
Makefile:
all_tests:
for test in tests/*.sh; do \
echo "Running $$test"; \
bash "$$test" || exit 1; \
done
justfile:
all_tests:
for test in tests/*.sh; do
echo "Running $test"
bash "$test" || exit 1
done
Key difference: Just recipe bodies are standard shell scripts. Make requires escaping line continuations.
Feature Comparison Table
| Feature | Make | Just |
|---|---|---|
| Tab indentation required | Yes (gotcha!) | No |
| Parameter passing | Via env vars or args | Native parameters |
| String interpolation | $(), ${} | {{}} |
| .env file support | No (manual) | Yes (built-in) |
| File timestamp tracking | Yes (core feature) | No |
| Recipe listing | No (must grep Makefile) | Yes (just --list, just --choose) |
| Cross-platform | Generally yes | Excellent |
| Error messages | Terse, cryptic | Clear, helpful |
| Default recipes | No (need .PHONY) | All recipes opt-in |
| Conditional logic | Yes (limited) | Yes (full shell + extras) |
| Recipe documentation | Comments only | Doc attributes (///) |
| Modules/imports | Limited | Yes (native support) |
| Implicit rules | Yes (can be surprising) | No |
| Recipe confirmation prompts | No | Yes (--confirm) |
| Stdin passing | Not native | Supported |
| Private recipes | No (convention: prefix with .) | Yes (prefix with .) |
Where Make Excels
-
Ubiquity: Installed on virtually every Unix system by default. Just must be installed separately.
-
Dependency Graph: File timestamp-based incremental builds are genuinely useful for compilation-heavy projects. Make's core strength.
-
Pattern Rules:
%.o: %.csyntax elegantly handles many-to-many file transformations (e.g., compile all .c files to .o files). -
Ecosystem: Decades of tooling, libraries, and documentation. Many projects assume Make is available.
-
Complex Pipelines: For large, interdependent build systems (Linux kernel, LLVM), Make's graph model is superior.
-
Parallel Execution:
-jflag for parallel builds is well-integrated and reliable.
Where Just Excels
-
Usability: No tab indentation requirements. Error messages are helpful. Recipes are self-documenting.
-
Modern Task Runner: Built-in parameter passing,
.envloading, confirmation prompts, and interactive recipe selection (--choose). -
Cross-Platform: No platform-specific pitfalls. Works identically on macOS, Linux, Windows.
-
Simplicity: No implicit behavior. What you write is what runs. No surprising "recipe already run" behavior.
-
Recipe Documentation: The
///doc syntax generates help text automatically. -
String Interpolation: Cleaner than Make's
$()soup, especially with nested variables. -
Modules: Import recipes from other justfiles, enabling reusability.
-
Developer Experience:
just --list,just --edit,--dry-run, and better diagnostics.
Common Gotchas When Switching
From Make to Just
-
No automatic file timestamp tracking: Just doesn't watch file modifications. Use separate tools (Cargo, webpack) or script the dependency logic.
-
Recipe names: Just treats all recipes equally; there's no
.PHONYconvention. Recipes named after files can shadow real files. -
Whitespace: Just uses spaces, but tabs are still conventional in some projects. Be consistent.
-
No implicit rules: You can't write
%.o: %.cpatterns. Use a loop in a recipe instead.
From Just to Make
-
Tab indentation: The first character of a recipe line must be a literal tab, not spaces. This is a common source of
[tab] (did you mean TAB?)errors. -
Variable expansion timing: Make expands variables at parse time (
:=) or at use time (=). This can be confusing. -
PHONY targets: Without
.PHONY, Make treats recipe names as files. Adding.PHONY: build testis essential for command-like targets. -
Default recipe: Make runs the first target by default. Organize your Makefile accordingly.
-
Shell differences: Each recipe line runs in a fresh shell. Use
\continuations or wrap in a single shell command with;.
When to Use Which
Use Make when:
- Compiling C/C++/other languages with incremental builds
- File timestamp-based dependency tracking is critical
- You need to be guaranteed availability on the target system
- You're maintaining a large, complex build graph
- You need parallel builds with
-j
Use Just when:
- Running project-specific commands and scripts
- Building a human-facing CLI for developers
- You want clarity and predictability over implicit behavior
- Cross-platform consistency matters
- Your project is newer or doesn't have decades of Make infrastructure
Use Both Together:
It's common to have a justfile that wraps make targets, or a Makefile that delegates to just recipes. Neither is absolute.
build: compile_c
just run_tests
compile_c:
@make -C src
run_tests:
@cargo test
Migration Guide: Makefile to justfile
Before (Makefile)
.PHONY: build test deploy clean
ENVIRONMENT ?= development
BUILD_DIR = build
build:
cargo build --release
test: build
cargo test --release
deploy: test
./scripts/deploy.sh $(ENVIRONMENT)
clean:
rm -rf $(BUILD_DIR)
cargo clean
After (justfile)
set default-recipe := "build"
ENVIRONMENT := env_var_or_default("ENVIRONMENT", "development")
BUILD_DIR := "build"
build:
cargo build --release
test: build
cargo test --release
deploy ENV=ENVIRONMENT: test
./scripts/deploy.sh {{ENV}}
clean:
rm -rf {{BUILD_DIR}}
cargo clean
Related Tutorials
- [[just-beginner-guide|Just Beginner Guide]]
- [[just-deep-dive|Just Deep Dive]]
- [[linux-permissions-beginner-guide]]
- [[dotfiles-beginner-guide]]
- [[kubernetes-beginner-guide]]
References
- Just Documentation: https://just.systems
- GNU Make Manual: https://www.gnu.org/software/make/manual/
- Just GitHub: https://github.com/casey/just
- Just vs Make Discussion: https://github.com/casey/just/issues/1422
Summary
Make and Just solve different problems. Make is a dependency-based build system optimized for incremental compilation and complex build graphs. Just is a command runner optimized for clarity, ease of use, and running project-specific tasks.
Choose Make for compilation pipelines where file timestamps matter. Choose Just for running commands, managing project workflows, and improving developer experience. Modern projects often use Just as their primary interface with optional Make underneath for C/C++ compilation.
The key insight: they're not competitors in most projects—they complement each other.