kit

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

commit 148b695cab846884c7340c153f645c3a6291866a
parent a66723352d1c160257286be2be3040eebf0d1eb2
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 17 Jul 2026 18:26:21 -0700

build: remove redundant config ids from traces

Diffstat:
Mdoc/BUILD_COORDINATOR.md | 96++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mdoc/plan/BUILD_INTERNALS.md | 63+++++++++++++++++++++++++++++++++------------------------------
Mdoc/plan/BUILD_TESTING.md | 2+-
Minclude/kit/build_coord.h | 14+++++++-------
Minclude/kit/cas.h | 2+-
Msrc/build/build.h | 4++--
Msrc/build/bundle.c | 26++------------------------
Msrc/build/bundle.h | 12++++++------
Msrc/build/cfg.h | 22++++++++++------------
Msrc/build/coord.h | 6+++---
Msrc/build/remote.h | 6+++---
Msrc/build/resolve.c | 4++--
Msrc/build/resolve.h | 9+++++----
Msrc/build/runner.c | 38+++++++++++++-------------------------
Msrc/build/runner.h | 16+++++++---------
Msrc/build/trace.c | 29++++++++---------------------
Msrc/build/trace.h | 17++++++++---------
Mtest/build/build_pure_test.c | 19+++++++++++++------
Mtest/buildcoord/run.sh | 21+++++++++++++++++++++
19 files changed, 196 insertions(+), 210 deletions(-)

diff --git a/doc/BUILD_COORDINATOR.md b/doc/BUILD_COORDINATOR.md @@ -345,8 +345,8 @@ WORKSPACE.kit [config default] ⊕ CLI --config / --env entries ``` -The resulting map is still serialized as the ordinary `config-id`; traces do not -record which profile names produced it. Changing a workspace default therefore +The resulting map is hashed as the ordinary `config-id`; traces do not record +that full map or which profile names produced it. Changing a workspace default therefore affects only targets whose recipes actually consume the changed keys, by the same config-observation rules as CLI config. `def-name` selects the package build-file basename for this workspace unless overridden by `--def-name`; it is @@ -455,7 +455,7 @@ kit build export-collect VAR TARGET... `export-get` is equivalent to `need TARGET` followed by parsing the target's exports and printing the selected variable. `export-collect` repeats that for several targets in argument order. The dependency recorded in the trace is still -only the ordinary target dep edge `(dep, dep-config-id, dep-argv-id, +only the ordinary target dep edge `(dep, dep-overlay-id, dep-argv-id, output-tree-id)`: if the provider's exports change, its output tree changes, so the dependent's shallow dep-output comparison fails and the dependent recipe runs again. There is no separate exports trace section, no ambient package-config @@ -477,8 +477,11 @@ Configuration has two scopes, distinguished by visibility: config dep); and a `need` may **overlay** it for the sub-build it triggers. A target's *effective* propagated config = its inherited config with overlays applied along the `need` path from the root. It is canonicalized to byte-stable - text and content-addressed: `config-id = BLAKE2b(canonical map)`, stored as a - CAS blob so any recorded `config-id` resolves back to the actual map. + text and content-addressed: `config-id = BLAKE2b(canonical map)`. That id is + part of the in-memory resolution identity, but the complete effective map is + not recorded in a trace: only consumed config observations are. Explicit + `need` overlay maps are stored as CAS blobs because shallow replay must recover + their values. Driver spelling is `--profile NAME` and `--config K=V`. For recipe environment pass-through, `--config env.NAME` copies the current process's `$NAME` value into config key `env.NAME`; `--env NAME` and `--env NAME=VALUE` are shorthand for @@ -495,17 +498,17 @@ Configuration has two scopes, distinguished by visibility: to its own canonical CAS blob, `argv-id`, and — unlike propagated config — `argv-id` *is* a first-class component of the resolution identity (below). -**Serialized for replay, not just hashed.** Both the effective config map and the -argv vector are persisted *by value* as canonical, content-addressed CAS blobs -(`config-id`, `argv-id`) — never reduced to an opaque one-way hash. A trace -references them by id, and they are part of its reachable closure (GC-rooted, and -bundled with [shared traces](#shared-traces-trusted-as-signed-packages)). This is -what lets the shallow path **replay every `need` request exactly** — reconstruct -the dep's target name, its full effective config with overlays applied, and its -argv — even when driving from an imported shallow trace with no access to the -original package catalog or workspace. Source files and globs are the opposite -case: kept by hash only, because they are *verified* (did this still hash the -same?), never replayed, and their bytes already live in the CAS. +**Serialized for replay, not just hashed.** A `need`'s explicit overlay map and +local argv vector are persisted *by value* as canonical, content-addressed CAS +blobs (`overlay-id`, `argv-id`). A shallow trace references those ids, and they +are part of its reachable closure (GC-rooted and bundled with shared traces). +Replay reconstructs the dep request as the target name plus the current parent's +effective config overlaid by the recovered map, and the recovered argv. The +parent's complete effective config is deliberately not persisted: its consumed +values are verified through `[config]` observations, while the current request +supplies inherited values for replay. Source files and globs are kept by hash +because they are verified, never replayed, and their bytes already live in the +CAS. The **resolution identity** of a build is therefore the triple `(target-name, config-id, argv-id)`, where `config-id` is the effective @@ -554,7 +557,7 @@ immutable and self-verifying; exactly one class of object is mutable. ``` <store>/ cas/ # the shared content store, per DISTRIBUTE.md - blob/<pp>/<blob-id> # raw bytes: sources, config maps, argv vectors, deepset nodes + blob/<pp>/<blob-id> # raw bytes: sources, need overlays, argv vectors, deepset nodes tree/<pp>/<tree-id> # canonical directory manifests (output trees) ... # (chunk/, index/ as in DISTRIBUTE.md) build/ @@ -601,18 +604,17 @@ kit-build-shallow 1 target //app:server recipe <recipe-id> output <output-tree-id> -config <config-id> ; this target's effective propagated config (full map blob) argv <argv-id> ; this target's local config (serialized vector blob) -[config] ; CONSUMED propagated key NAMES, sorted (values live in the map) -opt +[config] ; consumed propagated observations, sorted +opt present <value-hash> - [source] ; sorted by path ("<path> -" marks an absent read) src/main.c <blob-id> [glob] ; sorted by pattern src/*.c <glob-result-hash> [blob] ; 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> +[dep] ; sorted by (dep-name, dep-overlay-id, dep-argv-id) — each row replays one `need` +//lib:core <dep-overlay-id> <dep-argv-id> <dep-output-tree-id> ``` The `recipe <recipe-id>` scalar is the only catalog-derived input (recomputed @@ -642,7 +644,6 @@ kit-build-test-shallow 1 target //pkg:unit recipe <recipe-id> result <result-tree-id> -config <config-id> argv <argv-id> [config] ... @@ -731,9 +732,12 @@ src/*.c <glob-result-hash> 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. +- `[dep]` rows carry the **need's overlay-id and the dep's argv-id** (and output + `tree-id`). Replay recomputes the dep's effective propagated config as *this* + target's current config overlaid by the overlay, then re-resolves the dep under + that config *and* argv. The effective config-id is deliberately *not* stored: it + is exactly `id(overlay(config, overlay-id))` — a pure function of two fields + already in the row/trace, so it would be redundant. **Why the deep trace needs argv + the deepset closure.** At Phase 1 query time the coordinator has only the requested target's effective propagated config and @@ -775,7 +779,8 @@ different argvs). The store must never hand back a wrong output after a crash. The rules: -1. **Content objects** (`cas/blob`, `cas/tree`, `build/trace`, config maps, and a +1. **Content objects** (`cas/blob`, `cas/tree`, `build/trace`, need-overlay maps, + argv vectors, and a materialized `build/cache/<tree-id>/`) are written into `build/tmp/`, fsync'd, then **atomically renamed** to their final content-keyed path. A half-written object only ever exists under `tmp/` and is orphaned, never under its content @@ -879,8 +884,7 @@ resolve(T, cfg, argv, chain): for S in shallow_traces(T), newest-first: if S.recipe != recipe_id(T): continue # recipe edit/repoint (live defn) 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 config observation in S differs from cfg: continue if any direct source/glob/blob leaf of S changed: continue ok = true ; child_nodes = [] for (dep, overlay_id, dep_argv_id, recorded_tree) in S.deps: # may run in parallel @@ -926,11 +930,12 @@ This is the brief's flow, sharpened by config: projection is the same. - *Phase 2* is the shallow path, taken when Phase 1 finds nothing (config or argv changed, or a source moved). It guards on the target's own argv-id, consumed - config, and direct leaves, then re-resolves each recorded dep **under the dep's - recorded config-id and argv-id** and compares outputs. Re-using the recorded - `(dep_cfg_id, dep_argv_id)` is correct: if the parent's consumed config or argv - were different, the guards above would have failed first and the parent would - re-run, recomputing its `need` overlays. + config observations, and direct leaves, then re-resolves each recorded dep + under **the current parent config plus the recorded overlay**, with the + recorded argv, and compares outputs. This preserves newly inherited values + while replaying the need's explicit overrides exactly. If a parent-consumed + value that produced an overlay changes, the parent's config guard fails and + the recipe re-runs to discover a fresh overlay. - A Phase-2 hit also writes a *fresh* deep trace from the now-known closure, so the next request gets the Phase-1 fast path back. - *Phase 3* runs the recipe only when no trace holds, or when a hit's bytes were @@ -1055,11 +1060,11 @@ run_recipe(T, cfg, argv, chain): on nonzero exit: propagate failure, write NO trace on success: output = kit_cas_add_tree_from_dir(out) - put serialized cfg map (cfg.id) and argv vector (argv.id) into the CAS + put the serialized argv vector (argv.id) and each need-overlay map into the CAS node = union(direct{recipe_id, config/source/glob/blob/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) + write_shallow_trace(T, recipe_id, argv.id, config observations, + direct leaves, dep edges with overlay-ids and argv-ids, output) write_deep_trace (T, argv.id, node.id, output) # references the deepset prepend both to T's target record (dedup, truncate to cap) install out in build/cache/<output>/ @@ -1133,9 +1138,9 @@ command. | `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>...` | the verified blob-id + a local CAS path | blob 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 <target> [k=v…] [env…] [argv…]` | the dep's output `tree-id` + a path (blocks) | target dep edge: `(dep, dep-overlay-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)` | +| `need-await <token>` | the submitted dep's output `tree-id` + a path | target dep edge: `(dep, dep-overlay-id, dep-argv-id, output-tree-id)` | - **`config-get`** reads the target's *effective propagated* config and records whether the key was present, plus the value/default hashes needed to replay that @@ -1179,8 +1184,9 @@ command. argv vector sets the dep's **local argv** (omitted ⇒ the empty argv; local argv never propagates). The coordinator resolves `(dep, cfg ⊕ overrides, argv)` recursively, returns its output `tree-id` and a readable path, and records the - edge with the dep's `config-id` and `argv-id` so the shallow path can re-resolve - identically. `need` is also where cycles are caught. + edge with the need's overlay-id and the dep's argv-id so the shallow path can + reconstruct the request from the current parent config. `need` is also where + cycles are caught. - **`need-submit` / `need-await`** are the *future-based* form of `need`, for building a recipe's deps **concurrently**. `need-submit` resolves nothing inline — it dispatches `(dep, cfg ⊕ overrides, argv)` onto the coordinator's @@ -1304,9 +1310,9 @@ machinery wholesale: - A bundle is a signed manifest (`kit-build-traces 1`, signed exactly like a `kit-package` manifest) listing `(target-name, kind, trace-id, output-tree-id)` claims, carried in a `.kpkg` (portable tar.gz or native kpkg) alongside the - 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, + trace bodies and the CAS blobs they reference — the serialized **need-overlay + maps and argv blobs** required to replay shallow deps, and the **deepset + closure blobs** a deep trace points at (required, else an imported deep trace cannot refresh), including any **blob dependency** leaves in shallow traces or deepsets — plus, optionally, the referenced output trees/blobs. @@ -1410,5 +1416,5 @@ deterministically. the current package catalog format has no target-level no-cache flag. - **GC.** Build-store garbage collection is not implemented. The intended sweep roots at live target records, follows trace bodies to referenced output - trees/blobs, config maps, argv vectors, and deepset closure blobs, then removes + trees/blobs, need-overlay maps, argv vectors, and deepset closure blobs, then removes unreachable `build/trace/`, `cas/`, and `build/cache/` entries. diff --git a/doc/plan/BUILD_INTERNALS.md b/doc/plan/BUILD_INTERNALS.md @@ -23,8 +23,8 @@ The build layer sits entirely on the public kit surface — the content store (`<kit/core.h>`), and signed packages (`<kit/package.h>`) — and adds exactly one thing the CAS lacks: a *mutable per-target trace index* plus the resolution algorithm that drives it. Every id it computes is a plain BLAKE2b-256 of canonical -bytes, which **is** a CAS blob id (`kit_blob_info().id`), so config maps, argv -vectors, and **deepset closure nodes** content-address themselves through the +bytes, which **is** a CAS blob id (`kit_blob_info().id`), so need-overlay maps, +argv vectors, and **deepset closure nodes** content-address themselves through the ordinary CAS as ordinary blobs (self-verifying content). Trace bodies are content-addressed too, but live in `build/trace/` rather than `cas/`, because a trace is a *claim* (not self-verifying) — the criterion that sorts the two stores @@ -73,11 +73,12 @@ sets in the spike). Arrows point "depends on": **One id invariant the whole layer leans on:** for any canonical byte string `b`, `kit_blob_info(&info, b, len); info.id` equals the BLAKE2b-256 that names `b` as a -CAS blob. So "compute the config-id/argv-id/trace-id" and "store the blob and read -its id back" agree by construction — a trace can reference a config map by id and -later recover the exact bytes with `kit_cas_get_blob`. Pure index keys that are -*never* stored as content (`target-key`, `glob-result-hash`) use the same hash over -a domain-separated buffer. +CAS blob. So "compute the overlay-id/argv-id/trace-id" and "store the blob and +read its id back" agree by construction — a trace can reference an overlay map +by id and later recover the exact bytes with `kit_cas_get_blob`. Effective config +maps are hashed for resolution identity but are not trace content. Pure index +keys that are never stored as content (`target-key`, `glob-result-hash`) use the +same hash over a domain-separated buffer. --- @@ -113,12 +114,13 @@ void build_diagf(ctx, fmt, ...): // mirror cas_diagf ## L1 — `cfg` (propagated config + local argv) -**Role.** The two configuration objects a build records, each serialized *by value* -as a canonical content-addressed CAS blob (never an opaque one-way hash) so an -imported trace can replay every `need` exactly. `BuildConfig` is the effective -**propagated** map (`config-id`); `BuildArgv` is the **local** argv vector -(`argv-id`). Pure value logic: init / set / get / overlay / canonical emit / parse / -id. Entries hang off caller storage (the `DistTree` growable-buffer pattern, no VLAs). +**Role.** Canonical propagated-config maps and local argv vectors. Effective +propagated maps are hashed for the in-memory resolution identity but traces keep +only consumed observations. Explicit `need` overlay maps and local argv vectors +are serialized by value as CAS blobs so an imported shallow trace can replay a +need exactly. Pure value logic: init / set / get / overlay / canonical emit / +parse / id. Entries hang off caller storage (the `DistTree` growable-buffer +pattern, no VLAs). **Depends on.** `build` (L0), `<kit/core.h>` (`KitWriter`, `KitHeap`), `<kit/cas.h>` (`kit_blob_info`), `<kit/build_coord.h>` (`KitBuildKV` overlay pairs). @@ -213,11 +215,11 @@ as a match (fail safe). Sections hang off caller-provided growable buffers. int build_shallow_emit(t, w, err, errcap): write "kit-build-shallow 1\n" write "target ", t.target, '\n'; "recipe ", hex(t.recipe), '\n' - "output ", hex(t.output), '\n'; "config ", hex(t.config), '\n'; "argv ", hex(t.argv), '\n' - write "[config]\n"; for k in sort(config_keys): write k.name, '\n' + "output ", hex(t.output), '\n'; "argv ", hex(t.argv), '\n' + write "[config]\n"; for k in sort(configs): write observation(k), '\n' write "[source]\n"; for s in sort_by_path(sources): require path-valid; write s.path, ' ', s.absent?"-":hex(s.blob), '\n' write "[glob]\n"; for g in sort_by_pattern(globs): write g.pattern, ' ', hex(g.result_hash), '\n' - write "[dep]\n"; for d in sort_by(name,cfg,argv): write d.name,' ',hex(d.config_id),' ',hex(d.argv_id),' ',hex(d.output_tree),'\n' + write "[dep]\n"; for d in sort_by(name,overlay,argv): write d.name,' ',hex(d.overlay_id),' ',hex(d.argv_id),' ',hex(d.output_tree),'\n' return kit_writer_status(w) int build_deep_emit(t, w, err, errcap): // no [dep], no [config], no inline closure @@ -593,7 +595,8 @@ header promises. ## L4 — `resolve` (the three-phase algorithm) **Role.** `build_resolve(T, cfg, argv, chain) → {output-tree, path, leafset}`, doing -the least work necessary: Phase 1 deep fast path (same config-id+argv-id, did sources +the least work necessary: Phase 1 deep fast path (same argv-id and matching +scope-projected config observations, did sources move?), Phase 2 shallow path (config/argv/sources moved — guard consumed keys + direct leaves, re-resolve recorded deps by their recorded ids, compare *output* tree-ids), Phase 3 run the recipe (delegated to `runner`). Owns the per-path build **chain** @@ -642,12 +645,11 @@ resolve_phases(c, T, cfg, argv, chain, out): S = load+parse shallow trace (absent/garbage → skip) if !id_eq(S.recipe, recipe_id(T)): continue // recipe edit/repoint (live defn) if !id_eq(S.argv, argv.id): continue // local config differs - M = config_by_id(S.config) - if any name in S.config_keys differs between cfg and M (absent==unset): continue + if any observation in S.configs differs from cfg: continue if any direct source/glob leaf of S changed (refresh): continue ok = 1; children = [] - for (dep, dcfg_id, dargv_id, recorded_tree) in S.deps: // may fan out on threads - dcfg = config_by_id(dcfg_id); dargv = argv_by_id(dargv_id) + for (dep, overlay_id, dargv_id, recorded_tree) in S.deps: // may fan out on threads + dcfg = overlay(cfg, config_by_id(overlay_id)); dargv = argv_by_id(dargv_id) // recompute effective config r = build_resolve(c, dep, dcfg, dargv, chain, &child) // replay the need exactly if r != OK or !id_eq(child.output_tree, recorded_tree): ok = 0; break children.push(child.leafset) // child deepset nodes @@ -753,19 +755,20 @@ int build_runner_service(c, conn, T, cfg, chain, log): append (req.arg, result_hash) to log.globs reply matches in OK batch frames, then a GLOB_END frame // streamed, multi-frame NEED: dcfg = build_config_overlay(cfg, req.overrides, …); dargv = build_argv_set(req.argv, …) + overlay_id = store(overlay(empty, req.overrides)) // just the delta; effective cfg is derivable build_coord_jobs_release(c) // ← yield slot while blocked (findings #6) build_resolve(c, req.arg, &dcfg, &dargv, chain, &r) // inline (no worker) build_coord_jobs_acquire(c) // ← reclaim before resuming work - record_dep(log, req.arg, dcfg.id, dargv.id, &r) // append edge + push child node + record_dep(log, req.arg, overlay_id, dargv.id, &r) // append edge + push child node reply {OK, id=r.output_tree, text=r.path} - NEED_SUBMIT: dcfg = overlay; dargv = set + NEED_SUBMIT: dcfg = overlay; dargv = set; overlay_id = store(overlay(empty, req.overrides)) if build_dispatch(c, req.arg, &dcfg, &dargv, chain, &f, err) != OK: reply {ERROR} // cycle/unknown - tok = log.next_token++; log.pending.push({tok, req.arg, dcfg.id, dargv.id, f}) + tok = log.next_token++; log.pending.push({tok, req.arg, overlay_id, dargv.id, f}) reply {OK, token=tok} // does NOT block NEED_AWAIT: p = find_pending(log, req.token); if none: reply {ERROR} build_coord_jobs_release(c); build_coord_target_await(c, p.future, &r); build_coord_jobs_acquire(c) if r failed: reply {ERROR} - record_dep(log, p.dep, p.config_id, p.argv_id, &r) // edge logged HERE, on await + record_dep(log, p.dep, p.overlay_id, p.argv_id, &r) // edge logged HERE, on await remove p from log.pending; reply {OK, id=r.output_tree, text=r.path} // on EOF: for each still-pending need, exec->kill its in-flight recipes (best-effort); record NO edge @@ -773,7 +776,7 @@ int build_runner_record_traces(c, T, cfg, argv, log, output[32], &out_node): recipe_id = build_coord_recipe_id(c, T) build_leafset_union(c, direct_node{T, recipe_id, log.sources, log.globs}, log.child_leafsets, &node) // builds the root deepset node, emits it, stores it as a CAS blob, yields node.id (= deep-set-id) - S = shallow{T, recipe_id, output, cfg.id, argv.id, log.config_keys, log.sources, log.globs, log.deps} + S = shallow{T, recipe_id, output, argv.id, log.configs, log.sources, log.globs, log.deps} emit S → bytes; build_store_put_trace(store, bytes, &sid); build_store_record_update(store, key(T), T, SHALLOW, sid) D = deep{T, recipe_id, output, cfg.id, argv.id, deepset=node.id} // one pointer, not an inline closure emit D → bytes; build_store_put_trace(store, bytes, &did); build_store_record_update(store, key(T), T, DEEP, did) @@ -825,9 +828,9 @@ int build_bundle_export(c, opts): for T in opts.targets: rec = store_record_load(key(T), T) for row in rec (newest candidates): copy build/trace/<row.trace_id> → stage/trace/<id> - parse the body → collect referenced config-id, argv-id (and output-tree for the claim row) + parse the body → collect referenced overlay-id, argv-id (and output-tree for the claim row) claims.push({T, row.kind, row.trace_id, output_tree}) - for each referenced config-id/argv-id: kit_cas_get_blob → stage/cas/blob/<id> // required for replay + for each referenced overlay-id/argv-id: kit_cas_get_blob → stage/cas/blob/<id> // required for replay if opts.include_outputs: also copy referenced output trees + their blobs into stage/cas/ build_bundle_manifest_emit(claims, &w) → stage/manifest "kit-build-traces 1" kit_pkg_create({ root_dir: stage, format: opts.format, sk: opts.sk, keyid: opts.keyid, @@ -839,7 +842,7 @@ int build_bundle_import(c, opts, result): kit_pkg_verify({ pkg_data, pkg_len, format, pubkey_bytes, trusted_keys, tofu, unpack_dir: unpack }, &vr) if !verified: return BUILD_ERR // signature/trust failure read unpack/manifest → build_bundle_manifest_parse → claims - for blob in unpack/cas/blob/*: kit_cas_add_blob(cas, bytes) // config/argv/(outputs), hash-checked + for blob in unpack/cas/blob/*: kit_cas_add_blob(cas, bytes) // overlays/argv/(outputs), hash-checked for c in claims: install unpack/trace/<c.trace_id> via build_store_put_trace // id re-checked on store build_store_record_update(store, key(c.target), c.target, c.kind, c.trace_id) // prepend @@ -1074,7 +1077,7 @@ end-to-end coordinator to validate L0-L2 work. | `coord` | `src/build/coord.c`, `src/build/coord.h` | `cfg`, `defn`, `store`, `trace`, public CAS | context open/close; source/glob/config/argv/recipe memos; deepset intern/load; futures; stats; jobs semaphore | fake workspace memo counts; config/argv by-id load; recursive deepset load; sequential degrade; future dedup; stats bumps under lock | no resolution phase policy, no recipe spawn loop | | `resolve` | `src/build/resolve.c`, `src/build/resolve.h` | `coord`, `cfg`, `trace`; calls `build_run_recipe` by declared seam | chain/cycle detection; Phase 1/2/3 selection; materialization ladder; leafset refresh/union | deep hit; shallow hit after changed dep with same output; recipe-id bust; materialize miss; cycle path; exact stats deltas | no process I/O, no protocol service loop | | `runner` | `src/build/runner.c`, `src/build/runner.h` | `coord`, `resolve`, `protocol`, `trace`, `cfg` | spawn environment; protocol service; dep log; job-slot release across needs; trace recording | scripted transport/exec: config/source/glob/need logs; nonzero exit writes no trace; need-submit/await determinism; jobs=1 deep chain | no Phase 1/2 matching policy, no signed bundle logic | -| `bundle` | `src/build/bundle.c`, `src/build/bundle.h` | `coord`, `store`, public package/CAS | signed trace export/import; trace remote pull | package round-trip; untrusted signer rejected; tofu pin result; imported trace has config/argv/deepset blobs; bad trace id rejected | no object remote fetch, no recipe execution | +| `bundle` | `src/build/bundle.c`, `src/build/bundle.h` | `coord`, `store`, public package/CAS | signed trace export/import; trace remote pull | package round-trip; untrusted signer rejected; tofu pin result; imported trace has overlay/argv/deepset blobs; bad trace id rejected | no object remote fetch, no recipe execution | | `api` | `src/api/build_coord.c` or chosen composition TU, Makefile gating | all internal modules | public coordinator, stats, trace import/export, stubs under disabled gate | public API smoke using fake host; unsupported gate; status/diag mapping | no policy hidden in driver | | `driver` | `driver/cmd/build_coord.c` or final command name, driver tables | public API only | CLI parses flags, supplies hosted vtables, exposes shell helper subcommands for recipes | `run.sh` subprocess/socket smoke; helper commands call client API; clean env behavior | no internal `src/build/*` includes | diff --git a/doc/plan/BUILD_TESTING.md b/doc/plan/BUILD_TESTING.md @@ -76,7 +76,7 @@ harness (decision below). | L3 | `coord` | INTERNAL | all | memo (file hashed once); **futures dedup**; **sequential degrade** (`sched==NULL`); `deepset_load` interns+recurses, absent→ERR; `pull_once` idempotent; **stat counters** | | L4 | `resolve` | INTERNAL/PUBLIC | ipc + FakeExec | the **scenario spine** + soundness scenarios | | L5 | `runner` | INTERNAL | ipc + FakeExec | service loop logs every dep; nonzero exit → **no trace**; both traces + deepset written; **jobs slot released across `need`** (chain deeper than `jobs` completes) | -| L5 | `bundle` | PUBLIC | real pkg | export→import round-trip; **untrusted signer rejected**; tofu pin; deepset + config/argv blobs present so an imported trace refreshes/replays | +| L5 | `bundle` | PUBLIC | real pkg | export→import round-trip; **untrusted signer rejected**; tofu pin; deepset + overlay/argv blobs present so an imported trace refreshes/replays | | L6 | `api` + driver | PUBLIC + `run.sh` | ipc / real | `kit build` end-to-end; `kit_build_stats` deltas | ## Integration diff --git a/include/kit/build_coord.h b/include/kit/build_coord.h @@ -20,8 +20,8 @@ * and a complete cached-build workflow in `kit build --help`. * * Layering. The coordinator is built entirely on the public kit surface: the - * content store (<kit/cas.h>) holds source bytes, output trees, config maps, - * argv vectors, and deepset nodes; build/trace holds content-addressed trace + * content store (<kit/cas.h>) holds source bytes, output trees, need-overlay + * maps, argv vectors, and deepset nodes; build/trace holds content-addressed trace * claims; signed packages (<kit/package.h>) carry shared trace bundles; * digests (<kit/hash.h>) key the mutable index. The only mutable state the * coordinator adds over the CAS is a per-target trace index. @@ -352,10 +352,10 @@ typedef struct KitBuildExportOptions { KitSlice out_path; } KitBuildExportOptions; -/* Bundle the named targets' trace bodies (plus the serialized config-map and - * argv blobs they reference, required for replay, and optionally the referenced - * output trees/blobs) into a signed `.kpkg`, using the <kit/package.h> trust - * model. */ +/* Bundle the named targets' trace bodies (plus the serialized need-overlay maps + * and argv blobs they reference, required for replay, and optionally the + * referenced output trees/blobs) into a signed `.kpkg`, using the + * <kit/package.h> trust model. */ KIT_API KitStatus kit_build_traces_export(KitBuildCoordinator*, const KitBuildExportOptions*); @@ -379,7 +379,7 @@ typedef struct KitBuildImportResult { /* Verify a trace bundle's signature against the trust inputs, then install its * trace bodies into the store and prepend them to the relevant target records; - * referenced config/argv blobs (and any packed outputs) land in the CAS, + * referenced overlay/argv blobs (and any packed outputs) land in the CAS, * hash-verified on use. */ KIT_API KitStatus kit_build_traces_import(KitBuildCoordinator*, const KitBuildImportOptions*, diff --git a/include/kit/cas.h b/include/kit/cas.h @@ -91,7 +91,7 @@ KIT_API KitStatus kit_cas_blob_path(KitCas* cas, /* Read a stored blob's bytes by id. On success *out borrows the bytes; release * them with kit_cas_release. Returns KIT_NOT_FOUND on a miss. This is the - * by-id read path that lets a recorded config-id / argv-id resolve back to the + * by-id read path that lets a recorded overlay-id / argv-id resolve back to the * actual serialized map or vector (the build coordinator's `need` replay). */ KIT_API KitStatus kit_cas_get_blob(KitCas* cas, const uint8_t id[KIT_CAS_HASH_LEN], diff --git a/src/build/build.h b/src/build/build.h @@ -15,8 +15,8 @@ * digest surface (`<kit/hash.h>`), and the signed-package machinery * (`<kit/package.h>`). It deliberately does not reach into src/dist internals: * every id it computes is a plain BLAKE2b-256 of canonical bytes, which is - * exactly a CAS blob id (`kit_blob_info`), so config maps, argv vectors, and - * trace bodies content-address themselves through the ordinary CAS. + * exactly a CAS blob id (`kit_blob_info`), so need-overlay maps, argv vectors, + * and trace bodies content-address themselves through the ordinary CAS. * * Naming/result conventions mirror src/dist: internal modules return `int` * (BUILD_OK / BUILD_ERR), parse/emit modules also take a `char* err, size_t diff --git a/src/build/bundle.c b/src/build/bundle.c @@ -772,14 +772,11 @@ static int BUILD_MAYBE_UNUSED export_trace_refs(KitBuildCoordinator* c, t.cap_deps = rows; if (build_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) goto shallow_out; - if (payload_write_blob_from_cas(c, t.config, root, seen) != BUILD_OK || - payload_write_blob_from_cas(c, t.argv, root, seen) != BUILD_OK) + if (payload_write_blob_from_cas(c, t.argv, root, seen) != BUILD_OK) goto shallow_out; for (i = 0u; i < t.n_deps; ++i) { if (payload_write_blob_from_cas(c, t.deps[i].overlay_id, root, seen) != BUILD_OK || - payload_write_blob_from_cas(c, t.deps[i].config_id, root, seen) != - BUILD_OK || payload_write_blob_from_cas(c, t.deps[i].argv_id, root, seen) != BUILD_OK) goto shallow_out; @@ -824,14 +821,11 @@ shallow_out: t.cap_deps = rows; if (build_test_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) goto test_out; - if (payload_write_blob_from_cas(c, t.config, root, seen) != BUILD_OK || - payload_write_blob_from_cas(c, t.argv, root, seen) != BUILD_OK) + if (payload_write_blob_from_cas(c, t.argv, root, seen) != BUILD_OK) goto test_out; for (i = 0u; i < t.n_deps; ++i) { if (payload_write_blob_from_cas(c, t.deps[i].overlay_id, root, seen) != BUILD_OK || - payload_write_blob_from_cas(c, t.deps[i].config_id, root, seen) != - BUILD_OK || payload_write_blob_from_cas(c, t.deps[i].argv_id, root, seen) != BUILD_OK) goto test_out; @@ -910,10 +904,6 @@ static int BUILD_MAYBE_UNUSED import_trace_refs(KitBuildCoordinator* c, t.cap_deps = rows; if (build_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) goto shallow_out; - if (payload_install_blob(c, t.config, root, seen, NULL, NULL, &fd) != - BUILD_OK) - goto shallow_out; - release_file(c, &fd); if (payload_install_blob(c, t.argv, root, seen, NULL, NULL, &fd) != BUILD_OK) goto shallow_out; @@ -923,10 +913,6 @@ static int BUILD_MAYBE_UNUSED import_trace_refs(KitBuildCoordinator* c, &fd) != BUILD_OK) goto shallow_out; release_file(c, &fd); - if (payload_install_blob(c, t.deps[i].config_id, root, seen, NULL, NULL, - &fd) != BUILD_OK) - goto shallow_out; - release_file(c, &fd); if (payload_install_blob(c, t.deps[i].argv_id, root, seen, NULL, NULL, &fd) != BUILD_OK) goto shallow_out; @@ -977,10 +963,6 @@ shallow_out: t.cap_deps = rows; if (build_test_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) goto test_out; - if (payload_install_blob(c, t.config, root, seen, NULL, NULL, &fd) != - BUILD_OK) - goto test_out; - release_file(c, &fd); if (payload_install_blob(c, t.argv, root, seen, NULL, NULL, &fd) != BUILD_OK) goto test_out; @@ -990,10 +972,6 @@ shallow_out: &fd) != BUILD_OK) goto test_out; release_file(c, &fd); - if (payload_install_blob(c, t.deps[i].config_id, root, seen, NULL, NULL, - &fd) != BUILD_OK) - goto test_out; - release_file(c, &fd); if (payload_install_blob(c, t.deps[i].argv_id, root, seen, NULL, NULL, &fd) != BUILD_OK) goto test_out; diff --git a/src/build/bundle.h b/src/build/bundle.h @@ -18,10 +18,10 @@ * <kit/package.h> manifest + minisign + trust machinery wholesale: a signed * `kit-build-traces 1` manifest lists (target, kind, trace-id, output-tree-id) * claims, carried in a `.kpkg` alongside the trace bodies and the CAS blobs they - * reference — the serialized config-map / argv blobs and need-overlay 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. + * reference — the serialized need-overlay maps 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. * * The security split is precise: a local build can deep/shallow-HIT on a remote * builder's trace (obtaining the output without running the recipe) while @@ -38,7 +38,7 @@ typedef struct BuildTraceClaim { } BuildTraceClaim; /* Gather the named targets' recorded traces (newest candidates), collect the - * trace bodies + referenced config/argv blobs + the deepset-closure blobs each + * trace bodies + referenced overlay/argv blobs + the deepset-closure blobs each * deep trace transitively points at (+ optional output trees/blobs), emit and * sign a kit-build-traces manifest, and write the `.kpkg` to out_path. * Signing/format/key come from `opts`. */ @@ -48,7 +48,7 @@ int build_bundle_export(KitBuildCoordinator*, /* Verify a bundle's signature against the trust inputs (the unchanged * DISTRIBUTE.md model: -p key, the trusted-keys anchor, or --tofu; the signed * trusted comment binds the signature to the manifest hash), then install the - * trace bodies into build/trace/, install referenced config/argv (and any + * trace bodies into build/trace/, install referenced overlay/argv (and any * packed outputs) into the CAS, and prepend the imported traces to the relevant * target records. Output bytes are hash-verified on use. */ int build_bundle_import(KitBuildCoordinator*, const KitBuildImportOptions* opts, diff --git a/src/build/cfg.h b/src/build/cfg.h @@ -9,21 +9,19 @@ #include "build.h" /* - * The two configuration objects a build records, both serialized BY VALUE as - * canonical, content-addressed CAS blobs (never reduced to an opaque hash) so - * that an imported trace can replay every `need` exactly: + * The two configuration value forms used by the build coordinator: * - * BuildConfig - the effective PROPAGATED config: a key->value map that flows - * down the subtree, canonicalized to byte-stable text. Its - * blob id IS the `config-id`. Overlays applied along a `need` path produce a - * new map (and new id). BuildArgv - the LOCAL config: the target's argv - * vector, supplied by the build request, visible only to that one recipe and - * never propagated. Its blob id is the `argv-id`. + * BuildConfig - a PROPAGATED config map. Effective maps flow down the subtree + * and are hashed for resolution identity; traces record only + * consumed observations. Explicit `need` overlay maps are + * serialized by value as CAS blobs for shallow replay. + * BuildArgv - the LOCAL config: the target's argv vector, supplied by the + * build request, visible only to that recipe and never + * propagated. It is serialized by value as an `argv-id` blob. * * Pure value logic: canonicalize, hash, parse, look up, overlay. No I/O — the - * store persists/loads the resulting blobs through the CAS. Entries hang off a - * caller-provided, growable buffer (the DistTree pattern) so there are no VLAs - * and the api layer owns sizing. + * store persists/loads overlay and argv blobs through the CAS. Entries hang off + * caller-provided storage so there are no VLAs and the api layer owns sizing. */ /* The propagated-config value types are part of the public build model diff --git a/src/build/coord.h b/src/build/coord.h @@ -179,9 +179,9 @@ int build_coord_glob_target(KitBuildCoordinator*, KitSlice target, uint8_t out_result_hash[BUILD_HASH_LEN], BuildCoordGlobFn cb, void* cb_user); -/* Recover a propagated config map / an argv vector by its id (loads + parses - * the serialized CAS blob, memoized) — the replay path for a recorded `need`, - * which re-resolves the dep under its exact (config-id, argv-id). */ +/* Recover a need-overlay map / an argv vector by its id (loads + parses the + * serialized CAS blob, memoized) — the replay path that applies the overlay to + * the current parent config and re-resolves the dep with its recorded argv. */ int build_coord_config_by_id(KitBuildCoordinator*, const uint8_t config_id[BUILD_HASH_LEN], BuildConfig* out); diff --git a/src/build/remote.h b/src/build/remote.h @@ -29,9 +29,9 @@ * the whole tree is verified before any cache hit is served from it. * * There is no separate deepset kind: a deepset closure node is a self-verifying - * CAS blob (deep-set-id == blob-id), so it — like config-map and argv blobs — is - * fetched as an ordinary BLOB ({kind}=blob) on demand when build_coord_deepset_- - * load needs one the local CAS lacks. + * CAS blob (deep-set-id == blob-id), so it — like need-overlay and argv blobs — + * is fetched as an ordinary BLOB ({kind}=blob) on demand when + * build_coord_deepset_load needs one the local CAS lacks. * * (Trust, where it exists, is for signed trace bundles — bundle.h. Content * fetched here is trustless.) diff --git a/src/build/resolve.c b/src/build/resolve.c @@ -399,7 +399,7 @@ static int try_shallow_trace(KitBuildCoordinator* c, KitSlice target, log.n_deps = st->n_deps; log.child_leafsets = child_leafsets; log.n_children = st->n_deps; - return build_runner_record_traces(c, target, cfg, argv, &log, + return build_runner_record_traces(c, target, argv, &log, out->output_tree, &out->leafset); } @@ -462,7 +462,7 @@ static int try_test_shallow_trace(KitBuildCoordinator* c, KitSlice target, log.n_deps = st->n_deps; log.child_leafsets = child_leafsets; log.n_children = st->n_deps; - return build_runner_record_test_traces(c, target, cfg, argv, &log, + return build_runner_record_test_traces(c, target, argv, &log, out->result_tree, &out->leafset); } diff --git a/src/build/resolve.h b/src/build/resolve.h @@ -21,10 +21,11 @@ * recomputes the same) => reuse, no graph walk. * Phase 2 shallow path - config, argv, and/or sources moved: guard on the * recipe-id, argv-id, consumed keys + direct leaves, - * then re-resolve each recorded dep under its - * recorded (config-id, argv-id) and compare OUTPUT - * tree-ids; a match still skips the recipe (and - * writes a fresh deep trace + deepset). + * then re-resolve each recorded dep under the + * current parent config plus its recorded overlay, + * with its recorded argv, and compare OUTPUT tree-ids; + * a match still skips the recipe (and writes a fresh + * deep trace + deepset). * Phase 3 run the recipe - no trace holds (runner.h). * * On a local-record miss, resolution first attempts one lazy trace-remote pull diff --git a/src/build/runner.c b/src/build/runner.c @@ -191,20 +191,18 @@ static int append_blob(BuildDepLog* log, static int append_dep(BuildDepLog* log, KitSlice target, const uint8_t overlay_id[BUILD_HASH_LEN], - const uint8_t config_id[BUILD_HASH_LEN], const uint8_t argv_id[BUILD_HASH_LEN], const BuildResolved* r) { BuildDepEdge* row; size_t i; if (!log || !target.s || target.len == 0u || target.len >= BUILD_TARGET_MAX || - !overlay_id || !config_id || !argv_id || !r || !r->leafset) + !overlay_id || !argv_id || !r || !r->leafset) return BUILD_ERR; for (i = 0; i < log->n_deps; ++i) { row = &log->deps[i]; if (strlen(row->name) == target.len && memcmp(row->name, target.s, target.len) == 0 && build_id_eq(row->overlay_id, overlay_id) && - build_id_eq(row->config_id, config_id) && build_id_eq(row->argv_id, argv_id) && build_id_eq(row->output_tree, r->output_tree)) return BUILD_OK; @@ -216,7 +214,6 @@ static int append_dep(BuildDepLog* log, KitSlice target, memcpy(row->name, target.s, target.len); row->name[target.len] = '\0'; memcpy(row->overlay_id, overlay_id, BUILD_HASH_LEN); - memcpy(row->config_id, config_id, BUILD_HASH_LEN); memcpy(row->argv_id, argv_id, BUILD_HASH_LEN); memcpy(row->output_tree, r->output_tree, BUILD_HASH_LEN); log->child_leafsets[log->n_children++] = r->leafset; @@ -504,7 +501,7 @@ static int build_run_recipe_impl(KitBuildCoordinator* c, KitSlice target, sizeof out->path) != BUILD_OK) goto out_cleanup; if (record_traces) { - if (build_runner_record_traces(c, target, cfg, argv, &log, + if (build_runner_record_traces(c, target, argv, &log, out->output_tree, &out->leafset) != BUILD_OK) goto out_cleanup; } else { @@ -643,7 +640,7 @@ static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target, out->exit_code = exit_code; out->status = exit_code == 0 ? KIT_TEST_PASS : KIT_TEST_FAIL; if (out->status == KIT_TEST_PASS && record_traces) { - if (build_runner_record_test_traces(c, target, cfg, argv, &log, + if (build_runner_record_test_traces(c, target, argv, &log, out->result_tree, &out->leafset) != BUILD_OK) goto out_cleanup; @@ -792,8 +789,7 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn, BuildConfig dep_cfg, overlay_cfg, empty_cfg; BuildArgv dep_argv; BuildResolved r; - uint8_t overlay_id[BUILD_HASH_LEN], cfg_id[BUILD_HASH_LEN], - argv_id[BUILD_HASH_LEN]; + uint8_t overlay_id[BUILD_HASH_LEN], argv_id[BUILD_HASH_LEN]; char dep_target[BUILD_TARGET_MAX]; build_config_init(&dep_cfg, cfg_entries, sizeof cfg_entries / sizeof cfg_entries[0]); @@ -811,13 +807,12 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn, &overlay_cfg) != BUILD_OK || store_config_blob(c, &overlay_cfg, overlay_id) != BUILD_OK || build_argv_set(&dep_argv, req.argv, req.argc) != BUILD_OK || - build_config_id(c->ctx->heap, &dep_cfg, cfg_id) != BUILD_OK || build_argv_id(c->ctx->heap, &dep_argv, argv_id) != BUILD_OK || build_resolve(c, kit_slice_cstr(dep_target), &dep_cfg, &dep_argv, chain, &r) != BUILD_OK || - append_dep(log, kit_slice_cstr(dep_target), overlay_id, cfg_id, - argv_id, &r) != + append_dep(log, kit_slice_cstr(dep_target), overlay_id, argv_id, + &r) != BUILD_OK) { resp_error(&resp, KIT_ERR, "need failed"); } else { @@ -832,8 +827,7 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn, BuildArgv dep_argv; BuildTargetFuture* f = NULL; BuildPendingNeed* p; - uint8_t overlay_id[BUILD_HASH_LEN], cfg_id[BUILD_HASH_LEN], - argv_id[BUILD_HASH_LEN]; + uint8_t overlay_id[BUILD_HASH_LEN], argv_id[BUILD_HASH_LEN]; char dep_target[BUILD_TARGET_MAX]; char err[128]; build_config_init(&dep_cfg, cfg_entries, @@ -853,7 +847,6 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn, &overlay_cfg) != BUILD_OK || store_config_blob(c, &overlay_cfg, overlay_id) != BUILD_OK || build_argv_set(&dep_argv, req.argv, req.argc) != BUILD_OK || - build_config_id(c->ctx->heap, &dep_cfg, cfg_id) != BUILD_OK || build_argv_id(c->ctx->heap, &dep_argv, argv_id) != BUILD_OK || build_dispatch(c, kit_slice_cstr(dep_target), &dep_cfg, &dep_argv, chain, &f, err, sizeof err) != BUILD_OK) { @@ -864,7 +857,6 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn, p->token = log->next_token++; target_copy(kit_slice_cstr(dep_target), p->dep); memcpy(p->overlay_id, overlay_id, BUILD_HASH_LEN); - memcpy(p->config_id, cfg_id, BUILD_HASH_LEN); memcpy(p->argv_id, argv_id, BUILD_HASH_LEN); p->future = f; resp.token = p->token; @@ -880,7 +872,7 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn, resp.text = kit_slice_cstr(p->result.path); } else if (build_coord_target_await(c, p->future, &r) != BUILD_OK || append_dep(log, kit_slice_cstr(p->dep), p->overlay_id, - p->config_id, p->argv_id, &r) != BUILD_OK) { + p->argv_id, &r) != BUILD_OK) { resp_error(&resp, KIT_ERR, "need-await failed"); } else { p->awaited = 1; @@ -896,8 +888,7 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn, } int build_runner_record_traces(KitBuildCoordinator* c, KitSlice target, - const BuildConfig* cfg, const BuildArgv* argv, - const BuildDepLog* log, + const BuildArgv* argv, const BuildDepLog* log, const uint8_t output[BUILD_HASH_LEN], const BuildLeafSet** out_leafset) { BuildLeafSet direct; @@ -911,7 +902,7 @@ int build_runner_record_traces(KitBuildCoordinator* c, KitSlice target, char err[128]; int ok = BUILD_ERR; - if (!c || !cfg || !argv || !log || !output || !out_leafset) + if (!c || !argv || !log || !output || !out_leafset) return BUILD_ERR; memset(&direct, 0, sizeof direct); if (target_copy(target, direct.target) != BUILD_OK) return BUILD_ERR; @@ -935,8 +926,7 @@ int build_runner_record_traces(KitBuildCoordinator* c, KitSlice target, if (target_copy(target, shallow.target) != BUILD_OK) return BUILD_ERR; memcpy(shallow.recipe, direct.recipe, BUILD_HASH_LEN); memcpy(shallow.output, output, BUILD_HASH_LEN); - if (store_config_blob(c, cfg, shallow.config) != BUILD_OK || - store_argv_blob(c, argv, shallow.argv) != BUILD_OK) + if (store_argv_blob(c, argv, shallow.argv) != BUILD_OK) return BUILD_ERR; shallow.configs = log->configs; shallow.n_configs = log->n_configs; @@ -986,7 +976,6 @@ out: } int build_runner_record_test_traces(KitBuildCoordinator* c, KitSlice target, - const BuildConfig* cfg, const BuildArgv* argv, const BuildDepLog* log, const uint8_t result[BUILD_HASH_LEN], @@ -1002,7 +991,7 @@ int build_runner_record_test_traces(KitBuildCoordinator* c, KitSlice target, char err[128]; int ok = BUILD_ERR; - if (!c || !cfg || !argv || !log || !result || !out_leafset) + if (!c || !argv || !log || !result || !out_leafset) return BUILD_ERR; memset(&direct, 0, sizeof direct); if (target_copy(target, direct.target) != BUILD_OK) return BUILD_ERR; @@ -1026,8 +1015,7 @@ int build_runner_record_test_traces(KitBuildCoordinator* c, KitSlice target, if (target_copy(target, shallow.target) != BUILD_OK) return BUILD_ERR; memcpy(shallow.recipe, direct.recipe, BUILD_HASH_LEN); memcpy(shallow.output, result, BUILD_HASH_LEN); - if (store_config_blob(c, cfg, shallow.config) != BUILD_OK || - store_argv_blob(c, argv, shallow.argv) != BUILD_OK) + if (store_argv_blob(c, argv, shallow.argv) != BUILD_OK) return BUILD_ERR; shallow.configs = log->configs; shallow.n_configs = log->n_configs; diff --git a/src/build/runner.h b/src/build/runner.h @@ -25,14 +25,13 @@ */ /* One submitted-but-not-yet-awaited need: the coordinator-assigned token, the - * resolved (config-id, argv-id) it was dispatched under, and the in-flight - * future. Becomes a recorded dep edge only when AWAITED; un-awaited entries are + * (overlay-id, argv-id) it was dispatched under, and the in-flight future. + * Becomes a recorded dep edge only when AWAITED; un-awaited entries are * cancelled when the recipe exits. */ typedef struct BuildPendingNeed { uint64_t token; char dep[BUILD_TARGET_MAX]; uint8_t overlay_id[BUILD_HASH_LEN]; - uint8_t config_id[BUILD_HASH_LEN]; uint8_t argv_id[BUILD_HASH_LEN]; BuildTargetFuture* future; /* from build_coord_dispatch */ int awaited; @@ -108,16 +107,15 @@ int build_runner_service(KitBuildCoordinator*, KitBuildConn*, KitSlice target, * - emit + store the SHALLOW trace (direct inputs + dep edges, by hash) and the * DEEP trace (scalars + the root deep-set-id), and prepend both to the target * record. - * Records the target's recipe-id, config-id and argv-id, and each dep edge's - * (config-id, argv-id). Returns the root deepset node via *out_leafset so the - * caller can hand it back in BuildResolved. */ + * Records the target's recipe-id and argv-id, its propagated-config + * observations, and each dep edge's (overlay-id, argv-id). Returns the root + * deepset node via *out_leafset so the caller can hand it back in + * BuildResolved. */ int build_runner_record_traces(KitBuildCoordinator*, KitSlice target, - const BuildConfig* cfg, const BuildArgv* argv, - const BuildDepLog*, + const BuildArgv* argv, const BuildDepLog*, const uint8_t output[BUILD_HASH_LEN], const BuildLeafSet** out_leafset); int build_runner_record_test_traces(KitBuildCoordinator*, KitSlice target, - const BuildConfig* cfg, const BuildArgv* argv, const BuildDepLog*, const uint8_t result[BUILD_HASH_LEN], diff --git a/src/build/trace.c b/src/build/trace.c @@ -127,8 +127,6 @@ static int dep_cmp(const BuildDepEdge* a, const BuildDepEdge* b) { if (c != 0) return c; c = id_cmp(a->overlay_id, b->overlay_id); if (c != 0) return c; - c = id_cmp(a->config_id, b->config_id); - if (c != 0) return c; return id_cmp(a->argv_id, b->argv_id); } @@ -290,18 +288,14 @@ static int emit_sorted_blobs(const BuildBlobLeaf* rows, size_t n, } 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]; + char ovr[BUILD_HEX_LEN], argv[BUILD_HEX_LEN], tree[BUILD_HEX_LEN]; hex_encode(ovr, row->overlay_id); - hex_encode(cfg, row->config_id); hex_encode(argv, row->argv_id); hex_encode(tree, row->output_tree); if (emit_bytes(out, row->name) != BUILD_OK) return BUILD_ERR; if (emit_bytes(out, " ") != BUILD_OK) return BUILD_ERR; if (emit_bytes(out, ovr) != BUILD_OK) return BUILD_ERR; if (emit_bytes(out, " ") != BUILD_OK) return BUILD_ERR; - if (emit_bytes(out, cfg) != BUILD_OK) return BUILD_ERR; - if (emit_bytes(out, " ") != BUILD_OK) return BUILD_ERR; if (emit_bytes(out, argv) != BUILD_OK) return BUILD_ERR; if (emit_bytes(out, " ") != BUILD_OK) return BUILD_ERR; if (emit_bytes(out, tree) != BUILD_OK) return BUILD_ERR; @@ -421,7 +415,6 @@ static int shallow_emit_like(const BuildShallowTrace* t, KitWriter* out, if (emit_text_kv(out, "target", t->target) != BUILD_OK) return BUILD_ERR; if (emit_kv(out, "recipe", t->recipe) != BUILD_OK) return BUILD_ERR; if (emit_kv(out, result_key, t->output) != BUILD_OK) return BUILD_ERR; - if (emit_kv(out, "config", t->config) != BUILD_OK) return BUILD_ERR; if (emit_kv(out, "argv", t->argv) != BUILD_OK) return BUILD_ERR; if (emit_sorted_configs(t->configs, t->n_configs, out, err, errcap) != BUILD_OK) @@ -794,16 +787,15 @@ static int append_deepset_blob(BuildDeepSet* out, char** fields, size_t n, static int append_dep(BuildShallowTrace* out, char** fields, size_t n, char* err, size_t errcap) { BuildDepEdge* row; - if (expect_field_count(n, 5u, err, errcap) != BUILD_OK) return BUILD_ERR; + if (expect_field_count(n, 4u, err, errcap) != BUILD_OK) return BUILD_ERR; if (out->n_deps >= out->cap_deps) return set_err(err, errcap, "too many dep rows"); if (!valid_target(fields[0])) return set_err(err, errcap, "bad dep target"); row = &out->deps[out->n_deps]; snprintf(row->name, sizeof row->name, "%s", fields[0]); if (hex_decode_strict(fields[1], row->overlay_id) != BUILD_OK || - hex_decode_strict(fields[2], row->config_id) != BUILD_OK || - hex_decode_strict(fields[3], row->argv_id) != BUILD_OK || - hex_decode_strict(fields[4], row->output_tree) != BUILD_OK) + hex_decode_strict(fields[2], row->argv_id) != BUILD_OK || + hex_decode_strict(fields[3], row->output_tree) != BUILD_OK) return set_err(err, errcap, "bad dep hash"); if (out->n_deps && dep_cmp(&out->deps[out->n_deps - 1u], row) >= 0) return set_err(err, errcap, "non-canonical dep rows"); @@ -855,9 +847,8 @@ static int append_record_row(BuildTargetRecord* out, char** fields, size_t n, #define F_TARGET 0x01u #define F_RECIPE 0x02u #define F_OUTPUT 0x04u -#define F_CONFIG 0x08u -#define F_ARGV 0x10u -#define F_DEEPSET 0x20u +#define F_ARGV 0x08u +#define F_DEEPSET 0x10u static int shallow_parse_like(const uint8_t* data, size_t len, BuildShallowTrace* out, char* err, @@ -884,7 +875,7 @@ static int shallow_parse_like(const uint8_t* data, size_t len, p.first = 1; for (;;) { char* line; - char* fields[5]; + char* fields[4]; size_t n = 0; int r = parser_next(&p, &line, err, errcap); if (r < 0) return BUILD_ERR; @@ -897,7 +888,7 @@ static int shallow_parse_like(const uint8_t* data, size_t len, } if (line[0] == '[') { if (strcmp(line, "[config]") == 0 && sec == TRACE_SEC_TOP) { - if (seen != (F_TARGET | F_RECIPE | F_OUTPUT | F_CONFIG | F_ARGV)) + if (seen != (F_TARGET | F_RECIPE | F_OUTPUT | F_ARGV)) return set_err(err, errcap, "missing shallow field"); sec = TRACE_SEC_CONFIG; } else if (strcmp(line, "[source]") == 0 && sec == TRACE_SEC_CONFIG) { @@ -930,10 +921,6 @@ static int shallow_parse_like(const uint8_t* data, size_t len, if (parse_top_hash(fields, n, result_key, out->output, &seen, F_OUTPUT, err, errcap) != BUILD_OK) return BUILD_ERR; - } else if (!(seen & F_CONFIG)) { - if (parse_top_hash(fields, n, "config", out->config, &seen, F_CONFIG, - err, errcap) != BUILD_OK) - return BUILD_ERR; } else if (!(seen & F_ARGV)) { if (parse_top_hash(fields, n, "argv", out->argv, &seen, F_ARGV, err, errcap) != BUILD_OK) diff --git a/src/build/trace.h b/src/build/trace.h @@ -50,15 +50,15 @@ typedef struct BuildBlobLeaf { } BuildBlobLeaf; /* 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 - * parent config overlaid by overlay-id; config-id is retained for sharing, - * exact trace description, and bundle closure. Sorted by - * (name, overlay-id, config-id, argv-id). */ + * need overlay-id, the dep's effective argv-id, and the output tree-id it + * produced. Shallow replay recomputes the dep config as the current parent + * config overlaid by the map recovered through overlay-id, so the effective + * config-id is not stored: it is derived from the current request and carries + * no independent replay information. Sorted by + * (name, overlay-id, argv-id). */ typedef struct BuildDepEdge { char name[BUILD_TARGET_MAX]; uint8_t overlay_id[BUILD_HASH_LEN]; - uint8_t config_id[BUILD_HASH_LEN]; uint8_t argv_id[BUILD_HASH_LEN]; uint8_t output_tree[BUILD_HASH_LEN]; } BuildDepEdge; @@ -79,13 +79,12 @@ typedef struct BuildConfigLeaf { } BuildConfigLeaf; /* The shallow trace: direct inputs + direct deps, for fine-grained rechecking. - * `config` is this target's effective propagated config-id (full map blob); - * `argv` is its serialized local-config blob id. */ + * Propagated config is represented only by the consumed observations below; + * `argv` is the target's serialized local-config blob id. */ typedef struct BuildShallowTrace { char target[BUILD_TARGET_MAX]; uint8_t recipe[BUILD_HASH_LEN]; uint8_t output[BUILD_HASH_LEN]; - uint8_t config[BUILD_HASH_LEN]; uint8_t argv[BUILD_HASH_LEN]; BuildConfigLeaf* configs; /* [config] section */ size_t n_configs; diff --git a/test/build/build_pure_test.c b/test/build/build_pure_test.c @@ -256,8 +256,6 @@ static void test_trace(void) { "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\n" "output " "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20\n" - "config " - "02030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021\n" "argv " "030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122\n" "[config]\n" @@ -273,8 +271,6 @@ static void test_trace(void) { "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\n" "output " "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20\n" - "config " - "02030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021\n" "argv " "030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122\n" "[config]\n" @@ -302,7 +298,6 @@ static void test_trace(void) { strcpy(st.target, "//app:bin"); fill_id(st.recipe, 0); fill_id(st.output, 1); - fill_id(st.config, 2); fill_id(st.argv, 3); st.configs = keys; st.n_configs = 2; @@ -335,7 +330,6 @@ static void test_trace(void) { st.cap_deps = 1; strcpy(deps[0].name, "//lib:core"); fill_id(deps[0].overlay_id, 6); - fill_id(deps[0].config_id, 7); fill_id(deps[0].argv_id, 8); fill_id(deps[0].output_tree, 9); @@ -344,6 +338,15 @@ static void test_trace(void) { "emit shallow"); bytes.data = kit_writer_mem_bytes(w, &bytes.len); build_trace_id(bytes.data, bytes.len, trace_id); + EXPECT(!bytes_contains(bytes.data, bytes.len, "\nconfig "), + "shallow omits redundant effective config id"); + EXPECT(bytes_contains( + bytes.data, bytes.len, + "\n[dep]\n//lib:core " + "060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425 " + "08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627 " + "090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728\n"), + "shallow canonical dep row"); memset(&parsed, 0, sizeof parsed); parsed.configs = parsed_keys; @@ -361,6 +364,10 @@ static void test_trace(void) { strcmp(parsed.sources[0].path, "src/a.c") == 0 && parsed.sources[0].absent && memcmp(parsed.deps[0].overlay_id, deps[0].overlay_id, + BUILD_HASH_LEN) == 0 && + memcmp(parsed.deps[0].argv_id, deps[0].argv_id, + BUILD_HASH_LEN) == 0 && + memcmp(parsed.deps[0].output_tree, deps[0].output_tree, BUILD_HASH_LEN) == 0, "shallow canonical sections"); EXPECT(build_shallow_parse((const uint8_t*)dup_source, sizeof dup_source - 1u, diff --git a/test/buildcoord/run.sh b/test/buildcoord/run.sh @@ -131,6 +131,9 @@ EOF cat > "$ws/src/noop.txt" <<'EOF' first EOF +cat > "$ws/src/scope-noop.txt" <<'EOF' +first +EOF cat > "$ws/src/depfile/a.h" <<'EOF' dep-a:v1 EOF @@ -488,6 +491,7 @@ cat > "$ws/recipes/scope_leaf.sh" <<'EOF' #!/bin/sh set -eu mkdir -p "$KIT_BUILD_OUT" +"$KIT" build source src/scope-noop.txt >/dev/null mode=$("$KIT" build config-get mode || true) printf 'mode:%s\n' "$mode" > "$KIT_BUILD_OUT/mode.txt" EOF @@ -1320,6 +1324,23 @@ else "$work/config-override-tree.diag" fi +cat > "$ws/src/scope-noop.txt" <<'EOF' +second +EOF +build_assert_ok buildcoord-config-override-shallow --trace --stats \ + --config mode=profile //scope:parent-override +contains "buildcoord-config-override-shallow-parent-hit" \ + "$work/buildcoord-config-override-shallow.err" \ + "trace shallow-hit target=//scope:parent-override" +contains "buildcoord-config-override-shallow-one-run" \ + "$work/buildcoord-config-override-shallow.err" "recipes_run=1" +contains "buildcoord-config-override-shallow-count" \ + "$work/buildcoord-config-override-shallow.err" "shallow_hits=1" +override_shallow_path=$( + tree_path_from "$work/buildcoord-config-override-shallow.out") +contains "buildcoord-config-override-shallow-output" \ + "$override_shallow_path/mode.txt" "mode:forced" + build_assert_ok buildcoord-config-shadow-parent-cold --stats --config mode=parent1 \ //shadow:parent contains "buildcoord-config-shadow-parent-cold-run" \