Skip to content

Commit

Permalink
2024-08-13/01: publish
Browse files Browse the repository at this point in the history
  • Loading branch information
thiagokokada committed Aug 13, 2024
1 parent 894a388 commit 7d21288
Show file tree
Hide file tree
Showing 2 changed files with 286 additions and 41 deletions.
41 changes: 0 additions & 41 deletions 2024-08-13/.01-abusing-makefiles-for-fun-and-profit.md

This file was deleted.

286 changes: 286 additions & 0 deletions 2024-08-13/01-abusing-makefiles-for-fun-and-profit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
# Abusing Makefiles for fun and profit

If you are following this blog for a while, it should be no surprise that most
of the workflow in this blog is [automated using
Go](/2024-07-29/01-quick-bits-why-you-should-automate-everything.md). I
basically write Markdown files with some special rules inside the
[repository](https://github.com/thiagokokada/blog), commit and push it. In
seconds, the CI (currently [GitHub
Actions](https://github.com/thiagokokada/blog/blob/4e3f25485c6682f3e066b219df2290934bc0d256/.github/workflows/go.yml))
will take the latest commit, generate some files (since I use the [repository
itself](/2024-07-26/02-using-github-as-a-bad-blog-platform.md) as a backup
blog) and publish to the [capivaras.dev
website](https://kokada.capivaras.dev/).

Now, considering how much about Nix I talk in this blog, it should be a
surprise that the workflow above has **zero** Nix code inside it. I am not
saying this blog will never have it, but I am only going to add if this is
necessary, for example if I start using a tool to build this blog that I
generally don't expect it to be installed by the machine I am currently using.
Go is an exception of this rule since it is relatively straightfoward to
install (just download the [binary](https://go.dev/doc/install)) and because
its [stability guarantee](https://go.dev/doc/go1compat) means (hopefully) no
breakage. But most other things I consider moving targets, and would not be
comfortable to use unless I have Nix to ensure reproducibility.

This is why the other tool that this blog (ab)uses during its workflow is
[`Make`](https://en.wikipedia.org/wiki/Make_(software)), one of the oldest
build automation tool that exist. It is basically available in any *nix (do not
confuse with [Nix](https://nixos.org/)) system, from most Linux distros to
macOS, by default. So it is the tool I choose to automatise some tasks in this
blog, even if I consider writing a `Makefile` (the domain-specific language
that `Make` uses) kind of a lost, dark art.

To be clear, the idea of this post is not to be a `Makefile` tutorial. I will
explain some basic concepts, but if you want an actual tutorial a good one can
be found [here](https://makefiletutorial.com/). Also, while I am using `Make`
thanks to the reasons above, you can use many other tools for a similar
objective, like [Justfiles](https://github.com/casey/just),
[Taskfiles](https://taskfile.dev/) (sadly it uses
[YAML](/2024-07-31/01-generating-yaml-files-with-nix.md)), or even a small
script written in any language you want. The reason that I am writing this post
is why you should do it, not how.

A quick recap on how this blog works: inside the
[repository](https://github.com/thiagokokada/blog), a post is basically a
Markdown post following the directory structure below
([permalink](https://github.com/thiagokokada/blog/tree/894a388c61ca3a38dfc9d4cbe88dc684fd964bb7)
for the current version of this blog):

```console
.
<...>
├── 2024-08-07
│   ├── 01-quick-bits-is-crostini-a-microvm.md
│   └── 02-meta-are-quick-bits-really-quick.md
├── 2024-08-11
│   └── 01-building-static-binaries-in-nix.md
├── 2024-08-12
│   ├── 01-things-i-dont-like-in-my-chromebook-duet-3.md
│   └── Screenshot_2024-08-12_20.50.42.png
├── 2024-08-13
│   ├── 01-abusing-makefiles-for-fun-and-profit.md <-- this file
├── .github
│   └── workflows
│   └── go.yml
├── .gitignore
├── go.mod
├── go.sum
├── LICENSE
├── link_rewriter.go
├── Makefile
├── mataroa.go
├── README.md
├── rss.xml
└── .scripts
└── gen-post.sh
```

So I just create a new Markdown file following the
`YYYY-MM-DD/XX-title-slug.md` format. It **must** start with a `h1` header,
that will be automatically extract to be used as the post title, but otherwise
there is no other formatting rules. It is a highly optionated structure, but
the nice thing about being optionated is that we can extract lots of
information just from how the files are organised in the filesystem.

Most of the magic that converts those Markdown files to actual blog posts are
in the Go files that you can see above: `blog.go` is the main logic that walks
in the repository and extracts the necessary information, `mataroa.go` is
responsible for the [capivaras.dev](https://capivaras.dev/) integration (that
uses [Mataroa](https://mataroa.blog/) platform), while `link_rewriter.go` is
responsible to do some transformations in the Markdown files before posting.

While I could manage everything by just using `go` CLI and a few other *nix
commands, to make it easier to manager everything I have the following
`Makefile`:

```Makefile
MARKDOWN := $(shell find . -type f -name '*.md' -not -name README.md)

.PHONY: all
all: README.md rss.xml

blog: *.go go.*
go build

README.md: blog $(MARKDOWN)
./blog > README.md

rss.xml: blog $(MARKDOWN)
./blog -rss > rss.xml

.PHONY: publish
publish: blog
./blog -publish

DAY := $(shell date)
_PARSED_DAY := $(shell date "+%Y-%m-%d" -d "$(DAY)")
.PHONY: day
day:
mkdir -p "$(_PARSED_DAY)"


.PHONY: post
post: blog day
@[ "${TITLE}" ] || ( echo ">> TITLE is not set"; exit 1 )
./.scripts/gen-post.sh "$(_PARSED_DAY)" "$(TITLE)"

.PHONE: draft
draft:
@[ "${FILE}" ] || ( echo ">> FILE is not set"; exit 1 )
mv "$(FILE)" "$(dir $(FILE)).$(notdir $(FILE))"

.PHONE: undraft
undraft:
@[ "${FILE}" ] || ( echo ">> FILE is not set"; exit 1 )
mv "$(FILE)" "$(dir $(FILE))$(patsubst .%,%,$(notdir $(FILE)))"

.PHONY: clean
clean:
rm -rf blog
```

For those unfamiliar with `Makefile`, a quick explanation on how it works from
[Wikipedia](https://en.wikipedia.org/wiki/Make_(software)#Makefile):

> Each rule begins with a _dependency line_ which consists of the rule's target
> name followed by a colon (:) and optionally a list of targets on which the
> rule's target depends, its prerequisites

So if we look for example at the `blog` binary, the dependencies are all the
`.go` files and Go module files like `go.mod` and `go.sum`. We can make the
`blog` binary by running:

```console
$ make blog
go build
```

One nice thing about `Makefile` is that they track if any of the source files
has a newer timestamp than the target file, and only trigger the build again if
there are changes, for example:

```console
$ make blog
make: 'blog' is up to date.

$ touch blog.go

$ make blog
go build
```

But sometimes this property is undesirable. In those cases we can declare a
target as `.PHONY`, that basically instructs `Makefile` to always make the
target. One classic `.PHONY` target is `clean`, that removes the build
artifacts:

```console
$ make clean
rm -rf blog

$ make clean
rm -rf blog
```

By the way, it is better to declare a target as `.PHONY` than declaring
dependencies incorrectly, especially in languages that has fast build times
like e.g.: Go, because the worst thing that can happen is something not being
rebuild when it needs to. So my general recomendation if you are writing your
first `Makefile` is to just declare everything as `.PHONY`. You can always
improve it later.

One last basic concept that I want to explain about `Makefile` is the default
target: it is the target that is run if you just run `make` without arguments
inside the directory that contains a `Makefile`. It is common to have an `all`
target (that is also marked as `.PHONY`) that has as dependencies all the
targets that you want to build by default. In this particular case I declare
the `README.md` and `rss.xml` files to be build by default, and they themselves
depends in `blog` binary being build. So once I run `make` you get as result:

```console
$ make
go build
./blog > README.md
./blog -rss > rss.xml
```

And this result above highlights the first reason I think you should have a
`Makefile` or something similar in your projects: you don't need to remember
the exactly steps that you need to get things working. If I see one project of
mine having a `Makefile`, I can be reasonably confident that I can get it
working by just running `make`.

But now let's focus in the other targets that I have in the `Makefile` that are
not related to the build process but are there to help me manage my blog posts.
Remember the rules I explained above? Maybe not, but it should be no problem,
because:

```
$ make post TITLE="My new blog post"
mkdir -p "2024-08-13"
./.scripts/gen-post.sh "2024-08-13" "My new blog post"
Creating file: 2024-08-13/02-my-new-blog-post.md
$ cat 2024-08-13/02-my-new-blog-post.md
# My new blog post
```

This command, `make post`, is responsible for:

1. Create a new directory for today, if it doesn't exist
2. Run the
[`gen-post.sh`](https://github.com/thiagokokada/blog/blob/6a3b06970729f7650e5bee5fb0e1f9f2541ffea8/.scripts/gen-post.sh)
script, that:
1. Enumerates all posts from the day, so we can number the new post correctly
- We already had this post planned for 2024-08-13, so the new post is 02
2. Slugify the title, so we can create each Markdown file with the correct
filename
3. Creates a new Markdown file with the title as a `h1` header

The steps above may or may not seen trivial, and for a while I was doing them
manually. But not having to think what is the current date or if I already
posted that day or what is the slug is for the title make (pun intended) my
like much easier.

Yes, the code is ugly. The way variables works in `Make` is that you can
declare then inside the `Makefile`, but they can be overwritten in the terminal
if you pass them. I used this to allow `make post` to also work for future
posts:

```console
$ make post TITLE="Another new blog post" DAY=2024-12-12
mkdir -p "2024-12-12"
./.scripts/gen-post.sh "2024-12-12" "Another new blog post"
Creating file: 2024-12-12/01-another-new-blog-post.md
```

So in the above case, `DAY` is filled with the value passed in the terminal
instead of default (that would be the current day), and `_PARSED_DAY` is the
day we use to actually create the directory. We can actually pass any date
format recognised by
[`date`](https://www.gnu.org/software/coreutils/manual/html_node/Examples-of-date.html),
not just `YYYY-MM-DD`.

I have 2 other phony targets that I want to talk, `draft` and `undraft`. They
expect a `FILE` to be passed, and I use them to either hide or unhide a file:

```console
$ make draft FILE=2024-12-12/01-another-new-blog-post.md
mv "2024-12-12/01-another-new-blog-post.md" "2024-12-12/.01-another-new-blog-post.md"

$ make undraft FILE=2024-12-12/.01-another-new-blog-post.md
mv "2024-12-12/.01-another-new-blog-post.md" "2024-12-12/01-another-new-blog-post.md"
```

Why? Because hidden files are [explicit
ignored](https://github.com/thiagokokada/blog/blob/894a388c61ca3a38dfc9d4cbe88dc684fd964bb7/blog.go#L101-L104)
during my directory parser to mean they're a draft post and not ready to be
published. And the reason I created them is because I was tired of trying to
hide or unhide a file manually.

So that's it, for the same reason you [should probably automate
everything](/2024-07-29/01-quick-bits-why-you-should-automate-everything.md),
you also need to have some way to automate boring tasks. `Makefile` is one way
to do it, maybe not the best way to do it, but it works and it is available
anywhere.

0 comments on commit 7d21288

Please sign in to comment.