Felipe Jordão A.P Mattosinho

MKPM: Make Package Manager

Jul 25, 2026

I love Make! It's not only a great build tool, but also a great script runner, and it's available almost everywhere. It's been my default automation layer for years: mostly .PHONY targets I can run from anywhere, with the occasional real file-dependency rule when a build actually needs one.

What Make has never had is a story for sharing. Every new project I started got the same targets pasted into its Makefile - build, lint, test, format, deploy - and once the same block lives in ten repositories, fixing it in one means fixing it in ten. Which, of course, never happens. Every other ecosystem solved this decades ago with npm, pip, or cargo. Make has include, and include only knows about files that are already sitting on your disk.

For the past month I've been tinkering with the idea that include is actually enough, as long as something else takes care of putting the file there first. That turned into MKPM, a minimalist package manager for GNU Make, written almost entirely in pure GNU Make.

The model is deliberately small. Any directory with an mkpkg manifest is a package. You publish it to an OCI registry - GitHub's container registry works fine, since ORAS will happily store any artifact, not just container images - and pull it into another project with a single call:

$(call mkpm_load,my-package@1.2.3)

There's one honest caveat worth stating up front: Make variables are global, so packages share a single namespace. I haven't found an elegant way around that yet. For small teams, where collisions can be kept under control with a prefix convention, MKPM is genuinely useful.

This blog runs on Next.js in Docker. The .PHONY targets wrapping docker compose are exactly the block I had been copying from project to project, so they were the first thing I turned into a package - docker_compose. Here's what the blog's own Makefile looks like now:

## mkpm bootstrap
ifndef mkpm_included
ifeq ($(filter grouped-target,$(.FEATURES)),)
$(error mkpm requires GNU Make >= 4.3 (found $(MAKE_VERSION)). On macOS: brew install make, then use 'gmake')
endif
mkpm_dir := $(subst mkpm_dir=,,$(filter mkpm_dir=%,$(or $(file < .mkpmrc.local),$(file < .mkpmrc))))
ifneq ($(mkpm_dir),)
include $(if $(filter /%,$(mkpm_dir)),$(mkpm_dir),$(abspath $(mkpm_dir)))/Makefile
else
mkpm: REMOTE ?= https://mkpm.io/Makefile
mkpm:
	@curl -fsSL $(REMOTE) -o $@ || { echo "Failed to download $@ from $(REMOTE)" >&2; exit 1; }
include mkpm
endif
endif
## /mkpm bootstrap

$(call mkpm_load,docker@0.0.1)
$(call mkpm_load,docker_compose@0.0.1)

make help now lists every target those two packages bring in, alongside the ones I wrote here - and the next project gets the same set with two lines of mkpm_load instead of another copy and paste.

The mkpm_dir escape hatch in that bootstrap block earns its keep while you're writing a package. Point it at a local checkout in .mkpmrc.local - which stays out of git - and the project loads your working copy instead of the published one, so you can iterate on a package and try it in a real Makefile before tagging a version and pushing it to the registry.

Go check MKPM on Github for full documentation.