<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>automation · wleeaf field notes</title><link>https://articles.wleeaf.dev/tags/automation/</link><description>Hands-on guides and build logs from the wleeaf studio: self-hosting, Linux, networking, Docker, the command line and reverse engineering.</description><language>en-us</language><lastBuildDate>Fri, 03 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://articles.wleeaf.dev/tags/automation/feed.xml" rel="self" type="application/rss+xml"/><item><title>Makefiles Are Not Just for C</title><link>https://articles.wleeaf.dev/posts/makefiles-beyond-c/</link><pubDate>Fri, 03 Apr 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/makefiles-beyond-c/</guid><description>Using Make as a project task runner when npm scripts and shell aliases fall short.</description><category>make</category><category>automation</category><category>workflow</category><content:encoded><![CDATA[<p>Every project accumulates a collection of commands. Build this, test that, deploy here, clean up there. Most people throw these into npm scripts, a shell script, or a README that nobody reads. I put them in a Makefile.</p>
<p>Not because I write C. Because Make is a task runner that is already installed on every machine.</p>
<h2 id="the-basics">The Basics<a class="anchor" href="#the-basics" aria-label="Link to “The Basics”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">build</span> <span class="n">test</span> <span class="n">deploy</span> <span class="n">clean</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">build</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	docker compose build
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">test</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	docker compose run --rm app npm <span class="nb">test</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">deploy</span><span class="o">:</span> <span class="n">build</span>
</span></span><span class="line"><span class="cl">	rsync -a --delete ./dist/ server:/opt/app/
</span></span><span class="line"><span class="cl">	ssh server <span class="s2">&#34;cd /opt/app &amp;&amp; docker compose up -d&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">clean</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	docker compose down -v
</span></span><span class="line"><span class="cl">	rm -rf dist node_modules
</span></span></code></pre></div>
</div>
<p>Now:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">make build
</span></span><span class="line"><span class="cl">make <span class="nb">test</span>
</span></span><span class="line"><span class="cl">make deploy
</span></span><span class="line"><span class="cl">make clean</span></span></code></pre></div>
</div>
<p><code>.PHONY</code> tells Make these targets are commands, not files. Without it, if you have a directory called <code>build</code>, Make would say &ldquo;build is up to date&rdquo; and do nothing.</p>
<h2 id="dependencies">Dependencies<a class="anchor" href="#dependencies" aria-label="Link to “Dependencies”">#</a></h2>
<p>The power of Make is in the dependency graph. <code>deploy</code> depends on <code>build</code>:</p>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nf">deploy</span><span class="o">:</span> <span class="n">build</span>
</span></span><span class="line"><span class="cl">	rsync -a --delete ./dist/ server:/opt/app/
</span></span></code></pre></div>
</div>
<p>When you run <code>make deploy</code>, it runs <code>build</code> first. If <code>build</code> fails, <code>deploy</code> never runs. This is what npm scripts cannot do cleanly.</p>
<h2 id="variables">Variables<a class="anchor" href="#variables" aria-label="Link to “Variables”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nv">SERVER</span> <span class="o">:=</span> myserver
</span></span><span class="line"><span class="cl"><span class="nv">REMOTE_DIR</span> <span class="o">:=</span> /opt/app
</span></span><span class="line"><span class="cl"><span class="nv">IMAGE</span> <span class="o">:=</span> myapp:latest
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">build</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	docker build -t <span class="k">$(</span>IMAGE<span class="k">)</span> .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">push</span><span class="o">:</span> <span class="n">build</span>
</span></span><span class="line"><span class="cl">	docker push <span class="k">$(</span>IMAGE<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">deploy</span><span class="o">:</span> <span class="n">push</span>
</span></span><span class="line"><span class="cl">	ssh <span class="k">$(</span>SERVER<span class="k">)</span> <span class="s2">&#34;cd </span><span class="k">$(</span>REMOTE_DIR<span class="k">)</span><span class="s2"> &amp;&amp; docker pull </span><span class="k">$(</span>IMAGE<span class="k">)</span><span class="s2"> &amp;&amp; docker compose up -d&#34;</span>
</span></span></code></pre></div>
</div>
<p>Override from the command line:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">make deploy <span class="nv">SERVER</span><span class="o">=</span>staging-server</span></span></code></pre></div>
</div>
<h2 id="multi-stage-builds-with-file-targets">Multi-Stage Builds With File Targets<a class="anchor" href="#multi-stage-builds-with-file-targets" aria-label="Link to “Multi-Stage Builds With File Targets”">#</a></h2>
<p>Make tracks file timestamps. This is useful for caching:</p>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nf">node_modules</span><span class="o">:</span> <span class="n">package</span>.<span class="n">json</span> <span class="n">package</span>-<span class="n">lock</span>.<span class="n">json</span>
</span></span><span class="line"><span class="cl">	npm ci
</span></span><span class="line"><span class="cl">	touch node_modules
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">dist</span><span class="o">:</span> <span class="n">node_modules</span> <span class="k">$(</span><span class="nv">shell</span> <span class="nv">find</span> <span class="nv">src</span> -<span class="nv">type</span> <span class="nv">f</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">	npm run build
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">serve</span>
</span></span><span class="line"><span class="cl"><span class="nf">serve</span><span class="o">:</span> <span class="n">dist</span>
</span></span><span class="line"><span class="cl">	python3 -m http.server <span class="m">8080</span> -d dist
</span></span></code></pre></div>
</div>
<p><code>node_modules</code> only rebuilds if <code>package.json</code> or <code>package-lock.json</code> changed. <code>dist</code> only rebuilds if source files changed. Make compares file timestamps automatically.</p>
<h2 id="help-target">Help Target<a class="anchor" href="#help-target" aria-label="Link to “Help Target”">#</a></h2>
<p>Add a self-documenting help target:</p>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nv">.DEFAULT_GOAL</span> <span class="o">:=</span> <span class="nb">help</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">help</span><span class="o">:</span> <span class="c">## Show this help
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	@grep -E <span class="s1">&#39;^[a-zA-Z_-]+:.*?## .*$$&#39;</span> <span class="k">$(</span>MAKEFILE_LIST<span class="k">)</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>		awk <span class="s1">&#39;BEGIN {FS = &#34;:.*?## &#34;}; {printf &#34;  \033[36m%-15s\033[0m %s\n&#34;, $$1, $$2}&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">build</span><span class="o">:</span> <span class="c">## Build the Docker image
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker compose build
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">test</span><span class="o">:</span> <span class="c">## Run the test suite
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker compose run --rm app npm <span class="nb">test</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">deploy</span><span class="o">:</span> <span class="c">## Deploy to production
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	./deploy.sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">clean</span><span class="o">:</span> <span class="c">## Remove all build artifacts
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	rm -rf dist node_modules
</span></span></code></pre></div>
</div>
<p>Now <code>make</code> with no arguments prints:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>  build           Build the Docker image
  test            Run the test suite
  deploy          Deploy to production
  clean           Remove all build artifacts</code></pre>
</div>
<h2 id="environment-specific-configs">Environment-Specific Configs<a class="anchor" href="#environment-specific-configs" aria-label="Link to “Environment-Specific Configs”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nv">ENV</span> <span class="o">?=</span> development
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">ifeq</span> <span class="err">(</span><span class="k">$(</span><span class="nv">ENV</span><span class="k">)</span><span class="err">,production)</span>
</span></span><span class="line"><span class="cl">  <span class="nv">COMPOSE_FILE</span> <span class="o">:=</span> docker-compose.yml -f docker-compose.prod.yml
</span></span><span class="line"><span class="cl"><span class="err">else</span>
</span></span><span class="line"><span class="cl">  <span class="nv">COMPOSE_FILE</span> <span class="o">:=</span> docker-compose.yml
</span></span><span class="line"><span class="cl"><span class="err">endif</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">up</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	docker compose <span class="k">$(</span>addprefix -f ,<span class="k">$(</span>COMPOSE_FILE<span class="k">))</span> up -d
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Usage:
</span></span></span><span class="line"><span class="cl"><span class="c"># make up           → development
</span></span></span><span class="line"><span class="cl"><span class="c"># make up ENV=production  → production
</span></span></span></code></pre></div>
</div>
<h2 id="parallel-execution">Parallel Execution<a class="anchor" href="#parallel-execution" aria-label="Link to “Parallel Execution”">#</a></h2>
<p>Make can run independent targets in parallel:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">make -j4 lint <span class="nb">test</span> typecheck</span></span></code></pre></div>
</div>
<p>As long as these targets do not depend on each other, Make runs all four simultaneously. This is free parallelism with no extra scripting.</p>
<h2 id="a-real-project-makefile">A Real Project Makefile<a class="anchor" href="#a-real-project-makefile" aria-label="Link to “A Real Project Makefile”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">help</span> <span class="n">dev</span> <span class="n">build</span> <span class="n">test</span> <span class="n">lint</span> <span class="n">deploy</span> <span class="n">clean</span>
</span></span><span class="line"><span class="cl"><span class="nv">.DEFAULT_GOAL</span> <span class="o">:=</span> <span class="nb">help</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">IMAGE</span> <span class="o">:=</span> myapp
</span></span><span class="line"><span class="cl"><span class="nv">TAG</span> <span class="o">:=</span> <span class="k">$(</span>shell git rev-parse --short HEAD<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">help</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	@grep -E <span class="s1">&#39;^[a-zA-Z_-]+:.*?## .*$$&#39;</span> <span class="k">$(</span>MAKEFILE_LIST<span class="k">)</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>		awk <span class="s1">&#39;BEGIN {FS = &#34;:.*?## &#34;}; {printf &#34;  \033[36m%-15s\033[0m %s\n&#34;, $$1, $$2}&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">dev</span><span class="o">:</span> <span class="c">## Start development server
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker compose up -d
</span></span><span class="line"><span class="cl">	docker compose logs -f app
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">build</span><span class="o">:</span> <span class="c">## Build production image
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker build -t <span class="k">$(</span>IMAGE<span class="k">)</span>:<span class="k">$(</span>TAG<span class="k">)</span> -t <span class="k">$(</span>IMAGE<span class="k">)</span>:latest .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">test</span><span class="o">:</span> <span class="c">## Run tests
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker compose run --rm app npm <span class="nb">test</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">lint</span><span class="o">:</span> <span class="c">## Run linter
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker compose run --rm app npm run lint
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">deploy</span><span class="o">:</span> <span class="n">build</span> <span class="c">## Deploy to production
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker push <span class="k">$(</span>IMAGE<span class="k">)</span>:<span class="k">$(</span>TAG<span class="k">)</span>
</span></span><span class="line"><span class="cl">	ssh server <span class="s2">&#34;docker pull </span><span class="k">$(</span>IMAGE<span class="k">)</span><span class="s2">:</span><span class="k">$(</span>TAG<span class="k">)</span><span class="s2"> &amp;&amp; docker compose up -d&#34;</span>
</span></span><span class="line"><span class="cl">	@echo <span class="s2">&#34;Deployed </span><span class="k">$(</span>TAG<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">clean</span><span class="o">:</span> <span class="c">## Clean up everything
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker compose down -v
</span></span><span class="line"><span class="cl">	docker image prune -f
</span></span></code></pre></div>
</div>
<p>The Makefile is not trendy. It does not have a package ecosystem or a plugin system. But it works everywhere, it handles dependencies, it supports parallelism, and it does not require installing anything. For project automation, that is hard to beat.</p>
]]></content:encoded></item></channel></rss>