kit

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

commit 2fbb8b5957a5c3cd1b7ad6f178fec2e4c6fff6ae
parent c24b9e0300ad0eb206cd9dec61b78ad83e49800f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 18 Jun 2026 07:42:46 -0700

buildcoord: memoize inputs and support globstar

Diffstat:
Msrc/build/coord.c | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/build/runner.c | 20++++++++++++++++++++
Mtest/buildcoord/run.sh | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 238 insertions(+), 17 deletions(-)

diff --git a/src/build/coord.c b/src/build/coord.c @@ -25,6 +25,21 @@ struct BuildPulledSet { struct BuildPulledSet* next; }; +struct BuildSourceMemo { + char path[BUILD_PATH_MAX]; + uint8_t blob[BUILD_HASH_LEN]; + int present; + struct BuildSourceMemo* next; +}; + +struct BuildGlobMemo { + char pattern[BUILD_PATTERN_MAX]; + uint8_t result_hash[BUILD_HASH_LEN]; + BuildPathBlob* entries; + size_t n_entries; + struct BuildGlobMemo* next; +}; + typedef struct BuildDeepSetMemo { BuildLeafSet leaf; BuildSourceLeaf* sources; @@ -66,6 +81,13 @@ static int path_join2(char* out, size_t cap, const char* a, const char* b) { return BUILD_OK; } +static int slice_copy(char* out, size_t cap, KitSlice s) { + if (!out || cap == 0u || !s.s || s.len + 1u > cap) return BUILD_ERR; + memcpy(out, s.s, s.len); + out[s.len] = '\0'; + return BUILD_OK; +} + static int rel_path_safe(KitSlice s) { size_t i, start = 0; if (!s.s || s.len == 0u || s.len >= BUILD_PATH_MAX) return 0; @@ -125,20 +147,34 @@ static int glob_match_range(const char* pat, size_t pn, const char* text, return pi == pn; } +static int glob_segment_double_star(const char* pat, size_t start, + size_t end) { + return end == start + 2u && pat[start] == '*' && pat[start + 1u] == '*'; +} + +static int glob_match_segments(const char* pat, size_t pn, size_t pp, + const char* text, size_t tn, size_t tp) { + size_t pe = pp, te = tp; + if (pp == pn) return tp == tn; + while (pe < pn && pat[pe] != '/') ++pe; + if (glob_segment_double_star(pat, pp, pe)) { + size_t next_pp = pe < pn ? pe + 1u : pn; + if (glob_match_segments(pat, pn, next_pp, text, tn, tp)) return 1; + if (tp == tn) return 0; + while (te < tn && text[te] != '/') ++te; + return glob_match_segments(pat, pn, pp, text, tn, + te < tn ? te + 1u : tn); + } + if (tp == tn) return 0; + while (te < tn && text[te] != '/') ++te; + if (!glob_match_range(pat + pp, pe - pp, text + tp, te - tp)) return 0; + if (pe == pn || te == tn) return pe == pn && te == tn; + return glob_match_segments(pat, pn, pe + 1u, text, tn, te + 1u); +} + static int glob_match_path(KitSlice pattern, const char* path) { - size_t pp = 0, tp = 0, tlen; if (!pattern.s || !path) return 0; - tlen = strlen(path); - for (;;) { - size_t pe = pp, te = tp; - while (pe < pattern.len && pattern.s[pe] != '/') ++pe; - while (te < tlen && path[te] != '/') ++te; - if (!glob_match_range(pattern.s + pp, pe - pp, path + tp, te - tp)) - return 0; - if (pe == pattern.len || te == tlen) return pe == pattern.len && te == tlen; - pp = pe + 1u; - tp = te + 1u; - } + return glob_match_segments(pattern.s, pattern.len, 0, path, strlen(path), 0); } static int path_blob_cmp_qsort(const void* a, const void* b) { @@ -255,6 +291,33 @@ static void free_pulled(KitBuildCoordinator* c) { c->pulled = NULL; } +static void free_sources(KitBuildCoordinator* c) { + BuildSourceMemo* n; + if (!c || !c->ctx || !c->ctx->heap) return; + n = c->sources; + while (n) { + BuildSourceMemo* next = n->next; + c->ctx->heap->free(c->ctx->heap, n, sizeof *n); + n = next; + } + c->sources = NULL; +} + +static void free_globs(KitBuildCoordinator* c) { + BuildGlobMemo* n; + if (!c || !c->ctx || !c->ctx->heap) return; + n = c->globs; + while (n) { + BuildGlobMemo* next = n->next; + if (n->entries) + c->ctx->heap->free(c->ctx->heap, n->entries, + n->n_entries * sizeof *n->entries); + c->ctx->heap->free(c->ctx->heap, n, sizeof *n); + n = next; + } + c->globs = NULL; +} + void build_coord_stat_bump(KitBuildCoordinator* c, BuildStatField f) { if (!c) return; switch (f) { @@ -377,6 +440,8 @@ void build_coord_close(KitBuildCoordinator* c) { h = c->ctx->heap; free_targets(c); free_pulled(c); + free_sources(c); + free_globs(c); free_deepsets(c); release_defn(c); if (c->defn.targets) @@ -389,31 +454,58 @@ void build_coord_close(KitBuildCoordinator* c) { int build_coord_source_hash(KitBuildCoordinator* c, KitSlice path, uint8_t out_blob[BUILD_HASH_LEN], int* present) { char full[BUILD_PATH_MAX]; + char rel[BUILD_PATH_MAX]; KitFileData fd; KitBlobInfo info; + BuildSourceMemo* memo; + BuildSourceMemo* fresh = NULL; if (!c || !out_blob || !present || !rel_path_safe(path)) return BUILD_ERR; + if (slice_copy(rel, sizeof rel, path) != BUILD_OK) return BUILD_ERR; + for (memo = c->sources; memo; memo = memo->next) { + if (strcmp(memo->path, rel) == 0) { + memcpy(out_blob, memo->blob, BUILD_HASH_LEN); + *present = memo->present; + return BUILD_OK; + } + } + fresh = (BuildSourceMemo*)c->ctx->heap->alloc(c->ctx->heap, sizeof *fresh, + _Alignof(BuildSourceMemo)); + if (!fresh) return BUILD_ERR; + memset(fresh, 0, sizeof *fresh); + snprintf(fresh->path, sizeof fresh->path, "%s", rel); *present = 0; - if (path_join2(full, sizeof full, c->workspace_root, path.s) != - BUILD_OK) - return BUILD_ERR; + if (path_join2(full, sizeof full, c->workspace_root, rel) != BUILD_OK) + goto err; fd.data = NULL; fd.size = 0; fd.token = NULL; if (c->host.cas_host->file_io->read_all(c->host.cas_host->file_io->user, full, &fd) != KIT_OK) { memset(out_blob, 0, BUILD_HASH_LEN); + memset(fresh->blob, 0, BUILD_HASH_LEN); + fresh->present = 0; + fresh->next = c->sources; + c->sources = fresh; return BUILD_OK; } if (kit_cas_add_blob(c->cas, fd.data, fd.size, &info) != KIT_OK) { if (c->host.cas_host->file_io->release) c->host.cas_host->file_io->release(c->host.cas_host->file_io->user, &fd); - return BUILD_ERR; + goto err; } memcpy(out_blob, info.id, BUILD_HASH_LEN); + memcpy(fresh->blob, info.id, BUILD_HASH_LEN); *present = 1; + fresh->present = 1; if (c->host.cas_host->file_io->release) c->host.cas_host->file_io->release(c->host.cas_host->file_io->user, &fd); + fresh->next = c->sources; + c->sources = fresh; return BUILD_OK; + +err: + if (fresh) c->ctx->heap->free(c->ctx->heap, fresh, sizeof *fresh); + return BUILD_ERR; } int build_coord_glob(KitBuildCoordinator* c, KitSlice pattern, @@ -421,9 +513,24 @@ int build_coord_glob(KitBuildCoordinator* c, KitSlice pattern, BuildCoordGlobFn cb, void* cb_user) { BuildGlobExpand g; size_t i, out_n = 0; + char pat[BUILD_PATTERN_MAX]; + BuildGlobMemo* memo; + BuildGlobMemo* fresh = NULL; if (!c || !out_result_hash || !glob_pattern_safe(pattern) || !c->host.cas_host || !c->host.cas_host->walk_regular_files) return BUILD_ERR; + if (slice_copy(pat, sizeof pat, pattern) != BUILD_OK) return BUILD_ERR; + for (memo = c->globs; memo; memo = memo->next) { + if (strcmp(memo->pattern, pat) == 0) { + memcpy(out_result_hash, memo->result_hash, BUILD_HASH_LEN); + if (cb) { + for (i = 0; i < memo->n_entries; ++i) { + if (cb(cb_user, memo->entries[i].path, memo->entries[i].blob)) break; + } + } + return BUILD_OK; + } + } memset(&g, 0, sizeof g); g.c = c; g.pattern = pattern; @@ -445,6 +552,22 @@ int build_coord_glob(KitBuildCoordinator* c, KitSlice pattern, if (build_glob_result_hash(c->ctx->heap, g.entries, out_n, out_result_hash) != BUILD_OK) goto err; + fresh = (BuildGlobMemo*)c->ctx->heap->alloc(c->ctx->heap, sizeof *fresh, + _Alignof(BuildGlobMemo)); + if (!fresh) goto err; + memset(fresh, 0, sizeof *fresh); + snprintf(fresh->pattern, sizeof fresh->pattern, "%s", pat); + memcpy(fresh->result_hash, out_result_hash, BUILD_HASH_LEN); + if (out_n) { + fresh->entries = (BuildPathBlob*)c->ctx->heap->alloc( + c->ctx->heap, out_n * sizeof *fresh->entries, _Alignof(BuildPathBlob)); + if (!fresh->entries) goto err; + memcpy(fresh->entries, g.entries, out_n * sizeof *fresh->entries); + fresh->n_entries = out_n; + } + fresh->next = c->globs; + c->globs = fresh; + fresh = NULL; if (cb) { for (i = 0; i < out_n; ++i) { if (cb(cb_user, g.entries[i].path, g.entries[i].blob)) break; @@ -455,6 +578,12 @@ int build_coord_glob(KitBuildCoordinator* c, KitSlice pattern, return BUILD_OK; err: + if (fresh) { + if (fresh->entries) + c->ctx->heap->free(c->ctx->heap, fresh->entries, + fresh->n_entries * sizeof *fresh->entries); + c->ctx->heap->free(c->ctx->heap, fresh, sizeof *fresh); + } if (g.entries) c->ctx->heap->free(c->ctx->heap, g.entries, g.cap * sizeof *g.entries); return BUILD_ERR; diff --git a/src/build/runner.c b/src/build/runner.c @@ -48,8 +48,19 @@ static int append_config_key(BuildDepLog* log, KitSlice key) { static int append_source(BuildDepLog* log, KitSlice path, const uint8_t blob[BUILD_HASH_LEN], int present) { BuildSourceLeaf* row; + size_t i; if (!log || !path.s || path.len == 0u || path.len >= BUILD_PATH_MAX) return BUILD_ERR; + for (i = 0; i < log->n_sources; ++i) { + row = &log->sources[i]; + if (strlen(row->path) == path.len && + memcmp(row->path, path.s, path.len) == 0) { + int absent = present ? 0 : 1; + if (row->absent != absent) return BUILD_ERR; + if (!row->absent && !build_id_eq(row->blob, blob)) return BUILD_ERR; + return BUILD_OK; + } + } if (log->n_sources >= log->cap_sources) return BUILD_ERR; row = &log->sources[log->n_sources++]; memcpy(row->path, path.s, path.len); @@ -65,9 +76,18 @@ static int append_source(BuildDepLog* log, KitSlice path, static int append_glob(BuildDepLog* log, KitSlice pattern, const uint8_t result_hash[BUILD_HASH_LEN]) { BuildGlobLeaf* row; + size_t i; if (!log || !pattern.s || pattern.len == 0u || pattern.len >= BUILD_PATTERN_MAX || !result_hash) return BUILD_ERR; + for (i = 0; i < log->n_globs; ++i) { + row = &log->globs[i]; + if (strlen(row->pattern) == pattern.len && + memcmp(row->pattern, pattern.s, pattern.len) == 0) { + if (!build_id_eq(row->result_hash, result_hash)) return BUILD_ERR; + return BUILD_OK; + } + } if (log->n_globs >= log->cap_globs) return BUILD_ERR; row = &log->globs[log->n_globs++]; memcpy(row->pattern, pattern.s, pattern.len); diff --git a/test/buildcoord/run.sh b/test/buildcoord/run.sh @@ -22,7 +22,7 @@ kit_report_init ws="$work/ws" store="$work/store" -mkdir -p "$ws/recipes" "$ws/src/globset" "$store" +mkdir -p "$ws/recipes" "$ws/src/globset" "$ws/src/tree/nested/deeper" "$store" cat > "$ws/BUILD.kit" <<'EOF' kit-build 1 @@ -50,6 +50,8 @@ recipe recipes/env_probe.sh recipe recipes/fail.sh [target //globset:probe] recipe recipes/globset.sh +[target //globstar:probe] +recipe recipes/globstar.sh [target //lib:data] recipe recipes/lib.sh [target //material:probe] @@ -60,6 +62,8 @@ recipe recipes/needargv.sh recipe recipes/nondet.sh [target //recipe:stamp] recipe recipes/recipe.sh +[target //repeat:probe] +recipe recipes/repeat.sh [target //scope:leaf] recipe recipes/scope_leaf.sh [target //scope:parent-inherit] @@ -88,6 +92,15 @@ EOF cat > "$ws/src/globset/one.txt" <<'EOF' one EOF +cat > "$ws/src/tree/root.txt" <<'EOF' +root +EOF +cat > "$ws/src/tree/nested/mid.txt" <<'EOF' +mid +EOF +cat > "$ws/src/tree/nested/deeper/leaf.txt" <<'EOF' +leaf +EOF cat > "$ws/recipes/lib.sh" <<'EOF' #!/bin/sh @@ -224,6 +237,15 @@ for p in $("$KIT" build glob 'src/globset/*.txt'); do done > "$KIT_BUILD_OUT/files.txt" EOF +cat > "$ws/recipes/globstar.sh" <<'EOF' +#!/bin/sh +set -eu +mkdir -p "$KIT_BUILD_OUT" +"$KIT" build glob 'src/tree/**/*.txt' > "$KIT_BUILD_OUT/all.txt" +"$KIT" build glob 'src/tree/**/leaf.txt' > "$KIT_BUILD_OUT/leaf.txt" +"$KIT" build glob 'src/tree/**/root.txt' > "$KIT_BUILD_OUT/root.txt" +EOF + cat > "$ws/recipes/needargv.sh" <<'EOF' #!/bin/sh set -eu @@ -239,6 +261,18 @@ mkdir -p "$KIT_BUILD_OUT" printf 'recipe:v1\n' > "$KIT_BUILD_OUT/stamp.txt" EOF +cat > "$ws/recipes/repeat.sh" <<'EOF' +#!/bin/sh +set -eu +mkdir -p "$KIT_BUILD_OUT" +p1=$("$KIT" build source src/a.txt) +p2=$("$KIT" build source src/a.txt) +cat "$p1" > "$KIT_BUILD_OUT/source1.txt" +cat "$p2" > "$KIT_BUILD_OUT/source2.txt" +"$KIT" build glob 'src/*.txt' > "$KIT_BUILD_OUT/glob1.txt" +"$KIT" build glob 'src/*.txt' > "$KIT_BUILD_OUT/glob2.txt" +EOF + cat > "$ws/recipes/material.sh" <<'EOF' #!/bin/sh set -eu @@ -480,6 +514,24 @@ else not_ok "buildcoord-recipe-edit-new-tree" "$work/recipe-tree.diag" fi +build_assert_ok buildcoord-repeat-inputs-cold --stats //repeat:probe +contains "buildcoord-repeat-inputs-cold-run" \ + "$work/buildcoord-repeat-inputs-cold.err" "recipes_run=1" +repeat_path=$(tree_path_from "$work/buildcoord-repeat-inputs-cold.out") +contains "buildcoord-repeat-source-first" "$repeat_path/source1.txt" "alpha" +contains "buildcoord-repeat-source-second" "$repeat_path/source2.txt" "alpha" +if cmp -s "$repeat_path/glob1.txt" "$repeat_path/glob2.txt"; then + ok "buildcoord-repeat-glob-stable" +else + printf 'glob outputs differ\n' > "$work/repeat-glob.diag" + not_ok "buildcoord-repeat-glob-stable" "$work/repeat-glob.diag" +fi +build_assert_ok buildcoord-repeat-inputs-deep --stats //repeat:probe +contains "buildcoord-repeat-inputs-deep-hit" \ + "$work/buildcoord-repeat-inputs-deep.err" "deep_hits=1" +contains "buildcoord-repeat-inputs-deep-no-run" \ + "$work/buildcoord-repeat-inputs-deep.err" "recipes_run=0" + build_assert_ok buildcoord-stats-cold --stats //stable:parent contains "buildcoord-stats-cold-recipes" "$work/buildcoord-stats-cold.err" \ "recipes_run=2" @@ -697,6 +749,26 @@ globset_remove_path=$(tree_path_from "$work/buildcoord-globset-remove.out") contains "buildcoord-globset-remove-output" "$globset_remove_path/files.txt" \ "two" +build_assert_ok buildcoord-globstar-cold --stats //globstar:probe +contains "buildcoord-globstar-cold-run" "$work/buildcoord-globstar-cold.err" \ + "recipes_run=1" +globstar_path=$(tree_path_from "$work/buildcoord-globstar-cold.out") +contains "buildcoord-globstar-root" "$globstar_path/all.txt" \ + "src/tree/root.txt" +contains "buildcoord-globstar-mid" "$globstar_path/all.txt" \ + "src/tree/nested/mid.txt" +contains "buildcoord-globstar-leaf" "$globstar_path/all.txt" \ + "src/tree/nested/deeper/leaf.txt" +contains "buildcoord-globstar-specific-leaf" "$globstar_path/leaf.txt" \ + "src/tree/nested/deeper/leaf.txt" +contains "buildcoord-globstar-zero-depth" "$globstar_path/root.txt" \ + "src/tree/root.txt" +build_assert_ok buildcoord-globstar-deep --stats //globstar:probe +contains "buildcoord-globstar-deep-hit" "$work/buildcoord-globstar-deep.err" \ + "deep_hits=1" +contains "buildcoord-globstar-deep-no-run" "$work/buildcoord-globstar-deep.err" \ + "recipes_run=0" + run_fail "buildcoord-failure-first-fails" \ "$KIT" build --store "$store" --root "$ws" --def BUILD.kit \ --config "env.KIT=$KIT" //fail:probe