From ff9d205d5bce7e1f02b2e8ab3c57e394616726d9 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 10 Oct 2026 14:12:36 +0200 Subject: [PATCH 1/7] Track build configuration and preserve incremental header dependencies --- build.sh | 363 ++++++++++++++++++++++------------------------ tests/Makefile.am | 24 ++- 2 files changed, 187 insertions(+), 200 deletions(-) diff --git a/build.sh b/build.sh index 516f3b3e..79e36a05 100755 --- a/build.sh +++ b/build.sh @@ -1,216 +1,195 @@ #!/bin/bash -# Build script for uTun (supports both Linux and Windows/MSYS2) -# Usage: ./build.sh [options] -# Options: -# -h, --help Show this help -# --clean Clean before build -# --full Full rebuild (autoreconf + configure + make) -# --asan Build with AddressSanitizer (debug) -# --hardened Build with compiler security hardening flags -# -j[N] Build with N parallel jobs (default: 4) +# Инкрементальная сборка Linux и Windows/MSYS2; флаги задаются через configure. +set -euo pipefail +cd "$(dirname "$0")" -set -e # Exit on error - -# Detect platform -if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ -n "$MSYSTEM" ]]; then - IS_WINDOWS=1 - echo "Detected Windows/MSYS2 environment" -else - IS_WINDOWS=0 - echo "Detected Linux environment" -fi - -# Parse arguments +MODE=normal CLEAN=0 -FULL_REBUILD=0 -USE_ASAN=0 -USE_HARDENED=0 -JOBS="-j4" -MAKE_ARGS="" -HARDEN_FLAGS="" +FULL=0 +BUILD_GUI=1 +JOBS=4 +MAKE_ARGS=() +MAKE_SETTINGS=() +STATE=.utun-build-state while [[ $# -gt 0 ]]; do - case $1 in + case "$1" in -h|--help) - echo "Usage: ./build.sh [options]" - echo "Options:" - echo " -h, --help Show this help" - echo " --clean Clean before build" - echo " --full Full rebuild (autoreconf + configure + make)" - echo " --asan Build with AddressSanitizer" - echo " --hardened Build with compiler security hardening flags" - echo " -j[N] Build with N parallel jobs (default: 4)" - exit 0 - ;; - --clean) - CLEAN=1 - shift - ;; - --full) - FULL_REBUILD=1 - shift - ;; - --asan) - USE_ASAN=1 - shift - ;; - --hardened) - USE_HARDENED=1 - shift - ;; - -j*) - JOBS="$1" - shift - ;; + echo "Usage: ./build.sh [--clean] [--full] [--asan|--hardened] [--no-gui] [-jN] [make arguments]" + echo " --clean Rebuild objects without forcing configure" + echo " --full Regenerate, configure and rebuild everything" + echo " --asan AddressSanitizer; skip tests" + echo " --hardened Enable compiler security hardening" + echo " --no-gui Skip the separately configured ChatGUI" + echo " -jN, -j N Parallel jobs (default: 4)" + echo " CC/CFLAGS/CPPFLAGS/LDFLAGS/LIBS assignments are passed to configure" + exit 0 ;; + --clean) CLEAN=1 ;; + --full) FULL=1 ;; + --no-gui) BUILD_GUI=0 ;; + --asan|--hardened) + selected_mode=${1#--} + if [[ "$MODE" != normal && "$MODE" != "$selected_mode" ]]; then + echo "ERROR: --asan and --hardened cannot be combined" >&2; exit 1 + fi + MODE=$selected_mode ;; + -j) + [[ $# -gt 1 ]] || { echo "ERROR: -j requires a positive job count" >&2; exit 1; } + shift; JOBS=$1 ;; + -j*) JOBS=${1#-j} ;; + CC=*|CFLAGS=*|CPPFLAGS=*|LDFLAGS=*|LIBS=*) export "$1" ;; + CC+=*|CFLAGS+=*|CPPFLAGS+=*|LDFLAGS+=*|LIBS+=*) + echo "ERROR: use NAME=value for configure flags; append assignments are not supported" >&2; exit 1 ;; *) - MAKE_ARGS="$MAKE_ARGS $1" - shift - ;; + MAKE_ARGS+=("$1") + [[ "$1" != *=* ]] || MAKE_SETTINGS+=("$1") ;; esac + shift done +[[ "$JOBS" =~ ^[1-9][0-9]*$ ]] || { echo "ERROR: invalid job count: $JOBS" >&2; exit 1; } -# Check required tools -if ! command -v gcc &> /dev/null; then - echo "Error: gcc not found." - if [ "$IS_WINDOWS" -eq 1 ]; then - echo "Please install: pacman -S mingw-w64-ucrt-x86_64-toolchain" - else - echo "Please install build-essential or gcc" - fi - exit 1 +for tool in make sha256sum; do + command -v "$tool" >/dev/null || { echo "ERROR: required tool not found: $tool" >&2; exit 1; } +done +read -r -a COMPILER <<< "${CC:-gcc}" +command -v "${COMPILER[0]}" >/dev/null || { echo "ERROR: compiler not found: ${CC:-gcc}" >&2; exit 1; } +COMPILER_ID=$("${COMPILER[@]}" --version) || { echo "ERROR: cannot identify compiler: ${CC:-gcc}" >&2; exit 1; } + +# Хеши учитывают содержимое и имена файлов, включая добавление/удаление макросов. +hash_files() { + local result + result=$(sha256sum "$@" | sha256sum) + printf '%s\n' "${result%% *}" +} + +BUILD_DIRS=(lib lib/libopus src tests) +AUTOTOOLS_INPUTS=(configure.ac Makefile.am lib/Makefile.am lib/libopus/Makefile.am src/Makefile.am tests/Makefile.am) +AUTOTOOLS_OUTPUTS=(configure aclocal.m4 config.h.in Makefile.in lib/Makefile.in lib/libopus/Makefile.in src/Makefile.in tests/Makefile.in) +CONFIG_OUTPUTS=(config.status config.h Makefile lib/Makefile lib/libopus/Makefile src/Makefile tests/Makefile) +if [[ -d m4 ]]; then + while IFS= read -r -d '' macro; do AUTOTOOLS_INPUTS+=("$macro"); done < <(find m4 -type f -name '*.m4' -print0 | sort -z) fi - -# Clean if requested -if [ "$CLEAN" -eq 1 ]; then - echo "Cleaning build..." - make clean 2>/dev/null || true +GENERATION_INPUT=$(hash_files "${AUTOTOOLS_INPUTS[@]}") +GENERATION_OUTPUT=missing +CONFIG_OUTPUT=missing +missing_generation=0 +missing_configuration=0 +for file in "${AUTOTOOLS_OUTPUTS[@]}"; do [[ -f "$file" ]] || missing_generation=1; done +for file in "${CONFIG_OUTPUTS[@]}"; do [[ -f "$file" ]] || missing_configuration=1; done +if [[ "$missing_generation" = 0 ]]; then GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}"); fi +if [[ "$missing_configuration" = 0 ]]; then CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}"); fi + +FFMPEG_PACKAGES=(libavformat libavcodec libavutil libswscale libswresample) +FFMPEG_OPTION=--without-ffmpeg +FFMPEG_ID=unavailable +FFMPEG_CFLAGS="" +FFMPEG_LDFLAGS="" +if command -v pkg-config >/dev/null && pkg-config --exists "${FFMPEG_PACKAGES[@]}"; then + FFMPEG_OPTION=--with-ffmpeg + FFMPEG_ID=$(pkg-config --modversion "${FFMPEG_PACKAGES[@]}") + FFMPEG_CFLAGS=$(pkg-config --cflags "${FFMPEG_PACKAGES[@]}") + FFMPEG_LDFLAGS=$(pkg-config --libs-only-L "${FFMPEG_PACKAGES[@]}") fi - -# Full rebuild: regenerate build system -if [ "$FULL_REBUILD" -eq 1 ]; then - echo "" - echo "======================================" - echo "FULL REBUILD: autoreconf" - echo "======================================" - echo "" - - # Check for autotools - if ! command -v autoreconf &> /dev/null; then - echo "Error: autoreconf not found." - if [ "$IS_WINDOWS" -eq 1 ]; then - echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools" - else - echo "Install autotools: apt-get install autoconf automake libtool" - fi - exit 1 - fi - - # Clean generated files - echo "Cleaning generated files..." - make distclean 2>/dev/null || true - rm -rf build/ - - # Regenerate build system (root Makefile is hand-written, not generated) - echo "" - echo "Running autoreconf -fi..." - autoreconf -fi 2>&1 | tee autoreconf.log - - echo "" - echo "======================================" - echo "Build system regenerated successfully" - echo "======================================" - echo "" -else - # Quick/incremental build: generate configure if needed - if [ ! -f ./configure ]; then - echo "Configure script not found. Generating..." - if ! command -v autoreconf &> /dev/null; then - echo "Error: autoreconf not found." - if [ "$IS_WINDOWS" -eq 1 ]; then - echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools" - else - echo "Install autotools: apt-get install autoconf automake libtool" - fi - exit 1 - fi - autoreconf -fiv +request_sum=$({ + printf '%s\0' "$MODE" "${OSTYPE:-}" "${MSYSTEM:-}" "${MSYSTEM_PREFIX:-}" "${CC:-gcc}" "$COMPILER_ID" + printf '%s\0' "${CFLAGS+x}" "${CFLAGS-}" "${CPPFLAGS+x}" "${CPPFLAGS-}" "${LDFLAGS+x}" "${LDFLAGS-}" + printf '%s\0' "${LIBS+x}" "${LIBS-}" "${MAKEFLAGS-}" "${DEBUG_FLAGS+x}" "${DEBUG_FLAGS-}" + printf '%s\0' "$FFMPEG_OPTION" "$FFMPEG_ID" "$FFMPEG_CFLAGS" "$FFMPEG_LDFLAGS" + printf '%s\0' "${MAKE_SETTINGS[@]}" + sha256sum build.sh +} | sha256sum) +CONFIG_REQUEST=${request_sum%% *} + +OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT="" +if [[ -f "$STATE" ]]; then + if ! { + read -r OLD_GENERATION_INPUT; read -r OLD_GENERATION_OUTPUT + read -r OLD_CONFIG_REQUEST; read -r OLD_CONFIG_OUTPUT + } < "$STATE"; then + echo "WARN: incomplete build state; adopting the build with a clean rebuild" + OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT="" fi fi -# Auto-detect FFmpeg (optional video transcoding). If libav libraries are -# available, build with --with-ffmpeg; configure gracefully skips it otherwise. -FFMPEG_OPT="" -if command -v pkg-config >/dev/null 2>&1 && \ - pkg-config --exists libavformat libavcodec libavutil libswscale libswresample 2>/dev/null; then - FFMPEG_OPT="--with-ffmpeg" - echo "FFmpeg detected — building with video transcoding" -else - echo "FFmpeg not found — building without video transcoding" +REGENERATE=0 +RECONFIGURE=0 +if [[ "$FULL" = 1 || "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" || "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then + echo "INFO: regenerating Autotools: full=$FULL" + if [[ "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" ]]; then echo "INFO: Autotools descriptions changed or unrecorded"; fi + if [[ "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then echo "INFO: generated Autotools files changed or missing"; fi + command -v autoreconf >/dev/null || { echo "ERROR: autoreconf is required to regenerate the build" >&2; exit 1; } + mkdir -p m4 + autoreconf -fi 2>&1 | tee autoreconf.log || { echo "ERROR: autoreconf failed; see autoreconf.log" >&2; exit 1; } + REGENERATE=1 fi - -# Generate Makefiles if needed -if [ ! -f Makefile ]; then - echo "" - echo "Running ./configure..." - if [ "$USE_ASAN" -eq 1 ]; then - ./configure CFLAGS="-g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie" LDFLAGS="-no-pie" $FFMPEG_OPT - elif [ "$USE_HARDENED" -eq 1 ]; then - ./configure --enable-hardening $FFMPEG_OPT - else - ./configure $FFMPEG_OPT +if [[ "$REGENERATE" = 1 || "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" || "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then + echo "INFO: configuring: mode=$MODE regenerated=$REGENERATE" + if [[ "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" ]]; then echo "INFO: compiler, flags or optional dependencies changed or unrecorded"; fi + if [[ "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then echo "INFO: configuration files changed or missing"; fi + CONFIG_ARGS=("$FFMPEG_OPTION" --disable-hardening) + if [[ "$MODE" = asan ]]; then + export CFLAGS="${CFLAGS-} -g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie" + export LDFLAGS="${LDFLAGS-} -fsanitize=address -no-pie" + elif [[ "$MODE" = hardened ]]; then + CONFIG_ARGS+=(--enable-hardening) fi + if [[ "$FFMPEG_OPTION" = --with-ffmpeg ]]; then + export CPPFLAGS="${CPPFLAGS-} $FFMPEG_CFLAGS" + export LDFLAGS="${LDFLAGS-} $FFMPEG_LDFLAGS" + fi + ./configure "${CONFIG_ARGS[@]}" 2>&1 | tee configure.log || { echo "ERROR: configure failed; see configure.log" >&2; exit 1; } + RECONFIGURE=1 fi -# Build (all .o files go to build/ directory, binary copied to project root) -echo "" -echo "Compiling with $JOBS..." -BUILD_SUCCESS=0 -if [ "$USE_ASAN" -eq 1 ]; then - echo "(AddressSanitizer enabled — lib/ and src/ only, skipping tests)" - AF="-fsanitize=address -fno-omit-frame-pointer -g -Og" - AL="-fsanitize=address" - (cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; } - # libopus собирается без ASAN (собственный CFLAGS в Makefile, .a несовместим с PIE) - (cd lib/libopus && make clean $JOBS $MAKE_ARGS 2>&1 && make $JOBS $MAKE_ARGS 2>&1) || { echo "libopus build failed"; exit 1; } - (cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; } - BUILD_SUCCESS=1 -elif [ "$USE_HARDENED" -eq 1 ]; then - echo "(Hardening flags enabled)" - HF="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -Wformat=2 -Wformat-overflow=2 -fPIE" - HL="-Wl,-z,relro -Wl,-z,now -pie" - (cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; } - (cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; } - (cd tests && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "tests build failed"; exit 1; } - BUILD_SUCCESS=1 +# Проверяем только зависимости объектов, перечисленных Automake; посторонние сборки не затрагиваем. +MISSING_DEPS=0 +for directory in "${BUILD_DIRS[@]}"; do + while read -r keyword depfile marker; do + [[ "$keyword" = include && "$marker" = '# am--include-marker' && "$depfile" = *.Po ]] || continue + depfile=${depfile//\$\(DEPDIR\)/.deps} + object=${depfile/.deps\//} + object=${object%.Po} + [[ -f "$directory/$object.o" || -f "$directory/$object.obj" ]] || continue + dependency_line="" + if [[ -s "$directory/$depfile" ]]; then IFS= read -r dependency_line < "$directory/$depfile" || true; fi + if [[ "$dependency_line" != *:* ]]; then + echo "WARN: missing or empty dependencies for $directory/$object: $directory/$depfile" + MISSING_DEPS=1 + break + fi + done < "$directory/Makefile" +done +if [[ "$RECONFIGURE" = 1 || "$CLEAN" = 1 || "$MISSING_DEPS" = 1 ]]; then + echo "INFO: cleaning objects: reconfigured=$RECONFIGURE requested=$CLEAN missing_dependencies=$MISSING_DEPS" + make clean 2>&1 | tee build.log || { echo "ERROR: clean failed; see build.log" >&2; exit 1; } else - make $JOBS $MAKE_ARGS 2>&1 | tee build.log - if [ ${PIPESTATUS[0]} -eq 0 ]; then - BUILD_SUCCESS=1 - fi + echo "INFO: configuration unchanged; incremental build (mode=$MODE)" + : > build.log fi -if [ $BUILD_SUCCESS -eq 1 ]; then - echo "" - echo "Build completed successfully!" - echo "Binary: utun" - - # Rebuild chatgui (CMake project) if transport sources changed - if [ -f tools/chatgui/build/CMakeCache.txt ] && command -v cmake &> /dev/null; then - echo "" - echo "======================================" - echo "Rebuilding chatgui (CMake)" - echo "======================================" - ( - if [ "${MSYSTEM:-}" = UCRT64 ]; then - cd tools/chatgui && bash build.sh - else - cd tools/chatgui/build && cmake --build . -j4 2>&1 - fi - ) || echo "WARNING: chatgui build failed" +echo "INFO: compiling: mode=$MODE jobs=$JOBS" +if [[ "$MODE" = asan ]]; then + echo "INFO: AddressSanitizer: building lib, Opus and src; skipping tests" + MAKE_ARGS+=("SUBDIRS=lib lib/libopus src") +fi +make "-j$JOBS" "${MAKE_ARGS[@]}" 2>&1 | tee -a build.log || { echo "ERROR: build failed; see build.log" >&2; exit 1; } + +# Состояние обновляется после успешной сборки ядра; ошибка GUI не инвалидирует готовые объекты. +GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}") +CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}") +state_tmp="$STATE.tmp.$$" +trap 'rm -f "$state_tmp"' EXIT +printf '%s\n' "$GENERATION_INPUT" "$GENERATION_OUTPUT" "$CONFIG_REQUEST" "$CONFIG_OUTPUT" > "$state_tmp" +mv -f "$state_tmp" "$STATE" + +if [[ "$BUILD_GUI" = 1 && -f tools/chatgui/build/CMakeCache.txt ]]; then + command -v cmake >/dev/null || { echo "ERROR: ChatGUI exists but cmake is not available" >&2; exit 1; } + echo "INFO: building ChatGUI with its CMake configuration: jobs=$JOBS" + if [[ "${MSYSTEM:-}" = UCRT64 ]]; then + (cd tools/chatgui && bash build.sh "-j$JOBS") || { echo "ERROR: ChatGUI build failed" >&2; exit 1; } + else + cmake --build tools/chatgui/build --parallel "$JOBS" || { echo "ERROR: ChatGUI build failed" >&2; exit 1; } fi -else - echo "" - echo "ERROR: Build failed!" - echo "Check build.log for details" - exit 1 fi +echo "Build completed successfully!" +echo "Binary: src/utun" diff --git a/tests/Makefile.am b/tests/Makefile.am index 97a66d63..1c8c62ce 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -1,6 +1,6 @@ # Tests Makefile.am for utun - all tests with new ll_queue library -EXTRA_DIST = fixtures/reality_openssl_hello.hex data/silero_speech.pcm data/silero_speech.txt +EXTRA_DIST = fixtures/reality_openssl_hello.hex data/silero_speech.pcm data/silero_speech.txt test_build_script.py # Порядок по замерам Linux: быстрые проверки раньше длительных; состав зависит от прежних configure-условий. @@ -715,23 +715,31 @@ test_media_delivery_chat_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) all-local: copy-test-configs copy-test-configs: $(check_PROGRAMS) - @for conf in $(srcdir)/test_*.conf $(srcdir)/test_*.json; do \ - if test -f "$$conf"; then \ - cp -f "$$conf" "$(top_builddir)/tests/" 2>/dev/null || true; \ - fi; \ - done 2>/dev/null || true + @if test "$(abs_srcdir)" != "$(abs_builddir)"; then \ + for conf in $(srcdir)/test_*.conf $(srcdir)/test_*.json; do \ + if test -f "$$conf"; then \ + cp -f "$$conf" "$(abs_builddir)/" || { echo "ERROR: cannot copy $$conf" >&2; exit 1; }; \ + fi; \ + done; \ + fi # Clean clean-local: -rm -f $(check_PROGRAMS) - -rm -f $(top_builddir)/tests/test_*.conf $(top_builddir)/tests/test_*.json 2>/dev/null || true + @if test "$(abs_srcdir)" != "$(abs_builddir)"; then rm -f $(abs_builddir)/test_*.conf $(abs_builddir)/test_*.json; fi -rm -rf $(top_builddir)/tests/logs 2>/dev/null || true # Test logs directory TEST_LOG_DIR = $(top_builddir)/tests/logs # Custom check target that runs tests and saves logs to files -check-local: $(check_PROGRAMS) +.PHONY: check-build-script +check-build-script: + @if command -v python3 >/dev/null 2>&1; then \ + python3 "$(srcdir)/test_build_script.py" "$(top_srcdir)/build.sh"; \ + else echo "[SKIP] build.sh regression checks require python3"; fi + +check-local: $(check_PROGRAMS) check-build-script @rm -f $(TEST_LOG_DIR)/*.log 2>/dev/null || true; $(MKDIR_P) $(TEST_LOG_DIR); \ G='\033[0;32m'; R='\033[0;31m'; Y='\033[0;33m'; N='\033[0m'; \ passed=0; \ From ff2eae2efbb0d32d96cf9e8bb34fa7722db73826 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 10 Oct 2026 14:14:05 +0200 Subject: [PATCH 2/7] Verify build mode transitions and propagate GUI job counts --- .gitignore | 2 + tests/test_build_script.py | 198 +++++++++++++++++++++++++++++++++++++ tools/chatgui/build.sh | 15 ++- 3 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 tests/test_build_script.py diff --git a/.gitignore b/.gitignore index a067cc1b..d5fae1e5 100644 --- a/.gitignore +++ b/.gitignore @@ -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_* diff --git a/tests/test_build_script.py b/tests/test_build_script.py new file mode 100644 index 00000000..8cdb1939 --- /dev/null +++ b/tests/test_build_script.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Проверка настоящего build.sh на маленьком проекте Autotools, без изменения исходников uTun.""" +from pathlib import Path +import json +import os +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 = [] + +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): + 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(): + return run(['./src/utun']).stdout.strip() + +try: + shutil.copyfile(script, base / 'build.sh') + 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]) +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]) +if test "x$enable_hardening" = xyes; then + CFLAGS="$CFLAGS -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE" + LDFLAGS="$LDFLAGS -Wl,-z,relro -Wl,-z,now -pie" +fi +TEST_CONFIGURE_OPTION +AC_CONFIG_FILES([Makefile lib/Makefile lib/libopus/Makefile src/Makefile tests/Makefile]) +AC_OUTPUT +''') + 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 +#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') + write('tests/Makefile.am', 'noinst_PROGRAMS = probe\nprobe_SOURCES = probe.c\n') + write('tests/probe.c', 'int main(void) { return 0; }\n') + package_tool = write('bin/pkg-config', '''#!/bin/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()) + 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()) + + for mode in ['asan', 'hardened', 'normal']: + 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 '__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() + 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) + result = build('--clean') + check('explicit clean does not reconfigure', 'INFO: cleaning objects:' in result.stdout and 'INFO: configuring:' not in result.stdout) + 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', '#!/bin/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') + 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 diff --git a/tools/chatgui/build.sh b/tools/chatgui/build.sh index 5dffdb15..8b62d98e 100755 --- a/tools/chatgui/build.sh +++ b/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; } From 62a853e73cd4d2e80b1627c368fad0eb77f57535 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 10 Oct 2026 14:24:29 +0200 Subject: [PATCH 3/7] Detect compiler path changes and verify safe configuration cleanup --- build.sh | 4 ++-- tests/test_build_script.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/build.sh b/build.sh index 79e36a05..41d7940f 100755 --- a/build.sh +++ b/build.sh @@ -52,7 +52,7 @@ 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_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; } # Хеши учитывают содержимое и имена файлов, включая добавление/удаление макросов. @@ -91,7 +91,7 @@ if command -v pkg-config >/dev/null && pkg-config --exists "${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' "$MODE" "${OSTYPE:-}" "${MSYSTEM:-}" "${MSYSTEM_PREFIX:-}" "${CC:-gcc}" "$COMPILER_PATH" "$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" diff --git a/tests/test_build_script.py b/tests/test_build_script.py index 8cdb1939..f216fc0e 100644 --- a/tests/test_build_script.py +++ b/tests/test_build_script.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Проверка настоящего build.sh на маленьком проекте Autotools, без изменения исходников uTun.""" from pathlib import Path -import json import os +import shlex import shutil import subprocess import sys @@ -92,7 +92,9 @@ int main(void) { printf("%d\\n", worker() + fixture() + opus_fixture()); return 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') - write('tests/Makefile.am', 'noinst_PROGRAMS = probe\nprobe_SOURCES = probe.c\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', '''#!/bin/bash [[ "${BUILD_TEST_FFMPEG:-0}" = 1 ]] || exit 1 @@ -141,6 +143,14 @@ esac build('CFLAGS=-g -O0 -DUSER_OPTION=1') check('repeat build with quoted flags is incremental', initial == times()) build() + gcc = shutil.which('gcc') + for directory in ['compiler-first', 'compiler-second']: + wrapper = write(directory + '/gcc', '#!/bin/bash\nexec ' + shlex.quote(gcc) + ' "$@"\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() @@ -161,8 +171,11 @@ esac 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() @@ -191,6 +204,19 @@ esac 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', '-j2'], ['make', '-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' 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: From d6c6a4d111bf61b7a773a56e7aeed46438f66ef6 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 10 Oct 2026 14:25:07 +0200 Subject: [PATCH 4/7] Use verified incremental builds instead of mandatory clean --- AGENTS.md | 6 ++++-- tests/Makefile.am | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 92cb29f3..e43e5ba9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,8 @@ powershell build_full.bat # запускает bash build.sh --full чер ### 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 +128,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 +616,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`: он отслеживает конфигурацию и очищает объекты только при необходимости. - Все лишнее что менял при отладке - строго вернуть назад в состояние до вмешательства ## Написание кода diff --git a/tests/Makefile.am b/tests/Makefile.am index 1c8c62ce..e818f07e 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -726,7 +726,7 @@ copy-test-configs: $(check_PROGRAMS) # Clean clean-local: -rm -f $(check_PROGRAMS) - @if test "$(abs_srcdir)" != "$(abs_builddir)"; then rm -f $(abs_builddir)/test_*.conf $(abs_builddir)/test_*.json; fi + @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 From e0ba199e20944f17ffdbb6ff3705c5fe759a9cd2 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 10 Oct 2026 14:39:10 +0200 Subject: [PATCH 5/7] Share incremental builds on FreeBSD and select portable hardening flags --- build.sh | 45 +++++++++---- build_bsd_full.sh | 162 +++++++--------------------------------------- configure.ac | 19 +++++- 3 files changed, 72 insertions(+), 154 deletions(-) diff --git a/build.sh b/build.sh index 41d7940f..39f2502f 100755 --- a/build.sh +++ b/build.sh @@ -1,5 +1,5 @@ -#!/bin/bash -# Инкрементальная сборка Linux и Windows/MSYS2; флаги задаются через configure. +#!/usr/bin/env bash +# Инкрементальная сборка Linux, FreeBSD и Windows/MSYS2; флаги задаются через configure. set -euo pipefail cd "$(dirname "$0")" @@ -48,9 +48,25 @@ while [[ $# -gt 0 ]]; do 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 +PLATFORM=$(uname -s) +MAKE_TOOL=${MAKE:-make} +if [[ "$PLATFORM" = FreeBSD ]]; then + MAKE_TOOL=${MAKE:-gmake} + export CC="${CC:-clang}" +fi +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; } @@ -58,7 +74,7 @@ COMPILER_ID=$("${COMPILER[@]}" --version) || { echo "ERROR: cannot identify comp # Хеши учитывают содержимое и имена файлов, включая добавление/удаление макросов. hash_files() { local result - result=$(sha256sum "$@" | sha256sum) + result=$({ printf '%s\0' "$@"; "${HASH_COMMAND[@]}" "$@"; } | "${HASH_COMMAND[@]}") printf '%s\n' "${result%% *}" } @@ -66,9 +82,8 @@ 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 +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 @@ -91,13 +106,14 @@ if command -v pkg-config >/dev/null && pkg-config --exists "${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_PATH" "$COMPILER_ID" + 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[@]}" - sha256sum build.sh -} | sha256sum) + hash_files build.sh +} | "${HASH_COMMAND[@]}") CONFIG_REQUEST=${request_sum%% *} OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT="" @@ -130,6 +146,7 @@ if [[ "$REGENERATE" = 1 || "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" || "$CONFI 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 @@ -161,7 +178,7 @@ for directory in "${BUILD_DIRS[@]}"; do 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; } + "$MAKE_TOOL" 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 @@ -172,7 +189,7 @@ 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; } +"$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[@]}") diff --git a/build_bsd_full.sh b/build_bsd_full.sh index bc209ea9..8c4c9bf8 100755 --- a/build_bsd_full.sh +++ b/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 "$@" diff --git a/configure.ac b/configure.ac index 1bebe33b..e7dbafd1 100644 --- a/configure.ac +++ b/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 ==================== From c16334d551561b6db86f5ece51a463f4b6edb6f7 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 10 Oct 2026 14:59:47 +0200 Subject: [PATCH 6/7] Run build regressions with native FreeBSD tools and Windows binaries --- check.sh | 24 +++++++++++++-------- tests/test_build_script.py | 44 +++++++++++++++++++++++++------------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/check.sh b/check.sh index e9992b14..c0c2feaf 100755 --- a/check.sh +++ b/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 diff --git a/tests/test_build_script.py b/tests/test_build_script.py index f216fc0e..dd42407d 100644 --- a/tests/test_build_script.py +++ b/tests/test_build_script.py @@ -18,6 +18,8 @@ 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 @@ -26,6 +28,8 @@ def write(name, 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') @@ -46,28 +50,33 @@ def times(): return {str(p.relative_to(base)): p.stat().st_mtime_ns for p in base.rglob('*.o')} def value(): - return run(['./src/utun']).stdout.strip() + 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]) -if test "x$enable_hardening" = xyes; then - CFLAGS="$CFLAGS -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE" - LDFLAGS="$LDFLAGS -Wl,-z,relro -Wl,-z,now -pie" -fi +__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') @@ -96,7 +105,7 @@ int main(void) { printf("%d\\n", worker() + fixture() + opus_fixture()); return 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', '''#!/bin/bash + package_tool = write('bin/pkg-config', '''#!/usr/bin/env bash [[ "${BUILD_TEST_FFMPEG:-0}" = 1 ]] || exit 1 case "$1" in --exists) exit 0 ;; @@ -110,7 +119,7 @@ esac 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()) + 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) @@ -125,14 +134,19 @@ esac build() check('unrelated header does not recompile', initial == times()) - for mode in ['asan', 'hardened', 'normal']: + 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 '__asan_' not in run(['nm', '-u', 'lib/libopus/opus.o']).stdout) + 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) @@ -143,9 +157,9 @@ esac build('CFLAGS=-g -O0 -DUSER_OPTION=1') check('repeat build with quoted flags is incremental', initial == times()) build() - gcc = shutil.which('gcc') + compiler = shutil.which(default_compiler) for directory in ['compiler-first', 'compiler-second']: - wrapper = write(directory + '/gcc', '#!/bin/bash\nexec ' + shlex.quote(gcc) + ' "$@"\n') + 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']}) @@ -199,7 +213,7 @@ esac write('configure.ac', original) build() write('tools/chatgui/build/CMakeCache.txt', 'placeholder\n') - cmake = write('bin/cmake', '#!/bin/bash\nprintf "%s\\n" "$*" > gui-arguments\nexit 9\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) @@ -207,13 +221,13 @@ esac run(['make', 'distclean']) output_dir = base / 'out' output_dir.mkdir() - for args in [['../configure'], ['make', '-j2'], ['make', '-C', 'tests', 'clean-local']]: + 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' and '-j2' in args: + 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()) From 4908cb7e198f938ad26032acd9fd9bb4f65d4027 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 10 Oct 2026 15:01:30 +0200 Subject: [PATCH 7/7] Document shared FreeBSD builds and locate Bash through PATH --- AGENTS.md | 9 +++++++++ tools/chatgui/build_nix.sh | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index e43e5ba9..c0922f66 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,6 +51,15 @@ 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 # инкрементальная сборка, конфигурация и очистка при необходимости diff --git a/tools/chatgui/build_nix.sh b/tools/chatgui/build_nix.sh index 6f86c09f..705de5f9 100755 --- a/tools/chatgui/build_nix.sh +++ b/tools/chatgui/build_nix.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -euo pipefail # ============================================================================