Running common project tasks—starting dev servers, running tests, managing containers—seems simple until you work across multiple codebases with different tech stacks. Some projects use build tools or command runners, others rely on language-specific tooling, and some have custom shell scripts.

The inconsistency means constantly relearning how each project works: Is it make run? mix phx.server? npm start? For engineers working across different technologies, this cognitive overhead adds up.

This post explores three approaches to task running—Make, language-specific tools like Mix, and Just—comparing their strengths, weaknesses, and when each makes sense. The goal is to help you understand the tradeoffs so you can make informed decisions for your projects, whether you’re setting up a new repo or reconsidering your current toolchain.

Make

Make is the grandfather of build automation tools, dating back to 1976. It uses Makefile syntax to define targets and their dependencies, executing shell commands to build your project.

Strengths: Make is ubiquitous—it’s pre-installed on virtually every Unix-like system, making it incredibly portable. It excels at incremental builds through its dependency tracking system, only rebuilding what’s changed. The syntax, while quirky, is well-documented and understood by generations of developers. It’s genuinely language-agnostic since it just runs shell commands.

Weaknesses: The syntax is notoriously finicky, particularly around tabs versus spaces (Makefiles require actual tab characters for indentation, which trips up newcomers constantly). Cross-platform support is problematic—Windows doesn’t include Make by default, and shell commands often need platform-specific variations. The dependency resolution can be confusing for complex projects, and error messages are often cryptic.

Mix (Elixir)

I’m highlighting Mix here because Elixir has been the primary language for my current gig, but this discussion applies equally to NPM scripts, Cargo (Rust), or any other language-specific tooling.

Mix is Elixir’s built-in build tool and task runner, deeply integrated with the Elixir ecosystem—it handles dependencies, compilation, testing, and custom tasks seamlessly.

Strengths: The developer experience is excellent within its domain: tasks are discoverable (mix help), composable, and the syntax is clean Elixir code. For Elixir projects, it’s the obvious choice with zero configuration overhead.

Weaknesses: Mix is completely Elixir-specific. If your project involves multiple languages or technologies (frontend builds, Docker operations, database migrations in another language), you’re forced to either wrap everything in Mix tasks or maintain a separate task runner alongside Mix. This creates friction in polyglot codebases. Cross-platform support is good since Elixir itself is cross-platform, but you’re still limited to what Mix can express.

Just

Just is a modern command runner that aims to be a better Make—simpler syntax, better cross-platform support, and focused purely on running commands rather than build automation.

Strengths: Just has clean, intuitive syntax that’s easy to read and write. It handles cross-platform differences gracefully with built-in support for different shells and platform-specific recipes. The tool is truly language-agnostic—it just runs commands, so it works equally well for Python, JavaScript, Rust, or any combination. Error messages are clear, and it includes quality-of-life features like recipe parameters and recipe dependencies. It’s a single binary with no runtime dependencies, making installation straightforward.

Weaknesses: Just is relatively new (first released in 2016), so it’s not as universally available as Make—you need to install it explicitly. It deliberately avoids Make’s incremental build features, focusing instead on being a task runner rather than a build system. For complex build pipelines with intricate dependency graphs, you might need additional tooling. The ecosystem and community are smaller compared to Make.

Comparison Table

Feature Make Mix (or npm, etc.) Just
Installation Pre-installed on Unix systems Comes with language runtime Requires explicit install
Cross-platform Poor (Windows needs WSL/Cygwin) Good (if language is cross-platform) Excellent (native Windows support)
Syntax complexity High (tabs required, quirky rules) Low (native language syntax) Low (intuitive, forgiving)
Language-agnostic Yes No (tied to specific language) Yes
Incremental builds Yes (file dependency tracking) Varies by tool No (not designed for this)
Learning curve Steep Shallow (if you know the language) Shallow
Best for Complex builds with file dependencies Single-language projects Multi-tool orchestration, simple task running
Discoverability make help (if implemented) mix help, npm run just –list (built-in)
Community/ecosystem Massive (decades old) Varies by language Growing (relatively new)
When to use Actual build automation needs Pure language-specific work Cross-cutting concerns, polyglot projects

Which Tool To Use?

Here’s a practical framework for choosing the right tool for your project:

Start here: What kind of project is this?

Pure single-language project (only Elixir, only JavaScript, only Python, etc.)
→ Use the language’s native tooling (Mix, npm scripts, Poetry, etc.)
→ Only add Make or Just if you have operational concerns like Docker or Terraform

Polyglot project or single-language with cross-cutting concerns (e.g., Elixir + Terraform + Docker)
→ Use Just or Make for orchestration
→ Example: just serve starts Docker containers and runs mix phx.server

Do you need actual incremental builds with file dependency tracking?
→ Yes (compiling C/C++, processing assets, code generation): Use Make
→ No (just running commands): Use Just

Is cross-platform support critical? (team uses Windows, macOS, and Linux)
→ Just has the best cross-platform story
→ Make requires additional setup on Windows (WSL, Cygwin, etc.)

Conclusion

There’s no universally “right” choice here—each tool solves different problems well. Make excels when you need sophisticated build dependency tracking. Language-specific tools like Mix provide the best experience within their ecosystem. Just shines for modern, cross-platform task orchestration.

The key is matching the tool to your project’s actual needs rather than defaulting to what you know. A simple Elixir web app probably doesn’t need Just. A polyglot microservices setup with Docker, Terraform, and multiple languages probably shouldn’t rely on language-specific tooling alone.

In my current personal projects, I’ve found Just particularly valuable for bridging the gaps between different technologies—it’s become my go-to for projects that span beyond a single language runtime. But your mileage may vary depending on your team’s preferences and the complexity of your build requirements.

See you in the next post.