Quartz v5.25

Quake — The Quartz Build System

A self-hosted build automation tool written in Quartz. Replaces Rake, completing the self-hosting story.

Quick Start

# List available tasks
./self-hosted/bin/quake --list

# Run a task
./self-hosted/bin/quake build
./self-hosted/bin/quake qspec
./self-hosted/bin/quake format_check

# Get help
./self-hosted/bin/quake --help
./self-hosted/bin/quake --quake-help   # Launcher-specific help

Architecture

Quake has three layers:

1. Runtime Library (std/quake.qz)

The core library that Quakefiles import:

import * from quake

def main(): Int
  task("hello", "Say hello") do ->
    puts("Hello!")
  end
  return quake_main()
end

API Surface:

CategoryFunctions
Taskstask(name, desc, body), task_dep(name, desc, deps, body), task_alias(name, deps)
Shellsh(cmd), sh_capture(cmd), sh_quiet(cmd), sh_maybe(cmd)
Filesrm(path), mv(src, dst), cp(src, dst), mkdir_p(path), rm_rf(path)
Globglob(pattern) — pure Quartz, supports * and **
Envenv(key, val), env_get(key)
Progressstep(label, body) — cyan → label ✓ output
Errorsfail(msg) — red error output, exit 1
CLIquake_main() — parses --list, --help, -v, -q

Features:

  • Dependency resolution: topological sort with diamond-deduplication
  • Did you mean?: Levenshtein-like fuzzy matching for unknown task names
  • Aligned listing: Task names and descriptions in aligned columns

2. Launcher Binary (tools/quake.qzself-hosted/bin/quake)

The launcher handles:

  • Quakefile discovery: walks from cwd up to root looking for Quakefile.qz
  • Compile-and-cache: compiles the Quakefile through quartz → llc → clang, caches in .quake/bin/
  • Cache invalidation: file-size stamp detects changes; stale cache triggers recompilation
  • Compiler discovery: checks QUARTZ_HOME, relative path, PATH
  • Arg forwarding: passes all non-launcher args to the compiled Quakefile

3. Quakefile (Quakefile.qz)

The project’s task definitions. The Quartz project’s Quakefile includes:

TaskDescriptionRake Equivalent
buildCompile the compiler (debug)rake build
build:releaseCompile with -O2rake build:release
qspecRun 284 QSpec test filesrake qspec
fixpointgen0→gen1→gen2 validationrake quartz:fixpoint
validatefixpoint + benchrake quartz:validate
formatFormat all .qz filesrake format
format_checkCheck formatting (CI)rake format_check
cleanRemove build artifactsrake clean
benchFull benchmarksrake bench
snapshotsIR snapshot testsrake snapshots
installInstall versioned binaryrake install
releaseFull release pipelinerake release[ver]

Design Decisions

Why Not Just Use Make/Rake?

  1. Self-hosting: Quartz should build itself without external language runtimes
  2. Dogfooding: Using Quartz for build automation surfaces real-world pain points
  3. Single binary: No Ruby/Python/Node dependency — just the Quartz compiler

Compile-and-Cache vs Interpreted

Quakefiles are compiled to native binaries (via LLVM), not interpreted. This means:

  • Fast execution: Native-speed task logic, no interpreter overhead
  • Normal Quartz: Full language features (closures, generics, pattern matching)
  • Cached: Only recompiles when the Quakefile changes

File-Size Stamp vs Content Hash

Cache invalidation uses file size comparison rather than content hashing. This is a pragmatic trade-off:

  • Pro: Zero-cost check (single stat call)
  • Con: Doesn’t detect same-size changes (rare in practice)
  • Future: Content hashing planned when std/crypto.qz is available

Adding the .quake/ Directory to .gitignore

echo ".quake/" >> .gitignore

Writing a Quakefile

import * from quake

def main(): Int
  # Simple task
  task("hello", "Print a greeting") do ->
    puts("Hello, world!")
  end

  # Task with progress steps
  task("deploy", "Deploy the application") do ->
    step("Building") do ->
      sh("make build")
    end
    step("Uploading") do ->
      sh("rsync -avz ./dist/ server:/app/")
    end
  end

  # Task with dependencies
  var deps: Vec<String> = vec_new<String>()
  deps.push("hello")
  deps.push("deploy")
  task_dep("all", "Run everything", deps) do ->
    puts("Done!")
  end

  return quake_main()
end