Homie is small on purpose, but the surface that needs to grow first is distro and package-manager coverage. The code is structured so adding a new one is a focused change in two or three files.
Source lives at github.com/kurowski/homie.
PRs welcome; the rest of this page walks through the common paths.
Adding a new distro#
Distro support has three touch points. Grep for TODO(contrib) to see
the current set:
git grep -n 'TODO(contrib)'internal/detect/detect.go— recognise the distro’s/etc/os-releaseID=value and return it fromDetect(). Map it to the right package manager (apt,dnf, or a new one you’re adding alongside). macOS is a special case handled before the/etc/os-releaseparse: whenGOOS == "darwin",Detect()returns the platform keymacoswith package managerbrew.internal/packages/— if the distro uses an existing manager (aptordnf), you’re done after step 1. If it needs a new manager, see adding a package manager below.e2e/dockerfiles/— add a minimal base image so the e2e harness exercises the new distro. Copy the existingfedora.Dockerfileorubuntu.Dockerfileand adapt the base image plus any bootstrap packages (bash,git,sudo,ca-certificates).
Add a test entry in e2e/e2e_test.go for the new distro and run
make e2e locally to confirm.
Adding a package manager#
The interface is small:
type Manager interface {
IsAvailable() bool
IsInstalled(pkg string) bool
Install(packages []string) error
}To add one (e.g. pacman, zypper, apk):
- Create
internal/packages/<name>.goimplementingManager. Mirror the structure ofapt.goordnf.go— both use an injectable command runner so they’re unit-testable without shelling out for real. - Wire it into the selector in
internal/packages/manager.gosodetect.Env.PackageManager == "<name>"returns your implementation. - Update
internal/detect/detect.goto map the relevant distros to the new manager name. - Add a unit test alongside (
<name>_test.go) with table-driven IsInstalled/Install cases using the fake runner pattern.
Key invariants every manager must hold:
- Idempotent.
Installfilters out already-installed packages before calling out to the real tool. - Sudo only when needed. Check
os.Geteuid(); prependsudoonly when not root. Never assume passwordless sudo — return the underlying exit error and let the user see it. - No prompts. Pass whatever flag suppresses interactive prompts
(
-yfor apt/dnf,--noconfirmfor pacman, etc.). Ahm applymid-run should never block on a TTY question.
Running tests#
make build # static binary at ./hm
make test # go test ./... — unit tests only
make lint # go vet + golangci-lint if installed
make e2e # container-based e2e suite (needs Docker / Podman)The e2e suite builds the binary, builds one image per distro, runs
hm apply against a fixture user-repo, and asserts the resulting state.
It’s slow (~60s) but it’s the only way to catch regressions in the
package phase.
Code conventions#
- No
panicoutsidemain.goinitialization. - Wrap errors with context:
fmt.Errorf("link %s: %w", path, err). - No public API. Everything except
cmd/hmlives underinternal/. - Tests next to source. Fixtures in
<pkg>/testdata/. - No external state writes other than
$HOMEand (when root) what the package manager touches on its behalf.
Filing issues#
Bug reports and feature requests go to github.com/kurowski/homie/issues. Useful things to include:
- Distro and version (
cat /etc/os-release). hm --version.- Output of
hm doctorifhm applyis the failing command. - Whether you’re running in a container / Codespace.
For security issues, please don’t open a public issue — email the maintainer instead.
