commit 31706f330ce37fb7b4f6d0239b1d8a32dae813f3
parent 38da65309e91f80e291d29534719835b64843062
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 22:55:14 -0700
build: add coordinator foundation
Diffstat:
24 files changed, 4325 insertions(+), 7 deletions(-)
diff --git a/include/kit/cas.h b/include/kit/cas.h
@@ -107,6 +107,15 @@ KIT_API KitStatus kit_cas_tree_builder_finish(
KitCasTreeBuilder* b, uint8_t out_tree_id[KIT_CAS_HASH_LEN]);
KIT_API void kit_cas_tree_builder_free(KitCasTreeBuilder* b);
+/* Store an already-canonical tree manifest's bytes; fills out_tree_id with the
+ * manifest id. This is the remote/import counterpart to tree_builder_finish:
+ * callers that receive a tree manifest by content id can verify and install it
+ * without reconstructing a directory first. Non-canonical or malformed
+ * manifests return KIT_MALFORMED. */
+KIT_API KitStatus kit_cas_add_tree_manifest(
+ KitCas* cas, const uint8_t* data, size_t len,
+ uint8_t out_tree_id[KIT_CAS_HASH_LEN]);
+
/* Walk a directory (requires host->walk_regular_files), hashing and storing
* every regular file, then store the resulting tree and yield its id. */
KIT_API KitStatus kit_cas_add_tree_from_dir(
diff --git a/mk/lib_srcs.mk b/mk/lib_srcs.mk
@@ -33,8 +33,8 @@ endef
LIB_SRCS_ABI_CORE = src/abi/abi.c src/abi/registry.c
LIB_SRCS_API_CORE = $(filter-out src/api/archive.c src/api/disasm.c \
src/api/link.c src/api/build.c src/api/cas.c \
- src/api/package.c src/api/compress.c src/api/image.c \
- src/api/stubs.c, \
+ src/api/build_coord.c src/api/package.c \
+ src/api/compress.c src/api/image.c src/api/stubs.c, \
$(wildcard src/api/*.c))
LIB_SRCS_ARCH_CORE = $(filter-out src/arch/%_stubs.c,$(wildcard src/arch/*.c))
LIB_SRCS_ASM_CORE = $(wildcard src/asm/*.c)
@@ -132,6 +132,7 @@ LIB_SRCS_OBJ_IMAGE = src/obj/image.c
# Build orchestration tier (batch compile + ordered link). It uses the linker
# (kit_link_session_*), so it compiles only when KIT_LINK_ENABLED.
LIB_SRCS_API_BUILD = src/api/build.c
+LIB_SRCS_BUILD_COORD = $(shell find src/build -name '*.c' 2>/dev/null)
# Distribution subsystem (content store + signed packages). The cas layer
# needs blake2b (-> monocypher); the pkg layer adds the crypto/container shims
# and the second monocypher TU. The compression codecs (deflate + lz4 block)
@@ -141,6 +142,7 @@ LIB_SRCS_API_BUILD = src/api/build.c
# (lz4.c, lz4frame.c's xxhash/lz4hc/lz4frame) are #included by their src/dist
# shim (amalgamation), so they are NOT compiled standalone.
LIB_SRCS_API_CAS = src/api/cas.c
+LIB_SRCS_API_BUILD_COORD = src/api/build_coord.c
LIB_SRCS_API_PKG = src/api/package.c
LIB_SRCS_API_COMPRESS = src/api/compress.c
LIB_SRCS_DIST_CAS = src/dist/dist.c src/dist/blake2b.c src/dist/blob.c \
@@ -192,6 +194,9 @@ endif
ifeq ($(KIT_LINK_ENABLED),1)
LIB_SRCS += $(LIB_SRCS_API_LINK) $(LIB_SRCS_API_BUILD) $(LIB_SRCS_LINK)
endif
+ifeq ($(KIT_CAS_ENABLED),1)
+LIB_SRCS += $(LIB_SRCS_BUILD_COORD)
+endif
ifeq ($(KIT_IMAGE_ENABLED),1)
LIB_SRCS += $(LIB_SRCS_API_IMAGE) $(LIB_SRCS_OBJ_IMAGE)
endif
@@ -205,7 +210,8 @@ ifeq ($(KIT_GRAM_ENABLED),1)
LIB_SRCS += $(LIB_SRCS_GRAM)
endif
ifeq ($(KIT_CAS_ENABLED),1)
-LIB_SRCS += $(LIB_SRCS_API_CAS) $(LIB_SRCS_DIST_CAS) $(LIB_SRCS_VENDOR_CAS)
+LIB_SRCS += $(LIB_SRCS_API_CAS) $(LIB_SRCS_API_BUILD_COORD) \
+ $(LIB_SRCS_DIST_CAS) $(LIB_SRCS_VENDOR_CAS)
endif
# Shared compression codecs (deflate + lz4 block): pulled in once if either the
# compress API or the pkg layer needs them.
diff --git a/mk/test.mk b/mk/test.mk
@@ -55,6 +55,8 @@ TEST_TARGETS = \
test-bootstrap-toy \
test-bootstrap-toy-debug \
test-bootstrap-toy-release \
+ test-build \
+ test-build-pure \
test-bounce \
test-cbackend \
test-cg-api \
@@ -332,6 +334,17 @@ test-driver-cc: bin
test-driver-build: bin
@KIT=$(abspath $(BIN)) sh test/buildcmds/run.sh
+BUILD_PURE_TEST_BIN = build/test/build_pure_test
+BUILD_PUBLIC_LINK_TEST_BIN = build/test/build_public_link_test
+
+test-build-pure: $(BUILD_PURE_TEST_BIN)
+ $(BUILD_PURE_TEST_BIN)
+
+test-build-public-link: $(BUILD_PUBLIC_LINK_TEST_BIN)
+ $(BUILD_PUBLIC_LINK_TEST_BIN)
+
+test-build: test-build-pure test-build-public-link
+
test-rpn-lang: lib
@KIT_BUILD_DIR=$(abspath $(BUILD_DIR)/test/rpn-lang) sh test/rpn-lang/run.sh
diff --git a/mk/test_unit.mk b/mk/test_unit.mk
@@ -33,7 +33,7 @@ UNIT_TESTS_PUBLIC := \
ar_test target_test arm32_target_features_test cg_api_test cg_switch_test \
cg_fp_cmp_test \
cg_control_test cg_const_test hash_test cas_test release_index_test \
- panic_recovery_test profile_test \
+ panic_recovery_test profile_test build_public_link_test \
link_script_test \
rv64_jit_test rv32_jit_test aa64_inline_test rv64_inline_test x64_inline_test \
arm32_inline_test \
@@ -46,6 +46,7 @@ cas_test_SRC := test/api/cas_test.c
release_index_test_SRC := test/api/release_index_test.c
panic_recovery_test_SRC := test/api/panic_recovery_test.c
profile_test_SRC := test/api/profile_test.c
+build_public_link_test_SRC := test/build/build_public_link_test.c
link_script_test_SRC := test/link/link_script_test.c
cg_api_test_SRC := test/api/cg_type_test.c
cg_switch_test_SRC := test/api/cg_switch_test.c
@@ -61,6 +62,7 @@ x64_inline_test_SRC := test/arch/x64_inline_test.c
arm32_inline_test_SRC := test/arch/arm32_inline_test.c
UNIT_TESTS_INTERNAL := \
+ build_pure_test \
dwarf_test debug_roundtrip_unit debug_cfi_unit \
aa64_isa_test rv64_decode_test rv32_decode_test arm32_decode_test \
aa64_sweep_gen \
@@ -93,6 +95,7 @@ native_direct_target_test_SRC := test/cg/native_direct_target_test.c
x64_dbg_test_SRC := test/arch/x64_dbg_test.c
cg_ir_lower_test_SRC := test/opt/cg_ir_lower_test.c
tiny_inline_test_SRC := test/opt/tiny_inline_test.c
+build_pure_test_SRC := test/build/build_pure_test.c
# ---- build rules ------------------------------------------------------------
# Secondary expansion lets a static-pattern prerequisite reference the
diff --git a/src/api/build_coord.c b/src/api/build_coord.c
@@ -0,0 +1,136 @@
+#include <kit/build_coord.h>
+
+#include "build/bundle.h"
+#include "build/cfg.h"
+#include "build/coord.h"
+#include "build/resolve.h"
+
+#include <string.h>
+
+KitStatus kit_build_coordinator_open(const KitContext* ctx,
+ const KitBuildHost* host,
+ KitSlice store_root,
+ const KitBuildOptions* opts,
+ KitBuildCoordinator** out) {
+ return build_coord_open(ctx, host, store_root, opts, out);
+}
+
+void kit_build_coordinator_close(KitBuildCoordinator* c) {
+ build_coord_close(c);
+}
+
+KitStatus kit_build(KitBuildCoordinator* c, const KitBuildRequest* req,
+ KitBuildResult* out) {
+ BuildConfigEntry cfg_entries[128];
+ char argv_entries[128][BUILD_VAL_MAX];
+ BuildConfig cfg;
+ BuildArgv argv;
+ BuildResolved r;
+ size_t i;
+ if (!c || !req || !out) return KIT_INVALID;
+ build_config_init(&cfg, cfg_entries, sizeof cfg_entries / sizeof cfg_entries[0]);
+ for (i = 0; i < req->nconfig; ++i) {
+ if (build_config_set(&cfg, req->config[i].key, req->config[i].value) !=
+ BUILD_OK)
+ return KIT_INVALID;
+ }
+ build_argv_init(&argv, argv_entries,
+ sizeof argv_entries / sizeof argv_entries[0]);
+ if (build_argv_set(&argv, req->argv, req->argc) != BUILD_OK)
+ return KIT_INVALID;
+ memset(&r, 0, sizeof r);
+ if (build_resolve(c, req->target, &cfg, &argv, NULL, &r) != BUILD_OK)
+ return KIT_ERR;
+ memcpy(out->output_tree, r.output_tree, KIT_BUILD_HASH_LEN);
+ memcpy(out->path, r.path, sizeof out->path);
+ return KIT_OK;
+}
+
+void kit_build_stats(const KitBuildCoordinator* c, KitBuildStats* out) {
+ if (!out) return;
+ memset(out, 0, sizeof *out);
+ if (c) *out = c->stats;
+}
+
+KitStatus kit_build_traces_export(KitBuildCoordinator* c,
+ const KitBuildExportOptions* opts) {
+ return build_bundle_export(c, opts) == BUILD_OK ? KIT_OK : KIT_ERR;
+}
+
+KitStatus kit_build_traces_import(KitBuildCoordinator* c,
+ const KitBuildImportOptions* opts,
+ KitBuildImportResult* result) {
+ return build_bundle_import(c, opts, result) == BUILD_OK ? KIT_OK : KIT_ERR;
+}
+
+struct KitBuildClient {
+ const KitContext* ctx;
+ const KitBuildTransport* transport;
+ KitBuildConn* conn;
+};
+
+KitStatus kit_build_client_open(const KitContext* ctx,
+ const KitBuildTransport* transport,
+ KitBuildClient** out) {
+ (void)ctx;
+ (void)transport;
+ if (out) *out = NULL;
+ return KIT_UNSUPPORTED;
+}
+
+void kit_build_client_close(KitBuildClient* c) { (void)c; }
+
+KitStatus kit_build_client_config_get(KitBuildClient* c, KitSlice key,
+ KitSlice* value, int* present) {
+ (void)c;
+ (void)key;
+ if (value) *value = KIT_SLICE_NULL;
+ if (present) *present = 0;
+ return KIT_UNSUPPORTED;
+}
+
+KitStatus kit_build_client_source(KitBuildClient* c, KitSlice path,
+ uint8_t blob[KIT_BUILD_HASH_LEN],
+ KitSlice* realpath) {
+ (void)c;
+ (void)path;
+ if (blob) memset(blob, 0, KIT_BUILD_HASH_LEN);
+ if (realpath) *realpath = KIT_SLICE_NULL;
+ return KIT_UNSUPPORTED;
+}
+
+KitStatus kit_build_client_glob(KitBuildClient* c, KitSlice pattern,
+ KitBuildGlobFn cb, void* cb_user) {
+ (void)c;
+ (void)pattern;
+ (void)cb;
+ (void)cb_user;
+ return KIT_UNSUPPORTED;
+}
+
+KitStatus kit_build_client_need(KitBuildClient* c,
+ const KitBuildRequest* req,
+ KitBuildResult* out) {
+ (void)c;
+ (void)req;
+ (void)out;
+ return KIT_UNSUPPORTED;
+}
+
+KitStatus kit_build_client_need_submit(KitBuildClient* c,
+ const KitBuildRequest* req,
+ KitBuildNeedToken* out_token) {
+ (void)c;
+ (void)req;
+ if (out_token) out_token->id = 0;
+ return KIT_UNSUPPORTED;
+}
+
+KitStatus kit_build_client_need_await(KitBuildClient* c,
+ KitBuildNeedToken token,
+ KitBuildResult* out) {
+ (void)c;
+ (void)token;
+ (void)out;
+ return KIT_UNSUPPORTED;
+}
diff --git a/src/api/cas.c b/src/api/cas.c
@@ -218,6 +218,59 @@ KitStatus kit_cas_tree_builder_finish(KitCasTreeBuilder* b,
return KIT_OK;
}
+KitStatus kit_cas_add_tree_manifest(KitCas* cas, const uint8_t* data,
+ size_t len,
+ uint8_t out_tree_id[KIT_CAS_HASH_LEN]) {
+ DistTree tree;
+ DistTreeEntry* entries = NULL;
+ KitWriter* w = NULL;
+ const uint8_t* canon;
+ size_t canon_len;
+ char err[128];
+ KitStatus st = KIT_OK;
+
+ if (!cas || (!data && len) || !out_tree_id) return KIT_INVALID;
+ entries = (DistTreeEntry*)cas->ctx->heap->alloc(
+ cas->ctx->heap, DIST_MAX_FILES * sizeof *entries, _Alignof(DistTreeEntry));
+ if (!entries) return KIT_NOMEM;
+ tree.entries = entries;
+ tree.n_entries = 0;
+ tree.cap_entries = DIST_MAX_FILES;
+ if (dist_tree_parse(data, len, &tree, err, sizeof err) != DIST_OK) {
+ kit_ctx_diagf(cas->ctx, "%s", err);
+ st = KIT_MALFORMED;
+ goto out;
+ }
+ if (kit_writer_mem(cas->ctx->heap, &w) != KIT_OK) {
+ kit_ctx_diagf(cas->ctx, "failed to allocate tree writer");
+ st = KIT_NOMEM;
+ goto out;
+ }
+ if (dist_tree_emit(&tree, w) != DIST_OK || kit_writer_status(w) != KIT_OK) {
+ kit_ctx_diagf(cas->ctx, "failed to emit tree manifest");
+ st = KIT_ERR;
+ goto out;
+ }
+ canon = kit_writer_mem_bytes(w, &canon_len);
+ if (canon_len != len || (len && memcmp(canon, data, len) != 0)) {
+ kit_ctx_diagf(cas->ctx, "non-canonical tree manifest");
+ st = KIT_MALFORMED;
+ goto out;
+ }
+ dist_tree_id(out_tree_id, data, len);
+ if (dist_cas_put_tree(&cas->dist, out_tree_id, data, len) != DIST_OK) {
+ kit_ctx_diagf(cas->ctx, "failed to store tree manifest");
+ st = KIT_IO;
+ goto out;
+ }
+
+out:
+ if (w) kit_writer_close(w);
+ cas->ctx->heap->free(cas->ctx->heap, entries,
+ DIST_MAX_FILES * sizeof *entries);
+ return st;
+}
+
void kit_cas_tree_builder_free(KitCasTreeBuilder* b) {
KitHeap* h;
if (!b) return;
diff --git a/src/api/config_stubs.c b/src/api/config_stubs.c
@@ -1,4 +1,5 @@
#include <kit/archive.h>
+#include <kit/build_coord.h>
#include <kit/config.h>
#include <kit/dbg.h>
#include <kit/disasm.h>
@@ -14,6 +15,128 @@
#include "debug/debug.h"
#include "link/link.h"
+#if !KIT_CAS_ENABLED
+KitStatus kit_build_coordinator_open(const KitContext* ctx,
+ const KitBuildHost* host,
+ KitSlice store_root,
+ const KitBuildOptions* opts,
+ KitBuildCoordinator** out) {
+ (void)ctx;
+ (void)host;
+ (void)store_root;
+ (void)opts;
+ if (out) *out = NULL;
+ return KIT_UNSUPPORTED;
+}
+
+void kit_build_coordinator_close(KitBuildCoordinator* c) { (void)c; }
+
+KitStatus kit_build(KitBuildCoordinator* c, const KitBuildRequest* req,
+ KitBuildResult* out) {
+ (void)c;
+ (void)req;
+ (void)out;
+ return KIT_UNSUPPORTED;
+}
+
+void kit_build_stats(const KitBuildCoordinator* c, KitBuildStats* out) {
+ (void)c;
+ if (out) {
+ out->deep_hits = 0;
+ out->shallow_hits = 0;
+ out->recipes_run = 0;
+ out->materialize_misses = 0;
+ out->object_fetches = 0;
+ out->trace_pulls = 0;
+ }
+}
+
+KitStatus kit_build_traces_export(KitBuildCoordinator* c,
+ const KitBuildExportOptions* opts) {
+ (void)c;
+ (void)opts;
+ return KIT_UNSUPPORTED;
+}
+
+KitStatus kit_build_traces_import(KitBuildCoordinator* c,
+ const KitBuildImportOptions* opts,
+ KitBuildImportResult* result) {
+ (void)c;
+ (void)opts;
+ (void)result;
+ return KIT_UNSUPPORTED;
+}
+
+KitStatus kit_build_client_open(const KitContext* ctx,
+ const KitBuildTransport* transport,
+ KitBuildClient** out) {
+ (void)ctx;
+ (void)transport;
+ if (out) *out = NULL;
+ return KIT_UNSUPPORTED;
+}
+
+void kit_build_client_close(KitBuildClient* c) { (void)c; }
+
+KitStatus kit_build_client_config_get(KitBuildClient* c, KitSlice key,
+ KitSlice* value, int* present) {
+ (void)c;
+ (void)key;
+ if (value) *value = KIT_SLICE_NULL;
+ if (present) *present = 0;
+ return KIT_UNSUPPORTED;
+}
+
+KitStatus kit_build_client_source(KitBuildClient* c, KitSlice path,
+ uint8_t blob[KIT_BUILD_HASH_LEN],
+ KitSlice* realpath) {
+ (void)c;
+ (void)path;
+ if (blob) {
+ size_t i;
+ for (i = 0; i < KIT_BUILD_HASH_LEN; ++i) blob[i] = 0;
+ }
+ if (realpath) *realpath = KIT_SLICE_NULL;
+ return KIT_UNSUPPORTED;
+}
+
+KitStatus kit_build_client_glob(KitBuildClient* c, KitSlice pattern,
+ KitBuildGlobFn cb, void* cb_user) {
+ (void)c;
+ (void)pattern;
+ (void)cb;
+ (void)cb_user;
+ return KIT_UNSUPPORTED;
+}
+
+KitStatus kit_build_client_need(KitBuildClient* c,
+ const KitBuildRequest* req,
+ KitBuildResult* out) {
+ (void)c;
+ (void)req;
+ (void)out;
+ return KIT_UNSUPPORTED;
+}
+
+KitStatus kit_build_client_need_submit(KitBuildClient* c,
+ const KitBuildRequest* req,
+ KitBuildNeedToken* out_token) {
+ (void)c;
+ (void)req;
+ if (out_token) out_token->id = 0;
+ return KIT_UNSUPPORTED;
+}
+
+KitStatus kit_build_client_need_await(KitBuildClient* c,
+ KitBuildNeedToken token,
+ KitBuildResult* out) {
+ (void)c;
+ (void)token;
+ (void)out;
+ return KIT_UNSUPPORTED;
+}
+#endif
+
#if !KIT_AR_ENABLED
KitStatus kit_ar_write(KitWriter* out, const KitArInput* members,
uint32_t nmembers, const KitArWriteOptions* opts) {
diff --git a/src/build/build.c b/src/build/build.c
@@ -0,0 +1,103 @@
+#include "build.h"
+
+#include <stdio.h>
+#include <string.h>
+
+static int build_write(KitWriter* out, const void* data, size_t n) {
+ return out && kit_writer_write(out, data, n) == 0 ? BUILD_OK : BUILD_ERR;
+}
+
+static int build_write_cstr(KitWriter* out, const char* s) {
+ return build_write(out, s, strlen(s));
+}
+
+static int build_text_field_valid(const char* s, size_t cap) {
+ size_t i;
+ if (!s || !s[0]) return 0;
+ for (i = 0; i < cap; ++i) {
+ unsigned char c = (unsigned char)s[i];
+ if (c == '\0') return i < cap;
+ if (c == '\n' || c == '\r' || c == ' ' || c == '\t') return 0;
+ }
+ return 0;
+}
+
+static int build_slice_token_valid(KitSlice s, size_t cap) {
+ size_t i;
+ if (s.len == 0 || s.len >= cap || !s.s) return 0;
+ for (i = 0; i < s.len; ++i) {
+ unsigned char c = (unsigned char)s.s[i];
+ if (c == '\0' || c == '\n' || c == '\r' || c == ' ' || c == '\t')
+ return 0;
+ }
+ return 1;
+}
+
+static int build_path_blob_cmp(const BuildPathBlob* a, const BuildPathBlob* b) {
+ return strcmp(a->path, b->path);
+}
+
+int build_target_key(KitSlice target_name, uint8_t out[BUILD_HASH_LEN]) {
+ uint8_t buf[sizeof(BUILD_TARGET_KEY_DOMAIN) - 1u + BUILD_TARGET_MAX - 1u];
+ size_t domain_len = sizeof(BUILD_TARGET_KEY_DOMAIN) - 1u;
+ KitBlobInfo info;
+
+ if (!out) return BUILD_ERR;
+ if (!build_slice_token_valid(target_name, BUILD_TARGET_MAX)) return BUILD_ERR;
+
+ memcpy(buf, BUILD_TARGET_KEY_DOMAIN, domain_len);
+ memcpy(buf + domain_len, target_name.data, target_name.len);
+ kit_blob_info(&info, buf, domain_len + target_name.len);
+ memcpy(out, info.id, BUILD_HASH_LEN);
+ return BUILD_OK;
+}
+
+int build_glob_result_hash(KitHeap* heap, const BuildPathBlob* entries,
+ size_t n, uint8_t out[BUILD_HASH_LEN]) {
+ KitWriter* w = NULL;
+ const uint8_t* bytes;
+ size_t len;
+ size_t i;
+ KitBlobInfo info;
+
+ if (!heap || !out) return BUILD_ERR;
+ if (n && !entries) return BUILD_ERR;
+ if (kit_writer_mem(heap, &w) != 0 || !w) return BUILD_ERR;
+
+ for (i = 0; i < n; ++i) {
+ char hex[BUILD_HEX_LEN];
+ if (!build_text_field_valid(entries[i].path, BUILD_PATH_MAX)) goto err;
+ if (i > 0 && build_path_blob_cmp(&entries[i - 1u], &entries[i]) >= 0)
+ goto err;
+ kit_hex_encode(hex, entries[i].blob, BUILD_HASH_LEN);
+ if (build_write_cstr(w, entries[i].path) != BUILD_OK ||
+ build_write_cstr(w, " ") != BUILD_OK ||
+ build_write_cstr(w, hex) != BUILD_OK ||
+ build_write_cstr(w, "\n") != BUILD_OK)
+ goto err;
+ }
+
+ if (kit_writer_status(w) != 0) goto err;
+ bytes = kit_writer_mem_bytes(w, &len);
+ kit_blob_info(&info, bytes, len);
+ memcpy(out, info.id, BUILD_HASH_LEN);
+ kit_writer_close(w);
+ return BUILD_OK;
+
+err:
+ if (w) kit_writer_close(w);
+ return BUILD_ERR;
+}
+
+void build_diagf(const KitContext* ctx, const char* fmt, ...) {
+ KitSrcLoc loc;
+ va_list ap;
+ if (!ctx || !ctx->diag || !ctx->diag->emit || !fmt) return;
+ loc.file_id = 0;
+ loc.line = 0;
+ loc.col = 0;
+ va_start(ap, fmt);
+ ++ctx->diag->errors;
+ ctx->diag->emit(ctx->diag, KIT_DIAG_ERROR, loc, fmt, ap);
+ va_end(ap);
+}
diff --git a/src/build/bundle.c b/src/build/bundle.c
@@ -0,0 +1,220 @@
+#include "bundle.h"
+
+#include <stdio.h>
+#include <string.h>
+
+static int write_cstr(KitWriter* out, const char* s) {
+ return out && kit_writer_write(out, s, strlen(s)) == KIT_OK ? BUILD_OK
+ : BUILD_ERR;
+}
+
+static int hex_val(char c, unsigned* out) {
+ if (c >= '0' && c <= '9') {
+ *out = (unsigned)(c - '0');
+ return BUILD_OK;
+ }
+ if (c >= 'a' && c <= 'f') {
+ *out = (unsigned)(c - 'a') + 10u;
+ return BUILD_OK;
+ }
+ return BUILD_ERR;
+}
+
+static void hex_encode(char out[BUILD_HEX_LEN],
+ const uint8_t in[BUILD_HASH_LEN]) {
+ size_t i;
+ static const char hexdigits[] = "0123456789abcdef";
+ for (i = 0; i < BUILD_HASH_LEN; ++i) {
+ out[2u * i] = hexdigits[in[i] >> 4];
+ out[2u * i + 1u] = hexdigits[in[i] & 0x0fu];
+ }
+ out[2u * BUILD_HASH_LEN] = '\0';
+}
+
+static int hex_decode(const char* s, uint8_t out[BUILD_HASH_LEN]) {
+ size_t i;
+ if (!s || strlen(s) != 2u * BUILD_HASH_LEN) return BUILD_ERR;
+ for (i = 0; i < BUILD_HASH_LEN; ++i) {
+ unsigned hi, lo;
+ if (hex_val(s[2u * i], &hi) != BUILD_OK ||
+ hex_val(s[2u * i + 1u], &lo) != BUILD_OK)
+ return BUILD_ERR;
+ out[i] = (uint8_t)((hi << 4) | lo);
+ }
+ return BUILD_OK;
+}
+
+static int valid_token(const char* s, size_t cap) {
+ size_t i;
+ if (!s || !s[0]) return 0;
+ for (i = 0; s[i]; ++i) {
+ unsigned char c = (unsigned char)s[i];
+ if (i + 1u >= cap || c <= 0x20u || c >= 0x7fu) return 0;
+ }
+ return 1;
+}
+
+static int kind_valid(uint8_t kind) {
+ return kind == (uint8_t)BUILD_TRACE_DEEP ||
+ kind == (uint8_t)BUILD_TRACE_SHALLOW;
+}
+
+static const char* kind_name(uint8_t kind) {
+ if (kind == (uint8_t)BUILD_TRACE_DEEP) return "deep";
+ if (kind == (uint8_t)BUILD_TRACE_SHALLOW) return "shallow";
+ return NULL;
+}
+
+static int kind_parse(const char* s, uint8_t* out) {
+ if (strcmp(s, "deep") == 0) {
+ *out = (uint8_t)BUILD_TRACE_DEEP;
+ return BUILD_OK;
+ }
+ if (strcmp(s, "shallow") == 0) {
+ *out = (uint8_t)BUILD_TRACE_SHALLOW;
+ return BUILD_OK;
+ }
+ return BUILD_ERR;
+}
+
+static int claim_cmp(const BuildTraceClaim* a, const BuildTraceClaim* b) {
+ int c = strcmp(a->target, b->target);
+ if (c != 0) return c;
+ if (a->kind != b->kind) return a->kind < b->kind ? -1 : 1;
+ c = memcmp(a->trace_id, b->trace_id, BUILD_HASH_LEN);
+ if (c != 0) return c;
+ return memcmp(a->output_tree, b->output_tree, BUILD_HASH_LEN);
+}
+
+int build_bundle_manifest_emit(const BuildTraceClaim* claims, size_t n,
+ KitWriter* out) {
+ const BuildTraceClaim* prev = NULL;
+ size_t emitted = 0;
+ if (!out || (n && !claims)) return BUILD_ERR;
+ if (write_cstr(out, BUILD_TRACES_MAGIC "\n") != BUILD_OK) return BUILD_ERR;
+ while (emitted < n) {
+ const BuildTraceClaim* best = NULL;
+ size_t i;
+ for (i = 0; i < n; ++i) {
+ const BuildTraceClaim* cur = &claims[i];
+ if (!valid_token(cur->target, BUILD_TARGET_MAX) || !kind_valid(cur->kind))
+ return BUILD_ERR;
+ if ((!prev || claim_cmp(prev, cur) < 0) &&
+ (!best || claim_cmp(cur, best) < 0))
+ best = cur;
+ }
+ if (!best) return BUILD_ERR;
+ {
+ char tid[BUILD_HEX_LEN], tree[BUILD_HEX_LEN], line[BUILD_TARGET_MAX + 160u];
+ hex_encode(tid, best->trace_id);
+ hex_encode(tree, best->output_tree);
+ snprintf(line, sizeof line, "%s %s %s %s\n", best->target,
+ kind_name(best->kind), tid, tree);
+ if (write_cstr(out, line) != BUILD_OK) return BUILD_ERR;
+ }
+ prev = best;
+ ++emitted;
+ }
+ return kit_writer_status(out) == KIT_OK ? BUILD_OK : BUILD_ERR;
+}
+
+static int set_err(char* err, size_t errcap, const char* msg) {
+ if (err && errcap) snprintf(err, errcap, "%s", msg);
+ return BUILD_ERR;
+}
+
+static int next_line(const uint8_t* data, size_t len, size_t* pos,
+ char* line, size_t cap, char* err, size_t errcap) {
+ size_t end = *pos;
+ size_t n, i;
+ if (*pos >= len) return 0;
+ while (end < len && data[end] != '\n') ++end;
+ if (end == len) return set_err(err, errcap, "missing final newline");
+ n = end - *pos;
+ if (n >= cap) return set_err(err, errcap, "manifest line too long");
+ for (i = *pos; i < end; ++i)
+ if (data[i] == 0 || data[i] == '\r')
+ return set_err(err, errcap, "bad manifest byte");
+ memcpy(line, data + *pos, n);
+ line[n] = '\0';
+ *pos = end + 1u;
+ return 1;
+}
+
+static int split4(char* line, char** f) {
+ size_t n = 0;
+ char* p = line;
+ while (*p) {
+ if (*p == ' ' || n >= 4u) return BUILD_ERR;
+ f[n++] = p;
+ while (*p && *p != ' ') {
+ unsigned char c = (unsigned char)*p;
+ if (c <= 0x20u || c >= 0x7fu) return BUILD_ERR;
+ ++p;
+ }
+ if (*p == ' ') {
+ *p++ = '\0';
+ if (!*p) return BUILD_ERR;
+ }
+ }
+ return n == 4u ? BUILD_OK : BUILD_ERR;
+}
+
+int build_bundle_manifest_parse(const uint8_t* data, size_t len,
+ BuildTraceClaim* out, size_t cap, size_t* n,
+ char* err, size_t errcap) {
+ size_t pos = 0, count = 0;
+ char line[BUILD_TARGET_MAX + 160u];
+ BuildTraceClaim prev;
+ int have_prev = 0;
+ int r;
+ if (!data || !n || (cap && !out))
+ return set_err(err, errcap, "missing manifest storage");
+ *n = 0;
+ r = next_line(data, len, &pos, line, sizeof line, err, errcap);
+ if (r != 1 || strcmp(line, BUILD_TRACES_MAGIC) != 0)
+ return set_err(err, errcap, "bad trace manifest magic/version");
+ while (pos < len) {
+ char* f[4];
+ BuildTraceClaim row;
+ r = next_line(data, len, &pos, line, sizeof line, err, errcap);
+ if (r != 1) return BUILD_ERR;
+ if (split4(line, f) != BUILD_OK)
+ return set_err(err, errcap, "bad trace manifest row");
+ if (!valid_token(f[0], BUILD_TARGET_MAX) ||
+ kind_parse(f[1], &row.kind) != BUILD_OK ||
+ hex_decode(f[2], row.trace_id) != BUILD_OK ||
+ hex_decode(f[3], row.output_tree) != BUILD_OK)
+ return set_err(err, errcap, "bad trace manifest field");
+ snprintf(row.target, sizeof row.target, "%s", f[0]);
+ if (have_prev && claim_cmp(&prev, &row) >= 0)
+ return set_err(err, errcap, "non-canonical trace manifest ordering");
+ if (count >= cap) return set_err(err, errcap, "too many trace claims");
+ out[count++] = row;
+ prev = row;
+ have_prev = 1;
+ }
+ *n = count;
+ return BUILD_OK;
+}
+
+int build_bundle_export(KitBuildCoordinator* c,
+ const KitBuildExportOptions* opts) {
+ (void)c;
+ (void)opts;
+ return BUILD_ERR;
+}
+
+int build_bundle_import(KitBuildCoordinator* c, const KitBuildImportOptions* opts,
+ KitBuildImportResult* result) {
+ (void)c;
+ (void)opts;
+ (void)result;
+ return BUILD_ERR;
+}
+
+int build_trace_remote_pull(KitBuildCoordinator* c, KitSlice target) {
+ (void)c;
+ (void)target;
+ return BUILD_ERR;
+}
diff --git a/src/build/cfg.c b/src/build/cfg.c
@@ -0,0 +1,320 @@
+#include "cfg.h"
+
+#include <stdio.h>
+#include <string.h>
+
+static int set_err(char* err, size_t cap, const char* msg) {
+ if (err && cap) snprintf(err, cap, "%s", msg);
+ return BUILD_ERR;
+}
+
+static int write_bytes(KitWriter* out, const void* data, size_t n) {
+ return out && kit_writer_write(out, data, n) == 0 ? BUILD_OK : BUILD_ERR;
+}
+
+static int write_cstr(KitWriter* out, const char* s) {
+ return write_bytes(out, s, strlen(s));
+}
+
+static int slice_text_valid(KitSlice s, size_t cap, int allow_space) {
+ size_t i;
+ if (s.len >= cap) return 0;
+ if (s.len && !s.s) return 0;
+ for (i = 0; i < s.len; ++i) {
+ unsigned char c = (unsigned char)s.s[i];
+ if (c == '\0' || c == '\n' || c == '\r') return 0;
+ if (!allow_space && (c == ' ' || c == '\t')) return 0;
+ }
+ return 1;
+}
+
+static int cstr_text_valid(const char* s, size_t cap, int allow_space) {
+ size_t i;
+ if (!s) return 0;
+ for (i = 0; i < cap; ++i) {
+ unsigned char c = (unsigned char)s[i];
+ if (c == '\0') return 1;
+ if (c == '\n' || c == '\r') return 0;
+ if (!allow_space && (c == ' ' || c == '\t')) return 0;
+ }
+ return 0;
+}
+
+static int slice_cstr_cmp(KitSlice a, const char* b) {
+ size_t i = 0;
+ for (;;) {
+ int ac = i < a.len ? (int)(unsigned char)a.s[i] : 0;
+ int bc = b[i] ? (int)(unsigned char)b[i] : 0;
+ if (ac != bc) return ac < bc ? -1 : 1;
+ if (!ac) return 0;
+ ++i;
+ }
+}
+
+static int cstr_cmp(const char* a, const char* b) {
+ int c = strcmp(a, b);
+ return c < 0 ? -1 : (c > 0 ? 1 : 0);
+}
+
+static void copy_slice(char* dst, size_t cap, KitSlice s) {
+ (void)cap;
+ if (s.len) memcpy(dst, s.s, s.len);
+ dst[s.len] = '\0';
+}
+
+void build_config_init(BuildConfig* cfg, BuildConfigEntry* storage,
+ size_t cap) {
+ if (!cfg) return;
+ cfg->entries = storage;
+ cfg->n = 0;
+ cfg->cap = cap;
+}
+
+void build_argv_init(BuildArgv* argv, char (*storage)[BUILD_VAL_MAX],
+ size_t cap) {
+ if (!argv) return;
+ argv->args = storage;
+ argv->n = 0;
+ argv->cap = cap;
+}
+
+int build_config_set(BuildConfig* cfg, KitSlice key, KitSlice value) {
+ size_t lo, hi, pos, i;
+ int cmp = 0;
+
+ if (!cfg || (cfg->cap && !cfg->entries)) return BUILD_ERR;
+ if (!slice_text_valid(key, BUILD_KEY_MAX, 0) ||
+ !slice_text_valid(value, BUILD_VAL_MAX, 1) || key.len == 0)
+ return BUILD_ERR;
+
+ lo = 0;
+ hi = cfg->n;
+ while (lo < hi) {
+ size_t mid = lo + (hi - lo) / 2u;
+ cmp = slice_cstr_cmp(key, cfg->entries[mid].key);
+ if (cmp > 0)
+ lo = mid + 1u;
+ else
+ hi = mid;
+ }
+ pos = lo;
+ if (pos < cfg->n && slice_cstr_cmp(key, cfg->entries[pos].key) == 0) {
+ copy_slice(cfg->entries[pos].value, sizeof cfg->entries[pos].value, value);
+ return BUILD_OK;
+ }
+ if (cfg->n >= cfg->cap) return BUILD_ERR;
+ for (i = cfg->n; i > pos; --i) cfg->entries[i] = cfg->entries[i - 1u];
+ copy_slice(cfg->entries[pos].key, sizeof cfg->entries[pos].key, key);
+ copy_slice(cfg->entries[pos].value, sizeof cfg->entries[pos].value, value);
+ ++cfg->n;
+ return BUILD_OK;
+}
+
+int build_argv_set(BuildArgv* out, const KitSlice* args, size_t n) {
+ size_t i;
+ if (!out || (out->cap && !out->args)) return BUILD_ERR;
+ if (n > out->cap) return BUILD_ERR;
+ if (n && !args) return BUILD_ERR;
+ out->n = 0;
+ for (i = 0; i < n; ++i) {
+ if (!slice_text_valid(args[i], BUILD_VAL_MAX, 1)) return BUILD_ERR;
+ copy_slice(out->args[out->n], BUILD_VAL_MAX, args[i]);
+ ++out->n;
+ }
+ return BUILD_OK;
+}
+
+int build_config_get(const BuildConfig* cfg, KitSlice key, KitSlice* value,
+ int* present) {
+ size_t lo, hi;
+ if (!cfg || !present || !slice_text_valid(key, BUILD_KEY_MAX, 0) ||
+ key.len == 0)
+ return BUILD_ERR;
+ lo = 0;
+ hi = cfg->n;
+ while (lo < hi) {
+ size_t mid = lo + (hi - lo) / 2u;
+ int cmp = slice_cstr_cmp(key, cfg->entries[mid].key);
+ if (cmp == 0) {
+ if (value) {
+ value->s = cfg->entries[mid].value;
+ value->len = strlen(cfg->entries[mid].value);
+ }
+ *present = 1;
+ return BUILD_OK;
+ }
+ if (cmp > 0)
+ lo = mid + 1u;
+ else
+ hi = mid;
+ }
+ if (value) *value = KIT_SLICE_NULL;
+ *present = 0;
+ return BUILD_OK;
+}
+
+int build_config_overlay(const BuildConfig* base, const KitBuildKV* overrides,
+ size_t noverrides, BuildConfig* out) {
+ size_t i;
+ if (!base || !out || (base->n && !base->entries)) return BUILD_ERR;
+ if (noverrides && !overrides) return BUILD_ERR;
+ out->n = 0;
+ for (i = 0; i < base->n; ++i) {
+ if (build_config_set(out, kit_slice_cstr(base->entries[i].key),
+ kit_slice_cstr(base->entries[i].value)) != BUILD_OK)
+ return BUILD_ERR;
+ }
+ for (i = 0; i < noverrides; ++i)
+ if (build_config_set(out, overrides[i].key, overrides[i].value) != BUILD_OK)
+ return BUILD_ERR;
+ return BUILD_OK;
+}
+
+static int config_validate(const BuildConfig* cfg) {
+ size_t i;
+ if (!cfg || (cfg->n && !cfg->entries)) return BUILD_ERR;
+ for (i = 0; i < cfg->n; ++i) {
+ if (!cstr_text_valid(cfg->entries[i].key, BUILD_KEY_MAX, 0) ||
+ !cstr_text_valid(cfg->entries[i].value, BUILD_VAL_MAX, 1) ||
+ cfg->entries[i].key[0] == '\0')
+ return BUILD_ERR;
+ if (i > 0 && cstr_cmp(cfg->entries[i - 1u].key, cfg->entries[i].key) >= 0)
+ return BUILD_ERR;
+ }
+ return BUILD_OK;
+}
+
+int build_config_emit(const BuildConfig* cfg, KitWriter* out) {
+ size_t i;
+ if (config_validate(cfg) != BUILD_OK) return BUILD_ERR;
+ if (write_cstr(out, BUILD_CONFIG_MAGIC "\n") != BUILD_OK) return BUILD_ERR;
+ for (i = 0; i < cfg->n; ++i) {
+ if (write_cstr(out, cfg->entries[i].key) != BUILD_OK ||
+ write_cstr(out, " ") != BUILD_OK ||
+ write_cstr(out, cfg->entries[i].value) != BUILD_OK ||
+ write_cstr(out, "\n") != BUILD_OK)
+ return BUILD_ERR;
+ }
+ return kit_writer_status(out) == 0 ? BUILD_OK : BUILD_ERR;
+}
+
+int build_argv_emit(const BuildArgv* argv, KitWriter* out) {
+ size_t i;
+ if (!argv || (argv->n && !argv->args)) return BUILD_ERR;
+ if (write_cstr(out, BUILD_ARGV_MAGIC "\n") != BUILD_OK) return BUILD_ERR;
+ for (i = 0; i < argv->n; ++i) {
+ if (!cstr_text_valid(argv->args[i], BUILD_VAL_MAX, 1)) return BUILD_ERR;
+ if (write_cstr(out, argv->args[i]) != BUILD_OK ||
+ write_cstr(out, "\n") != BUILD_OK)
+ return BUILD_ERR;
+ }
+ return kit_writer_status(out) == 0 ? BUILD_OK : BUILD_ERR;
+}
+
+static int next_line(const uint8_t* data, size_t len, size_t* pos,
+ KitSlice* line) {
+ size_t start = *pos;
+ size_t end = start;
+ while (end < len && data[end] != '\n') ++end;
+ if (end == len) return BUILD_ERR;
+ line->data = data + start;
+ line->len = end - start;
+ *pos = end + 1u;
+ return BUILD_OK;
+}
+
+int build_config_parse(const uint8_t* data, size_t len, BuildConfig* out,
+ char* err, size_t errcap) {
+ size_t pos = 0;
+ KitSlice line;
+ char prev[BUILD_KEY_MAX];
+ int have_prev = 0;
+
+ if (!data || !out) return set_err(err, errcap, "missing config");
+ out->n = 0;
+ if (next_line(data, len, &pos, &line) != BUILD_OK ||
+ !kit_slice_eq_cstr(line, BUILD_CONFIG_MAGIC))
+ return set_err(err, errcap, "bad config magic/version");
+ while (pos < len) {
+ size_t i;
+ KitSlice key, value;
+ if (next_line(data, len, &pos, &line) != BUILD_OK)
+ return set_err(err, errcap, "unterminated config line");
+ for (i = 0; i < line.len && line.s[i] != ' '; ++i) {
+ }
+ if (i == 0 || i == line.len)
+ return set_err(err, errcap, "expected config key value");
+ key.s = line.s;
+ key.len = i;
+ value.s = line.s + i + 1u;
+ value.len = line.len - i - 1u;
+ if (!slice_text_valid(key, BUILD_KEY_MAX, 0) ||
+ !slice_text_valid(value, BUILD_VAL_MAX, 1))
+ return set_err(err, errcap, "invalid config text");
+ if (have_prev && slice_cstr_cmp(key, prev) <= 0)
+ return set_err(err, errcap, "non-canonical config ordering");
+ if (build_config_set(out, key, value) != BUILD_OK)
+ return set_err(err, errcap, "too many config entries");
+ copy_slice(prev, sizeof prev, key);
+ have_prev = 1;
+ }
+ return BUILD_OK;
+}
+
+int build_argv_parse(const uint8_t* data, size_t len, BuildArgv* out, char* err,
+ size_t errcap) {
+ size_t pos = 0;
+ KitSlice line;
+
+ if (!data || !out) return set_err(err, errcap, "missing argv");
+ out->n = 0;
+ if (next_line(data, len, &pos, &line) != BUILD_OK ||
+ !kit_slice_eq_cstr(line, BUILD_ARGV_MAGIC))
+ return set_err(err, errcap, "bad argv magic/version");
+ while (pos < len) {
+ if (next_line(data, len, &pos, &line) != BUILD_OK)
+ return set_err(err, errcap, "unterminated argv line");
+ if (!slice_text_valid(line, BUILD_VAL_MAX, 1))
+ return set_err(err, errcap, "invalid argv text");
+ if (out->n >= out->cap) return set_err(err, errcap, "too many argv args");
+ copy_slice(out->args[out->n], BUILD_VAL_MAX, line);
+ ++out->n;
+ }
+ return BUILD_OK;
+}
+
+int build_config_id(KitHeap* heap, const BuildConfig* cfg,
+ uint8_t out[BUILD_HASH_LEN]) {
+ KitWriter* w = NULL;
+ const uint8_t* bytes;
+ size_t len;
+ KitBlobInfo info;
+ if (!heap || !out || kit_writer_mem(heap, &w) != 0 || !w) return BUILD_ERR;
+ if (build_config_emit(cfg, w) != BUILD_OK || kit_writer_status(w) != 0) {
+ kit_writer_close(w);
+ return BUILD_ERR;
+ }
+ bytes = kit_writer_mem_bytes(w, &len);
+ kit_blob_info(&info, bytes, len);
+ memcpy(out, info.id, BUILD_HASH_LEN);
+ kit_writer_close(w);
+ return BUILD_OK;
+}
+
+int build_argv_id(KitHeap* heap, const BuildArgv* argv,
+ uint8_t out[BUILD_HASH_LEN]) {
+ KitWriter* w = NULL;
+ const uint8_t* bytes;
+ size_t len;
+ KitBlobInfo info;
+ if (!heap || !out || kit_writer_mem(heap, &w) != 0 || !w) return BUILD_ERR;
+ if (build_argv_emit(argv, w) != BUILD_OK || kit_writer_status(w) != 0) {
+ kit_writer_close(w);
+ return BUILD_ERR;
+ }
+ bytes = kit_writer_mem_bytes(w, &len);
+ kit_blob_info(&info, bytes, len);
+ memcpy(out, info.id, BUILD_HASH_LEN);
+ kit_writer_close(w);
+ return BUILD_OK;
+}
diff --git a/src/build/cfg.h b/src/build/cfg.h
@@ -17,7 +17,7 @@
* 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, fixed by the build definition, visible only to that one recipe and
+ * vector, supplied by the build request, visible only to that one recipe and
* never propagated. Its blob id is the `argv-id`.
*
* Pure value logic: canonicalize, hash, parse, look up, overlay. No I/O — the
diff --git a/src/build/coord.c b/src/build/coord.c
@@ -0,0 +1,406 @@
+#include "coord.h"
+
+#include "bundle.h"
+
+#include <stdio.h>
+#include <string.h>
+
+struct BuildTargetFuture {
+ int done;
+ int failed;
+ BuildResolved result;
+};
+
+static int path_set(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 path_join2(char* out, size_t cap, const char* a, const char* b) {
+ size_t na, nb;
+ int need_sep;
+ if (!out || cap == 0u || !a || !b) return BUILD_ERR;
+ na = strlen(a);
+ nb = strlen(b);
+ need_sep = na > 0u && a[na - 1u] != '/';
+ if (na + (need_sep ? 1u : 0u) + nb + 1u > cap) return BUILD_ERR;
+ memcpy(out, a, na);
+ if (need_sep) out[na++] = '/';
+ memcpy(out + na, b, nb);
+ out[na + nb] = '\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;
+ if (s.s[0] == '/') return 0;
+ for (i = 0; i <= s.len; ++i) {
+ if (i == s.len || s.s[i] == '/') {
+ size_t n = i - start;
+ if (n == 0u) return 0;
+ if (n == 1u && s.s[start] == '.') return 0;
+ if (n == 2u && s.s[start] == '.' && s.s[start + 1u] == '.') return 0;
+ start = i + 1u;
+ } else if (s.s[i] == '\0' || s.s[i] == '\\' || s.s[i] == ':') {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+static size_t count_targets(const uint8_t* data, size_t len) {
+ size_t i, n = 0;
+ static const char marker[] = "[target ";
+ for (i = 0; i + sizeof marker - 1u <= len; ++i) {
+ if ((i == 0u || data[i - 1u] == '\n') &&
+ memcmp(data + i, marker, sizeof marker - 1u) == 0)
+ ++n;
+ }
+ return n;
+}
+
+static void release_defn(KitBuildCoordinator* c) {
+ const KitFileIO* fio;
+ if (!c || !c->defn_bytes.data) return;
+ fio = c->host.cas_host ? c->host.cas_host->file_io : NULL;
+ if (fio && fio->release) fio->release(fio->user, &c->defn_bytes);
+ c->defn_bytes.data = NULL;
+ c->defn_bytes.size = 0;
+ c->defn_bytes.token = NULL;
+}
+
+void build_coord_stat_bump(KitBuildCoordinator* c, BuildStatField f) {
+ if (!c) return;
+ switch (f) {
+ case BUILD_STAT_DEEP_HIT: ++c->stats.deep_hits; break;
+ case BUILD_STAT_SHALLOW_HIT: ++c->stats.shallow_hits; break;
+ case BUILD_STAT_RECIPE_RUN: ++c->stats.recipes_run; break;
+ case BUILD_STAT_MATERIALIZE_MISS: ++c->stats.materialize_misses; break;
+ case BUILD_STAT_OBJECT_FETCH: ++c->stats.object_fetches; break;
+ case BUILD_STAT_TRACE_PULL: ++c->stats.trace_pulls; break;
+ }
+}
+
+KitStatus build_coord_open(const KitContext* ctx, const KitBuildHost* host,
+ KitSlice store_root, const KitBuildOptions* opts,
+ KitBuildCoordinator** out) {
+ KitBuildCoordinator* c;
+ KitHeap* h;
+ KitFileData fd;
+ size_t ntargets;
+ char defn_path[BUILD_PATH_MAX];
+ char err[160];
+ KitStatus st;
+
+ if (!ctx || !ctx->heap || !host || !host->cas_host ||
+ !host->cas_host->file_io || !host->store_io || !opts || !out)
+ return KIT_INVALID;
+ *out = NULL;
+ h = ctx->heap;
+ c = (KitBuildCoordinator*)h->alloc(h, sizeof *c,
+ _Alignof(KitBuildCoordinator));
+ if (!c) return KIT_NOMEM;
+ memset(c, 0, sizeof *c);
+ c->ctx_storage = *ctx;
+ c->ctx = &c->ctx_storage;
+ c->host = *host;
+ c->opts = *opts;
+ c->jobs_limit = opts->jobs > 0 ? opts->jobs : 1;
+ if (!host->sched) c->jobs_limit = 1;
+ if (path_set(c->workspace_root, sizeof c->workspace_root,
+ opts->workspace_root) != BUILD_OK ||
+ path_set(c->store_root, sizeof c->store_root, store_root) != BUILD_OK ||
+ path_join2(c->cas_root, sizeof c->cas_root, c->store_root, "cas") !=
+ BUILD_OK) {
+ h->free(h, c, sizeof *c);
+ return KIT_INVALID;
+ }
+
+ st = kit_cas_open(c->ctx, host->cas_host, c->cas_root, &c->cas);
+ if (st != KIT_OK) {
+ h->free(h, c, sizeof *c);
+ return st;
+ }
+ if (build_store_open(c->ctx, c->cas, host->store_io, host->cas_host,
+ store_root, &c->store) != BUILD_OK) {
+ kit_cas_close(c->cas);
+ h->free(h, c, sizeof *c);
+ return KIT_IO;
+ }
+
+ if (!opts->build_def_path.s || opts->build_def_path.len == 0u) {
+ build_diagf(c->ctx, "build: missing build definition path");
+ kit_cas_close(c->cas);
+ h->free(h, c, sizeof *c);
+ return KIT_INVALID;
+ }
+ if (rel_path_safe(opts->build_def_path)) {
+ if (path_join2(defn_path, sizeof defn_path, c->workspace_root,
+ opts->build_def_path.s) != BUILD_OK) {
+ kit_cas_close(c->cas);
+ h->free(h, c, sizeof *c);
+ return KIT_INVALID;
+ }
+ } else if (path_set(defn_path, sizeof defn_path, opts->build_def_path) !=
+ BUILD_OK) {
+ kit_cas_close(c->cas);
+ h->free(h, c, sizeof *c);
+ return KIT_INVALID;
+ }
+ fd.data = NULL;
+ fd.size = 0;
+ fd.token = NULL;
+ if (host->cas_host->file_io->read_all(host->cas_host->file_io->user,
+ defn_path, &fd) != KIT_OK) {
+ build_diagf(c->ctx, "build: failed to read definition: %s", defn_path);
+ kit_cas_close(c->cas);
+ h->free(h, c, sizeof *c);
+ return KIT_IO;
+ }
+ c->defn_bytes = fd;
+ ntargets = count_targets(fd.data, fd.size);
+ c->defn.targets = NULL;
+ c->defn.cap_targets = ntargets;
+ if (ntargets) {
+ c->defn.targets = (BuildTargetDefn*)h->alloc(
+ h, ntargets * sizeof *c->defn.targets, _Alignof(BuildTargetDefn));
+ if (!c->defn.targets) {
+ release_defn(c);
+ kit_cas_close(c->cas);
+ h->free(h, c, sizeof *c);
+ return KIT_NOMEM;
+ }
+ }
+ if (build_defn_parse(fd.data, fd.size, &c->defn, err, sizeof err) !=
+ BUILD_OK) {
+ build_diagf(c->ctx, "build: %s", err);
+ if (c->defn.targets)
+ h->free(h, c->defn.targets, ntargets * sizeof *c->defn.targets);
+ release_defn(c);
+ kit_cas_close(c->cas);
+ h->free(h, c, sizeof *c);
+ return KIT_MALFORMED;
+ }
+ *out = c;
+ return KIT_OK;
+}
+
+void build_coord_close(KitBuildCoordinator* c) {
+ KitHeap* h;
+ if (!c) return;
+ h = c->ctx->heap;
+ release_defn(c);
+ if (c->defn.targets)
+ h->free(h, c->defn.targets,
+ c->defn.cap_targets * sizeof *c->defn.targets);
+ kit_cas_close(c->cas);
+ h->free(h, c, sizeof *c);
+}
+
+int build_coord_source_hash(KitBuildCoordinator* c, KitSlice path,
+ uint8_t out_blob[BUILD_HASH_LEN], int* present) {
+ char full[BUILD_PATH_MAX];
+ KitFileData fd;
+ KitBlobInfo info;
+ if (!c || !out_blob || !present || !rel_path_safe(path)) return BUILD_ERR;
+ *present = 0;
+ if (path_join2(full, sizeof full, c->workspace_root, path.s) !=
+ BUILD_OK)
+ return BUILD_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);
+ 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;
+ }
+ memcpy(out_blob, info.id, BUILD_HASH_LEN);
+ *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);
+ return BUILD_OK;
+}
+
+int build_coord_glob(KitBuildCoordinator* c, KitSlice pattern,
+ uint8_t out_result_hash[BUILD_HASH_LEN],
+ BuildCoordGlobFn cb, void* cb_user) {
+ (void)c;
+ (void)pattern;
+ (void)out_result_hash;
+ (void)cb;
+ (void)cb_user;
+ return BUILD_ERR;
+}
+
+int build_coord_config_by_id(KitBuildCoordinator* c,
+ const uint8_t config_id[BUILD_HASH_LEN],
+ BuildConfig* out) {
+ KitFileData fd;
+ int r;
+ if (!c || !config_id || !out) return BUILD_ERR;
+ fd.data = NULL;
+ fd.size = 0;
+ fd.token = NULL;
+ if (kit_cas_get_blob(c->cas, config_id, &fd) != KIT_OK) return BUILD_ERR;
+ r = build_config_parse(fd.data, fd.size, out, NULL, 0);
+ kit_cas_release(c->cas, &fd);
+ return r;
+}
+
+int build_coord_argv_by_id(KitBuildCoordinator* c,
+ const uint8_t argv_id[BUILD_HASH_LEN],
+ BuildArgv* out) {
+ KitFileData fd;
+ int r;
+ if (!c || !argv_id || !out) return BUILD_ERR;
+ fd.data = NULL;
+ fd.size = 0;
+ fd.token = NULL;
+ if (kit_cas_get_blob(c->cas, argv_id, &fd) != KIT_OK) return BUILD_ERR;
+ r = build_argv_parse(fd.data, fd.size, out, NULL, 0);
+ kit_cas_release(c->cas, &fd);
+ return r;
+}
+
+int build_coord_recipe_id(KitBuildCoordinator* c, KitSlice target,
+ uint8_t out[BUILD_HASH_LEN]) {
+ const BuildTargetDefn* t;
+ char full[BUILD_PATH_MAX];
+ KitFileData fd;
+ KitBlobInfo info;
+ if (!c || !out) return BUILD_ERR;
+ t = build_defn_find(&c->defn, target);
+ if (!t) return BUILD_ERR;
+ if (path_join2(full, sizeof full, c->workspace_root, t->recipe_path) !=
+ BUILD_OK)
+ return BUILD_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)
+ return BUILD_ERR;
+ 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;
+ }
+ memcpy(out, info.id, BUILD_HASH_LEN);
+ 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_OK;
+}
+
+int build_coord_leafset_intern(KitBuildCoordinator* c, const BuildLeafSet* in,
+ const BuildLeafSet** out) {
+ BuildLeafSet* copy;
+ if (!c || !in || !out) return BUILD_ERR;
+ copy = (BuildLeafSet*)c->ctx->heap->alloc(c->ctx->heap, sizeof *copy,
+ _Alignof(BuildLeafSet));
+ if (!copy) return BUILD_ERR;
+ *copy = *in;
+ *out = copy;
+ return BUILD_OK;
+}
+
+int build_coord_deepset_load(KitBuildCoordinator* c,
+ const uint8_t deepset_id[BUILD_HASH_LEN],
+ const BuildLeafSet** out) {
+ (void)c;
+ (void)deepset_id;
+ (void)out;
+ return BUILD_ERR;
+}
+
+int build_coord_deepset_valid_get(KitBuildCoordinator* c,
+ const uint8_t deepset_id[BUILD_HASH_LEN],
+ int* known, int* valid) {
+ (void)c;
+ (void)deepset_id;
+ if (known) *known = 0;
+ if (valid) *valid = 0;
+ return BUILD_OK;
+}
+
+void build_coord_deepset_valid_set(KitBuildCoordinator* c,
+ const uint8_t deepset_id[BUILD_HASH_LEN],
+ int valid) {
+ (void)c;
+ (void)deepset_id;
+ (void)valid;
+}
+
+int build_coord_trace_remote_pull_once(KitBuildCoordinator* c, KitSlice target,
+ int* pulled_now) {
+ if (!c || !pulled_now) return BUILD_ERR;
+ *pulled_now = 0;
+ if (!c->opts.n_trace_remotes) return BUILD_OK;
+ if (build_trace_remote_pull(c, target) == BUILD_OK) {
+ *pulled_now = 1;
+ build_coord_stat_bump(c, BUILD_STAT_TRACE_PULL);
+ return BUILD_OK;
+ }
+ return BUILD_OK;
+}
+
+void build_coord_jobs_acquire(KitBuildCoordinator* c) { (void)c; }
+void build_coord_jobs_release(KitBuildCoordinator* c) { (void)c; }
+
+int build_coord_spawn(KitBuildCoordinator* c, void (*fn)(void*), void* arg) {
+ if (!c || !c->host.sched || !c->host.sched->thread_spawn || !fn)
+ return BUILD_ERR;
+ return c->host.sched->thread_spawn(c->host.sched->user, fn, arg, NULL) == 0
+ ? BUILD_OK
+ : BUILD_ERR;
+}
+
+int build_coord_target_intern(KitBuildCoordinator* c, KitSlice target,
+ const uint8_t config_id[BUILD_HASH_LEN],
+ const uint8_t argv_id[BUILD_HASH_LEN],
+ BuildTargetFuture** out, int* is_fresh) {
+ BuildTargetFuture* f;
+ (void)target;
+ (void)config_id;
+ (void)argv_id;
+ if (!c || !out || !is_fresh) return BUILD_ERR;
+ f = (BuildTargetFuture*)c->ctx->heap->alloc(c->ctx->heap, sizeof *f,
+ _Alignof(BuildTargetFuture));
+ if (!f) return BUILD_ERR;
+ memset(f, 0, sizeof *f);
+ *out = f;
+ *is_fresh = 1;
+ return BUILD_OK;
+}
+
+int build_coord_target_await(KitBuildCoordinator* c, BuildTargetFuture* f,
+ BuildResolved* out) {
+ (void)c;
+ if (!f || !out || !f->done || f->failed) return BUILD_ERR;
+ *out = f->result;
+ return BUILD_OK;
+}
+
+void build_coord_target_complete(KitBuildCoordinator* c, BuildTargetFuture* f,
+ const BuildResolved* r) {
+ (void)c;
+ if (!f || !r) return;
+ f->result = *r;
+ f->done = 1;
+ f->failed = 0;
+}
+
+void build_coord_target_fail(KitBuildCoordinator* c, BuildTargetFuture* f) {
+ (void)c;
+ if (!f) return;
+ f->done = 1;
+ f->failed = 1;
+}
diff --git a/src/build/coord.h b/src/build/coord.h
@@ -77,6 +77,9 @@ struct KitBuildCoordinator {
const KitContext* ctx;
KitBuildHost host; /* borrowed vtables */
KitBuildOptions opts; /* borrowed strings */
+ char workspace_root[BUILD_PATH_MAX];
+ char store_root[BUILD_PATH_MAX];
+ char cas_root[BUILD_PATH_MAX];
KitCas* cas; /* the shared content store */
BuildStore store; /* paths + record RMW + tree cache over build/ */
BuildDefn defn; /* parsed build definition (a tracked source) */
diff --git a/src/build/defn.c b/src/build/defn.c
@@ -0,0 +1,138 @@
+#include "defn.h"
+
+#include <stdio.h>
+#include <string.h>
+
+static int set_err(char* err, size_t cap, const char* msg) {
+ if (err && cap) snprintf(err, cap, "%s", msg);
+ return BUILD_ERR;
+}
+
+static int slice_valid_token(KitSlice s, size_t cap) {
+ size_t i;
+ if (s.len == 0 || s.len >= cap || !s.s) return 0;
+ for (i = 0; i < s.len; ++i) {
+ unsigned char c = (unsigned char)s.s[i];
+ if (c == '\0' || c == '\n' || c == '\r' || c == ' ' || c == '\t')
+ return 0;
+ }
+ return 1;
+}
+
+static int rel_path_valid(KitSlice s) {
+ size_t i;
+ size_t start = 0;
+ if (!slice_valid_token(s, BUILD_PATH_MAX)) return 0;
+ if (s.s[0] == '/') return 0;
+ for (i = 0; i <= s.len; ++i) {
+ if (i == s.len || s.s[i] == '/') {
+ size_t n = i - start;
+ if (n == 0) return 0;
+ if (n == 1u && s.s[start] == '.') return 0;
+ if (n == 2u && s.s[start] == '.' && s.s[start + 1u] == '.') return 0;
+ start = i + 1u;
+ } else if (s.s[i] == '\\' || s.s[i] == ':') {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+static int next_line(const uint8_t* data, size_t len, size_t* pos,
+ KitSlice* line) {
+ size_t start = *pos;
+ size_t end = start;
+ while (end < len && data[end] != '\n') ++end;
+ if (end == len) return BUILD_ERR;
+ line->data = data + start;
+ line->len = end - start;
+ *pos = end + 1u;
+ return BUILD_OK;
+}
+
+static void copy_slice(char* dst, size_t cap, KitSlice s) {
+ (void)cap;
+ if (s.len) memcpy(dst, s.s, s.len);
+ dst[s.len] = '\0';
+}
+
+static int cstr_slice_cmp(const char* a, KitSlice b) {
+ size_t i = 0;
+ for (;;) {
+ int ac = a[i] ? (int)(unsigned char)a[i] : 0;
+ int bc = i < b.len ? (int)(unsigned char)b.s[i] : 0;
+ if (ac != bc) return ac < bc ? -1 : 1;
+ if (!ac) return 0;
+ ++i;
+ }
+}
+
+int build_defn_parse(const uint8_t* data, size_t len, BuildDefn* out, char* err,
+ size_t errcap) {
+ size_t pos = 0;
+ KitSlice line;
+
+ if (!data || !out) return set_err(err, errcap, "missing build definition");
+ if (out->cap_targets && !out->targets)
+ return set_err(err, errcap, "missing target storage");
+ out->bytes = data;
+ out->len = len;
+ out->n_targets = 0;
+
+ if (next_line(data, len, &pos, &line) != BUILD_OK ||
+ !kit_slice_eq_cstr(line, BUILD_DEFN_MAGIC))
+ return set_err(err, errcap, "bad build definition magic/version");
+
+ while (pos < len) {
+ KitSlice name, recipe;
+ BuildTargetDefn* t;
+
+ if (next_line(data, len, &pos, &line) != BUILD_OK)
+ return set_err(err, errcap, "unterminated target stanza");
+ if (line.len < sizeof("[target ]") || line.s[0] != '[' ||
+ memcmp(line.s, "[target ", sizeof("[target ") - 1u) != 0 ||
+ line.s[line.len - 1u] != ']')
+ return set_err(err, errcap, "expected target stanza");
+ name.s = line.s + sizeof("[target ") - 1u;
+ name.len = line.len - (sizeof("[target ") - 1u) - 1u;
+ if (!slice_valid_token(name, BUILD_TARGET_MAX))
+ return set_err(err, errcap, "invalid target name");
+ if (out->n_targets > 0 &&
+ cstr_slice_cmp(out->targets[out->n_targets - 1u].name, name) >= 0)
+ return set_err(err, errcap, "non-canonical target ordering");
+ if (out->n_targets >= out->cap_targets)
+ return set_err(err, errcap, "too many targets");
+
+ if (next_line(data, len, &pos, &line) != BUILD_OK)
+ return set_err(err, errcap, "missing recipe line");
+ if (line.len < sizeof("recipe ") ||
+ memcmp(line.s, "recipe ", sizeof("recipe ") - 1u) != 0)
+ return set_err(err, errcap, "expected recipe line");
+ recipe.s = line.s + sizeof("recipe ") - 1u;
+ recipe.len = line.len - (sizeof("recipe ") - 1u);
+ if (!rel_path_valid(recipe))
+ return set_err(err, errcap, "invalid recipe path");
+
+ t = &out->targets[out->n_targets++];
+ copy_slice(t->name, sizeof t->name, name);
+ copy_slice(t->recipe_path, sizeof t->recipe_path, recipe);
+ }
+ return BUILD_OK;
+}
+
+const BuildTargetDefn* build_defn_find(const BuildDefn* defn, KitSlice name) {
+ size_t lo, hi;
+ if (!defn || !slice_valid_token(name, BUILD_TARGET_MAX)) return NULL;
+ lo = 0;
+ hi = defn->n_targets;
+ while (lo < hi) {
+ size_t mid = lo + (hi - lo) / 2u;
+ int cmp = cstr_slice_cmp(defn->targets[mid].name, name);
+ if (cmp == 0) return &defn->targets[mid];
+ if (cmp < 0)
+ lo = mid + 1u;
+ else
+ hi = mid;
+ }
+ return NULL;
+}
diff --git a/src/build/defn.h b/src/build/defn.h
@@ -42,7 +42,9 @@ typedef struct BuildTargetDefn {
} BuildTargetDefn;
/* The parsed definition: an index of stanzas over caller-provided storage, plus
- * the raw bytes (borrowed) so the coordinator can hash the file as a source. */
+ * the raw bytes (borrowed) kept alive for the definition's lifetime. The
+ * definition file as a whole is not a source dependency; only each target's
+ * resolved recipe-id is. */
typedef struct BuildDefn {
const uint8_t* bytes; /* the whole definition file, borrowed */
size_t len;
diff --git a/src/build/protocol.c b/src/build/protocol.c
@@ -0,0 +1,355 @@
+#include "protocol.h"
+
+#include <string.h>
+
+typedef struct Cursor {
+ const uint8_t* in;
+ uint8_t* out;
+ size_t len;
+ size_t pos;
+ int err;
+} Cursor;
+
+static int text_valid(KitSlice s, size_t cap, int allow_space) {
+ size_t i;
+ if (s.len >= cap) return 0;
+ if (s.len && !s.s) return 0;
+ for (i = 0; i < s.len; ++i) {
+ unsigned char c = (unsigned char)s.s[i];
+ if (c == '\0' || c == '\n' || c == '\r') return 0;
+ if (!allow_space && (c == ' ' || c == '\t')) return 0;
+ }
+ return 1;
+}
+
+static void put_u8(Cursor* c, uint8_t v) {
+ if (c->pos + 1u > c->len) {
+ c->err = 1;
+ return;
+ }
+ c->out[c->pos++] = v;
+}
+
+static void put_u16(Cursor* c, uint16_t v) {
+ put_u8(c, (uint8_t)(v & 0xffu));
+ put_u8(c, (uint8_t)((v >> 8) & 0xffu));
+}
+
+static void put_u64(Cursor* c, uint64_t v) {
+ size_t i;
+ for (i = 0; i < 8u; ++i) put_u8(c, (uint8_t)((v >> (8u * i)) & 0xffu));
+}
+
+static void put_bytes(Cursor* c, const void* data, size_t n) {
+ if (n && !data) {
+ c->err = 1;
+ return;
+ }
+ if (c->pos > c->len || n > c->len - c->pos) {
+ c->err = 1;
+ return;
+ }
+ if (n) memcpy(c->out + c->pos, data, n);
+ c->pos += n;
+}
+
+static void put_slice(Cursor* c, KitSlice s) {
+ if (s.len > 0xffffu) {
+ c->err = 1;
+ return;
+ }
+ put_u16(c, (uint16_t)s.len);
+ put_bytes(c, s.data, s.len);
+}
+
+static uint8_t get_u8(Cursor* c) {
+ if (c->pos + 1u > c->len) {
+ c->err = 1;
+ return 0;
+ }
+ return c->in[c->pos++];
+}
+
+static uint16_t get_u16(Cursor* c) {
+ uint16_t lo = get_u8(c);
+ uint16_t hi = get_u8(c);
+ return (uint16_t)(lo | (uint16_t)(hi << 8));
+}
+
+static uint64_t get_u64(Cursor* c) {
+ uint64_t v = 0;
+ size_t i;
+ for (i = 0; i < 8u; ++i) v |= (uint64_t)get_u8(c) << (8u * i);
+ return v;
+}
+
+static void get_bytes(Cursor* c, uint8_t* out, size_t n) {
+ if (c->pos > c->len || n > c->len - c->pos) {
+ c->err = 1;
+ return;
+ }
+ if (n) memcpy(out, c->in + c->pos, n);
+ c->pos += n;
+}
+
+static KitSlice get_slice(Cursor* c) {
+ KitSlice s;
+ uint16_t n = get_u16(c);
+ if (c->pos > c->len || (size_t)n > c->len - c->pos) {
+ c->err = 1;
+ return KIT_SLICE_NULL;
+ }
+ s.data = c->in + c->pos;
+ s.len = n;
+ c->pos += n;
+ return s;
+}
+
+static int known_cmd(uint8_t cmd) {
+ return cmd == BUILD_CMD_CONFIG_GET || cmd == BUILD_CMD_SOURCE ||
+ cmd == BUILD_CMD_GLOB || cmd == BUILD_CMD_NEED ||
+ cmd == BUILD_CMD_NEED_SUBMIT || cmd == BUILD_CMD_NEED_AWAIT;
+}
+
+static int token_valid(KitSlice s, size_t cap) {
+ return s.len > 0 && text_valid(s, cap, 0);
+}
+
+static int valid_arg_for_cmd(uint8_t cmd, KitSlice s) {
+ if (cmd == BUILD_CMD_CONFIG_GET) return token_valid(s, BUILD_KEY_MAX);
+ if (cmd == BUILD_CMD_SOURCE) return token_valid(s, BUILD_PATH_MAX);
+ if (cmd == BUILD_CMD_GLOB) return token_valid(s, BUILD_PATTERN_MAX);
+ if (cmd == BUILD_CMD_NEED || cmd == BUILD_CMD_NEED_SUBMIT)
+ return token_valid(s, BUILD_TARGET_MAX);
+ return 1;
+}
+
+int build_proto_encode_req(const BuildReq* req, uint8_t* buf, size_t cap,
+ size_t* n) {
+ size_t i;
+ Cursor c;
+ if (!req || !buf || !n) return BUILD_ERR;
+ if (!known_cmd(req->cmd)) return BUILD_ERR;
+ c.in = NULL;
+ c.out = buf;
+ c.len = cap < BUILD_FRAME_MAX ? cap : BUILD_FRAME_MAX;
+ c.pos = 0;
+ c.err = 0;
+ put_u8(&c, req->cmd);
+ if (req->cmd == BUILD_CMD_CONFIG_GET || req->cmd == BUILD_CMD_SOURCE ||
+ req->cmd == BUILD_CMD_GLOB) {
+ if (!valid_arg_for_cmd(req->cmd, req->arg)) return BUILD_ERR;
+ put_slice(&c, req->arg);
+ } else if (req->cmd == BUILD_CMD_NEED ||
+ req->cmd == BUILD_CMD_NEED_SUBMIT) {
+ if (!valid_arg_for_cmd(req->cmd, req->arg)) return BUILD_ERR;
+ if (req->noverrides > 0xffffu || req->argc > 0xffffu) return BUILD_ERR;
+ if (req->noverrides && !req->overrides) return BUILD_ERR;
+ if (req->argc && !req->argv) return BUILD_ERR;
+ put_slice(&c, req->arg);
+ put_u16(&c, (uint16_t)req->noverrides);
+ for (i = 0; i < req->noverrides; ++i) {
+ if (!token_valid(req->overrides[i].key, BUILD_KEY_MAX) ||
+ !text_valid(req->overrides[i].value, BUILD_VAL_MAX, 1))
+ return BUILD_ERR;
+ put_slice(&c, req->overrides[i].key);
+ put_slice(&c, req->overrides[i].value);
+ }
+ put_u16(&c, (uint16_t)req->argc);
+ for (i = 0; i < req->argc; ++i) {
+ if (!text_valid(req->argv[i], BUILD_VAL_MAX, 1)) return BUILD_ERR;
+ put_slice(&c, req->argv[i]);
+ }
+ } else {
+ put_u64(&c, req->token);
+ }
+ if (c.err || c.pos > BUILD_FRAME_MAX) return BUILD_ERR;
+ *n = c.pos;
+ return BUILD_OK;
+}
+
+int build_proto_decode_req(const uint8_t* buf, size_t len, BuildReq* out,
+ KitBuildKV* ovr_storage, size_t ovr_cap,
+ KitSlice* argv_storage, size_t argv_cap) {
+ size_t i;
+ Cursor c;
+ if (!buf || !out || len == 0 || len > BUILD_FRAME_MAX) return BUILD_ERR;
+ memset(out, 0, sizeof *out);
+ c.in = buf;
+ c.out = NULL;
+ c.len = len;
+ c.pos = 0;
+ c.err = 0;
+ out->cmd = get_u8(&c);
+ if (!known_cmd(out->cmd)) return BUILD_ERR;
+ if (out->cmd == BUILD_CMD_CONFIG_GET || out->cmd == BUILD_CMD_SOURCE ||
+ out->cmd == BUILD_CMD_GLOB) {
+ out->arg = get_slice(&c);
+ if (!valid_arg_for_cmd(out->cmd, out->arg)) return BUILD_ERR;
+ } else if (out->cmd == BUILD_CMD_NEED ||
+ out->cmd == BUILD_CMD_NEED_SUBMIT) {
+ uint16_t noverrides, argc;
+ out->arg = get_slice(&c);
+ if (!valid_arg_for_cmd(out->cmd, out->arg)) return BUILD_ERR;
+ noverrides = get_u16(&c);
+ if ((size_t)noverrides > ovr_cap) return BUILD_ERR;
+ if (noverrides && !ovr_storage) return BUILD_ERR;
+ out->overrides = ovr_storage;
+ out->noverrides = noverrides;
+ for (i = 0; i < out->noverrides; ++i) {
+ ovr_storage[i].key = get_slice(&c);
+ ovr_storage[i].value = get_slice(&c);
+ if (!token_valid(ovr_storage[i].key, BUILD_KEY_MAX) ||
+ !text_valid(ovr_storage[i].value, BUILD_VAL_MAX, 1))
+ return BUILD_ERR;
+ }
+ argc = get_u16(&c);
+ if ((size_t)argc > argv_cap) return BUILD_ERR;
+ if (argc && !argv_storage) return BUILD_ERR;
+ out->argv = argv_storage;
+ out->argc = argc;
+ for (i = 0; i < out->argc; ++i) {
+ argv_storage[i] = get_slice(&c);
+ if (!text_valid(argv_storage[i], BUILD_VAL_MAX, 1)) return BUILD_ERR;
+ }
+ } else {
+ out->token = get_u64(&c);
+ }
+ if (c.err || c.pos != c.len) return BUILD_ERR;
+ return BUILD_OK;
+}
+
+static int status_known(uint8_t status) {
+ return status == BUILD_RESP_OK || status == BUILD_RESP_UNSET ||
+ status == BUILD_RESP_ERROR || status == BUILD_RESP_GLOB_END ||
+ status == BUILD_RESP_ABSENT;
+}
+
+int build_proto_encode_resp(const BuildReq* for_cmd, const BuildResp* resp,
+ uint8_t* buf, size_t cap, size_t* n) {
+ Cursor c;
+ if (!for_cmd || !resp || !buf || !n) return BUILD_ERR;
+ if (!known_cmd(for_cmd->cmd) || !status_known(resp->status)) return BUILD_ERR;
+ c.in = NULL;
+ c.out = buf;
+ c.len = cap < BUILD_FRAME_MAX ? cap : BUILD_FRAME_MAX;
+ c.pos = 0;
+ c.err = 0;
+ put_u8(&c, resp->status);
+ if (resp->status == BUILD_RESP_ERROR) {
+ if (!text_valid(resp->text, BUILD_FRAME_MAX, 1)) return BUILD_ERR;
+ put_u16(&c, resp->error_status);
+ put_slice(&c, resp->text);
+ } else if (for_cmd->cmd == BUILD_CMD_CONFIG_GET) {
+ if (resp->status == BUILD_RESP_UNSET) {
+ } else if (resp->status == BUILD_RESP_OK) {
+ if (!text_valid(resp->text, BUILD_VAL_MAX, 1)) return BUILD_ERR;
+ put_slice(&c, resp->text);
+ } else {
+ return BUILD_ERR;
+ }
+ } else if (for_cmd->cmd == BUILD_CMD_SOURCE) {
+ if (resp->status == BUILD_RESP_ABSENT) {
+ } else if (resp->status == BUILD_RESP_OK) {
+ if (!token_valid(resp->text, BUILD_PATH_MAX)) return BUILD_ERR;
+ put_bytes(&c, resp->id, BUILD_HASH_LEN);
+ put_slice(&c, resp->text);
+ } else {
+ return BUILD_ERR;
+ }
+ } else if (for_cmd->cmd == BUILD_CMD_GLOB) {
+ if (resp->status == BUILD_RESP_GLOB_END) {
+ } else if (resp->status == BUILD_RESP_OK) {
+ if (resp->text.len == 0) {
+ put_u16(&c, 0);
+ } else {
+ if (!token_valid(resp->text, BUILD_PATH_MAX)) return BUILD_ERR;
+ put_u16(&c, 1);
+ put_slice(&c, resp->text);
+ }
+ } else {
+ return BUILD_ERR;
+ }
+ } else if (for_cmd->cmd == BUILD_CMD_NEED ||
+ for_cmd->cmd == BUILD_CMD_NEED_AWAIT) {
+ if (resp->status != BUILD_RESP_OK ||
+ !token_valid(resp->text, BUILD_PATH_MAX))
+ return BUILD_ERR;
+ put_bytes(&c, resp->id, BUILD_HASH_LEN);
+ put_slice(&c, resp->text);
+ } else if (for_cmd->cmd == BUILD_CMD_NEED_SUBMIT) {
+ if (resp->status != BUILD_RESP_OK) return BUILD_ERR;
+ put_u64(&c, resp->token);
+ } else {
+ return BUILD_ERR;
+ }
+ if (c.err || c.pos > BUILD_FRAME_MAX) return BUILD_ERR;
+ *n = c.pos;
+ return BUILD_OK;
+}
+
+int build_proto_decode_resp(const uint8_t* buf, size_t len, uint8_t cmd,
+ BuildResp* out, BuildProtoGlobFn glob_cb,
+ void* glob_user) {
+ Cursor c;
+ if (!buf || !out || len == 0 || len > BUILD_FRAME_MAX || !known_cmd(cmd))
+ return BUILD_ERR;
+ memset(out, 0, sizeof *out);
+ c.in = buf;
+ c.out = NULL;
+ c.len = len;
+ c.pos = 0;
+ c.err = 0;
+ out->status = get_u8(&c);
+ if (!status_known(out->status)) return BUILD_ERR;
+ if (out->status == BUILD_RESP_ERROR) {
+ out->error_status = get_u16(&c);
+ out->text = get_slice(&c);
+ if (!text_valid(out->text, BUILD_FRAME_MAX, 1)) return BUILD_ERR;
+ } else if (cmd == BUILD_CMD_CONFIG_GET) {
+ if (out->status == BUILD_RESP_UNSET) {
+ } else if (out->status == BUILD_RESP_OK) {
+ out->text = get_slice(&c);
+ if (!text_valid(out->text, BUILD_VAL_MAX, 1)) return BUILD_ERR;
+ } else {
+ return BUILD_ERR;
+ }
+ } else if (cmd == BUILD_CMD_SOURCE) {
+ if (out->status == BUILD_RESP_ABSENT) {
+ } else if (out->status == BUILD_RESP_OK) {
+ get_bytes(&c, out->id, BUILD_HASH_LEN);
+ out->text = get_slice(&c);
+ if (!token_valid(out->text, BUILD_PATH_MAX)) return BUILD_ERR;
+ } else {
+ return BUILD_ERR;
+ }
+ } else if (cmd == BUILD_CMD_GLOB) {
+ if (out->status == BUILD_RESP_GLOB_END) {
+ } else if (out->status == BUILD_RESP_OK) {
+ uint16_t npaths = get_u16(&c);
+ uint16_t i;
+ int stopped = 0;
+ for (i = 0; i < npaths; ++i) {
+ KitSlice path = get_slice(&c);
+ if (!token_valid(path, BUILD_PATH_MAX)) return BUILD_ERR;
+ if (!stopped && glob_cb) {
+ if (glob_cb(glob_user, path)) stopped = 1;
+ }
+ }
+ } else {
+ return BUILD_ERR;
+ }
+ } else if (cmd == BUILD_CMD_NEED || cmd == BUILD_CMD_NEED_AWAIT) {
+ if (out->status != BUILD_RESP_OK) return BUILD_ERR;
+ get_bytes(&c, out->id, BUILD_HASH_LEN);
+ out->text = get_slice(&c);
+ if (!token_valid(out->text, BUILD_PATH_MAX)) return BUILD_ERR;
+ } else if (cmd == BUILD_CMD_NEED_SUBMIT) {
+ if (out->status != BUILD_RESP_OK) return BUILD_ERR;
+ out->token = get_u64(&c);
+ } else {
+ return BUILD_ERR;
+ }
+ if (c.err || c.pos != c.len) return BUILD_ERR;
+ return BUILD_OK;
+}
diff --git a/src/build/protocol.h b/src/build/protocol.h
@@ -102,7 +102,7 @@ int build_proto_decode_req(const uint8_t* buf, size_t len, BuildReq* out,
/* Reports one glob match while decoding a glob response. Return non-zero to
* stop. */
-typedef int (*BuildProtoGlobFn)(void* user, const char* path);
+typedef int (*BuildProtoGlobFn)(void* user, KitSlice path);
int build_proto_decode_resp(const uint8_t* buf, size_t len, uint8_t cmd,
BuildResp* out, BuildProtoGlobFn glob_cb,
diff --git a/src/build/remote.c b/src/build/remote.c
@@ -0,0 +1,344 @@
+#include "remote.h"
+
+#include <stdio.h>
+#include <string.h>
+
+typedef struct RenderedArgv {
+ KitSlice* argv;
+ size_t argc;
+ char* storage;
+ size_t storage_size;
+} RenderedArgv;
+
+typedef struct RenderTokenSet {
+ const char* kind;
+ const char* pp;
+ const char* id;
+ const char* out;
+} RenderTokenSet;
+
+static int ascii_space(char c) {
+ return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' ||
+ c == '\v';
+}
+
+static int add_size(size_t* v, size_t n) {
+ if (!v || *v > (size_t)-1 - n) return BUILD_ERR;
+ *v += n;
+ return BUILD_OK;
+}
+
+static int path_join(char* out, size_t cap, KitSlice dir, const char* leaf) {
+ size_t nd, nl;
+ int slash;
+ if (!out || cap == 0u || !dir.s || dir.len == 0u || !leaf) return BUILD_ERR;
+ nd = dir.len;
+ nl = strlen(leaf);
+ slash = dir.s[nd - 1u] != '/';
+ if (nd + (slash ? 1u : 0u) + nl + 1u > cap) return BUILD_ERR;
+ memcpy(out, dir.s, nd);
+ if (slash) out[nd++] = '/';
+ memcpy(out + nd, leaf, nl);
+ out[nd + nl] = '\0';
+ return BUILD_OK;
+}
+
+static const char* remote_kind_name(BuildRemoteKind kind) {
+ if (kind == BUILD_REMOTE_BLOB) return "blob";
+ if (kind == BUILD_REMOTE_TREE) return "tree";
+ return NULL;
+}
+
+static int token_match(const char* p, const char* end, const char* lit,
+ const char* repl, const char** repl_out,
+ size_t* lit_len_out, size_t* repl_len_out) {
+ size_t n;
+ if (!lit || !repl) return 0;
+ n = strlen(lit);
+ if ((size_t)(end - p) < n || memcmp(p, lit, n) != 0) return 0;
+ *repl_out = repl;
+ *lit_len_out = n;
+ *repl_len_out = strlen(repl);
+ return 1;
+}
+
+static int find_token(const char* p, const char* end,
+ const RenderTokenSet* toks, const char** repl_out,
+ size_t* lit_len_out, size_t* repl_len_out) {
+ return token_match(p, end, "{kind}", toks->kind, repl_out, lit_len_out,
+ repl_len_out) ||
+ token_match(p, end, "{pp}", toks->pp, repl_out, lit_len_out,
+ repl_len_out) ||
+ token_match(p, end, "{id}", toks->id, repl_out, lit_len_out,
+ repl_len_out) ||
+ token_match(p, end, "{out}", toks->out, repl_out, lit_len_out,
+ repl_len_out);
+}
+
+static int emit_bytes(char* storage, size_t* off, size_t cap, const char* data,
+ size_t len) {
+ if (!off || (!data && len)) return BUILD_ERR;
+ if (storage) {
+ if (*off > cap || len > cap - *off) return BUILD_ERR;
+ if (len) memcpy(storage + *off, data, len);
+ }
+ return add_size(off, len);
+}
+
+static int render_pass(KitSlice tmpl, const RenderTokenSet* toks,
+ KitSlice* argv, size_t* argc_io, char* storage,
+ size_t storage_cap, size_t* storage_len_out) {
+ const char* p;
+ const char* end;
+ size_t argc = 0u;
+ size_t off = 0u;
+ size_t word_start = 0u;
+ size_t word_len = 0u;
+ int in_word = 0;
+ char quote = 0;
+
+ if (!tmpl.s || !toks || !argc_io || !storage_len_out) return BUILD_ERR;
+ p = tmpl.s;
+ end = tmpl.s + tmpl.len;
+ while (p < end) {
+ char c = *p;
+ if (quote == 0 && ascii_space(c)) {
+ if (in_word) {
+ if (argv) {
+ if (argc >= *argc_io || off >= storage_cap) return BUILD_ERR;
+ storage[off] = '\0';
+ argv[argc].s = storage + word_start;
+ argv[argc].len = word_len;
+ }
+ if (add_size(&off, 1u) != BUILD_OK) return BUILD_ERR;
+ ++argc;
+ in_word = 0;
+ word_len = 0u;
+ }
+ ++p;
+ continue;
+ }
+
+ if (!in_word) {
+ in_word = 1;
+ word_start = off;
+ word_len = 0u;
+ }
+
+ if (quote == 0 && (c == '\'' || c == '"')) {
+ quote = c;
+ ++p;
+ continue;
+ }
+ if (quote != 0 && c == quote) {
+ quote = 0;
+ ++p;
+ continue;
+ }
+ if ((quote == 0 || quote == '"') && c == '\\') {
+ if (p + 1 >= end) return BUILD_ERR;
+ if (emit_bytes(storage, &off, storage_cap, p + 1, 1u) != BUILD_OK)
+ return BUILD_ERR;
+ ++word_len;
+ p += 2;
+ continue;
+ }
+
+ {
+ const char* repl = NULL;
+ size_t lit_len = 0u;
+ size_t repl_len = 0u;
+ if (find_token(p, end, toks, &repl, &lit_len, &repl_len)) {
+ if (emit_bytes(storage, &off, storage_cap, repl, repl_len) != BUILD_OK)
+ return BUILD_ERR;
+ if (add_size(&word_len, repl_len) != BUILD_OK) return BUILD_ERR;
+ p += lit_len;
+ } else {
+ if (emit_bytes(storage, &off, storage_cap, p, 1u) != BUILD_OK)
+ return BUILD_ERR;
+ ++word_len;
+ ++p;
+ }
+ }
+ }
+
+ if (quote != 0) return BUILD_ERR;
+ if (in_word) {
+ if (argv) {
+ if (argc >= *argc_io || off >= storage_cap) return BUILD_ERR;
+ storage[off] = '\0';
+ argv[argc].s = storage + word_start;
+ argv[argc].len = word_len;
+ }
+ if (add_size(&off, 1u) != BUILD_OK) return BUILD_ERR;
+ ++argc;
+ }
+ if (argv && argc != *argc_io) return BUILD_ERR;
+ *argc_io = argc;
+ *storage_len_out = off;
+ return BUILD_OK;
+}
+
+static void rendered_argv_free(const KitContext* ctx, RenderedArgv* r) {
+ if (!ctx || !ctx->heap || !r) return;
+ if (r->storage) ctx->heap->free(ctx->heap, r->storage, r->storage_size);
+ if (r->argv) ctx->heap->free(ctx->heap, r->argv, r->argc * sizeof *r->argv);
+ r->argv = NULL;
+ r->argc = 0u;
+ r->storage = NULL;
+ r->storage_size = 0u;
+}
+
+static int render_argv(const KitContext* ctx, KitSlice tmpl,
+ const RenderTokenSet* toks, RenderedArgv* out) {
+ size_t argc = 0u;
+ size_t storage_len = 0u;
+ if (!ctx || !ctx->heap || !out) return BUILD_ERR;
+ memset(out, 0, sizeof *out);
+ if (render_pass(tmpl, toks, NULL, &argc, NULL, 0u, &storage_len) !=
+ BUILD_OK ||
+ argc == 0u)
+ return BUILD_ERR;
+ out->argv = (KitSlice*)ctx->heap->alloc(ctx->heap, argc * sizeof *out->argv,
+ _Alignof(KitSlice));
+ if (!out->argv) return BUILD_ERR;
+ out->storage =
+ (char*)ctx->heap->alloc(ctx->heap, storage_len, _Alignof(char));
+ if (!out->storage) {
+ rendered_argv_free(ctx, out);
+ return BUILD_ERR;
+ }
+ out->argc = argc;
+ out->storage_size = storage_len;
+ if (render_pass(tmpl, toks, out->argv, &argc, out->storage, storage_len,
+ &storage_len) != BUILD_OK) {
+ rendered_argv_free(ctx, out);
+ return BUILD_ERR;
+ }
+ return BUILD_OK;
+}
+
+static int read_fetched_file(const KitContext* ctx, const char* path,
+ KitFileData* out) {
+ if (!ctx || !ctx->file_io || !ctx->file_io->read_all || !path || !out)
+ return BUILD_ERR;
+ out->data = NULL;
+ out->size = 0u;
+ out->token = NULL;
+ return ctx->file_io->read_all(ctx->file_io->user, path, out) == KIT_OK
+ ? BUILD_OK
+ : BUILD_ERR;
+}
+
+static void release_fetched_file(const KitContext* ctx, KitFileData* fd) {
+ if (!ctx || !ctx->file_io || !fd) return;
+ if (fd->data && ctx->file_io->release)
+ ctx->file_io->release(ctx->file_io->user, fd);
+ fd->data = NULL;
+ fd->size = 0u;
+ fd->token = NULL;
+}
+
+static int verify_bytes(const uint8_t* data, size_t len,
+ const uint8_t id[BUILD_HASH_LEN], KitBlobInfo* out) {
+ KitBlobInfo bi;
+ if (!id || (!data && len)) return BUILD_ERR;
+ kit_blob_info(&bi, data, len);
+ if (!build_id_eq(bi.id, id)) return BUILD_ERR;
+ if (out) *out = bi;
+ return BUILD_OK;
+}
+
+static int run_fetch(const KitBuildExec* exec, const KitSlice* argv,
+ size_t argc, KitSlice cwd) {
+ KitBuildProc* proc = NULL;
+ int exit_code = 1;
+ if (!exec || !exec->spawn || !exec->wait || !argv || argc == 0u)
+ return BUILD_ERR;
+ if (exec->spawn(exec->user, argv, argc, NULL, 0u, cwd, &proc) != 0 || !proc)
+ return BUILD_ERR;
+ if (exec->wait(exec->user, proc, &exit_code) != 0) return BUILD_ERR;
+ return exit_code == 0 ? BUILD_OK : BUILD_ERR;
+}
+
+int build_remote_fetch(const KitContext* ctx, const KitBuildExec* exec,
+ const KitBuildObjectRemote* remotes, size_t nremotes,
+ KitCas* cas, KitSlice tmp_dir, BuildRemoteKind kind,
+ const uint8_t id[BUILD_HASH_LEN]) {
+ char hex[BUILD_HEX_LEN];
+ char pp[BUILD_PP_LEN + 1u];
+ char leaf[16u + BUILD_HEX_LEN];
+ char out_path[BUILD_PATH_MAX];
+ const char* kind_s;
+ size_t i;
+
+ if (!ctx || !ctx->heap || !exec || !remotes || nremotes == 0u || !cas || !id)
+ return BUILD_ERR;
+ kind_s = remote_kind_name(kind);
+ if (!kind_s) return BUILD_ERR;
+ kit_hex_encode(hex, id, BUILD_HASH_LEN);
+ pp[0] = hex[0];
+ pp[1] = hex[1];
+ pp[2] = '\0';
+ if (snprintf(leaf, sizeof leaf, "fetch-%s-%s", kind_s, hex) >=
+ (int)sizeof leaf)
+ return BUILD_ERR;
+ if (path_join(out_path, sizeof out_path, tmp_dir, leaf) != BUILD_OK)
+ return BUILD_ERR;
+
+ for (i = 0u; i < nremotes; ++i) {
+ RenderTokenSet toks;
+ RenderedArgv argv;
+ KitFileData fd;
+ KitBlobInfo bi;
+ int verified;
+
+ toks.kind = kind_s;
+ toks.pp = pp;
+ toks.id = hex;
+ toks.out = out_path;
+ memset(&argv, 0, sizeof argv);
+ if (render_argv(ctx, remotes[i].fetch_argv_template, &toks, &argv) !=
+ BUILD_OK) {
+ build_diagf(ctx, "remote fetch: bad argv template");
+ continue;
+ }
+ if (run_fetch(exec, argv.argv, argv.argc, tmp_dir) != BUILD_OK) {
+ rendered_argv_free(ctx, &argv);
+ continue;
+ }
+ rendered_argv_free(ctx, &argv);
+
+ if (read_fetched_file(ctx, out_path, &fd) != BUILD_OK) continue;
+ verified = verify_bytes(fd.data, fd.size, id, &bi);
+ if (verified != BUILD_OK) {
+ release_fetched_file(ctx, &fd);
+ build_diagf(ctx, "remote fetch: %s %s failed content verification",
+ kind_s, hex);
+ continue;
+ }
+
+ if (kind == BUILD_REMOTE_BLOB) {
+ KitBlobInfo stored;
+ if (kit_cas_add_blob(cas, fd.data, fd.size, &stored) == KIT_OK &&
+ build_id_eq(stored.id, id)) {
+ release_fetched_file(ctx, &fd);
+ return BUILD_OK;
+ }
+ release_fetched_file(ctx, &fd);
+ continue;
+ }
+
+ {
+ uint8_t stored_tree[BUILD_HASH_LEN];
+ if (kit_cas_add_tree_manifest(cas, fd.data, fd.size, stored_tree) ==
+ KIT_OK &&
+ build_id_eq(stored_tree, id)) {
+ release_fetched_file(ctx, &fd);
+ return BUILD_OK;
+ }
+ }
+ release_fetched_file(ctx, &fd);
+ }
+ return BUILD_ERR;
+}
diff --git a/src/build/resolve.c b/src/build/resolve.c
@@ -0,0 +1,152 @@
+#include "resolve.h"
+
+#include "remote.h"
+
+#include <stdio.h>
+#include <string.h>
+
+static int path_join2(char* out, size_t cap, const char* a, const char* b) {
+ size_t na, nb;
+ int need_sep;
+ if (!out || cap == 0u || !a || !b) return BUILD_ERR;
+ na = strlen(a);
+ nb = strlen(b);
+ need_sep = na > 0u && a[na - 1u] != '/';
+ if (na + (need_sep ? 1u : 0u) + nb + 1u > cap) return BUILD_ERR;
+ memcpy(out, a, na);
+ if (need_sep) out[na++] = '/';
+ memcpy(out + na, b, nb);
+ out[na + nb] = '\0';
+ return BUILD_OK;
+}
+
+static int target_copy(KitSlice target, char out[BUILD_TARGET_MAX]) {
+ if (!target.s || target.len == 0u || target.len >= BUILD_TARGET_MAX)
+ return BUILD_ERR;
+ memcpy(out, target.s, target.len);
+ out[target.len] = '\0';
+ return BUILD_OK;
+}
+
+static int chain_has(const BuildChainFrame* f, KitSlice target,
+ const uint8_t config_id[BUILD_HASH_LEN],
+ const uint8_t argv_id[BUILD_HASH_LEN]) {
+ for (; f; f = f->parent) {
+ if (strlen(f->target) == target.len &&
+ memcmp(f->target, target.s, target.len) == 0 &&
+ build_id_eq(f->config_id, config_id) && build_id_eq(f->argv_id, argv_id))
+ return 1;
+ }
+ return 0;
+}
+
+int build_chain_extend(KitBuildCoordinator* c, const BuildChainFrame* parent,
+ KitSlice target, const uint8_t config_id[BUILD_HASH_LEN],
+ const uint8_t argv_id[BUILD_HASH_LEN],
+ const BuildChainFrame** out, char* err, size_t errcap) {
+ BuildChainFrame* f;
+ if (!c || !config_id || !argv_id || !out) return BUILD_ERR;
+ *out = NULL;
+ if (chain_has(parent, target, config_id, argv_id)) {
+ if (err && errcap) snprintf(err, errcap, "dependency cycle at %.*s",
+ KIT_SLICE_ARG(target));
+ return BUILD_ERR;
+ }
+ f = (BuildChainFrame*)c->ctx->heap->alloc(c->ctx->heap, sizeof *f,
+ _Alignof(BuildChainFrame));
+ if (!f) return BUILD_ERR;
+ memset(f, 0, sizeof *f);
+ f->parent = parent;
+ if (target_copy(target, f->target) != BUILD_OK) return BUILD_ERR;
+ memcpy(f->config_id, config_id, BUILD_HASH_LEN);
+ memcpy(f->argv_id, argv_id, BUILD_HASH_LEN);
+ *out = f;
+ return BUILD_OK;
+}
+
+int build_materialize(KitBuildCoordinator* c,
+ const uint8_t tree_id[BUILD_HASH_LEN], char* path_out,
+ size_t cap) {
+ char tmp_dir[BUILD_PATH_MAX];
+ if (!c || !tree_id || !path_out) return BUILD_ERR;
+ if (build_store_cache_lookup(&c->store, tree_id, path_out, cap) == BUILD_OK)
+ return BUILD_OK;
+ if (build_store_cache_materialize(&c->store, tree_id, path_out, cap) ==
+ BUILD_OK)
+ return BUILD_OK;
+ if (c->opts.n_object_remotes && c->host.exec) {
+ if (path_join2(tmp_dir, sizeof tmp_dir, c->store.root, "tmp") ==
+ BUILD_OK &&
+ build_remote_fetch(c->ctx, c->host.exec, c->opts.object_remotes,
+ c->opts.n_object_remotes, c->cas,
+ kit_slice_cstr(tmp_dir), BUILD_REMOTE_TREE,
+ tree_id) == BUILD_OK) {
+ build_coord_stat_bump(c, BUILD_STAT_OBJECT_FETCH);
+ if (build_store_cache_materialize(&c->store, tree_id, path_out, cap) ==
+ BUILD_OK)
+ return BUILD_OK;
+ }
+ }
+ build_coord_stat_bump(c, BUILD_STAT_MATERIALIZE_MISS);
+ return BUILD_ERR;
+}
+
+int build_resolve(KitBuildCoordinator* c, KitSlice target, const BuildConfig* cfg,
+ const BuildArgv* argv, const BuildChainFrame* chain,
+ BuildResolved* out) {
+ uint8_t config_id[BUILD_HASH_LEN], argv_id[BUILD_HASH_LEN];
+ const BuildChainFrame* child;
+ char err[128];
+ (void)out;
+ if (!c || !cfg || !argv) return BUILD_ERR;
+ if (build_config_id(c->ctx->heap, cfg, config_id) != BUILD_OK ||
+ build_argv_id(c->ctx->heap, argv, argv_id) != BUILD_OK)
+ return BUILD_ERR;
+ if (build_chain_extend(c, chain, target, config_id, argv_id, &child, err,
+ sizeof err) != BUILD_OK) {
+ build_diagf(c->ctx, "build: %s", err);
+ return BUILD_ERR;
+ }
+ (void)child;
+ build_diagf(c->ctx, "build: resolver/runner is not implemented yet");
+ return BUILD_ERR;
+}
+
+int build_dispatch(KitBuildCoordinator* c, KitSlice target,
+ const BuildConfig* cfg, const BuildArgv* argv,
+ const BuildChainFrame* chain,
+ BuildTargetFuture** out_future, char* err, size_t errcap) {
+ uint8_t config_id[BUILD_HASH_LEN], argv_id[BUILD_HASH_LEN];
+ int fresh;
+ (void)chain;
+ if (!c || !cfg || !argv || !out_future) return BUILD_ERR;
+ if (build_config_id(c->ctx->heap, cfg, config_id) != BUILD_OK ||
+ build_argv_id(c->ctx->heap, argv, argv_id) != BUILD_OK)
+ return BUILD_ERR;
+ if (build_coord_target_intern(c, target, config_id, argv_id, out_future,
+ &fresh) != BUILD_OK)
+ return BUILD_ERR;
+ (void)fresh;
+ build_coord_target_fail(c, *out_future);
+ if (err && errcap) snprintf(err, errcap, "resolver/runner is not implemented");
+ return BUILD_ERR;
+}
+
+int build_leafset_union(KitBuildCoordinator* c, const BuildLeafSet* direct,
+ const BuildLeafSet* const* children, size_t nchildren,
+ const BuildLeafSet** out) {
+ (void)c;
+ (void)direct;
+ (void)children;
+ (void)nchildren;
+ (void)out;
+ return BUILD_ERR;
+}
+
+int build_leafset_refresh(KitBuildCoordinator* c, const BuildLeafSet* leafset,
+ int* all_match) {
+ (void)c;
+ (void)leafset;
+ if (all_match) *all_match = 0;
+ return BUILD_OK;
+}
diff --git a/src/build/runner.c b/src/build/runner.c
@@ -0,0 +1,40 @@
+#include "runner.h"
+
+int build_run_recipe(KitBuildCoordinator* c, KitSlice target,
+ const BuildConfig* cfg, const BuildArgv* argv,
+ const BuildChainFrame* chain, BuildResolved* out) {
+ (void)target;
+ (void)cfg;
+ (void)argv;
+ (void)chain;
+ (void)out;
+ if (c) build_diagf(c->ctx, "build: recipe runner is not implemented yet");
+ return BUILD_ERR;
+}
+
+int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn,
+ KitSlice target, const BuildConfig* cfg,
+ const BuildChainFrame* chain, BuildDepLog* log) {
+ (void)c;
+ (void)conn;
+ (void)target;
+ (void)cfg;
+ (void)chain;
+ (void)log;
+ return BUILD_ERR;
+}
+
+int build_runner_record_traces(KitBuildCoordinator* c, KitSlice target,
+ const BuildConfig* cfg, const BuildArgv* argv,
+ const BuildDepLog* log,
+ const uint8_t output[BUILD_HASH_LEN],
+ const BuildLeafSet** out_leafset) {
+ (void)c;
+ (void)target;
+ (void)cfg;
+ (void)argv;
+ (void)log;
+ (void)output;
+ (void)out_leafset;
+ return BUILD_ERR;
+}
diff --git a/src/build/store.c b/src/build/store.c
@@ -0,0 +1,450 @@
+#include "store.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#undef KIT_TRACE_MODULE
+#define KIT_TRACE_MODULE "build/store"
+
+static KitSlice str_slice(const char* s) { return kit_slice_cstr(s); }
+
+static int path_set(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 path_join2(char* out, size_t cap, const char* a, const char* b) {
+ size_t na, nb;
+ int need_sep;
+ if (!out || cap == 0u || !a || !b) return BUILD_ERR;
+ na = strlen(a);
+ nb = strlen(b);
+ need_sep = na > 0u && a[na - 1u] != '/';
+ if (na + (need_sep ? 1u : 0u) + nb + 1u > cap) return BUILD_ERR;
+ memcpy(out, a, na);
+ if (need_sep) out[na++] = '/';
+ memcpy(out + na, b, nb);
+ out[na + nb] = '\0';
+ return BUILD_OK;
+}
+
+static int path_parent(const char* path, char* out, size_t cap) {
+ size_t n, i;
+ if (!path || !out || cap == 0u) return BUILD_ERR;
+ n = strlen(path);
+ for (i = n; i > 0u; --i) {
+ if (path[i - 1u] == '/') {
+ size_t len = i - 1u;
+ if (len == 0u) len = 1u;
+ if (len + 1u > cap) return BUILD_ERR;
+ memcpy(out, path, len);
+ out[len] = '\0';
+ return BUILD_OK;
+ }
+ }
+ if (cap < 2u) return BUILD_ERR;
+ out[0] = '.';
+ out[1] = '\0';
+ return BUILD_OK;
+}
+
+static int id_path(const BuildStore* s, const char* kind,
+ const uint8_t id[BUILD_HASH_LEN], char* out, size_t cap) {
+ char hex[BUILD_HEX_LEN];
+ char pp[BUILD_PP_LEN + 1u];
+ char rel[BUILD_PATH_MAX];
+ kit_hex_encode(hex, id, BUILD_HASH_LEN);
+ pp[0] = hex[0];
+ pp[1] = hex[1];
+ pp[2] = '\0';
+ if (snprintf(rel, sizeof rel, "%s/%s/%s", kind, pp, hex) >=
+ (int)sizeof rel)
+ return BUILD_ERR;
+ return path_join2(out, cap, s->root, rel);
+}
+
+static int ensure_dir(const BuildStore* s, const char* path) {
+ if (!s || !s->cas_host || !s->cas_host->mkdir_p || !path) return BUILD_ERR;
+ return s->cas_host->mkdir_p(s->cas_host->user, path) == 0 ? BUILD_OK
+ : BUILD_ERR;
+}
+
+static int ensure_parent(const BuildStore* s, const char* path) {
+ char parent[BUILD_PATH_MAX];
+ if (path_parent(path, parent, sizeof parent) != BUILD_OK) return BUILD_ERR;
+ return ensure_dir(s, parent);
+}
+
+static int sync_path(const BuildStore* s, const char* path) {
+ if (!s || !s->io || !s->io->sync_path || !path) return BUILD_OK;
+ return s->io->sync_path(s->io->user, str_slice(path)) == 0 ? BUILD_OK
+ : BUILD_ERR;
+}
+
+static int remove_path(const BuildStore* s, const char* path, int recursive) {
+ if (!s || !s->io || !s->io->remove || !path) return BUILD_ERR;
+ return s->io->remove(s->io->user, str_slice(path), recursive) == 0
+ ? BUILD_OK
+ : BUILD_ERR;
+}
+
+static int make_tmp_dir(const BuildStore* s, char* out, size_t cap) {
+ char tmp_parent[BUILD_PATH_MAX];
+ if (!s || !s->io || !s->io->make_temp_dir) return BUILD_ERR;
+ if (path_join2(tmp_parent, sizeof tmp_parent, s->root, "tmp") != BUILD_OK)
+ return BUILD_ERR;
+ if (ensure_dir(s, tmp_parent) != BUILD_OK) return BUILD_ERR;
+ if (s->io->make_temp_dir(s->io->user, str_slice(tmp_parent), out, cap) != 0)
+ return BUILD_ERR;
+ return BUILD_OK;
+}
+
+static int write_file(const BuildStore* s, const char* path,
+ const uint8_t* data, size_t len) {
+ const KitFileIO* fio;
+ KitWriter* w = NULL;
+ KitStatus st;
+ if (!s || !s->cas_host || !s->cas_host->file_io || !path) return BUILD_ERR;
+ fio = s->cas_host->file_io;
+ if (!fio->open_writer) return BUILD_ERR;
+ if (fio->open_writer(fio->user, path, &w) != KIT_OK || !w) return BUILD_ERR;
+ st = len ? kit_writer_write(w, data, len) : KIT_OK;
+ if (st == KIT_OK) st = kit_writer_status(w);
+ kit_writer_close(w);
+ return st == KIT_OK ? BUILD_OK : BUILD_ERR;
+}
+
+static int atomic_write_file(BuildStore* s, const char* final_path,
+ const uint8_t* data, size_t len) {
+ char tmp_dir[BUILD_PATH_MAX];
+ char tmp_file[BUILD_PATH_MAX];
+ char parent[BUILD_PATH_MAX];
+ int ok = BUILD_ERR;
+ if (!s || !s->io || !s->io->rename || !final_path) return BUILD_ERR;
+ if (ensure_parent(s, final_path) != BUILD_OK) return BUILD_ERR;
+ if (make_tmp_dir(s, tmp_dir, sizeof tmp_dir) != BUILD_OK) return BUILD_ERR;
+ if (path_join2(tmp_file, sizeof tmp_file, tmp_dir, "body") != BUILD_OK)
+ goto out;
+ if (write_file(s, tmp_file, data, len) != BUILD_OK) goto out;
+ if (sync_path(s, tmp_file) != BUILD_OK) goto out;
+ if (sync_path(s, tmp_dir) != BUILD_OK) goto out;
+ if (s->io->rename(s->io->user, str_slice(tmp_file), str_slice(final_path)) !=
+ 0)
+ goto out;
+ if (path_parent(final_path, parent, sizeof parent) == BUILD_OK)
+ (void)sync_path(s, parent);
+ ok = BUILD_OK;
+out:
+ (void)remove_path(s, tmp_dir, 1);
+ return ok;
+}
+
+static int read_file(BuildStore* s, const char* path, KitFileData* out) {
+ const KitFileIO* fio;
+ if (!s || !s->cas_host || !s->cas_host->file_io || !path || !out)
+ return BUILD_ERR;
+ fio = s->cas_host->file_io;
+ out->data = NULL;
+ out->size = 0;
+ out->token = NULL;
+ return fio->read_all && fio->read_all(fio->user, path, out) == KIT_OK
+ ? BUILD_OK
+ : BUILD_ERR;
+}
+
+static void release_file(BuildStore* s, KitFileData* fd) {
+ const KitFileIO* fio;
+ if (!s || !fd || !s->cas_host || !s->cas_host->file_io) return;
+ fio = s->cas_host->file_io;
+ if (fd->data && fio->release) fio->release(fio->user, fd);
+ fd->data = NULL;
+ fd->size = 0;
+ fd->token = NULL;
+}
+
+static int read_verified_trace(BuildStore* s,
+ const uint8_t trace_id[BUILD_HASH_LEN],
+ KitFileData* out) {
+ char path[BUILD_PATH_MAX];
+ uint8_t actual[BUILD_HASH_LEN];
+ if (build_store_trace_path(s, trace_id, path, sizeof path) != BUILD_OK)
+ return BUILD_ERR;
+ if (read_file(s, path, out) != BUILD_OK) return BUILD_ERR;
+ build_trace_id(out->data, out->size, actual);
+ if (!build_id_eq(actual, trace_id)) {
+ release_file(s, out);
+ return BUILD_ERR;
+ }
+ return BUILD_OK;
+}
+
+static int copy_target(KitSlice name, char out[BUILD_TARGET_MAX]) {
+ if (!name.s || name.len == 0u || name.len >= BUILD_TARGET_MAX)
+ return BUILD_ERR;
+ memcpy(out, name.s, name.len);
+ out[name.len] = '\0';
+ return BUILD_OK;
+}
+
+static void record_empty(KitSlice target_name, BuildTargetRecord* out) {
+ if (!out) return;
+ out->n_rows = 0;
+ memset(out->target, 0, sizeof out->target);
+ if (target_name.s && target_name.len < BUILD_TARGET_MAX) {
+ memcpy(out->target, target_name.s, target_name.len);
+ out->target[target_name.len] = '\0';
+ }
+}
+
+static int ignore_dir_entry(void* user, KitSlice name) {
+ (void)user;
+ (void)name;
+ return 0;
+}
+
+static int cache_dir_confirmed(const BuildStore* s, const char* path) {
+ if (!s || !s->io || !s->io->list_dir || !path) return BUILD_ERR;
+ return s->io->list_dir(s->io->user, str_slice(path), ignore_dir_entry,
+ NULL) == 0
+ ? BUILD_OK
+ : BUILD_ERR;
+}
+
+int build_store_open(const KitContext* ctx, KitCas* cas,
+ const KitBuildStoreIo* io, const KitCasHost* cas_host,
+ KitSlice store_root, BuildStore* out) {
+ char root[BUILD_PATH_MAX];
+ char path[BUILD_PATH_MAX];
+ if (!ctx || !cas || !io || !cas_host || !cas_host->file_io || !out)
+ return BUILD_ERR;
+ memset(out, 0, sizeof *out);
+ if (path_set(root, sizeof root, store_root) != BUILD_OK) return BUILD_ERR;
+ if (path_join2(out->root, sizeof out->root, root, "build") != BUILD_OK)
+ return BUILD_ERR;
+ out->ctx = ctx;
+ out->cas = cas;
+ out->io = io;
+ out->cas_host = cas_host;
+ if (ensure_dir(out, out->root) != BUILD_OK) return BUILD_ERR;
+ if (path_join2(path, sizeof path, out->root, "trace") != BUILD_OK ||
+ ensure_dir(out, path) != BUILD_OK)
+ return BUILD_ERR;
+ if (path_join2(path, sizeof path, out->root, "target") != BUILD_OK ||
+ ensure_dir(out, path) != BUILD_OK)
+ return BUILD_ERR;
+ if (path_join2(path, sizeof path, out->root, "cache") != BUILD_OK ||
+ ensure_dir(out, path) != BUILD_OK)
+ return BUILD_ERR;
+ if (path_join2(path, sizeof path, out->root, "tmp") != BUILD_OK ||
+ ensure_dir(out, path) != BUILD_OK)
+ return BUILD_ERR;
+ return BUILD_OK;
+}
+
+int build_store_trace_path(const BuildStore* s,
+ const uint8_t trace_id[BUILD_HASH_LEN], char* out,
+ size_t cap) {
+ if (!s || !trace_id) return BUILD_ERR;
+ return id_path(s, "trace", trace_id, out, cap);
+}
+
+int build_store_target_path(const BuildStore* s,
+ const uint8_t target_key[BUILD_HASH_LEN],
+ char* out, size_t cap) {
+ if (!s || !target_key) return BUILD_ERR;
+ return id_path(s, "target", target_key, out, cap);
+}
+
+int build_store_cache_path(const BuildStore* s,
+ const uint8_t tree_id[BUILD_HASH_LEN], char* out,
+ size_t cap) {
+ if (!s || !tree_id) return BUILD_ERR;
+ return id_path(s, "cache", tree_id, out, cap);
+}
+
+int build_store_put_trace(BuildStore* s, const uint8_t* body, size_t len,
+ uint8_t out_trace_id[BUILD_HASH_LEN]) {
+ char path[BUILD_PATH_MAX];
+ KitFileData existing;
+ if (!s || (!body && len) || !out_trace_id) return BUILD_ERR;
+ build_trace_id(body, len, out_trace_id);
+ kit_hex_encode(path, out_trace_id, BUILD_HASH_LEN);
+ KIT_LOGD("put trace %s bytes=%llu", path, (unsigned long long)len);
+ if (read_verified_trace(s, out_trace_id, &existing) == BUILD_OK) {
+ release_file(s, &existing);
+ return BUILD_OK;
+ }
+ if (build_store_trace_path(s, out_trace_id, path, sizeof path) != BUILD_OK)
+ return BUILD_ERR;
+ if (atomic_write_file(s, path, body, len) == BUILD_OK) return BUILD_OK;
+ if (read_verified_trace(s, out_trace_id, &existing) == BUILD_OK) {
+ release_file(s, &existing);
+ return BUILD_OK;
+ }
+ return BUILD_ERR;
+}
+
+int build_store_get_trace(BuildStore* s,
+ const uint8_t trace_id[BUILD_HASH_LEN],
+ KitFileData* out) {
+ char hex[BUILD_HEX_LEN];
+ int r;
+ if (!s || !trace_id || !out) return BUILD_ERR;
+ kit_hex_encode(hex, trace_id, BUILD_HASH_LEN);
+ KIT_LOGD("get trace %s", hex);
+ r = read_verified_trace(s, trace_id, out);
+ KIT_LOGT("get trace %s %s", hex, r == BUILD_OK ? "hit" : "miss");
+ return r;
+}
+
+void build_store_release(BuildStore* s, KitFileData* fd) { release_file(s, fd); }
+
+int build_store_record_load(BuildStore* s,
+ const uint8_t target_key[BUILD_HASH_LEN],
+ KitSlice target_name, BuildTargetRecord* out) {
+ char path[BUILD_PATH_MAX];
+ KitFileData fd;
+ char err[128];
+ char target[BUILD_TARGET_MAX];
+ if (!s || !target_key || !out) return BUILD_ERR;
+ if (copy_target(target_name, target) != BUILD_OK) return BUILD_ERR;
+ record_empty(target_name, out);
+ if (build_store_target_path(s, target_key, path, sizeof path) != BUILD_OK)
+ return BUILD_ERR;
+ if (read_file(s, path, &fd) != BUILD_OK) return BUILD_OK;
+ if (build_record_parse(fd.data, fd.size, out, err, sizeof err) != BUILD_OK ||
+ strcmp(out->target, target) != 0) {
+ record_empty(target_name, out);
+ }
+ release_file(s, &fd);
+ return BUILD_OK;
+}
+
+int build_store_record_update(BuildStore* s,
+ const uint8_t target_key[BUILD_HASH_LEN],
+ KitSlice target_name, BuildTraceKind kind,
+ const uint8_t trace_id[BUILD_HASH_LEN]) {
+ BuildRecordRow rows[2u * KIT_BUILD_RECORD_CAP];
+ BuildTargetRecord rec;
+ BuildRecordRow row;
+ KitBuildLock* lock = NULL;
+ char lock_key[BUILD_HEX_LEN];
+ char path[BUILD_PATH_MAX];
+ KitWriter* w = NULL;
+ const uint8_t* bytes;
+ size_t len;
+ char err[128];
+ int ok = BUILD_ERR;
+ if (!s || !target_key || !trace_id) return BUILD_ERR;
+ kit_hex_encode(lock_key, target_key, BUILD_HASH_LEN);
+ if (s->io && s->io->lock) {
+ if (s->io->lock(s->io->user, str_slice(lock_key), &lock) != 0 || !lock)
+ return BUILD_ERR;
+ }
+ rec.rows = rows;
+ rec.n_rows = 0;
+ rec.cap_rows = sizeof rows / sizeof rows[0];
+ if (build_store_record_load(s, target_key, target_name, &rec) != BUILD_OK)
+ goto out;
+ row.kind = (uint8_t)kind;
+ memcpy(row.trace_id, trace_id, BUILD_HASH_LEN);
+ if (build_record_prepend(&rec, &row) != BUILD_OK) goto out;
+ if (kit_writer_mem(s->ctx->heap, &w) != KIT_OK) goto out;
+ if (build_record_emit(&rec, w, err, sizeof err) != BUILD_OK ||
+ kit_writer_status(w) != KIT_OK)
+ goto out;
+ bytes = kit_writer_mem_bytes(w, &len);
+ if (build_store_target_path(s, target_key, path, sizeof path) != BUILD_OK)
+ goto out;
+ if (atomic_write_file(s, path, bytes, len) != BUILD_OK) goto out;
+ ok = BUILD_OK;
+out:
+ if (w) kit_writer_close(w);
+ if (lock && s->io && s->io->unlock) s->io->unlock(s->io->user, lock);
+ return ok;
+}
+
+int build_store_cache_lookup(const BuildStore* s,
+ const uint8_t tree_id[BUILD_HASH_LEN],
+ char* path_out, size_t cap) {
+ char path[BUILD_PATH_MAX];
+ if (!s || !tree_id || !path_out) return BUILD_ERR;
+ if (build_store_cache_path(s, tree_id, path, sizeof path) != BUILD_OK)
+ return BUILD_ERR;
+ if (cache_dir_confirmed(s, path) != BUILD_OK) return BUILD_ERR;
+ return path_set(path_out, cap, str_slice(path));
+}
+
+int build_store_cache_materialize(BuildStore* s,
+ const uint8_t tree_id[BUILD_HASH_LEN],
+ char* path_out, size_t cap) {
+ char final_path[BUILD_PATH_MAX];
+ char tmp_dir[BUILD_PATH_MAX];
+ char parent[BUILD_PATH_MAX];
+ int ok = BUILD_ERR;
+ if (!s || !tree_id || !path_out) return BUILD_ERR;
+ if (build_store_cache_lookup(s, tree_id, path_out, cap) == BUILD_OK)
+ return BUILD_OK;
+ if (build_store_cache_path(s, tree_id, final_path, sizeof final_path) !=
+ BUILD_OK)
+ return BUILD_ERR;
+ if (ensure_parent(s, final_path) != BUILD_OK) return BUILD_ERR;
+ if (make_tmp_dir(s, tmp_dir, sizeof tmp_dir) != BUILD_OK) return BUILD_ERR;
+ if (kit_cas_materialize_tree(s->cas, tree_id, tmp_dir) != KIT_OK) goto out;
+ if (sync_path(s, tmp_dir) != BUILD_OK) goto out;
+ if (s->io->rename(s->io->user, str_slice(tmp_dir), str_slice(final_path)) !=
+ 0) {
+ if (build_store_cache_lookup(s, tree_id, path_out, cap) == BUILD_OK) {
+ ok = BUILD_OK;
+ goto out;
+ }
+ goto out;
+ }
+ if (path_parent(final_path, parent, sizeof parent) == BUILD_OK)
+ (void)sync_path(s, parent);
+ if (path_set(path_out, cap, str_slice(final_path)) != BUILD_OK) goto out_no_rm;
+ ok = BUILD_OK;
+out_no_rm:
+ if (ok == BUILD_OK) return BUILD_OK;
+out:
+ (void)remove_path(s, tmp_dir, 1);
+ return ok;
+}
+
+int build_store_ingest_output(BuildStore* s, const char* out_dir,
+ uint8_t out_tree_id[BUILD_HASH_LEN],
+ char* path_out, size_t cap) {
+ if (!s || !out_dir || !out_tree_id || !path_out) return BUILD_ERR;
+ if (kit_cas_add_tree_from_dir(s->cas, out_dir, out_tree_id) != KIT_OK)
+ return BUILD_ERR;
+ return build_store_cache_materialize(s, out_tree_id, path_out, cap);
+}
+
+int build_store_sandbox_new(BuildStore* s, char* sandbox_out,
+ size_t sandbox_cap, char* out_dir_out,
+ size_t out_cap) {
+ char out_dir[BUILD_PATH_MAX];
+ if (!s || !sandbox_out || !out_dir_out) return BUILD_ERR;
+ if (make_tmp_dir(s, sandbox_out, sandbox_cap) != BUILD_OK) return BUILD_ERR;
+ if (path_join2(out_dir, sizeof out_dir, sandbox_out, "out") != BUILD_OK) {
+ (void)remove_path(s, sandbox_out, 1);
+ return BUILD_ERR;
+ }
+ if (ensure_dir(s, out_dir) != BUILD_OK) {
+ (void)remove_path(s, sandbox_out, 1);
+ return BUILD_ERR;
+ }
+ (void)sync_path(s, sandbox_out);
+ if (path_set(out_dir_out, out_cap, str_slice(out_dir)) != BUILD_OK) {
+ (void)remove_path(s, sandbox_out, 1);
+ return BUILD_ERR;
+ }
+ return BUILD_OK;
+}
+
+void build_store_sandbox_done(BuildStore* s, const char* sandbox) {
+ if (!s || !sandbox) return;
+ (void)remove_path(s, sandbox, 1);
+}
diff --git a/src/build/trace.c b/src/build/trace.c
@@ -0,0 +1,1006 @@
+#include "trace.h"
+
+#include <stdio.h>
+#include <string.h>
+
+static int set_err(char* err, size_t errcap, const char* msg) {
+ if (err && errcap) snprintf(err, errcap, "%s", msg);
+ return BUILD_ERR;
+}
+
+static int emit_bytes(KitWriter* out, const char* s) {
+ return kit_writer_write(out, s, strlen(s)) == KIT_OK ? BUILD_OK : BUILD_ERR;
+}
+
+static void hex_encode(char out[BUILD_HEX_LEN],
+ const uint8_t in[BUILD_HASH_LEN]) {
+ size_t i;
+ for (i = 0; i < BUILD_HASH_LEN; ++i) {
+ unsigned hi = (unsigned)(in[i] >> 4);
+ unsigned lo = (unsigned)(in[i] & 0x0fu);
+ out[2u * i] = (char)(hi < 10u ? '0' + hi : 'a' + (hi - 10u));
+ out[2u * i + 1u] = (char)(lo < 10u ? '0' + lo : 'a' + (lo - 10u));
+ }
+ out[2u * BUILD_HASH_LEN] = '\0';
+}
+
+static int hex_val(char c, unsigned* out) {
+ if (c >= '0' && c <= '9') {
+ *out = (unsigned)(c - '0');
+ return BUILD_OK;
+ }
+ if (c >= 'a' && c <= 'f') {
+ *out = (unsigned)(c - 'a') + 10u;
+ return BUILD_OK;
+ }
+ return BUILD_ERR;
+}
+
+static int hex_decode_strict(const char* s, uint8_t out[BUILD_HASH_LEN]) {
+ size_t i;
+ if (!s || strlen(s) != 2u * BUILD_HASH_LEN) return BUILD_ERR;
+ for (i = 0; i < BUILD_HASH_LEN; ++i) {
+ unsigned hi, lo;
+ if (hex_val(s[2u * i], &hi) != BUILD_OK) return BUILD_ERR;
+ if (hex_val(s[2u * i + 1u], &lo) != BUILD_OK) return BUILD_ERR;
+ out[i] = (uint8_t)((hi << 4) | lo);
+ }
+ return BUILD_OK;
+}
+
+static int is_token_char(unsigned char c) {
+ return c > 0x20u && c < 0x7fu;
+}
+
+static int valid_token(const char* s, size_t cap) {
+ size_t i;
+ if (!s || !s[0]) return 0;
+ for (i = 0; s[i]; ++i) {
+ if (i + 1u >= cap) return 0;
+ if (!is_token_char((unsigned char)s[i])) return 0;
+ }
+ return 1;
+}
+
+static int valid_target(const char* s) { return valid_token(s, BUILD_TARGET_MAX); }
+
+static int valid_key(const char* s) { return valid_token(s, BUILD_KEY_MAX); }
+
+static int valid_path(const char* s) { return valid_token(s, BUILD_PATH_MAX); }
+
+static int valid_pattern(const char* s) {
+ return valid_token(s, BUILD_PATTERN_MAX);
+}
+
+static int copy_field(char* dst, size_t cap, const char* src, char* err,
+ size_t errcap) {
+ if (!valid_token(src, cap)) return set_err(err, errcap, "bad field value");
+ snprintf(dst, cap, "%s", src);
+ return BUILD_OK;
+}
+
+static int emit_kv(KitWriter* out, const char* key,
+ const uint8_t h[BUILD_HASH_LEN]) {
+ char hex[BUILD_HEX_LEN];
+ char line[96];
+ hex_encode(hex, h);
+ snprintf(line, sizeof line, "%s %s\n", key, hex);
+ return emit_bytes(out, line);
+}
+
+static int emit_text_kv(KitWriter* out, const char* key, const char* val) {
+ char line[BUILD_TARGET_MAX + 32u];
+ snprintf(line, sizeof line, "%s %s\n", key, val);
+ return emit_bytes(out, line);
+}
+
+static int str_cmp(const char* a, const char* b) { return strcmp(a, b); }
+
+static int id_cmp(const uint8_t a[BUILD_HASH_LEN],
+ const uint8_t b[BUILD_HASH_LEN]) {
+ return memcmp(a, b, BUILD_HASH_LEN);
+}
+
+static int source_cmp(const BuildSourceLeaf* a, const BuildSourceLeaf* b) {
+ return strcmp(a->path, b->path);
+}
+
+static int glob_cmp(const BuildGlobLeaf* a, const BuildGlobLeaf* b) {
+ return strcmp(a->pattern, b->pattern);
+}
+
+static int dep_cmp(const BuildDepEdge* a, const BuildDepEdge* b) {
+ int c = strcmp(a->name, b->name);
+ 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);
+}
+
+static int emit_section(KitWriter* out, const char* name) {
+ char line[32];
+ snprintf(line, sizeof line, "[%s]\n", name);
+ return emit_bytes(out, line);
+}
+
+static int emit_sorted_config_keys(const BuildConfigKey* rows, size_t n,
+ KitWriter* out, char* err, size_t errcap) {
+ const BuildConfigKey* prev = NULL;
+ size_t emitted = 0;
+ if (n && !rows) return set_err(err, errcap, "missing config rows");
+ if (emit_section(out, "config") != BUILD_OK) return BUILD_ERR;
+ while (emitted < n) {
+ const BuildConfigKey* best = NULL;
+ size_t i;
+ for (i = 0; i < n; ++i) {
+ const BuildConfigKey* cur = &rows[i];
+ int after_prev;
+ if (!valid_key(cur->name))
+ return set_err(err, errcap, "bad config key");
+ after_prev = !prev || str_cmp(prev->name, cur->name) < 0;
+ if (after_prev && (!best || str_cmp(cur->name, best->name) < 0))
+ best = cur;
+ }
+ if (!best) return set_err(err, errcap, "duplicate config key");
+ if (emit_bytes(out, best->name) != BUILD_OK || emit_bytes(out, "\n") != BUILD_OK)
+ return BUILD_ERR;
+ prev = best;
+ ++emitted;
+ }
+ return BUILD_OK;
+}
+
+static int emit_source_row(KitWriter* out, const BuildSourceLeaf* row) {
+ char hex[BUILD_HEX_LEN];
+ if (emit_bytes(out, row->path) != BUILD_OK) return BUILD_ERR;
+ if (emit_bytes(out, " ") != BUILD_OK) return BUILD_ERR;
+ if (row->absent) {
+ if (emit_bytes(out, "-") != BUILD_OK) return BUILD_ERR;
+ } else {
+ hex_encode(hex, row->blob);
+ if (emit_bytes(out, hex) != BUILD_OK) return BUILD_ERR;
+ }
+ return emit_bytes(out, "\n");
+}
+
+static int emit_sorted_sources(const BuildSourceLeaf* rows, size_t n,
+ KitWriter* out, char* err, size_t errcap) {
+ const BuildSourceLeaf* prev = NULL;
+ size_t emitted = 0;
+ if (n && !rows) return set_err(err, errcap, "missing source rows");
+ if (emit_section(out, "source") != BUILD_OK) return BUILD_ERR;
+ while (emitted < n) {
+ const BuildSourceLeaf* best = NULL;
+ size_t i;
+ for (i = 0; i < n; ++i) {
+ const BuildSourceLeaf* cur = &rows[i];
+ int after_prev;
+ if (!valid_path(cur->path)) return set_err(err, errcap, "bad source path");
+ if (cur->absent != 0 && cur->absent != 1)
+ return set_err(err, errcap, "bad source absent flag");
+ after_prev = !prev || source_cmp(prev, cur) < 0;
+ if (after_prev && (!best || source_cmp(cur, best) < 0)) best = cur;
+ }
+ if (!best) return set_err(err, errcap, "duplicate source path");
+ if (emit_source_row(out, best) != BUILD_OK) return BUILD_ERR;
+ prev = best;
+ ++emitted;
+ }
+ return BUILD_OK;
+}
+
+static int emit_glob_row(KitWriter* out, const BuildGlobLeaf* row) {
+ char hex[BUILD_HEX_LEN];
+ hex_encode(hex, row->result_hash);
+ if (emit_bytes(out, row->pattern) != BUILD_OK) return BUILD_ERR;
+ if (emit_bytes(out, " ") != BUILD_OK) return BUILD_ERR;
+ if (emit_bytes(out, hex) != BUILD_OK) return BUILD_ERR;
+ return emit_bytes(out, "\n");
+}
+
+static int emit_sorted_globs(const BuildGlobLeaf* rows, size_t n, KitWriter* out,
+ char* err, size_t errcap) {
+ const BuildGlobLeaf* prev = NULL;
+ size_t emitted = 0;
+ if (n && !rows) return set_err(err, errcap, "missing glob rows");
+ if (emit_section(out, "glob") != BUILD_OK) return BUILD_ERR;
+ while (emitted < n) {
+ const BuildGlobLeaf* best = NULL;
+ size_t i;
+ for (i = 0; i < n; ++i) {
+ const BuildGlobLeaf* cur = &rows[i];
+ int after_prev;
+ if (!valid_pattern(cur->pattern))
+ return set_err(err, errcap, "bad glob pattern");
+ after_prev = !prev || glob_cmp(prev, cur) < 0;
+ if (after_prev && (!best || glob_cmp(cur, best) < 0)) best = cur;
+ }
+ if (!best) return set_err(err, errcap, "duplicate glob pattern");
+ if (emit_glob_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 cfg[BUILD_HEX_LEN], argv[BUILD_HEX_LEN], tree[BUILD_HEX_LEN];
+ 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, 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;
+ return emit_bytes(out, "\n");
+}
+
+static int emit_sorted_deps(const BuildDepEdge* rows, size_t n, KitWriter* out,
+ char* err, size_t errcap) {
+ const BuildDepEdge* prev = NULL;
+ size_t emitted = 0;
+ if (n && !rows) return set_err(err, errcap, "missing dep rows");
+ if (emit_section(out, "dep") != BUILD_OK) return BUILD_ERR;
+ while (emitted < n) {
+ const BuildDepEdge* best = NULL;
+ size_t i;
+ for (i = 0; i < n; ++i) {
+ const BuildDepEdge* cur = &rows[i];
+ int after_prev;
+ if (!valid_target(cur->name)) return set_err(err, errcap, "bad dep name");
+ after_prev = !prev || dep_cmp(prev, cur) < 0;
+ if (after_prev && (!best || dep_cmp(cur, best) < 0)) best = cur;
+ }
+ if (!best) return set_err(err, errcap, "duplicate dep row");
+ if (emit_dep_row(out, best) != BUILD_OK) return BUILD_ERR;
+ prev = best;
+ ++emitted;
+ }
+ return BUILD_OK;
+}
+
+static int emit_child_row(KitWriter* out, const uint8_t id[BUILD_HASH_LEN]) {
+ char hex[BUILD_HEX_LEN];
+ hex_encode(hex, id);
+ if (emit_bytes(out, hex) != BUILD_OK) return BUILD_ERR;
+ return emit_bytes(out, "\n");
+}
+
+static int emit_sorted_children(const uint8_t (*rows)[BUILD_HASH_LEN], size_t n,
+ KitWriter* out, char* err, size_t errcap) {
+ const uint8_t* prev = NULL;
+ size_t emitted = 0;
+ if (n && !rows) return set_err(err, errcap, "missing child rows");
+ if (emit_section(out, "child") != BUILD_OK) return BUILD_ERR;
+ while (emitted < n) {
+ const uint8_t* best = NULL;
+ size_t i;
+ for (i = 0; i < n; ++i) {
+ const uint8_t* cur = rows[i];
+ int after_prev = !prev || id_cmp(prev, cur) < 0;
+ if (after_prev && (!best || id_cmp(cur, best) < 0)) best = cur;
+ }
+ if (!best) return set_err(err, errcap, "duplicate child row");
+ if (emit_child_row(out, best) != BUILD_OK) return BUILD_ERR;
+ prev = best;
+ ++emitted;
+ }
+ return BUILD_OK;
+}
+
+static int check_record_kind(uint8_t kind) {
+ return kind == (uint8_t)BUILD_TRACE_DEEP ||
+ kind == (uint8_t)BUILD_TRACE_SHALLOW;
+}
+
+static const char* record_kind_name(uint8_t kind) {
+ if (kind == (uint8_t)BUILD_TRACE_DEEP) return "deep";
+ if (kind == (uint8_t)BUILD_TRACE_SHALLOW) return "shallow";
+ return NULL;
+}
+
+static int record_kind_parse(const char* s, uint8_t* out) {
+ if (strcmp(s, "deep") == 0) {
+ *out = (uint8_t)BUILD_TRACE_DEEP;
+ return BUILD_OK;
+ }
+ if (strcmp(s, "shallow") == 0) {
+ *out = (uint8_t)BUILD_TRACE_SHALLOW;
+ return BUILD_OK;
+ }
+ return BUILD_ERR;
+}
+
+static int emit_record_rows(const BuildRecordRow* rows, size_t n, KitWriter* out,
+ char* err, size_t errcap) {
+ size_t i, j;
+ size_t deep = 0, shallow = 0;
+ if (n && !rows) return set_err(err, errcap, "missing record rows");
+ for (i = 0; i < n; ++i) {
+ const char* kind = record_kind_name(rows[i].kind);
+ char hex[BUILD_HEX_LEN];
+ if (!kind) return set_err(err, errcap, "bad record kind");
+ if (rows[i].kind == (uint8_t)BUILD_TRACE_DEEP) {
+ if (++deep > KIT_BUILD_RECORD_CAP)
+ return set_err(err, errcap, "too many deep records");
+ } else {
+ if (++shallow > KIT_BUILD_RECORD_CAP)
+ return set_err(err, errcap, "too many shallow records");
+ }
+ for (j = 0; j < i; ++j) {
+ if (rows[j].kind == rows[i].kind &&
+ build_id_eq(rows[j].trace_id, rows[i].trace_id))
+ return set_err(err, errcap, "duplicate record row");
+ }
+ hex_encode(hex, rows[i].trace_id);
+ if (emit_text_kv(out, kind, hex) != BUILD_OK) return BUILD_ERR;
+ }
+ return BUILD_OK;
+}
+
+int build_shallow_emit(const BuildShallowTrace* t, KitWriter* out, char* err,
+ size_t errcap) {
+ if (!t || !out) return set_err(err, errcap, "missing shallow trace");
+ if (!valid_target(t->target)) return set_err(err, errcap, "bad target");
+ if (emit_bytes(out, BUILD_SHALLOW_MAGIC "\n") != BUILD_OK) return BUILD_ERR;
+ 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, "output", 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_config_keys(t->config_keys, t->n_config_keys, out, err,
+ errcap) != BUILD_OK)
+ return BUILD_ERR;
+ if (emit_sorted_sources(t->sources, t->n_sources, out, err, errcap) !=
+ BUILD_OK)
+ return BUILD_ERR;
+ if (emit_sorted_globs(t->globs, t->n_globs, 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;
+}
+
+int build_deep_emit(const BuildDeepTrace* t, KitWriter* out, char* err,
+ size_t errcap) {
+ if (!t || !out) return set_err(err, errcap, "missing deep trace");
+ if (!valid_target(t->target)) return set_err(err, errcap, "bad target");
+ if (emit_bytes(out, BUILD_DEEP_MAGIC "\n") != BUILD_OK) return BUILD_ERR;
+ 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, "output", t->output) != BUILD_OK) return BUILD_ERR;
+ if (emit_kv(out, "root-config", t->root_config) != BUILD_OK)
+ return BUILD_ERR;
+ if (emit_kv(out, "argv", t->argv) != BUILD_OK) return BUILD_ERR;
+ if (emit_kv(out, "deepset", t->deepset) != BUILD_OK) return BUILD_ERR;
+ return kit_writer_status(out) == KIT_OK ? BUILD_OK : BUILD_ERR;
+}
+
+int build_deepset_emit(const BuildDeepSet* t, KitWriter* out, char* err,
+ size_t errcap) {
+ if (!t || !out) return set_err(err, errcap, "missing deepset");
+ if (!valid_target(t->target)) return set_err(err, errcap, "bad target");
+ if (emit_bytes(out, BUILD_DEEPSET_MAGIC "\n") != BUILD_OK) return BUILD_ERR;
+ 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_sorted_sources(t->sources, t->n_sources, out, err, errcap) !=
+ BUILD_OK)
+ return BUILD_ERR;
+ if (emit_sorted_globs(t->globs, t->n_globs, 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;
+ return kit_writer_status(out) == KIT_OK ? BUILD_OK : BUILD_ERR;
+}
+
+int build_record_emit(const BuildTargetRecord* t, KitWriter* out, char* err,
+ size_t errcap) {
+ if (!t || !out) return set_err(err, errcap, "missing target record");
+ if (!valid_target(t->target)) return set_err(err, errcap, "bad target");
+ if (emit_bytes(out, BUILD_RECORD_MAGIC "\n") != BUILD_OK) return BUILD_ERR;
+ if (emit_text_kv(out, "target", t->target) != BUILD_OK) return BUILD_ERR;
+ if (emit_record_rows(t->rows, t->n_rows, out, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ return kit_writer_status(out) == KIT_OK ? BUILD_OK : BUILD_ERR;
+}
+
+typedef enum TraceSection {
+ TRACE_SEC_TOP = 0,
+ TRACE_SEC_CONFIG,
+ TRACE_SEC_SOURCE,
+ TRACE_SEC_GLOB,
+ TRACE_SEC_DEP,
+ TRACE_SEC_CHILD,
+ TRACE_SEC_RECORD_ROWS
+} TraceSection;
+
+typedef struct TraceParser {
+ const uint8_t* data;
+ size_t len;
+ size_t pos;
+ int first;
+ char line[BUILD_PATH_MAX + 4u * BUILD_HEX_LEN + BUILD_TARGET_MAX + 16u];
+} TraceParser;
+
+static int parser_next(TraceParser* p, char** line, char* err, size_t errcap) {
+ size_t end, n, i;
+ if (p->pos >= p->len) return 0;
+ end = p->pos;
+ while (end < p->len && p->data[end] != '\n') ++end;
+ if (end == p->len) {
+ set_err(err, errcap, "missing final newline");
+ return -1;
+ }
+ n = end - p->pos;
+ if (n >= sizeof p->line) {
+ set_err(err, errcap, "line too long");
+ return -1;
+ }
+ for (i = p->pos; i < end; ++i) {
+ if (p->data[i] == 0 || p->data[i] == '\r') {
+ set_err(err, errcap, "bad byte in trace");
+ return -1;
+ }
+ }
+ memcpy(p->line, p->data + p->pos, n);
+ p->line[n] = '\0';
+ p->pos = end + 1u;
+ *line = p->line;
+ return 1;
+}
+
+static int split_line(char* line, char** fields, size_t cap, size_t* nout) {
+ char* p = line;
+ size_t n = 0;
+ if (!line || !line[0]) return BUILD_ERR;
+ while (*p) {
+ if (*p == ' ') return BUILD_ERR;
+ if (n >= cap) return BUILD_ERR;
+ fields[n++] = p;
+ while (*p && *p != ' ') {
+ if (!is_token_char((unsigned char)*p)) return BUILD_ERR;
+ ++p;
+ }
+ if (*p == ' ') {
+ *p++ = '\0';
+ if (!*p) return BUILD_ERR;
+ }
+ }
+ *nout = n;
+ return BUILD_OK;
+}
+
+static int expect_field_count(size_t got, size_t want, char* err,
+ size_t errcap) {
+ if (got != want) return set_err(err, errcap, "bad trace row");
+ return BUILD_OK;
+}
+
+static int parse_top_text(char** fields, size_t n, const char* key, char* dst,
+ size_t cap, uint32_t* seen, uint32_t flag, char* err,
+ size_t errcap) {
+ if (expect_field_count(n, 2u, err, errcap) != BUILD_OK) return BUILD_ERR;
+ if (strcmp(fields[0], key) != 0) return set_err(err, errcap, "bad field order");
+ if (*seen & flag) return set_err(err, errcap, "duplicate field");
+ if (copy_field(dst, cap, fields[1], err, errcap) != BUILD_OK) return BUILD_ERR;
+ *seen |= flag;
+ return BUILD_OK;
+}
+
+static int parse_top_hash(char** fields, size_t n, const char* key,
+ uint8_t dst[BUILD_HASH_LEN], uint32_t* seen,
+ uint32_t flag, char* err, size_t errcap) {
+ if (expect_field_count(n, 2u, err, errcap) != BUILD_OK) return BUILD_ERR;
+ if (strcmp(fields[0], key) != 0) return set_err(err, errcap, "bad field order");
+ if (*seen & flag) return set_err(err, errcap, "duplicate field");
+ if (hex_decode_strict(fields[1], dst) != BUILD_OK)
+ return set_err(err, errcap, "bad hash");
+ *seen |= flag;
+ return BUILD_OK;
+}
+
+static int append_config_key(BuildShallowTrace* out, const char* name, char* err,
+ size_t errcap) {
+ BuildConfigKey* row;
+ if (out->n_config_keys >= out->cap_config_keys)
+ return set_err(err, errcap, "too many config keys");
+ if (!valid_key(name)) return set_err(err, errcap, "bad config key");
+ if (out->n_config_keys &&
+ strcmp(out->config_keys[out->n_config_keys - 1u].name, name) >= 0)
+ return set_err(err, errcap, "non-canonical config keys");
+ row = &out->config_keys[out->n_config_keys++];
+ snprintf(row->name, sizeof row->name, "%s", name);
+ return BUILD_OK;
+}
+
+static int parse_source_leaf(char** fields, size_t n, BuildSourceLeaf* row,
+ char* err, size_t errcap) {
+ if (expect_field_count(n, 2u, err, errcap) != BUILD_OK) return BUILD_ERR;
+ if (!valid_path(fields[0])) return set_err(err, errcap, "bad source path");
+ snprintf(row->path, sizeof row->path, "%s", fields[0]);
+ if (strcmp(fields[1], "-") == 0) {
+ memset(row->blob, 0, sizeof row->blob);
+ row->absent = 1;
+ } else {
+ if (hex_decode_strict(fields[1], row->blob) != BUILD_OK)
+ return set_err(err, errcap, "bad source hash");
+ row->absent = 0;
+ }
+ return BUILD_OK;
+}
+
+static int append_shallow_source(BuildShallowTrace* out, char** fields, size_t n,
+ char* err, size_t errcap) {
+ BuildSourceLeaf* row;
+ if (out->n_sources >= out->cap_sources)
+ return set_err(err, errcap, "too many source rows");
+ row = &out->sources[out->n_sources];
+ if (parse_source_leaf(fields, n, row, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ if (out->n_sources && source_cmp(&out->sources[out->n_sources - 1u], row) >= 0)
+ return set_err(err, errcap, "non-canonical source rows");
+ ++out->n_sources;
+ return BUILD_OK;
+}
+
+static int append_deepset_source(BuildDeepSet* out, char** fields, size_t n,
+ char* err, size_t errcap) {
+ BuildSourceLeaf* row;
+ if (out->n_sources >= out->cap_sources)
+ return set_err(err, errcap, "too many source rows");
+ row = &out->sources[out->n_sources];
+ if (parse_source_leaf(fields, n, row, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ if (out->n_sources && source_cmp(&out->sources[out->n_sources - 1u], row) >= 0)
+ return set_err(err, errcap, "non-canonical source rows");
+ ++out->n_sources;
+ return BUILD_OK;
+}
+
+static int parse_glob_leaf(char** fields, size_t n, BuildGlobLeaf* row,
+ char* err, size_t errcap) {
+ if (expect_field_count(n, 2u, err, errcap) != BUILD_OK) return BUILD_ERR;
+ if (!valid_pattern(fields[0])) return set_err(err, errcap, "bad glob pattern");
+ snprintf(row->pattern, sizeof row->pattern, "%s", fields[0]);
+ if (hex_decode_strict(fields[1], row->result_hash) != BUILD_OK)
+ return set_err(err, errcap, "bad glob hash");
+ return BUILD_OK;
+}
+
+static int append_shallow_glob(BuildShallowTrace* out, char** fields, size_t n,
+ char* err, size_t errcap) {
+ BuildGlobLeaf* row;
+ if (out->n_globs >= out->cap_globs)
+ return set_err(err, errcap, "too many glob rows");
+ row = &out->globs[out->n_globs];
+ if (parse_glob_leaf(fields, n, row, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ if (out->n_globs && glob_cmp(&out->globs[out->n_globs - 1u], row) >= 0)
+ return set_err(err, errcap, "non-canonical glob rows");
+ ++out->n_globs;
+ return BUILD_OK;
+}
+
+static int append_deepset_glob(BuildDeepSet* out, char** fields, size_t n,
+ char* err, size_t errcap) {
+ BuildGlobLeaf* row;
+ if (out->n_globs >= out->cap_globs)
+ return set_err(err, errcap, "too many glob rows");
+ row = &out->globs[out->n_globs];
+ if (parse_glob_leaf(fields, n, row, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ if (out->n_globs && glob_cmp(&out->globs[out->n_globs - 1u], row) >= 0)
+ return set_err(err, errcap, "non-canonical glob rows");
+ ++out->n_globs;
+ return BUILD_OK;
+}
+
+static int append_dep(BuildShallowTrace* out, char** fields, size_t n, char* err,
+ size_t errcap) {
+ BuildDepEdge* row;
+ 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->config_id) != 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");
+ ++out->n_deps;
+ return BUILD_OK;
+}
+
+static int append_child(BuildDeepSet* out, char** fields, size_t n, char* err,
+ size_t errcap) {
+ uint8_t* row;
+ if (expect_field_count(n, 1u, err, errcap) != BUILD_OK) return BUILD_ERR;
+ if (out->n_children >= out->cap_children)
+ return set_err(err, errcap, "too many child rows");
+ row = out->children[out->n_children];
+ if (hex_decode_strict(fields[0], row) != BUILD_OK)
+ return set_err(err, errcap, "bad child hash");
+ if (out->n_children && id_cmp(out->children[out->n_children - 1u], row) >= 0)
+ return set_err(err, errcap, "non-canonical child rows");
+ ++out->n_children;
+ return BUILD_OK;
+}
+
+static int append_record_row(BuildTargetRecord* out, char** fields, size_t n,
+ char* err, size_t errcap) {
+ BuildRecordRow* row;
+ size_t i;
+ size_t same_kind = 0;
+ if (expect_field_count(n, 2u, err, errcap) != BUILD_OK) return BUILD_ERR;
+ if (out->n_rows >= out->cap_rows)
+ return set_err(err, errcap, "too many record rows");
+ row = &out->rows[out->n_rows];
+ if (record_kind_parse(fields[0], &row->kind) != BUILD_OK)
+ return set_err(err, errcap, "bad record kind");
+ if (hex_decode_strict(fields[1], row->trace_id) != BUILD_OK)
+ return set_err(err, errcap, "bad record hash");
+ for (i = 0; i < out->n_rows; ++i) {
+ if (out->rows[i].kind == row->kind) {
+ ++same_kind;
+ if (build_id_eq(out->rows[i].trace_id, row->trace_id))
+ return set_err(err, errcap, "duplicate record row");
+ }
+ }
+ if (same_kind >= KIT_BUILD_RECORD_CAP)
+ return set_err(err, errcap, "too many record rows for kind");
+ ++out->n_rows;
+ return BUILD_OK;
+}
+
+#define F_TARGET 0x01u
+#define F_RECIPE 0x02u
+#define F_OUTPUT 0x04u
+#define F_CONFIG 0x08u
+#define F_ARGV 0x10u
+#define F_DEEPSET 0x20u
+
+int build_shallow_parse(const uint8_t* data, size_t len, BuildShallowTrace* out,
+ char* err, size_t errcap) {
+ TraceParser p;
+ TraceSection sec = TRACE_SEC_TOP;
+ uint32_t seen = 0;
+ 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_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_deps = 0;
+ memset(&p, 0, sizeof p);
+ p.data = data;
+ p.len = len;
+ p.first = 1;
+ for (;;) {
+ char* line;
+ char* fields[4];
+ size_t n = 0;
+ int r = parser_next(&p, &line, err, errcap);
+ if (r < 0) return BUILD_ERR;
+ if (r == 0) break;
+ if (p.first) {
+ p.first = 0;
+ if (strcmp(line, BUILD_SHALLOW_MAGIC) != 0)
+ return set_err(err, errcap, "bad shallow magic/version");
+ continue;
+ }
+ if (line[0] == '[') {
+ if (strcmp(line, "[config]") == 0 && sec == TRACE_SEC_TOP) {
+ if (seen != (F_TARGET | F_RECIPE | F_OUTPUT | F_CONFIG | F_ARGV))
+ return set_err(err, errcap, "missing shallow field");
+ sec = TRACE_SEC_CONFIG;
+ } else if (strcmp(line, "[source]") == 0 && sec == TRACE_SEC_CONFIG) {
+ 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) {
+ sec = TRACE_SEC_DEP;
+ } else {
+ return set_err(err, errcap, "bad shallow section");
+ }
+ continue;
+ }
+ if (split_line(line, fields, sizeof fields / sizeof fields[0], &n) !=
+ BUILD_OK)
+ return set_err(err, errcap, "bad shallow row");
+ if (sec == TRACE_SEC_TOP) {
+ if (!(seen & F_TARGET)) {
+ if (parse_top_text(fields, n, "target", out->target, sizeof out->target,
+ &seen, F_TARGET, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } else if (!(seen & F_RECIPE)) {
+ if (parse_top_hash(fields, n, "recipe", out->recipe, &seen, F_RECIPE,
+ err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } else if (!(seen & F_OUTPUT)) {
+ if (parse_top_hash(fields, n, "output", 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)
+ return BUILD_ERR;
+ } else {
+ return set_err(err, errcap, "unexpected shallow field");
+ }
+ } else if (sec == TRACE_SEC_CONFIG) {
+ if (expect_field_count(n, 1u, err, errcap) != BUILD_OK ||
+ append_config_key(out, fields[0], err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } else if (sec == TRACE_SEC_SOURCE) {
+ if (append_shallow_source(out, fields, n, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } 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_DEP) {
+ if (append_dep(out, fields, n, err, errcap) != BUILD_OK) return BUILD_ERR;
+ } else {
+ return set_err(err, errcap, "bad shallow row");
+ }
+ }
+ if (p.first) return set_err(err, errcap, "bad shallow magic/version");
+ if (sec != TRACE_SEC_DEP) return set_err(err, errcap, "missing shallow section");
+ return BUILD_OK;
+}
+
+int build_deep_parse(const uint8_t* data, size_t len, BuildDeepTrace* out,
+ char* err, size_t errcap) {
+ TraceParser p;
+ uint32_t seen = 0;
+ if (!data || !out) return set_err(err, errcap, "missing deep trace");
+ memset(out->target, 0, sizeof out->target);
+ memset(&p, 0, sizeof p);
+ p.data = data;
+ p.len = len;
+ p.first = 1;
+ for (;;) {
+ char* line;
+ char* fields[2];
+ size_t n = 0;
+ int r = parser_next(&p, &line, err, errcap);
+ if (r < 0) return BUILD_ERR;
+ if (r == 0) break;
+ if (p.first) {
+ p.first = 0;
+ if (strcmp(line, BUILD_DEEP_MAGIC) != 0)
+ return set_err(err, errcap, "bad deep magic/version");
+ continue;
+ }
+ if (line[0] == '[') return set_err(err, errcap, "bad deep section");
+ if (split_line(line, fields, sizeof fields / sizeof fields[0], &n) !=
+ BUILD_OK)
+ return set_err(err, errcap, "bad deep row");
+ if (!(seen & F_TARGET)) {
+ if (parse_top_text(fields, n, "target", out->target, sizeof out->target,
+ &seen, F_TARGET, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } else if (!(seen & F_RECIPE)) {
+ if (parse_top_hash(fields, n, "recipe", out->recipe, &seen, F_RECIPE,
+ err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } else if (!(seen & F_OUTPUT)) {
+ if (parse_top_hash(fields, n, "output", out->output, &seen, F_OUTPUT, err,
+ errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } else if (!(seen & F_CONFIG)) {
+ if (parse_top_hash(fields, n, "root-config", out->root_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)
+ return BUILD_ERR;
+ } else if (!(seen & F_DEEPSET)) {
+ if (parse_top_hash(fields, n, "deepset", out->deepset, &seen, F_DEEPSET,
+ err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } else {
+ return set_err(err, errcap, "unexpected deep field");
+ }
+ }
+ if (p.first) return set_err(err, errcap, "bad deep magic/version");
+ if (seen != (F_TARGET | F_RECIPE | F_OUTPUT | F_CONFIG | F_ARGV | F_DEEPSET))
+ return set_err(err, errcap, "missing deep field");
+ return BUILD_OK;
+}
+
+int build_deepset_parse(const uint8_t* data, size_t len, BuildDeepSet* out,
+ char* err, size_t errcap) {
+ TraceParser p;
+ TraceSection sec = TRACE_SEC_TOP;
+ 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_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_children = 0;
+ memset(&p, 0, sizeof p);
+ p.data = data;
+ p.len = len;
+ p.first = 1;
+ for (;;) {
+ char* line;
+ char* fields[2];
+ size_t n = 0;
+ int r = parser_next(&p, &line, err, errcap);
+ if (r < 0) return BUILD_ERR;
+ if (r == 0) break;
+ if (p.first) {
+ p.first = 0;
+ if (strcmp(line, BUILD_DEEPSET_MAGIC) != 0)
+ return set_err(err, errcap, "bad deepset magic/version");
+ continue;
+ }
+ if (line[0] == '[') {
+ if (strcmp(line, "[source]") == 0 && sec == TRACE_SEC_TOP) {
+ if (seen != (F_TARGET | F_RECIPE))
+ return set_err(err, errcap, "missing deepset field");
+ 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) {
+ sec = TRACE_SEC_CHILD;
+ } else {
+ return set_err(err, errcap, "bad deepset section");
+ }
+ continue;
+ }
+ if (split_line(line, fields, sizeof fields / sizeof fields[0], &n) !=
+ BUILD_OK)
+ return set_err(err, errcap, "bad deepset row");
+ if (sec == TRACE_SEC_TOP) {
+ if (!(seen & F_TARGET)) {
+ if (parse_top_text(fields, n, "target", out->target, sizeof out->target,
+ &seen, F_TARGET, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } else if (!(seen & F_RECIPE)) {
+ if (parse_top_hash(fields, n, "recipe", out->recipe, &seen, F_RECIPE,
+ err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } else {
+ return set_err(err, errcap, "unexpected deepset field");
+ }
+ } else if (sec == TRACE_SEC_SOURCE) {
+ if (append_deepset_source(out, fields, n, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } 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_CHILD) {
+ if (append_child(out, fields, n, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } else {
+ return set_err(err, errcap, "bad deepset row");
+ }
+ }
+ if (p.first) return set_err(err, errcap, "bad deepset magic/version");
+ if (sec != TRACE_SEC_CHILD)
+ return set_err(err, errcap, "missing deepset section");
+ return BUILD_OK;
+}
+
+int build_record_parse(const uint8_t* data, size_t len, BuildTargetRecord* out,
+ char* err, size_t errcap) {
+ TraceParser p;
+ uint32_t seen = 0;
+ if (!data || !out) return set_err(err, errcap, "missing target record");
+ if (out->cap_rows && !out->rows)
+ return set_err(err, errcap, "missing record storage");
+ memset(out->target, 0, sizeof out->target);
+ out->n_rows = 0;
+ memset(&p, 0, sizeof p);
+ p.data = data;
+ p.len = len;
+ p.first = 1;
+ for (;;) {
+ char* line;
+ char* fields[2];
+ size_t n = 0;
+ int r = parser_next(&p, &line, err, errcap);
+ if (r < 0) return BUILD_ERR;
+ if (r == 0) break;
+ if (p.first) {
+ p.first = 0;
+ if (strcmp(line, BUILD_RECORD_MAGIC) != 0)
+ return set_err(err, errcap, "bad record magic/version");
+ continue;
+ }
+ if (line[0] == '[') return set_err(err, errcap, "bad record section");
+ if (split_line(line, fields, sizeof fields / sizeof fields[0], &n) !=
+ BUILD_OK)
+ return set_err(err, errcap, "bad record row");
+ if (!(seen & F_TARGET)) {
+ if (parse_top_text(fields, n, "target", out->target, sizeof out->target,
+ &seen, F_TARGET, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ } else {
+ if (append_record_row(out, fields, n, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ }
+ }
+ if (p.first) return set_err(err, errcap, "bad record magic/version");
+ if (seen != F_TARGET) return set_err(err, errcap, "missing record target");
+ return BUILD_OK;
+}
+
+void build_trace_id(const uint8_t* body, size_t len,
+ uint8_t out[BUILD_HASH_LEN]) {
+ KitBlobInfo bi;
+ kit_blob_info(&bi, body, len);
+ memcpy(out, bi.id, BUILD_HASH_LEN);
+}
+
+void build_deepset_id(const uint8_t* body, size_t len,
+ uint8_t out[BUILD_HASH_LEN]) {
+ build_trace_id(body, len, out);
+}
+
+int build_record_prepend(BuildTargetRecord* rec, const BuildRecordRow* row) {
+ size_t read, write, count, i;
+ if (!rec || !row) return BUILD_ERR;
+ if (!check_record_kind(row->kind)) return BUILD_ERR;
+ if (rec->cap_rows && !rec->rows) return BUILD_ERR;
+ if (rec->n_rows > rec->cap_rows) return BUILD_ERR;
+ write = 0;
+ for (read = 0; read < rec->n_rows; ++read) {
+ if (rec->rows[read].kind == row->kind &&
+ build_id_eq(rec->rows[read].trace_id, row->trace_id))
+ continue;
+ if (write != read) rec->rows[write] = rec->rows[read];
+ ++write;
+ }
+ rec->n_rows = write;
+ if (rec->n_rows >= rec->cap_rows) {
+ size_t last_same = rec->n_rows;
+ count = 0;
+ for (i = 0; i < rec->n_rows; ++i) {
+ if (rec->rows[i].kind == row->kind) {
+ ++count;
+ last_same = i;
+ }
+ }
+ if (count < KIT_BUILD_RECORD_CAP || last_same == rec->n_rows)
+ return BUILD_ERR;
+ for (i = last_same + 1u; i < rec->n_rows; ++i)
+ rec->rows[i - 1u] = rec->rows[i];
+ --rec->n_rows;
+ }
+ for (i = rec->n_rows; i > 0; --i) rec->rows[i] = rec->rows[i - 1u];
+ rec->rows[0] = *row;
+ ++rec->n_rows;
+
+ count = 0;
+ write = 0;
+ for (read = 0; read < rec->n_rows; ++read) {
+ if (rec->rows[read].kind == row->kind) {
+ ++count;
+ if (count > KIT_BUILD_RECORD_CAP) continue;
+ }
+ if (write != read) rec->rows[write] = rec->rows[read];
+ ++write;
+ }
+ rec->n_rows = write;
+ return BUILD_OK;
+}
diff --git a/test/build/build_public_link_test.c b/test/build/build_public_link_test.c
@@ -0,0 +1,63 @@
+#include <kit/build_coord.h>
+
+#include "lib/kit_unit.h"
+
+#include <string.h>
+
+static KitUnit g_u;
+
+#define EXPECT(c, ...) CU_EXPECT(&g_u, c, __VA_ARGS__)
+
+static void test_public_symbols(void) {
+ KitBuildStats stats;
+ KitBuildClient* client = (KitBuildClient*)1;
+ KitBuildNeedToken token;
+ KitBuildResult result;
+ KitSlice value;
+ int present = 1;
+
+ memset(&stats, 0xff, sizeof stats);
+ kit_build_stats(NULL, &stats);
+ EXPECT(stats.deep_hits == 0 && stats.shallow_hits == 0 &&
+ stats.recipes_run == 0 && stats.materialize_misses == 0 &&
+ stats.object_fetches == 0 && stats.trace_pulls == 0,
+ "stats null handle clears output");
+ EXPECT(kit_build(NULL, NULL, NULL) == KIT_INVALID, "kit_build rejects nulls");
+ EXPECT(kit_build_coordinator_open(NULL, NULL, KIT_SLICE_NULL, NULL, NULL) ==
+ KIT_INVALID,
+ "coordinator open rejects nulls");
+ EXPECT(kit_build_traces_export(NULL, NULL) == KIT_ERR,
+ "trace export fails explicitly");
+ EXPECT(kit_build_traces_import(NULL, NULL, NULL) == KIT_ERR,
+ "trace import fails explicitly");
+
+ EXPECT(kit_build_client_open(NULL, NULL, &client) == KIT_UNSUPPORTED &&
+ client == NULL,
+ "client open unsupported");
+ EXPECT(kit_build_client_config_get(NULL, KIT_SLICE_LIT("x"), &value,
+ &present) == KIT_UNSUPPORTED &&
+ value.s == NULL && value.len == 0 && present == 0,
+ "client config unsupported");
+ EXPECT(kit_build_client_source(NULL, KIT_SLICE_LIT("x"), result.output_tree,
+ &value) == KIT_UNSUPPORTED,
+ "client source unsupported");
+ EXPECT(kit_build_client_glob(NULL, KIT_SLICE_LIT("*"), NULL, NULL) ==
+ KIT_UNSUPPORTED,
+ "client glob unsupported");
+ EXPECT(kit_build_client_need(NULL, NULL, &result) == KIT_UNSUPPORTED,
+ "client need unsupported");
+ EXPECT(kit_build_client_need_submit(NULL, NULL, &token) == KIT_UNSUPPORTED &&
+ token.id == 0,
+ "client need-submit unsupported");
+ token.id = 7;
+ EXPECT(kit_build_client_need_await(NULL, token, &result) == KIT_UNSUPPORTED,
+ "client need-await unsupported");
+ kit_build_client_close(NULL);
+}
+
+int main(void) {
+ kit_unit_init(&g_u);
+ test_public_symbols();
+ kit_unit_summary(&g_u, "build_public_link_test");
+ return kit_unit_status(&g_u);
+}
diff --git a/test/build/build_pure_test.c b/test/build/build_pure_test.c
@@ -0,0 +1,373 @@
+#include "build/build.h"
+#include "build/bundle.h"
+#include "build/cfg.h"
+#include "build/defn.h"
+#include "build/protocol.h"
+#include "build/trace.h"
+#include "lib/kit_unit.h"
+
+#include <string.h>
+
+static KitUnit g_u;
+
+#define EXPECT(c, ...) CU_EXPECT(&g_u, c, __VA_ARGS__)
+
+static int emit_to_slice(int (*emit)(void*, KitWriter*), void* obj,
+ KitSlice* out, KitWriter** writer_out) {
+ KitWriter* w = NULL;
+ const uint8_t* bytes;
+ size_t len;
+ if (kit_writer_mem(&g_u.heap, &w) != KIT_OK || !w) return 0;
+ if (emit(obj, w) != BUILD_OK || kit_writer_status(w) != KIT_OK) {
+ kit_writer_close(w);
+ return 0;
+ }
+ bytes = kit_writer_mem_bytes(w, &len);
+ out->data = bytes;
+ out->len = len;
+ *writer_out = w;
+ return 1;
+}
+
+static int emit_config_adapter(void* obj, KitWriter* w) {
+ return build_config_emit((const BuildConfig*)obj, w);
+}
+
+static int emit_argv_adapter(void* obj, KitWriter* w) {
+ return build_argv_emit((const BuildArgv*)obj, w);
+}
+
+static void fill_id(uint8_t id[BUILD_HASH_LEN], uint8_t seed) {
+ size_t i;
+ for (i = 0; i < BUILD_HASH_LEN; ++i) id[i] = (uint8_t)(seed + i);
+}
+
+static void test_config_and_argv(void) {
+ BuildConfigEntry cfg_storage[4], parsed_storage[4], over_storage[4];
+ char argv_storage[3][BUILD_VAL_MAX], parsed_argv_storage[3][BUILD_VAL_MAX];
+ BuildConfig cfg, parsed, over;
+ BuildArgv argv, parsed_argv;
+ KitBuildKV ov[2];
+ KitSlice bytes;
+ KitWriter* w = NULL;
+ KitSlice val;
+ int present = 0;
+ uint8_t id1[BUILD_HASH_LEN], id2[BUILD_HASH_LEN];
+ static const char bad[] = "kit-build-config 1\nz 1\na 2\n";
+
+ build_config_init(&cfg, cfg_storage, 4);
+ build_config_init(&parsed, parsed_storage, 4);
+ build_config_init(&over, over_storage, 4);
+ EXPECT(build_config_set(&cfg, KIT_SLICE_LIT("opt"), KIT_SLICE_LIT("2")) ==
+ BUILD_OK,
+ "set opt");
+ EXPECT(build_config_set(&cfg, KIT_SLICE_LIT("arch"), KIT_SLICE_LIT("aa64")) ==
+ BUILD_OK,
+ "set arch");
+ EXPECT(emit_to_slice(emit_config_adapter, &cfg, &bytes, &w),
+ "emit config");
+ EXPECT(bytes.len == strlen("kit-build-config 1\narch aa64\nopt 2\n") &&
+ memcmp(bytes.data, "kit-build-config 1\narch aa64\nopt 2\n",
+ bytes.len) == 0,
+ "canonical config bytes");
+ EXPECT(build_config_parse(bytes.data, bytes.len, &parsed, NULL, 0) ==
+ BUILD_OK,
+ "parse emitted config");
+ EXPECT(build_config_get(&parsed, KIT_SLICE_LIT("arch"), &val, &present) ==
+ BUILD_OK &&
+ present && kit_slice_eq(val, KIT_SLICE_LIT("aa64")),
+ "get parsed config");
+ EXPECT(build_config_id(&g_u.heap, &cfg, id1) == BUILD_OK &&
+ build_config_id(&g_u.heap, &parsed, id2) == BUILD_OK &&
+ build_id_eq(id1, id2),
+ "config id roundtrip");
+ kit_writer_close(w);
+
+ EXPECT(build_config_parse((const uint8_t*)bad, sizeof bad - 1u, &parsed, NULL,
+ 0) == BUILD_ERR,
+ "reject non-canonical config");
+
+ ov[0].key = KIT_SLICE_LIT("opt");
+ ov[0].value = KIT_SLICE_LIT("3");
+ ov[1].key = KIT_SLICE_LIT("mode");
+ ov[1].value = KIT_SLICE_LIT("dbg");
+ EXPECT(build_config_overlay(&cfg, ov, 2, &over) == BUILD_OK,
+ "overlay config");
+ EXPECT(build_config_get(&over, KIT_SLICE_LIT("opt"), &val, &present) ==
+ BUILD_OK &&
+ present && kit_slice_eq(val, KIT_SLICE_LIT("3")),
+ "overlay replaces");
+
+ build_argv_init(&argv, argv_storage, 3);
+ build_argv_init(&parsed_argv, parsed_argv_storage, 3);
+ {
+ KitSlice args[2];
+ args[0] = KIT_SLICE_LIT("--emit");
+ args[1] = KIT_SLICE_LIT("obj");
+ EXPECT(build_argv_set(&argv, args, 2) == BUILD_OK, "set argv");
+ }
+ EXPECT(emit_to_slice(emit_argv_adapter, &argv, &bytes, &w), "emit argv");
+ EXPECT(bytes.len == strlen("kit-build-argv 1\n--emit\nobj\n") &&
+ memcmp(bytes.data, "kit-build-argv 1\n--emit\nobj\n",
+ bytes.len) == 0,
+ "argv preserves order");
+ EXPECT(build_argv_parse(bytes.data, bytes.len, &parsed_argv, NULL, 0) ==
+ BUILD_OK &&
+ parsed_argv.n == 2 &&
+ strcmp(parsed_argv.args[0], "--emit") == 0 &&
+ strcmp(parsed_argv.args[1], "obj") == 0,
+ "parse argv");
+ kit_writer_close(w);
+}
+
+static void test_defn(void) {
+ static const char text[] = "kit-build 1\n"
+ "[target //app:bin]\n"
+ "recipe recipes/app.sh\n"
+ "[target //lib:core]\n"
+ "recipe recipes/lib.sh\n";
+ static const char unsorted[] = "kit-build 1\n"
+ "[target //z]\n"
+ "recipe recipes/z.sh\n"
+ "[target //a]\n"
+ "recipe recipes/a.sh\n";
+ BuildTargetDefn storage[4];
+ BuildDefn defn;
+ const BuildTargetDefn* t;
+
+ memset(&defn, 0, sizeof defn);
+ defn.targets = storage;
+ defn.cap_targets = 4;
+ EXPECT(build_defn_parse((const uint8_t*)text, sizeof text - 1u, &defn, NULL,
+ 0) == BUILD_OK,
+ "parse defn");
+ t = build_defn_find(&defn, KIT_SLICE_LIT("//lib:core"));
+ EXPECT(t && strcmp(t->recipe_path, "recipes/lib.sh") == 0, "find target");
+ EXPECT(build_defn_find(&defn, KIT_SLICE_LIT("//none")) == NULL,
+ "missing target");
+ EXPECT(build_defn_parse((const uint8_t*)unsorted, sizeof unsorted - 1u, &defn,
+ NULL, 0) == BUILD_ERR,
+ "reject unsorted defn");
+}
+
+static void test_trace(void) {
+ BuildShallowTrace st;
+ BuildConfigKey keys[2];
+ BuildSourceLeaf sources[2], parsed_sources[2];
+ BuildGlobLeaf globs[1], parsed_globs[1];
+ BuildDepEdge deps[1], parsed_deps[1];
+ BuildConfigKey parsed_keys[2];
+ BuildShallowTrace parsed;
+ BuildTargetRecord rec;
+ BuildRecordRow rows[20], row;
+ KitWriter* w = NULL;
+ KitSlice bytes;
+ uint8_t trace_id[BUILD_HASH_LEN];
+ char err[128];
+ static const char dup_source[] =
+ "kit-build-shallow 1\n"
+ "target //app:bin\n"
+ "recipe "
+ "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\n"
+ "output "
+ "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20\n"
+ "config "
+ "02030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021\n"
+ "argv "
+ "030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122\n"
+ "[config]\n"
+ "[source]\n"
+ "a.c -\n"
+ "a.c -\n"
+ "[glob]\n"
+ "[dep]\n";
+
+ memset(&st, 0, sizeof st);
+ 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.config_keys = keys;
+ st.n_config_keys = 2;
+ st.cap_config_keys = 2;
+ strcpy(keys[0].name, "z");
+ strcpy(keys[1].name, "a");
+ st.sources = sources;
+ st.n_sources = 2;
+ st.cap_sources = 2;
+ strcpy(sources[0].path, "src/z.c");
+ fill_id(sources[0].blob, 4);
+ sources[0].absent = 0;
+ strcpy(sources[1].path, "src/a.c");
+ sources[1].absent = 1;
+ st.globs = globs;
+ st.n_globs = 1;
+ st.cap_globs = 1;
+ strcpy(globs[0].pattern, "src/*.c");
+ fill_id(globs[0].result_hash, 5);
+ st.deps = deps;
+ st.n_deps = 1;
+ st.cap_deps = 1;
+ strcpy(deps[0].name, "//lib:core");
+ fill_id(deps[0].config_id, 6);
+ fill_id(deps[0].argv_id, 7);
+ fill_id(deps[0].output_tree, 8);
+
+ EXPECT(kit_writer_mem(&g_u.heap, &w) == KIT_OK && w, "trace writer");
+ EXPECT(build_shallow_emit(&st, w, err, sizeof err) == BUILD_OK,
+ "emit shallow");
+ bytes.data = kit_writer_mem_bytes(w, &bytes.len);
+ build_trace_id(bytes.data, bytes.len, trace_id);
+
+ memset(&parsed, 0, sizeof parsed);
+ parsed.config_keys = parsed_keys;
+ parsed.cap_config_keys = 2;
+ parsed.sources = parsed_sources;
+ parsed.cap_sources = 2;
+ parsed.globs = parsed_globs;
+ parsed.cap_globs = 1;
+ parsed.deps = parsed_deps;
+ parsed.cap_deps = 1;
+ EXPECT(build_shallow_parse(bytes.data, bytes.len, &parsed, err, sizeof err) ==
+ BUILD_OK,
+ "parse shallow");
+ EXPECT(strcmp(parsed.config_keys[0].name, "a") == 0 &&
+ strcmp(parsed.sources[0].path, "src/a.c") == 0 &&
+ parsed.sources[0].absent,
+ "shallow canonical sections");
+ EXPECT(build_shallow_parse((const uint8_t*)dup_source, sizeof dup_source - 1u,
+ &parsed, err, sizeof err) == BUILD_ERR,
+ "reject duplicate source");
+ kit_writer_close(w);
+
+ memset(&rec, 0, sizeof rec);
+ strcpy(rec.target, "//app:bin");
+ rec.rows = rows;
+ rec.cap_rows = sizeof rows / sizeof rows[0];
+ row.kind = (uint8_t)BUILD_TRACE_DEEP;
+ memcpy(row.trace_id, trace_id, BUILD_HASH_LEN);
+ EXPECT(build_record_prepend(&rec, &row) == BUILD_OK && rec.n_rows == 1,
+ "record prepend");
+ EXPECT(build_record_prepend(&rec, &row) == BUILD_OK && rec.n_rows == 1,
+ "record dedup");
+}
+
+static int glob_seen(void* user, KitSlice path) {
+ int* n = (int*)user;
+ if (kit_slice_eq(path, KIT_SLICE_LIT("src/a.c"))) ++*n;
+ return 0;
+}
+
+static void test_protocol(void) {
+ uint8_t buf[BUILD_FRAME_MAX];
+ size_t n = 0;
+ KitBuildKV overrides[1], dec_overrides[1];
+ KitSlice argv[1], dec_argv[1];
+ BuildReq req, dec_req;
+ BuildResp resp, dec_resp;
+ int seen = 0;
+
+ memset(&req, 0, sizeof req);
+ req.cmd = BUILD_CMD_NEED;
+ req.arg = KIT_SLICE_LIT("//lib:core");
+ overrides[0].key = KIT_SLICE_LIT("opt");
+ overrides[0].value = KIT_SLICE_LIT("2");
+ req.overrides = overrides;
+ req.noverrides = 1;
+ argv[0] = KIT_SLICE_LIT("--fast");
+ req.argv = argv;
+ req.argc = 1;
+ EXPECT(build_proto_encode_req(&req, buf, sizeof buf, &n) == BUILD_OK,
+ "encode need");
+ EXPECT(build_proto_decode_req(buf, n, &dec_req, dec_overrides, 1, dec_argv,
+ 1) == BUILD_OK,
+ "decode need");
+ EXPECT(dec_req.cmd == BUILD_CMD_NEED &&
+ kit_slice_eq(dec_req.arg, KIT_SLICE_LIT("//lib:core")) &&
+ dec_req.noverrides == 1 &&
+ kit_slice_eq(dec_req.overrides[0].value, KIT_SLICE_LIT("2")) &&
+ dec_req.argc == 1 &&
+ kit_slice_eq(dec_req.argv[0], KIT_SLICE_LIT("--fast")),
+ "need roundtrip");
+
+ memset(&req, 0, sizeof req);
+ req.cmd = BUILD_CMD_GLOB;
+ memset(&resp, 0, sizeof resp);
+ resp.status = BUILD_RESP_OK;
+ resp.text = KIT_SLICE_LIT("src/a.c");
+ EXPECT(build_proto_encode_resp(&req, &resp, buf, sizeof buf, &n) == BUILD_OK,
+ "encode glob batch");
+ EXPECT(build_proto_decode_resp(buf, n, BUILD_CMD_GLOB, &dec_resp, glob_seen,
+ &seen) == BUILD_OK &&
+ seen == 1,
+ "decode glob batch");
+}
+
+static void test_bundle_manifest(void) {
+ BuildTraceClaim claims[2], parsed[2];
+ KitWriter* w = NULL;
+ KitSlice bytes;
+ size_t n = 0;
+ char err[128];
+ static const char bad[] =
+ "kit-build-traces 1\n"
+ "//z deep "
+ "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f "
+ "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20\n"
+ "//a deep "
+ "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f "
+ "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20\n";
+
+ memset(claims, 0, sizeof claims);
+ strcpy(claims[0].target, "//z");
+ claims[0].kind = (uint8_t)BUILD_TRACE_SHALLOW;
+ fill_id(claims[0].trace_id, 1);
+ fill_id(claims[0].output_tree, 2);
+ strcpy(claims[1].target, "//a");
+ claims[1].kind = (uint8_t)BUILD_TRACE_DEEP;
+ fill_id(claims[1].trace_id, 3);
+ fill_id(claims[1].output_tree, 4);
+
+ EXPECT(kit_writer_mem(&g_u.heap, &w) == KIT_OK && w, "bundle writer");
+ EXPECT(build_bundle_manifest_emit(claims, 2, w) == BUILD_OK,
+ "emit bundle manifest");
+ bytes.data = kit_writer_mem_bytes(w, &bytes.len);
+ EXPECT(build_bundle_manifest_parse(bytes.data, bytes.len, parsed, 2, &n, err,
+ sizeof err) == BUILD_OK &&
+ n == 2 && strcmp(parsed[0].target, "//a") == 0 &&
+ parsed[0].kind == (uint8_t)BUILD_TRACE_DEEP,
+ "parse sorted bundle manifest");
+ EXPECT(build_bundle_manifest_parse((const uint8_t*)bad, sizeof bad - 1u,
+ parsed, 2, &n, err,
+ sizeof err) == BUILD_ERR,
+ "reject unsorted bundle manifest");
+ kit_writer_close(w);
+}
+
+int main(void) {
+ uint8_t key1[BUILD_HASH_LEN], key2[BUILD_HASH_LEN];
+ BuildPathBlob paths[1];
+ uint8_t glob_id[BUILD_HASH_LEN];
+
+ kit_unit_init(&g_u);
+
+ EXPECT(build_target_key(KIT_SLICE_LIT("//app:bin"), key1) == BUILD_OK,
+ "target key");
+ EXPECT(build_target_key(KIT_SLICE_LIT("//app:bin"), key2) == BUILD_OK &&
+ build_id_eq(key1, key2),
+ "target key stable");
+ strcpy(paths[0].path, "src/a.c");
+ fill_id(paths[0].blob, 9);
+ EXPECT(build_glob_result_hash(&g_u.heap, paths, 1, glob_id) == BUILD_OK,
+ "glob result hash");
+
+ test_config_and_argv();
+ test_defn();
+ test_trace();
+ test_protocol();
+ test_bundle_manifest();
+
+ kit_unit_summary(&g_u, "build_pure_test");
+ return kit_unit_status(&g_u);
+}