# ci18n build rules
#
#   make           build the example and build-and-run the tests
#   make example   build the example only
#   make examples  build the three larger examples and check their output
#   make test      build and run the unit tests
#   make bench     measure speed and memory, see bench/
#   make clean     remove build artifacts
#
# Override the compiler or add flags without losing the defaults:
#   make CC=clang
#   make EXTRA_CFLAGS=-Werror

# Default to gcc, but honour CC from the environment or the command line.
# A plain `CC ?= gcc` would not work here: make predefines CC as `cc`.
ifeq ($(origin CC),default)
CC = gcc
endif

# The language standard is separate so CI can sweep it without restating the
# warning flags: make CSTD=c11
CSTD ?= c99

CFLAGS ?= -Wall -Wextra -std=$(CSTD)
CPPFLAGS += -I./include

ALL_CFLAGS = $(CFLAGS) $(EXTRA_CFLAGS) $(CPPFLAGS)

.PHONY: all clean example test test-threads test-shared test-po valgrind fuzz fuzz-run fuzz-replay fuzz-corpus cldr-samples cldr-numbers bench bench-threads bench-gettext examples test-mo test-compiled test-avr

all: example test

example: examples/example.c include/ci18n.h
	$(CC) $(ALL_CFLAGS) -o example examples/example.c

# Builds and runs the suite. The binary exits non-zero if any test fails.
test: tests/test_ci18n.c include/ci18n.h
	$(CC) $(ALL_CFLAGS) -o test_ci18n tests/test_ci18n.c
	./test_ci18n

# Thread-local context tests. Kept out of `all` because they need pthreads,
# which not every host running `make` will have.
test-threads: tests/test_thread_local.c include/ci18n.h
	$(CC) $(ALL_CFLAGS) -pthread -o test_thread_local tests/test_thread_local.c
	./test_thread_local

# Shared context tests, the CI18N_THREAD_SHARED mode. Needs pthreads, and is
# only meaningful under a thread sanitizer, which CI supplies.
# _POSIX_C_SOURCE because glibc hides pthread_rwlock_* in the strict ANSI mode
# that -std=c99 selects. The header says so too, rather than letting the
# declarations arrive implicitly.
test-shared: tests/test_thread_shared.c include/ci18n.h
	$(CC) $(ALL_CFLAGS) -D_POSIX_C_SOURCE=200809L -pthread \
		-o test_thread_shared tests/test_thread_shared.c
	./test_thread_shared

# Round trip through the .po converter. Needs python3, so it is not part of
# `all`, and the converter lives outside the library on purpose.
PYTHON ?= python3

test-po: tests/test_po_roundtrip.c tools/po2ci18n.py tests/po/sample.po include/ci18n.h
	$(PYTHON) tools/po2ci18n.py tests/po/sample.po -o po_roundtrip.txt
	$(CC) $(ALL_CFLAGS) -o test_po_roundtrip tests/test_po_roundtrip.c
	./test_po_roundtrip po_roundtrip.txt

# The .mo loader against the converter: msgfmt compiles the same sample in
# both byte orders, and each result must match what po2ci18n.py wrote.
# Needs msgfmt from GNU gettext.
test-mo: tests/test_po_roundtrip.c tools/po2ci18n.py tests/po/sample.po include/ci18n.h
	$(PYTHON) tools/po2ci18n.py tests/po/sample.po -o po_roundtrip.txt
	msgfmt --endianness=little -o po_roundtrip_le.mo tests/po/sample.po
	msgfmt --endianness=big -o po_roundtrip_be.mo tests/po/sample.po
	$(CC) $(ALL_CFLAGS) -o test_po_roundtrip tests/test_po_roundtrip.c
	./test_po_roundtrip po_roundtrip.txt po_roundtrip_le.mo po_roundtrip_be.mo

# Runs the suite under valgrind. Mostly overlapping with AddressSanitizer,
# which CI already runs, but not identically: valgrind sees uninitialised
# reads that ASan does not, and needs no instrumentation, so it also checks
# the code as an ordinary build produces it.
#
# Cannot be combined with a sanitizer build: valgrind and ASan both want to
# own the allocator.
VALGRIND ?= valgrind
VALGRIND_FLAGS ?= --error-exitcode=1 --leak-check=full --show-leak-kinds=all \
	--track-origins=yes --errors-for-leak-kinds=all

valgrind: tests/test_ci18n.c include/ci18n.h
	$(CC) $(ALL_CFLAGS) -g -O0 -o test_ci18n tests/test_ci18n.c
	$(VALGRIND) $(VALGRIND_FLAGS) ./test_ci18n

# Fuzzing the parser. libFuzzer ships with clang, so this uses clang whatever
# CC is set to; override with FUZZ_CC if yours lives elsewhere.
FUZZ_CC ?= clang
FUZZ_SECONDS ?= 60

fuzz: tests/fuzz_load_buffer.c include/ci18n.h
	$(FUZZ_CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CPPFLAGS) -g \
		-fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all \
		-o fuzz_load_buffer tests/fuzz_load_buffer.c

# The first directory is where libFuzzer writes what it finds, the second is
# read only, so the committed seeds stay as they are.
fuzz-run: fuzz
	mkdir -p fuzz_findings
	./fuzz_load_buffer fuzz_findings tests/fuzz_corpus \
		-max_total_time=$(FUZZ_SECONDS) -print_final_stats=1

# Replays one input without libFuzzer, so a crash found by CI can be
# reproduced with any compiler. INPUT names the file.
fuzz-replay: tests/fuzz_load_buffer.c include/ci18n.h
	$(CC) $(ALL_CFLAGS) -DCI18N_FUZZ_REPLAY -g -o fuzz_replay tests/fuzz_load_buffer.c
	./fuzz_replay $(INPUT)

# Replays the whole seed corpus. Useful as a plain regression check on a
# toolchain that has no libFuzzer at all.
fuzz-corpus: tests/fuzz_load_buffer.c include/ci18n.h
	$(CC) $(ALL_CFLAGS) -DCI18N_FUZZ_REPLAY -g -o fuzz_replay tests/fuzz_load_buffer.c
	@for input in tests/fuzz_corpus/*; do ./fuzz_replay "$$input" || exit 1; done

# Regenerate the CLDR sample table the unit tests check the plural and
# ordinal rules against. Fetches the release pinned in the script, so it
# needs a network; the table is committed, so nothing else does.
cldr-samples:
	$(PYTHON) tools/cldr_samples.py

# Regenerate the number symbol table in the header, and the cases the unit
# tests check it against. Fetches the pinned CLDR release, like the above.
cldr-numbers:
	$(PYTHON) tools/cldr_numbers.py

# Benchmarks. Optimised whatever CFLAGS says, since timing a -O0 build tells
# you nothing. Each prints a Markdown table; see bench/README.md.
BENCH_CFLAGS ?= -O2

bench: bench/bench.c include/ci18n.h
	$(CC) $(ALL_CFLAGS) $(BENCH_CFLAGS) -o bench_ci18n bench/bench.c
	./bench_ci18n

# One catalogue shared by 1 to 8 threads. Needs pthreads.
bench-threads: bench/bench_threads.c include/ci18n.h
	$(CC) $(ALL_CFLAGS) $(BENCH_CFLAGS) -D_POSIX_C_SOURCE=200809L -pthread \
		-o bench_threads bench/bench_threads.c
	./bench_threads

# The same lookups through glibc gettext. Linux only, needs msgfmt and the
# ru_RU.UTF-8 locale.
bench-gettext: bench/bench_gettext.c
	$(CC) $(ALL_CFLAGS) $(BENCH_CFLAGS) -o bench_gettext bench/bench_gettext.c
	./bench_gettext

# Compiled catalogues: the generator, then a test that loads every fixture
# both ways and demands the same answers, once as is and once in the shared
# threading mode. Then two things that must fail: a mistyped CI18N_KEY must
# not compile, and a file the loader would have to drop a line from must not
# generate.
COMPILED_DIR = build/compiled
COMPILE = ./ci18n_compile

test-compiled: tools/ci18n_compile.c tests/test_compiled.c tests/compiled/stress.txt \
		tests/avr/compiled.c include/ci18n.h
	mkdir -p $(COMPILED_DIR)
	$(CC) $(ALL_CFLAGS) -o ci18n_compile tools/ci18n_compile.c
	$(COMPILE) -o $(COMPILED_DIR)/stress.h stress tests/compiled/stress.txt
	$(COMPILE) -o $(COMPILED_DIR)/tr_en.h tr_en translations/en.txt
	$(COMPILE) -o $(COMPILED_DIR)/tr_ru.h tr_ru translations/ru.txt
	$(COMPILE) -o $(COMPILED_DIR)/tr_es.h tr_es translations/es.txt
	$(COMPILE) -o $(COMPILED_DIR)/sync_ru.h sync_ru examples/cli_sync/locales/ru.txt
	$(CC) $(ALL_CFLAGS) -I$(COMPILED_DIR) -o test_compiled tests/test_compiled.c
	./test_compiled
	$(CC) $(ALL_CFLAGS) -I$(COMPILED_DIR) -D_POSIX_C_SOURCE=200809L -DCI18N_THREAD_SHARED \
		-pthread -o test_compiled_shared tests/test_compiled.c
	./test_compiled_shared
	$(COMPILE) -o $(COMPILED_DIR)/en.h en tests/avr/en.txt
	$(COMPILE) -o $(COMPILED_DIR)/ru.h ru tests/avr/ru.txt
	$(CC) $(ALL_CFLAGS) -I$(COMPILED_DIR) -o test_compiled_copy tests/avr/compiled.c
	./test_compiled_copy
	! $(CC) $(ALL_CFLAGS) -I$(COMPILED_DIR) -c -o typo.o tests/compiled/typo.c 2>/dev/null
	! $(COMPILE) -o $(COMPILED_DIR)/broken.h broken examples/cli_sync/locales/de.txt 2>/dev/null
	@echo "test-compiled: the mistyped key and the broken file were both refused"

# The tests on 8-bit AVR under QEMU: the unit tests on an ATmega2560, and
# catalogues kept in flash on it and on an Arduino Uno. Needs avr-gcc,
# avr-libc and qemu-system-avr.
test-avr: include/ci18n.h
	HOSTCC="$(CC)" sh tests/avr/run.sh

# The three examples in examples/*/, each run and compared with the output it
# is known to give, so a change that alters what they print cannot slip by.
# The server needs pthreads. The checker exits 1 on purpose: one of its
# sample files is broken so there is something to report.
examples: examples/cli_sync/i18n_check.c examples/server/server.c \
		examples/embedded_ui/ui.c include/ci18n.h tools/ci18n_compile.c
	$(CC) $(ALL_CFLAGS) -o example_cli_sync examples/cli_sync/i18n_check.c
	$(CC) $(ALL_CFLAGS) -D_POSIX_C_SOURCE=200809L -pthread \
		-o example_server examples/server/server.c
	mkdir -p $(COMPILED_DIR)/ui
	$(CC) $(ALL_CFLAGS) -o ci18n_compile tools/ci18n_compile.c
	for l in en ru ar; do \
		$(COMPILE) -o $(COMPILED_DIR)/ui/$$l.h $$l examples/embedded_ui/locales/$$l.txt || exit 1; \
	done
	$(CC) $(ALL_CFLAGS) -Os -I$(COMPILED_DIR)/ui -o example_ui examples/embedded_ui/ui.c
	./example_cli_sync examples/cli_sync/locales/en.txt \
		examples/cli_sync/locales/ru.txt examples/cli_sync/locales/de.txt \
		> example_cli_sync.out; test $$? -eq 1
	diff -u examples/cli_sync/expected.txt example_cli_sync.out
	./example_server > example_server.out
	diff -u examples/server/expected.txt example_server.out
	./example_ui > example_ui.out
	diff -u examples/embedded_ui/expected.txt example_ui.out

# Installation. There is nothing to compile, so this copies one header and
# generates a pkg-config file next to it.
#
#   make install                     into /usr/local
#   make install PREFIX=$HOME/.local
#   make install DESTDIR=/tmp/stage  staging root for a package build
PREFIX ?= /usr/local
INCLUDEDIR ?= $(PREFIX)/include
PKGCONFIGDIR ?= $(PREFIX)/lib/pkgconfig

# One source of truth: the version comes out of the header it describes.
#
# Two make quirks to avoid here: a `)` in the sed script would close
# $(shell ...) early, and a `#` would start a comment and truncate the line.
# Hence no capture groups and no literal hash.
CI18N_VERSION = $(shell sed -n 's/.*CI18N_VERSION_STRING "//p' include/ci18n.h | tr -d '"')

.PHONY: install uninstall

install: include/ci18n.h ci18n.pc.in
	mkdir -p "$(DESTDIR)$(INCLUDEDIR)" "$(DESTDIR)$(PKGCONFIGDIR)"
	cp include/ci18n.h "$(DESTDIR)$(INCLUDEDIR)/ci18n.h"
	sed -e 's|@PREFIX@|$(PREFIX)|' \
	    -e 's|@INCLUDEDIR@|$(INCLUDEDIR)|' \
	    -e 's|@VERSION@|$(CI18N_VERSION)|' \
	    ci18n.pc.in > "$(DESTDIR)$(PKGCONFIGDIR)/ci18n.pc"
	@echo "installed ci18n $(CI18N_VERSION) to $(DESTDIR)$(INCLUDEDIR)"

uninstall:
	$(RM) "$(DESTDIR)$(INCLUDEDIR)/ci18n.h"
	$(RM) "$(DESTDIR)$(PKGCONFIGDIR)/ci18n.pc"

# $(RM) is `rm -f`, which make runs through its shell. Both name variants are
# listed because MinGW gcc appends .exe to an extensionless -o target.
#
# Do not switch on $(OS) here: it reads Windows_NT under Git Bash too, and the
# cmd.exe form of this rule then leaves a junk file named `nul` in the tree.
clean:
	$(RM) example example.exe test_ci18n test_ci18n.exe \
		test_thread_local test_thread_local.exe \
		fuzz_load_buffer fuzz_load_buffer.exe fuzz_replay fuzz_replay.exe \
		test_po_roundtrip test_po_roundtrip.exe po_roundtrip.txt \
		po_roundtrip_le.mo po_roundtrip_be.mo \
		test_thread_shared test_thread_shared.exe \
		bench_ci18n bench_ci18n.exe bench_threads bench_threads.exe \
		bench_gettext bench_gettext.exe \
		example_cli_sync example_cli_sync.exe example_cli_sync.out \
		example_server example_server.exe example_server.out \
		example_ui example_ui.exe example_ui.out \
		ci18n_compile ci18n_compile.exe test_compiled test_compiled.exe \
		test_compiled_shared test_compiled_shared.exe \
		test_compiled_copy test_compiled_copy.exe typo.o
	$(RM) -r build/compiled
