SHELL=bash
BINARY=chalk
CHALK_BUILD?=release
CHALK_ARGS=

INTERACTIVE := $(shell [ -t 0 ] && echo interactive)

# auto-clone sibling repos if not present; SSH in interactive sessions, HTTPS in CI
# $(1) = repo name, $(2) = destination path, $(3) = branch/commit
ifeq "$(INTERACTIVE)" "interactive"
CON4M_VERSION ?= dev
NIMUTILS_VERSION ?= dev
define clone-sibling
git clone git@github.com:crashappsec/$(1).git $(2)
git --git-dir=$(2)/.git --work-tree=$(2) checkout $(3)
endef
else
CON4M_VERSION ?= $(shell awk '/con4m:/{print $$NF}' chalk.nimble)
NIMUTILS_VERSION ?= $(shell awk '/nimutils:/{print $$NF}' chalk.nimble)
define clone-sibling
git init $(2)
git -C $(2) fetch --depth=1 https://github.com/crashappsec/$(1).git $(3)
git -C $(2) checkout FETCH_HEAD
endef
endif

ifeq "$(CHALK_BUILD)" "debug"
export DEBUG=true
CHALK_ARGS=--log-level=trace
endif

_DOCKER_ARGS=
_DOCKER=docker compose run --rm $(_DOCKER_ARGS) chalk
ifneq "$(shell which docker 2> /dev/null)" ""
DOCKER?=$(_DOCKER)
endif

SOURCES=$(wildcard *.nims)
SOURCES+=$(wildcard *.nimble)
SOURCES+=nimble.paths
SOURCES+=$(shell find src/ -name '*.nim')
SOURCES+=$(shell find src/ -name '*.c4m')
SOURCES+=$(shell find src/ -name '*.c42spec')
SOURCES+=$(shell find src/ -name '*.md')
SOURCES+=$(shell find src/ -name '*.c')
SOURCES+=$(shell find ../con4m -name '*.nim' 2> /dev/null)
SOURCES+=$(shell find ../con4m -name '*.c4m' 2> /dev/null)
SOURCES+=$(shell find ../nimutils -name '*.nim' 2> /dev/null)
SOURCES+=$(shell find ../nimutils -name '*.c' 2> /dev/null)
SOURCES+=src/docs/CHANGELOG.md

VERSION=$(shell cat src/configs/base_keyspecs.c4m \
          | grep -E "chalk_version\s+:=" | cut -d'"' -f2 | head -n1)

BUILDS=$(shell cat *.nimble | grep -E '^task.*build' | cut -d, -f1 | awk '{print $$2}')

# in case nimble bin is not in PATH - e.g. vanilla shell
export PATH:=$(HOME)/.nimble/bin:$(PATH)

# not PHONY jobs on purpose but instead rebuilds chalk
# when any of the nim sources change
# (a.k.a what Makefile is good at :D)
$(BINARY): $(BINARY).bck
	cp $^ $@
# automatically run setup to configure cosign for easy local testing
ifneq "$(CHALK_PASSWORD)" ""
	./$@ setup $(CHALK_ARGS)
endif

# as chalk load modifies existing binary,
# when no source files change, recopy the backup
# to get back to original compiled binary
$(BINARY).bck: $(SOURCES)
ifneq "$(TMUX)" ""
	@test -t 0 && reset || true
	@test -t 0 && tmux clear-history || true
endif
	$(DOCKER) nimble -y $(CHALK_BUILD); \
		_rc=$$?; \
		touch $(WATCH_DONE); \
		exit $$_rc
	mv $(BINARY) $@
	cp $@ $(BINARY)
	ls -la $(BINARY) $@

debug: DEBUG=true
$(BUILDS):
	$(eval export CHALK_BUILD=$@)
	$(eval export DEBUG)
	-rm -f $(BINARY) $(BINARY).bck
	$(MAKE) $(BINARY)

src/docs/CHANGELOG.md: CHANGELOG.md
	cp $^ $@

.PHONY: version
version:
	  @echo $(VERSION)

.PHONY: clean
clean:
	-$(DOCKER) rm -rf $(BINARY) $(BINARY).bck dist nimble.develop nimble.paths

# WATCH_LOG  - build output is tee'd here on every rebuild cycle.
#              Truncated once when "make watch" starts (tee opens for write);
#              subsequent rebuilds append to the same file until watch restarts.
#              A successful build ends with the "mv chalk chalk.bck" line.
#              Any compile error appears before that line.
#
# WATCH_DONE - touched after every nimble invocation (success or failure) so
#              that watch automation can wait on a single file event rather than
#              polling. Use "echo $(WATCH_DONE) | entr -npz tail -5 $(WATCH_LOG)"
#              to block until a cycle completes, then inspect the log.
#              NOTE: the touch runs unconditionally (exit code saved and
#              restored) so Make correctly fails the target on build errors.
WATCH_LOG  ?= /tmp/chalk-watch.log
WATCH_DONE ?= /tmp/chalk-watch-done

watch: $(SOURCES)
	echo $^ | tr ' ' '\n' | entr $(MAKE) 2>&1 | tee $(WATCH_LOG)

../nimutils/README.md:
	$(call clone-sibling,nimutils,$(@D),$(NIMUTILS_VERSION))

../con4m/README.md:
	$(call clone-sibling,con4m,$(@D),$(CON4M_VERSION))

# On macOS the build runs on the host (no Docker). Populate ~/.local/c0 with
# the pre-built static libs from nimutils/files/deps/ and the bundled C
# headers from nimutils/nimutils/c/ - exactly what the Dockerfile does for
# Linux. Uses libgit2.a as a stamp: skip if already present.
ifeq "$(shell uname -s 2>/dev/null)" "Darwin"
MACOS_DEPS_STAMP := $(HOME)/.local/c0/libs/libgit2.a

$(MACOS_DEPS_STAMP): ../nimutils/README.md
	bash ../nimutils/bin/buildlibs.sh ../nimutils/files/deps
	NIMUTILS_DIR=../nimutils bash ../nimutils/bin/header_install.sh

nimble.paths: $(MACOS_DEPS_STAMP)
endif

nimble.paths: ../nimutils/README.md ../con4m/README.md
	echo '--path:"$(abspath $(dir $(shell find ../nimutils -name nimutils.nim)))"' > $@
	echo '--path:"$(abspath $(dir $(shell find ../con4m -name con4m.nim)))"' >> $@

# ----------------------------------------------------------------------------
# TESTS

# needed for the registry tests
export IP=$(firstword $(shell hostname -I 2> /dev/null || hostname -i 2> /dev/null))

# normally this is not required to run on a host
# as it always runs within the test container
# but is provided as separate make target to provision
# acceptable builder instance locally for multi-platform builds
.PHONY: docker-builder
docker-builder:
	./tests/functional/entrypoint.sh true

# docker buildx - each builder node has its own config
# how it handles insecure registries however as chalk
# splits 'docker buildx --push' into:
# * docker buildx build
# * docker push
# and `docker push` does not honor buildx registry configs
# we need to adjust docker daemon configs to allow insecure
# registry which is used in tests
.PHONY: /etc/docker/daemon.json
/etc/docker/daemon.json:
ifneq "$(shell which systemctl 2> /dev/null)" ""
	sudo -E python3 ./tests/functional/data/templates/docker/daemon.py $@ --write --fail-on-changes \
		|| sudo systemctl restart docker \
		|| echo Please restart docker daemon after changing docker config
endif

$(HOME)/.pdbrc.py:
	touch $@

.PHONY: docker-setup
docker-setup: /etc/docker/daemon.json
docker-setup: /etc/docker/certs.d/$(IP)\:5045/ca.crt
# this actually gets used by 5046 which tests whether 5046 can be
# connected to with insecure https
docker-setup: /etc/docker/certs.d/$(IP)\:5044/ca.crt

/etc/docker/certs.d/$(IP)\:%/ca.crt:
	sudo mkdir -p $(@D)
	sudo mkdir -p /etc/docker/keys/$(IP):$*
	sudo openssl req \
	  -newkey rsa:2048 \
	  -nodes -sha256 \
	  -subj '/CN=$(IP)/O=CrashOverride./C=US' \
	  -addext "subjectAltName = IP:$(IP)" \
	  -x509 \
	  -days 365 \
	  -keyout /etc/docker/keys/$(IP):$*/ca.key \
	  -out $@

.PHONY: tests
tests: DOCKER=$(_DOCKER) # force rebuilds to use docker to match tests
tests: $(HOME)/.pdbrc.py
tests: docker-setup
tests: $(BINARY) # note this will rebuild chalk if necessary
	CI=true docker compose run --rm tests $(make_args) $(args)

.PHONY: unit-tests
unit-tests:
	CI=true $(DOCKER) nimble test args='$(args)'

.PHONY: parallel
tests_parallel: make_args=-nauto
tests_parallel: tests

.PHONY: tests_bash
tests_bash:
	docker compose run --rm --no-deps tests bash

.PHONY: src/utils/pingttl
src/utils/pingttl: src/utils/pingttl.nim
	$(DOCKER) nim c -r $< 1.1.1.1 1
	-docker compose run --rm --no-deps --entrypoint=strace tests -- $$(pwd)/$@ 1.1.1.1 1
	-docker compose run --rm --no-deps --entrypoint=strace tests -- $$(pwd)/$@ 1.1.1.1 2

# ----------------------------------------------------------------------------
# MISC

.PHONY: lint-dust
lint-dust:
	pre-commit run --files ./dust-lambda-extension/**

.PHONY: lint-all
lint-all:
	pre-commit run --all-files
