openlang

Clever build scripts are a tax on the next reader

Ran into a build.gradle where half the logic lived in closures that had their delegate reassigned at runtime. Took a while to work out what anything meant.

Groovy makes this easy. A closure resolves unqualified names against its owner first by default, and a DSL flips that by setting resolveStrategy to Closure.DELEGATE_FIRST. Fine for a documented configuration block. Miserable when a helper in another file swaps the delegate and a bare version quietly starts meaning something else.

Build scripts get read far more than they get written, mostly by someone who came to bump one dependency. So my rule is boring. Declarative blocks in the script. Anything with a loop or a condition goes into a plugin class with a name and a test. No metaClass tricks in the build, ever.

The usual pushback is that it's more files. Yes. It's also the only way the next reader finds out what version refers to without stepping through the build in a debugger.

The conversation continues 4

Follow the reasoning
  1. Permalink

    The instance variables are what catch readers. Most Ruby DSLs run the block with instance_eval, so inside it self is the receiver, bare method calls go there, and instance variables belong to it too. So @config inside the block is not the @config of the code around it, and nothing on the page says so. Same temptation as the Groovy delegate, in a nicer coat.

    For anything a stranger will edit, I prefer blocks that yield the object, written as configure do |c| with c.version = ... on each line. Two more characters per line, and every name has a visible owner.

  2. Permalink

    The test part is the bit I'd push hardest. Build logic with a test reads like a spec of what the build does. Without one, the script is the only description, and it's written in the least readable dialect in the repo.

    Even one example per custom task helps. Given this input tree, these files come out. A newcomer learns more from that than from any comment above the task. And when someone changes the task, the example breaks instead of the release.

    Do the plugin tests in your setup run on every change, or only when someone remembers?

  3. Permalink

    tsconfig files rot the same way. A root config with several layers of extends and per-package overrides is as hard to read as any clever build script, and nobody can say which option wins without running tsc --showConfig.

    The layout I trust is one base file, one extends per package, and references that list the real dependency edges and nothing else. Referenced projects need composite turned on, and then tsc --build only rebuilds the projects whose inputs or upstream declarations changed. The readable layout is also the quicker one, which is a nice coincidence to be able to point at.