Improve make blogpost
This commit is contained in:
parent
f10d9df62d
commit
1f45d1dc09
1 changed files with 59 additions and 35 deletions
|
@ -4,20 +4,19 @@ published: 2020-06-02
|
|||
|
||||
---
|
||||
|
||||
Text-based slides are boring and not effective.
|
||||
While preparing a presentation for my intermediary thesis' defence, I wanted to create something that would support my story better.
|
||||
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 importing slide-sized graphics in a GUI, I thought a makefile would be a nice way to do the trick.
|
||||
Text-based slides are boring and inefficient.
|
||||
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.
|
||||
Rather than fiddling with shapes and images in traditional office software, I knew I'd be less frustrated just using the SVG editor Inkscape.
|
||||
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.
|
||||
But they can do much more than that!
|
||||
## File dependencies with makefile
|
||||
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.
|
||||
Traditionally makefiles are used for C programs, but there's no reason you can't use them for any build you want.
|
||||
Even this website is built by a makefile!
|
||||
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.
|
||||
Want to join me and write one together?
|
||||
|
||||
## Writing the makefile
|
||||
Putting the name of our output file in variables will make it easier to change it later.
|
||||
Putting the name of our output file in a variable will make it to change it later, if we'd ever want that.
|
||||
```makefile
|
||||
OUTPUT = slides.pdf
|
||||
```
|
||||
|
@ -32,46 +31,70 @@ SLIDES_PDF ::= $(SLIDES:%.svg=build/%.pdf)
|
|||
|
||||
I love this next rule.
|
||||
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.
|
||||
|
||||
**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
|
||||
build/%.pdf: %.svg build
|
||||
build/%.pdf: %.svg
|
||||
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.
|
||||
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.
|
||||
And if any slide was changed, the end result will be updated too.
|
||||
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 PDF was updated, the end result will be, too.
|
||||
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?**
|
||||
```makefile
|
||||
$(OUTPUT): $(SLIDES_PDF)
|
||||
pdfjoin $(SLIDES_PDF) -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 "$@"
|
||||
pdfjoin $+ -o "$@"
|
||||
```
|
||||
|
||||
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
|
||||
.PHONY: clean
|
||||
|
||||
clean:
|
||||
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]).
|
||||
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:
|
||||
|
||||
**TODO verify up to date**
|
||||
**TODO verify actually works**
|
||||
|
||||
```makefile
|
||||
OUTPUT = slides.pdf
|
||||
|
||||
|
@ -86,20 +112,18 @@ SLIDES ::= $(sort $(wildcard *.svg))
|
|||
SLIDES_PDF ::= $(SLIDES:%.svg=build/%.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 "$@" "$<"
|
||||
|
||||
build:
|
||||
mkdir -p "$@"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf "$(OUTPUT)" build/
|
||||
```
|
||||
|
||||
**TODO: write about first rule being the default**
|
||||
**TODO: split into 2 posts?**
|
||||
|
||||
## Further help with *make*
|
||||
|
|
Loading…
Reference in a new issue