Skip to content

Instantly share code, notes, and snippets.

@steshaw
Last active June 23, 2022 08:10
Show Gist options
  • Save steshaw/9f81d64526856c761cdc43290f0fc017 to your computer and use it in GitHub Desktop.
Save steshaw/9f81d64526856c761cdc43290f0fc017 to your computer and use it in GitHub Desktop.

Project Scripts

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).
  • 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.

Some Haskell specifics

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.

References

I don’t have a complete example of the ideas at the moment. There are some public inspirations:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment