DRAFT This document is only a rough draft. Feedback welcome.
There are some downsides to using a project Makefile
and so I would like
to advocate for using project scripts instead.
I understand the inclination towards using a Makefile
in software
projects. The idea is to have a common place to file away all sorts of
useful commands that can be shared amongst team members. This gives
developers a common understanding of the project and is a boon to newcomers.
The practice of using a Makefile
to achieve these ends seems quite
prevalent in Haskell projects. On Rust projects, for instance, it is much
less common — probably because of the standardisation around Cargo.
Neither on NPM/Yarn projects because of the standardised “scripts” field of
their package.json
.
Downsides of a project Makefile
:
- You’ll have to learn a good deal of GNU Make and it’s pretty arcane:
- The surprising differences in interpolation between Make and Bash. e.g.
the difference in meaning between
$TERM
,${TERM}
, and$(TERM)
in Make and Bash. - The two flavours of variables, i.e. definitions using
=
versus:=
- The two types of prerequisites: normal vs order-only. A truly odd thing to need to know when all of your targets are phoney. You might try an internet search about the interactions between prerequisite types and phoney/non-phoney targets to see how folks can be puzzled.
- The unreliable/surprising way in which GNU Make variables are overridden (or not) with variables from your shell environment. See 6.10 Variables from the Environment.
- The array of special variables though these are unlikely to be used in a phoney Makefile.
- Quirky requirement to prefix commands with
@
to prevent tracing each shell command that is executed. - The concrete syntax of Makefiles requires tab characters which is a well-known pitfall for novices.
- Newcomers to a project will need to learn GNU Make to maintain the project’s commands.
- Using GNU Make does not preclude knowing Bash as you need to know Bash to use GNU Make.
- Early-career developers may have never been exposed to GNU Make because most C and C++ projects are built with newer build tools these days (such as CMake, Meson, GYP, Bazel, and the rest).
- The surprising differences in interpolation between Make and Bash. e.g.
the difference in meaning between
- Linters for Make don’t seem as useful as ShellCheck is for Bash, so you find more errors using a Bash script.
- When reviewing project Makefiles, I usually find errors such as not all
phoney targets are declared as
.PHONY
. - It’s nice to have
make help
, which is usually hardcoded and so often out-of-sync with the project’s commands/targets.
An alternative is to write a series of Bash scripts with a single dispatcher
script called project
as an interface to the others (in a similar way to
which the Makefile
is used). I put the scripts into a project-root
script/
directory and use direnv
, along with a .envrc
to put
script/
on the PATH
automatically. When you are also using Nix, this
can be combined with nix-direnv for fast, persistent use_nix
.
What’s nice about Bash scripts is mostly ShellCheck and avoidance of the
bootstrapping problem (as Bash is already present on most systems). With the
central dispatching script, you retain the developer experience that folks
expect. e.g. make build
becomes project build
, make fmt
becomes
project fmt
, and so on.
Instead of Bash scripts, I’ve tried Haskell scripts using stack script
but
this isn’t ideal as stack
isn’t always in scope. I was also using turtle
at the time. Sometimes, Stack would download a GHC binary distribution and
then compile turtle
and all it’s dependencies just to do a project build
or answer project --help
.
I’d like to try again to Haskell instead of Bash but rather than stack
script
, just using runhaskell
and with no external dependencies such as
turtle
. Some niceties along the lines of turtle
or shellmet and options
parsing primitives could be included directly. Alternatively, if your
project is “all in” on Nix, then along with direnv
and nix-direnv
, the
world becomes your oyster.
I don’t have a complete example of the ideas at the moment. There are some public inspirations:
- https://github.com/steshaw/nix-dev-env-example/ — includes
direnv
,use_nix
, project scripts, and starting/stopping services when entering a development shell with direnv+nix. There is no mainproject
script :(. - https://github.com/steshaw/turtle-shell-with-nix — using direnv+nix so that you can use Turtle Haskell scripts directly.
- An example in the wild spotten recently https://github.com/newhoggy/jgrep/blob/main/project.sh — there is no
direnv
here or Nix but this is a clear example of using a project script instead of aMakefile
.