Browse Source

Track build configuration and preserve incremental header dependencies

master
evgeny 2 days ago
parent
commit
ff9d205d5b
  1. 363
      build.sh
  2. 24
      tests/Makefile.am

363
build.sh

@ -1,216 +1,195 @@
#!/bin/bash #!/bin/bash
# Build script for uTun (supports both Linux and Windows/MSYS2) # Инкрементальная сборка Linux и Windows/MSYS2; флаги задаются через configure.
# Usage: ./build.sh [options] set -euo pipefail
# Options: cd "$(dirname "$0")"
# -h, --help Show this help
# --clean Clean before build
# --full Full rebuild (autoreconf + configure + make)
# --asan Build with AddressSanitizer (debug)
# --hardened Build with compiler security hardening flags
# -j[N] Build with N parallel jobs (default: 4)
set -e # Exit on error MODE=normal
# Detect platform
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ -n "$MSYSTEM" ]]; then
IS_WINDOWS=1
echo "Detected Windows/MSYS2 environment"
else
IS_WINDOWS=0
echo "Detected Linux environment"
fi
# Parse arguments
CLEAN=0 CLEAN=0
FULL_REBUILD=0 FULL=0
USE_ASAN=0 BUILD_GUI=1
USE_HARDENED=0 JOBS=4
JOBS="-j4" MAKE_ARGS=()
MAKE_ARGS="" MAKE_SETTINGS=()
HARDEN_FLAGS="" STATE=.utun-build-state
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case "$1" in
-h|--help) -h|--help)
echo "Usage: ./build.sh [options]" echo "Usage: ./build.sh [--clean] [--full] [--asan|--hardened] [--no-gui] [-jN] [make arguments]"
echo "Options:" echo " --clean Rebuild objects without forcing configure"
echo " -h, --help Show this help" echo " --full Regenerate, configure and rebuild everything"
echo " --clean Clean before build" echo " --asan AddressSanitizer; skip tests"
echo " --full Full rebuild (autoreconf + configure + make)" echo " --hardened Enable compiler security hardening"
echo " --asan Build with AddressSanitizer" echo " --no-gui Skip the separately configured ChatGUI"
echo " --hardened Build with compiler security hardening flags" echo " -jN, -j N Parallel jobs (default: 4)"
echo " -j[N] Build with N parallel jobs (default: 4)" echo " CC/CFLAGS/CPPFLAGS/LDFLAGS/LIBS assignments are passed to configure"
exit 0 exit 0 ;;
;; --clean) CLEAN=1 ;;
--clean) --full) FULL=1 ;;
CLEAN=1 --no-gui) BUILD_GUI=0 ;;
shift --asan|--hardened)
;; selected_mode=${1#--}
--full) if [[ "$MODE" != normal && "$MODE" != "$selected_mode" ]]; then
FULL_REBUILD=1 echo "ERROR: --asan and --hardened cannot be combined" >&2; exit 1
shift fi
;; MODE=$selected_mode ;;
--asan) -j)
USE_ASAN=1 [[ $# -gt 1 ]] || { echo "ERROR: -j requires a positive job count" >&2; exit 1; }
shift shift; JOBS=$1 ;;
;; -j*) JOBS=${1#-j} ;;
--hardened) CC=*|CFLAGS=*|CPPFLAGS=*|LDFLAGS=*|LIBS=*) export "$1" ;;
USE_HARDENED=1 CC+=*|CFLAGS+=*|CPPFLAGS+=*|LDFLAGS+=*|LIBS+=*)
shift echo "ERROR: use NAME=value for configure flags; append assignments are not supported" >&2; exit 1 ;;
;;
-j*)
JOBS="$1"
shift
;;
*) *)
MAKE_ARGS="$MAKE_ARGS $1" MAKE_ARGS+=("$1")
shift [[ "$1" != *=* ]] || MAKE_SETTINGS+=("$1") ;;
;;
esac esac
shift
done done
[[ "$JOBS" =~ ^[1-9][0-9]*$ ]] || { echo "ERROR: invalid job count: $JOBS" >&2; exit 1; }
# Check required tools for tool in make sha256sum; do
if ! command -v gcc &> /dev/null; then command -v "$tool" >/dev/null || { echo "ERROR: required tool not found: $tool" >&2; exit 1; }
echo "Error: gcc not found." done
if [ "$IS_WINDOWS" -eq 1 ]; then read -r -a COMPILER <<< "${CC:-gcc}"
echo "Please install: pacman -S mingw-w64-ucrt-x86_64-toolchain" command -v "${COMPILER[0]}" >/dev/null || { echo "ERROR: compiler not found: ${CC:-gcc}" >&2; exit 1; }
else COMPILER_ID=$("${COMPILER[@]}" --version) || { echo "ERROR: cannot identify compiler: ${CC:-gcc}" >&2; exit 1; }
echo "Please install build-essential or gcc"
fi # Хеши учитывают содержимое и имена файлов, включая добавление/удаление макросов.
exit 1 hash_files() {
local result
result=$(sha256sum "$@" | sha256sum)
printf '%s\n' "${result%% *}"
}
BUILD_DIRS=(lib lib/libopus src tests)
AUTOTOOLS_INPUTS=(configure.ac Makefile.am lib/Makefile.am lib/libopus/Makefile.am src/Makefile.am tests/Makefile.am)
AUTOTOOLS_OUTPUTS=(configure aclocal.m4 config.h.in Makefile.in lib/Makefile.in lib/libopus/Makefile.in src/Makefile.in tests/Makefile.in)
CONFIG_OUTPUTS=(config.status config.h Makefile lib/Makefile lib/libopus/Makefile src/Makefile tests/Makefile)
if [[ -d m4 ]]; then
while IFS= read -r -d '' macro; do AUTOTOOLS_INPUTS+=("$macro"); done < <(find m4 -type f -name '*.m4' -print0 | sort -z)
fi fi
GENERATION_INPUT=$(hash_files "${AUTOTOOLS_INPUTS[@]}")
# Clean if requested GENERATION_OUTPUT=missing
if [ "$CLEAN" -eq 1 ]; then CONFIG_OUTPUT=missing
echo "Cleaning build..." missing_generation=0
make clean 2>/dev/null || true missing_configuration=0
for file in "${AUTOTOOLS_OUTPUTS[@]}"; do [[ -f "$file" ]] || missing_generation=1; done
for file in "${CONFIG_OUTPUTS[@]}"; do [[ -f "$file" ]] || missing_configuration=1; done
if [[ "$missing_generation" = 0 ]]; then GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}"); fi
if [[ "$missing_configuration" = 0 ]]; then CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}"); fi
FFMPEG_PACKAGES=(libavformat libavcodec libavutil libswscale libswresample)
FFMPEG_OPTION=--without-ffmpeg
FFMPEG_ID=unavailable
FFMPEG_CFLAGS=""
FFMPEG_LDFLAGS=""
if command -v pkg-config >/dev/null && pkg-config --exists "${FFMPEG_PACKAGES[@]}"; then
FFMPEG_OPTION=--with-ffmpeg
FFMPEG_ID=$(pkg-config --modversion "${FFMPEG_PACKAGES[@]}")
FFMPEG_CFLAGS=$(pkg-config --cflags "${FFMPEG_PACKAGES[@]}")
FFMPEG_LDFLAGS=$(pkg-config --libs-only-L "${FFMPEG_PACKAGES[@]}")
fi fi
request_sum=$({
# Full rebuild: regenerate build system printf '%s\0' "$MODE" "${OSTYPE:-}" "${MSYSTEM:-}" "${MSYSTEM_PREFIX:-}" "${CC:-gcc}" "$COMPILER_ID"
if [ "$FULL_REBUILD" -eq 1 ]; then printf '%s\0' "${CFLAGS+x}" "${CFLAGS-}" "${CPPFLAGS+x}" "${CPPFLAGS-}" "${LDFLAGS+x}" "${LDFLAGS-}"
echo "" printf '%s\0' "${LIBS+x}" "${LIBS-}" "${MAKEFLAGS-}" "${DEBUG_FLAGS+x}" "${DEBUG_FLAGS-}"
echo "======================================" printf '%s\0' "$FFMPEG_OPTION" "$FFMPEG_ID" "$FFMPEG_CFLAGS" "$FFMPEG_LDFLAGS"
echo "FULL REBUILD: autoreconf" printf '%s\0' "${MAKE_SETTINGS[@]}"
echo "======================================" sha256sum build.sh
echo "" } | sha256sum)
CONFIG_REQUEST=${request_sum%% *}
# Check for autotools
if ! command -v autoreconf &> /dev/null; then OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT=""
echo "Error: autoreconf not found." if [[ -f "$STATE" ]]; then
if [ "$IS_WINDOWS" -eq 1 ]; then if ! {
echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools" read -r OLD_GENERATION_INPUT; read -r OLD_GENERATION_OUTPUT
else read -r OLD_CONFIG_REQUEST; read -r OLD_CONFIG_OUTPUT
echo "Install autotools: apt-get install autoconf automake libtool" } < "$STATE"; then
fi echo "WARN: incomplete build state; adopting the build with a clean rebuild"
exit 1 OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT=""
fi
# Clean generated files
echo "Cleaning generated files..."
make distclean 2>/dev/null || true
rm -rf build/
# Regenerate build system (root Makefile is hand-written, not generated)
echo ""
echo "Running autoreconf -fi..."
autoreconf -fi 2>&1 | tee autoreconf.log
echo ""
echo "======================================"
echo "Build system regenerated successfully"
echo "======================================"
echo ""
else
# Quick/incremental build: generate configure if needed
if [ ! -f ./configure ]; then
echo "Configure script not found. Generating..."
if ! command -v autoreconf &> /dev/null; then
echo "Error: autoreconf not found."
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools"
else
echo "Install autotools: apt-get install autoconf automake libtool"
fi
exit 1
fi
autoreconf -fiv
fi fi
fi fi
# Auto-detect FFmpeg (optional video transcoding). If libav libraries are REGENERATE=0
# available, build with --with-ffmpeg; configure gracefully skips it otherwise. RECONFIGURE=0
FFMPEG_OPT="" if [[ "$FULL" = 1 || "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" || "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then
if command -v pkg-config >/dev/null 2>&1 && \ echo "INFO: regenerating Autotools: full=$FULL"
pkg-config --exists libavformat libavcodec libavutil libswscale libswresample 2>/dev/null; then if [[ "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" ]]; then echo "INFO: Autotools descriptions changed or unrecorded"; fi
FFMPEG_OPT="--with-ffmpeg" if [[ "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then echo "INFO: generated Autotools files changed or missing"; fi
echo "FFmpeg detected — building with video transcoding" command -v autoreconf >/dev/null || { echo "ERROR: autoreconf is required to regenerate the build" >&2; exit 1; }
else mkdir -p m4
echo "FFmpeg not found — building without video transcoding" autoreconf -fi 2>&1 | tee autoreconf.log || { echo "ERROR: autoreconf failed; see autoreconf.log" >&2; exit 1; }
REGENERATE=1
fi fi
if [[ "$REGENERATE" = 1 || "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" || "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then
# Generate Makefiles if needed echo "INFO: configuring: mode=$MODE regenerated=$REGENERATE"
if [ ! -f Makefile ]; then if [[ "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" ]]; then echo "INFO: compiler, flags or optional dependencies changed or unrecorded"; fi
echo "" if [[ "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then echo "INFO: configuration files changed or missing"; fi
echo "Running ./configure..." CONFIG_ARGS=("$FFMPEG_OPTION" --disable-hardening)
if [ "$USE_ASAN" -eq 1 ]; then if [[ "$MODE" = asan ]]; then
./configure CFLAGS="-g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie" LDFLAGS="-no-pie" $FFMPEG_OPT export CFLAGS="${CFLAGS-} -g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie"
elif [ "$USE_HARDENED" -eq 1 ]; then export LDFLAGS="${LDFLAGS-} -fsanitize=address -no-pie"
./configure --enable-hardening $FFMPEG_OPT elif [[ "$MODE" = hardened ]]; then
else CONFIG_ARGS+=(--enable-hardening)
./configure $FFMPEG_OPT
fi fi
if [[ "$FFMPEG_OPTION" = --with-ffmpeg ]]; then
export CPPFLAGS="${CPPFLAGS-} $FFMPEG_CFLAGS"
export LDFLAGS="${LDFLAGS-} $FFMPEG_LDFLAGS"
fi
./configure "${CONFIG_ARGS[@]}" 2>&1 | tee configure.log || { echo "ERROR: configure failed; see configure.log" >&2; exit 1; }
RECONFIGURE=1
fi fi
# Build (all .o files go to build/ directory, binary copied to project root) # Проверяем только зависимости объектов, перечисленных Automake; посторонние сборки не затрагиваем.
echo "" MISSING_DEPS=0
echo "Compiling with $JOBS..." for directory in "${BUILD_DIRS[@]}"; do
BUILD_SUCCESS=0 while read -r keyword depfile marker; do
if [ "$USE_ASAN" -eq 1 ]; then [[ "$keyword" = include && "$marker" = '# am--include-marker' && "$depfile" = *.Po ]] || continue
echo "(AddressSanitizer enabled — lib/ and src/ only, skipping tests)" depfile=${depfile//\$\(DEPDIR\)/.deps}
AF="-fsanitize=address -fno-omit-frame-pointer -g -Og" object=${depfile/.deps\//}
AL="-fsanitize=address" object=${object%.Po}
(cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; } [[ -f "$directory/$object.o" || -f "$directory/$object.obj" ]] || continue
# libopus собирается без ASAN (собственный CFLAGS в Makefile, .a несовместим с PIE) dependency_line=""
(cd lib/libopus && make clean $JOBS $MAKE_ARGS 2>&1 && make $JOBS $MAKE_ARGS 2>&1) || { echo "libopus build failed"; exit 1; } if [[ -s "$directory/$depfile" ]]; then IFS= read -r dependency_line < "$directory/$depfile" || true; fi
(cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; } if [[ "$dependency_line" != *:* ]]; then
BUILD_SUCCESS=1 echo "WARN: missing or empty dependencies for $directory/$object: $directory/$depfile"
elif [ "$USE_HARDENED" -eq 1 ]; then MISSING_DEPS=1
echo "(Hardening flags enabled)" break
HF="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -Wformat=2 -Wformat-overflow=2 -fPIE" fi
HL="-Wl,-z,relro -Wl,-z,now -pie" done < "$directory/Makefile"
(cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; } done
(cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; } if [[ "$RECONFIGURE" = 1 || "$CLEAN" = 1 || "$MISSING_DEPS" = 1 ]]; then
(cd tests && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "tests build failed"; exit 1; } echo "INFO: cleaning objects: reconfigured=$RECONFIGURE requested=$CLEAN missing_dependencies=$MISSING_DEPS"
BUILD_SUCCESS=1 make clean 2>&1 | tee build.log || { echo "ERROR: clean failed; see build.log" >&2; exit 1; }
else else
make $JOBS $MAKE_ARGS 2>&1 | tee build.log echo "INFO: configuration unchanged; incremental build (mode=$MODE)"
if [ ${PIPESTATUS[0]} -eq 0 ]; then : > build.log
BUILD_SUCCESS=1
fi
fi fi
if [ $BUILD_SUCCESS -eq 1 ]; then echo "INFO: compiling: mode=$MODE jobs=$JOBS"
echo "" if [[ "$MODE" = asan ]]; then
echo "Build completed successfully!" echo "INFO: AddressSanitizer: building lib, Opus and src; skipping tests"
echo "Binary: utun" MAKE_ARGS+=("SUBDIRS=lib lib/libopus src")
fi
# Rebuild chatgui (CMake project) if transport sources changed make "-j$JOBS" "${MAKE_ARGS[@]}" 2>&1 | tee -a build.log || { echo "ERROR: build failed; see build.log" >&2; exit 1; }
if [ -f tools/chatgui/build/CMakeCache.txt ] && command -v cmake &> /dev/null; then
echo "" # Состояние обновляется после успешной сборки ядра; ошибка GUI не инвалидирует готовые объекты.
echo "======================================" GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}")
echo "Rebuilding chatgui (CMake)" CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}")
echo "======================================" state_tmp="$STATE.tmp.$$"
( trap 'rm -f "$state_tmp"' EXIT
if [ "${MSYSTEM:-}" = UCRT64 ]; then printf '%s\n' "$GENERATION_INPUT" "$GENERATION_OUTPUT" "$CONFIG_REQUEST" "$CONFIG_OUTPUT" > "$state_tmp"
cd tools/chatgui && bash build.sh mv -f "$state_tmp" "$STATE"
else
cd tools/chatgui/build && cmake --build . -j4 2>&1 if [[ "$BUILD_GUI" = 1 && -f tools/chatgui/build/CMakeCache.txt ]]; then
fi command -v cmake >/dev/null || { echo "ERROR: ChatGUI exists but cmake is not available" >&2; exit 1; }
) || echo "WARNING: chatgui build failed" echo "INFO: building ChatGUI with its CMake configuration: jobs=$JOBS"
if [[ "${MSYSTEM:-}" = UCRT64 ]]; then
(cd tools/chatgui && bash build.sh "-j$JOBS") || { echo "ERROR: ChatGUI build failed" >&2; exit 1; }
else
cmake --build tools/chatgui/build --parallel "$JOBS" || { echo "ERROR: ChatGUI build failed" >&2; exit 1; }
fi fi
else
echo ""
echo "ERROR: Build failed!"
echo "Check build.log for details"
exit 1
fi fi
echo "Build completed successfully!"
echo "Binary: src/utun"

24
tests/Makefile.am

@ -1,6 +1,6 @@
# Tests Makefile.am for utun - all tests with new ll_queue library # Tests Makefile.am for utun - all tests with new ll_queue library
EXTRA_DIST = fixtures/reality_openssl_hello.hex data/silero_speech.pcm data/silero_speech.txt EXTRA_DIST = fixtures/reality_openssl_hello.hex data/silero_speech.pcm data/silero_speech.txt test_build_script.py
# Порядок по замерам Linux: быстрые проверки раньше длительных; состав зависит от прежних configure-условий. # Порядок по замерам Linux: быстрые проверки раньше длительных; состав зависит от прежних configure-условий.
@ -715,23 +715,31 @@ test_media_delivery_chat_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
all-local: copy-test-configs all-local: copy-test-configs
copy-test-configs: $(check_PROGRAMS) copy-test-configs: $(check_PROGRAMS)
@for conf in $(srcdir)/test_*.conf $(srcdir)/test_*.json; do \ @if test "$(abs_srcdir)" != "$(abs_builddir)"; then \
if test -f "$$conf"; then \ for conf in $(srcdir)/test_*.conf $(srcdir)/test_*.json; do \
cp -f "$$conf" "$(top_builddir)/tests/" 2>/dev/null || true; \ if test -f "$$conf"; then \
fi; \ cp -f "$$conf" "$(abs_builddir)/" || { echo "ERROR: cannot copy $$conf" >&2; exit 1; }; \
done 2>/dev/null || true fi; \
done; \
fi
# Clean # Clean
clean-local: clean-local:
-rm -f $(check_PROGRAMS) -rm -f $(check_PROGRAMS)
-rm -f $(top_builddir)/tests/test_*.conf $(top_builddir)/tests/test_*.json 2>/dev/null || true @if test "$(abs_srcdir)" != "$(abs_builddir)"; then rm -f $(abs_builddir)/test_*.conf $(abs_builddir)/test_*.json; fi
-rm -rf $(top_builddir)/tests/logs 2>/dev/null || true -rm -rf $(top_builddir)/tests/logs 2>/dev/null || true
# Test logs directory # Test logs directory
TEST_LOG_DIR = $(top_builddir)/tests/logs TEST_LOG_DIR = $(top_builddir)/tests/logs
# Custom check target that runs tests and saves logs to files # Custom check target that runs tests and saves logs to files
check-local: $(check_PROGRAMS) .PHONY: check-build-script
check-build-script:
@if command -v python3 >/dev/null 2>&1; then \
python3 "$(srcdir)/test_build_script.py" "$(top_srcdir)/build.sh"; \
else echo "[SKIP] build.sh regression checks require python3"; fi
check-local: $(check_PROGRAMS) check-build-script
@rm -f $(TEST_LOG_DIR)/*.log 2>/dev/null || true; $(MKDIR_P) $(TEST_LOG_DIR); \ @rm -f $(TEST_LOG_DIR)/*.log 2>/dev/null || true; $(MKDIR_P) $(TEST_LOG_DIR); \
G='\033[0;32m'; R='\033[0;31m'; Y='\033[0;33m'; N='\033[0m'; \ G='\033[0;32m'; R='\033[0;31m'; Y='\033[0;33m'; N='\033[0m'; \
passed=0; \ passed=0; \

Loading…
Cancel
Save