Choose a cache location¶
Without a cache, every resolve re-downloads — and a resolve that verifies bytes it cannot store returns an error rather than discarding them silently. So a cache is effectively required; the decision is where.
The conventional location¶
dir, err := artifacts.UserCacheDir()
if err != nil {
return err
}
client, err := artifacts.New(verifier, artifacts.WithCache(artifacts.NewDirCache(dir)))
UserCacheDir() returns <os.UserCacheDir()>/phpboyscout/artifacts —
~/.cache/phpboyscout/artifacts on Linux,
~/Library/Caches/phpboyscout/artifacts on macOS.
Note it is not named after your tool. Two tools belonging to the same user and
resolving the same runtime share one copy. The alternative produces a directory
named after whichever tool installed it first, which is how krites' resolver
ended up populating a krites directory for a different tool entirely.
That sharing is within a user account, not across one. Cached files are
written with mode 0600 — a consequence of the atomic temp-file-and-rename
write — so a cache populated by one user is not readable by another. A genuinely
multi-user shared cache needs each user to have their own, or a wrapper that
relaxes the mode after Put returns.
NewDirCache takes an explicit path rather than calling UserCacheDir() itself.
A library that decides where to write on someone's disk without being asked is a
library that surprises somebody.
A pinned location¶
For a container image, a CI job, or anywhere the home directory is not meaningful:
The directory is created on first write, with mode 0755.
The layout¶
which mirrors the channel, so a cache is browsable and a stale entry is obvious
to whoever is deleting it. Clearing one version is rm -rf <root>/onnxruntime/1.28.0.
What the cache checks¶
Bytes are verified on the way out as well as on the way in. A hit is re-hashed against a digest from a manifest verified in the same resolution, so an entry is never returned on the strength of having been verified once, at some point, by something.
An earlier version of this guide said the opposite — that a hit checked presence only, on the reasoning that re-hashing would make the cache slower than the download it avoids. That reasoning does not survive arithmetic: hashing tens of megabytes locally is cheaper than fetching them, and cheaper than the OpenPGP and WKD work already happening on the same path.
The consequence for choosing a location is smaller than it was, but not gone:
- an entry someone has edited is detected and replaced, not served;
- so a corrupt or tampered cache costs a re-download rather than a wrong artefact;
- but the file is still opened by path after it is returned, so a local actor
who can write to the directory between the check and your
openis outside what this defends. That is the same caveat the trust model records.
A cache under /var/lib or /opt shared between users should still not be
group- or world-writable — the failure is now noisy rather than silent, which is
an improvement and not a licence.
If you want to audit a cache you did not populate:
which re-reads the file and re-hashes it against a digest you supply — from
client.Manifest, which is signed.
Concurrency¶
Put writes to a temporary file and renames, so a concurrent reader sees either
no file or a complete one, never a half-written artefact that Has would then
report as present. Two processes resolving the same version resolve to the same
bytes, so whichever rename wins is correct.
Supplying your own¶
Cache is an interface — implement it for an in-memory cache in tests, an
object store, or a layout your deployment already has:
type Cache interface {
Path(ref Ref, file string) string
Has(ref Ref, file string) bool
Put(ref Ref, file string, data []byte) (string, error)
}
Put receives already-verified bytes. A cache that stores unverified bytes
is a cache that serves them, so verification is not something an implementation
is trusted to repeat.