Documentation
¶
Overview ¶
Package buildcache is the ONE store-backed cache of host-built plugin provider binaries.
WHY THIS PACKAGE EXISTS (R2). Before it, the plugin build cache had TWO live sibling implementations of one concept: charly/charly/plugin_loader.go's cluster and a copy in sdk/deploykit/bake_plugins.go. Both are removed across the two ordered PRs of ONE cutover (producer-first, since a PR cannot span repos):
- the sdk/deploykit COPY is deleted in the SAME PR that adds this package — THIS one;
- the package-main LOADER cluster is deleted in the pair's SECOND PR, which is also where the loader's two build arms are repointed onto Build. It is NOT deleted here, so at THIS commit the loader still owns its cluster.
The copy deleted here was LOAD-BEARING until the loader is repointed, not vestigial: its caller runs OUT-OF-PROCESS in candy/plugin-build, imports only the sdk, and therefore cannot call the package-main copy. That is why the replacement had to PUBLISH first, and why it is this package — not a repoint — that is the one home. See "WHAT THIS PACKAGE REPLACES" below for the per-PR split.
THE KEY IS D-STORE-4's KEY (the phase's whole claim). D-STORE-4: "A plugin binary's key covers its module source, go.sum, the toolchain and the platform." That input set IS the store key, which deletes the sidecar dance BY CONSTRUCTION rather than by discipline:
- Freshness is asked of the STORE ("is this key present?"), never of a sidecar digest written beside the binary. There is no ".stamp" concept in this package at all, and no way for a stamp and its binary to disagree — the key addresses the binary's whole input set.
- The key is CONTENT, not a path. The old path key ("<safeName>-<sha256(srcDir)[:8]>") could not answer "is this binary still current" — it is unchanged by a dependency bump — which is exactly why a content-stamp sidecar had to exist. A key over the inputs changes on any input change, so the question the stamp answered is answered by the key itself.
- No absolute path and no timestamp reaches a payload (D-STORE-6): roots are hashed by LABEL plus content (never by their path), the key is a digest, and the only time-bearing value in the key is a toolchain version string — an INPUT, not a clock reading.
KEYVERSION — THE INVALIDATION LEVER, and the only sanctioned way to change the key's meaning. keyVersion participates in the key, so BUMPING IT INVALIDATES EVERY ENTRY in the store at once (each old key is simply absent, and the store's immutable blobs stay reachable by digest until reclamation). Add or rename a component and you MUST bump it, or every existing entry silently keeps answering a question the new composition no longer asks — entries stranded as permanently-valid-but-wrong. Nothing else belongs in the key without that consequence in mind.
WHAT THIS PACKAGE REPLACES, and WHERE EACH HALF DIES (R5, claim-keyed grep in each PR). The plugin build cache had two sibling clusters for one concept; this cutover is delivered as TWO ORDERED PRs (producer-first, since a PR cannot span repos), and each PR is complete for its repo:
- THIS PR (sdk, producer) DELETES sdk/deploykit/bake_plugins.go's cluster — pluginBuildCacheDir, pluginSourceTag, buildPluginBinary, safePluginBinName (deduped onto SafePluginBinName here) and the five helpers that existed only to serve the deleted build — and repoints EmitBakedPlugins at Build. sdk's main therefore never carries two implementations.
- THE PAIR'S SECOND PR (charly, consumer) DELETES the package-main cluster in charly/charly/plugin_loader.go, including all of plugin_build_stamp.go with pluginBuildStamp/pluginStampPath/pluginBinaryIsFresh/writePluginStamp and the pluginSourceRoots/localReplaceDirs/hashDirContent/envValue helpers that served the STAMP — and with it the ".stamp" and ".lock" sidecars, which this package makes unnecessary by construction.
hashDirContent, localReplaceDirs and envValue are NOT deleted: they LIVE HERE now, carried over because a content key over the module source needs exactly that traversal. Only the loader's copies of them die, with the cluster they served.
THE LOCK MOVED OUT OF THE CACHE (the homes rule). A store tree contains NO locks: a lock is transient runtime state with no content address, and a digest-keyed lock could even be shared between two processes that must not share it. The per-key build lock therefore lives under the RUNTIME root, and storekit guards the store side with a test that walks a full store lifecycle and fails on any ".lock"/"locks/" artifact. This package must never re-introduce one.
THE VIEW IS A HARDLINK. What the loader execs is a hardlink of the payload blob, never a second copy (LinkBlob has deliberately NO cross-device fallback, because a silent copy is how "bytes exist once per digest" would stop being true unnoticed). SameFile lets a caller ASSERT the sharing, and the view lives under the store tree so the hardlink is same-filesystem by construction.
Index ¶
Constants ¶
const MediaTypePluginBinary = "application/vnd.opencharly.plugin.binary.v1"
Media type of a plugin-build payload. RFC 6838 conformant and opencharly-namespaced, so an entry is self-describing without claiming to be a runnable container image.
const StoreName = "plugin-build"
StoreName is the named store this cache lives in, under storekit's ONE cache root. It is deliberately its own store: the plugin-BUILD cache (host-compiled provider binaries) is a different thing from the pulled-plugin artifact store, and fusing them would give two lifecycles one name.
Variables ¶
This section is empty.
Functions ¶
func SafePluginBinName ¶
SafePluginBinName flattens a candy key (which may be an @github ref with slashes/colons) to a single filesystem-safe component. Exported because the deferred render closure (sdk/deploykit/bake_plugins.go) needs the same flattening for the baked binary's filename, and one implementation is the point of this package.
func SharedView ¶
SharedView reports whether viewPath and the store's payload blob are the SAME file (the same inode), i.e. that the bytes exist once. Callers and tests ASSERT the sharing with this rather than assume it — the operator's "cached once" property is proven by measuring the inode, never by the absence of an error.
Types ¶
type Key ¶
type Key struct {
// Plugin is WHICH plugin this binary is for, derived from the candy's LEAF name — never a path.
//
// It is in the key because D-STORE-4's inputs identify the BYTES, not the BUILT UNIT: two
// DIFFERENT plugins with byte-identical source (the same candy content installed under two
// names, a shared module built per-plugin, a test fixture reused across names) key identically
// without it, so one would OVERWRITE the other's entry. Measured: eight concurrent
// distinct-NAME builds sharing one module source produced ONE key and the store kept a single
// entry — the collision is real, and a same-source test cannot see it because in production two
// plugins usually differ in source too.
//
// The leaf name is used rather than the full candy key (which may be an @github ref with
// slashes) and rather than the source PATH (which would put a location in the identity): the
// leaf is the same keying a baked plugin binary's filename uses, so the cache and the baked
// route agree on what a plugin is called.
Plugin string
// ModuleSource digests the candy's own source tree PLUS every module it reaches through a
// local `replace` directive, transitively. CONTENT, not a path: two worktrees with
// byte-identical trees key alike, and an uncommitted edit inside a replace target
// invalidates — which a git-HEAD key would miss, and that is the case that matters most day
// to day.
ModuleSource string
// GoSum digests the module's go.sum. It is inside ModuleSource's traversal too, but D-STORE-4
// names it explicitly, so it is carried as its own labelled component: a reader auditing the
// key against the design should not have to infer it from a directory hash.
GoSum string
// Toolchain is runtime.Version() of the building toolchain.
Toolchain string
// BuildEnv is the platform/build-env axis, one "KEY=VALUE" per buildEnvKeys entry.
BuildEnv map[string]string
}
Key is D-STORE-4's input set for one plugin binary. Its String() IS the store key.
func ComputeKey ¶
ComputeKey derives D-STORE-4's key from a plugin candy's resolved source directory AND the plugin's identity.
The name is REQUIRED, not optional, because D-STORE-4's inputs alone identify the BYTES rather than the BUILT UNIT: two different plugins whose source is byte-identical would key alike and one would overwrite the other's entry. Measured — eight distinct-NAME builds sharing one module source and one key, with the store keeping a single entry.
func (Key) Components ¶
Components returns the key's components in a STABLE, deterministic order, so the canonical key string is reproducible and can be audited against the design without recomputing anything.
func (Key) String ¶
String renders the store key: the canonical, deterministic encoding of the components.
The store treats a key as OPAQUE — canonicalizing a set of build inputs into a key is the CALLER's semantic, and if storekit owned it the store would have to know what a build key is, which is the boundary we agreed against. So the composition lives here and the store only addresses it.
type Request ¶
type Request struct {
// SrcDir is the plugin candy's resolved source directory — its own Go module (go.mod plus a
// main serving via the plugin SDK).
SrcDir string
// Name is the plugin candy key, for diagnostics and the entry's identity. It may be an
// @github ref with slashes, so it is never used as a path component; the view filename uses
// its LEAF name, the same keying a baked plugin binary uses.
Name string
}
Request is one build-cache operation.
type Result ¶
type Result struct {
// ViewPath is what the caller execs: a hardlink of the payload blob, same inode.
ViewPath string
// Digest is the payload blob's content address.
Digest string
// Hit reports that the key was already present, so NO `go build` ran. This is the observable
// a caller — and the R10 bed — keys on: a hit is the ABSENCE of work, never a success exit,
// because a success exit is also produced by a cache that does nothing at all.
Hit bool
// Key is the store key this build was published under ("" only when the key could not be
// computed, in which case Build degrades to an unconditional rebuild).
Key string
}
Result is the outcome of Build.
func Build ¶
Build resolves D-STORE-4's key for req, serves the entry from the store on a HIT without running `go build`, and on a MISS compiles the provider binary, publishes it as a content-addressed payload blob, records the keyed entry, and returns a hardlink view.
A key-computation error is NEVER fatal to the caller's build: it degrades to an unconditional rebuild published under no key — correct, just not reusable — mirroring the old stamp's failure contract (a stamping error was never fatal either). A cache must never fail a build.