kit

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

commit 31706f330ce37fb7b4f6d0239b1d8a32dae813f3
parent 38da65309e91f80e291d29534719835b64843062
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 17 Jun 2026 22:55:14 -0700

build: add coordinator foundation

Diffstat:
Minclude/kit/cas.h | 9+++++++++
Mmk/lib_srcs.mk | 12+++++++++---
Mmk/test.mk | 13+++++++++++++
Mmk/test_unit.mk | 5++++-
Asrc/api/build_coord.c | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/api/cas.c | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/api/config_stubs.c | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/build/build.c | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/build/bundle.c | 220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/build/cfg.c | 320+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/build/cfg.h | 2+-
Asrc/build/coord.c | 406+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/build/coord.h | 3+++
Asrc/build/defn.c | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/build/defn.h | 4+++-
Asrc/build/protocol.c | 355+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/build/protocol.h | 2+-
Asrc/build/remote.c | 344+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/build/resolve.c | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/build/runner.c | 40++++++++++++++++++++++++++++++++++++++++
Asrc/build/store.c | 450+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/build/trace.c | 1006+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/build/build_public_link_test.c | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/build/build_pure_test.c | 373+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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); +}