# SPDX-License-Identifier: GPL-2.0-only
obj-m += scradle.o
scradle-y := main.o tracer.o cow.o snapdev.o ksyms.o
# The writable-snapshot write overlay. Unconditional, like everything else
# here: the file compiles to a handful of functions that return immediately
# unless SCRADLE_SNAP_WRITABLE was asked for, and a build-time switch would
# mean the flag is accepted by some builds of one version and refused by
# others, which is exactly the ambiguity the flag exists to remove.
scradle-y += overlay.o
# The two caller-supplied region maps (the free-space map and the COW exclusion
# list). One file because they are one implementation of the whole-chunk rule
# and the argument for why a chunk may be skipped; splitting them is how the
# two rules would drift apart. Unconditional: the ioctls are ABI.
scradle-y += freespace.o
# Module parameters, in one file rather than beside each user: the set of
# things an operator may change is itself an interface, and one that is
# spread over six files is one nobody can review.
scradle-y += params.o
# /sys/class/scradle. Unconditional and with no CONFIG_ guard at all, unlike
# debugfs and /proc: a kernel that can load a module has sysfs.
scradle-y += sysfs.o
# Persistent changed-block tracking. Unconditional: the file compiles to a
# few functions that do nothing at all unless an attach asks for them, and a
# build-time switch would mean the ioctl surface differs between builds of
# the same version, which is worse than the handful of kilobytes.
scradle-y += state.o
# Observability export. Compiled unconditionally and empty when the kernel
# has no CONFIG_DEBUG_FS -- keying the Makefile off that symbol as well as
# the header would be two conditions that must agree, and the failure when
# they disagree is an undefined symbol at modpost rather than a build error
# anyone can read.
scradle-y += debugfs.o
# /proc/scradle-info. Same arrangement again: compiled unconditionally and
# empty without CONFIG_PROC_FS.
scradle-y += proc.o
# Fault injection, same arrangement and for the same reason: compiled
# unconditionally and empty without CONFIG_FAULT_INJECTION, so the header
# stubs and the object cannot disagree about which symbols exist.
scradle-y += fault.o
# Self-verification of the COW store. Unconditional and never behind a
# CONFIG_ guard: a build in which the corruption check is missing looks
# exactly like one in which it passed.
scradle-y += verify.o
# The COW store's compressor. Written into this module rather than linked
# against lib/lzo or crypto/, and unconditional like everything else here: a
# build in which the codec is absent would either fail to load with an
# unresolved symbol or silently store every chunk raw, and neither is
# distinguishable from the other end. See the header comment in complz.c.
scradle-y += complz.o
# The write-intent log. Also unconditional: a build without it and a build
# with it disabled at run time must be distinguishable from userspace, and
# a CONFIG_ guard makes them identical.
scradle-y += wil.o
# Files this driver keeps on the device it is protecting, read and written as
# raw bios at extents resolved once. One implementation on purpose: the
# validation that stands between this mode and writing over unrelated blocks
# of somebody's filesystem is not a thing to have three copies of.
scradle-y += rawfile.o
# Static tracepoints. Unconditional, like everything above it, and for a
# reason specific to this one: an event that exists in some builds and not
# others is worse than no event, because the `perf` or `bpftrace` command in
# a runbook then fails on the machine somebody is trying to debug and looks
# like the driver is not loaded. With CONFIG_TRACEPOINTS=n the whole file
# compiles to nothing at all -- see the header comment in scradle_trace.h.
#
# This object exists only to carry CREATE_TRACE_POINTS; the events
# themselves are declared in scradle_trace.h and fired from the paths they
# describe.
scradle-y += trace.o

# ------------------------------------------------------------------
# Rollback. THE ONLY OPTIONAL OBJECT IN THIS MODULE, and the only one that
# writes to the device this driver exists to protect.
#
# Every object above is unconditional, and each one says why. Read a few of
# them and the same argument keeps appearing: a feature that exists in some
# builds and not others is worse than the handful of kilobytes it costs,
# because the ioctl surface then differs between builds of the same version
# and an agent cannot tell which it is talking to. That argument is correct
# and it is why `verify`, `state`, `wil` and `overlay` are all compiled in and
# switched off at run time instead.
#
# It does not apply here, and the reason is not size. It is that the argument
# assumes the worst case of building a feature in is that it sits idle. For
# every object above, that is true: with the flag unset, overlay.o returns
# immediately, state.o does nothing, wil.o never opens a log. The worst case of
# building THIS one in is a module that contains a code path which issues
# REQ_OP_WRITE to a mounted production volume. Whether that path can be reached
# by mistake, by a malformed ioctl, or by a bug in the validation above it, is
# a question an operator running a fleet is entitled to answer with "it is not
# in the binary" rather than with an audit of this driver's argument handling.
#
# So the ambiguity that keeps everything else unconditional is accepted here,
# and paid for by making it legible instead of hiding it. The ioctl surface
# genuinely does differ between builds; SCRADLE_IOC_ROLLBACK returns
# -EOPNOTSUPP with SCRADLE_ROLLBACK_REFUSE_NOT_BUILT rather than -ENOTTY, so an
# agent can tell "this module has no rollback" from "this module is too old to
# have heard of the command", and three independent checks answer the question
# from userspace without issuing any ioctl at all:
#
#     cat /sys/module/scradle/parameters/rollback_supported    -> N or Y
#     modinfo -F parm scradle | grep rollback_supported
#     grep scradle_rollback_run /proc/kallsyms                 -> empty if off
#
# The third is the one that cannot be faked. The parameter is a value in
# params.o and would still read N if this Makefile and that default ever
# disagreed; the absent symbol is the absent object.
#
# Default OFF, and set on the command line or in dkms.conf:
#
#     make SCRADLE_ROLLBACK=y
#
# The -D and the object are driven by the same variable in the same place, so
# they cannot disagree. A build that sets one without the other would be a
# module whose gate in tracer.c is compiled out while rollback.o sets the flag
# it tests, which is a merge with no protection against a concurrent write.
# ------------------------------------------------------------------
SCRADLE_ROLLBACK ?= n
ifeq ($(SCRADLE_ROLLBACK),y)
scradle-y   += rollback.o
ccflags-y   += -DSCRADLE_HAVE_ROLLBACK=1
endif

# Where define_trace.h looks when it re-reads scradle_trace.h by name.
#
# A trace event header is included several times, and each pass is resolved
# by the preprocessor as "./scradle_trace.h" from inside the kernel's own
# include/trace/define_trace.h -- which is not this directory. Without this
# flag the build fails on a file it can see right next to it, and the error
# names the kernel's header rather than ours. $(src) is the module source
# directory on every kernel in the supported range, including 6.13 and later
# where it became an absolute path.
ccflags-y += -I$(src)

# Tell compat.h that probe results exist, without asking the compiler.
#
# compat.h used to find scradle_config.h with __has_include, which GCC only
# implements from 5.1. Under the gcc 4.8.5 an el7 tree requires, that test is
# false, the generated header is never included, and every probe result is
# silently discarded in favour of the LINUX_VERSION_CODE fallback that
# probe.sh exists to avoid. Nothing fails: the module builds against the
# wrong compat branch and the build log says "probed 36 kernel features".
#
# $(wildcard) answers the same question at the only moment it can be wrong,
# on every compiler. A build that bypasses this Makefile and does not probe
# gets no define and the __has_include path in compat.h, which is where it
# was already.
ccflags-y += $(if $(wildcard $(src)/scradle_config.h),-DSCRADLE_HAVE_CONFIG_H=1,)

KVER  ?= $(shell uname -r)
KDIR  ?= /lib/modules/$(KVER)/build
PWD   := $(shell pwd)

# Feature probing must run before the module is compiled: it generates
# scradle_config.h, which compat.h consumes. Version numbers alone are not
# trustworthy on enterprise kernels (see probe.sh).
CONFIG_H := $(PWD)/scradle_config.h

# Compiler override, for a build tree that pins a gcc which is not the one
# this host defaults to. Kernel build trees assign CC in the Makefile, so it
# has to be set on the command line to take effect; an exported CC is
# ignored. Empty means "whatever the tree says", which is the normal case.
SCRADLE_CC ?=
CCARGS := $(if $(SCRADLE_CC),CC=$(SCRADLE_CC) HOSTCC=$(SCRADLE_CC),)

all: $(CONFIG_H)
	$(MAKE) -C $(KDIR) M=$(PWD) $(CCARGS) modules

$(CONFIG_H): $(PWD)/probe.sh
	@SCRADLE_CC="$(SCRADLE_CC)" sh $(PWD)/probe.sh $(KDIR) $@

# Rebuild the probe results from scratch.
probe:
	@rm -f $(CONFIG_H)
	@$(MAKE) $(CONFIG_H)

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) $(CCARGS) clean
	rm -f $(CONFIG_H)

.PHONY: all clean probe
