#!/usr/bin/env bash # ============================================================================ # Gemeinsame Funktionen fuer make_installer_*.sh # ============================================================================ set -euo pipefail # Verzeichnisse SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SOURCE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" EXCLUDE_FILE="${SCRIPT_DIR}/exclude_patterns.txt" INSTALLER_DIR="${SCRIPT_DIR}/installer" BUILD_CONF="${SCRIPT_DIR}/build.conf" BUILD_LOG="${SCRIPT_DIR}/build.log" # --- build.conf laden --- load_build_conf() { # Defaults INCLUDE_AUDIO_ASSETS=true INCLUDE_MUSIC_ASSETS=false INCLUDE_MORPHER_ASSETS=true INCLUDE_WAKEWORD_MODELS=true INCLUDE_PIPER_MODELS=true # models/piper/*.onnx (inkl. Custom-Voices) INCLUDE_SYMSPELL=true INCLUDE_LLM_MODELS=false INCLUDE_PLUGIN_MODELS=false INCLUDE_TRAINER_SOURCE=true INCLUDE_TRAINING_DATA=false INCLUDE_DOCS=true INCLUDE_BASH_SCRIPTS=true if [[ -f "${BUILD_CONF}" ]]; then # Nur KEY=value Zeilen laden (keine Kommentare, keine leeren Zeilen) while IFS='=' read -r key value; do key=$(echo "${key}" | xargs) # Whitespace trimmen value=$(echo "${value}" | xargs) [[ -z "${key}" || "${key}" == \#* ]] && continue declare -g "${key}=${value}" done < "${BUILD_CONF}" echo " build.conf geladen" else echo " WARNUNG: ${BUILD_CONF} nicht gefunden — verwende Defaults" fi } # --- Dynamische Excludes aus build.conf generieren --- build_conf_excludes() { local mode="$1" local excludes="" # Assets if [[ "${INCLUDE_AUDIO_ASSETS}" != "true" ]]; then excludes+=" assets/default/audio" fi if [[ "${INCLUDE_MUSIC_ASSETS}" != "true" ]]; then excludes+=" assets/default/music" fi if [[ "${INCLUDE_MORPHER_ASSETS}" != "true" ]]; then excludes+=" assets/default/audio_morpher" fi # Modelle if [[ "${INCLUDE_WAKEWORD_MODELS}" != "true" ]]; then excludes+=" models/wakeword/*/*.onnx" fi if [[ "${INCLUDE_PIPER_MODELS}" != "true" ]]; then excludes+=" models/piper/*.onnx models/piper/.cache" fi if [[ "${INCLUDE_SYMSPELL}" != "true" ]]; then excludes+=" models/symspell" fi if [[ "${INCLUDE_LLM_MODELS}" != "true" ]]; then excludes+=" models/nlp/*.gguf" fi if [[ "${INCLUDE_PLUGIN_MODELS}" != "true" ]]; then # Alle Plugin-Models ausschliessen # Piper-Voices werden ueber models/piper/ (INCLUDE_PIPER_MODELS) behalten excludes+=" plugins/*/models" fi # Trainer if [[ "${INCLUDE_TRAINER_SOURCE}" != "true" ]]; then excludes+=" trainer" fi if [[ "${INCLUDE_TRAINING_DATA}" != "true" ]]; then excludes+=" trainer/data/raw trainer/data/processed trainer/assets trainer/cache trainer/processed" fi # Sonstiges if [[ "${INCLUDE_DOCS}" != "true" ]]; then excludes+=" docs" fi if [[ "${INCLUDE_BASH_SCRIPTS}" != "true" ]]; then excludes+=" bash" fi echo "${excludes}" } # --- Version aus trixy_core/utils/version.py parsen --- parse_version() { local version_file="${SOURCE_DIR}/trixy_core/utils/version.py" if [[ ! -f "${version_file}" ]]; then echo "FEHLER: ${version_file} nicht gefunden" >&2 exit 1 fi local major minor revision major=$(grep -E '^MAJOR:\s*int\s*=' "${version_file}" | grep -oE '[0-9]+') minor=$(grep -E '^MINOR:\s*int\s*=' "${version_file}" | grep -oE '[0-9]+') revision=$(grep -E '^REVISION:\s*int\s*=' "${version_file}" | grep -oE '[0-9]+') echo "${major}.${minor}.${revision}" } # --- Git-Revision ermitteln (Kurz-Hash, oder "nogit") --- get_git_revision() { if command -v git >/dev/null 2>&1 && git -C "${SOURCE_DIR}" rev-parse --git-dir >/dev/null 2>&1; then git -C "${SOURCE_DIR}" rev-parse --short HEAD 2>/dev/null || echo "nogit" else echo "nogit" fi } # --- Git-Status pruefen: "clean" oder "dirty" --- get_git_status() { if command -v git >/dev/null 2>&1 && git -C "${SOURCE_DIR}" rev-parse --git-dir >/dev/null 2>&1; then # Cache- und Runtime-Dateien ignorieren bei der Pruefung local changes changes=$(git -C "${SOURCE_DIR}" status --porcelain 2>/dev/null \ | grep -v '__pycache__' \ | grep -v '\.pyc$' \ | grep -v '^\s*M logs/' \ | grep -v '^\s*M data/activity/' \ | grep -v '^\s*M cron/' \ || true) if [[ -z "${changes}" ]]; then echo "clean" else echo "dirty" fi else echo "nogit" fi } # --- Naechste Build-Nummer ermitteln --- # Wenn der aktuelle Commit-Hash bereits in der build.log steht, wird dieselbe # Build-Nummer zurueckgegeben. Sonst wird max(build_number) + 1 verwendet. get_next_build_number() { local git_rev="$1" if [[ ! -f "${BUILD_LOG}" ]]; then echo "1" return fi # Suche existierenden Eintrag mit gleichem git_rev (Spalte 4) local existing existing=$(awk -F',' -v rev="${git_rev}" ' NR > 1 && $4 == rev { print $1; exit } ' "${BUILD_LOG}") if [[ -n "${existing}" ]]; then echo "${existing}" return fi # Neue Build-Nummer = max + 1 local max_build max_build=$(awk -F',' ' NR > 1 && $1 ~ /^[0-9]+$/ { if ($1 > m) m = $1 } END { print m + 0 } ' "${BUILD_LOG}") echo "$((max_build + 1))" } # --- build.log Eintrag hinzufuegen/aktualisieren --- # Spalten: build_number,version,full_version,git_rev,git_status,timestamp,modes # # Wenn git_rev bereits existiert, wird der Modus zur modes-Spalte hinzugefuegt # (falls noch nicht vorhanden) und timestamp aktualisiert. update_build_log() { local build_number="$1" local version="$2" local full_version="$3" local git_rev="$4" local git_status="$5" local mode="$6" local timestamp timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") # Header erstellen wenn Datei neu if [[ ! -f "${BUILD_LOG}" ]]; then echo "build_number,version,full_version,git_rev,git_status,timestamp,modes" > "${BUILD_LOG}" fi # Existierenden Eintrag suchen if grep -q "^${build_number}," "${BUILD_LOG}" 2>/dev/null; then # Eintrag aktualisieren: modes ergaenzen, timestamp updaten local tmp tmp=$(mktemp) awk -F',' -v bn="${build_number}" -v mode="${mode}" -v ts="${timestamp}" -v gs="${git_status}" ' BEGIN { OFS = "," } NR == 1 { print; next } $1 == bn { # Modes zusammenfuehren (ohne Duplikate) n = split($7, arr, "|") found = 0 for (i = 1; i <= n; i++) if (arr[i] == mode) found = 1 if (!found) { if ($7 == "") $7 = mode else $7 = $7 "|" mode } $5 = gs # status aktualisieren $6 = ts # timestamp aktualisieren print next } { print } ' "${BUILD_LOG}" > "${tmp}" mv "${tmp}" "${BUILD_LOG}" else # Neuer Eintrag echo "${build_number},${version},${full_version},${git_rev},${git_status},${timestamp},${mode}" >> "${BUILD_LOG}" fi } # --- VERSION-Datei fuer den Build generieren --- # Wird ins Release-Verzeichnis geschrieben UND ins Source-Tar aufgenommen. generate_version_file() { local target_path="$1" local mode="$2" local version="$3" local build_number="$4" local full_version="$5" local git_rev="$6" local git_status="$7" local timestamp timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") cat > "${target_path}" << EOF Name: Trixy Mode: ${mode} Version: ${version} Build: ${build_number} FullVersion: ${full_version} GitRevision: ${git_rev} GitStatus: ${git_status} BuildDate: ${timestamp} EOF } # --- Build-Info sammeln und ausgeben --- # Setzt globale Variablen: BUILD_VERSION, BUILD_NUMBER, BUILD_FULL_VERSION, # BUILD_GIT_REV, BUILD_GIT_STATUS collect_build_info() { BUILD_VERSION=$(parse_version) BUILD_GIT_REV=$(get_git_revision) BUILD_GIT_STATUS=$(get_git_status) BUILD_NUMBER=$(get_next_build_number "${BUILD_GIT_REV}") BUILD_FULL_VERSION="${BUILD_VERSION}.${BUILD_NUMBER}" echo " Version: ${BUILD_VERSION}" echo " Build: ${BUILD_NUMBER}" echo " FullVersion: ${BUILD_FULL_VERSION}" echo " Git-Revision: ${BUILD_GIT_REV} (${BUILD_GIT_STATUS})" } # --- Release-Verzeichnis erstellen --- prepare_release_dir() { local version="$1" local mode="$2" local release_dir="${SCRIPT_DIR}/Release/trixy-${mode}-${version}" mkdir -p "${release_dir}" echo "${release_dir}" } # --- build.conf Einstellungen anzeigen --- print_build_conf() { local mode="$1" echo " Build-Konfiguration:" echo " Audio-Assets: ${INCLUDE_AUDIO_ASSETS}" echo " Musik-Assets: ${INCLUDE_MUSIC_ASSETS}" echo " Morpher-Assets: ${INCLUDE_MORPHER_ASSETS}" echo " Wakeword-Modelle: ${INCLUDE_WAKEWORD_MODELS}" echo " Piper-Modelle: ${INCLUDE_PIPER_MODELS}" echo " SymSpell: ${INCLUDE_SYMSPELL}" echo " LLM-Modelle: ${INCLUDE_LLM_MODELS}" echo " Plugin-Modelle: ${INCLUDE_PLUGIN_MODELS}" echo " Trainer-Source: ${INCLUDE_TRAINER_SOURCE}" echo " Trainings-Daten: ${INCLUDE_TRAINING_DATA}" echo " Dokumentation: ${INCLUDE_DOCS}" echo " Bash-Skripte: ${INCLUDE_BASH_SCRIPTS}" echo "" } # --- Source-tar erstellen --- create_source_tar() { local mode="$1" local version="$2" local release_dir="$3" local extra_excludes="$4" # Zusaetzliche Exclude-Patterns (Leerzeichen-getrennt) local tar_name="trixy-${mode}-${version}.tar.gz" local tar_path="${release_dir}/${tar_name}" echo " Erstelle ${tar_name}..." # VERSION-Datei temporaer in SOURCE_DIR schreiben (wird mit ins tar aufgenommen) local version_file="${SOURCE_DIR}/VERSION" generate_version_file "${version_file}" \ "${mode}" \ "${BUILD_VERSION}" \ "${BUILD_NUMBER}" \ "${BUILD_FULL_VERSION}" \ "${BUILD_GIT_REV}" \ "${BUILD_GIT_STATUS}" # Kopie ins Release-Verzeichnis cp "${version_file}" "${release_dir}/VERSION" # Basis-Excludes aus exclude_patterns.txt local exclude_opts=() while IFS= read -r pattern; do [[ -z "${pattern}" || "${pattern}" == \#* ]] && continue exclude_opts+=("--exclude=${pattern}") done < "${EXCLUDE_FILE}" # build.conf-basierte Excludes local conf_excludes conf_excludes=$(build_conf_excludes "${mode}") for pattern in ${conf_excludes}; do exclude_opts+=("--exclude=${pattern}") done # Modus-spezifische Excludes for pattern in ${extra_excludes}; do exclude_opts+=("--exclude=${pattern}") done # tar in temporaere Datei schreiben, dann verschieben local tmp_tar tmp_tar="$(mktemp)" local prefix="trixy-${mode}-${version}" tar czf "${tmp_tar}" \ -C "${SOURCE_DIR}" \ "${exclude_opts[@]}" \ --exclude='make_installer/Release' \ --transform="s,^\\.,${prefix}," \ . mv "${tmp_tar}" "${tar_path}" # VERSION-Datei aus SOURCE_DIR wieder entfernen rm -f "${version_file}" echo " -> ${tar_name} ($(du -h "${tar_path}" | cut -f1))" } # --- Installer-tar erstellen --- create_installer_tar() { local mode="$1" local version="$2" local release_dir="$3" local tar_name="trixy-installer-${mode}-${version}.tar.gz" local tar_path="${release_dir}/${tar_name}" echo " Erstelle ${tar_name}..." tar czf "${tar_path}" \ -C "${INSTALLER_DIR}/.." \ --transform='s,^installer,trixy_installer,' \ --exclude='__pycache__' \ --exclude='*.pyc' \ installer/ echo " -> ${tar_name} ($(du -h "${tar_path}" | cut -f1))" } # --- install.sh generieren --- generate_install_sh() { local mode="$1" local version="$2" local release_dir="$3" local install_sh="${release_dir}/install.sh" local source_tar="trixy-${mode}-${version}.tar.gz" local installer_tar="trixy-installer-${mode}-${version}.tar.gz" echo " Generiere install.sh..." cat > "${install_sh}" << 'INSTALL_HEADER' #!/usr/bin/env bash # ============================================================================ # Trixy Installer — Bootstrap-Script # # Entpackt den Installer, erstellt ein temporaeres venv und startet die TUI. # ============================================================================ set -euo pipefail INSTALL_HEADER cat >> "${install_sh}" << INSTALL_VARS # --- Eingebettete Werte --- MODE="${mode}" VERSION="${version}" SOURCE_TAR="${source_tar}" INSTALLER_TAR="${installer_tar}" INSTALL_VARS cat >> "${install_sh}" << 'INSTALL_BODY' # --- Farben --- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TEMP_DIR="" cleanup() { if [[ -n "${TEMP_DIR}" && -d "${TEMP_DIR}" ]]; then echo -e "${YELLOW}Raeume auf...${NC}" rm -rf "${TEMP_DIR}" fi } trap cleanup EXIT echo "" echo -e "${GREEN}========================================" echo " Trixy Installer v${VERSION} [${MODE}]" echo -e "========================================${NC}" echo "" # 1. Pruefe ob tar-Dateien vorhanden if [[ ! -f "${SCRIPT_DIR}/${SOURCE_TAR}" ]]; then echo -e "${RED}FEHLER: ${SOURCE_TAR} nicht gefunden${NC}" exit 1 fi if [[ ! -f "${SCRIPT_DIR}/${INSTALLER_TAR}" ]]; then echo -e "${RED}FEHLER: ${INSTALLER_TAR} nicht gefunden${NC}" exit 1 fi # 2. Python >= 3.10 sicherstellen echo "Pruefe Python-Version..." PYTHON_BIN="" for py in python3.12 python3.11 python3.10 python3; do if command -v "${py}" >/dev/null 2>&1; then py_minor=$("${py}" -c "import sys; print(sys.version_info.minor)") if [[ "${py_minor}" -ge 10 ]]; then PYTHON_BIN="${py}" break fi fi done if [[ -z "${PYTHON_BIN}" ]]; then echo -e "${YELLOW}Python >= 3.10 nicht gefunden. Versuche Installation...${NC}" if command -v apt-get >/dev/null 2>&1; then sudo apt-get update -qq sudo apt-get install -y -qq python3 python3-venv python3-pip PYTHON_BIN="python3" else echo -e "${RED}FEHLER: Python >= 3.10 wird benoetigt${NC}" exit 1 fi fi echo " Verwende: ${PYTHON_BIN} ($(${PYTHON_BIN} --version 2>&1))" # 3. Installer-tar in temp-Verzeichnis entpacken TEMP_DIR="$(mktemp -d)" echo "Entpacke Installer nach ${TEMP_DIR}..." tar xzf "${SCRIPT_DIR}/${INSTALLER_TAR}" -C "${TEMP_DIR}" # 4. Installer-venv erstellen INST_VENV="${TEMP_DIR}/installer-venv" echo "Erstelle Installer-venv..." "${PYTHON_BIN}" -m venv "${INST_VENV}" "${INST_VENV}/bin/pip" install --upgrade pip -q 2>/dev/null "${INST_VENV}/bin/pip" install -r "${TEMP_DIR}/trixy_installer/requirements.txt" -q # 5. TUI starten echo "" echo -e "${GREEN}Starte Installer-TUI...${NC}" echo "" PYTHONPATH="${TEMP_DIR}" "${INST_VENV}/bin/python" -m trixy_installer \ --mode "${MODE}" \ --version "${VERSION}" \ --source-archive "${SCRIPT_DIR}/${SOURCE_TAR}" echo "" echo -e "${GREEN}Installer beendet.${NC}" INSTALL_BODY chmod +x "${install_sh}" echo " -> install.sh generiert" } # --- Build zusammenfassen --- print_summary() { local mode="$1" local version="$2" local release_dir="$3" echo "" echo "========================================" echo " Build abgeschlossen: ${mode} v${BUILD_FULL_VERSION:-${version}}" echo "========================================" echo "" echo " Version: ${BUILD_VERSION:-${version}}" echo " Build: ${BUILD_NUMBER:-?}" echo " FullVersion: ${BUILD_FULL_VERSION:-${version}}" echo " Git: ${BUILD_GIT_REV:-unknown} (${BUILD_GIT_STATUS:-unknown})" echo "" echo " Release-Verzeichnis: ${release_dir}" echo "" ls -lh "${release_dir}/" echo "" echo " Deployment:" echo " 1. Release-Verzeichnis auf Zielgeraet kopieren" echo " 2. chmod +x install.sh" echo " 3. ./install.sh" echo "" }