Skip to main content

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

FeatureMakeJust
Tab indentation requiredYes (gotcha!)No
Parameter passingVia env vars or argsNative parameters
String interpolation$(), ${}{{}}
.env file supportNo (manual)Yes (built-in)
File timestamp trackingYes (core feature)No
Recipe listingNo (must grep Makefile)Yes (just --list, just --choose)
Cross-platformGenerally yesExcellent
Error messagesTerse, crypticClear, helpful
Default recipesNo (need .PHONY)All recipes opt-in
Conditional logicYes (limited)Yes (full shell + extras)
Recipe documentationComments onlyDoc attributes (///)
Modules/importsLimitedYes (native support)
Implicit rulesYes (can be surprising)No
Recipe confirmation promptsNoYes (--confirm)
Stdin passingNot nativeSupported
Private recipesNo (convention: prefix with .)Yes (prefix with .)

Where Make Excels

  1. Ubiquity: Installed on virtually every Unix system by default. Just must be installed separately.

  2. Dependency Graph: File timestamp-based incremental builds are genuinely useful for compilation-heavy projects. Make's core strength.

  3. Pattern Rules: %.o: %.c syntax elegantly handles many-to-many file transformations (e.g., compile all .c files to .o files).

  4. Ecosystem: Decades of tooling, libraries, and documentation. Many projects assume Make is available.

  5. Complex Pipelines: For large, interdependent build systems (Linux kernel, LLVM), Make's graph model is superior.

  6. Parallel Execution: -j flag for parallel builds is well-integrated and reliable.

Where Just Excels

  1. Usability: No tab indentation requirements. Error messages are helpful. Recipes are self-documenting.

  2. Modern Task Runner: Built-in parameter passing, .env loading, confirmation prompts, and interactive recipe selection (--choose).

  3. Cross-Platform: No platform-specific pitfalls. Works identically on macOS, Linux, Windows.

  4. Simplicity: No implicit behavior. What you write is what runs. No surprising "recipe already run" behavior.

  5. Recipe Documentation: The /// doc syntax generates help text automatically.

  6. String Interpolation: Cleaner than Make's $() soup, especially with nested variables.

  7. Modules: Import recipes from other justfiles, enabling reusability.

  8. Developer Experience: just --list, just --edit, --dry-run, and better diagnostics.

Common Gotchas When Switching

From Make to Just

  1. No automatic file timestamp tracking: Just doesn't watch file modifications. Use separate tools (Cargo, webpack) or script the dependency logic.

  2. Recipe names: Just treats all recipes equally; there's no .PHONY convention. Recipes named after files can shadow real files.

  3. Whitespace: Just uses spaces, but tabs are still conventional in some projects. Be consistent.

  4. No implicit rules: You can't write %.o: %.c patterns. Use a loop in a recipe instead.

From Just to Make

  1. 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.

  2. Variable expansion timing: Make expands variables at parse time (:=) or at use time (=). This can be confusing.

  3. PHONY targets: Without .PHONY, Make treats recipe names as files. Adding .PHONY: build test is essential for command-like targets.

  4. Default recipe: Make runs the first target by default. Organize your Makefile accordingly.

  5. 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
  • [[just-beginner-guide|Just Beginner Guide]]
  • [[just-deep-dive|Just Deep Dive]]
  • [[linux-permissions-beginner-guide]]
  • [[dotfiles-beginner-guide]]
  • [[kubernetes-beginner-guide]]

References

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.