From c16334d551561b6db86f5ece51a463f4b6edb6f7 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 10 Oct 2026 14:59:47 +0200 Subject: [PATCH] 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())