Browse Source

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	tests/Makefile.am
master
evgeny 1 week ago
parent
commit
69a99c7860
  1. 2
      .gitignore
  2. 15
      AGENTS.md
  3. 382
      build.sh
  4. 162
      build_bsd_full.sh
  5. 24
      check.sh
  6. 19
      configure.ac
  7. 26
      tests/Makefile.am
  8. 238
      tests/test_build_script.py
  9. 15
      tools/chatgui/build.sh
  10. 2
      tools/chatgui/build_nix.sh

2
.gitignore vendored

@ -32,6 +32,7 @@ index.html
*.exe
# Autotools
/.utun-build-state*
Makefile
!tools/etcpmon/Makefile
Makefile.in
@ -84,6 +85,7 @@ logs/
tests/test_*
!tests/test_*.c
!tests/test_*.h
!tests/test_*.py
!tests/test_*.conf
!tests/test_*.json
tests/bench_*

15
AGENTS.md

@ -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
```
### 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
```bash
./build.sh -j4 # make с авто-конфигурацией если надо
./build.sh -j4 # инкрементальная сборка, конфигурация и очистка при необходимости
./build.sh --no-gui -j4 # только ядро и тесты, без отдельной сборки GUI
```
```powershell
powershell -Command ".\build.bat" 2>&1 # Windows, логи: build_win.log
@ -127,6 +137,7 @@ cd tests && ./test_services
```
Использовать цель из `tests/Makefile.am`: ручная компиляция `src/*.c` пропускает подкаталоги и добавляет лишний `main`.
Уровни и категории диагностики настраиваются в самом тесте через `debug_set_level` / `debug_set_category_level`.
Проверки самого скрипта сборки входят в `./check.sh`; отдельно: `make -C tests check-build-script` (нужен Python 3).
## Code Style Guidelines
@ -614,7 +625,7 @@ DEBUG_LEVEL_DISABLED полностью отключает категорию,
- Нельзя ничего восстанавливать из репозитория не спрашивая
- Для отладки не printf а DEBUG_*
- Запрещено прямое включение arpa/inet.h, sys/time.h, unistd.h — использовать ../lib/platform_compat.h + socket_compat.h
- Перед сборкой всегда make clean
- Меняй режим и флаги сборки через `./build.sh`: он отслеживает конфигурацию и очищает объекты только при необходимости.
- Все лишнее что менял при отладке - строго вернуть назад в состояние до вмешательства
## Написание кода

382
build.sh

@ -1,216 +1,212 @@
#!/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)
#!/usr/bin/env bash
# Инкрементальная сборка Linux, FreeBSD и 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
fi
# Clean if requested
if [ "$CLEAN" -eq 1 ]; then
echo "Cleaning build..."
make clean 2>/dev/null || true
PLATFORM=$(uname -s)
MAKE_TOOL=${MAKE:-make}
if [[ "$PLATFORM" = FreeBSD ]]; then
MAKE_TOOL=${MAKE:-gmake}
export CC="${CC:-clang}"
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 ""
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
# 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
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
# 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"
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
# 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_TOOL" 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_TOOL" "-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"

162
build_bsd_full.sh

@ -1,144 +1,32 @@
#!/bin/sh
# Build script for uTun on FreeBSD (full rebuild)
# 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)
# Полная сборка FreeBSD: зависимости и общий инкрементальный скрипт.
set -e
# Parse arguments
USE_ASAN=0
USE_HARDENED=0
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
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
need_install=0
for tool in bash gmake autoreconf aclocal automake libtoolize pkg-config; do
command -v "$tool" >/dev/null 2>&1 || need_install=1
done
if [ -z "$JOBS" ]; then
if command -v nproc >/dev/null 2>&1; then
JOBS="-j$(nproc)"
else
JOBS="-j$(sysctl -n hw.ncpu)"
fi
if [ "$need_install" -eq 1 ]; then
echo "INFO: installing missing FreeBSD build dependencies (requires root)"
pkg install -y bash gmake autoconf automake libtool pkgconf openssl || {
echo "ERROR: cannot install FreeBSD build dependencies" >&2; exit 1;
}
fi
# Проверка и установка зависимостей
need_install=""
for cmd in autoreconf aclocal automake libtoolize; do
if ! command -v "$cmd" >/dev/null 2>&1; then
need_install=1
break
fi
jobs_found=0
for argument do
case "$argument" in -j*) jobs_found=1 ;; esac
done
if [ -n "$need_install" ]; then
echo "Устанавливаются необходимые пакеты (нужен root)..."
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"
if [ "$jobs_found" -eq 0 ]; then
jobs=$(sysctl -n hw.ncpu) || { echo "ERROR: cannot determine CPU count" >&2; exit 1; }
set -- "-j$jobs" "$@"
fi
echo "INFO: running shared build script for FreeBSD"
exec bash "$SCRIPT_DIR/build.sh" --full "$@"

24
check.sh

@ -1,6 +1,10 @@
#!/bin/bash
#!/usr/bin/env bash
set -eo pipefail
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
TMO=$(( LAST + 30 ))
@ -8,7 +12,7 @@ TMO=$(( LAST + 30 ))
START=$(date +%s)
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"
[ $(date +%s) -ge $((START + TMO)) ] && echo "TIMEOUT (${TMO}s)" >&2
exit 1
@ -18,17 +22,19 @@ rm -f "$TMP"
# Интеграционный тест tcp_proxy_full (требует sudo; без passwordless sudo — пропускаем)
echo ""
if [ "$(id -u)" -eq 0 ]; then
make -C tests check-proxy
make -C tests check-burst
make -C tests check-load
if [ "$(uname -s)" != Linux ]; then
echo "[SKIP] tcp_proxy_full integration tests require Linux networking tools"
elif [ "$(id -u)" -eq 0 ]; then
"$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
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) ==="
sudo -n make -C tests check-burst
sudo -n "$MAKE_TOOL" -C tests check-burst
echo "=== tcp_proxy_full load test (sudo) ==="
sudo -n make -C tests check-load
sudo -n "$MAKE_TOOL" -C tests check-load
else
echo "[SKIP] tcp_proxy_full integration test (requires passwordless sudo)"
fi

19
configure.ac

@ -62,11 +62,24 @@ AC_ARG_ENABLE([hardening],
[enable_hardening=no])
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_LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie"
HARDEN_CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -Wformat=2"
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"
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
# ==================== НАДЁЖНЫЙ ДЕТЕКТ WINDOWS ====================

26
tests/Makefile.am

@ -1,6 +1,7 @@
# 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-условий.
@ -715,23 +716,25 @@ 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
# По умолчанию 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)
.PHONY: check-runner
@ -739,6 +742,13 @@ check-runner:
@python3 "$(srcdir)/test_run_tests.py"
@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.
.PHONY: check-proxy
check-proxy:

238
tests/test_build_script.py

@ -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

15
tools/chatgui/build.sh

@ -14,6 +14,18 @@ log ""
SRC_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="$SRC_DIR/build"
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 "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" || {
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 " 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; }
log ""
JOBS=$(nproc 2>/dev/null || echo 4)
log "[2/4] Building (parallel: $JOBS jobs)..."
cmake --build "$BUILD_DIR" -j"$JOBS" 2>&1 | tee -a "$LOG_FILE"
test ${PIPESTATUS[0]} -eq 0 || { log "Build FAILED"; exit 1; }

2
tools/chatgui/build_nix.sh

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
set -euo pipefail
# ============================================================================

Loading…
Cancel
Save