Skip to content

Commit

Permalink
Added post on Makefile tips
Browse files Browse the repository at this point in the history
* Update hugo locked version to v0.115.0
* Update Congo theme to v2.6.1
* Added makefile tag to hugo-openring post
  • Loading branch information
nikitawootten committed Jul 9, 2023
1 parent b0f5eb5 commit 8333906
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 18 deletions.
2 changes: 1 addition & 1 deletion content/posts/hugo-openring.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "Configuring my Congo themed Hugo blog to use Openring"
date: 2022-12-03T18:26:40-05:00
draft: false
tags: [hugo, meta, tutorial]
tags: [hugo, meta, tutorial, makefile]
---

I really like Drew DeVault's [Openring](https://git.sr.ht/~sircmpwn/openring), an elegant utility that generates links to blogs that I follow under my posts (scroll to the end of this article, you may find something you like!).
Expand Down
Binary file added content/posts/makefile-tips/images/make-help.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
225 changes: 225 additions & 0 deletions content/posts/makefile-tips/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
---
title: "Tips for using Makefiles in your projects"
date: 2023-07-07T21:54:43-04:00
draft: false
tags: [makefile, nix, tips]
---

I have a secret: I adore Makefiles.
I'll admit, the syntax is a bit arcane, and if you don't know what you're doing you can create some really insidious bugs, but once things are set up you can really improve the developer experience on your projects likely without requiring developers to install any [additional tools](https://github.com/casey/just).
In this post I'd like to share some tips I've gathered for making your Makefiles more effective.

{{< alert "circle-info" >}}
This post assumes that you have surface knowledge of Makefiles.
If you'd like to learn more about Makefiles, check out the resources in the [conclusion](#conclusion).
{{< /alert >}}

## Tip: Automatically document your Makefiles

Picture this, you clone a random project off the internet.
The project's documentation instructs you to run `make help`, and to your delight you are greeted with a nicely formatted list of targets and their purpose.
This happened to me when playing around with a project called [`sbomnix`](https://github.com/tiiuae/sbomnix/tree/main) and since discovering it I've begun including a "self-documenting" `help` target to all of my projects:

```Makefile
.PHONY: help
help: ## Show this help message
@grep --no-filename -E '^[a-zA-Z_-]+:.*?##.*$$' $(MAKEFILE_LIST) | awk 'BEGIN { \
FS = ":.*?## "; \
printf "\033[1m%-30s\033[0m %s\n", "TARGET", "DESCRIPTION" \
} \
{ printf "\033[32m%-30s\033[0m %s\n", $$1, $$2 }'
```

Now, running `make help` with a Makefile with this target present produces a nice list of targets and their description.
Any target annotated with `## <comment>` will show up (including the help target).
For example, here is the output of `make help` on the [Makefile used to build this very site](https://github.com/nikitawootten/nikitawootten.github.io/blob/main/Makefile):

{{<figure src="images/make-help.png" title="'make help' example output">}}

This snippet is modified from [this blog post](https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html).
I modified it to support targets within [Makefile includes](https://www.gnu.org/software/make/manual/html_node/Include.html) and to add the header.
[Other variations](https://docs.cloudposse.com/reference/best-practices/make-best-practices/#help-target) of this idea exist, choose one that suits you best, or make your own!

You might also want to consider making `help` your default goal:

```Makefile
# Run the help goal when the user runs `make`
.DEFAULT_GOAL: help
```

## Tip: Parallelize your Makefile

For larger projects with a lot of moving parts, you could potentially drastically speed up your build by running some targets in parallel.
Faster builds means better developer productivity, and also much better CI performance!
GitHub Actions gives you a [2-core CPU by default](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources), so there is performance you are potentially leaving on the table!

Thankfully, running Make operations in parallel is trivially easy, just add a `--jobs <n>` flag (where `n` is the limit to the number of jobs a Makefile can run at once, usually the number of cores your machine has).

```sh
# Run the specified target with up to <n> jobs in parallel
make <target> --jobs <n>
```

For more details, check out the ["Parallel Execution" section of the GNU Make Manual](https://www.gnu.org/software/make/manual/html_node/Parallel.html).

## Tip: Augment common Makefile operations with canned recipes

Canned recipes are useful when several targets have a lot of similarities.
Recipes can also improve the readability of a Makefile.

For more details, see the ["Canned Recipes" section of the GNU Make Manual](https://www.gnu.org/software/make/manual/html_node/Canned-Recipes.html).

### Example: Run commands within a Nix shell

Canned recipes are particularly useful for reducing the amount of repeated code, which can improve readability and reduce the possibility of mistakes.

For example, I house my [NixOS configurations in a repository](https://github.com/nikitawootten/infra) with a Makefile for common operations (updating, building, etc).
Some of these commands run in a special [Nix Shell](https://nixos.org/manual/nix/stable/command-ref/nix-shell.html) environment, allowing me to guarantee that the person running the commands has the correct dependencies.

I initially wrote my Makefile like this:

```Makefile
.PHONY: help test update switch-home build-home

test: ## Test flake outputs with "nix flake check"
nix-shell shell.nix --command 'nix flake check'

update: ## Update "flake.lock"
nix-shell shell.nix --command 'nix flake update'

switch-home: ## Switch local home-manager config
nix-shell shell.nix --command 'home-manager switch --flake .'

build-home: ## Build local home-manager config
nix-shell shell.nix --command 'home-manager build --flake .'

# ... more targets excluded for brevity
```

Using canned recipes I reduced it to this:

```Makefile
# Run command in nix-shell for maximum reproducibility (idiot [me] proofing)
define IN_NIXSHELL
nix-shell shell.nix --command '$1'
endef

.PHONY: help test update switch-home build-home

test: ## Test flake outputs with "nix flake check"
$(call IN_NIXSHELL,nix flake check)

update: ## Update "flake.lock"
$(call IN_NIXSHELL,nix flake update)

switch-home: ## Switch local home-manager config
$(call IN_NIXSHELL,home-manager switch --flake .)

build-home: ## Build local home-manager config
$(call IN_NIXSHELL,home-manager build --flake .)
```

### Example: A simple For-Each recipe

The following canned recipe creates a simple "for-each" loop:

```Makefile
# $(call FOREACH,<item variable>,<items list>,<command>)
define FOREACH
for $1 in $2; do {\
$3 ;\
} done
endef

friends:=bob alice

.PHONY: greet
greet:
# Note that shell variables must be escaped with a double-$
$(call FOREACH,friend,$(friends),echo "hello $$friend")
```

{{< alert >}}
This method does not play nicely with [parallelization](#tip-parallelize-your-makefile), since the loop runs serially.
In a lot of cases a better approach is to use [patterns](https://www.gnu.org/software/make/manual/html_node/Pattern-Intro.html) and [wildcard rules](https://earthly.dev/blog/using-makefile-wildcards/)
{{< /alert >}}

Running this makefile produces the output:

```sh
$ make
for friend in bob alice; do { echo "hello $friend" ; } done
hello bob
hello alice
```

### Example: Extending the For-Each recipe for multi-Makefile monorepos

Sometimes you'll have repositories with a lot of moving parts, including several project each complete with their own Makefiles.
Wouldn't it be great to have a single top-level Makefile that can run the `test` target for each project?
Fortunately extending [the For-Each recipe](#example-a-simple-for-each-recipe) to do so is trivial:

```Makefile
# $(call FOREACH_MAKE,<target>,<directories list>)
# Run a Makefile target for each directory, requiring each directory to
# have a given target
define FOREACH_MAKE
@echo Running makefile target \'$1\' on all subdirectory makefiles
@$(call FOREACH,dir,$2,$(MAKE) -C $$dir $1)
endef

# For all Makefiles matched by the wildcard, extract the directory
dirs:=$(dir $(wildcard ./*/Makefile))

.PHONY: test
test: ## Run all tests
$(call FOREACH_MAKE,$@,$(dirs))
```

Running `make test` now runs each sub-directory's `test` target.

In some cases you might want to run a target on each Makefile, ignoring Makefiles that do not define one.
For example, say some sub-projects have a `clean` target, and others don't.
The following canned recipe allows you to run `make clean` only on directories that have a `clean` target:

```Makefile
# $(call FOREACH_MAKE_OPTIONAL,<target>,<directories list>)
# Run a Makefile target for each directory, skipping directories whose Makefile does not contain a rule
define FOREACH_MAKE_OPTIONAL
@echo Running makefile target \'$1\' on all subdirectory makefiles that contain the rule
@$(call FOREACH,dir,$2,$(MAKE) -C $$dir -n $1 &> /dev/null && $(MAKE) -C $$dir $1 || echo "Makefile target '$1' does not exist in "$$dir". Continuing...")
endef

dirs:=$(dir $(wildcard ./*/Makefile))

.PHONY: clean
clean: ## Remove any generated test or build artifacts
$(call FOREACH_MAKE_OPTIONAL,$@,$(dirs))
```

## Tip: Use Makefile `include` for multi-Makefile projects

Using some of the tips you've gathered in this post, you may have accrued quite a bit of boilerplate now replicated in multiple Makefiles within the same repository.
Maybe now you've added [a "help" target to each Makefile](#tip-automatically-document-your-makefiles) and [one or two shared canned recipes](#tip-augment-common-makefile-operations-with-canned-recipes).
This is not very [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), we can do better!

Fortunately, [Makefile includes](https://www.gnu.org/software/make/manual/html_node/Include.html) make it simple to consolidate shared roles, recipes, and variables.

{{< alert "circle-info" >}}
Makefile inclusions are not "namespaced", so beware of clashing target and variable names.
{{< /alert >}}

```Makefile
# Include the contents of the Makefile ../shared/common.mk
include ../shared/common.mk
```

## Conclusion

If you have any Makefile tips that you'd like to share, feel free to leave a comment below or contact me.

If you'd like to learn more about Makefiles, check out some of the links below:

- [This really gentle introduction to Makefiles](https://endler.dev/2017/makefiles/) is great to send to team members looking to get started.
- [The GNU Make Manual](https://www.gnu.org/software/make/manual/html_node/) is an excellent reference. I find something interesting each time I read through it.
- [Self-Documented Makefile](https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html) is the excellent blog whose ["help" target I modified above](#tip-automatically-document-your-makefiles).
50 changes: 34 additions & 16 deletions devenv.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1680782537,
"narHash": "sha256-oQQoEzpuo4fjyPNCu3/Tc+f8hPAvvuoy4bIo9hn7akg=",
"lastModified": 1688824367,
"narHash": "sha256-qHxX0U8K+BvN+P1+ZhPuFMHRL2aCmQxSnXLzzNpTLD0=",
"owner": "cachix",
"repo": "devenv",
"rev": "e639583e5974e1d0f58ac110356f7f182d6f6004",
"rev": "1e4701fb1f51f8e6fe3b0318fc2b80aed0761914",
"type": "github"
},
"original": {
Expand All @@ -34,12 +34,15 @@
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"lastModified": 1685518550,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
"type": "github"
},
"original": {
Expand Down Expand Up @@ -71,11 +74,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1680942619,
"narHash": "sha256-kpCW1IegAZfEjCVJW7IPN/hEtRL/9dxaFFYiHS5qVAk=",
"lastModified": 1688798537,
"narHash": "sha256-+3QEnDgBiso8lgUJpMagn6xCujmarc6zCWfKYAd6nqU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6f95dd4fd050daf017cae2dfeb1cea1ec0e4c1a1",
"rev": "842e90934a352f517d23963df3ec0474612e483c",
"type": "github"
},
"original": {
Expand All @@ -87,16 +90,16 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1678872516,
"narHash": "sha256-/E1YwtMtFAu2KUQKV/1+KFuReYPANM2Rzehk84VxVoc=",
"lastModified": 1685801374,
"narHash": "sha256-otaSUoFEMM+LjBI1XL/xGB5ao6IwnZOXc47qhIgJe8U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9b8e5abb18324c7fe9f07cb100c3cd4a29cda8b8",
"rev": "c37ca420157f4abc31e26f436c1145f8951ff373",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-22.11",
"ref": "nixos-23.05",
"repo": "nixpkgs",
"type": "github"
}
Expand All @@ -112,11 +115,11 @@
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1680981441,
"narHash": "sha256-Tqr2mCVssUVp1ZXXMpgYs9+ZonaWrZGPGltJz94FYi4=",
"lastModified": 1688596063,
"narHash": "sha256-9t7RxBiKWHygsqXtiNATTJt4lim/oSYZV3RG8OjDDng=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "2144d9ddcb550d6dce64a2b44facdc8c5ea2e28a",
"rev": "c8d18ba345730019c3faf412c96a045ade171895",
"type": "github"
},
"original": {
Expand All @@ -131,6 +134,21 @@
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module github.com/nikitawootten/nikitawootten.github.io

go 1.19

require github.com/jpanther/congo/v2 v2.5.2 // indirect
require github.com/jpanther/congo/v2 v2.6.1 // indirect
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
github.com/jpanther/congo/v2 v2.5.2 h1:cSM6cnBfbsSN9uYIKXjnyudZDWK+OWWVPVlFmkQQSGE=
github.com/jpanther/congo/v2 v2.5.2/go.mod h1:1S7DRoO1ZYS4YUdFd1LjTkdyjQwsjFWd8TqSfz3Jd+M=
github.com/jpanther/congo/v2 v2.6.1 h1:iA8uosVsiMl3JbBSBwMvrxEibzBcDp+RIj18f/cmlDs=
github.com/jpanther/congo/v2 v2.6.1/go.mod h1:1S7DRoO1ZYS4YUdFd1LjTkdyjQwsjFWd8TqSfz3Jd+M=

0 comments on commit 8333906

Please sign in to comment.