What I built: raptor, a zero-dependency task runner for a common subset of go-task/task. One binary. Blank go.mod. 24 standard library substitutions. Built for Zero Dependency 2026.
1. What I Reimplemented
The honest headline: I replaced a Go tool that itself replaces make — and did it with an empty go.mod (zero third-party imports).
go-task/task is a popular, cross-platform, YAML-driven task runner. In its dependency tree, you will find gopkg.in/yaml.v3 and github.com/mvdan/sh (an entire shell interpreter written in Go). I wanted to understand what those packages actually buy you — and rebuild that machinery with only Go standard library primitives:
- Indentation-sensitive parser: Hand-written state machine for the Taskfile subset with line:column error reporting.
- DAG dependency scheduler: Depth-first search for topological sort and cycle detection.
-
Bounded parallel execution: Channel-based semaphores scaling to
runtime.NumCPU(). -
Incremental builds:
sources/generatestracking using timestamp andcrypto/sha256content hashing. -
Dynamic variables & templating: Evaluating
{ sh: cmd }once per run, plus{{.KEY | upper}}template functions andfor:loops. - Deterministic output modes: Interleaved live streaming, prefixed lines, and topological group output.
2. What the Standard Library Made Painfully Obvious
YAML is deceptively large, and Go's stdlib has none of it.
That was the biggest lesson. Go ships with encoding/json, but zero YAML support. Within an hour, "writing a small YAML parser" turned into "writing a strict parser for a subset of YAML that fails loudly on invalid syntax." Indentation isn't formatting — it is syntax. A tab changes semantic meaning. A # inside quotes is text, not a comment. A : in a URL isn't a key delimiter. Writing this parser taught me why developers import yaml.v3 without thinking twice.
The stdlib has no recursive `-glob.** filepath.Glob
handles flat , but not recursive path matching (/.go`). Hand-rolling a segment-by-segment recursive matcher was subtle — getting it wrong causes incremental builds to silently fall back to rebuilding everything.
Content hashing over timestamps.
File modification times (mtime) fail when developers switch git branches or restore older commits. Implementing true content-based incremental caching meant piping source files into crypto/sha256 to record deterministic .checksum states.
3. The Edge Case That Ate an Afternoon: Concurrency Log Tearing
When 5 tasks run concurrently and write to os.Stdout, terminal logs tear and scramble unpredictably.
My initial thought was to buffer each task's stdout in a memory buffer and flush it upon completion. But if a task streams a multi-gigabyte build artifact, memory blows up.
The fix: Designing an opt-in deterministic output engine (--ordered-output) that spools concurrent task streams to OS temp files (os.CreateTemp), and re-emits them strictly in topological DAG order. The lesson wasn't about clever data structures — it was deciding how to manage resources cleanly under concurrency.
4. The 24-Substitution Architecture
We documented every single standard library replacement in STDLIB.md:
-
gopkg.in/yaml.v3➔ Hand-written indentation tokenizer (bufio.Scanner+bytes.Buffer) -
spf13/cobra➔flag.FlagSetwith custom dispatch -
golang.org/x/sync/errgroup➔ Channel semaphoremake(chan struct{}, jobs) -
bmatcuk/doublestar➔ Recursive segment matcher onfilepath.Match -
mvdan/sh➔ Native shell dispatch (cmd.exe /con Windows,sh -con Unix)
Conclusion
One command to compile (go build .), zero dependencies to audit, and byte-for-byte reproducible. The Go standard library is vastly more capable than most modern dependency trees give it credit for.
Top comments (0)