#!/bin/bash # Инкрементальная сборка Linux и Windows/MSYS2; флаги задаются через configure. set -euo pipefail cd "$(dirname "$0")" MODE=normal CLEAN=0 FULL=0 BUILD_GUI=1 JOBS=4 MAKE_ARGS=() MAKE_SETTINGS=() STATE=.utun-build-state while [[ $# -gt 0 ]]; do case "$1" in -h|--help) 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+=("$1") [[ "$1" != *=* ]] || MAKE_SETTINGS+=("$1") ;; esac shift done [[ "$JOBS" =~ ^[1-9][0-9]*$ ]] || { echo "ERROR: invalid job count: $JOBS" >&2; 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 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 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 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 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 # Проверяем только зависимости объектов, перечисленных 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 echo "INFO: configuration unchanged; incremental build (mode=$MODE)" : > build.log fi 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 fi echo "Build completed successfully!" echo "Binary: src/utun"