# SPDX-License-Identifier: Apache-2.0 # Makefile for nginx-ipng-stats-plugin # # Targets: # build - build ngx_http_ipng_stats_module.so out-of-tree. # pkg-deb - build a .deb via dpkg-buildpackage for the current release. # robot-test - build .deb, then run Robot Framework end-to-end tests # in containerlab (requires docker + containerlab). # install-deps - install build and test dependencies via apt. # clean - remove build artifacts and the fetched nginx source tree. # help - print this help. # # Overridable variables: # NGINX_SRC - path to an unpacked nginx source tree. If unset, the # `build` target will apt-source one into ./build/nginx-src. MODULE_NAME := ngx_http_ipng_stats_module MODULE_DIR := $(CURDIR) BUILD_DIR := $(CURDIR)/build # Single source of truth for the module version. When cutting a release, # bump this AND add a matching top entry to debian/changelog — dpkg reads # the package version from there directly. The C code picks up VERSION # via the generated src/version.h (written by the version-header target # below and depended on by the module build). VERSION := 0.3.0 NGINX_SRC ?= .PHONY: help build build-asan pkg-deb robot-test install-deps clean fetch-nginx-src version-header TEST ?= tests/ help: @echo "nginx-ipng-stats-plugin — make targets" @echo "" @echo " make build Build $(MODULE_NAME).so out-of-tree." @echo " make build-asan Build a full nginx + module with AddressSanitizer" @echo " into build/nginx-asan/ for local crash-hunting." @echo " make pkg-deb Build a Debian package via dpkg-buildpackage." @echo " make robot-test Run Robot Framework e2e tests (all suites)." @echo " make install-deps Install build and test dependencies (apt)." @echo " make clean Remove build artifacts." @echo "" @echo "Overridable:" @echo " NGINX_SRC= Use an existing nginx source tree." @echo " TEST= Run a specific .robot file (default: tests/)." # ---------------------------------------------------------------------- # build: out-of-tree dynamic module build # ---------------------------------------------------------------------- build: $(BUILD_DIR)/$(MODULE_NAME).so @echo "" @echo "Built: $(BUILD_DIR)/$(MODULE_NAME).so" @echo "" @echo "To try it locally without installing a .deb:" @echo " sudo install -m 0644 $(BUILD_DIR)/$(MODULE_NAME).so /usr/lib/nginx/modules/" @echo " echo 'load_module modules/$(MODULE_NAME).so;' | sudo tee /etc/nginx/modules-enabled/50-mod-http-ipng-stats.conf" @echo " sudo nginx -t && sudo nginx -s reload" # version-header: write src/version.h iff its contents would change. The # target is .PHONY so it's re-evaluated every build, but the file itself # is only touched on VERSION bumps — keeps the .so from rebuilding when # nothing has actually changed. version-header: @NEW='#define NGX_HTTP_IPNG_STATS_VERSION "$(VERSION)"'; \ if [ ! -f $(MODULE_DIR)/src/version.h ] \ || [ "$$NEW" != "$$(cat $(MODULE_DIR)/src/version.h)" ]; then \ echo "Generating src/version.h (VERSION=$(VERSION))"; \ echo "$$NEW" > $(MODULE_DIR)/src/version.h; \ fi $(BUILD_DIR)/$(MODULE_NAME).so: version-header fetch-nginx-src @set -e; \ if [ -z "$(NGINX_SRC)" ]; then \ NGX_SRC="$(BUILD_DIR)/nginx-src"; \ else \ NGX_SRC="$(NGINX_SRC)"; \ fi; \ echo "Configuring nginx in $$NGX_SRC against module at $(MODULE_DIR)"; \ cd "$$NGX_SRC" && ./configure --with-compat --add-dynamic-module=$(MODULE_DIR); \ echo "Building module"; \ $(MAKE) -C "$$NGX_SRC" -f objs/Makefile modules; \ mkdir -p $(BUILD_DIR); \ cp "$$NGX_SRC/objs/$(MODULE_NAME).so" $(BUILD_DIR)/$(MODULE_NAME).so fetch-nginx-src: @set -e; \ if [ -n "$(NGINX_SRC)" ]; then \ echo "Using NGINX_SRC=$(NGINX_SRC)"; \ exit 0; \ fi; \ if [ -d "$(BUILD_DIR)/nginx-src" ] && [ -f "$(BUILD_DIR)/nginx-src/configure" ]; then \ echo "Reusing $(BUILD_DIR)/nginx-src"; \ exit 0; \ fi; \ mkdir -p $(BUILD_DIR); \ if [ -d /usr/share/nginx/src ] && [ -f /usr/share/nginx/src/configure ]; then \ echo "Copying /usr/share/nginx/src (from nginx-dev) to $(BUILD_DIR)/nginx-src"; \ rm -rf $(BUILD_DIR)/nginx-src; \ cp -a /usr/share/nginx/src $(BUILD_DIR)/nginx-src; \ chmod -R u+w $(BUILD_DIR)/nginx-src; \ exit 0; \ fi; \ rm -rf $(BUILD_DIR)/apt-src; \ mkdir -p $(BUILD_DIR)/apt-src; \ echo "Fetching nginx source via \`apt source nginx\` in $(BUILD_DIR)/apt-src"; \ cd $(BUILD_DIR)/apt-src && apt source nginx; \ NGX_DIR=$$(find $(BUILD_DIR)/apt-src -maxdepth 1 -type d -name 'nginx-*' | head -n1); \ if [ -z "$$NGX_DIR" ]; then \ echo "error: could not find unpacked nginx source tree under $(BUILD_DIR)/apt-src" >&2; \ exit 1; \ fi; \ rm -rf $(BUILD_DIR)/nginx-src; \ mv "$$NGX_DIR" $(BUILD_DIR)/nginx-src; \ rm -rf $(BUILD_DIR)/apt-src # ---------------------------------------------------------------------- # build-asan: full nginx + module built with AddressSanitizer. ASan # needs to be present in the main binary to instrument dynamic modules; # running a stock nginx with LD_PRELOAD=libasan.so does NOT work with # a statically-linked module's allocations. So we build nginx itself # here, into an isolated prefix under build/nginx-asan/, which you can # run by hand against any config. Normal `make build` is unaffected. # # Typical workflow for the reload-crash hunt: # make build-asan # cp your-test.conf build/nginx-asan/conf/nginx.conf # ASAN_OPTIONS=detect_odr_violation=0:abort_on_error=1:halt_on_error=1:detect_leaks=0 \ # build/nginx-asan/sbin/nginx -p build/nginx-asan # # in another shell: # build/nginx-asan/sbin/nginx -p build/nginx-asan -s reload # ASan writes any findings to stderr of the master before aborting. # # detect_odr_violation=0 suppresses false positives for symbols nginx # intentionally duplicates between its main binary and dynamic modules # (e.g. ngx_module_names in objs/ngx_http_ipng_stats_module_modules.c). # ---------------------------------------------------------------------- # -fno-sanitize=nonnull-attribute suppresses a UBSan finding in nginx's # ngx_cpymem (src/core/ngx_string.c:84): the macro expands to memcpy(...) # which glibc declares _Nonnull, but nginx legitimately calls it with # (dst, NULL, 0) in many places. Filtering just that check keeps real # undefined behaviour visible. ASAN_CFLAGS := -fsanitize=address -fsanitize=undefined -fno-sanitize=nonnull-attribute -fno-omit-frame-pointer -fno-common -g -O1 ASAN_LDFLAGS := -fsanitize=address -fsanitize=undefined # The ASan build rebuilds nginx from source rather than only the module, # because ASan must instrument the main binary to catch heap issues in # modules it loads. The `nginx-dev` shim source tree under /usr/share # only carries headers, so we always fall through to `apt source nginx` # here and keep the full source tree isolated in build/nginx-asan-src/. build-asan: version-header @set -e; \ NGX_SRC="$(BUILD_DIR)/nginx-asan-src"; \ if [ ! -d "$$NGX_SRC" ] || [ ! -f "$$NGX_SRC/src/core/nginx.c" ]; then \ echo "Fetching full nginx source via apt-source for ASan build"; \ mkdir -p $(BUILD_DIR)/apt-src-asan; \ rm -rf $$NGX_SRC; \ cd $(BUILD_DIR)/apt-src-asan && apt source nginx; \ NGX_DIR=$$(find $(BUILD_DIR)/apt-src-asan -maxdepth 1 -type d -name 'nginx-*' | head -n1); \ if [ -z "$$NGX_DIR" ]; then \ echo "error: could not find unpacked nginx source tree" >&2; \ exit 1; \ fi; \ mv "$$NGX_DIR" $$NGX_SRC; \ rm -rf $(BUILD_DIR)/apt-src-asan; \ fi; \ PREFIX="$(BUILD_DIR)/nginx-asan"; \ echo "Configuring ASan nginx in $$NGX_SRC (prefix=$$PREFIX)"; \ cd "$$NGX_SRC" && ./configure \ --prefix="$$PREFIX" \ --with-cc-opt="$(ASAN_CFLAGS)" \ --with-ld-opt="$(ASAN_LDFLAGS)" \ --with-compat \ --with-debug \ --add-dynamic-module=$(MODULE_DIR); \ echo "Building ASan nginx + module (this takes a minute)"; \ $(MAKE) -C "$$NGX_SRC" -f objs/Makefile build; \ $(MAKE) -C "$$NGX_SRC" -f objs/Makefile install; \ install -d $$PREFIX/modules; \ install -m 0644 "$$NGX_SRC/objs/$(MODULE_NAME).so" $$PREFIX/modules/; \ echo ""; \ echo "ASan build ready:"; \ echo " nginx: $$PREFIX/sbin/nginx"; \ echo " module: $$PREFIX/modules/$(MODULE_NAME).so"; \ echo ""; \ echo "Run with:"; \ echo " ASAN_OPTIONS=detect_odr_violation=0:abort_on_error=1:halt_on_error=1:detect_leaks=0 \\"; \ echo " $$PREFIX/sbin/nginx -p $$PREFIX" # ---------------------------------------------------------------------- # pkg-deb: build a .deb # ---------------------------------------------------------------------- pkg-deb: dpkg-buildpackage -us -uc -b @mkdir -p $(BUILD_DIR) @# dpkg-buildpackage writes artifacts to ../ — relocate them into @# $(BUILD_DIR) so everything ephemeral lives under build/. @for f in ../libnginx-mod-http-ipng-stats*.deb \ ../libnginx-mod-http-ipng-stats*.ddeb \ ../nginx-ipng-stats-plugin_*.buildinfo \ ../nginx-ipng-stats-plugin_*.changes; do \ if [ -f "$$f" ]; then mv -f "$$f" $(BUILD_DIR)/; fi; \ done @echo "" @echo "Resulting .deb(s):" @ls -1 $(BUILD_DIR)/*.deb 2>/dev/null || true # ---------------------------------------------------------------------- # clean # ---------------------------------------------------------------------- # ---------------------------------------------------------------------- # robot-test: containerlab + Robot Framework end-to-end tests # ---------------------------------------------------------------------- tests/.venv: tests/requirements.txt python3 -m venv tests/.venv tests/.venv/bin/pip install -q -r tests/requirements.txt robot-test: tests/.venv tests/rf-run.sh docker $(TEST) # ---------------------------------------------------------------------- # install-deps: install build and test dependencies # ---------------------------------------------------------------------- install-deps: sudo apt-get update -qq sudo apt-get install -y \ nginx-dev dpkg-dev debhelper \ python3 python3-venv \ curl @echo "" @echo "Build dependencies installed. For 'make robot-test' you also need:" @echo " - docker: https://docs.docker.com/engine/install/debian/" @echo " - containerlab: https://containerlab.dev/install/" # ---------------------------------------------------------------------- # clean # ---------------------------------------------------------------------- clean: rm -rf $(BUILD_DIR) tests/.venv tests/out rm -f $(MODULE_DIR)/src/version.h -dh_clean 2>/dev/null || true