2 changed files with 187 additions and 200 deletions
@ -1,216 +1,195 @@ |
|||||||
#!/bin/bash |
#!/bin/bash |
||||||
# Build script for uTun (supports both Linux and Windows/MSYS2) |
# Инкрементальная сборка Linux и Windows/MSYS2; флаги задаются через configure. |
||||||
# Usage: ./build.sh [options] |
set -euo pipefail |
||||||
# Options: |
cd "$(dirname "$0")" |
||||||
# -h, --help Show this help |
|
||||||
# --clean Clean before build |
|
||||||
# --full Full rebuild (autoreconf + configure + make) |
|
||||||
# --asan Build with AddressSanitizer (debug) |
|
||||||
# --hardened Build with compiler security hardening flags |
|
||||||
# -j[N] Build with N parallel jobs (default: 4) |
|
||||||
|
|
||||||
set -e # Exit on error |
MODE=normal |
||||||
|
|
||||||
# Detect platform |
|
||||||
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ -n "$MSYSTEM" ]]; then |
|
||||||
IS_WINDOWS=1 |
|
||||||
echo "Detected Windows/MSYS2 environment" |
|
||||||
else |
|
||||||
IS_WINDOWS=0 |
|
||||||
echo "Detected Linux environment" |
|
||||||
fi |
|
||||||
|
|
||||||
# Parse arguments |
|
||||||
CLEAN=0 |
CLEAN=0 |
||||||
FULL_REBUILD=0 |
FULL=0 |
||||||
USE_ASAN=0 |
BUILD_GUI=1 |
||||||
USE_HARDENED=0 |
JOBS=4 |
||||||
JOBS="-j4" |
MAKE_ARGS=() |
||||||
MAKE_ARGS="" |
MAKE_SETTINGS=() |
||||||
HARDEN_FLAGS="" |
STATE=.utun-build-state |
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do |
while [[ $# -gt 0 ]]; do |
||||||
case $1 in |
case "$1" in |
||||||
-h|--help) |
-h|--help) |
||||||
echo "Usage: ./build.sh [options]" |
echo "Usage: ./build.sh [--clean] [--full] [--asan|--hardened] [--no-gui] [-jN] [make arguments]" |
||||||
echo "Options:" |
echo " --clean Rebuild objects without forcing configure" |
||||||
echo " -h, --help Show this help" |
echo " --full Regenerate, configure and rebuild everything" |
||||||
echo " --clean Clean before build" |
echo " --asan AddressSanitizer; skip tests" |
||||||
echo " --full Full rebuild (autoreconf + configure + make)" |
echo " --hardened Enable compiler security hardening" |
||||||
echo " --asan Build with AddressSanitizer" |
echo " --no-gui Skip the separately configured ChatGUI" |
||||||
echo " --hardened Build with compiler security hardening flags" |
echo " -jN, -j N Parallel jobs (default: 4)" |
||||||
echo " -j[N] Build with N parallel jobs (default: 4)" |
echo " CC/CFLAGS/CPPFLAGS/LDFLAGS/LIBS assignments are passed to configure" |
||||||
exit 0 |
exit 0 ;; |
||||||
;; |
--clean) CLEAN=1 ;; |
||||||
--clean) |
--full) FULL=1 ;; |
||||||
CLEAN=1 |
--no-gui) BUILD_GUI=0 ;; |
||||||
shift |
--asan|--hardened) |
||||||
;; |
selected_mode=${1#--} |
||||||
--full) |
if [[ "$MODE" != normal && "$MODE" != "$selected_mode" ]]; then |
||||||
FULL_REBUILD=1 |
echo "ERROR: --asan and --hardened cannot be combined" >&2; exit 1 |
||||||
shift |
fi |
||||||
;; |
MODE=$selected_mode ;; |
||||||
--asan) |
-j) |
||||||
USE_ASAN=1 |
[[ $# -gt 1 ]] || { echo "ERROR: -j requires a positive job count" >&2; exit 1; } |
||||||
shift |
shift; JOBS=$1 ;; |
||||||
;; |
-j*) JOBS=${1#-j} ;; |
||||||
--hardened) |
CC=*|CFLAGS=*|CPPFLAGS=*|LDFLAGS=*|LIBS=*) export "$1" ;; |
||||||
USE_HARDENED=1 |
CC+=*|CFLAGS+=*|CPPFLAGS+=*|LDFLAGS+=*|LIBS+=*) |
||||||
shift |
echo "ERROR: use NAME=value for configure flags; append assignments are not supported" >&2; exit 1 ;; |
||||||
;; |
|
||||||
-j*) |
|
||||||
JOBS="$1" |
|
||||||
shift |
|
||||||
;; |
|
||||||
*) |
*) |
||||||
MAKE_ARGS="$MAKE_ARGS $1" |
MAKE_ARGS+=("$1") |
||||||
shift |
[[ "$1" != *=* ]] || MAKE_SETTINGS+=("$1") ;; |
||||||
;; |
|
||||||
esac |
esac |
||||||
|
shift |
||||||
done |
done |
||||||
|
[[ "$JOBS" =~ ^[1-9][0-9]*$ ]] || { echo "ERROR: invalid job count: $JOBS" >&2; exit 1; } |
||||||
|
|
||||||
# Check required tools |
for tool in make sha256sum; do |
||||||
if ! command -v gcc &> /dev/null; then |
command -v "$tool" >/dev/null || { echo "ERROR: required tool not found: $tool" >&2; exit 1; } |
||||||
echo "Error: gcc not found." |
done |
||||||
if [ "$IS_WINDOWS" -eq 1 ]; then |
read -r -a COMPILER <<< "${CC:-gcc}" |
||||||
echo "Please install: pacman -S mingw-w64-ucrt-x86_64-toolchain" |
command -v "${COMPILER[0]}" >/dev/null || { echo "ERROR: compiler not found: ${CC:-gcc}" >&2; exit 1; } |
||||||
else |
COMPILER_ID=$("${COMPILER[@]}" --version) || { echo "ERROR: cannot identify compiler: ${CC:-gcc}" >&2; exit 1; } |
||||||
echo "Please install build-essential or gcc" |
|
||||||
fi |
# Хеши учитывают содержимое и имена файлов, включая добавление/удаление макросов. |
||||||
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 |
fi |
||||||
|
GENERATION_INPUT=$(hash_files "${AUTOTOOLS_INPUTS[@]}") |
||||||
# Clean if requested |
GENERATION_OUTPUT=missing |
||||||
if [ "$CLEAN" -eq 1 ]; then |
CONFIG_OUTPUT=missing |
||||||
echo "Cleaning build..." |
missing_generation=0 |
||||||
make clean 2>/dev/null || true |
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 |
fi |
||||||
|
request_sum=$({ |
||||||
# Full rebuild: regenerate build system |
printf '%s\0' "$MODE" "${OSTYPE:-}" "${MSYSTEM:-}" "${MSYSTEM_PREFIX:-}" "${CC:-gcc}" "$COMPILER_ID" |
||||||
if [ "$FULL_REBUILD" -eq 1 ]; then |
printf '%s\0' "${CFLAGS+x}" "${CFLAGS-}" "${CPPFLAGS+x}" "${CPPFLAGS-}" "${LDFLAGS+x}" "${LDFLAGS-}" |
||||||
echo "" |
printf '%s\0' "${LIBS+x}" "${LIBS-}" "${MAKEFLAGS-}" "${DEBUG_FLAGS+x}" "${DEBUG_FLAGS-}" |
||||||
echo "======================================" |
printf '%s\0' "$FFMPEG_OPTION" "$FFMPEG_ID" "$FFMPEG_CFLAGS" "$FFMPEG_LDFLAGS" |
||||||
echo "FULL REBUILD: autoreconf" |
printf '%s\0' "${MAKE_SETTINGS[@]}" |
||||||
echo "======================================" |
sha256sum build.sh |
||||||
echo "" |
} | sha256sum) |
||||||
|
CONFIG_REQUEST=${request_sum%% *} |
||||||
# Check for autotools |
|
||||||
if ! command -v autoreconf &> /dev/null; then |
OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT="" |
||||||
echo "Error: autoreconf not found." |
if [[ -f "$STATE" ]]; then |
||||||
if [ "$IS_WINDOWS" -eq 1 ]; then |
if ! { |
||||||
echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools" |
read -r OLD_GENERATION_INPUT; read -r OLD_GENERATION_OUTPUT |
||||||
else |
read -r OLD_CONFIG_REQUEST; read -r OLD_CONFIG_OUTPUT |
||||||
echo "Install autotools: apt-get install autoconf automake libtool" |
} < "$STATE"; then |
||||||
fi |
echo "WARN: incomplete build state; adopting the build with a clean rebuild" |
||||||
exit 1 |
OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT="" |
||||||
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 |
|
||||||
fi |
fi |
||||||
fi |
fi |
||||||
|
|
||||||
# Auto-detect FFmpeg (optional video transcoding). If libav libraries are |
REGENERATE=0 |
||||||
# available, build with --with-ffmpeg; configure gracefully skips it otherwise. |
RECONFIGURE=0 |
||||||
FFMPEG_OPT="" |
if [[ "$FULL" = 1 || "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" || "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then |
||||||
if command -v pkg-config >/dev/null 2>&1 && \ |
echo "INFO: regenerating Autotools: full=$FULL" |
||||||
pkg-config --exists libavformat libavcodec libavutil libswscale libswresample 2>/dev/null; then |
if [[ "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" ]]; then echo "INFO: Autotools descriptions changed or unrecorded"; fi |
||||||
FFMPEG_OPT="--with-ffmpeg" |
if [[ "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then echo "INFO: generated Autotools files changed or missing"; fi |
||||||
echo "FFmpeg detected — building with video transcoding" |
command -v autoreconf >/dev/null || { echo "ERROR: autoreconf is required to regenerate the build" >&2; exit 1; } |
||||||
else |
mkdir -p m4 |
||||||
echo "FFmpeg not found — building without video transcoding" |
autoreconf -fi 2>&1 | tee autoreconf.log || { echo "ERROR: autoreconf failed; see autoreconf.log" >&2; exit 1; } |
||||||
|
REGENERATE=1 |
||||||
fi |
fi |
||||||
|
if [[ "$REGENERATE" = 1 || "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" || "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then |
||||||
# Generate Makefiles if needed |
echo "INFO: configuring: mode=$MODE regenerated=$REGENERATE" |
||||||
if [ ! -f Makefile ]; then |
if [[ "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" ]]; then echo "INFO: compiler, flags or optional dependencies changed or unrecorded"; fi |
||||||
echo "" |
if [[ "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then echo "INFO: configuration files changed or missing"; fi |
||||||
echo "Running ./configure..." |
CONFIG_ARGS=("$FFMPEG_OPTION" --disable-hardening) |
||||||
if [ "$USE_ASAN" -eq 1 ]; then |
if [[ "$MODE" = asan ]]; then |
||||||
./configure CFLAGS="-g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie" LDFLAGS="-no-pie" $FFMPEG_OPT |
export CFLAGS="${CFLAGS-} -g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie" |
||||||
elif [ "$USE_HARDENED" -eq 1 ]; then |
export LDFLAGS="${LDFLAGS-} -fsanitize=address -no-pie" |
||||||
./configure --enable-hardening $FFMPEG_OPT |
elif [[ "$MODE" = hardened ]]; then |
||||||
else |
CONFIG_ARGS+=(--enable-hardening) |
||||||
./configure $FFMPEG_OPT |
|
||||||
fi |
fi |
||||||
|
if [[ "$FFMPEG_OPTION" = --with-ffmpeg ]]; then |
||||||
|
export CPPFLAGS="${CPPFLAGS-} $FFMPEG_CFLAGS" |
||||||
|
export LDFLAGS="${LDFLAGS-} $FFMPEG_LDFLAGS" |
||||||
|
fi |
||||||
|
./configure "${CONFIG_ARGS[@]}" 2>&1 | tee configure.log || { echo "ERROR: configure failed; see configure.log" >&2; exit 1; } |
||||||
|
RECONFIGURE=1 |
||||||
fi |
fi |
||||||
|
|
||||||
# Build (all .o files go to build/ directory, binary copied to project root) |
# Проверяем только зависимости объектов, перечисленных Automake; посторонние сборки не затрагиваем. |
||||||
echo "" |
MISSING_DEPS=0 |
||||||
echo "Compiling with $JOBS..." |
for directory in "${BUILD_DIRS[@]}"; do |
||||||
BUILD_SUCCESS=0 |
while read -r keyword depfile marker; do |
||||||
if [ "$USE_ASAN" -eq 1 ]; then |
[[ "$keyword" = include && "$marker" = '# am--include-marker' && "$depfile" = *.Po ]] || continue |
||||||
echo "(AddressSanitizer enabled — lib/ and src/ only, skipping tests)" |
depfile=${depfile//\$\(DEPDIR\)/.deps} |
||||||
AF="-fsanitize=address -fno-omit-frame-pointer -g -Og" |
object=${depfile/.deps\//} |
||||||
AL="-fsanitize=address" |
object=${object%.Po} |
||||||
(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; } |
[[ -f "$directory/$object.o" || -f "$directory/$object.obj" ]] || continue |
||||||
# libopus собирается без ASAN (собственный CFLAGS в Makefile, .a несовместим с PIE) |
dependency_line="" |
||||||
(cd lib/libopus && make clean $JOBS $MAKE_ARGS 2>&1 && make $JOBS $MAKE_ARGS 2>&1) || { echo "libopus build failed"; exit 1; } |
if [[ -s "$directory/$depfile" ]]; then IFS= read -r dependency_line < "$directory/$depfile" || true; fi |
||||||
(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; } |
if [[ "$dependency_line" != *:* ]]; then |
||||||
BUILD_SUCCESS=1 |
echo "WARN: missing or empty dependencies for $directory/$object: $directory/$depfile" |
||||||
elif [ "$USE_HARDENED" -eq 1 ]; then |
MISSING_DEPS=1 |
||||||
echo "(Hardening flags enabled)" |
break |
||||||
HF="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -Wformat=2 -Wformat-overflow=2 -fPIE" |
fi |
||||||
HL="-Wl,-z,relro -Wl,-z,now -pie" |
done < "$directory/Makefile" |
||||||
(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; } |
done |
||||||
(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; } |
if [[ "$RECONFIGURE" = 1 || "$CLEAN" = 1 || "$MISSING_DEPS" = 1 ]]; then |
||||||
(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; } |
echo "INFO: cleaning objects: reconfigured=$RECONFIGURE requested=$CLEAN missing_dependencies=$MISSING_DEPS" |
||||||
BUILD_SUCCESS=1 |
make clean 2>&1 | tee build.log || { echo "ERROR: clean failed; see build.log" >&2; exit 1; } |
||||||
else |
else |
||||||
make $JOBS $MAKE_ARGS 2>&1 | tee build.log |
echo "INFO: configuration unchanged; incremental build (mode=$MODE)" |
||||||
if [ ${PIPESTATUS[0]} -eq 0 ]; then |
: > build.log |
||||||
BUILD_SUCCESS=1 |
|
||||||
fi |
|
||||||
fi |
fi |
||||||
|
|
||||||
if [ $BUILD_SUCCESS -eq 1 ]; then |
echo "INFO: compiling: mode=$MODE jobs=$JOBS" |
||||||
echo "" |
if [[ "$MODE" = asan ]]; then |
||||||
echo "Build completed successfully!" |
echo "INFO: AddressSanitizer: building lib, Opus and src; skipping tests" |
||||||
echo "Binary: utun" |
MAKE_ARGS+=("SUBDIRS=lib lib/libopus src") |
||||||
|
fi |
||||||
# Rebuild chatgui (CMake project) if transport sources changed |
make "-j$JOBS" "${MAKE_ARGS[@]}" 2>&1 | tee -a build.log || { echo "ERROR: build failed; see build.log" >&2; exit 1; } |
||||||
if [ -f tools/chatgui/build/CMakeCache.txt ] && command -v cmake &> /dev/null; then |
|
||||||
echo "" |
# Состояние обновляется после успешной сборки ядра; ошибка GUI не инвалидирует готовые объекты. |
||||||
echo "======================================" |
GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}") |
||||||
echo "Rebuilding chatgui (CMake)" |
CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}") |
||||||
echo "======================================" |
state_tmp="$STATE.tmp.$$" |
||||||
( |
trap 'rm -f "$state_tmp"' EXIT |
||||||
if [ "${MSYSTEM:-}" = UCRT64 ]; then |
printf '%s\n' "$GENERATION_INPUT" "$GENERATION_OUTPUT" "$CONFIG_REQUEST" "$CONFIG_OUTPUT" > "$state_tmp" |
||||||
cd tools/chatgui && bash build.sh |
mv -f "$state_tmp" "$STATE" |
||||||
else |
|
||||||
cd tools/chatgui/build && cmake --build . -j4 2>&1 |
if [[ "$BUILD_GUI" = 1 && -f tools/chatgui/build/CMakeCache.txt ]]; then |
||||||
fi |
command -v cmake >/dev/null || { echo "ERROR: ChatGUI exists but cmake is not available" >&2; exit 1; } |
||||||
) || echo "WARNING: chatgui build failed" |
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 |
fi |
||||||
else |
|
||||||
echo "" |
|
||||||
echo "ERROR: Build failed!" |
|
||||||
echo "Check build.log for details" |
|
||||||
exit 1 |
|
||||||
fi |
fi |
||||||
|
echo "Build completed successfully!" |
||||||
|
echo "Binary: src/utun" |
||||||
|
|||||||
Loading…
Reference in new issue