Show HN: Home Maker: Declare Your Dev Tools in a Makefile

Home Maker is a minimalist system that uses a single Makefile to declare and manage developer tools across multiple package managers, making machine setup and upgrades seamless.
Your laptop has ripgrep, installed via cargo install. ruff is there too, via uv tool install. golangci-lint came from go install. bash-language-server was npm i -g. Neovim was a tarball download. Kitty was a curl script.
Six months later you get a new machine, or you just want to upgrade or reinstall. What do you even have installed? How did you install each one? Which version? Good luck.
This is a small system that answers those questions — a single Makefile that declares every tool you care about, grouped by purpose, with one command to install anything.
Note: If you just want to use this, head straight to github.com/santhoshtr/hm and start adding your packages. The rest of this post explains how it works.
The Problem
A developer’s machine accumulates tools from five or more package managers. Each has its own syntax:
sudo apt-get install -y ripgrep
cargo install eza
uv tool install ruff
go install golang.org/x/tools/gopls@latest
sudo npm i -g prettier
curl -L https://example.com/tool | sh
You remember these the day you install them. Three months later, you remember nothing. When the next laptop arrives, or you want to upgrade or reinstall some packages, you spend a day re-discovering what you had and how to get it.
The fix is not another tool. It is a text file you already understand. Instead of running the installation commands and scripts in your terminal, you record it for the future.
One Makefile
Create a directory. Inside it, a single Makefile and a few .mk files under dev/:
hm/
├── Makefile
├── dev/
│ ├── cli.mk
│ ├── python.mk
│ ├── node.mk
│ ├── go.mk
│ ├── rust.mk
│ └── lsp.mk
└── desktop/
└── apps.mk
Each .mk file declares packages for one manager using simple += appends:
# dev/cli.mk
APT += ripgrep jq bat fzf htop tmux
CARGO += eza zoxide fd
PKG_fd := fd-find
That is the entire package declaration system. Five variables (APT, CARGO, UV, GO, NPM), one per manager. Each .mk file appends to whichever variable it needs. The Makefile includes them all.
How the Makefile Works
The central Makefile does three things: include the .mk files, define target generators, and expand the lists into targets.
Include everything:
include dev/cli.mk
include dev/python.mk
# ... etc
Define the install commands:
APT_INSTALL := sudo apt-get install -y
CARGO_INSTALL := cargo install
UV_INSTALL := uv tool install
GO_INSTALL := go install
NPM_INSTALL := sudo npm i -g
Define helper functions to split name@version syntax and resolve PKG_ overrides:
pkg-name = $(firstword $(subst @, ,$(1)))
pkg-version = $(word 2,$(subst @, ,$(1)))
pkg-pkgname = $(or $(PKG_$(call pkg-name,$(1))),$(call pkg-name,$(1)))
Define a generator macro per manager. Here is gen-apt:
define gen-apt
.PHONY: $(call pkg-name,$(1))
$(call pkg-name,$(1)):
@echo "installing/upgrading $$@..."
@$(APT_INSTALL) $(call pkg-pkgname,$(1))
endef
Expand every list into targets:
$(foreach p,$(APT),$(eval $(call gen-apt,$(p))))
$(foreach p,$(CARGO),$(eval $(call gen-cargo,$(p))))
$(foreach p,$(UV),$(eval $(call gen-uv,$(p))))
$(foreach p,$(GO),$(eval $(call gen-go,$(p))))
$(foreach p,$(NPM),$(eval $(call gen-npm,$(p))))
That is the entire mechanism. No generated files, no YAML, no DSL. make evaluates the foreach, expands each entry, and creates a .PHONY target. make ripgrep runs sudo apt-get install -y ripgrep. Every target appears because it was appended to a list in some .mk file.
Three Patterns for Adding a Package
Pattern 1: package manager has it, name matches. Append to the list: APT += htop.
Pattern 2: pin a version. Use name@version. The macros handle the specific syntax for each manager.
Pattern 3: target name differs from package name. Use PKG_ override: CARGO += fd and PKG_fd := fd-find.
The Interactive Installer
hm.sh is a 30-line shell script that discovers every target from the Makefiles using make -pn and presents them in fzf. This allows you to browse, search, and multi-select packages to install with a visual preview of the command that will be executed.
Why Not Nix or Ansible?
Nix solves a real problem — reproducible, hermetic environments. But it asks you to learn a functional language and adopt a parallel package universe. Ansible is powerful but often overkill for a single machine setup, requiring YAML and complex modules. Home Maker uses tools you already know to provide a transparent, lightweight alternative.
Source: Hacker News









