# Makefile for pg_vault_tde (PGXS)
#
# All source modules are listed here. Extension compiles as a single .so.
# PGXS handles the include paths, DESTDIR, and make installcheck wiring.
#
# Hardware-accelerated AES (AES-NI, VAES, ARM Crypto Extensions, SVE2) is
# provided automatically at runtime by OpenSSL's EVP layer — see
# src/crypto/pg_vault_tde_hw_accel.c and `make check-cpu` / `make bench-cpu`
# below. There is no separate CPU-specific build: it would not make the
# crypto path any faster, since pg_vault_tde never implements AES itself.
#
# Optimization level:
#   make TDE_OPTIMIZE=standard            (default — -O2)
#   make TDE_OPTIMIZE=max                 (-O3 -funroll-loops -fomit-frame-pointer)

TDE_OPTIMIZE    ?= standard

# Optimization flags
ifeq ($(TDE_OPTIMIZE),max)
   TDE_OPT_CFLAGS := -O3 -funroll-loops -fomit-frame-pointer
else
   TDE_OPT_CFLAGS := -O2
endif

# ---------------------------------------------------------------------------
# PostgreSQL version validation
#
# Supported range: PG 17 .. PG 19.  Update TDE_PG_MAX when adding a new
# major version (see .github/copilot-instructions.md § 0.5).
# ---------------------------------------------------------------------------
TDE_PG_MIN := 17
TDE_PG_MAX := 19

PG_CONFIG   ?= pg_config
TDE_PG_MAJOR := $(shell $(PG_CONFIG) --version | sed 's/PostgreSQL //' | cut -d. -f1)
$(if $(shell [ $(TDE_PG_MAJOR) -lt $(TDE_PG_MIN) ] && echo fail), \
  $(error pg_vault_tde requires PostgreSQL >= $(TDE_PG_MIN), detected $(TDE_PG_MAJOR)))
$(if $(shell [ $(TDE_PG_MAJOR) -gt $(TDE_PG_MAX) ] && echo fail), \
  $(warning pg_vault_tde is untested on PostgreSQL $(TDE_PG_MAJOR) — max tested is $(TDE_PG_MAX)))

$(info === pg_vault_tde build: OPT=$(TDE_OPTIMIZE) PG=$(TDE_PG_MAJOR) ===)

EXTENSION   = pg_vault_tde
MODULE_big  = pg_vault_tde

OBJS = \
	src/pg_vault_tde.o \
	src/kms/pg_vault_tde_kms.o \
	src/kms/pg_vault_tde_kms_local.o \
	src/kms/pg_vault_tde_kms_pkcs11.o \
	src/kms/pg_vault_tde_catalog.o \
	src/kms/pg_vault_tde_rotation_bgw.o \
	src/kms/pg_vault_tde_seal.o \
	src/crypto/pg_vault_tde_crypto.o \
	src/crypto/pg_vault_tde_hw_accel.o \
	src/tam/pg_vault_tde_tam.o \
	src/tam/pg_vault_tde_toast.o \
	src/iam/pg_vault_tde_iam.o \
	src/logical/pg_vault_tde_pgoutput.o \
	src/logical/pg_vault_tde_rmgr.o

# SQL scripts installed as part of the extension.
DATA = sql/pg_vault_tde--1.7.sql 

# pg_regress test targets (filenames without .sql suffix)
REGRESS = pg_vault_tde_init

# Isolation test specs (test/isolation/{specs,expected}/per_table_dek_rotation.*)
ISOLATION = per_table_dek_rotation
ISOLATION_OPTS = --inputdir=test/isolation

PGXS        := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

TDE_BUILD_VERSION := $(shell cat VERSION)

# ---------------------------------------------------------------------------
# dist: build a PGXN-ready release zip (dist/pg_vault_tde-<version>.zip) from
# the current git HEAD. Requires META.json's "version" and the .control
# file's "default_version" to have been bumped together beforehand.
# See README.md § "Releasing to PGXN" for the full upload procedure.
# Lands in dist/ alongside the packaging/ .deb+.rpm build output — both are
# git-ignored release artifacts, never committed.
# ---------------------------------------------------------------------------
EXTVERSION := $(shell grep default_version $(EXTENSION).control | \
                sed -e "s/default_version[[:space:]]*=[[:space:]]*'\([^']*\)'/\1/")

.PHONY: dist
dist:
	mkdir -p dist
	git archive --format zip --prefix=$(EXTENSION)-$(EXTVERSION)/ \
	    --output ./dist/$(EXTENSION)-$(EXTVERSION).zip HEAD

# Extra compiler/linker flags — must come AFTER include $(PGXS) so they
# append to PGXS defaults rather than being overwritten by them.
#
# OpenSSL 3.x and libcurl are required. pkg-config locates them.
# -std=c99 enforces the language standard mandated by copilot-instructions.md.
# -Wall -Wextra catch common PostgreSQL extension pitfalls early.
# VERSION must be a single line with no trailing content; note that changing
# VERSION does not force a rebuild of already-compiled .o files under plain
# incremental `make` — a `make clean` is needed after bumping VERSION for the
# embedded build-version string to update (accepted limitation, not solved
# via fancier Make dependency tracking).
override CFLAGS  += -Wall -Wextra -std=c99 \
                    -Wno-unused-parameter \
                    -I$(srcdir)/src \
                    $(TDE_OPT_CFLAGS) \
                    $(shell pkg-config --cflags openssl libcurl) \
                    -DPG_VAULT_TDE_BUILD_VERSION='"$(TDE_BUILD_VERSION)"'
# -ldl: dlopen() of the vendor PKCS#11 module (pkcs11 KMS provider).
# No-op on glibc >= 2.34 (dlopen lives in libc) but required for portability.
override SHLIB_LINK += $(shell pkg-config --libs openssl libcurl) -ldl

# ---------------------------------------------------------------------------
# check-cpu: print CPU hardware encryption capabilities. This does not affect
# the build — OpenSSL detects and uses these instructions automatically at
# runtime regardless of how pg_vault_tde.so was compiled.
# ---------------------------------------------------------------------------
.PHONY: check-cpu
check-cpu:
	@echo "=== CPU Hardware Encryption Capabilities ==="
	@grep -m1 "model name" /proc/cpuinfo 2>/dev/null || sysctl -n machdep.cpu.brand_string 2>/dev/null || echo "Unknown CPU"
	@echo "x86 flags:" ; grep -m1 "^flags" /proc/cpuinfo 2>/dev/null | tr ' ' '\n' | grep -E "^(aes|avx|vaes|pclmulqdq|sse4)" | sort || echo "  not applicable"
	@echo "ARM features:" ; grep "^Features" /proc/cpuinfo 2>/dev/null | tr ' ' '\n' | grep -E "(aes|sha|pmull|crc)" | sort || echo "  not applicable"
	@openssl speed -evp aes-256-gcm 2>/dev/null | tail -3 || echo "  openssl speed not available"

# ---------------------------------------------------------------------------
# bench-cpu: quick OpenSSL AES-256-GCM benchmark + HW engine detection
# ---------------------------------------------------------------------------
.PHONY: bench-cpu
bench-cpu:
	@echo "=== pg_vault_tde: OpenSSL AES-256-GCM benchmark ==="
	@openssl speed -evp aes-256-gcm 2>&1 | grep -E "(Doing|aes)"
	@echo ""
	@echo "=== Check if AES-NI is loaded in OpenSSL ==="
	@openssl engine 2>/dev/null | grep -i "aesni" && echo "  AES-NI engine active" || echo "  Using default provider (auto-dispatch)"

# ===========================================================================
# Local CI Pipeline (containerized)
#
# All targets delegate to ci/scripts/ which auto-detect podman/docker.
# Override container runtime:  make ci-all CONTAINER_RT=docker
# ===========================================================================
.PHONY: ci-all ci-regress ci-checksums ci-tap ci-isolation ci-vault ci-wallet ci-pkcs11 ci-schema ci-bench ci-install-test ci-clean

ci-all:
	@bash ci/scripts/run-all.sh

ci-regress:
	@bash ci/scripts/run-regress.sh

ci-checksums:
	@bash ci/scripts/run-checksums.sh

ci-tap:
	@bash ci/scripts/run-tap.sh

ci-isolation:
	@bash ci/scripts/run-isolation.sh

ci-vault:
	@bash ci/scripts/run-vault.sh

ci-wallet:
	@bash ci/scripts/run-wallet.sh

ci-pkcs11:
	@bash ci/scripts/run-pkcs11.sh

ci-schema:
	@bash ci/scripts/run-schema.sh

ci-bench:
	@bash ci/scripts/run-bench.sh; rc=$$?; \
	if [ $$rc -eq 7 ]; then echo "ci-bench: avg overhead above threshold (non-fatal WARN, matches run-all.sh)"; exit 0; fi; \
	exit $$rc

ci-install-test:
	@bash ci/scripts/run-install-test.sh --all

ci-clean:
	@echo "Cleaning up CI containers and images ..."
	@RT=$$(command -v podman 2>/dev/null || echo docker); \
	$$RT rm -f pg-tde-test pg-tde-checksums pg-tde-vault vault-mock 2>/dev/null || true; \
	$$RT rmi -f pg-tde-test:latest vault-mock:latest 2>/dev/null || true; \
	echo "CI cleanup complete."

# Full pipeline alias
.PHONY: ci-full
ci-full: ci-all

# ---------------------------------------------------------------------------
# pg_dump_tde: standalone backup encryption wrapper for pg_dump
#
# This binary is NOT a PostgreSQL extension module (.so).  It is a standalone
# C tool that forks pg_dump, intercepts its stdout through a pipe, and
# re-encrypts each 64 KB block with AES-256-GCM before writing to disk.
#
# Build:    make pg_dump_tde
# Install:  make install-pg-dump-tde
# Clean:    make clean-pg-dump-tde
#
# The object files use a _bin.o suffix to avoid collisions with the _bin.o
# objects already compiled as -fPIC for the extension .so.
# ---------------------------------------------------------------------------
PG_DUMP_TDE_SHARED = \
	src/backup/pg_vault_tde_backup.c \
	src/backup/pg_dump_tde_kms_vault.c \
	src/backup/pg_dump_tde_kms_local.c 

PG_DUMP_TDE_SRCS = \
	$(PG_DUMP_TDE_SHARED) \
	src/backup/pg_dump_tde.c 

PG_RESTORE_TDE_SRCS = \
	$(PG_DUMP_TDE_SHARED) \
	src/backup/pg_restore_tde.c 

PG_DUMP_TDE_OBJS = $(PG_DUMP_TDE_SRCS:.c=_bin.o)
PG_RESTORE_TDE_OBJS = $(PG_RESTORE_TDE_SRCS:.c=_bin.o)

# pg_basebackup_tde: pg_basebackup wrapper that also stores one sealed
# wrapped-DEK bundle per database (via pg_vault_tde_seal_keys_bytea).
# libpq only: no frontend KMS, no block encryption — the physical files are
# already encrypted on disk.
PG_BASEBACKUP_TDE_SRCS = src/backup/pg_basebackup_tde.c
PG_BASEBACKUP_TDE_OBJS = $(PG_BASEBACKUP_TDE_SRCS:.c=_bin.o)

# pkg-config fallback: if not available, use well-known paths.
HAS_PKG_CONFIG := $(shell command -v pkg-config 2>/dev/null)

ifdef HAS_PKG_CONFIG
  OPENSSL_CFLAGS  := $(shell pkg-config --cflags openssl)
  OPENSSL_LIBS    := $(shell pkg-config --libs openssl)
  LIBCURL_CFLAGS  := $(shell pkg-config --cflags libcurl)
  LIBCURL_LIBS    := $(shell pkg-config --libs libcurl)
else
  OPENSSL_CFLAGS  :=
  OPENSSL_LIBS    := -lssl -lcrypto
  LIBCURL_CFLAGS  :=
  LIBCURL_LIBS    := -lcurl
endif

PG_DUMP_TDE_CFLAGS = \
	$(TDE_OPT_CFLAGS) \
	-Wall -Wextra -std=c99 \
	-Wno-unused-parameter \
	-DFRONTEND \
	-D_GNU_SOURCE \
	-I$(shell $(PG_CONFIG) --includedir) \
	-I$(shell $(PG_CONFIG) --includedir-server) \
	-Isrc \
	-Isrc/include \
	$(OPENSSL_CFLAGS) \
	$(LIBCURL_CFLAGS)

PG_DUMP_TDE_LDFLAGS = \
    -L$(shell $(PG_CONFIG) --libdir) \
    -L$(shell $(PG_CONFIG) --pkglibdir) \
    -lpq -lpgcommon -lpgport \
    $(OPENSSL_LIBS) \
    $(LIBCURL_LIBS)

# Pattern rule for _bin.o objects (standalone compilation, no -fPIC).
# Must be declared before include $(PGXS) would shadow it, but we define
# it after so PGXS %.o rules are not confused.
%_bin.o: %.c
	$(CC) $(PG_DUMP_TDE_CFLAGS) -c -o $@ $<

pg_dump_tde: $(PG_DUMP_TDE_OBJS)
	$(CC) -o $@ $^ $(PG_DUMP_TDE_LDFLAGS)

pg_restore_tde: $(PG_RESTORE_TDE_OBJS)
	$(CC) -o $@ $^ $(PG_DUMP_TDE_LDFLAGS)

pg_basebackup_tde: $(PG_BASEBACKUP_TDE_OBJS)
	$(CC) -o $@ $^ $(PG_DUMP_TDE_LDFLAGS)

# Hook into the standard PGXS targets so the frontend tools are always
# built, installed, and cleaned together with the extension.
all: pg_dump_tde pg_restore_tde pg_basebackup_tde

bindir := $(shell $(PG_CONFIG) --bindir)

.PHONY: install-pg-dump-tde
install-pg-dump-tde: pg_dump_tde pg_restore_tde pg_basebackup_tde
	install -m 755 pg_dump_tde $(bindir)/pg_dump_tde
	install -m 755 pg_restore_tde $(bindir)/pg_restore_tde
	install -m 755 pg_basebackup_tde $(bindir)/pg_basebackup_tde

install: install-pg-dump-tde

.PHONY: clean-pg-dump-tde
clean-pg-dump-tde:
	rm -f pg_dump_tde pg_restore_tde pg_basebackup_tde \
	    $(PG_DUMP_TDE_OBJS) $(PG_RESTORE_TDE_OBJS) $(PG_BASEBACKUP_TDE_OBJS)

clean: clean-pg-dump-tde
