#   nsjail - Makefile
#   -----------------------------------------

PKG_CONFIG := $(shell command -v pkg-config 2> /dev/null)
ifeq ($(PKG_CONFIG),)
$(error "Install pkg-config to make it work")
endif

CC ?= gcc
CXX ?= g++

# pkg-config for protobuf can be expensive/slow
# Skip pkg-config for: clean, indent, kafel_init, and kafel-only builds
ifneq ($(filter-out clean indent kafel_init kafel/libkafel.a,$(MAKECMDGOALS)),)
PROTOBUF_CFLAGS := $(shell pkg-config --cflags protobuf)
PROTOBUF_LIBS   := $(shell pkg-config --libs protobuf)
else ifeq ($(MAKECMDGOALS),)
# Default target (all) requires protobuf
PROTOBUF_CFLAGS := $(shell pkg-config --cflags protobuf)
PROTOBUF_LIBS   := $(shell pkg-config --libs protobuf)
endif

NL3_EXISTS := $(shell pkg-config --exists libnl-route-3.0 && echo yes)

COMMON_FLAGS += -O2 -c \
	-D_GNU_SOURCE -D_FILE_OFFSET_BITS=64 \
	-fPIE \
	-Wformat -Wformat-security -Wno-format-nonliteral \
	-Wall -Wextra -Werror \
	-Ikafel/include

CXXFLAGS += $(USER_DEFINES) $(COMMON_FLAGS) $(PROTOBUF_CFLAGS) \
	-std=c++20 -fno-exceptions -Wno-unused -Wno-unused-parameter

LDFLAGS += -pie -Wl,-z,noexecstack -lpthread $(PROTOBUF_LIBS)

ifeq ($(NL3_EXISTS), yes)
	CXXFLAGS += $(shell pkg-config --cflags libnl-route-3.0)
	LDFLAGS += $(shell pkg-config --libs libnl-route-3.0)
endif

ifdef DEBUG
	CXXFLAGS += -g -ggdb -gdwarf-4
endif

BIN = nsjail
LIBS = kafel/libkafel.a

# If PASTA_BIN_PATH is not provided in env, dynamically search for it if EMBED_PASTA is requested
# or fallback to it naturally.
ifdef EMBED_PASTA
	ifeq ($(PASTA_BIN_PATH),)
		PASTA_BIN_PATH := $(shell which pasta)
	endif
ifeq ($(PASTA_BIN_PATH),)
$(error "'pasta' binary not found. Provide location with PASTA_BIN_PATH")
endif
endif

ifneq ($(PASTA_BIN_PATH),)
	CXXFLAGS += -DPASTA_BIN_PATH='"$(PASTA_BIN_PATH)"'
endif

SRCS_CXX = caps.cc cgroup.cc cgroup2.cc cmdline.cc config.cc contain.cc cpu.cc logs.cc mnt.cc mnt_legacy.cc mnt_newapi.cc net.cc nsjail.cc pid.cc sandbox.cc subproc.cc uts.cc user.cc util.cc
SRCS_PROTO = config.proto

SRCS_PB_CXX = $(SRCS_PROTO:.proto=.pb.cc)
SRCS_PB_H = $(SRCS_PROTO:.proto=.pb.h)
SRCS_PB_O = $(SRCS_PROTO:.proto=.pb.o)

OBJS = $(SRCS_CXX:.cc=.o) $(SRCS_PB_CXX:.cc=.o)

# 4. TARGETS

.PHONY: all clean depend indent kafel_init

all: $(BIN)

# Main Binary Linkage
$(BIN): $(LIBS) $(OBJS)
ifneq ($(NL3_EXISTS), yes)
	$(warning "You probably miss libnl3(-dev)/libnl-route-3(-dev) libraries")
endif
	$(CXX) -o $(BIN) $(OBJS) $(LIBS) $(LDFLAGS)

# Standard Object Compilation
# The | $(SRCS_PB_H) ensures headers exist before we try to compile .cc files
%.o: %.cc | $(SRCS_PB_H)
	$(CXX) $(CXXFLAGS) $< -o $@

# Protobuf Generation
# We only define the recipe for the .cc file to prevent race conditions.
$(SRCS_PB_CXX): $(SRCS_PROTO)
	protoc --cpp_out=. $(SRCS_PROTO)

# The .h file is a side-effect of the .cc rule
$(SRCS_PB_H): $(SRCS_PB_CXX)

# Kafel Submodule Handling
kafel_init:
ifeq ("$(wildcard kafel/Makefile)","")
	git submodule update --init
endif

kafel/include/kafel.h: kafel_init

kafel/libkafel.a: kafel_init
	+LDFLAGS="" CFLAGS=-fPIE $(MAKE) -C kafel

# Utilities
clean:
	$(RM) core Makefile.bak $(OBJS) $(SRCS_PB_CXX) $(SRCS_PB_H) $(SRCS_PB_O) $(BIN)
ifneq ("$(wildcard kafel/Makefile)","")
	+$(MAKE) -C kafel clean
endif

depend: all
	makedepend -Y -Ykafel/include -- -- $(SRCS_CXX) $(SRCS_PB_CXX)

indent:
	clang-format -i -sort-includes $(SRCS_CXX:.cc=.h) macros.h $(SRCS_CXX) $(SRCS_PROTO) configs/*.json

# Install
PREFIX ?= /usr
BINDIR ?= $(PREFIX)/bin
MANDIR ?= $(PREFIX)/share/man/man1

.PHONY: install
install: $(BIN)
	install -m 755 -d $(DESTDIR)$(BINDIR)
	install -m 755 $(BIN) $(DESTDIR)$(BINDIR)
	install -m 755 -d $(DESTDIR)$(MANDIR)
	install -m 644 nsjail.1 $(DESTDIR)$(MANDIR)

define run_test
	@echo "Testing: $(1) (expecting exit code $(2))"; \
	($(1)); ret=$$?; \
	if [ "$$ret" -ne "$(2)" ]; then \
		echo "❌ FAIL: '$(1)' returned $$ret, expected $(2)"; \
		exit 1; \
	else \
		echo "✅ PASS: '$(1)' returned $$ret"; \
	fi
endef

OLD_EF := --experimental_mnt=old
NEW_EF := --experimental_mnt=new
UID := $(shell id -u)

.PHONY: test
test: $(BIN)
	$(call run_test, ./nsjail $(OLD_EF) -q -Mo --rw --chroot / --user 99999 --group 99999 -- /bin/bash -c 'touch $(HOME)/nsjail_test && exit 77', 77)
	$(call run_test, ./nsjail $(OLD_EF) -q -Mo --chroot / --user 99999 --group 99999 -- /bin/bash -c 'touch $(HOME)/nsjail_test || exit 77', 77)
	$(call run_test, rm -f $(HOME)/nsjail_test, 0)
	$(call run_test, ./nsjail $(OLD_EF) -q -Mo --chroot / -m none:/tmp:tmpfs --user 99999 --group 99999 -- /bin/bash -c 'touch /tmp/nsjail_test && rm -f /tmp/nsjail_test', 0)
	$(call run_test, ./nsjail $(OLD_EF) -q -Mo --chroot / -m none:/tmp:tmpfs:rw --user 99999 --group 99999 -- /bin/bash -c 'touch /tmp/nsjail_test && rm -f /tmp/nsjail_test', 0)
	$(call run_test, ./nsjail $(OLD_EF) -q -Mo --chroot / -m none:/tmp:tmpfs:ro --user 99999 --group 99999 -- /bin/bash -c 'touch /tmp/nsjail_test || exit 77', 77)
	$(call run_test, ./nsjail $(OLD_EF) -q -Mo --chroot / -R /tmp --user 99999 --group 99999 -- /bin/bash -c 'touch /tmp/nsjail_test || exit 77', 77)
	$(call run_test, ./nsjail $(OLD_EF) -q -Mo --chroot / -B /tmp --user 99999 --group 99999 -- /bin/bash -c 'touch /tmp/nsjail_test && rm -f /tmp/nsjail_test', 0)
	$(call run_test, ./nsjail $(OLD_EF) -q -Mo --chroot / --user 99999 --group 99999 -- /bin/bash -c 'touch /run/user/$(UID)/nsjail_test2 && exit 77', 77) # --rw (or lack of thereof) doesn't change already mounted tmpfs
	$(call run_test, ./nsjail $(OLD_EF) -q -Mo --chroot / --user 99999 --group 99999 --rw -- /bin/bash -c 'touch /run/user/$(UID)/nsjail_test2 && exit 77', 77) # --rw (or lack of thereof) doesn't affect already mounted tmpfs
	$(call run_test, rm -f /run/user/$(UID)/nsjail_test2, 0)
	$(call run_test, ./nsjail $(OLD_EF) --config configs/bash-with-fake-geteuid.cfg -q -t 1 < /dev/null, 0)
	$(call run_test, ./nsjail $(OLD_EF) --config configs/bash-with-fake-geteuid.json -q -t 1 < /dev/null, 0)
	$(call run_test, ./nsjail $(OLD_EF) --config configs/static-busybox-with-execveat.cfg -q -t 1, 137)
	$(call run_test, ./nsjail $(OLD_EF) --config configs/home-documents-with-xorg-no-net.cfg -q -- /bin/true, 0)
	$(call run_test, ./nsjail $(OLD_EF) --config configs/home-documents-with-xorg-no-net.cfg -q -- /bin/false, 1)
	$(call run_test, ./nsjail $(OLD_EF) --config configs/firefox-with-net-X11.cfg -q -t 3, 137)
	$(call run_test, ./nsjail $(OLD_EF) --config configs/firefox-with-net-wayland.cfg -q -t 3, 137)
	$(call run_test, ./nsjail $(OLD_EF) --config configs/chromium-with-net-wayland.cfg -q -t 3, 137)
	$(call run_test, ./nsjail $(NEW_EF) -q -Mo --rw --chroot / --user 99999 --group 99999 -- /bin/bash -c 'touch $(HOME)/nsjail_test && exit 77', 77)
	$(call run_test, ./nsjail $(NEW_EF) -q -Mo --chroot / --user 99999 --group 99999 -- /bin/bash -c 'touch $(HOME)/nsjail_test || exit 77', 77)
	$(call run_test, rm -f $(HOME)/nsjail_test, 0)
	$(call run_test, ./nsjail $(NEW_EF) -q -Mo --chroot / -m none:/tmp:tmpfs --user 99999 --group 99999 -- /bin/bash -c 'touch /tmp/nsjail_test && rm -f /tmp/nsjail_test', 0)
	$(call run_test, ./nsjail $(NEW_EF) -q -Mo --chroot / -m none:/tmp:tmpfs:rw --user 99999 --group 99999 -- /bin/bash -c 'touch /tmp/nsjail_test && rm -f /tmp/nsjail_test', 0)
	$(call run_test, ./nsjail $(NEW_EF) -q -Mo --chroot / -m none:/tmp:tmpfs:ro --user 99999 --group 99999 -- /bin/bash -c 'touch /tmp/nsjail_test || exit 77', 77)
	$(call run_test, ./nsjail $(NEW_EF) -q -Mo --chroot / -R /tmp --user 99999 --group 99999 -- /bin/bash -c 'touch /tmp/nsjail_test || exit 77', 77)
	$(call run_test, ./nsjail $(NEW_EF) -q -Mo --chroot / -B /tmp --user 99999 --group 99999 -- /bin/bash -c 'touch /tmp/nsjail_test && rm -f /tmp/nsjail_test', 0)
	$(call run_test, ./nsjail $(NEW_EF) -q -Mo --chroot / --user 99999 --group 99999 -- /bin/bash -c 'touch /run/user/$(UID)/nsjail_test2 || exit 77', 77)
	$(call run_test, ./nsjail $(NEW_EF) -q -Mo --chroot / --user 99999 --group 99999 --rw -- /bin/bash -c 'touch /run/user/$(UID)/nsjail_test2 && exit 77', 77)
	$(call run_test, rm -f /run/user/$(UID)/nsjail_test2, 0)
	$(call run_test, ./nsjail $(NEW_EF) --config configs/bash-with-fake-geteuid.cfg -q -t 1 < /dev/null, 0)
	$(call run_test, ./nsjail $(NEW_EF) --config configs/bash-with-fake-geteuid.json -q -t 1 < /dev/null, 0)
	$(call run_test, ./nsjail $(NEW_EF) --config configs/static-busybox-with-execveat.cfg -q -t 1, 137)
	$(call run_test, ./nsjail $(NEW_EF) --config configs/home-documents-with-xorg-no-net.cfg -q -- /bin/true, 0)
	$(call run_test, ./nsjail $(NEW_EF) --config configs/home-documents-with-xorg-no-net.cfg -q -- /bin/false, 1)
	$(call run_test, ./nsjail $(NEW_EF) --config configs/firefox-with-net-X11.cfg -q -t 3, 137)
	$(call run_test, ./nsjail $(NEW_EF) --config configs/firefox-with-net-wayland.cfg -q -t 3, 137)
	$(call run_test, ./nsjail $(NEW_EF) --config configs/chromium-with-net-wayland.cfg -q -t 3, 137)

# Dependencies (Generated by makedepend)
# DO NOT DELETE THIS LINE -- make depend depends on it.

caps.o: caps.h nsjail.h config.pb.h logs.h macros.h util.h
cgroup.o: cgroup.h nsjail.h config.pb.h logs.h util.h
cgroup2.o: cgroup2.h nsjail.h config.pb.h logs.h util.h
cmdline.o: cmdline.h nsjail.h config.pb.h caps.h config.h logs.h macros.h
cmdline.o: mnt.h mnt_newapi.h user.h util.h
config.o: config.h nsjail.h config.pb.h caps.h cmdline.h logs.h macros.h
config.o: mnt.h user.h util.h
contain.o: contain.h nsjail.h config.pb.h caps.h cgroup.h cgroup2.h config.h
contain.o: cpu.h logs.h macros.h mnt.h net.h pid.h user.h util.h uts.h
cpu.o: cpu.h nsjail.h config.pb.h logs.h util.h
logs.o: logs.h macros.h util.h nsjail.h config.pb.h
mnt.o: mnt.h nsjail.h config.pb.h logs.h macros.h mnt_legacy.h mnt_newapi.h
mnt.o: subproc.h util.h
mnt_legacy.o: mnt_legacy.h mnt.h nsjail.h config.pb.h logs.h macros.h util.h
mnt_newapi.o: mnt_newapi.h mnt.h nsjail.h config.pb.h logs.h util.h
net.o: net.h nsjail.h config.pb.h logs.h macros.h util.h
nsjail.o: nsjail.h config.pb.h cgroup2.h cmdline.h logs.h macros.h net.h
nsjail.o: sandbox.h subproc.h util.h
pid.o: pid.h nsjail.h config.pb.h logs.h subproc.h
sandbox.o: sandbox.h nsjail.h config.pb.h kafel/include/kafel.h logs.h util.h
subproc.o: subproc.h nsjail.h config.pb.h cgroup.h cgroup2.h contain.h logs.h
subproc.o: macros.h net.h sandbox.h user.h util.h
uts.o: uts.h nsjail.h config.pb.h logs.h
user.o: user.h nsjail.h config.pb.h logs.h macros.h subproc.h util.h
util.o: util.h nsjail.h config.pb.h logs.h macros.h
config.pb.o: config.pb.h
