# dhake $ dhake -j 4▊

A build tool whose buildfile can’t lie.

Dhall is total, terminating, and typechecked — so your build definition always means exactly what it says. Typed actions · incremental · parallel · hash-verified · sandboxed · self-hosting.

Build (self-hosting)

// dhake builds itself from a Dhakefile.dhall

No build system required — dhake evaluates its own buildfile and builds dhake.com from source. The committed dhake.com is the bootstrap binary.

$ ./dhake.com.dbg          # evaluates Dhakefile.dhall, builds dhake.com
$ ./dhake.com.dbg --list   # list targets
$ ./tests/build.sh ./dhake.com.dbg   # run the test suite

Features

// parallel · typed · incremental · verified · sandboxed · cached
01

Parallel builds

-j N runs up to N independent targets concurrently. On first failure it stops scheduling new targets but lets running ones finish (make-style).

02

Typed actions

Recipes are Dhall tagged-union values: Shell, Copy, Mkdir, Rm, Touch, Move, Symlink, Chmod, Echo, Env, Run.

03

Dependency graph

Topologically orders targets, detects cycles, and builds only the subgraph reachable from the requested target(s).

04

Incremental

Nanosecond-mtime up-to-date checks skip unchanged targets and their dependency closures.

05

Dry-run & phony

-n prints the actions without running them; phony targets (e.g. clean) are always rebuilt.

06

Self-hosting, verified

A single cosmocc APE that runs on Linux, macOS, Windows, and the BSDs — and builds itself, with every source and the produced binary pinned by hash.

07

Verified builds

hash / depsHash pin the expected sha256 of outputs and source deps, checked before and after every build. Mismatches fail hard — unless you pass --warn-hash-mismatch, which prints the new hash so pins are trivial to update.

08

Sandboxing (Landlock + seccomp)

Opt-in write-containment by default — recipes can only write under the build tree, /tmp, and an explicit unveil whitelist. Set readExec to also restrict reads and execs to the toolchain. Set denyNetwork to block network socket creation via seccomp. A rogue recipe can't touch the rest of your disk or phone home.

09

Recursive Mkdir / Rm

Mkdir and Rm support parents / recursive flags for mkdir -p and rm -rf behaviour, while legacy bare-path usage stays non-recursive.

10

Lockfile / SBOM

--lock[=FILE] writes a machine-readable dhake.lock after a successful build: every target's output and dep hashes plus its transitive dependency closure. A commit-and-diff supply-chain artifact for CI and provenance tools.

11

Verify / check

--verify (alias --check) pre-flights a build: it checks every pinned hash and up-to-dateness without running any recipe. A cheap CI gate.

12

Content-addressed

--hash-uptodate decides up-to-dateness by content hash instead of mtime — a touched-but-unchanged file no longer triggers a rebuild.

13

Dev-loop watch

--watch (alias -w) uses inotify on your source-file dependencies and rebuilds on any change — edit-save-refresh becomes a single seamless loop.

14

Explain / why

--explain (alias --why) prints why each dirty target needs rebuilding — newer dep, hash mismatch, or missing output — without running anything.

15

Quiet / silent

--quiet (alias -s) suppresses per-recipe command echo; summaries and errors stay visible.

16

Build parameters

--define KEY=VALUE (alias -D) injects values into the buildfile's env: imports — one Dhakefile does debug/release or -O0/-O3, CMake-style.

17

Dependency graph

--graph[=dot|mermaid] dumps the full resolved graph (target→dep edges, phony styling, expected hashes) — perfect for inspecting the topology and for docs.

18

Per-target cwd

An optional cwd field runs a target's recipe in a subdirectory — no shell cd boilerplate, and it composes with the sandbox.

19

Multi-arch builds

--arch=NAME sets $DHAKE_ARCH and lets targets carry an arch filter — one Dhakefile cross-compiles x86_64 and aarch64 from a single run.

20

Build cache

--cache[=DIR] adds a ccache-style cache keyed on the target name, arch, recipe, and content hashes of all inputs. Unchanged inputs restore the output and skip the recipe — pinned output hashes are still verified on restore.

Example Dhakefile.dhall

// targets = List { mapKey, mapValue } · default required
let Action = < Shell : Text
             | Rm : Text >
let Target = { deps : List Text, phony : Bool, recipe : List Action
             , hash : Text
             , depsHash : List { path : Text, hash : Text }
             , cwd : Text
             , arch : Optional Text             }
in  { sandbox = { enable = True, readExec = True, denyNetwork = True, unveil = [ "rwc:~/.cache", "rwc:~/.npm" ] }
  , targets =
      [ { mapKey = "hello"
        , mapValue = { deps = [ "hello.c" ], phony = False
                     , recipe = [ < Shell = "cc -o hello hello.c" > ]
                     , hash = "sha256:abc123…"
                     , depsHash = [ { path = "hello.c", hash = "sha256:def456…" } ]
                     , cwd = "src"
                     , arch = None Text
                     }
        }
      , { mapKey = "clean"
        , mapValue = { deps = [] : List Text, phony = True
                     , recipe = [ < Rm = "hello" > ] }
        }
      ]
  , default = "hello"
  }

Add optional hash and depsHash fields to pin the expected sha256 of outputs and source deps, cwd to run the recipe in a subdirectory, and arch to limit a target to a specific architecture. A top-level sandbox block runs every recipe in a Landlock write-containment sandbox — see the README.

Why Dhall?

// total · terminating · typechecked

Makefiles are turing-tarpit shell scripts with surprising whitespace semantics. Dhall is a strongly-typed, total configuration language — your build definition gets type checking, imports, and reusable functions, and it always terminates. The build plan is a plain value you can reason about, not a recipe of shell side effects.