Improve make blogpost

This commit is contained in:
Midgard 2020-06-11 01:15:21 +02:00
parent f10d9df62d
commit 1f45d1dc09
Signed by untrusted user who does not match committer: midgard
GPG key ID: 511C112F1331BBB4

View file

@ -4,20 +4,19 @@ published: 2020-06-02
--- ---
Text-based slides are boring and not effective. Text-based slides are boring and inefficient.
While preparing a presentation for my intermediary thesis' defence, I wanted to create something that would support my story better. While preparing a presentation for my intermediary thesis' defence, I wanted to use graphics instead of words so my audience wouldn't have to read and listen at the same time.
I wanted to use graphics instead of words so the language centres of my dear audience's brains could focus on my speech rather than reading. Rather than fiddling with shapes and images in traditional office software, I knew I'd be less frustrated just using the SVG editor Inkscape.
Rather than importing slide-sized graphics in a GUI, I thought a makefile would be a nice way to do the trick. And a makefile would be nice to stitch the SVGs together to create a PDF file that I could use during my exposé.
Makefiles these days are commonly used as a shell script with multiple entry points. ## File dependencies with makefile
But they can do much more than that! Makefiles are commonly used as a shell script with multiple entry points, but they can do much more than that!
You give it a filename, and it will build that file if its dependencies have been changed. You give it a filename, and it will build that file if its dependencies have been changed.
Traditionally makefiles are used for C programs, but there's no reason you can't use them for any build you want. Traditionally makefiles are used for C programs, but you can use them for absolutely any build where you have dependencies between files.
Even this website is built by a makefile! Even this website is built by a makefile.
Want to join me and write one together? Want to join me and write one together?
## Writing the makefile Putting the name of our output file in a variable will make it to change it later, if we'd ever want that.
Putting the name of our output file in variables will make it easier to change it later.
```makefile ```makefile
OUTPUT = slides.pdf OUTPUT = slides.pdf
``` ```
@ -32,46 +31,70 @@ SLIDES_PDF ::= $(SLIDES:%.svg=build/%.pdf)
I love this next rule. I love this next rule.
It teaches *make* that it should use Inkscape if it needs to create a PDF. It teaches *make* that it should use Inkscape if it needs to create a PDF.
`$@` refers to the target (`build/….pdf`), and `$<` refers to the first dependency (`….svg`).
Inkscape's `--export-pdf filename` option exports to PDF without opening the GUI. Inkscape's `--export-pdf filename` option exports to PDF without opening the GUI.
**TODO: isn't there a way to automatically depend on all parent directories?** `$@` refers to the target (`build/….pdf`), and `$<` refers to the first dependency (`….svg`).
Do note, the indentation *must* be a tab; spaces are not allowed.
```makefile ```makefile
build/%.pdf: %.svg build build/%.pdf: %.svg
inkscape --export-pdf "$@" "$<" inkscape --export-pdf "$@" "$<"
``` ```
Now we tell *make* that the final output can be created from the individual slides (`$(SLIDES_PDF)`) by calling pdfjoin. Next we tell *make* that the final output can be created from the individual slides (`$(SLIDES_PDF)`) by calling pdfjoin.
*Make* will recursively make sure that all dependencies are up to date. *Make* will recursively make sure that all dependencies are up to date.
Because we add the SVG source file as a dependency, it will know that the individual PDF files have to be rebuilt if their source was changed. We already declared the corresponding SVG file as a dependency for each slide's PDF, so *make* will know that it should rebuild them if the SVG was changed.
And if any slide was changed, the end result will be updated too. And if any PDF was updated, the end result will be, too.
But if there was no change, it won't; nothing is rebuilt unnecessarily! But if there was no change, it won't; nothing is rebuilt unnecessarily!
**TODO: DRY** `$+` means all prerequisites, retaining duplicate entries.
**TODO: Do we need to fix sh's space disease here?** **TODO: Do we need to fix sh's space disease here?**
```makefile ```makefile
$(OUTPUT): $(SLIDES_PDF) $(OUTPUT): $(SLIDES_PDF)
pdfjoin $(SLIDES_PDF) -o "$@" pdfjoin $+ -o "$@"
```
If you look at the `build/%.pdf` rule, you see that it depends on `build`.
With this rule, *make* will know how to create this directory.
```makefile
build:
mkdir -p "$@"
``` ```
It is common to create a `clean` to clean up. It is common to create a `clean` to clean up.
Since `clean` is not a file, we have to mark the target as "phony".
Failing to do so would result in **TODO how to finish this sentence?**
```makefile ```makefile
.PHONY: clean
clean: clean:
rm -rf "$(OUTPUT)" build/ rm -rf "$(OUTPUT)" build/
``` ```
If you now create a file `clean` and run `make clean`, *make* will quite contently say it doesn't need to do anything: the file `clean` exists and it depends on nothing else.
To avoid this, non-file targets must be marked as "phony".
```makefile
.PHONY: clean
```
# Creating directories
If you would use our makefile up to now, Inkscape would fail, complaining that `build/` doesn't exist.
This is to be expected, since directories have to be created before you can place files in them.
Much to my dismay, there seems to be no clean, standard way to create the necessary directories in *make*.
In my opinion, this is *make*'s single biggest shortcoming.
There [are a few ways](https://www.cmcrossroads.com/article/making-directories-gnu-make), none of them elegant.
For our simple makefile, we can just put `@mkdir -p $(@D)` in each rule.
It's not ideal for performance, **TODO and it might give race conditions if there are more directories and rules, should think how to word this and stuff when less tired**
You'll have noticed by default *make* prints the commands it executes; the `@` in front of this trivial command suppresses this.
`$(@D)` refers to the parent directory of the current target.
`$(OUTPUT)` is currently defined as `slides.pdf`, but we might change it to another directory later.
So just for good measure its rule too gets the special `mkdir` treatment.
```makefile
build/%.pdf: %.svg
@mkdir -p $(@D)
inkscape --export-pdf "$@" "$<"
$(OUTPUT): $(SLIDES_PDF)
@mkdir -p $(@D)
pdfjoin $+ -o "$@"
```
# The final makefile
One more thing: the default target, for when you call *make* without arguments, is the first one in the makefile (with [some caveats][gmake_docs_targets]). One more thing: the default target, for when you call *make* without arguments, is the first one in the makefile (with [some caveats][gmake_docs_targets]).
You could override this with `.DEFAULT_GOAL := your_target_name_here`, but it's just as easy to reorder the rules, which is what I've done in the summary below. You could override this with `.DEFAULT_GOAL := your_target_name_here`, but it's just as easy to reorder the rules, which is what I've done in the summary below.
@ -79,6 +102,9 @@ You could override this with `.DEFAULT_GOAL := your_target_name_here`, but it's
This is the finished makefile: This is the finished makefile:
**TODO verify up to date**
**TODO verify actually works**
```makefile ```makefile
OUTPUT = slides.pdf OUTPUT = slides.pdf
@ -86,20 +112,18 @@ SLIDES ::= $(sort $(wildcard *.svg))
SLIDES_PDF ::= $(SLIDES:%.svg=build/%.pdf) SLIDES_PDF ::= $(SLIDES:%.svg=build/%.pdf)
$(OUTPUT): $(SLIDES_PDF) $(OUTPUT): $(SLIDES_PDF)
pdfjoin $(SLIDES_PDF) -o "$@" @mkdir -p $(@D)
pdfjoin $+ -o "$@"
build/%.pdf: %.svg build build/%.pdf: %.svg
@mkdir -p $(@D)
inkscape --export-pdf "$@" "$<" inkscape --export-pdf "$@" "$<"
build:
mkdir -p "$@"
.PHONY: clean .PHONY: clean
clean: clean:
rm -rf "$(OUTPUT)" build/ rm -rf "$(OUTPUT)" build/
``` ```
**TODO: write about first rule being the default**
**TODO: split into 2 posts?** **TODO: split into 2 posts?**
## Further help with *make* ## Further help with *make*