Qvantor logo QvantorInfrastructure, explained plainly
Dev Tools

Makefile Task Runner Setup for Small Projects

Open the README of almost any small project and you will find a block of seven or eight shell commands under a heading like "Getting started". Half of them are out of date.

Terminal window listing make targets for setup, dev, test, lint and clean in a small code project

Open the README of almost any small project and you will find a block of seven or eight shell commands under a heading like "Getting started". Half of them are out of date. The person who wrote them left months ago, and the new hire spends their first afternoon guessing which flags still matter.

A Makefile task runner fixes that with one boring file that sits in the repository root. Type make test or make dev and the right thing happens, on every laptop and in CI. I have set this up on a dozen small codebases, and the pattern that survives is short, plain and slightly unfashionable. Here is how to build one people keep using.

Why a Makefile Task Runner Beats a README Full of Commands

Documentation describes what someone should run. A Makefile is what they actually run. That difference matters because commands that are executed every day get fixed the moment they break, while commands that live in prose quietly rot.

Make is also already there. It ships with every Linux distribution and with the Xcode command line tools on macOS, so nobody installs anything. Compare that with task runners that need their own package, their own version pin and their own config syntax. For a team of two to ten people, fewer moving parts wins.

It is also discoverable. A new developer can type make help and see every supported task with a one line explanation. That single command replaces a page of onboarding notes.

The Five Targets Almost Every Project Needs

Resist the urge to wrap every command you have ever typed. Start with five targets and add more only when someone asks twice for the same thing.

  • setup installs dependencies and creates any local config file from a template.
  • dev starts the app with hot reload or whatever local server you use.
  • test runs the full test suite exactly the way CI runs it.
  • lint runs formatters and static checks, ideally in check mode rather than rewrite mode.
  • clean removes build output, caches and temporary databases.

On one Python service I maintain, those five targets cover roughly 90 percent of what anyone types in a week. The remaining tasks, such as database migrations or seeding test data, earned their place later because people kept asking in chat how to run them.

Writing Targets That Do Not Surprise Anyone

Make was built to compile C files, so a few of its defaults trip up anyone using it as a task runner. Three habits remove most of the surprises.

Declare phony targets

If a folder called test ever appears in the repository, Make will decide the test target is already up to date and skip it. Listing your task names on a .PHONY line tells Make they are actions, not files. Put that line at the top so it is impossible to miss.

Use one shell per recipe when you need it

By default each line of a recipe runs in a fresh shell, so changing directory on one line has no effect on the next. Either keep related commands on a single line or enable the .ONESHELL setting. I prefer single lines, because they behave the same on older versions of Make that some CI images still carry.

Indent with tabs, and tell your editor

Recipes must start with a tab character. Spaces produce the famous "missing separator" error. Add a rule to your editor config file so every contributor gets tabs in Makefiles automatically. That one line saves more frustration than any other tip here.

A Self Documenting Help Target

The trick that makes a Makefile feel friendly is a help target that reads comments from the file itself. Write a short description after two hash marks on the same line as each target, then have the help target print every line that matches that pattern. Make help the default goal so typing a bare make shows the menu instead of running something unexpected.

The result looks like this when a new teammate runs it:

CommandWhat it does
make setupInstall dependencies and copy the example env file
make devRun the app locally on port 8000
make testRun unit and integration tests like CI
make lintCheck formatting and types without changing files
make cleanDelete caches, build output and the local test database

The Advice I Ignore: Do Not Put Everything in Make

You will read that Make should orchestrate your entire workflow, from Docker builds to deployments to release notes. I disagree, at least for small teams.

Make is a thin layer. Once a target grows past about ten lines, or needs loops, error handling and conditionals, it becomes hard to read and harder to debug. Move that logic into a script in a scripts folder and have the target call it with a single line. The Makefile stays a readable index of tasks, and the real logic lives in a language with proper tooling.

Deployments are the other exception. A deploy that can be triggered by a stray make in the wrong terminal is a risk I would rather not carry. Keep production deploys in CI, behind a review step, and leave the Makefile for things that are safe to run twenty times a day.

Making CI and Laptops Run the Same Commands

The biggest payoff arrives when your CI configuration calls the same targets developers use. Instead of copying the test command into a pipeline file, the pipeline step simply runs make test. When someone adds a new flag, both places pick it up at once.

This closes one of the most common gaps I see, the slow drift between how code runs locally and how it runs on the server. If that problem sounds familiar, our piece on how a local setup drifts away from production covers the wider causes. Shared Makefile targets will not fix every difference, but they remove the cheapest one.

The same targets fit neatly into commit checks too. A lint target that finishes in under five seconds is a good candidate for the hooks described in our guide to pre-commit hooks small teams keep using.

Windows, Containers and Other Friction Points

Make is not installed on Windows by default, and that is the most common objection I hear. In practice most developers on Windows already work inside WSL or a dev container, where Make is one package install away. If a teammate refuses both, a short PowerShell script that mirrors the five core targets is an honest fallback, though you now maintain two files.

When the project runs inside Docker, have each target call the container rather than the host tools. For example, the test target runs the test command through your compose file. Developers then need only Docker and Make on their machine, which shrinks setup from an afternoon to about ten minutes.

FAQ

Is Make too old for modern projects?

Its age is an advantage here. The syntax for simple task targets has not changed in decades, it runs everywhere, and it adds no dependency to your project.

Should I use a newer task runner instead?

If your team already loves one, keep it. Switch to Make only when the current tool adds install steps or config that nobody understands. The pattern matters more than the program.

How many targets is too many?

Once the help output scrolls off a laptop screen, around twenty targets, prune it. Group rarely used tasks behind scripts and keep the menu focused on daily work.

Start With One File This Week

Pick the five commands your team runs most, wrap each in a target with a two hash description, add the help target as the default, and point CI at the same names. Then delete the stale command block from the README and replace it with one line: run make. For more practical tooling ideas, browse the dev tools section.

MQ
Marisa Quan

Marisa hosts game servers for her friends and writes about latency, uptime and the people who keep servers alive.

More posts by Marisa

More in Dev Tools

01doorganico 084goad 3hot8 4465456 after5internprogram ahandfulofblessings ahmadpc ahomcybersecurity allodialresources annodex antiquesandperiodcollectibles aotc asianworldkashmir beedlectrics bloggertemplatefree bluefeatheragency bodyweightsolution campaign08blog carfixadvisor catstreetcollective cbstvsports cnfanspurchasing committeeof500yearsofdignityandresistance covertopsdjs cs-kaiyuangame cuestacomunicaciontotal cxoutlet dancebetgame deskgearhub destinyvillastay detectmagazine di33 diamondsteelbuildings donmo dyanadropship emarsysfinance emimattl escapechan eskitarih evautoplay everythingweddings-orlando farmzone feedviewerapp foxstudiosscratch gamevui123 gleasongrade greetingsday hacklabasu haskell iw-kaiyun jagoanproperti jilidskripsi joltbusinessfinance juegodepokergratis juegospucca kaeru-festival latestmehndidesigns lestanzediottaviano litemagazines localcommunityhero lustrsazan mariashebets menswheels newwebstechs1 nikkit ootheday oxigenvpn plurfitness prefetial probudgetcarpetcleaning psychicmrsmartin pyaten rejoignezlogis rexzbot riselayouts ruchin rwxqsgd5 sakaltime shibiaowang shop-bot skrtips strcoding tetapatrium topselller uneplanteartificielle vipertechcr webtech1223411 wo-kygaming yamaha-mekar yeknava yoybuysheets zenunwa yamaha-mekar jilidskripsi ankertoto ankertoto ankertoto ankertoto