kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 191086c86039bef04b03180077d01a52acf86360
parent 25c31e015969ea48307092158c5aaf6be03485d0
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 18 Jun 2026 11:32:19 -0700

Record build fetch blobs in traces

Diffstat:
Mdoc/BUILD_COORDINATOR.md | 44++++++++++++++++++++++++++++++--------------
Minclude/kit/build_coord.h | 8++++----
Msrc/build/bundle.c | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/build/coord.c | 38+++++++++++++++++++++++++++++++++++---
Msrc/build/coord.h | 4+++-
Msrc/build/resolve.c | 21+++++++++++++++++++++
Msrc/build/resolve.h | 7++++---
Msrc/build/runner.c | 32+++++++++++++++++++++++++++++++-
Msrc/build/runner.h | 4+++-
Msrc/build/trace.c | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/build/trace.h | 19++++++++++++++++---
Mtest/buildcoord/run.sh | 7+++++++
12 files changed, 328 insertions(+), 40 deletions(-)

diff --git a/doc/BUILD_COORDINATOR.md b/doc/BUILD_COORDINATOR.md @@ -54,17 +54,17 @@ the resolution algorithm that drives it. | **Test target request** | A request to execute a target as a test (`kit build test ...`). It uses the same recipe protocol and dependency discovery as a build request, but returns a typed test result. | | **Test result tree** | The directory a test recipe produces, plus coordinator-injected `stdout` and `stderr` files, captured as a CAS tree (`result-tree-id`). | | **PASS-only test trace** | A test trace is a cacheable claim only when the recipe exits 0. Nonzero exits return FAIL results for inspection but write no reusable trace. | -| **Base inputs** | The *leaves* of the dependency graph, dynamically requested by recipes: **config values**, **source files**, and **globs**. | +| **Base inputs** | The *leaves* of the dependency graph, dynamically requested by recipes: **config values**, **source files**, **globs**, and hash-pinned **fetch blobs**. | | **Target dep** | A dependency of one target on the *output tree* of another. The graph's interior edges, created by `need`. | | **Shallow trace** | A record of one build: its direct base inputs + its direct target deps (by output `tree-id`) → its output `tree-id`. | | **Deep trace** | A record of one build: `(root config-id, argv)` + a pointer to the transitive input closure (a **deepset**) → its output `tree-id`. | -| **Deepset** | One node of the transitive input-closure DAG (`deep-set-id` = `BLAKE2b(canonical body)`): a target's recipe-id + its direct source/glob leaves + its children's `deep-set-id`s. A self-verifying CAS blob; shared structurally across ancestors. | +| **Deepset** | One node of the transitive input-closure DAG (`deep-set-id` = `BLAKE2b(canonical body)`): a target's recipe-id + its direct source/glob/fetch leaves + its children's `deep-set-id`s. A self-verifying CAS blob; shared structurally across ancestors. | | **Tree cache** | On-disk *materialized* output directories, keyed by `tree-id`, ready to hand back as a filesystem path. | -The three base-input kinds are exactly the dynamically-requested leaves: a recipe -asks the coordinator for a config value, the bytes of a source file, or the -expansion of a glob, and each request is logged as a dependency so a later -rebuild knows precisely what to re-check. +The base-input kinds are exactly the dynamically-requested leaves: a recipe asks +the coordinator for a config value, the bytes of a source file, the expansion of +a glob, or a hash-pinned fetched blob, and each request is logged as a dependency +so a later rebuild knows precisely what to re-check. ## Build definitions @@ -232,6 +232,8 @@ opt src/main.c <blob-id> [glob] ; sorted by pattern src/*.c <glob-result-hash> +[fetch] ; sorted by blob id +<blob-id> [dep] ; sorted by (dep-name, dep-config-id, dep-argv-id) — each row replays one `need` //lib:core <dep-config-id> <dep-argv-id> <dep-output-tree-id> ``` @@ -303,6 +305,8 @@ src/main.c <blob-id> config.h - ; an ABSENT leaf: "-" = the path was absent when read [glob] ; this node's DIRECT glob leaves, sorted by pattern src/*.c <glob-result-hash> +[fetch] ; this node's DIRECT fetched blobs, sorted by blob id +<blob-id> [child] ; direct deps' deep-set-ids, sorted <deep-set-id of //lib:core's node> ``` @@ -338,19 +342,24 @@ src/*.c <glob-result-hash> pattern matched (sorted by path). It changes if any matched file is added, removed, or edited — so one glob row covers the existence *and* content of its whole match set, and files read through a glob need no separate `[source]` rows. +- `[fetch]` rows record hash-pinned external blobs requested through the recipe + protocol. Only the blob id is cache identity; URL hints are deliberately not + recorded because they are transport choices. A matching trace requires the + blob to be present in the local CAS, and trace bundles include these blobs so + imported traces retain the real dependency. - `[dep]` rows carry the **dep's config-id and argv-id** (and output `tree-id`); together with the dep's own serialized config/argv blobs this re-resolves each dep under the exact propagated config *and* argv it used, overlays included. **Why the deep trace needs only `root-config` + the deepset closure.** If the request's `config-id` equals `root-config`, its `argv-id` equals the recorded -`argv`, *and* refreshing the deepset finds nothing moved — every source/glob leaf +`argv`, *and* refreshing the deepset finds nothing moved — every source/glob/fetch leaf still matches the live workspace **and** every node's `recipe-id` still recomputes the same through the live definition — then every recipe in the closure has byte-identical inputs *and* identical code, so every `need` overlay it computes is identical, every effective config downstream is identical, and by determinism the output is identical. Any input that could perturb a downstream overlay (a source a -recipe branches on, a config value) is itself either a recorded source/glob leaf, +recipe branches on, a config value) is itself either a recorded source/glob/fetch leaf, a per-node `recipe-id`, or folded into `root-config`, so nothing escapes the check. Using the whole `root-config` (rather than just consumed keys) is a deliberate, conservative simplification: an *unconsumed* @@ -490,14 +499,14 @@ resolve(T, cfg, argv, chain): if S.argv != argv.id: continue # local config M = config_by_id(S.config) # the built map (serialized blob) if any consumed key in S.config-keys differs between cfg and M: continue - if any direct source/glob leaf of S changed: continue + if any direct source/glob/fetch leaf of S changed: continue ok = true ; child_nodes = [] for (dep, dep_cfg_id, dep_argv_id, recorded_tree) in S.deps: # may run in parallel r = resolve(dep, config_by_id(dep_cfg_id), argv_by_id(dep_argv_id), chain') # replay the need by id if r.output != recorded_tree: ok = false; break child_nodes.push(r.leafset) if ok: - node = union(direct{recipe_id, source/glob leaves} of S, child_nodes) # builds+stores the deepset + node = union(direct{recipe_id, source/glob/fetch leaves} of S, child_nodes) # builds+stores the deepset write_deep_trace(T, cfg.id, argv.id, node.id, S.output) # refresh the deep trace p = materialize(S.output) # pure; ERR => fall through if p ok: return done(S.output, p, node) @@ -526,7 +535,7 @@ tree), and removes the resolve→runner edge. This is the brief's flow, sharpened by config: - *Phase 1* is the deep fast path: identical config-id **and argv-id** and an - all-clear deepset refresh — every source/glob leaf unchanged **and** every + all-clear deepset refresh — every source/glob/fetch leaf unchanged **and** every node's `recipe-id` still recomputing the same — ⇒ reuse. `refresh` hits the per-process memos and the deep-set-id validity cache, so a shared subtree is checked once and an unchanged subtree is skipped by id equality. @@ -602,7 +611,7 @@ run_recipe(T, cfg, argv, chain): on success: output = kit_cas_add_tree_from_dir(out) put serialized cfg map (cfg.id) and argv vector (argv.id) into the CAS - node = union(direct{recipe_id, source/glob/absent leaves}, child deepset nodes) + node = union(direct{recipe_id, source/glob/fetch/absent leaves}, child deepset nodes) # builds + stores the root deepset as a CAS blob, yields its deep-set-id write_shallow_trace(T, recipe_id, argv.id, cfg.id, consumed key names, direct leaves, dep edges with their config-ids and argv-ids, output) @@ -678,6 +687,7 @@ command. | `config-get <key>` | the propagated value (or *unset*) | config dep: key recorded as consumed; its value is the one in the effective `config-id` map (absent = unset) | | `source <path>` | `blob-id` (or *absent*) + a path to read | source dep: `(path, blob-id)`, or `(path, absent)` | | `glob <pattern>` | sorted list of matching paths (streamed) | glob dep: `(pattern, glob-result-hash)` | +| `fetch <blob-id> <url>...` | local CAS path to the verified blob | fetch dep: `blob-id` | | `need <target> [k=v…] [env…] [argv…]` | the dep's output `tree-id` + a path (blocks) | target dep edge: `(dep, dep-config-id, dep-argv-id, output-tree-id)` | | `need-submit <target> [k=v…] [env…] [argv…]` | a **token** (does not block) | *(nothing yet — logged on await)* | | `need-await <token>` | the submitted dep's output `tree-id` + a path | target dep edge: `(dep, dep-config-id, dep-argv-id, output-tree-id)` | @@ -704,6 +714,11 @@ command. recipe then reads the returned paths without further declaration. Matches are **streamed** across as many frames as needed, so an arbitrarily large match set is not bounded by a single frame. +- **`fetch`** retrieves an externally stored blob by expected content id, trying + URL hints in order and installing only bytes that hash to that id. The logged + dependency is the blob id itself. URLs are not cache identity: changing mirrors + or URL order does not make a different build as long as the same blob id is + requested. - **`need`** is the dynamic-dependency primitive. The optional `k=v` pairs **overlay** propagated config for that sub-build; `--env NAME` / `--env NAME=value` are accepted as env-prefixed config overlays; an optional @@ -791,8 +806,9 @@ machinery wholesale: trace bodies and the CAS blobs they reference — the **serialized config-map and argv blobs** (required, else an imported shallow trace cannot replay its `need`s) and the **deepset closure blobs** a deep trace points at (required, - else an imported deep trace cannot refresh) — plus, optionally, the referenced - output trees/blobs. + else an imported deep trace cannot refresh), including any **fetched blob** + leaves in shallow traces or deepsets — plus, optionally, the referenced output + trees/blobs. - Trust is the DISTRIBUTE.md model unchanged: verify the minisign signature against the trusted-keys file (`-p KEY`, the anchor file, or `--tofu`); the signed trusted comment binds the signature to the manifest hash. diff --git a/include/kit/build_coord.h b/include/kit/build_coord.h @@ -431,10 +431,10 @@ KIT_API KitStatus kit_build_client_source(KitBuildClient*, KitSlice path, uint8_t blob[KIT_BUILD_HASH_LEN], KitSlice* realpath); -/* Fetch a hash-pinned blob into the coordinator CAS, trying URL hints in order. - * The URLs are transport hints only and are not cache identity; the expected - * blob id must come from tracked recipe/config/source data. On success *path is - * the local CAS blob path, borrowed until the next client call. */ +/* Fetch a hash-pinned blob into the coordinator CAS, trying URL hints in order + * and logging the blob id as a dependency. The URLs are transport hints only + * and are not cache identity. On success *path is the local CAS blob path, + * borrowed until the next client call. */ KIT_API KitStatus kit_build_client_fetch( KitBuildClient*, const uint8_t blob[KIT_BUILD_HASH_LEN], const KitSlice* urls, size_t nurls, KitSlice* path); diff --git a/src/build/bundle.c b/src/build/bundle.c @@ -354,23 +354,28 @@ static int BUILD_MAYBE_UNUSED parse_trace_header(const KitContext* ctx, _Alignof(BuildSourceLeaf)); t.globs = (BuildGlobLeaf*)heap_array(ctx, rows, sizeof *t.globs, _Alignof(BuildGlobLeaf)); + t.fetches = (BuildFetchLeaf*)heap_array(ctx, rows, sizeof *t.fetches, + _Alignof(BuildFetchLeaf)); t.deps = (BuildDepEdge*)heap_array(ctx, rows, sizeof *t.deps, _Alignof(BuildDepEdge)); - if (!t.config_keys || !t.sources || !t.globs || !t.deps) { + if (!t.config_keys || !t.sources || !t.globs || !t.fetches || !t.deps) { heap_free_array(ctx, t.config_keys, rows, sizeof *t.config_keys); heap_free_array(ctx, t.sources, rows, sizeof *t.sources); heap_free_array(ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(ctx, t.fetches, rows, sizeof *t.fetches); heap_free_array(ctx, t.deps, rows, sizeof *t.deps); return BUILD_ERR; } t.cap_config_keys = rows; t.cap_sources = rows; t.cap_globs = rows; + t.cap_fetches = rows; t.cap_deps = rows; if (build_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) { heap_free_array(ctx, t.config_keys, rows, sizeof *t.config_keys); heap_free_array(ctx, t.sources, rows, sizeof *t.sources); heap_free_array(ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(ctx, t.fetches, rows, sizeof *t.fetches); heap_free_array(ctx, t.deps, rows, sizeof *t.deps); return BUILD_ERR; } @@ -380,6 +385,7 @@ static int BUILD_MAYBE_UNUSED parse_trace_header(const KitContext* ctx, heap_free_array(ctx, t.config_keys, rows, sizeof *t.config_keys); heap_free_array(ctx, t.sources, rows, sizeof *t.sources); heap_free_array(ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(ctx, t.fetches, rows, sizeof *t.fetches); heap_free_array(ctx, t.deps, rows, sizeof *t.deps); return BUILD_OK; } @@ -395,23 +401,28 @@ static int BUILD_MAYBE_UNUSED parse_trace_header(const KitContext* ctx, _Alignof(BuildSourceLeaf)); t.globs = (BuildGlobLeaf*)heap_array(ctx, rows, sizeof *t.globs, _Alignof(BuildGlobLeaf)); + t.fetches = (BuildFetchLeaf*)heap_array(ctx, rows, sizeof *t.fetches, + _Alignof(BuildFetchLeaf)); t.deps = (BuildDepEdge*)heap_array(ctx, rows, sizeof *t.deps, _Alignof(BuildDepEdge)); - if (!t.config_keys || !t.sources || !t.globs || !t.deps) { + if (!t.config_keys || !t.sources || !t.globs || !t.fetches || !t.deps) { heap_free_array(ctx, t.config_keys, rows, sizeof *t.config_keys); heap_free_array(ctx, t.sources, rows, sizeof *t.sources); heap_free_array(ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(ctx, t.fetches, rows, sizeof *t.fetches); heap_free_array(ctx, t.deps, rows, sizeof *t.deps); return BUILD_ERR; } t.cap_config_keys = rows; t.cap_sources = rows; t.cap_globs = rows; + t.cap_fetches = rows; t.cap_deps = rows; if (build_test_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) { heap_free_array(ctx, t.config_keys, rows, sizeof *t.config_keys); heap_free_array(ctx, t.sources, rows, sizeof *t.sources); heap_free_array(ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(ctx, t.fetches, rows, sizeof *t.fetches); heap_free_array(ctx, t.deps, rows, sizeof *t.deps); return BUILD_ERR; } @@ -421,6 +432,7 @@ static int BUILD_MAYBE_UNUSED parse_trace_header(const KitContext* ctx, heap_free_array(ctx, t.config_keys, rows, sizeof *t.config_keys); heap_free_array(ctx, t.sources, rows, sizeof *t.sources); heap_free_array(ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(ctx, t.fetches, rows, sizeof *t.fetches); heap_free_array(ctx, t.deps, rows, sizeof *t.deps); return BUILD_OK; } @@ -618,14 +630,21 @@ static int export_deepset_closure(KitBuildCoordinator* c, _Alignof(BuildSourceLeaf)); ds.globs = (BuildGlobLeaf*)heap_array(c->ctx, rows, sizeof *ds.globs, _Alignof(BuildGlobLeaf)); + ds.fetches = (BuildFetchLeaf*)heap_array(c->ctx, rows, sizeof *ds.fetches, + _Alignof(BuildFetchLeaf)); ds.children = (uint8_t(*)[BUILD_HASH_LEN])heap_array( c->ctx, rows, sizeof *ds.children, _Alignof(uint8_t)); - if (!ds.sources || !ds.globs || !ds.children) goto out; + if (!ds.sources || !ds.globs || !ds.fetches || !ds.children) goto out; ds.cap_sources = rows; ds.cap_globs = rows; + ds.cap_fetches = rows; ds.cap_children = rows; if (build_deepset_parse(fd.data, fd.size, &ds, err, sizeof err) != BUILD_OK) goto out; + for (i = 0u; i < ds.n_fetches; ++i) + if (payload_write_blob_from_cas(c, ds.fetches[i].blob, root, seen) != + BUILD_OK) + goto out; for (i = 0u; i < ds.n_children; ++i) if (export_deepset_closure(c, ds.children[i], root, seen) != BUILD_OK) goto out; @@ -633,6 +652,7 @@ static int export_deepset_closure(KitBuildCoordinator* c, out: heap_free_array(c->ctx, ds.sources, rows, sizeof *ds.sources); heap_free_array(c->ctx, ds.globs, rows, sizeof *ds.globs); + heap_free_array(c->ctx, ds.fetches, rows, sizeof *ds.fetches); heap_free_array(c->ctx, ds.children, rows, sizeof *ds.children); kit_cas_release(c->cas, &fd); return ok; @@ -658,14 +678,25 @@ static int import_deepset_closure(KitBuildCoordinator* c, _Alignof(BuildSourceLeaf)); ds.globs = (BuildGlobLeaf*)heap_array(c->ctx, rows, sizeof *ds.globs, _Alignof(BuildGlobLeaf)); + ds.fetches = (BuildFetchLeaf*)heap_array(c->ctx, rows, sizeof *ds.fetches, + _Alignof(BuildFetchLeaf)); ds.children = (uint8_t(*)[BUILD_HASH_LEN])heap_array( c->ctx, rows, sizeof *ds.children, _Alignof(uint8_t)); - if (!ds.sources || !ds.globs || !ds.children) goto out; + if (!ds.sources || !ds.globs || !ds.fetches || !ds.children) goto out; ds.cap_sources = rows; ds.cap_globs = rows; + ds.cap_fetches = rows; ds.cap_children = rows; if (build_deepset_parse(data, len, &ds, err, sizeof err) != BUILD_OK) goto out; + for (i = 0u; i < ds.n_fetches; ++i) { + KitFileData dep_fd; + memset(&dep_fd, 0, sizeof dep_fd); + if (payload_install_blob(c, ds.fetches[i].blob, root, seen, NULL, NULL, + &dep_fd) != BUILD_OK) + goto out; + release_file(c, &dep_fd); + } for (i = 0u; i < ds.n_children; ++i) if (import_deepset_closure(c, ds.children[i], root, seen) != BUILD_OK) goto out; @@ -673,6 +704,7 @@ static int import_deepset_closure(KitBuildCoordinator* c, out: heap_free_array(c->ctx, ds.sources, rows, sizeof *ds.sources); heap_free_array(c->ctx, ds.globs, rows, sizeof *ds.globs); + heap_free_array(c->ctx, ds.fetches, rows, sizeof *ds.fetches); heap_free_array(c->ctx, ds.children, rows, sizeof *ds.children); release_file(c, &fd); return ok; @@ -721,12 +753,16 @@ static int BUILD_MAYBE_UNUSED export_trace_refs(KitBuildCoordinator* c, _Alignof(BuildSourceLeaf)); t.globs = (BuildGlobLeaf*)heap_array(c->ctx, rows, sizeof *t.globs, _Alignof(BuildGlobLeaf)); + t.fetches = (BuildFetchLeaf*)heap_array(c->ctx, rows, sizeof *t.fetches, + _Alignof(BuildFetchLeaf)); t.deps = (BuildDepEdge*)heap_array(c->ctx, rows, sizeof *t.deps, _Alignof(BuildDepEdge)); - if (!t.config_keys || !t.sources || !t.globs || !t.deps) goto shallow_out; + if (!t.config_keys || !t.sources || !t.globs || !t.fetches || !t.deps) + goto shallow_out; t.cap_config_keys = rows; t.cap_sources = rows; t.cap_globs = rows; + t.cap_fetches = rows; t.cap_deps = rows; if (build_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) goto shallow_out; @@ -742,11 +778,16 @@ static int BUILD_MAYBE_UNUSED export_trace_refs(KitBuildCoordinator* c, BUILD_OK) goto shallow_out; } + for (i = 0u; i < t.n_fetches; ++i) + if (payload_write_blob_from_cas(c, t.fetches[i].blob, root, seen) != + BUILD_OK) + goto shallow_out; ok = BUILD_OK; shallow_out: heap_free_array(c->ctx, t.config_keys, rows, sizeof *t.config_keys); heap_free_array(c->ctx, t.sources, rows, sizeof *t.sources); heap_free_array(c->ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(c->ctx, t.fetches, rows, sizeof *t.fetches); heap_free_array(c->ctx, t.deps, rows, sizeof *t.deps); return ok; } @@ -764,12 +805,16 @@ shallow_out: _Alignof(BuildSourceLeaf)); t.globs = (BuildGlobLeaf*)heap_array(c->ctx, rows, sizeof *t.globs, _Alignof(BuildGlobLeaf)); + t.fetches = (BuildFetchLeaf*)heap_array(c->ctx, rows, sizeof *t.fetches, + _Alignof(BuildFetchLeaf)); t.deps = (BuildDepEdge*)heap_array(c->ctx, rows, sizeof *t.deps, _Alignof(BuildDepEdge)); - if (!t.config_keys || !t.sources || !t.globs || !t.deps) goto test_out; + if (!t.config_keys || !t.sources || !t.globs || !t.fetches || !t.deps) + goto test_out; t.cap_config_keys = rows; t.cap_sources = rows; t.cap_globs = rows; + t.cap_fetches = rows; t.cap_deps = rows; if (build_test_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) goto test_out; @@ -785,11 +830,16 @@ shallow_out: BUILD_OK) goto test_out; } + for (i = 0u; i < t.n_fetches; ++i) + if (payload_write_blob_from_cas(c, t.fetches[i].blob, root, seen) != + BUILD_OK) + goto test_out; ok = BUILD_OK; test_out: heap_free_array(c->ctx, t.config_keys, rows, sizeof *t.config_keys); heap_free_array(c->ctx, t.sources, rows, sizeof *t.sources); heap_free_array(c->ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(c->ctx, t.fetches, rows, sizeof *t.fetches); heap_free_array(c->ctx, t.deps, rows, sizeof *t.deps); return ok; } @@ -849,12 +899,16 @@ static int BUILD_MAYBE_UNUSED import_trace_refs(KitBuildCoordinator* c, _Alignof(BuildSourceLeaf)); t.globs = (BuildGlobLeaf*)heap_array(c->ctx, rows, sizeof *t.globs, _Alignof(BuildGlobLeaf)); + t.fetches = (BuildFetchLeaf*)heap_array(c->ctx, rows, sizeof *t.fetches, + _Alignof(BuildFetchLeaf)); t.deps = (BuildDepEdge*)heap_array(c->ctx, rows, sizeof *t.deps, _Alignof(BuildDepEdge)); - if (!t.config_keys || !t.sources || !t.globs || !t.deps) goto shallow_out; + if (!t.config_keys || !t.sources || !t.globs || !t.fetches || !t.deps) + goto shallow_out; t.cap_config_keys = rows; t.cap_sources = rows; t.cap_globs = rows; + t.cap_fetches = rows; t.cap_deps = rows; if (build_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) goto shallow_out; @@ -880,12 +934,19 @@ static int BUILD_MAYBE_UNUSED import_trace_refs(KitBuildCoordinator* c, goto shallow_out; release_file(c, &fd); } + for (i = 0u; i < t.n_fetches; ++i) { + if (payload_install_blob(c, t.fetches[i].blob, root, seen, NULL, NULL, + &fd) != BUILD_OK) + goto shallow_out; + release_file(c, &fd); + } ok = BUILD_OK; shallow_out: release_file(c, &fd); heap_free_array(c->ctx, t.config_keys, rows, sizeof *t.config_keys); heap_free_array(c->ctx, t.sources, rows, sizeof *t.sources); heap_free_array(c->ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(c->ctx, t.fetches, rows, sizeof *t.fetches); heap_free_array(c->ctx, t.deps, rows, sizeof *t.deps); return ok; } @@ -905,12 +966,16 @@ shallow_out: _Alignof(BuildSourceLeaf)); t.globs = (BuildGlobLeaf*)heap_array(c->ctx, rows, sizeof *t.globs, _Alignof(BuildGlobLeaf)); + t.fetches = (BuildFetchLeaf*)heap_array(c->ctx, rows, sizeof *t.fetches, + _Alignof(BuildFetchLeaf)); t.deps = (BuildDepEdge*)heap_array(c->ctx, rows, sizeof *t.deps, _Alignof(BuildDepEdge)); - if (!t.config_keys || !t.sources || !t.globs || !t.deps) goto test_out; + if (!t.config_keys || !t.sources || !t.globs || !t.fetches || !t.deps) + goto test_out; t.cap_config_keys = rows; t.cap_sources = rows; t.cap_globs = rows; + t.cap_fetches = rows; t.cap_deps = rows; if (build_test_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) goto test_out; @@ -936,12 +1001,19 @@ shallow_out: goto test_out; release_file(c, &fd); } + for (i = 0u; i < t.n_fetches; ++i) { + if (payload_install_blob(c, t.fetches[i].blob, root, seen, NULL, NULL, + &fd) != BUILD_OK) + goto test_out; + release_file(c, &fd); + } ok = BUILD_OK; test_out: release_file(c, &fd); heap_free_array(c->ctx, t.config_keys, rows, sizeof *t.config_keys); heap_free_array(c->ctx, t.sources, rows, sizeof *t.sources); heap_free_array(c->ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(c->ctx, t.fetches, rows, sizeof *t.fetches); heap_free_array(c->ctx, t.deps, rows, sizeof *t.deps); return ok; } diff --git a/src/build/coord.c b/src/build/coord.c @@ -44,6 +44,7 @@ typedef struct BuildDeepSetMemo { BuildLeafSet leaf; BuildSourceLeaf* sources; BuildGlobLeaf* globs; + BuildFetchLeaf* fetches; const BuildLeafSet** children; int valid_known; int valid; @@ -255,6 +256,9 @@ static void free_deepsets(KitBuildCoordinator* c) { if (n->globs) c->ctx->heap->free(c->ctx->heap, n->globs, n->leaf.n_globs * sizeof *n->globs); + if (n->fetches) + c->ctx->heap->free(c->ctx->heap, n->fetches, + n->leaf.n_fetches * sizeof *n->fetches); if (n->children) c->ctx->heap->free(c->ctx->heap, n->children, n->leaf.n_children * sizeof *n->children); @@ -737,6 +741,13 @@ int build_coord_leafset_intern(KitBuildCoordinator* c, const BuildLeafSet* in, if (!n->globs) goto oom; memcpy(n->globs, in->globs, in->n_globs * sizeof *n->globs); } + if (in->n_fetches) { + n->fetches = (BuildFetchLeaf*)c->ctx->heap->alloc( + c->ctx->heap, in->n_fetches * sizeof *n->fetches, + _Alignof(BuildFetchLeaf)); + if (!n->fetches) goto oom; + memcpy(n->fetches, in->fetches, in->n_fetches * sizeof *n->fetches); + } if (in->n_children) { n->children = (const BuildLeafSet**)c->ctx->heap->alloc( c->ctx->heap, in->n_children * sizeof *n->children, @@ -746,6 +757,7 @@ int build_coord_leafset_intern(KitBuildCoordinator* c, const BuildLeafSet* in, } n->leaf.sources = n->sources; n->leaf.globs = n->globs; + n->leaf.fetches = n->fetches; n->leaf.children = n->children; n->next = c->deepsets; c->deepsets = n; @@ -759,6 +771,9 @@ oom: if (n->globs) c->ctx->heap->free(c->ctx->heap, n->globs, in->n_globs * sizeof *n->globs); + if (n->fetches) + c->ctx->heap->free(c->ctx->heap, n->fetches, + in->n_fetches * sizeof *n->fetches); if (n->children) c->ctx->heap->free(c->ctx->heap, n->children, in->n_children * sizeof *n->children); @@ -767,11 +782,12 @@ oom: } static void count_deepset_rows(const uint8_t* data, size_t len, size_t* ns, - size_t* ng, size_t* nc) { + size_t* ng, size_t* nf, size_t* nc) { size_t pos = 0; int sec = 0; *ns = 0; *ng = 0; + *nf = 0; *nc = 0; while (pos < len) { size_t start = pos; @@ -788,10 +804,14 @@ static void count_deepset_rows(const uint8_t* data, size_t len, size_t* ns, } else if (n == sizeof("[child]") - 1u && memcmp(data + start, "[child]", n) == 0) { sec = 3; + } else if (n == sizeof("[fetch]") - 1u && + memcmp(data + start, "[fetch]", n) == 0) { + sec = 4; } else if (data[start] != '[' && data[start] != 'k') { if (sec == 1) ++*ns; if (sec == 2) ++*ng; if (sec == 3) ++*nc; + if (sec == 4) ++*nf; } } ++pos; @@ -807,9 +827,10 @@ int build_coord_deepset_load(KitBuildCoordinator* c, BuildLeafSet leaf; BuildSourceLeaf* sources = NULL; BuildGlobLeaf* globs = NULL; + BuildFetchLeaf* fetches = NULL; uint8_t(*child_ids)[BUILD_HASH_LEN] = NULL; const BuildLeafSet** children = NULL; - size_t ns, ng, nc, i; + size_t ns, ng, nf, nc, i; int ok = BUILD_ERR; if (!c || !deepset_id || !out) return BUILD_ERR; for (memo = c->deepsets; memo; memo = memo->next) { @@ -822,7 +843,7 @@ int build_coord_deepset_load(KitBuildCoordinator* c, fd.size = 0; fd.token = NULL; if (kit_cas_get_blob(c->cas, deepset_id, &fd) != KIT_OK) return BUILD_ERR; - count_deepset_rows(fd.data, fd.size, &ns, &ng, &nc); + count_deepset_rows(fd.data, fd.size, &ns, &ng, &nf, &nc); if (ns) { sources = (BuildSourceLeaf*)c->ctx->heap->alloc( c->ctx->heap, ns * sizeof *sources, _Alignof(BuildSourceLeaf)); @@ -833,6 +854,11 @@ int build_coord_deepset_load(KitBuildCoordinator* c, c->ctx->heap, ng * sizeof *globs, _Alignof(BuildGlobLeaf)); if (!globs) goto out_release; } + if (nf) { + fetches = (BuildFetchLeaf*)c->ctx->heap->alloc( + c->ctx->heap, nf * sizeof *fetches, _Alignof(BuildFetchLeaf)); + if (!fetches) goto out_release; + } if (nc) { child_ids = (uint8_t(*)[BUILD_HASH_LEN])c->ctx->heap->alloc( c->ctx->heap, nc * sizeof *child_ids, _Alignof(uint8_t)); @@ -845,6 +871,8 @@ int build_coord_deepset_load(KitBuildCoordinator* c, ds.cap_sources = ns; ds.globs = globs; ds.cap_globs = ng; + ds.fetches = fetches; + ds.cap_fetches = nf; ds.children = child_ids; ds.cap_children = nc; if (build_deepset_parse(fd.data, fd.size, &ds, NULL, 0) != BUILD_OK) @@ -861,6 +889,8 @@ int build_coord_deepset_load(KitBuildCoordinator* c, leaf.n_sources = ds.n_sources; leaf.globs = globs; leaf.n_globs = ds.n_globs; + leaf.fetches = fetches; + leaf.n_fetches = ds.n_fetches; leaf.children = children; leaf.n_children = ds.n_children; if (build_coord_leafset_intern(c, &leaf, out) != BUILD_OK) goto out_release; @@ -871,6 +901,8 @@ out_release: if (sources) c->ctx->heap->free(c->ctx->heap, sources, ns * sizeof *sources); if (globs) c->ctx->heap->free(c->ctx->heap, globs, ng * sizeof *globs); + if (fetches) + c->ctx->heap->free(c->ctx->heap, fetches, nf * sizeof *fetches); if (child_ids) c->ctx->heap->free(c->ctx->heap, child_ids, nc * sizeof *child_ids); if (children) diff --git a/src/build/coord.h b/src/build/coord.h @@ -31,7 +31,7 @@ /* The in-memory deepset node: one node of the transitive input-closure DAG that * a resolution returns and the targets memo caches (trace.h BuildDeepSet is its - * on-disk/CAS-blob form). It carries this node's DIRECT source/glob leaves, its + * on-disk/CAS-blob form). It carries this node's DIRECT source/glob/fetch leaves, its * target + recipe-id, and pointers to its children's (interned) nodes. `id` is * the deep-set-id — the CAS blob id of the emitted node — so an unchanged * subtree is recognized by id equality without descending. Folded into a parent @@ -46,6 +46,8 @@ typedef struct BuildLeafSet { size_t n_sources; BuildGlobLeaf* globs; size_t n_globs; + BuildFetchLeaf* fetches; + size_t n_fetches; const struct BuildLeafSet** children; /* direct deps' interned nodes */ size_t n_children; } BuildLeafSet; diff --git a/src/build/resolve.c b/src/build/resolve.c @@ -86,6 +86,9 @@ static int shallow_direct_match(KitBuildCoordinator* c, return 0; if (!build_id_eq(hash, st->globs[i].result_hash)) return 0; } + for (i = 0; i < st->n_fetches; ++i) { + if (kit_cas_has_blob(c->cas, st->fetches[i].blob) != KIT_OK) return 0; + } return 1; } @@ -139,6 +142,8 @@ static int try_shallow_trace(KitBuildCoordinator* c, KitSlice target, log.n_sources = st->n_sources; log.globs = st->globs; log.n_globs = st->n_globs; + log.fetches = st->fetches; + log.n_fetches = st->n_fetches; log.deps = deps_copy; log.n_deps = st->n_deps; log.child_leafsets = child_leafsets; @@ -200,6 +205,8 @@ static int try_test_shallow_trace(KitBuildCoordinator* c, KitSlice target, log.n_sources = st->n_sources; log.globs = st->globs; log.n_globs = st->n_globs; + log.fetches = st->fetches; + log.n_fetches = st->n_fetches; log.deps = deps_copy; log.n_deps = st->n_deps; log.child_leafsets = child_leafsets; @@ -432,6 +439,7 @@ scan_record: BuildConfigKey keys[128]; BuildSourceLeaf sources[128]; BuildGlobLeaf globs[128]; + BuildFetchLeaf fetches[128]; BuildDepEdge deps[128]; BuildShallowTrace st; size_t rows; @@ -450,6 +458,8 @@ scan_record: st.cap_sources = sizeof sources / sizeof sources[0]; st.globs = globs; st.cap_globs = sizeof globs / sizeof globs[0]; + st.fetches = fetches; + st.cap_fetches = sizeof fetches / sizeof fetches[0]; st.deps = deps; st.cap_deps = sizeof deps / sizeof deps[0]; if (rows <= sizeof keys / sizeof keys[0] && @@ -552,6 +562,7 @@ scan_record: BuildConfigKey keys[128]; BuildSourceLeaf sources[128]; BuildGlobLeaf globs[128]; + BuildFetchLeaf fetches[128]; BuildDepEdge deps[128]; BuildShallowTrace st; size_t nrows; @@ -570,6 +581,8 @@ scan_record: st.cap_sources = sizeof sources / sizeof sources[0]; st.globs = globs; st.cap_globs = sizeof globs / sizeof globs[0]; + st.fetches = fetches; + st.cap_fetches = sizeof fetches / sizeof fetches[0]; st.deps = deps; st.cap_deps = sizeof deps / sizeof deps[0]; if (nrows <= sizeof keys / sizeof keys[0] && @@ -661,6 +674,8 @@ int build_leafset_union(KitBuildCoordinator* c, const BuildLeafSet* direct, ds.n_sources = direct->n_sources; ds.globs = direct->globs; ds.n_globs = direct->n_globs; + ds.fetches = direct->fetches; + ds.n_fetches = direct->n_fetches; ds.children = child_ids; ds.n_children = nchildren; if (kit_writer_mem(c->ctx->heap, &w) != KIT_OK || !w) goto out; @@ -680,6 +695,8 @@ int build_leafset_union(KitBuildCoordinator* c, const BuildLeafSet* direct, leaf.n_sources = direct->n_sources; leaf.globs = direct->globs; leaf.n_globs = direct->n_globs; + leaf.fetches = direct->fetches; + leaf.n_fetches = direct->n_fetches; leaf.children = child_ptrs; leaf.n_children = nchildren; if (build_coord_leafset_intern(c, &leaf, out) != BUILD_OK) goto out; @@ -733,6 +750,10 @@ int build_leafset_refresh(KitBuildCoordinator* c, const BuildLeafSet* leafset, return BUILD_ERR; if (!build_id_eq(hash, leafset->globs[i].result_hash)) goto done; } + for (i = 0; i < leafset->n_fetches; ++i) { + if (kit_cas_has_blob(c->cas, leafset->fetches[i].blob) != KIT_OK) + goto done; + } for (i = 0; i < leafset->n_children; ++i) { int child_match = 0; if (build_leafset_refresh(c, leafset->children[i], &child_match) != diff --git a/src/build/resolve.h b/src/build/resolve.h @@ -15,7 +15,7 @@ * §Resolution algorithm): * * Phase 1 deep fast path - same config-id AND argv-id, and an all-clear - * refresh of the deepset closure (every source/glob + * refresh of the deepset closure (every source/glob/fetch * leaf unchanged AND every node's recipe-id still * recomputes the same) => reuse, no graph walk. * Phase 2 shallow path - config, argv, and/or sources moved: guard on the @@ -133,7 +133,7 @@ int build_materialize(KitBuildCoordinator*, size_t cap); /* Build a parent's deepset node from its DIRECT leaves and its children's nodes. - * `direct` carries this node's target, recipe-id, and direct source/glob leaves + * `direct` carries this node's target, recipe-id, and direct source/glob/fetch leaves * (its children fields empty); `children` are the deps' resolved nodes. Computes * the deep-set-id, stores the canonical node as a CAS blob (kit_cas_add_blob — * the children's blobs were stored when they were built, so this is incremental @@ -144,7 +144,8 @@ int build_leafset_union(KitBuildCoordinator*, const BuildLeafSet* direct, const BuildLeafSet** out); /* Refresh a deepset node against the live workspace: per node, rehash its direct - * source leaves (absent stays absent), reglob its glob leaves, recompute its + * source leaves (absent stays absent), reglob its glob leaves, check its fetched + * blobs are still present in the CAS, recompute its * recipe-id through the LIVE definition (catching a dep repointed to a * different-content recipe, treating same-content as unchanged), and recurse * into children — all through the per-process memos and the deep-set-id validity diff --git a/src/build/runner.c b/src/build/runner.c @@ -96,6 +96,21 @@ static int append_glob(BuildDepLog* log, KitSlice pattern, return BUILD_OK; } +static int append_fetch(BuildDepLog* log, + const uint8_t blob[BUILD_HASH_LEN]) { + BuildFetchLeaf* row; + size_t i; + if (!log || !blob) return BUILD_ERR; + for (i = 0; i < log->n_fetches; ++i) { + row = &log->fetches[i]; + if (build_id_eq(row->blob, blob)) return BUILD_OK; + } + if (log->n_fetches >= log->cap_fetches) return BUILD_ERR; + row = &log->fetches[log->n_fetches++]; + memcpy(row->blob, blob, BUILD_HASH_LEN); + return BUILD_OK; +} + static int append_dep(BuildDepLog* log, KitSlice target, const uint8_t overlay_id[BUILD_HASH_LEN], const uint8_t config_id[BUILD_HASH_LEN], @@ -335,6 +350,7 @@ static int build_run_recipe_impl(KitBuildCoordinator* c, KitSlice target, BuildConfigKey config_keys[128]; BuildSourceLeaf sources[128]; BuildGlobLeaf globs[128]; + BuildFetchLeaf fetches[128]; BuildDepEdge deps[128]; const BuildLeafSet* child_leafsets[128]; BuildPendingNeed pending[128]; @@ -369,6 +385,8 @@ static int build_run_recipe_impl(KitBuildCoordinator* c, KitSlice target, log.cap_sources = sizeof sources / sizeof sources[0]; log.globs = globs; log.cap_globs = sizeof globs / sizeof globs[0]; + log.fetches = fetches; + log.cap_fetches = sizeof fetches / sizeof fetches[0]; log.deps = deps; log.cap_deps = sizeof deps / sizeof deps[0]; log.child_leafsets = child_leafsets; @@ -462,6 +480,7 @@ static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target, BuildConfigKey config_keys[128]; BuildSourceLeaf sources[128]; BuildGlobLeaf globs[128]; + BuildFetchLeaf fetches[128]; BuildDepEdge deps[128]; const BuildLeafSet* child_leafsets[128]; BuildPendingNeed pending[128]; @@ -504,6 +523,8 @@ static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target, log.cap_sources = sizeof sources / sizeof sources[0]; log.globs = globs; log.cap_globs = sizeof globs / sizeof globs[0]; + log.fetches = fetches; + log.cap_fetches = sizeof fetches / sizeof fetches[0]; log.deps = deps; log.cap_deps = sizeof deps / sizeof deps[0]; log.child_leafsets = child_leafsets; @@ -661,7 +682,8 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn, char path[BUILD_PATH_MAX]; if (kit_hex_decode(blob, req.arg.s, BUILD_HASH_LEN) != KIT_OK || build_coord_fetch_blob(c, blob, req.argv, req.argc, path, - sizeof path) != BUILD_OK) { + sizeof path) != BUILD_OK || + append_fetch(log, blob) != BUILD_OK) { resp_error(&resp, KIT_ERR, "fetch failed"); } else { memcpy(resp.id, blob, BUILD_HASH_LEN); @@ -810,6 +832,8 @@ int build_runner_record_traces(KitBuildCoordinator* c, KitSlice target, direct.n_sources = log->n_sources; direct.globs = log->globs; direct.n_globs = log->n_globs; + direct.fetches = log->fetches; + direct.n_fetches = log->n_fetches; direct.children = log->child_leafsets; direct.n_children = log->n_children; if (build_leafset_union(c, &direct, log->child_leafsets, log->n_children, @@ -829,6 +853,8 @@ int build_runner_record_traces(KitBuildCoordinator* c, KitSlice target, shallow.n_sources = log->n_sources; shallow.globs = log->globs; shallow.n_globs = log->n_globs; + shallow.fetches = log->fetches; + shallow.n_fetches = log->n_fetches; shallow.deps = log->deps; shallow.n_deps = log->n_deps; @@ -896,6 +922,8 @@ int build_runner_record_test_traces(KitBuildCoordinator* c, KitSlice target, direct.n_sources = log->n_sources; direct.globs = log->globs; direct.n_globs = log->n_globs; + direct.fetches = log->fetches; + direct.n_fetches = log->n_fetches; direct.children = log->child_leafsets; direct.n_children = log->n_children; if (build_leafset_union(c, &direct, log->child_leafsets, log->n_children, @@ -915,6 +943,8 @@ int build_runner_record_test_traces(KitBuildCoordinator* c, KitSlice target, shallow.n_sources = log->n_sources; shallow.globs = log->globs; shallow.n_globs = log->n_globs; + shallow.fetches = log->fetches; + shallow.n_fetches = log->n_fetches; shallow.deps = log->deps; shallow.n_deps = log->n_deps; diff --git a/src/build/runner.h b/src/build/runner.h @@ -52,6 +52,8 @@ typedef struct BuildDepLog { size_t n_sources, cap_sources; BuildGlobLeaf* globs; /* direct globs */ size_t n_globs, cap_globs; + BuildFetchLeaf* fetches; /* direct hash-pinned fetches */ + size_t n_fetches, cap_fetches; BuildDepEdge* deps; /* AWAITED `need` edges (recorded) */ size_t n_deps, cap_deps; const BuildLeafSet** child_leafsets; /* one deepset node per awaited dep */ @@ -81,7 +83,7 @@ typedef struct BuildDepLog { * dispatch them, appending each dependency to `log`. Drives the protocol.h * codec over the transport. `chain` is this recipe's chain frame (its dispatched * needs extend it). Commands: - * config-get/source/glob — synchronous; log the dep, reply. + * config-get/source/glob/fetch — synchronous; log the dep, reply. * need (fused) — build_resolve inline on this thread; log the edge. * need-submit — build_coord_dispatch(dep) (concurrent, no block), * stash a BuildPendingNeed, reply a token. Cycles and diff --git a/src/build/trace.c b/src/build/trace.c @@ -109,6 +109,10 @@ static int glob_cmp(const BuildGlobLeaf* a, const BuildGlobLeaf* b) { return strcmp(a->pattern, b->pattern); } +static int fetch_cmp(const BuildFetchLeaf* a, const BuildFetchLeaf* b) { + return id_cmp(a->blob, b->blob); +} + static int dep_cmp(const BuildDepEdge* a, const BuildDepEdge* b) { int c = strcmp(a->name, b->name); if (c != 0) return c; @@ -225,6 +229,35 @@ static int emit_sorted_globs(const BuildGlobLeaf* rows, size_t n, KitWriter* out return BUILD_OK; } +static int emit_fetch_row(KitWriter* out, const BuildFetchLeaf* row) { + char hex[BUILD_HEX_LEN]; + hex_encode(hex, row->blob); + if (emit_bytes(out, hex) != BUILD_OK) return BUILD_ERR; + return emit_bytes(out, "\n"); +} + +static int emit_sorted_fetches(const BuildFetchLeaf* rows, size_t n, + KitWriter* out, char* err, size_t errcap) { + const BuildFetchLeaf* prev = NULL; + size_t emitted = 0; + if (n && !rows) return set_err(err, errcap, "missing fetch rows"); + if (emit_section(out, "fetch") != BUILD_OK) return BUILD_ERR; + while (emitted < n) { + const BuildFetchLeaf* best = NULL; + size_t i; + for (i = 0; i < n; ++i) { + const BuildFetchLeaf* cur = &rows[i]; + int after_prev = !prev || fetch_cmp(prev, cur) < 0; + if (after_prev && (!best || fetch_cmp(cur, best) < 0)) best = cur; + } + if (!best) return set_err(err, errcap, "duplicate fetch row"); + if (emit_fetch_row(out, best) != BUILD_OK) return BUILD_ERR; + prev = best; + ++emitted; + } + return BUILD_OK; +} + static int emit_dep_row(KitWriter* out, const BuildDepEdge* row) { char ovr[BUILD_HEX_LEN], cfg[BUILD_HEX_LEN], argv[BUILD_HEX_LEN], tree[BUILD_HEX_LEN]; @@ -367,6 +400,9 @@ static int shallow_emit_like(const BuildShallowTrace* t, KitWriter* out, return BUILD_ERR; if (emit_sorted_globs(t->globs, t->n_globs, out, err, errcap) != BUILD_OK) return BUILD_ERR; + if (emit_sorted_fetches(t->fetches, t->n_fetches, out, err, errcap) != + BUILD_OK) + return BUILD_ERR; if (emit_sorted_deps(t->deps, t->n_deps, out, err, errcap) != BUILD_OK) return BUILD_ERR; return kit_writer_status(out) == KIT_OK ? BUILD_OK : BUILD_ERR; @@ -422,6 +458,9 @@ int build_deepset_emit(const BuildDeepSet* t, KitWriter* out, char* err, return BUILD_ERR; if (emit_sorted_globs(t->globs, t->n_globs, out, err, errcap) != BUILD_OK) return BUILD_ERR; + if (emit_sorted_fetches(t->fetches, t->n_fetches, out, err, errcap) != + BUILD_OK) + return BUILD_ERR; if (emit_sorted_children(t->children, t->n_children, out, err, errcap) != BUILD_OK) return BUILD_ERR; @@ -444,6 +483,7 @@ typedef enum TraceSection { TRACE_SEC_CONFIG, TRACE_SEC_SOURCE, TRACE_SEC_GLOB, + TRACE_SEC_FETCH, TRACE_SEC_DEP, TRACE_SEC_CHILD, TRACE_SEC_RECORD_ROWS @@ -630,6 +670,42 @@ static int append_deepset_glob(BuildDeepSet* out, char** fields, size_t n, return BUILD_OK; } +static int parse_fetch_leaf(char** fields, size_t n, BuildFetchLeaf* row, + char* err, size_t errcap) { + if (expect_field_count(n, 1u, err, errcap) != BUILD_OK) return BUILD_ERR; + if (hex_decode_strict(fields[0], row->blob) != BUILD_OK) + return set_err(err, errcap, "bad fetch hash"); + return BUILD_OK; +} + +static int append_shallow_fetch(BuildShallowTrace* out, char** fields, size_t n, + char* err, size_t errcap) { + BuildFetchLeaf* row; + if (out->n_fetches >= out->cap_fetches) + return set_err(err, errcap, "too many fetch rows"); + row = &out->fetches[out->n_fetches]; + if (parse_fetch_leaf(fields, n, row, err, errcap) != BUILD_OK) + return BUILD_ERR; + if (out->n_fetches && fetch_cmp(&out->fetches[out->n_fetches - 1u], row) >= 0) + return set_err(err, errcap, "non-canonical fetch rows"); + ++out->n_fetches; + return BUILD_OK; +} + +static int append_deepset_fetch(BuildDeepSet* out, char** fields, size_t n, + char* err, size_t errcap) { + BuildFetchLeaf* row; + if (out->n_fetches >= out->cap_fetches) + return set_err(err, errcap, "too many fetch rows"); + row = &out->fetches[out->n_fetches]; + if (parse_fetch_leaf(fields, n, row, err, errcap) != BUILD_OK) + return BUILD_ERR; + if (out->n_fetches && fetch_cmp(&out->fetches[out->n_fetches - 1u], row) >= 0) + return set_err(err, errcap, "non-canonical fetch rows"); + ++out->n_fetches; + return BUILD_OK; +} + static int append_dep(BuildShallowTrace* out, char** fields, size_t n, char* err, size_t errcap) { BuildDepEdge* row; @@ -708,12 +784,14 @@ static int shallow_parse_like(const uint8_t* data, size_t len, if (!data || !out) return set_err(err, errcap, "missing shallow trace"); if ((out->cap_config_keys && !out->config_keys) || (out->cap_sources && !out->sources) || (out->cap_globs && !out->globs) || + (out->cap_fetches && !out->fetches) || (out->cap_deps && !out->deps)) return set_err(err, errcap, "missing trace storage"); memset(out->target, 0, sizeof out->target); out->n_config_keys = 0; out->n_sources = 0; out->n_globs = 0; + out->n_fetches = 0; out->n_deps = 0; memset(&p, 0, sizeof p); p.data = data; @@ -741,7 +819,10 @@ static int shallow_parse_like(const uint8_t* data, size_t len, sec = TRACE_SEC_SOURCE; } else if (strcmp(line, "[glob]") == 0 && sec == TRACE_SEC_SOURCE) { sec = TRACE_SEC_GLOB; - } else if (strcmp(line, "[dep]") == 0 && sec == TRACE_SEC_GLOB) { + } else if (strcmp(line, "[fetch]") == 0 && sec == TRACE_SEC_GLOB) { + sec = TRACE_SEC_FETCH; + } else if (strcmp(line, "[dep]") == 0 && + (sec == TRACE_SEC_GLOB || sec == TRACE_SEC_FETCH)) { sec = TRACE_SEC_DEP; } else { return set_err(err, errcap, "bad shallow section"); @@ -785,6 +866,9 @@ static int shallow_parse_like(const uint8_t* data, size_t len, } else if (sec == TRACE_SEC_GLOB) { if (append_shallow_glob(out, fields, n, err, errcap) != BUILD_OK) return BUILD_ERR; + } else if (sec == TRACE_SEC_FETCH) { + if (append_shallow_fetch(out, fields, n, err, errcap) != BUILD_OK) + return BUILD_ERR; } else if (sec == TRACE_SEC_DEP) { if (append_dep(out, fields, n, err, errcap) != BUILD_OK) return BUILD_ERR; } else { @@ -890,11 +974,13 @@ int build_deepset_parse(const uint8_t* data, size_t len, BuildDeepSet* out, uint32_t seen = 0; if (!data || !out) return set_err(err, errcap, "missing deepset"); if ((out->cap_sources && !out->sources) || (out->cap_globs && !out->globs) || + (out->cap_fetches && !out->fetches) || (out->cap_children && !out->children)) return set_err(err, errcap, "missing deepset storage"); memset(out->target, 0, sizeof out->target); out->n_sources = 0; out->n_globs = 0; + out->n_fetches = 0; out->n_children = 0; memset(&p, 0, sizeof p); p.data = data; @@ -920,7 +1006,10 @@ int build_deepset_parse(const uint8_t* data, size_t len, BuildDeepSet* out, sec = TRACE_SEC_SOURCE; } else if (strcmp(line, "[glob]") == 0 && sec == TRACE_SEC_SOURCE) { sec = TRACE_SEC_GLOB; - } else if (strcmp(line, "[child]") == 0 && sec == TRACE_SEC_GLOB) { + } else if (strcmp(line, "[fetch]") == 0 && sec == TRACE_SEC_GLOB) { + sec = TRACE_SEC_FETCH; + } else if (strcmp(line, "[child]") == 0 && + (sec == TRACE_SEC_GLOB || sec == TRACE_SEC_FETCH)) { sec = TRACE_SEC_CHILD; } else { return set_err(err, errcap, "bad deepset section"); @@ -948,6 +1037,9 @@ int build_deepset_parse(const uint8_t* data, size_t len, BuildDeepSet* out, } else if (sec == TRACE_SEC_GLOB) { if (append_deepset_glob(out, fields, n, err, errcap) != BUILD_OK) return BUILD_ERR; + } else if (sec == TRACE_SEC_FETCH) { + if (append_deepset_fetch(out, fields, n, err, errcap) != BUILD_OK) + return BUILD_ERR; } else if (sec == TRACE_SEC_CHILD) { if (append_child(out, fields, n, err, errcap) != BUILD_OK) return BUILD_ERR; diff --git a/src/build/trace.h b/src/build/trace.h @@ -42,6 +42,13 @@ typedef struct BuildGlobLeaf { uint8_t result_hash[BUILD_HASH_LEN]; } BuildGlobLeaf; +/* One fetched blob leaf: a hash-pinned external object requested through + * `fetch`. URLs are transport hints only and are not cache identity; the blob id + * is the dependency. Sorted by blob id. */ +typedef struct BuildFetchLeaf { + uint8_t blob[BUILD_HASH_LEN]; +} BuildFetchLeaf; + /* One target-dep edge (shallow only): replays one `need`. Carries the explicit * need overlay-id, the dep's effective config-id and argv-id, and the output * tree-id it produced. Shallow replay recomputes the dep config as the current @@ -82,6 +89,9 @@ typedef struct BuildShallowTrace { BuildGlobLeaf* globs; /* [glob] section */ size_t n_globs; size_t cap_globs; + BuildFetchLeaf* fetches; /* [fetch] section */ + size_t n_fetches; + size_t cap_fetches; BuildDepEdge* deps; /* [dep] section */ size_t n_deps; size_t cap_deps; @@ -91,8 +101,8 @@ typedef struct BuildShallowTrace { * pointer to the TRANSITIVE input closure, held structurally as a deepset DAG * (below) rather than inlined. If the request's config-id equals `root_config`, * its argv-id equals `argv`, and refreshing the `deepset` closure finds nothing - * moved (every source/glob leaf still matches AND every node's recipe-id still - * recomputes the same), determinism makes the output identical — no recipe runs. + * moved (every source/glob/fetch leaf still matches AND every node's recipe-id + * still recomputes the same), determinism makes the output identical — no recipe runs. * The deepset blob must be present to refresh; if absent (GC'd), the trace is * treated as absent and resolution falls through (fail-safe). */ typedef struct BuildDeepTrace { @@ -106,7 +116,7 @@ typedef struct BuildDeepTrace { /* One node of the transitive input-closure DAG — the structural form of the old * flat leafset ("compact deep traces"). One node per target build: its DIRECT - * source/glob leaves, its recipe-id, and the deep-set-ids of its DIRECT deps. + * source/glob/fetch leaves, its recipe-id, and the deep-set-ids of its DIRECT deps. * The fully-flattened closure is the recursive union, NEVER materialized: a * subtree reached through many parents is one node, stored once — a Merkle DAG * that structurally shares like the CAS. Building a parent is "concat my direct @@ -126,6 +136,9 @@ typedef struct BuildDeepSet { BuildGlobLeaf* globs; /* this node's DIRECT glob leaves */ size_t n_globs; size_t cap_globs; + BuildFetchLeaf* fetches; /* this node's DIRECT fetched blobs */ + size_t n_fetches; + size_t cap_fetches; uint8_t (*children)[BUILD_HASH_LEN]; /* direct deps' deep-set-ids, sorted */ size_t n_children; size_t cap_children; diff --git a/test/buildcoord/run.sh b/test/buildcoord/run.sh @@ -1083,6 +1083,13 @@ contains "buildcoord-fetch-mirror-run" "$work/buildcoord-fetch-mirror.err" \ fetch_path=$(tree_path_from "$work/buildcoord-fetch-mirror.out") contains "buildcoord-fetch-mirror-output" "$fetch_path/fetched.txt" \ "remote:v1" +find "$store/build/trace" -type f -exec grep -l "$fetch_id" {} + \ + > "$work/fetch-traces.txt" 2> "$work/fetch-traces.err" +if [ -s "$work/fetch-traces.txt" ]; then + ok "buildcoord-fetch-logged-in-trace" +else + not_ok "buildcoord-fetch-logged-in-trace" "$work/fetch-traces.err" +fi build_assert_ok buildcoord-fetch-cas-hit-bad-url --stats \ --config env.PATH=/no/such //fetch:blob -- \ "$fetch_id" "file://$work/still-missing-fetch.txt"