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