10 changed files with 530 additions and 355 deletions
@ -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" |
||||
|
||||
@ -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 "$@" |
||||
|
||||
@ -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 |
||||
Loading…
Reference in new issue