zupt/Makefile
2026-08-31 14:14:36 -03:00

795 lines
37 KiB
Makefile

# SPDX-License-Identifier: AGPL-3.0-or-later
# ZUPT — backup compression with hybrid post-quantum encryption
# Build system. Pure GNU make, no autotools, no cmake required.
#
# Targets:
# make Build the zupt binary
# make V=1 Verbose: show every command line
# make install Install to /usr/local (override with PREFIX=/usr)
# make check Run the source-only distribution test suite
# make test-all Run the extended upstream test suite
# make test-asan Build with AddressSanitizer + UBSan
# make test-asan-run Execute the sanitizer smoke test
# make clean Remove build artifacts
#
# Build profiles (all controllable via standard env vars):
# CC=clang make Use Clang instead of GCC
# CFLAGS="-O3 -g" make Override the default optimization
# make PREFIX=/usr DESTDIR=/tmp/stage Staged install for packagers
#
# The compiler target, rather than the build host, controls architecture
# selection. This keeps cross builds from accidentally enabling host assembly.
CC ?= cc
CPPFLAGS ?=
CFLAGS ?= -O2 -g
LDFLAGS ?=
LDLIBS ?=
AR ?= ar
ARFLAGS ?= rcs
RANLIB ?= ranlib
STRIP ?= strip
PKG_CONFIG ?= pkg-config
ASFLAGS ?=
# GNU make has a built-in ARFLAGS=rv. Use archive creation flags by default,
# while preserving values supplied through the environment or command line.
ifeq ($(origin ARFLAGS),default)
ARFLAGS := rcs
endif
DESTDIR ?=
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin
LIBDIR ?= $(PREFIX)/lib
INCLUDEDIR ?= $(PREFIX)/include
DATADIR ?= $(PREFIX)/share
MANDIR ?= $(DATADIR)/man
MAN1DIR ?= $(MANDIR)/man1
BASHCOMPDIR ?= $(DATADIR)/bash-completion/completions
ZSHCOMPDIR ?= $(DATADIR)/zsh/site-functions
FISHCOMPDIR ?= $(DATADIR)/fish/vendor_completions.d
LICENSEDIR ?= $(DATADIR)/licenses/zupt
GZIP ?= gzip
GZIPFLAGS ?= -9 -n
INSTALL_LEGACY_ALIAS ?= 0
INSTALL_LICENSES ?= 1
LICENSE_FILES = LICENSE LICENSE-AGPL-3.0 LICENSE-GPL-3.0 \
LICENSE-BSD-2-Clause LICENSE-BSD-3-Clause LICENSE-CC0-1.0 \
NOTICE THIRD-PARTY-NOTICES.md
# Standard packager variables above are never rewritten. Project-owned flags
# are passed alongside them on every command line.
PROJECT_CPPFLAGS := -D_DEFAULT_SOURCE -Iinclude -Isrc
PROJECT_CFLAGS := -Wall -Wextra -Woverlength-strings -std=c11
PROJECT_LDFLAGS :=
PROJECT_CLI_LDFLAGS :=
PROJECT_LDLIBS := -lm
EXEEXT :=
CREATE_TEST_ALIAS := 0
FEATURE_CPPFLAGS :=
FEATURE_LDLIBS :=
# Clang's -Wcast-align diagnoses the pointer casts required by the explicitly
# unaligned x86 load/store intrinsics, and cannot infer alignment through the
# byte-backed VaptVupt arenas. Those arenas start at malloc alignment and all
# typed offsets are rounded to at least 8 bytes. Keep the compatibility
# suppression local to the three audited translation units; every other file
# retains a caller-supplied -Wcast-align/-Werror policy.
CLANG_CAST_ALIGN_FLAGS :=
ifneq ($(findstring clang,$(shell $(CC) --version 2>/dev/null | head -n 1)),)
CLANG_CAST_ALIGN_FLAGS := -Wno-cast-align
endif
CLANG_CAST_ALIGN_OBJS := src/vv_ans.o src/vv_simd.o src/zupt_sha256_shani.o
TARGET_MACHINE ?= $(shell $(CC) -dumpmachine 2>/dev/null)
TARGET_CPU := $(firstword $(subst -, ,$(TARGET_MACHINE)))
ifeq ($(strip $(TARGET_CPU)),)
TARGET_CPU := unknown
endif
# The Windows extraction path uses the documented NtCreateFile RootDirectory
# facility so directory components are resolved relative to pinned handles.
# A self-contained PE is required because POSIX-thread MinGW toolchains may
# otherwise add an undeclared libwinpthread-1.dll runtime dependency.
ifneq ($(strip $(findstring mingw,$(TARGET_MACHINE))$(findstring windows,$(TARGET_MACHINE))),)
EXEEXT := .exe
CREATE_TEST_ALIAS := 0
PROJECT_LDFLAGS += -static
PROJECT_CLI_LDFLAGS += -municode
PROJECT_LDLIBS += -lntdll
endif
# pthread is part of bionic and the Windows implementation uses native APIs.
ifeq ($(findstring android,$(TARGET_MACHINE)),)
ifeq ($(findstring mingw,$(TARGET_MACHINE)),)
ifeq ($(findstring windows,$(TARGET_MACHINE)),)
PROJECT_CFLAGS += -pthread
PROJECT_LDLIBS += -pthread
endif
endif
endif
ifneq ($(filter $(INSTALL_LEGACY_ALIAS),0 1),$(INSTALL_LEGACY_ALIAS))
$(error INSTALL_LEGACY_ALIAS must be 0 or 1)
endif
ifneq ($(filter $(INSTALL_LICENSES),0 1),$(INSTALL_LICENSES))
$(error INSTALL_LICENSES must be 0 or 1)
endif
# --- Verbose build ---
V ?= 0
ifeq ($(V),1)
Q =
else
Q = @
endif
# --- ZUPT core sources ---
ZUPT_SOURCES = src/zupt_main.c src/zupt_format.c src/zupt_lz.c src/zupt_lzh.c \
src/zupt_xxh.c src/zupt_sha256.c src/zupt_sha256_shani.c src/zupt_aes256.c src/zupt_crypto.c \
src/zupt_crypto_sdk.c src/zupt_crypto_pqbox.c \
src/zupt_predict.c src/zupt_parallel.c src/zupt_keccak.c \
src/zupt_x25519.c src/zupt_mlkem.c src/zupt_cpuid.c src/zupt_mlock.c \
src/zupt_filetype.c src/zupt_disk.c src/zupt_dedup.c
# --- Optional system libraries (never vendored, never downloaded) ---
WITH_SDK ?= 0
WITH_PQBOX ?= 0
ifneq ($(filter $(WITH_SDK),0 1),$(WITH_SDK))
$(error WITH_SDK must be 0 or 1)
endif
ifneq ($(filter $(WITH_PQBOX),0 1),$(WITH_PQBOX))
$(error WITH_PQBOX must be 0 or 1)
endif
ifeq ($(WITH_SDK),1)
SDK_PKG_CONFIG ?= libvuptsdk
SDK_CPPFLAGS ?= $(shell $(PKG_CONFIG) --cflags $(SDK_PKG_CONFIG) 2>/dev/null)
SDK_LDLIBS ?= $(shell $(PKG_CONFIG) --libs $(SDK_PKG_CONFIG) 2>/dev/null)
ifeq ($(strip $(SDK_LDLIBS)),)
$(error WITH_SDK=1 requires the system libvuptsdk development package; set SDK_CPPFLAGS and SDK_LDLIBS to explicit system paths if no pkg-config file is provided)
endif
FEATURE_CPPFLAGS += -DZUPT_WITH_SDK $(SDK_CPPFLAGS)
FEATURE_LDLIBS += $(SDK_LDLIBS)
endif
ifeq ($(WITH_PQBOX),1)
PQBOX_PKG_CONFIG ?= libpqvaptvupt
PQBOX_CPPFLAGS ?= $(shell $(PKG_CONFIG) --cflags $(PQBOX_PKG_CONFIG) 2>/dev/null)
PQBOX_LDLIBS ?= $(shell $(PKG_CONFIG) --libs $(PQBOX_PKG_CONFIG) 2>/dev/null)
ifeq ($(strip $(PQBOX_LDLIBS)),)
$(error WITH_PQBOX=1 requires the system libpqvaptvupt development package; set PQBOX_CPPFLAGS and PQBOX_LDLIBS to explicit system paths if no pkg-config file is provided)
endif
FEATURE_CPPFLAGS += -DZUPT_WITH_PQBOX $(PQBOX_CPPFLAGS)
FEATURE_LDLIBS += $(PQBOX_LDLIBS)
endif
# --- VAPTVUPT: VaptVupt codec sources (GPL-3.0-or-later; tool is AGPL-3.0-or-later) ---
VV_SOURCES = src/vv_encoder.c src/vv_decoder.c src/vv_ans.c src/vv_bcj.c \
src/vv_huffman.c src/vv_simd.c src/vv_xxh64.c src/vaptvupt_api.c
SOURCES = $(ZUPT_SOURCES) $(VV_SOURCES)
HEADERS = include/zupt.h include/zupt_keccak.h include/zupt_mlkem.h \
include/zupt_x25519.h include/zupt_cpuid.h include/zupt_jasmin.h \
include/zupt_acsl.h \
include/vaptvupt.h include/vaptvupt_api.h include/vv_huffman.h include/vv_ans.h \
include/vv_platform.h \
src/zupt_thread.h src/zupt_parallel.h src/zupt_internal.h
PROGRAM = zupt
TARGET = $(PROGRAM)$(EXEEXT)
LEGACY_PROGRAM = vaptvupt
LEGACY_LINK = $(LEGACY_PROGRAM)$(EXEEXT)
MANPAGE = doc/zupt.1
MANPAGE_GZ = $(PROGRAM).1.gz
# Architecture-specific code is opt-in and isolated. The normal x86_64 build
# stays at the ABI baseline; in particular, no complete codec TU gets -mavx2.
SHANI_FLAGS :=
ifneq ($(filter x86_64 amd64 i386 i486 i586 i686,$(TARGET_CPU)),)
SHANI_FLAGS := -msha -mssse3 -msse4.1
endif
# Optional textual assembly is disabled by default so every
# compiler/architecture has the audited C fallback. Four files are jasminc
# outputs and zupt_aes_ctr4.s is separately identified as hand-written. When
# requested, the compiler driver assembles them while preserving cross-target
# and sysroot settings.
WITH_JASMIN ?= 0
ifneq ($(filter $(WITH_JASMIN),0 1),$(WITH_JASMIN))
$(error WITH_JASMIN must be 0 or 1)
endif
JAZZ_S = jasmin/zupt_mac_verify.s jasmin/zupt_mlkem_select.s \
jasmin/zupt_aes_ctr.s jasmin/zupt_x25519_fe.s jasmin/zupt_aes_ctr4.s
JAZZ_O :=
ifeq ($(WITH_JASMIN),1)
ifeq ($(filter x86_64 amd64,$(TARGET_CPU)),)
$(error WITH_JASMIN=1 is supported only for an x86_64 compiler target; detected $(TARGET_MACHINE))
endif
ifneq ($(words $(wildcard $(JAZZ_S))),$(words $(JAZZ_S)))
$(error WITH_JASMIN=1 requested, but one or more optional .s sources are missing)
endif
FEATURE_CPPFLAGS += -DZUPT_USE_JASMIN
JAZZ_O := $(JAZZ_S:.s=.o)
endif
# --- Object files ---
# These objects contain the baseline codec implementation. Optimized SHA-NI
# remains in its own translation unit below and is guarded at runtime.
VV_SIMD_OBJS = src/vv_encoder.o src/vv_decoder.o src/vv_simd.o
# The bundled codec uses the same warning policy as the application. Do not
# add broad -Wno-* flags here: localized upstream changes are recorded in
# THIRD-PARTY-NOTICES.md and must compile without masked diagnostics.
VV_PLAIN_OBJS = src/vv_ans.o src/vv_huffman.o src/vv_xxh64.o src/vv_bcj.o src/vaptvupt_api.o
ZUPT_OBJS = $(patsubst %.c,%.o,$(ZUPT_SOURCES))
ALL_OBJS = $(ZUPT_OBJS) $(VV_SIMD_OBJS) $(VV_PLAIN_OBJS)
# ═══════════════════════════════════════════════════════════════════
# BUILD RULES
# ═══════════════════════════════════════════════════════════════════
.DELETE_ON_ERROR:
.PHONY: all clean install uninstall test test-all release-check test-asan test-asan-run \
test-vectors test-f06 test-vv fuzz-build fuzz-format fuzz-format-run \
help audit-licenses source-audit dist check
all: $(TARGET)
# ═══════════════════════════════════════════════════════════════════
# audit-licenses — verify covered code, build, CI, and packaging files carry
# the correct SPDX marker. Legal-document completeness is audited separately
# through LICENSE*, NOTICE, and THIRD-PARTY-NOTICES.md; this target is not a
# claim of full REUSE conformance.
# AGPL-3.0-or-later for the application/core, GPL-3.0-or-later for the
# bundled codec files, BSD-2-Clause for the two xxHash-derived units, and
# CC0-1.0 for the pq-crystals/kyber-derived portions of native ML-KEM, and
# BSD-3-Clause for curve25519-donna-derived X25519 portions.
# See THIRD-PARTY-NOTICES.md.
# ═══════════════════════════════════════════════════════════════════
audit-licenses:
@MISSING=0; WRONG=0; \
for f in $$(find . -type f \( -name '*.c' -o -name '*.h' -o -name '*.hpp' \
-o -name '*.py' -o -name '*.sh' -o -name '*.yml' -o -name '*.yaml' \
-o -name '*.jazz' -o -name '*.s' -o -name '*.S' -o -name 'Makefile' \
-o -name '*.map' -o -name '*.bat' -o -name '*.command' \
-o -name '*.desktop' -o -name '*.nemo_action' -o -name '*.spec' \
-o -name '*.rb' -o -name '*.scm' -o -name '*.nix' \
-o -name '*.iss' -o -name '*.nsi' -o -name '*.fish' \
-o -name 'PKGBUILD' -o -name '_service' -o -name 'rules' \) \
-not -path './build/*' \
-not -path './build_obj/*' \
-not -path './sdk/build/*'); do \
case "$$f" in \
./src/zupt_mlkem.c) \
EXPECTED_ID="AGPL-3.0-or-later AND CC0-1.0" ;; \
./src/zupt_x25519.c) \
EXPECTED_ID="AGPL-3.0-or-later AND BSD-3-Clause" ;; \
./src/zupt_xxh.c) \
EXPECTED_ID="AGPL-3.0-or-later AND BSD-2-Clause" ;; \
./src/vv_xxh64.c) \
EXPECTED_ID="GPL-3.0-or-later AND BSD-2-Clause" ;; \
./src/vv_*.c|./src/vaptvupt_api.c|./include/vv_*.h|./include/vaptvupt*.h) \
EXPECTED_ID="GPL-3.0-or-later" ;; \
*) \
EXPECTED_ID="AGPL-3.0-or-later" ;; \
esac; \
HEADER=$$(sed -n '1,12p' "$$f"); \
HEADER_COUNT=$$(printf '%s\n' "$$HEADER" | \
grep -c 'SPDX-License-Identifier:' || true); \
ACTUAL_ID=$$(printf '%s\n' "$$HEADER" | \
sed -n 's/^.*SPDX-License-Identifier:[[:space:]]*//p' | \
sed 's/[[:space:]]*\*\/[[:space:]]*$$//; s/[[:space:]]*-->[[:space:]]*$$//; s/[[:space:]]*$$//' | \
head -n 1); \
if [ "$$HEADER_COUNT" -eq 0 ]; then \
echo "$$f (missing SPDX)"; \
MISSING=$$((MISSING+1)); \
elif [ "$$HEADER_COUNT" -ne 1 ] || [ "$$ACTUAL_ID" != "$$EXPECTED_ID" ]; then \
echo "$$f (wrong SPDX header, expected exactly once: $$EXPECTED_ID)"; \
WRONG=$$((WRONG+1)); \
fi; \
done; \
if [ $$MISSING -eq 0 ] && [ $$WRONG -eq 0 ]; then \
echo " ✓ Covered code/build/CI/packaging files carry correct SPDX markers"; \
echo " (AGPL core; GPL codec; BSD-2 XXH64; BSD-3 X25519; CC0 ML-KEM)"; \
else \
echo ""; \
echo " $$MISSING missing, $$WRONG with wrong SPDX. Aborting."; \
exit 1; \
fi
# Optional Jasmin textual assembly (x86_64 only). Use the compiler driver so a
# cross compiler's target, sysroot, assembler and reproducibility flags apply.
jasmin/%.o: jasmin/%.s
$(Q)$(CC) $(CPPFLAGS) $(ASFLAGS) -c -o $@ $<
# VaptVupt codec files are compiled for the target ABI baseline.
$(VV_SIMD_OBJS): src/%.o: src/%.c $(HEADERS)
$(Q)$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) \
$(if $(filter $@,$(CLANG_CAST_ALIGN_OBJS)),$(CLANG_CAST_ALIGN_FLAGS)) \
-c -o $@ $<
# VaptVupt non-SIMD files use the same warning policy.
$(VV_PLAIN_OBJS): src/%.o: src/%.c $(HEADERS)
$(Q)$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) \
$(if $(filter $@,$(CLANG_CAST_ALIGN_OBJS)),$(CLANG_CAST_ALIGN_FLAGS)) \
-c -o $@ $<
# ZUPT core files (the SHA-NI object has its own rule below with -msha)
ZUPT_OBJS_GENERIC = $(filter-out src/zupt_sha256_shani.o,$(ZUPT_OBJS))
$(ZUPT_OBJS_GENERIC): src/%.o: src/%.c $(HEADERS)
$(Q)$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) -c -o $@ $<
# SHA-NI path needs -msha -mssse3 -msse4.1 on x86_64.
# On non-x86_64, SHANI_FLAGS is empty and the file is a no-op TU.
src/zupt_sha256_shani.o: src/zupt_sha256_shani.c $(HEADERS)
$(Q)$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(SHANI_FLAGS) \
$(if $(filter $@,$(CLANG_CAST_ALIGN_OBJS)),$(CLANG_CAST_ALIGN_FLAGS)) \
-c -o $@ $<
# Final link step. Order matters: CFLAGS before LDFLAGS, then objects,
# then LDLIBS — keeps GCC/Clang happy when LDFLAGS contains -pie or
# similar position-sensitive flags.
$(TARGET): $(ALL_OBJS) $(JAZZ_O)
$(Q)$(CC) $(CFLAGS) $(PROJECT_CFLAGS) $(LDFLAGS) $(PROJECT_LDFLAGS) $(PROJECT_CLI_LDFLAGS) \
$(ALL_OBJS) $(JAZZ_O) -o $(TARGET) \
$(FEATURE_LDLIBS) $(PROJECT_LDLIBS) $(LDLIBS)
@# In-tree test compatibility only. Installation emits the legacy
@# alias solely when INSTALL_LEGACY_ALIAS=1 is requested explicitly.
$(Q)if [ "$(CREATE_TEST_ALIAS)" = 0 ]; then \
:; \
elif [ -L "$(LEGACY_LINK)" ]; then \
test "$$(readlink "$(LEGACY_LINK)")" = "$(TARGET)" || { \
echo "ERROR: refusing to replace non-ZUPT symlink: $(LEGACY_LINK)" >&2; exit 1; \
}; \
elif [ -e "$(LEGACY_LINK)" ]; then \
echo "ERROR: refusing to replace existing path: $(LEGACY_LINK)" >&2; exit 1; \
fi; \
if [ "$(CREATE_TEST_ALIAS)" = 1 ]; then ln -sf "$(TARGET)" "$(LEGACY_LINK)"; fi
@if [ "$(CREATE_TEST_ALIAS)" = 1 ]; then \
echo "Build complete: ./$(TARGET) [$(TARGET_MACHINE)] (test alias: ./$(LEGACY_LINK) -> $(TARGET))"; \
else \
echo "Build complete: ./$(TARGET) [$(TARGET_MACHINE)] (no in-tree compatibility alias)"; \
fi
# ═══════════════════════════════════════════════════════════════════
# INSTALL / UNINSTALL
# ═══════════════════════════════════════════════════════════════════
install: $(TARGET)
$(Q)mkdir -p "$(DESTDIR)$(BINDIR)"
$(Q)install -m 0755 "$(TARGET)" "$(DESTDIR)$(BINDIR)/$(TARGET)"
$(Q)if [ "$(INSTALL_LICENSES)" = 1 ]; then \
mkdir -p "$(DESTDIR)$(LICENSEDIR)"; \
install -m 0644 $(LICENSE_FILES) "$(DESTDIR)$(LICENSEDIR)/"; \
fi
$(Q)if [ "$(INSTALL_LEGACY_ALIAS)" = 1 ]; then \
ln -sf "$(TARGET)" "$(DESTDIR)$(BINDIR)/$(LEGACY_LINK)"; \
fi
$(Q)if [ -f "$(MANPAGE)" ]; then \
mkdir -p "$(DESTDIR)$(MAN1DIR)"; \
$(GZIP) $(GZIPFLAGS) -c "$(MANPAGE)" > "$(DESTDIR)$(MAN1DIR)/$(MANPAGE_GZ)"; \
chmod 0644 "$(DESTDIR)$(MAN1DIR)/$(MANPAGE_GZ)"; \
if [ "$(INSTALL_LEGACY_ALIAS)" = 1 ]; then \
ln -sf "$(MANPAGE_GZ)" "$(DESTDIR)$(MAN1DIR)/$(LEGACY_PROGRAM).1.gz"; \
fi; \
echo "Installed: $(DESTDIR)$(MAN1DIR)/$(MANPAGE_GZ)"; \
else \
echo "Warning: man page not found: $(MANPAGE)"; \
fi
# Shell completions (v2.4.7+). Honour distro path conventions where
# possible; downstream packagers can override DESTDIR + the specific
# dirs as needed.
$(Q)if [ -f completions/zupt.bash ]; then \
mkdir -p "$(DESTDIR)$(BASHCOMPDIR)"; \
install -m 0644 completions/zupt.bash \
"$(DESTDIR)$(BASHCOMPDIR)/$(PROGRAM)"; \
if [ "$(INSTALL_LEGACY_ALIAS)" = 1 ]; then \
ln -sf "$(PROGRAM)" "$(DESTDIR)$(BASHCOMPDIR)/$(LEGACY_PROGRAM)"; \
fi; \
echo "Installed: $(DESTDIR)$(BASHCOMPDIR)/$(PROGRAM)"; \
fi
$(Q)if [ -f completions/_zupt ]; then \
mkdir -p "$(DESTDIR)$(ZSHCOMPDIR)"; \
install -m 0644 completions/_zupt \
"$(DESTDIR)$(ZSHCOMPDIR)/_$(PROGRAM)"; \
if [ "$(INSTALL_LEGACY_ALIAS)" = 1 ]; then \
ln -sf "_$(PROGRAM)" "$(DESTDIR)$(ZSHCOMPDIR)/_$(LEGACY_PROGRAM)"; \
fi; \
echo "Installed: $(DESTDIR)$(ZSHCOMPDIR)/_$(PROGRAM)"; \
fi
$(Q)if [ -f completions/zupt.fish ]; then \
mkdir -p "$(DESTDIR)$(FISHCOMPDIR)"; \
install -m 0644 completions/zupt.fish \
"$(DESTDIR)$(FISHCOMPDIR)/$(PROGRAM).fish"; \
if [ "$(INSTALL_LEGACY_ALIAS)" = 1 ]; then \
ln -sf "$(PROGRAM).fish" "$(DESTDIR)$(FISHCOMPDIR)/$(LEGACY_PROGRAM).fish"; \
fi; \
echo "Installed: $(DESTDIR)$(FISHCOMPDIR)/$(PROGRAM).fish"; \
fi
@echo "Installed: $(DESTDIR)$(BINDIR)/$(TARGET)"
uninstall:
$(Q)rm -f "$(DESTDIR)$(BINDIR)/$(TARGET)"
$(Q)rm -f "$(DESTDIR)$(MAN1DIR)/$(MANPAGE_GZ)"
$(Q)rm -f "$(DESTDIR)$(BASHCOMPDIR)/$(PROGRAM)"
$(Q)rm -f "$(DESTDIR)$(ZSHCOMPDIR)/_$(PROGRAM)"
$(Q)rm -f "$(DESTDIR)$(FISHCOMPDIR)/$(PROGRAM).fish"
$(Q)set -eu; for license_file in $(LICENSE_FILES); do \
rm -f "$(DESTDIR)$(LICENSEDIR)/$${license_file##*/}"; \
done
$(Q)for item in \
"$(DESTDIR)$(BINDIR)/$(LEGACY_LINK):$(TARGET)" \
"$(DESTDIR)$(MAN1DIR)/$(LEGACY_PROGRAM).1.gz:$(MANPAGE_GZ)" \
"$(DESTDIR)$(BASHCOMPDIR)/$(LEGACY_PROGRAM):$(PROGRAM)" \
"$(DESTDIR)$(ZSHCOMPDIR)/_$(LEGACY_PROGRAM):_$(PROGRAM)" \
"$(DESTDIR)$(FISHCOMPDIR)/$(LEGACY_PROGRAM).fish:$(PROGRAM).fish"; do \
path=$${item%:*}; expected=$${item##*:}; \
if [ -L "$$path" ] && [ "$$(readlink "$$path")" = "$$expected" ]; then rm -f "$$path"; fi; \
done
# ═══════════════════════════════════════════════════════════════════
# DIST — reproducible source tarball for distro packaging
# ═══════════════════════════════════════════════════════════════════
#
# `make dist` produces zupt-VERSION.tar.gz that is BYTE-IDENTICAL given
# the same input source tree. Properties:
#
# - Files sorted by name (stable order regardless of filesystem layout)
# - mtime fixed to SOURCE_DATE_EPOCH (`.source-date-epoch`, then HEAD fallback)
# - uid/gid fixed to root (0/0) via --owner / --group
# - gzip wrapped with --no-name (no embedded timestamp/filename)
# - No binaries, no .o, no .so. Source only.
#
# The archive always represents the tree of committed HEAD, never ignored build
# output or uncommitted files. Archiving the tree object also avoids Git's
# commit-ID PAX header, so an export-ignored-only commit cannot change its bytes.
# Override DIST_TARBALL for a packaging work directory.
DIST_VERSION = $(shell sed -n 's/^\#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
DIST_NAME = $(PROGRAM)-$(DIST_VERSION)
DIST_TARBALL ?= /tmp/$(DIST_NAME).tar.gz
SOURCE_DATE_EPOCH ?= $(shell epoch=$$(sed -n 's/^[[:space:]]*\([0-9][0-9]*\)[[:space:]]*$$/\1/p' .source-date-epoch 2>/dev/null | head -n 1); if test -n "$$epoch"; then printf '%s' "$$epoch"; else git log -1 --format=%ct HEAD 2>/dev/null; fi)
SOURCE_AUDIT ?= scripts/check-source-only.sh
source-audit:
$(Q)test -f "$(SOURCE_AUDIT)" || { \
echo "ERROR: source-only scanner not found: $(SOURCE_AUDIT)" >&2; \
exit 1; \
}
$(Q)bash "$(SOURCE_AUDIT)"
dist:
$(Q)set -eu; \
export LC_ALL=C; \
umask 022; \
unset TAR_OPTIONS; \
test -n "$(DIST_VERSION)" || { echo "ERROR: cannot determine source version" >&2; exit 1; }; \
test -n "$(SOURCE_DATE_EPOCH)" || { echo "ERROR: SOURCE_DATE_EPOCH is empty" >&2; exit 1; }; \
case "$(SOURCE_DATE_EPOCH)" in *[!0-9]*) echo "ERROR: SOURCE_DATE_EPOCH must be an integer" >&2; exit 1;; esac; \
test -f "$(SOURCE_AUDIT)" || { echo "ERROR: source-only scanner not found: $(SOURCE_AUDIT)" >&2; exit 1; }; \
git rev-parse --verify 'HEAD^{commit}' >/dev/null; \
git rev-parse --verify 'HEAD^{tree}' >/dev/null; \
tmp=$$(mktemp -d "$${TMPDIR:-/tmp}/zupt-dist.XXXXXXXX"); \
trap 'rm -rf -- "$$tmp"' EXIT HUP INT TERM; \
git archive --format=tar --mtime="@$(SOURCE_DATE_EPOCH)" \
--prefix="$(DIST_NAME)/" 'HEAD^{tree}' | \
$(GZIP) $(GZIPFLAGS) > "$$tmp/$(DIST_NAME).tar.gz"; \
bash "$(SOURCE_AUDIT)" --archive "$$tmp/$(DIST_NAME).tar.gz"; \
mkdir -p "$$(dirname "$(DIST_TARBALL)")"; \
mv -f "$$tmp/$(DIST_NAME).tar.gz" "$(DIST_TARBALL)"; \
trap - EXIT HUP INT TERM; \
rm -rf -- "$$tmp"; \
if command -v sha256sum >/dev/null 2>&1; then \
digest=$$(sha256sum "$(DIST_TARBALL)" | awk '{print $$1}'); \
elif command -v shasum >/dev/null 2>&1; then \
digest=$$(shasum -a 256 "$(DIST_TARBALL)" | awk '{print $$1}'); \
else \
echo "ERROR: sha256sum or shasum is required" >&2; exit 1; \
fi; \
bytes=$$(wc -c < "$(DIST_TARBALL)"); \
printf '\n Reproducible source tarball:\n %s\n sha256: %s\n bytes: %s\n' \
"$(DIST_TARBALL)" "$$digest" "$$bytes"
# ═══════════════════════════════════════════════════════════════════
# CLEAN
# ═══════════════════════════════════════════════════════════════════
clean:
$(Q)rm -f $(PROGRAM) $(PROGRAM).exe $(LEGACY_PROGRAM) $(LEGACY_PROGRAM).exe $(MANPAGE_GZ) \
zupt_asan \
test_vectors test_f06 test_vaptvupt \
fuzz_decompress fuzz_vv_decompress tests/fuzz_format \
*.gcda *.gcno *.profraw *.profdata \
src/*.o src/*.gcda src/*.gcno jasmin/*.o jasmin/*.gcda jasmin/*.gcno \
tests/*.gcda tests/*.gcno sdk/*.gcda sdk/*.gcno
$(Q)for link in $(LEGACY_PROGRAM) $(LEGACY_PROGRAM).exe; do \
if [ -L "$$link" ]; then \
case "$$(readlink "$$link")" in $(PROGRAM)|$(PROGRAM).exe) rm -f "$$link" ;; esac; \
fi; \
done
$(Q)rm -rf sdk/build build build_obj coverage
# ═══════════════════════════════════════════════════════════════════
# TEST TARGETS
# ═══════════════════════════════════════════════════════════════════
test: check
test-all: check
$(Q)bash tests/regression.sh ./$(TARGET)
$(Q)sh tests/test_threaded.sh ./$(TARGET)
$(Q)sh tests/test_pq.sh ./$(TARGET)
$(Q)bash tests/test_dedup_props.sh ./$(TARGET)
$(Q)bash tests/test_ct_timing.sh
$(Q)bash tests/test_codec_exact_size.sh
$(Q)bash tests/test_mlkem_fips203.sh
$(Q)bash tests/test_sdk.sh
$(Q)bash tests/test_pqbox.sh
$(Q)bash tests/test_audit.sh
$(Q)bash tests/test_kdf_transparency.sh
# Release-only gates need a committed Git checkout and packaging metadata.
# Keep them out of downstream %check, which intentionally has no dist rebuild.
release-check: test-all audit-licenses
$(Q)bash tests/test_static_analysis.sh
$(Q)bash tests/test_packaging_syntax.sh
$(Q)bash scripts/test-installed-zupt.sh ./$(TARGET)
$(Q)if [ "$(WITH_SDK)" = 1 ]; then \
bash tests/test_audit_flake.sh "$${AUDIT_FLAKE_RUNS:-3}"; \
else \
echo "SKIP: audit flake stress needs WITH_SDK=1 and system libvuptsdk"; \
fi
$(Q)$(MAKE) clean
$(Q)bash "$(SOURCE_AUDIT)"
$(Q)bash tests/test_dist_reproducible.sh
# ═══════════════════════════════════════════════════════════════════
# CHECK — distro-friendly safe subset
# ═══════════════════════════════════════════════════════════════════
#
# Targeted at downstream packagers (openSUSE OBS, Debian, Fedora) who
# need a `%check` / `override_dh_auto_test` target that:
#
# - Runs in a few minutes (not the full byte-sweep arc)
# - Doesn't call `make clean` mid-stream (rules out test_dist_reproducible.sh)
# - Doesn't depend on tools that may be absent in the build chroot
# (no python3 PyYAML, no ruby, no dpkg-parsechangelog)
# - Doesn't depend on multi-threading that's flaky under emulation
# (skips test_threaded.sh and test_pq.sh's MT subset)
# - Covers the source-only CLI, archive safety, HMAC/integrity regressions,
# codec checks and cryptographic primitive vectors
# - Verifies cryptographic primitives against NIST/RFC vectors
#
# This is the recommended target for OBS %check sections.
check: $(TARGET) test-vectors test-f06 test-vv
$(Q)sh tests/run_quick.sh ./$(TARGET)
$(Q)bash tests/test_path_traversal.sh ./$(TARGET)
$(Q)bash tests/test_arg_order.sh ./$(TARGET)
$(Q)bash tests/test_password_sources.sh ./$(TARGET)
$(Q)bash tests/test_password_prompt_signal.sh ./$(TARGET)
$(Q)bash tests/test_key_files.sh ./$(TARGET)
$(Q)ZUPT_BIN="$(CURDIR)/$(TARGET)" bash tests/test_f08_topmac.sh
$(Q)ZUPT_BIN="$(CURDIR)/$(TARGET)" bash tests/test_f09_preface.sh
$(Q)ZUPT_BIN="$(CURDIR)/$(TARGET)" bash tests/test_f10_kdf_default.sh
$(Q)ZUPT_BIN="$(CURDIR)/$(TARGET)" bash tests/test_f11_authfail_message.sh
$(Q)ZUPT_BIN="$(CURDIR)/$(TARGET)" bash tests/test_f12_comment.sh
$(Q)bash tests/test_atomic_archive_output.sh ./$(TARGET)
$(Q)bash tests/test_legacy_disk_5_2_1.sh ./$(TARGET)
$(Q)bash tests/test_disk_device_capacity.sh ./$(TARGET)
$(Q)bash tests/test_block_swap.sh ./$(TARGET)
$(Q)bash tests/test_block_type_confusion.sh ./$(TARGET)
$(Q)bash tests/test_authenticated_dedup_reorder.sh ./$(TARGET)
$(Q)bash tests/test_format_little_endian.sh ./$(TARGET)
$(Q)bash tests/test_dedup_nonce.sh ./$(TARGET)
$(Q)bash tests/test_gui_branding.sh ./$(TARGET)
$(Q)bash tests/test_help_consistency.sh ./$(TARGET)
$(Q)bash tests/test_benchmark_temp_safety.sh ./$(TARGET)
$(Q)bash tests/test_vv_decode_slack.sh ./$(TARGET)
$(Q)bash tests/test_sha256_shani.sh
$(Q)bash tests/test_hmac_incremental.sh
$(Q)bash tests/test_completions_manpage.sh
$(Q)bash tests/test_source_only.sh
$(Q)./test_vectors
@echo ""
@echo " ═════════════════════════════════════════"
@echo " All executed distro-safe checks passed (see SKIP lines above)."
@echo " ═════════════════════════════════════════"
TEST_CRYPTO_SOURCES = src/zupt_sha256.c src/zupt_sha256_shani.c \
src/zupt_crypto.c src/zupt_aes256.c src/zupt_xxh.c src/zupt_keccak.c \
src/zupt_x25519.c src/zupt_mlkem.c src/zupt_cpuid.c src/zupt_mlock.c
TEST_CRYPTO_OBJS = $(TEST_CRYPTO_SOURCES:.c=.o)
test-vectors: tests/test_vectors.c $(HEADERS) $(TEST_CRYPTO_OBJS) $(JAZZ_O)
$(Q)$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(LDFLAGS) $(PROJECT_LDFLAGS) \
tests/test_vectors.c $(TEST_CRYPTO_OBJS) $(JAZZ_O) \
-o test_vectors $(FEATURE_LDLIBS) $(PROJECT_LDLIBS) $(LDLIBS)
# F-06 regression — HMAC accept-on-disjoint-bits (ZUPT 2.2.5).
# Inherits $(CFLAGS) so ZUPT_USE_JASMIN is defined on x86_64 (exercising
# the original buggy path). Links the same crypto modules as test-vectors
# plus the Jasmin .o files when available.
test-f06: tests/test_f06_hmac.c $(HEADERS) $(TEST_CRYPTO_OBJS) $(JAZZ_O)
$(Q)$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(LDFLAGS) $(PROJECT_LDFLAGS) \
tests/test_f06_hmac.c $(TEST_CRYPTO_OBJS) $(JAZZ_O) -o test_f06 \
$(FEATURE_LDLIBS) $(PROJECT_LDLIBS) $(LDLIBS)
$(Q)./test_f06
# VAPTVUPT: VaptVupt codec unit tests
TEST_VV_OBJS = $(VV_SIMD_OBJS) $(VV_PLAIN_OBJS) src/zupt_xxh.o src/zupt_cpuid.o
test-vv: tests/test_vaptvupt.c $(HEADERS) $(TEST_VV_OBJS)
$(Q)$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(CFLAGS) $(PROJECT_CFLAGS) \
$(LDFLAGS) $(PROJECT_LDFLAGS) tests/test_vaptvupt.c $(TEST_VV_OBJS) \
-o test_vaptvupt $(PROJECT_LDLIBS) $(LDLIBS)
$(Q)./test_vaptvupt
ASAN_BUILD_DIR = build/asan
ASAN_CFLAGS ?= -O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined
ASAN_LDFLAGS ?= -fsanitize=address,undefined
ASAN_OBJS = $(patsubst src/%.c,$(ASAN_BUILD_DIR)/%.o,$(SOURCES))
$(ASAN_BUILD_DIR):
$(Q)mkdir -p "$@"
$(ASAN_BUILD_DIR)/zupt_sha256_shani.o: src/zupt_sha256_shani.c $(HEADERS) | $(ASAN_BUILD_DIR)
$(Q)$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(ASAN_CFLAGS) $(SHANI_FLAGS) -c -o $@ $<
$(ASAN_BUILD_DIR)/vv_%.o: src/vv_%.c $(HEADERS) | $(ASAN_BUILD_DIR)
$(Q)$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(ASAN_CFLAGS) -c -o $@ $<
$(ASAN_BUILD_DIR)/vaptvupt_api.o: src/vaptvupt_api.c $(HEADERS) | $(ASAN_BUILD_DIR)
$(Q)$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(ASAN_CFLAGS) -c -o $@ $<
$(ASAN_BUILD_DIR)/%.o: src/%.c $(HEADERS) | $(ASAN_BUILD_DIR)
$(Q)$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(ASAN_CFLAGS) -c -o $@ $<
test-asan: $(ASAN_OBJS) $(JAZZ_O)
$(Q)$(CC) $(CFLAGS) $(PROJECT_CFLAGS) $(ASAN_CFLAGS) \
$(LDFLAGS) $(PROJECT_LDFLAGS) $(ASAN_LDFLAGS) \
$(ASAN_OBJS) $(JAZZ_O) -o zupt_asan \
$(FEATURE_LDLIBS) $(PROJECT_LDLIBS) $(LDLIBS)
@echo "ASAN build: ./zupt_asan"
# Build the format-parser fuzz harness. Runs against ./zupt_asan to catch
# memory errors AND crashes in mutation-fuzz of the listing/extract path.
fuzz-format: tests/fuzz_format
tests/fuzz_format: tests/fuzz_format.c
$(Q)$(CC) $(CPPFLAGS) $(CFLAGS) $(PROJECT_CFLAGS) \
$(LDFLAGS) $(PROJECT_LDFLAGS) tests/fuzz_format.c \
-o tests/fuzz_format $(PROJECT_LDLIBS) $(LDLIBS)
@echo "Format fuzz harness: ./tests/fuzz_format"
# Run 5000 iterations of mutation fuzz against the ASAN binary.
# Any crash or sanitizer error fails CI.
fuzz-format-run: tests/fuzz_format test-asan $(TARGET)
$(Q)set -eu; \
tmp=$$(mktemp -d "$${TMPDIR:-/tmp}/zupt-fuzz.XXXXXXXX"); \
trap 'rm -rf -- "$$tmp"' EXIT HUP INT TERM; \
printf '%s\n' 'fuzz seed file' > "$$tmp/input.txt"; \
./$(TARGET) c "$$tmp/seed.zupt" "$$tmp/input.txt" >/dev/null; \
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 \
./tests/fuzz_format 1000 ./zupt_asan "$$tmp/seed.zupt"
@echo " Format fuzz: 1000 iterations under ASAN/UBSAN — no crashes."
# Runs a round-trip smoke test against the ASAN/UBSAN/LSAN-instrumented binary.
# The exhaustive codec exact-size sanitizer loop remains part of test-all.
test-asan-run: test-asan
$(Q)set -eu; \
tmp=$$(mktemp -d "$${TMPDIR:-/tmp}/zupt-asan.XXXXXXXX"); \
trap 'rm -rf -- "$$tmp"' EXIT HUP INT TERM; \
export ASAN_OPTIONS=detect_leaks=1:abort_on_error=1; \
export UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1; \
printf '%s\n' 'sanitizer round-trip' > "$$tmp/input.txt"; \
./zupt_asan c "$$tmp/archive.zupt" "$$tmp/input.txt" >/dev/null; \
./zupt_asan t "$$tmp/archive.zupt" >/dev/null; \
mkdir "$$tmp/out"; \
(cd "$$tmp/out" && "$(CURDIR)/zupt_asan" x "$$tmp/archive.zupt" >/dev/null); \
extracted=$$(find "$$tmp/out" -type f -print -quit); \
test -n "$$extracted"; \
cmp "$$tmp/input.txt" "$$extracted"; \
dd if=/dev/urandom of="$$tmp/block" bs=65536 count=1 2>/dev/null; \
cp "$$tmp/block" "$$tmp/disk.img"; \
dd if="$$tmp/block" of="$$tmp/disk.img" bs=65536 seek=1 conv=notrunc 2>/dev/null; \
printf '%s\n' 'sanitizer-disk-password' > "$$tmp/password"; \
./zupt_asan disk backup --dedup -b 65536 --pass-file "$$tmp/password" -s \
"$$tmp/disk.zupt" "$$tmp/disk.img" >/dev/null; \
./zupt_asan t --pass-file "$$tmp/password" "$$tmp/disk.zupt" >/dev/null; \
./zupt_asan disk restore --pass-file "$$tmp/password" \
"$$tmp/disk.zupt" "$$tmp/restored.img" >/dev/null; \
cmp "$$tmp/disk.img" "$$tmp/restored.img"; \
./zupt_asan --help >/dev/null; \
./zupt_asan --version >/dev/null
@echo " ASAN/UBSAN: source-only smoke test passed."
# AFL++ fuzzing harnesses (requires afl-clang-fast). Compile every source with
# instrumentation while retaining translation-unit-local ISA flags.
AFL_CC ?= afl-clang-fast
FUZZ_BUILD_DIR = build/fuzz
FUZZ_SOURCES = $(filter-out src/zupt_main.c,$(SOURCES))
FUZZ_OBJS = $(patsubst src/%.c,$(FUZZ_BUILD_DIR)/%.o,$(FUZZ_SOURCES))
FUZZ_VV_OBJS = $(addprefix $(FUZZ_BUILD_DIR)/,vv_encoder.o vv_decoder.o \
vv_ans.o vv_huffman.o vv_simd.o zupt_xxh.o zupt_cpuid.o)
$(FUZZ_BUILD_DIR):
$(Q)mkdir -p "$@"
$(FUZZ_BUILD_DIR)/zupt_sha256_shani.o: src/zupt_sha256_shani.c $(HEADERS) | $(FUZZ_BUILD_DIR)
$(Q)$(AFL_CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(ASAN_CFLAGS) $(SHANI_FLAGS) -c -o $@ $<
$(FUZZ_BUILD_DIR)/vv_%.o: src/vv_%.c $(HEADERS) | $(FUZZ_BUILD_DIR)
$(Q)$(AFL_CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(ASAN_CFLAGS) $(VV_WARNING_FLAGS) \
$(if $(filter $(FUZZ_BUILD_DIR)/vv_decoder.o,$@),$(VV_DECODER_WARNING_FLAGS)) -c -o $@ $<
$(FUZZ_BUILD_DIR)/vaptvupt_api.o: src/vaptvupt_api.c $(HEADERS) | $(FUZZ_BUILD_DIR)
$(Q)$(AFL_CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(ASAN_CFLAGS) $(VV_WARNING_FLAGS) -c -o $@ $<
$(FUZZ_BUILD_DIR)/%.o: src/%.c $(HEADERS) | $(FUZZ_BUILD_DIR)
$(Q)$(AFL_CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(ASAN_CFLAGS) -c -o $@ $<
fuzz_decompress: tests/fuzz_decompress.c $(FUZZ_OBJS)
$(Q)$(AFL_CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(FEATURE_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(ASAN_CFLAGS) \
$(LDFLAGS) $(PROJECT_LDFLAGS) $(ASAN_LDFLAGS) \
tests/fuzz_decompress.c $(FUZZ_OBJS) -o $@ \
$(FEATURE_LDLIBS) $(PROJECT_LDLIBS) $(LDLIBS)
fuzz_vv_decompress: tests/fuzz_vv_decompress.c $(FUZZ_VV_OBJS)
$(Q)$(AFL_CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) \
$(CFLAGS) $(PROJECT_CFLAGS) $(ASAN_CFLAGS) \
$(LDFLAGS) $(PROJECT_LDFLAGS) $(ASAN_LDFLAGS) \
tests/fuzz_vv_decompress.c $(FUZZ_VV_OBJS) -o $@ \
$(PROJECT_LDLIBS) $(LDLIBS)
fuzz-build: fuzz_decompress fuzz_vv_decompress
@echo "Fuzz harnesses built. Run:"
@echo " afl-fuzz -i corpus -o findings -- ./fuzz_decompress"
@echo " afl-fuzz -i corpus_vv -o findings_vv -- ./fuzz_vv_decompress"
help:
@echo "ZUPT v$(DIST_VERSION) build targets:"
@echo " make Build zupt binary"
@echo " make V=1 Build with verbose output"
@echo " make check Distro-safe source-only test suite"
@echo " make test-all Complete runtime suite; unavailable integrations SKIP"
@echo " make release-check Runtime, static, packaging, source and dist gates"
@echo " make test-vv VaptVupt codec unit tests"
@echo " make test-asan Build with AddressSanitizer"
@echo " make fuzz-build Build AFL++ fuzzing harnesses"
@echo " make dist Reproducible, audited source archive"
@echo " make source-audit Audit tracked, worktree and HEAD archive content"
@echo " make install Install to $(PREFIX)"
@echo " make uninstall Remove from $(PREFIX)"
@echo " make clean Remove build artifacts"
@echo ""
@echo "Compiler target: $(TARGET_MACHINE)"
@echo "Optional integrations (off by default):"
@echo " WITH_SDK=1 system libvuptsdk via pkg-config/overrides"
@echo " WITH_PQBOX=1 system libpqvaptvupt via pkg-config/overrides"
@echo " WITH_JASMIN=1 optional textual assembly on x86_64"
@echo " INSTALL_LEGACY_ALIAS=1 installs opt-in 'vaptvupt' compatibility links"
# ─────────────────────────────────────────────────────────────────────
# SDK targets — see sdk/Makefile.sdk
# ─────────────────────────────────────────────────────────────────────
include sdk/Makefile.sdk