The cache key
The cache key is a SHA-256 hash computed from everything that can legitimately change a target’s output. If two builds produce the same key, they produce the same outputs. If anything in the recipe shifts, the key shifts, and Giant rebuilds.
What’s in the hash
Section titled “What’s in the hash”Composed in this order (the exact byte stream matters for reproducibility):
- Schema version marker - a leading version tag. It bumps when a Giant change would alter what a target produces or how the key is computed, invalidating old entries deterministically. Routine Giant releases don’t bump it, so upgrading Giant keeps your caches warm.
- The command - verbatim. Changing
go buildtogo build -trimpathchanges the key. - The cwd - workspace-relative path.
- Env vars - the target’s
env:, sorted by name, plus one built-in Giant always sets:GIANT_TARGET_TRIPLE. - File inputs - for every file matched by an input glob, its
resolved workspace-relative path and content hash. Sorted by path.
A package-relative input (
src/foo.rs) is resolved against the package directory before hashing, so the hash always sees the same workspace-relative path regardless of where the glob was written. - Dep outputs - for each dependency target, its
outputs_content_hash(the hash-of-hashes of its outputs), NOT its cache key. Sorted by hash so dep order in your YAML never shifts the key. This is the early-cutoff property (see below). (giant explaindisplays this section sorted by dep label for readability - the order in the hash itself is by hash value.)
outputs: are NOT in the cache key. The recipe determines what
gets built; the recipe’s hash determines if we’ve seen it before.
Neither the workspace name nor the target label is hashed. Two targets with an identical command, inputs, env, and deps produce the same cache key - the label does not disambiguate them. If you want two recipes to cache separately, something in the recipe itself has to differ.
What’s NOT in the hash
Section titled “What’s NOT in the hash”- The current time, current user, current host. Two users on two machines running the same command on the same inputs get the same cache key.
- The Giant version. Upgrading Giant doesn’t invalidate your cache. The schema version marker covers the rare release that changes what a recipe would produce.
- Output file paths. Changing where outputs land doesn’t shift the key (but it does change the recipe - adjust thoughtfully).
- Comments in your config file. Giant parses the YAML; whitespace and comments are normalized away.
- The order of inputs in your YAML. Inputs are sorted before hashing.
Inspecting a key
Section titled “Inspecting a key”$ giant explain //cmd/server:servertarget: //cmd/server:servercache key: 3a7f9c4e8b2d1f5e6a8c9d7e4f3b2a1c5d6e9f8a7b4c3d2e1f5a6b7c8d9ecache state: hit
command: go build -o bin/server ./cmd/servercwd: //
env (2): CGO_ENABLED=0 GIANT_TARGET_TRIPLE=x86_64-unknown-linux-gnu
file inputs (12): cmd/server/main.go sha256:9f3c8d... internal/auth/auth.go sha256:7e2a4b... ...
deps (2): //proto:gen sha256:a1b2c3... //src/core:core sha256:d4e5f6...giant explain is the first thing to reach for when “why did this
rebuild?” comes up.
Comparing two breakdowns
Section titled “Comparing two breakdowns”When you want to know what’s different between two targets’
keys - same recipe, different arch flag; same target before/after a
refactor - pass --diff <other-target>:
$ giant explain //cmd/server:server --diff //cmd/server:server-debugcomparing: - //cmd/server:server (3a7f9c4e…) + //cmd/server:server-debug (8d2b1f4a…)
── command ── - go build -o bin/server ./cmd/server + go build -gcflags='all=-N -l' -o bin/server-debug ./cmd/server
── env (user) ── - CGO_ENABLED=0 + CGO_ENABLED=1Identical fields are suppressed. If the keys match, you get a “cache keys are identical” line and nothing else.
Early cutoff
Section titled “Early cutoff”A subtle but valuable property: an upstream rebuild doesn’t always invalidate downstream.
Scenario:
- Target
//proto:gendepends onproto/foo.proto. - Edit
proto/foo.proto(cosmetic change - whitespace in a comment). //proto:gen’s cache key shifts (input content changed) → rebuild.- But
//proto:genproduces byte-identical output (gen/foo.pb.gois the same). - Downstream
//cmd/server:serverconsumesgen/foo.pb.go.
server’s cache key contribution from //proto:gen is
outputs_content_hash, NOT //proto:gen’s cache key. Since the outputs
are byte-identical, the hash-of-hashes is unchanged. server
cache-hits, never re-runs.
This is what makes large monorepos tolerable. Whitespace and comment edits don’t ripple through the dep graph as full rebuilds.
Toolchain versions
Section titled “Toolchain versions”The cache key covers the command, inputs, env, and dependency outputs - but
not the compiler that runs the command. Two machines on different Go or
rustc versions compute the same key for the same target, and a shared remote
cache will hand one a stale artifact built by the other. So a toolchain
version has to be made part of the key explicitly.
The right way is a toolchain target: a toolchain-tagged target whose
input is whatever pins your tools (a devenv.lock / flake.lock, an asdf
.tool-versions, a checked-in or git-lfs binary) and whose output is a
content-derived identity. Build targets deps: on it, so a toolchain bump
re-keys exactly the targets in that ecosystem and leaves the rest cache-warm.
Pinning toolchains is the full guide - it covers
devenv/Nix (resolving the store path), git-lfs binaries (hashing the bytes),
per-tool targets, and why a system-installed tool can’t be pinned honestly.
The quick-and-dirty alternative is to stamp the version into env: so it
folds into the key directly:
- name: "server" command: "go build -o //bin/server ." env: GOVERSION: "1.23.4" # bump this by hand when you bump GoThis works but is fragile - you have to remember to bump it, and nothing
checks that the string matches the go actually on PATH. Prefer a toolchain
target, which derives the identity from the real tool.