# Navette — agentic code editor (llama.cpp client).
# See NAVETTE.md for the full project specification.

PREFIX  ?= /usr/local
DESTDIR ?=

# Single source of truth for the version. src/main.c picks it up via
# -DNAVETTE_VERSION_STR below, and the tazpkg target stamps it into the
# staged receipt, so `navette --version` and the package can never
# disagree with each other.
VERSION := 0.4.1

CC      ?= cc
CFLAGS  ?= -O2 -Wall -Wextra -std=c11
# Feature-test macro, NOT part of CFLAGS. -std=c11 alone hides POSIX from
# the headers: struct timeval stays incomplete and realpath() is only
# implicitly declared, so server.c does not compile. A distro build system
# that exports its own CFLAGS would otherwise drop it -- SliTaz cook does
# exactly that (it forces -march=x86-64 -Os -pipe -fstack-protector-strong)
# and the build died on those two symbols until this moved out.
FEATFLAGS := -D_GNU_SOURCE
# Header dependency tracking. Without it, changing a struct or a signature
# in a .h does not rebuild the .o files that include it: the stale objects
# link against the new ones with a different struct layout and the binary
# segfaults with no warning (seen for real when a field was added to
# agent_cfg_t). -MMD writes a .d per object, -MP adds phony targets so a
# deleted header does not break the build. Kept separate from CFLAGS so an
# overriding CFLAGS= on the command line cannot silently drop it.
DEPFLAGS := -MMD -MP
LDFLAGS ?=
# Wide (UTF-8) ncurses: required so accented input renders as glyphs rather
# than meta-escapes. Asked of pkg-config so each distro states its own
# answer (Debian adds -ltinfo, SliTaz does not), falling back to the plain
# flag where no .pc is installed.
#
# pkg-config does not protect against everything: ld searches /lib before
# /usr/lib, and an out-of-date SliTaz ncursesw-dev put libncursesw.a in
# /lib while the .so was in /usr/lib, so -lncursesw quietly linked the
# STATIC library and the binary needed nothing but libc. ncursesw.pc
# reports libdir, not a corrective -L, so pkg-config could not see it
# either. The SliTaz receipt passes -L/usr/lib for that reason. Left out
# of here: it is a distro packaging accident, not something navette
# should carry.
LIBS    ?= $(shell pkg-config --libs ncursesw 2>/dev/null || echo -lncursesw)

SRCDIR  := src
TESTDIR := test
BUILD   := build
DATADIR := data

PROMPT_TXT  := $(DATADIR)/system_prompt.txt
PROMPT_HDR  := $(SRCDIR)/prompt_data.h

MODELS_CONF := $(DATADIR)/models.conf
MODELS_HDR  := $(SRCDIR)/models_conf_data.h

SKILL_RECEIPT_MD  := $(DATADIR)/skills/receipt.md
SKILL_RECEIPT_HDR := $(SRCDIR)/skill_receipt_data.h

CORE_SRCS := \
	$(SRCDIR)/util.c \
	$(SRCDIR)/log.c \
	$(SRCDIR)/json.c \
	$(SRCDIR)/xml.c \
	$(SRCDIR)/http.c \
	$(SRCDIR)/sse.c \
	$(SRCDIR)/engine.c \
	$(SRCDIR)/config.c \
	$(SRCDIR)/server.c \
	$(SRCDIR)/session.c \
	$(SRCDIR)/tools.c \
	$(SRCDIR)/tools/read_file.c \
	$(SRCDIR)/tools/write_file.c \
	$(SRCDIR)/tools/edit_file.c \
	$(SRCDIR)/tools/list_dir.c \
	$(SRCDIR)/tools/shell.c \
	$(SRCDIR)/tools/defaults.c \
	$(SRCDIR)/prompt.c \
	$(SRCDIR)/agent.c \
	$(SRCDIR)/render_cli.c \
	$(SRCDIR)/tui_buffer.c \
	$(SRCDIR)/render_tui.c \
	$(SRCDIR)/models.c \
	$(SRCDIR)/gguf.c \
	$(SRCDIR)/ctxsize.c \
	$(SRCDIR)/skills.c \
	$(SRCDIR)/doctor.c \
	$(SRCDIR)/ui.c

CORE_OBJS := $(CORE_SRCS:$(SRCDIR)/%.c=$(BUILD)/%.o)

MAIN_SRC    := $(SRCDIR)/main.c
MAIN_OBJ    := $(BUILD)/main.o
NAVETTE_BIN := navette

TEST_SRCS := \
	$(TESTDIR)/test_util.c \
	$(TESTDIR)/test_json.c \
	$(TESTDIR)/test_xml.c \
	$(TESTDIR)/test_sse.c \
	$(TESTDIR)/test_http.c \
	$(TESTDIR)/test_engine.c \
	$(TESTDIR)/test_config.c \
	$(TESTDIR)/test_server.c \
	$(TESTDIR)/test_session.c \
	$(TESTDIR)/test_tools_shell.c \
	$(TESTDIR)/test_tools_files.c \
	$(TESTDIR)/test_tui_buffer.c \
	$(TESTDIR)/test_prompt.c \
	$(TESTDIR)/test_agent.c \
	$(TESTDIR)/test_e2e.c \
	$(TESTDIR)/test_models.c \
	$(TESTDIR)/test_gguf.c \
	$(TESTDIR)/test_ctxsize.c \
	$(TESTDIR)/test_skills.c

TEST_BINS := $(TEST_SRCS:$(TESTDIR)/%.c=$(BUILD)/%)

MOCK_BIN  := $(BUILD)/mock_server
FAKE_BIN  := $(BUILD)/fake_llama

.PHONY: all build test asan fuzz fixtures install tazpkg version-check clean help

.DEFAULT_GOAL := all

all: build $(NAVETTE_BIN)

build: $(CORE_OBJS)

# The shipped binary. Tests link CORE_OBJS without main.o so they can
# define their own main(); only the navette binary pulls main.o in.
$(NAVETTE_BIN): $(MAIN_OBJ) $(CORE_OBJS)
	$(CC) $(CFLAGS) -o $@ $(MAIN_OBJ) $(CORE_OBJS) $(LDFLAGS) $(LIBS)

$(BUILD)/%.o: $(SRCDIR)/%.c
	@mkdir -p $(dir $@)
	$(CC) $(CFLAGS) $(FEATFLAGS) $(DEPFLAGS) -I$(SRCDIR) -c -o $@ $<

# main.c reports the version; the value comes from VERSION above rather
# than a second literal in the source. main.c keeps a #ifndef fallback so
# it still compiles if built outside this Makefile. Note that bumping
# VERSION does not by itself invalidate main.o -- `make clean` is part of
# the release sequence anyway, and version-check catches a stale binary.
$(MAIN_OBJ): CFLAGS += -DNAVETTE_VERSION_STR=\"$(VERSION)\"

# Embed the default system prompt as a C byte array. We `cd` into the
# data dir before invoking xxd so the generated symbol names depend
# only on the basename, not the full path. BusyBox xxd (used on
# SliTaz) does not support GNU xxd's -n flag, so the names come from
# the filename: `system_prompt_txt[]` and `system_prompt_txt_len`.
$(PROMPT_HDR): $(PROMPT_TXT)
	@cd $(DATADIR) && xxd -i $(notdir $<) > $(abspath $@)

$(BUILD)/prompt.o: $(PROMPT_HDR)

# Embed the default model catalog. Same xxd-in-DATADIR trick so the
# symbol names are `models_conf[]` and `models_conf_len` regardless
# of the absolute path. BusyBox xxd has no -n flag.
$(MODELS_HDR): $(MODELS_CONF)
	@cd $(DATADIR) && xxd -i $(notdir $<) > $(abspath $@)

$(BUILD)/models.o: $(MODELS_HDR)

# Embed the default `receipt` skill. xxd-in-DATADIR/skills so the symbols
# are `receipt_md[]` / `receipt_md_len` (BusyBox xxd has no -n flag).
$(SKILL_RECEIPT_HDR): $(SKILL_RECEIPT_MD)
	@cd $(DATADIR)/skills && xxd -i $(notdir $<) > $(abspath $@)

$(BUILD)/skills.o: $(SKILL_RECEIPT_HDR)

# Test binaries link against the full core. Phase A onwards the TUI
# renderer is in CORE_OBJS, so the same -lncurses the main binary uses
# is required here too even if no test exercises the renderer directly.
$(BUILD)/test_%: $(TESTDIR)/test_%.c $(CORE_OBJS)
	@mkdir -p $(BUILD)
	$(CC) $(CFLAGS) $(FEATFLAGS) $(DEPFLAGS) -I$(SRCDIR) -I$(TESTDIR) -DNAVETTE_BUILD_DIR=\"$(BUILD)\" -o $@ $< $(CORE_OBJS) $(LIBS)

$(MOCK_BIN): $(TESTDIR)/mock_server.c
	@mkdir -p $(BUILD)
	$(CC) $(CFLAGS) $(FEATFLAGS) -o $@ $<

$(FAKE_BIN): $(TESTDIR)/fake_llama.c
	@mkdir -p $(BUILD)
	$(CC) $(CFLAGS) $(FEATFLAGS) -o $@ $<

fixtures:
	@sh $(TESTDIR)/fixtures/gen.sh

test: $(TEST_BINS) $(MOCK_BIN) $(FAKE_BIN) fixtures
	@BUILD=$(BUILD) sh test/run_tests.sh

# Run the whole suite under AddressSanitizer + UndefinedBehaviorSanitizer.
# Its own object directory: sanitizer objects are not link-compatible with
# the normal ones and both would otherwise share build/. Not part of
# `make test` because it is several times slower; run it before a release
# and after touching anything that allocates.
ASAN_FLAGS := -fsanitize=address,undefined -fno-omit-frame-pointer
asan:
	@$(MAKE) --no-print-directory BUILD=build-asan \
		CFLAGS="-O1 -g -Wall -Wextra -std=c11 $(ASAN_FLAGS)" \
		LDFLAGS="$(ASAN_FLAGS)" test

# Structure-aware fuzzing of the tool-call parser. xml.c eats raw model
# output — the least trustworthy input navette handles, and a small model
# emits truncated blocks and stray delimiters routinely. Always built with
# the sanitizers: the pass condition is "no crash, no leak", which only
# means something under ASan. Seeds are fixed so a failure reproduces.
FUZZ_ITERS ?= 40000
FUZZ_SEEDS ?= 1 7 42 1337 99991
fuzz:
	@mkdir -p build-fuzz
	@$(CC) -O1 -g -Wall -Wextra -std=c11 $(FEATFLAGS) $(ASAN_FLAGS) \
		-I$(SRCDIR) -o build-fuzz/fuzz_xml $(TESTDIR)/fuzz_xml.c \
		$(SRCDIR)/xml.c $(SRCDIR)/util.c $(SRCDIR)/log.c
	@for s in $(FUZZ_SEEDS); do \
		build-fuzz/fuzz_xml $$s $(FUZZ_ITERS) || exit 1; \
	done

install: $(NAVETTE_BIN)
	@mkdir -p $(DESTDIR)$(PREFIX)/bin
	install -m 0755 $(NAVETTE_BIN) $(DESTDIR)$(PREFIX)/bin/$(NAVETTE_BIN)

# Build a local .tazpkg from the current tree. Stages the binary under
# build/tazpkg/navette-$(VERSION)/fs/ via `make install`, drops the
# receipt next to it, then invokes `tazpkg pack`. If the SliTaz tools
# are not installed, the target prints a notice and exits 0 — the
# release is not blocked on the dev machine lacking SliTaz tooling.
# Guard against a half-done release. The staged receipt is stamped from
# VERSION anyway, so the built package is always right -- but the receipt
# in the tree is what gets merged into the SliTaz wok, and a stale VERSION
# there would ship a package claiming the wrong release. Same for a binary
# built before a VERSION bump.
version-check: $(NAVETTE_BIN)
	@r=$$(sed -n 's/^VERSION="\(.*\)"/\1/p' pkg/tazpkg/receipt); \
	if [ "$$r" != "$(VERSION)" ]; then \
		echo "version drift: pkg/tazpkg/receipt says $$r, Makefile says $(VERSION)"; \
		exit 1; \
	fi
	@b=$$(./$(NAVETTE_BIN) --version | awk '{print $$2}'); \
	if [ "$$b" != "$(VERSION)" ]; then \
		echo "version drift: built binary says $$b, Makefile says $(VERSION)"; \
		echo "  (stale object file -- run 'make clean' then rebuild)"; \
		exit 1; \
	fi
	@echo "version-check: $(VERSION) consistent across Makefile, receipt and binary"

tazpkg: $(NAVETTE_BIN) version-check
	@command -v tazpkg >/dev/null 2>&1 || { \
		echo "tazpkg build requires SliTaz tools (tazpkg), run on target"; \
		exit 0; \
	}
	@rm -rf $(BUILD)/tazpkg
	@mkdir -p $(BUILD)/tazpkg/navette-$(VERSION)
	$(MAKE) install DESTDIR=$(BUILD)/tazpkg/navette-$(VERSION)/fs PREFIX=/usr
	@sed 's/^VERSION=.*/VERSION="$(VERSION)"/' pkg/tazpkg/receipt \
		> $(BUILD)/tazpkg/navette-$(VERSION)/receipt
	@cd $(BUILD)/tazpkg && tazpkg pack navette-$(VERSION)
	@mv $(BUILD)/tazpkg/navette-$(VERSION).tazpkg .
	@echo "built: navette-$(VERSION).tazpkg"

clean:
	rm -rf $(BUILD) build-asan build-fuzz $(NAVETTE_BIN) $(MODELS_HDR) $(SKILL_RECEIPT_HDR) navette-$(VERSION).tazpkg

# Pull in the generated header dependencies. Wildcard, not the object
# list: on a clean tree no .d exists yet and the first build creates them.
# `-include` so a missing file is not an error.
-include $(shell find $(BUILD) -name '*.d' 2>/dev/null)

help:
	@echo "Navette Makefile targets:"
	@echo "  make           build core modules and ./$(NAVETTE_BIN) binary"
	@echo "  make test      build and run test suite (offline)"
	@echo "  make asan      run the suite under ASan+UBSan (slower)"
	@echo "  make fuzz      fuzz the tool-call parser under ASan"
	@echo "  make fixtures  regenerate test/fixtures/*.http"
	@echo "  make install   install to \$$DESTDIR\$$PREFIX/bin"
	@echo "  make tazpkg    build navette-$(VERSION).tazpkg (SliTaz)"
	@echo "  make version-check  Makefile/receipt/binary agree on $(VERSION)"
	@echo "  make clean     remove build artifacts"
