Does your Racket project need a makefile?

:: Racket, racket-cookbook

NOTE: See my newer post.

Most of my Racket projects don’t use a makefile. Why would they? raco make or raco setup suffices.

But a makefile can consistently define some common project tasks. And it can really help when you want to generate HTML documentation from Scribble sources, and publish it to GitHub Pages using the automagical gh-pages branch.1

GitHub Pages lets you provide a web site for each of your projects. You:

  1. Create an empty, orphan branch named gh-pages. GitHub instructions.

  2. Somehow make gh-pages contain just the HTML and other files required for the web site (but preferably not the source files from your master branch).

  3. Somehow keep that up-to-date.

Steps 2 and 3 are awkward. To-date I’ve used some shell scripts that felt like duct tape.

Yesterday I discovered Tony Garnock-Jones’ makefile for his racket-bitsyntax project. It’s brilliant. I promptly stole it, asked him a question about how it works, and asked if I could blog about it.

Here is his original makefile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
PACKAGENAME=bitsyntax
COLLECTS=bitsyntax

all: setup

clean:
	find . -name compiled -type d | xargs rm -rf
	rm -rf htmldocs

setup:
	raco setup $(COLLECTS)

link:
	raco pkg install --link -n $(PACKAGENAME) $$(pwd)

unlink:
	raco pkg remove $(PACKAGENAME)

htmldocs:
	raco scribble \
		--html \
		--dest htmldocs \
		--dest-name index \
		++main-xref-in \
		--redirect-main http://docs.racket-lang.org/ \
		\
		bitsyntax/scribblings/bitsyntax.scrbl

pages:
	@(git branch -v | grep -q gh-pages || (echo local gh-pages branch missing; false))
	@echo
	@git branch -av | grep gh-pages
	@echo
	@(echo 'Is the branch up to date? Press enter to continue.'; read dummy)
	git clone -b gh-pages . pages

publish: htmldocs pages
	rm -rf pages/*
	cp -r htmldocs/. pages/.
	(cd pages; git add -A)
	-(cd pages; git commit -m "Update $$(date +%Y%m%d%H%M%S)")
	(cd pages; git push)
	rm -rf pages

As you can see, it defines some targets for common actions like running raco setup, doing a local package link and unlink.

The really interesting bits are the htmldocs, pages and publish targets.

  • htmldocs simply runs raco scribble with the options I can never remember and always need to look up.

  • pages assumes that you’ve already created a gh-pages branch, and… does something really weird. It does a git clone of the gh-pages branch to a pages/ subdirectory. Yes, it makes another Git repo down there.

  • publish

    • Deletes the contents of that pages/ subdirectory — but leaving its .git folder alone — and copies the htmldocs/ files into pages/. In other words it makes the files in pages/ be an exact copy of htmldocs/.

    • Commits all changes (if any), using an “Update datetime” commit message.

    • Does a git push. This is interesting. To what “remote” repo does it push? Not GitHub. It pushes to the repo in the parent directory — the one from which it was cloned.

    • Blows away the pages/ subdirectory.

Voila, the gh-pages branch has been updated with the latest HTML output files, and only those files. Very cool.

My confusion

When I tried using it, two things confused me:

  1. When publish finishes, the result has not yet been pushed to the remote for the main repo — nothing has been pushed back to GitHub, yet. This confused me at first. I thought “publish” would mean, “actually push to GitHub”.2 To do so, there needs to be one more git push, up in the main repo directory, as a new last step.
  1. If you’re using a newer Git, like version 1.9.3, when you do just git push you may see this warning:

    warning: push.default is unset; its implicit value is changing in
    Git 2.0 from 'matching' to 'simple'. To squelch this message
    and maintain the current behavior after the default changes, use:
    
      git config --global push.default matching
    
    To squelch this message and adopt the new behavior now, use:
    
      git config --global push.default simple
    
    When push.default is set to 'matching', git will push local branches
    to the remote branches that already exist with the same name.
    
    In Git 2.0, Git will default to the more conservative 'simple'
    behavior, which only pushes the current branch to the corresponding
    remote branch that 'git pull' uses to update the current branch.
    
    See 'git help config' and search for 'push.default' for further information.
    (the 'simple' mode was introduced in Git 1.7.11. Use the similar mode
    'current' instead of 'simple' if you sometimes use older versions of Git)

    To make it go away, you could either:

    • Make the global configuration change it recommends.

    • Be explicit and say git push origin gh-pages. I’m leaning towards this so that the makefile will work for someone else, regardless of their Git config.

Combining these two points, here is the makefile I made to use with my #lang rackjure project:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
PACKAGENAME=rackjure
COLLECTS=rackjure
SCRBL=rackjure/manual.scrbl

all: setup

clean:
	find . -name compiled -type d | xargs rm -rf
	rm -rf htmldocs

setup:
	raco setup $(COLLECTS)

link:
	raco pkg install --link -n $(PACKAGENAME) $$(pwd)

unlink:
	raco pkg remove $(PACKAGENAME)

htmldocs: $(SCRBL)
	raco scribble \
		--html \
		--dest htmldocs \
		--dest-name index \
		++main-xref-in \
		--redirect-main http://docs.racket-lang.org/ \
		\
		$(SCRBL)

pages:
	@(git branch -v | grep -q gh-pages || (echo local gh-pages branch missing; false))
	@echo
	@git branch -av | grep gh-pages
	@echo
	@(echo 'Is the branch up to date? Press enter to continue.'; read dummy)
	git clone -b gh-pages . pages

publish: htmldocs pages
	rm -rf pages/*
	cp -r htmldocs/. pages/.
	(cd pages; git add -A)
	-(cd pages; git commit -m "Update $$(date +%Y%m%d%H%M%S)")
	(cd pages; git push origin gh-pages)
	rm -rf pages
	git push origin gh-pages

However in case I change it after this blog post, see the latest version here.

Also, FYI, here’s a variant I made for Fear of Macros. This one has targets to build both multi-page and all-in-one HTML.


Racket doesn’t need as much external “infrastructure” as some other languages. You don’t need this for very casual projects. But a makefile like this, plus perhaps a travis.yml for Travis-CI, can be nice for some projects.

  1. Although overall I much prefer Racket’s new package manager, the old PLaneT system automatically hosts Scribble documentation. When using the new package manager, this makefile makes it much less tedious to host the documentation on GitHub. 

  2. To be clear, Tony did it this way on purpose. He wanted the final push to GitHub to be something he did manually, as a safety-check. I completely understand that. I just expected and want it to behave differently.