resolve.h (10378B)
1 #ifndef KIT_BUILD_RESOLVE_H 2 #define KIT_BUILD_RESOLVE_H 3 4 #include <kit/core.h> 5 #include <stddef.h> 6 #include <stdint.h> 7 8 #include "build.h" 9 #include "cfg.h" 10 #include "coord.h" 11 12 /* 13 * The resolution algorithm: resolve(T, cfg, chain) -> {output-tree, path, 14 * leafset}, doing the least work necessary. Three phases (see doc/BUILD_COORDINATOR.md 15 * §Resolution algorithm): 16 * 17 * Phase 1 deep fast path - same argv-id, matching target-scope projected 18 * config observations, and an all-clear refresh of 19 * the deepset closure (every source/glob/blob leaf 20 * unchanged AND every node's recipe-id still 21 * recomputes the same) => reuse, no graph walk. 22 * Phase 2 shallow path - config, argv, and/or sources moved: guard on the 23 * recipe-id, argv-id, consumed keys + direct leaves, 24 * then re-resolve each recorded dep under the 25 * current parent config plus its recorded overlay, 26 * with its recorded argv, and compare OUTPUT tree-ids; 27 * a match still skips the recipe (and writes a fresh 28 * deep trace + deepset). 29 * Phase 3 run the recipe - no trace holds (runner.h). 30 * 31 * On a local-record miss, resolution first attempts one lazy trace-remote pull 32 * (build_coord_trace_remote_pull_once) and re-scans before Phase 3. The config- 33 * dependence is recorded in and verified against the traces; the deep path 34 * checks target-scope projected consumed keys + argv-id, the shallow path checks 35 * direct consumed keys + argv-id. Recipe identity rides as a per-node scalar in 36 * the deepset (Phase 1) and as the recipe-id guard (Phase 2); there is NO 37 * whole-definition dependency. Cross-path dedup is the coordinator's targets 38 * memo; per-path cycle detection is the build chain below. 39 */ 40 41 /* One frame of the per-path build chain: the (target, config-id, argv-id) at one 42 * level, linked to its parent (NULL at the root). The chain is a parent-linked 43 * "cactus stack", NOT a mutable array — each dispatched `need` extends the 44 * shared, read-only parent prefix with its own frame, so concurrent siblings 45 * never copy or mutate one another's chain (the property the future-based `need` 46 * fan-out requires). A `need` whose triple already appears on the walk from a 47 * frame up to the root is a dependency cycle; walking the links renders the error 48 * path (//a -> //b -> //a). Per-path, distinct from the cross-path futures memo: 49 * a shared dep appears in many chains, never twice in one. Frames are allocated 50 * off the coordinator's per-build arena and live until the build completes 51 * (parents outlive their concurrent children by construction — no pop). */ 52 typedef struct BuildChainFrame { 53 const struct BuildChainFrame* parent; /* NULL at the root request */ 54 BuildActionKind action; 55 char target[BUILD_TARGET_MAX]; 56 uint8_t config_id[BUILD_HASH_LEN]; 57 uint8_t argv_id[BUILD_HASH_LEN]; 58 } BuildChainFrame; 59 60 /* Extend `parent` (NULL at the root) with (target, config-id, argv-id), returning 61 * the new tip in *out. Walks parent links to detect a cycle (the triple already 62 * present) -> BUILD_ERR with the rendered path in err. The frame is allocated off 63 * the coordinator's per-build arena; there is no pop (frames are immutable). */ 64 int build_chain_extend(KitBuildCoordinator*, const BuildChainFrame* parent, 65 BuildActionKind action, KitSlice target, 66 const uint8_t config_id[BUILD_HASH_LEN], 67 const uint8_t argv_id[BUILD_HASH_LEN], 68 const BuildChainFrame** out, char* err, size_t errcap); 69 70 /* Resolve a target under the effective propagated config `cfg` and effective 71 * local `argv` (cfg carries its config-id; argv hashes to its argv-id) — the 72 * full (target, config-id, argv-id) identity. `chain` is the parent frame tip 73 * (NULL at the root); this call extends it. Equivalent to dispatch+await on the 74 * same thread (used by the top-level request and the fused one-shot `need`). On 75 * success *out holds the output tree-id, a materialized path, and the deepset 76 * node (its closure DAG root). Errors (cycle, recipe failure, missing inputs) 77 * surface through the coordinator's diag. */ 78 int build_resolve(KitBuildCoordinator*, KitSlice target, const BuildConfig* cfg, 79 const BuildArgv* argv, const BuildChainFrame* chain, 80 BuildResolved* out); 81 82 int build_test_resolve(KitBuildCoordinator*, KitSlice target, 83 const BuildConfig* cfg, const BuildArgv* argv, 84 const BuildChainFrame* chain, BuildTestResolved* out); 85 86 /* The async half of resolution — the engine behind recipe-side `need_submit`. 87 * Extends `chain` (cycle-checked here, so submit fails fast on a cycle), interns 88 * the (target, config-id, argv-id) future, and — if fresh — drives the 3 phases 89 * on a worker thread (build_coord_spawn) that completes the future; a duplicate 90 * attaches to the in-flight future with no new thread. Returns the future in 91 * *out_future WITHOUT blocking, so a recipe can submit many and let them resolve 92 * concurrently. The result is collected later via build_coord_target_await. 93 * `build_resolve` is the inline sibling (dispatch's phases run on the caller's 94 * thread); in sequential mode (no sched) dispatch resolves inline and returns an 95 * already-completed future, so submit/await stays semantically transparent. */ 96 int build_dispatch(KitBuildCoordinator*, KitSlice target, const BuildConfig* cfg, 97 const BuildArgv* argv, const BuildChainFrame* chain, 98 BuildTargetFuture** out_future, char* err, size_t errcap); 99 100 /* Phase 3 of resolution: run the recipe — reached when no trace holds, or a 101 * hit's bytes were evicted everywhere (build_materialize returned BUILD_ERR). 102 * resolve and runner form one strongly-connected component (resolve calls 103 * build_run_recipe for Phase 3; runner's service loop calls build_resolve for 104 * each `need`): NOT a DAG edge. To keep that recursion compiling without a 105 * resolve.h -> runner.h include, build_run_recipe is DECLARED here (the Phase-3 106 * continuation of build_resolve) and IMPLEMENTED in runner.c (which owns the 107 * spawn + service-loop machinery). Same *out contract as build_resolve. */ 108 int build_run_recipe(KitBuildCoordinator*, KitSlice target, 109 const BuildConfig* cfg, const BuildArgv* argv, 110 const BuildChainFrame* chain, BuildResolved* out); 111 112 /* Verification-only sibling of build_run_recipe: executes the recipe and 113 * ingests/materializes the fresh output tree, but writes NO shallow/deep trace 114 * and does not update the target record. Used by verify mode to audit a cache 115 * hit without replacing the cache's claim before the comparison succeeds. */ 116 int build_run_recipe_probe(KitBuildCoordinator*, KitSlice target, 117 const BuildConfig* cfg, const BuildArgv* argv, 118 const BuildChainFrame* chain, BuildResolved* out); 119 int build_run_test_recipe(KitBuildCoordinator*, KitSlice target, 120 const BuildConfig* cfg, const BuildArgv* argv, 121 const BuildChainFrame* chain, BuildTestResolved* out, 122 int record_traces); 123 124 /* The materialization ladder shared by every cache hit: 125 * tree cache/ -> local CAS -> remote object fetch. 126 * A PURE locator: writes the materialized directory path and returns BUILD_OK, 127 * or BUILD_ERR when the bytes are gone everywhere (not in the cache, not in the 128 * CAS, no remote supplied them). It NEVER runs a recipe — on BUILD_ERR the 129 * caller (resolve) falls through to Phase 3, which rebuilds and records the 130 * correct output. Keeping it recipe-free removes the resolve->runner edge and 131 * the stale-tree-id hazard a rebuild-inside-materialize would create (the cache 132 * hit's recorded tree-id always describes the bytes returned). */ 133 int build_materialize(KitBuildCoordinator*, 134 const uint8_t tree_id[BUILD_HASH_LEN], char* path_out, 135 size_t cap); 136 137 /* Build a parent's deepset node from its DIRECT leaves, awaited dep edges, and 138 * child nodes. `direct` carries this node's target, recipe-id, and direct leaves 139 * (its children fields empty); `deps` and `children` are aligned arrays for the 140 * awaited deps. The node stores this target's projected config closure: direct 141 * config observations plus child config observations whose keys were not 142 * overlaid by that dep edge. Computes 143 * the deep-set-id, stores the canonical node as a CAS blob (kit_cas_add_blob — 144 * the children's blobs were stored when they were built, so this is incremental 145 * and deduped), and returns the interned node. This IS the closure union — 146 * "concat direct leaves + projected config leaves + child ids, hash" — never a 147 * flat N-way merge. */ 148 int build_leafset_union(KitBuildCoordinator*, const BuildLeafSet* direct, 149 const BuildDepEdge* deps, 150 const BuildLeafSet* const* children, size_t nchildren, 151 const BuildLeafSet** out); 152 153 /* Refresh a deepset node against the live workspace: per node, rehash its direct 154 * source leaves (absent stays absent), reglob its glob leaves, check its fetched 155 * blobs are still present in the CAS, check the root node's projected config 156 * observations against the query config, recompute each node's 157 * recipe-id through the LIVE definition (catching a dep repointed to a 158 * different-content recipe, treating same-content as unchanged), and recurse 159 * into children — all through the per-process memos and the deep-set-id validity 160 * cache (build_coord_deepset_valid_*), so a shared subtree is checked once and 161 * an unchanged subtree is skipped by id equality without descending. BUILD_OK + 162 * *all_match==1 when the whole closure still matches; *all_match==0 on the first 163 * divergence (Phase 1 then gives up). Phase 1 calls build_coord_deepset_load to 164 * turn the trace's deep-set-id into the node passed here. */ 165 int build_leafset_refresh(KitBuildCoordinator*, const BuildLeafSet*, 166 const BuildConfig*, int* all_match); 167 168 #endif