Merge remote-tracking branch 'origin/master'

# Conflicts:
#	tests/Makefile.am
This commit is contained in:
evgeny committed 2026-10-10 17:25:40 +02:00
commit 69a99c7860
10 files changed
+529 -354

No files matched your search

+2
View File
@@ -32,6 +32,7 @@ index.html
*.exe *.exe
# Autotools # Autotools
/.utun-build-state*
Makefile Makefile
!tools/etcpmon/Makefile !tools/etcpmon/Makefile
Makefile.in Makefile.in
@@ -84,6 +85,7 @@ logs/
tests/test_* tests/test_*
!tests/test_*.c !tests/test_*.c
!tests/test_*.h !tests/test_*.h
!tests/test_*.py
!tests/test_*.conf !tests/test_*.conf
!tests/test_*.json !tests/test_*.json
tests/bench_* tests/bench_*
+13 -2
View File
@@ -51,9 +51,19 @@ This file contains essential information for AI coding agents working in the uTu
powershell build_full.bat # запускает bash build.sh --full через MSYS2 UCRT64 powershell build_full.bat # запускает bash build.sh --full через MSYS2 UCRT64
``` ```
### Build (FreeBSD)
```sh
# Первичная установка инструментов от root:
pkg install -y bash gmake autoconf automake libtool pkgconf openssl python3
./build.sh -j4 # общий скрипт; автоматически выбирает gmake и Clang
./build_bsd_full.sh -j4 # полная сборка через общий скрипт
```
Python 3 нужен для проверок скрипта сборки. SHA-256 берётся из базовой системы FreeBSD.
### Incremental Build ### Incremental Build
```bash ```bash
./build.sh -j4 # make с авто-конфигурацией если надо ./build.sh -j4 # инкрементальная сборка, конфигурация и очистка при необходимости
./build.sh --no-gui -j4 # только ядро и тесты, без отдельной сборки GUI
``` ```
```powershell ```powershell
powershell -Command ".\build.bat" 2>&1 # Windows, логи: build_win.log powershell -Command ".\build.bat" 2>&1 # Windows, логи: build_win.log
@@ -127,6 +137,7 @@ cd tests && ./test_services
``` ```
Использовать цель из `tests/Makefile.am`: ручная компиляция `src/*.c` пропускает подкаталоги и добавляет лишний `main`. Использовать цель из `tests/Makefile.am`: ручная компиляция `src/*.c` пропускает подкаталоги и добавляет лишний `main`.
Уровни и категории диагностики настраиваются в самом тесте через `debug_set_level` / `debug_set_category_level`. Уровни и категории диагностики настраиваются в самом тесте через `debug_set_level` / `debug_set_category_level`.
Проверки самого скрипта сборки входят в `./check.sh`; отдельно: `make -C tests check-build-script` (нужен Python 3).
## Code Style Guidelines ## Code Style Guidelines
@@ -614,7 +625,7 @@ DEBUG_LEVEL_DISABLED полностью отключает категорию,
- Нельзя ничего восстанавливать из репозитория не спрашивая - Нельзя ничего восстанавливать из репозитория не спрашивая
- Для отладки не printf а DEBUG_* - Для отладки не printf а DEBUG_*
- Запрещено прямое включение arpa/inet.h, sys/time.h, unistd.h — использовать ../lib/platform_compat.h + socket_compat.h - Запрещено прямое включение arpa/inet.h, sys/time.h, unistd.h — использовать ../lib/platform_compat.h + socket_compat.h
- Перед сборкой всегда make clean - Меняй режим и флаги сборки через `./build.sh`: он отслеживает конфигурацию и очищает объекты только при необходимости.
- Все лишнее что менял при отладке - строго вернуть назад в состояние до вмешательства - Все лишнее что менял при отладке - строго вернуть назад в состояние до вмешательства
## Написание кода ## Написание кода
+189 -193
View File
@@ -1,216 +1,212 @@
#!/bin/bash #!/usr/bin/env bash
# Build script for uTun (supports both Linux and Windows/MSYS2) # Инкрементальная сборка Linux, FreeBSD и 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 PLATFORM=$(uname -s)
if ! command -v gcc &> /dev/null; then MAKE_TOOL=${MAKE:-make}
echo "Error: gcc not found." if [[ "$PLATFORM" = FreeBSD ]]; then
if [ "$IS_WINDOWS" -eq 1 ]; then MAKE_TOOL=${MAKE:-gmake}
echo "Please install: pacman -S mingw-w64-ucrt-x86_64-toolchain" export CC="${CC:-clang}"
else fi
echo "Please install build-essential or gcc" MAKE_PATH=$(command -v "$MAKE_TOOL") || { echo "ERROR: GNU make not found: $MAKE_TOOL (FreeBSD: pkg install gmake)" >&2; exit 1; }
MAKE_ID=$("$MAKE_TOOL" --version) || { echo "ERROR: GNU make is required; use MAKE=gmake on BSD" >&2; exit 1; }
[[ "$MAKE_ID" = *'GNU Make'* ]] || { echo "ERROR: GNU make is required; use MAKE=gmake on BSD" >&2; exit 1; }
export MAKE="$MAKE_TOOL"
if command -v sha256sum >/dev/null; then
HASH_COMMAND=(sha256sum)
elif command -v sha256 >/dev/null; then
HASH_COMMAND=(sha256 -r)
elif command -v shasum >/dev/null; then
HASH_COMMAND=(shasum -a 256)
else
echo "ERROR: SHA-256 utility not found: sha256sum, sha256 or shasum is required" >&2; exit 1
fi
read -r -a COMPILER <<< "${CC:-gcc}"
COMPILER_PATH=$(command -v "${COMPILER[0]}") || { 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=$({ printf '%s\0' "$@"; "${HASH_COMMAND[@]}" "$@"; } | "${HASH_COMMAND[@]}")
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)
shopt -s globstar nullglob
for macro in m4/**/*.m4; do AUTOTOOLS_INPUTS+=("$macro"); done
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" "$PLATFORM" "${MSYSTEM:-}" "${MSYSTEM_PREFIX:-}" "${CC:-gcc}" "$COMPILER_PATH" "$COMPILER_ID"
printf '%s\0' "$MAKE_PATH" "$MAKE_ID" "${HASH_COMMAND[@]}"
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[@]}"
hash_files build.sh
} | "${HASH_COMMAND[@]}")
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
exit 1
fi fi
# Clean if requested REGENERATE=0
if [ "$CLEAN" -eq 1 ]; then RECONFIGURE=0
echo "Cleaning build..." if [[ "$FULL" = 1 || "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" || "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then
make clean 2>/dev/null || true 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"
if [[ "$PLATFORM" = FreeBSD ]]; then export ASAN_OPTIONS="${ASAN_OPTIONS:-detect_leaks=0}"; fi
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 fi
# Full rebuild: regenerate build system # Проверяем только зависимости объектов, перечисленных Automake; посторонние сборки не затрагиваем.
if [ "$FULL_REBUILD" -eq 1 ]; then MISSING_DEPS=0
echo "" for directory in "${BUILD_DIRS[@]}"; do
echo "======================================" while read -r keyword depfile marker; do
echo "FULL REBUILD: autoreconf" [[ "$keyword" = include && "$marker" = '# am--include-marker' && "$depfile" = *.Po ]] || continue
echo "======================================" depfile=${depfile//\$\(DEPDIR\)/.deps}
echo "" object=${depfile/.deps\//}
object=${object%.Po}
# Check for autotools [[ -f "$directory/$object.o" || -f "$directory/$object.obj" ]] || continue
if ! command -v autoreconf &> /dev/null; then dependency_line=""
echo "Error: autoreconf not found." if [[ -s "$directory/$depfile" ]]; then IFS= read -r dependency_line < "$directory/$depfile" || true; fi
if [ "$IS_WINDOWS" -eq 1 ]; then if [[ "$dependency_line" != *:* ]]; then
echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools" echo "WARN: missing or empty dependencies for $directory/$object: $directory/$depfile"
else MISSING_DEPS=1
echo "Install autotools: apt-get install autoconf automake libtool" break
fi fi
exit 1 done < "$directory/Makefile"
fi done
if [[ "$RECONFIGURE" = 1 || "$CLEAN" = 1 || "$MISSING_DEPS" = 1 ]]; then
# Clean generated files echo "INFO: cleaning objects: reconfigured=$RECONFIGURE requested=$CLEAN missing_dependencies=$MISSING_DEPS"
echo "Cleaning generated files..." "$MAKE_TOOL" clean 2>&1 | tee build.log || { echo "ERROR: clean failed; see build.log" >&2; exit 1; }
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 else
# Quick/incremental build: generate configure if needed echo "INFO: configuration unchanged; incremental build (mode=$MODE)"
if [ ! -f ./configure ]; then : > build.log
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
# Auto-detect FFmpeg (optional video transcoding). If libav libraries are echo "INFO: compiling: mode=$MODE jobs=$JOBS"
# available, build with --with-ffmpeg; configure gracefully skips it otherwise. if [[ "$MODE" = asan ]]; then
FFMPEG_OPT="" echo "INFO: AddressSanitizer: building lib, Opus and src; skipping tests"
if command -v pkg-config >/dev/null 2>&1 && \ MAKE_ARGS+=("SUBDIRS=lib lib/libopus src")
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"
fi fi
"$MAKE_TOOL" "-j$JOBS" "${MAKE_ARGS[@]}" 2>&1 | tee -a build.log || { echo "ERROR: build failed; see build.log" >&2; exit 1; }
# Generate Makefiles if needed # Состояние обновляется после успешной сборки ядра; ошибка GUI не инвалидирует готовые объекты.
if [ ! -f Makefile ]; then GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}")
echo "" CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}")
echo "Running ./configure..." state_tmp="$STATE.tmp.$$"
if [ "$USE_ASAN" -eq 1 ]; then trap 'rm -f "$state_tmp"' EXIT
./configure CFLAGS="-g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie" LDFLAGS="-no-pie" $FFMPEG_OPT printf '%s\n' "$GENERATION_INPUT" "$GENERATION_OUTPUT" "$CONFIG_REQUEST" "$CONFIG_OUTPUT" > "$state_tmp"
elif [ "$USE_HARDENED" -eq 1 ]; then mv -f "$state_tmp" "$STATE"
./configure --enable-hardening $FFMPEG_OPT
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 else
./configure $FFMPEG_OPT cmake --build tools/chatgui/build --parallel "$JOBS" || { echo "ERROR: ChatGUI build failed" >&2; exit 1; }
fi fi
fi fi
echo "Build completed successfully!"
# Build (all .o files go to build/ directory, binary copied to project root) echo "Binary: src/utun"
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
else
make $JOBS $MAKE_ARGS 2>&1 | tee build.log
if [ ${PIPESTATUS[0]} -eq 0 ]; then
BUILD_SUCCESS=1
fi
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"
fi
else
echo ""
echo "ERROR: Build failed!"
echo "Check build.log for details"
exit 1
fi
+24 -136
View File
@@ -1,144 +1,32 @@
#!/bin/sh #!/bin/sh
# Build script for uTun on FreeBSD (full rebuild) # Полная сборка FreeBSD: зависимости и общий инкрементальный скрипт.
# Usage: ./build_bsd_full.sh [options]
# Options:
# -h, --help Show this help
# --asan Build with AddressSanitizer (clang)
# --hardened Build with compiler security hardening flags (clang)
# -j[N] Build with N parallel jobs (default: sysctl hw.ncpu)
set -e set -e
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
case " ${*} " in
*" --help "*|*" -h "*)
echo "Usage: ./build_bsd_full.sh [--asan|--hardened] [-jN] [build.sh arguments]"
echo "Installs missing build tools and runs build.sh --full"
exit 0 ;;
esac
# Parse arguments need_install=0
USE_ASAN=0 for tool in bash gmake autoreconf aclocal automake libtoolize pkg-config; do
USE_HARDENED=0 command -v "$tool" >/dev/null 2>&1 || need_install=1
JOBS=""
MAKE_ARGS=""
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
echo "Usage: ./build_bsd_full.sh [options]"
echo "Options:"
echo " -h, --help Show this help"
echo " --asan Build with AddressSanitizer"
echo " --hardened Build with compiler security hardening flags"
echo " -j[N] Build with N parallel jobs (default: sysctl hw.ncpu)"
exit 0
;;
--asan)
USE_ASAN=1
shift
;;
--hardened)
USE_HARDENED=1
shift
;;
-j*)
JOBS="$1"
shift
;;
*)
MAKE_ARGS="$MAKE_ARGS $1"
shift
;;
esac
done done
if [ "$need_install" -eq 1 ]; then
if [ -z "$JOBS" ]; then echo "INFO: installing missing FreeBSD build dependencies (requires root)"
if command -v nproc >/dev/null 2>&1; then pkg install -y bash gmake autoconf automake libtool pkgconf openssl || {
JOBS="-j$(nproc)" echo "ERROR: cannot install FreeBSD build dependencies" >&2; exit 1;
else }
JOBS="-j$(sysctl -n hw.ncpu)"
fi
fi fi
# Проверка и установка зависимостей jobs_found=0
need_install="" for argument do
case "$argument" in -j*) jobs_found=1 ;; esac
for cmd in autoreconf aclocal automake libtoolize; do
if ! command -v "$cmd" >/dev/null 2>&1; then
need_install=1
break
fi
done done
if [ "$jobs_found" -eq 0 ]; then
if [ -n "$need_install" ]; then jobs=$(sysctl -n hw.ncpu) || { echo "ERROR: cannot determine CPU count" >&2; exit 1; }
echo "Устанавливаются необходимые пакеты (нужен root)..." set -- "-j$jobs" "$@"
pkg update
pkg install -y autoconf automake libtool pkgconf openssl
fi
# Предпочитаем GNU make (gmake), но fallback на bmake — он собирает
# autotools-генерённые Makefile данного проекта. gmake ставится отдельно:
# pkg install gmake (нужен root)
GMAKE="$(command -v gmake || command -v make)"
echo "Используем make: $GMAKE"
mkdir -p m4
echo "=== Полная пересборка uTun ==="
# 1. Очистка
echo "[1/4] Очистка..."
$GMAKE distclean 2>/dev/null || true
rm -f config.h config.status config.log
rm -f configure.log autoreconf.log build.log
# 2. Генерация configure
echo "[2/4] Автогенерация..."
autoreconf -fi 2>&1 | tee autoreconf.log
# Проверяем, появился ли configure
if [ ! -f configure ]; then
echo "Ошибка: autoreconf не создал configure."
exit 1
fi
chmod +x configure
# 3. Конфигурация
echo "[3/4] Конфигурация..."
# FFmpeg (опционально) — автоопределение через pkg-config
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"
fi
if [ "$USE_ASAN" -eq 1 ]; then
echo "(AddressSanitizer enabled — clang, lib/ + src/ only)"
./configure --prefix=/usr/local CC=clang \
CFLAGS="-g -O1 -fsanitize=address -fno-omit-frame-pointer" \
LDFLAGS="-fsanitize=address" $FFMPEG_OPT 2>&1 | tee configure.log
elif [ "$USE_HARDENED" -eq 1 ]; then
echo "(Hardening enabled — clang-safe flags)"
./configure --prefix=/usr/local CC=clang \
CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE" \
LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie" $FFMPEG_OPT 2>&1 | tee configure.log
else
./configure --prefix=/usr/local $FFMPEG_OPT 2>&1 | tee configure.log
fi
# 4. Сборка
echo "[4/4] Сборка..."
if [ "$USE_ASAN" -eq 1 ]; then
(cd lib && $GMAKE $JOBS $MAKE_ARGS 2>&1 | tee ../build.log)
(cd lib/libopus && $GMAKE $JOBS $MAKE_ARGS 2>&1 | tee ../../build.log)
(cd src && $GMAKE $JOBS $MAKE_ARGS 2>&1 | tee ../build.log)
else
$GMAKE $JOBS $MAKE_ARGS 2>&1 | tee build.log
fi
echo
echo "=== Готово ==="
echo "Бинарный файл: src/utun"
if [ "$USE_ASAN" -eq 1 ]; then
echo
echo "Запуск с ASAN (detect_leaks=0 обязателен на FreeBSD):"
echo " ASAN_OPTIONS=detect_leaks=0:halt_on_error=0 ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log"
fi fi
echo "INFO: running shared build script for FreeBSD"
exec bash "$SCRIPT_DIR/build.sh" --full "$@"
+15 -9
View File
@@ -1,6 +1,10 @@
#!/bin/bash #!/usr/bin/env bash
set -eo pipefail set -eo pipefail
cd "$(dirname "$0")" cd "$(dirname "$0")"
MAKE_TOOL=${MAKE:-$(command -v gmake || command -v make)}
TIMEOUT_TOOL=$(command -v timeout || command -v gtimeout) || {
echo "ERROR: timeout or gtimeout is required for the test runner" >&2; exit 1;
}
LAST=0; [ -f .check_time ] && read -r LAST < .check_time LAST=0; [ -f .check_time ] && read -r LAST < .check_time
TMO=$(( LAST + 30 )) TMO=$(( LAST + 30 ))
@@ -8,7 +12,7 @@ TMO=$(( LAST + 30 ))
START=$(date +%s) START=$(date +%s)
TMP=$(mktemp) TMP=$(mktemp)
timeout $TMO make check -j4 > "$TMP" 2>&1 || { "$TIMEOUT_TOOL" "$TMO" "$MAKE_TOOL" check -j4 > "$TMP" 2>&1 || {
tail -40 "$TMP"; rm -f "$TMP" tail -40 "$TMP"; rm -f "$TMP"
[ $(date +%s) -ge $((START + TMO)) ] && echo "TIMEOUT (${TMO}s)" >&2 [ $(date +%s) -ge $((START + TMO)) ] && echo "TIMEOUT (${TMO}s)" >&2
exit 1 exit 1
@@ -18,17 +22,19 @@ rm -f "$TMP"
# Интеграционный тест tcp_proxy_full (требует sudo; без passwordless sudo — пропускаем) # Интеграционный тест tcp_proxy_full (требует sudo; без passwordless sudo — пропускаем)
echo "" echo ""
if [ "$(id -u)" -eq 0 ]; then if [ "$(uname -s)" != Linux ]; then
make -C tests check-proxy echo "[SKIP] tcp_proxy_full integration tests require Linux networking tools"
make -C tests check-burst elif [ "$(id -u)" -eq 0 ]; then
make -C tests check-load "$MAKE_TOOL" -C tests check-proxy
"$MAKE_TOOL" -C tests check-burst
"$MAKE_TOOL" -C tests check-load
elif command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then elif command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
echo "=== tcp_proxy_full integration test (sudo) ===" echo "=== tcp_proxy_full integration test (sudo) ==="
sudo -n make -C tests check-proxy sudo -n "$MAKE_TOOL" -C tests check-proxy
echo "=== tcp_proxy_full burst test (sudo) ===" echo "=== tcp_proxy_full burst test (sudo) ==="
sudo -n make -C tests check-burst sudo -n "$MAKE_TOOL" -C tests check-burst
echo "=== tcp_proxy_full load test (sudo) ===" echo "=== tcp_proxy_full load test (sudo) ==="
sudo -n make -C tests check-load sudo -n "$MAKE_TOOL" -C tests check-load
else else
echo "[SKIP] tcp_proxy_full integration test (requires passwordless sudo)" echo "[SKIP] tcp_proxy_full integration test (requires passwordless sudo)"
fi fi
+16 -3
View File
@@ -62,11 +62,24 @@ AC_ARG_ENABLE([hardening],
[enable_hardening=no]) [enable_hardening=no])
if test "x$enable_hardening" = "xyes"; then if test "x$enable_hardening" = "xyes"; then
HARDEN_CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -Wformat=2 -Wformat-overflow=2 -fPIE" HARDEN_CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -Wformat=2"
HARDEN_LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie" compiler_version=$($CC --version 2>&1) || AC_MSG_ERROR([Cannot identify compiler for hardening])
case "$compiler_version" in
*clang*) ;;
*) HARDEN_CFLAGS="$HARDEN_CFLAGS -fstack-clash-protection -Wformat-overflow=2" ;;
esac
case "$host" in
*mingw* | *msys* | *cygwin*)
HARDEN_LDFLAGS="-Wl,--dynamicbase,--nxcompat,--high-entropy-va"
;;
*)
HARDEN_CFLAGS="$HARDEN_CFLAGS -fPIE"
HARDEN_LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie"
;;
esac
CFLAGS="$CFLAGS $HARDEN_CFLAGS" CFLAGS="$CFLAGS $HARDEN_CFLAGS"
LDFLAGS="$LDFLAGS $HARDEN_LDFLAGS" LDFLAGS="$LDFLAGS $HARDEN_LDFLAGS"
AC_MSG_NOTICE([Hardening flags enabled: $HARDEN_CFLAGS]) AC_MSG_NOTICE([Hardening flags enabled: CFLAGS=$HARDEN_CFLAGS LDFLAGS=$HARDEN_LDFLAGS])
fi fi
# ==================== НАДЁЖНЫЙ ДЕТЕКТ WINDOWS ==================== # ==================== НАДЁЖНЫЙ ДЕТЕКТ WINDOWS ====================
+18 -8
View File
@@ -1,6 +1,7 @@
# 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 test_net.h run_tests.py test_run_tests.py EXTRA_DIST = fixtures/reality_openssl_hello.hex data/silero_speech.pcm data/silero_speech.txt test_net.h \
run_tests.py test_run_tests.py test_build_script.py
# Порядок по замерам Linux: быстрые проверки раньше длительных; состав зависит от прежних configure-условий. # Порядок по замерам Linux: быстрые проверки раньше длительных; состав зависит от прежних configure-условий.
@@ -715,23 +716,25 @@ 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
# По умолчанию jobs = доступные CPU; TEST_JOBS=1 сохраняет последовательный запуск. # По умолчанию jobs = доступные CPU; TEST_JOBS=1 сохраняет последовательный запуск.
check-local: check-runner $(check_PROGRAMS) check-local: check-runner check-build-script $(check_PROGRAMS)
@python3 "$(srcdir)/run_tests.py" --log-dir "$(TEST_LOG_DIR)" $(check_PROGRAMS) @python3 "$(srcdir)/run_tests.py" --log-dir "$(TEST_LOG_DIR)" $(check_PROGRAMS)
.PHONY: check-runner .PHONY: check-runner
@@ -739,6 +742,13 @@ check-runner:
@python3 "$(srcdir)/test_run_tests.py" @python3 "$(srcdir)/test_run_tests.py"
@echo "[INFO] test runner regression checks passed" @echo "[INFO] test runner regression checks passed"
.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
# Интеграционный тест tcp_proxy_full (требует sudo, TUN, iptables). Запускается из check.sh. # Интеграционный тест tcp_proxy_full (требует sudo, TUN, iptables). Запускается из check.sh.
.PHONY: check-proxy .PHONY: check-proxy
check-proxy: check-proxy:
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""Проверка настоящего build.sh на маленьком проекте Autotools, без изменения исходников uTun."""
from pathlib import Path
import os
import shlex
import shutil
import subprocess
import sys
import tempfile
import time
script = Path(sys.argv[1]).resolve()
base = Path(tempfile.mkdtemp(prefix='utun-build-tests-'))
log = base / 'commands.log'
environment = {**os.environ, 'ASAN_OPTIONS': 'detect_leaks=0'}
environment.pop('MAKEFLAGS', None)
environment.pop('MSYSTEM', None)
for name in ['CC', 'CFLAGS', 'CPPFLAGS', 'LDFLAGS', 'LIBS', 'DEBUG_FLAGS']:
environment.pop(name, None)
checks = []
default_compiler = 'clang' if sys.platform.startswith('freebsd') else 'gcc'
make_tool = environment.get('MAKE') or ('gmake' if sys.platform.startswith('freebsd') else 'make')
def write(name, content):
path = base / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
return path
def run(args, ok=True, extra_environment=None):
if args[0] == 'make':
args = [make_tool, *args[1:]]
result = subprocess.run(args, cwd=base, env={**environment, **(extra_environment or {})}, text=True, capture_output=True)
with log.open('a') as output:
output.write(f'{args!r}\nexit={result.returncode}\n{result.stdout}{result.stderr}\n')
if ok and result.returncode:
raise AssertionError(f'{args!r}: exit={result.returncode}; see {log}\n{result.stderr[-1500:]}')
return result
def build(*args, **kwargs):
return run(['bash', 'build.sh', '--no-gui', '-j', '2', *args], **kwargs)
def check(name, condition):
if not condition:
raise AssertionError(f'{name}; see {log}')
checks.append(name)
print(f'[PASS] build.sh: {name}', flush=True)
def times():
return {str(p.relative_to(base)): p.stat().st_mtime_ns for p in base.rglob('*.o')}
def value():
binary = base / 'src/utun'
if not binary.exists():
binary = binary.with_suffix('.exe')
return run([str(binary)]).stdout.strip()
try:
shutil.copyfile(script, base / 'build.sh')
actual_configuration = (script.parent / 'configure.ac').read_text()
hardening_start = actual_configuration.index('if test "x$enable_hardening" = "xyes"; then')
hardening_end = actual_configuration.index('# ==================== НАДЁЖНЫЙ ДЕТЕКТ WINDOWS', hardening_start)
hardening_rules = actual_configuration[hardening_start:hardening_end]
write('configure.ac', '''AC_INIT([build-test], [1.0])
AC_CONFIG_SRCDIR([src/main.c])
AC_CONFIG_HEADERS([config.h])
AC_CONFIG_MACRO_DIR([m4])
AC_CANONICAL_HOST
AM_INIT_AUTOMAKE([foreign subdir-objects])
AC_PROG_CC
AC_PROG_RANLIB
AM_PROG_AR
AC_ARG_WITH([ffmpeg], [], [], [with_ffmpeg=no])
AC_ARG_ENABLE([hardening], [], [], [enable_hardening=no])
__HARDENING_RULES__
TEST_CONFIGURE_OPTION
AC_CONFIG_FILES([Makefile lib/Makefile lib/libopus/Makefile src/Makefile tests/Makefile])
AC_OUTPUT
'''.replace('__HARDENING_RULES__', hardening_rules))
write('m4/test.m4', 'AC_DEFUN([TEST_CONFIGURE_OPTION], [AC_DEFINE([TEST_OPTION], [1], [Test option])])\n')
write('Makefile.am', 'SUBDIRS = lib lib/libopus src tests\n')
write('lib/Makefile.am', 'noinst_LIBRARIES = libfixture.a\nlibfixture_a_SOURCES = fixture.c\n')
write('lib/fixture.c', 'int fixture(void) { return 0; }\n')
write('lib/libopus/Makefile.am', '''noinst_LIBRARIES = libopus_internal.a
libopus_internal_a_SOURCES = opus.c
CFLAGS = -g -Og -fno-omit-frame-pointer -fno-pie -fPIC
''')
write('lib/libopus/opus.c', 'int opus_fixture(void) { return 0; }\n')
write('src/Makefile.am', '''bin_PROGRAMS = utun
utun_SOURCES = main.c worker.c api.h nested.h
AM_CFLAGS = -DLOCAL_OPTION=1
utun_LDADD = ../lib/libfixture.a ../lib/libopus/libopus_internal.a
''')
write('src/main.c', '''#include <stdio.h>
#include "api.h"
int worker(void);
int fixture(void);
int opus_fixture(void);
int main(void) { printf("%d\\n", worker() + fixture() + opus_fixture()); return 0; }
''')
write('src/worker.c', '#include "api.h"\nint worker(void) { return VALUE + LOCAL_OPTION; }\n')
write('src/api.h', '#include "nested.h"\n')
write('src/nested.h', '#define VALUE 10\n')
test_rules = (script.parent / 'tests/Makefile.am').read_text()
config_rules = test_rules[test_rules.index('all-local: copy-test-configs'):test_rules.index('# Test logs directory')]
write('tests/Makefile.am', 'noinst_PROGRAMS = probe\nprobe_SOURCES = probe.c\n' + config_rules)
write('tests/probe.c', 'int main(void) { return 0; }\n')
package_tool = write('bin/pkg-config', '''#!/usr/bin/env bash
[[ "${BUILD_TEST_FFMPEG:-0}" = 1 ]] || exit 1
case "$1" in
--exists) exit 0 ;;
--modversion) echo 1.0 ;;
--cflags|--libs-only-L) exit 0 ;;
*) exit 1 ;;
esac
''')
package_tool.chmod(0o755)
environment['PATH'] = str(base / 'bin') + os.pathsep + environment['PATH']
result = build()
check('fresh build generates, configures and builds dependencies', value() == '11' and (base / 'lib/libopus/libopus_internal.a').exists())
check('normal build includes tests', (base / 'tests/probe').exists() or (base / 'tests/probe.exe').exists())
initial = times()
result = build()
check('repeat build does not regenerate, configure or compile', initial == times() and 'INFO: configuring:' not in result.stdout and 'INFO: regenerating' not in result.stdout)
time.sleep(1.1)
write('src/nested.h', '#define VALUE 20\n')
result = build()
current = times()
check('transitive header rebuilds both dependent objects', current['src/main.o'] > initial['src/main.o'] and current['src/worker.o'] > initial['src/worker.o'])
check('header change preserves unrelated objects and configuration', current['lib/fixture.o'] == initial['lib/fixture.o'] and 'INFO: configuring:' not in result.stdout and value() == '21')
initial = times()
write('src/unused.h', '#define UNUSED 1\n')
build()
check('unrelated header does not recompile', initial == times())
write('asan_probe.c', 'int main(void) { return 0; }\n')
asan_supported = run([default_compiler, '-fsanitize=address', '-fno-pie', '-no-pie', 'asan_probe.c', '-o', 'asan_probe'], ok=False).returncode == 0
modes = ['asan', 'hardened', 'normal'] if asan_supported else ['hardened', 'normal']
if not asan_supported:
print('[SKIP] build.sh: ASAN runtime is unavailable in this compiler toolchain', flush=True)
for mode in modes:
args = [] if mode == 'normal' else ['--' + mode]
result = build(*args)
symbols = run(['nm', '-u', 'src/worker.o']).stdout
check(f'{mode}: changing mode reconfigures and cleans', 'INFO: configuring:' in result.stdout and 'INFO: cleaning objects:' in result.stdout)
check(f'{mode}: object instrumentation matches mode', ('__asan_' in symbols) == (mode == 'asan'))
if mode == 'asan':
check('ASAN skips tests and preserves unsanitized Opus', not (base / 'tests/probe').exists() and not (base / 'tests/probe.exe').exists() and '__asan_' not in run(['nm', '-u', 'lib/libopus/opus.o']).stdout)
initial = times()
result = build(*args)
check(f'{mode}: repeat build is incremental', initial == times() and 'INFO: configuring:' not in result.stdout and 'INFO: regenerating' not in result.stdout)
result = build('CFLAGS=-g -O0 -DUSER_OPTION=1')
check('quoted compiler flags are passed intact through configure', 'CFLAGS = -g -O0 -DUSER_OPTION=1' in (base / 'src/Makefile').read_text())
initial = times()
build('CFLAGS=-g -O0 -DUSER_OPTION=1')
check('repeat build with quoted flags is incremental', initial == times())
build()
compiler = shutil.which(default_compiler)
for directory in ['compiler-first', 'compiler-second']:
wrapper = write(directory + '/' + default_compiler, '#!/usr/bin/env bash\nexec ' + shlex.quote(compiler) + ' "$@"\n')
wrapper.chmod(0o755)
build(extra_environment={'PATH': str(base / 'compiler-first') + os.pathsep + environment['PATH']})
result = build(extra_environment={'PATH': str(base / 'compiler-second') + os.pathsep + environment['PATH']})
check('compiler path change is detected even with identical version', 'INFO: configuring:' in result.stdout)
build()
time.sleep(1.1)
write('src/Makefile.am', (base / 'src/Makefile.am').read_text().replace('LOCAL_OPTION=1', 'LOCAL_OPTION=2'))
result = build()
check('Makefile.am flag change rebuilds stale objects', 'INFO: regenerating' in result.stdout and value() == '22')
macro = base / 'm4/test.m4'
saved_time = macro.stat()
write('m4/test.m4', macro.read_text().replace('[1]', '[2]'))
os.utime(macro, ns=(saved_time.st_atime_ns, saved_time.st_mtime_ns))
build()
check('macro content change is detected with unchanged timestamp', '#define TEST_OPTION 2' in (base / 'config.h').read_text())
result = build(extra_environment={'BUILD_TEST_FFMPEG': '1'})
check('newly available optional dependency reconfigures', 'INFO: configuring:' in result.stdout and '--with-ffmpeg' in run(['./config.status', '--config']).stdout)
result = build()
check('disappearing optional dependency reconfigures', 'INFO: configuring:' in result.stdout and '--without-ffmpeg' in run(['./config.status', '--config']).stdout)
(base / 'src/.deps/worker.Po').unlink()
result = build()
check('missing dependencies force rebuild without configure', 'missing_dependencies=1' in result.stdout and 'INFO: configuring:' not in result.stdout)
write('src/.deps/worker.Po', '# dummy\n')
result = build()
check('dummy dependencies force rebuild', 'missing_dependencies=1' in result.stdout)
write('tests/test_preserve.conf', 'preserve configuration\n')
write('tests/test_preserve.json', '{"preserve":true}\n')
result = build('--clean')
check('explicit clean does not reconfigure', 'INFO: cleaning objects:' in result.stdout and 'INFO: configuring:' not in result.stdout)
check('in-source clean preserves configuration files', (base / 'tests/test_preserve.conf').exists() and (base / 'tests/test_preserve.json').exists())
result = build('--full')
check('explicit full regenerates, configures and rebuilds', 'INFO: regenerating' in result.stdout and 'INFO: configuring:' in result.stdout)
(base / 'src/Makefile').unlink()
result = build()
check('missing child Makefile is recreated', (base / 'src/Makefile').exists() and 'INFO: configuring:' in result.stdout)
run(['./configure', 'CFLAGS=-O0'])
result = build()
check('external configure is detected', 'INFO: configuring:' in result.stdout and 'CFLAGS = -g -O2' in (base / 'src/Makefile').read_text())
check('conflicting modes fail', build('--asan', '--hardened', ok=False).returncode != 0)
check('invalid job count fails', build('-j0', ok=False).returncode != 0)
state = (base / '.utun-build-state').read_bytes()
original = (base / 'src/worker.c').read_text()
write('src/worker.c', original + '\n#error deliberate_failure\n')
check('compiler failure propagates without committing state', build(ok=False).returncode != 0 and (base / '.utun-build-state').read_bytes() == state)
write('src/worker.c', original)
check('build recovers from compiler failure', build().returncode == 0 and value() == '22')
original = (base / 'configure.ac').read_text()
write('configure.ac', original + '\nm4_fatal([deliberate_autoconf_failure])\n')
check('autoreconf failure propagates through tee', build(ok=False).returncode != 0 and (base / '.utun-build-state').read_bytes() == state)
write('configure.ac', original)
build()
write('tools/chatgui/build/CMakeCache.txt', 'placeholder\n')
cmake = write('bin/cmake', '#!/usr/bin/env bash\nprintf "%s\\n" "$*" > gui-arguments\nexit 9\n')
cmake.chmod(0o755)
result = run(['bash', 'build.sh', '-j', '3'], ok=False)
check('GUI failure propagates and success is not printed', result.returncode != 0 and 'Build completed successfully!' not in result.stdout)
check('GUI receives selected job count', (base / 'gui-arguments').read_text().strip() == '--build tools/chatgui/build --parallel 3')
run(['make', 'distclean'])
output_dir = base / 'out'
output_dir.mkdir()
for args in [['../configure'], [make_tool, '-j2'], [make_tool, '-C', 'tests', 'clean-local']]:
result = subprocess.run(args, cwd=output_dir, env=environment, capture_output=True, text=True)
with log.open('a') as output:
output.write(f'OUT-OF-TREE {args!r}\nexit={result.returncode}\n{result.stdout}{result.stderr}\n')
if result.returncode:
raise AssertionError(f'out-of-tree {args!r}: see {log}')
if args[0] == make_tool and '-j2' in args:
check('out-of-tree build copies test configurations', (output_dir / 'tests/test_preserve.conf').exists())
check('out-of-tree clean removes copies and preserves source configurations',
not (output_dir / 'tests/test_preserve.conf').exists() and (base / 'tests/test_preserve.conf').exists())
print(f'[PASS] build.sh: {len(checks)} checks completed', flush=True)
shutil.rmtree(base)
except Exception:
print(f'[FAIL] build.sh: diagnostic files retained in {base}', file=sys.stderr)
raise
+13 -2
View File
@@ -14,6 +14,18 @@ log ""
SRC_DIR="$(cd "$(dirname "$0")" && pwd)" SRC_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="$SRC_DIR/build" BUILD_DIR="$SRC_DIR/build"
RUNTIME_PREFIX="${MSYSTEM_PREFIX:-/ucrt64}" RUNTIME_PREFIX="${MSYSTEM_PREFIX:-/ucrt64}"
JOBS=$(nproc 2>/dev/null || echo 4)
while [[ $# -gt 0 ]]; do
case "$1" in
-j)
[[ $# -gt 1 ]] || { log "ERROR: -j requires a positive job count"; exit 1; }
shift; JOBS=$1 ;;
-j*) JOBS=${1#-j} ;;
*) log "ERROR: unknown argument: $1"; exit 1 ;;
esac
shift
done
[[ "$JOBS" =~ ^[1-9][0-9]*$ ]] || { log "ERROR: invalid job count: $JOBS"; exit 1; }
log "Source: $SRC_DIR" log "Source: $SRC_DIR"
log "Build: $BUILD_DIR" log "Build: $BUILD_DIR"
@@ -36,7 +48,7 @@ if [ "${MSYSTEM:-}" = UCRT64 ]; then
-DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON 2>&1 | tee -a "$LOG_FILE" || { -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON 2>&1 | tee -a "$LOG_FILE" || {
log " ERROR: bundled rlottie configure failed"; exit 1; log " ERROR: bundled rlottie configure failed"; exit 1;
} }
cmake --build "$SRC_DIR/rlottie_build" --target rlottie --parallel 4 2>&1 | tee -a "$LOG_FILE" || { cmake --build "$SRC_DIR/rlottie_build" --target rlottie --parallel "$JOBS" 2>&1 | tee -a "$LOG_FILE" || {
log " ERROR: bundled rlottie build failed"; exit 1; log " ERROR: bundled rlottie build failed"; exit 1;
} }
log " OK: bundled rlottie ready in $SRC_DIR/rlottie_build" log " OK: bundled rlottie ready in $SRC_DIR/rlottie_build"
@@ -47,7 +59,6 @@ cmake -S "$SRC_DIR" -B "$BUILD_DIR" -G Ninja 2>&1 | tee -a "$LOG_FILE"
test ${PIPESTATUS[0]} -eq 0 || { log "CMake configure FAILED"; exit 1; } test ${PIPESTATUS[0]} -eq 0 || { log "CMake configure FAILED"; exit 1; }
log "" log ""
JOBS=$(nproc 2>/dev/null || echo 4)
log "[2/4] Building (parallel: $JOBS jobs)..." log "[2/4] Building (parallel: $JOBS jobs)..."
cmake --build "$BUILD_DIR" -j"$JOBS" 2>&1 | tee -a "$LOG_FILE" cmake --build "$BUILD_DIR" -j"$JOBS" 2>&1 | tee -a "$LOG_FILE"
test ${PIPESTATUS[0]} -eq 0 || { log "Build FAILED"; exit 1; } test ${PIPESTATUS[0]} -eq 0 || { log "Build FAILED"; exit 1; }
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# ============================================================================ # ============================================================================