#!/bin/bash # Manifest-driven module management. Stages repositories, applies module # metadata hooks, manages configuration files, and flags rebuild requirements. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" MODULE_HELPER="$SCRIPT_DIR/modules.py" DEFAULT_ENV_PATH="$PROJECT_ROOT/.env" ENV_PATH="${MODULES_ENV_PATH:-$DEFAULT_ENV_PATH}" BLUE='\033[0;34m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' PLAYERBOTS_DB_UPDATE_LOGGED=0 info(){ printf '%b\n' "${BLUE}ℹ️ $*${NC}"; } ok(){ printf '%b\n' "${GREEN}✅ $*${NC}"; } warn(){ printf '%b\n' "${YELLOW}⚠️ $*${NC}"; } err(){ printf '%b\n' "${RED}❌ $*${NC}"; exit 1; } read_env_value(){ local key="$1" default="${2:-}" value="${!key:-}" if [ -n "$value" ]; then echo "$value" return fi if [ -f "$ENV_PATH" ]; then value="$(grep -E "^${key}=" "$ENV_PATH" 2>/dev/null | tail -n1 | cut -d'=' -f2- | tr -d '\r')" value="$(echo "$value" | sed 's/[[:space:]]*#.*//' | sed 's/[[:space:]]*$//')" if [[ "$value" == \"*\" && "$value" == *\" ]]; then value="${value:1:-1}" elif [[ "$value" == \'*\' && "$value" == *\' ]]; then value="${value:1:-1}" fi fi if [ -z "${value:-}" ]; then value="$default" fi printf '%s\n' "${value}" } ensure_python(){ if ! command -v python3 >/dev/null 2>&1; then err "python3 is required but not installed in PATH" fi } resolve_manifest_path(){ if [ -n "${MODULES_MANIFEST_PATH:-}" ] && [ -f "${MODULES_MANIFEST_PATH}" ]; then echo "${MODULES_MANIFEST_PATH}" return fi local candidate candidate="$PROJECT_ROOT/config/modules.json" if [ -f "$candidate" ]; then echo "$candidate" return fi candidate="$SCRIPT_DIR/../config/modules.json" if [ -f "$candidate" ]; then echo "$candidate" return fi candidate="/tmp/config/modules.json" if [ -f "$candidate" ]; then echo "$candidate" return fi err "Unable to locate module manifest (set MODULES_MANIFEST_PATH or ensure config/modules.json exists)" } setup_git_config(){ info "Configuring git identity" git config --global user.name "${GIT_USERNAME:-ac-compose}" >/dev/null 2>&1 || true git config --global user.email "${GIT_EMAIL:-noreply@azerothcore.org}" >/dev/null 2>&1 || true } generate_module_state(){ mkdir -p "$STATE_DIR" if ! python3 "$MODULE_HELPER" --env-path "$ENV_PATH" --manifest "$MANIFEST_PATH" generate --output-dir "$STATE_DIR"; then err "Module manifest validation failed" fi local env_file="$STATE_DIR/modules.env" if [ ! -f "$env_file" ]; then err "modules.env not produced at $env_file" fi # shellcheck disable=SC1090 source "$env_file" if ! MODULE_SHELL_STATE="$(python3 "$MODULE_HELPER" --env-path "$ENV_PATH" --manifest "$MANIFEST_PATH" dump --format shell)"; then err "Unable to load manifest metadata" fi local eval_script eval_script="$(echo "$MODULE_SHELL_STATE" | sed 's/^declare -A /declare -gA /')" eval "$eval_script" IFS=' ' read -r -a MODULES_COMPILE_LIST <<< "${MODULES_COMPILE:-}" if [ "${#MODULES_COMPILE_LIST[@]}" -eq 1 ] && [ -z "${MODULES_COMPILE_LIST[0]}" ]; then MODULES_COMPILE_LIST=() fi } remove_disabled_modules(){ for key in "${MODULE_KEYS[@]}"; do local dir dir="${MODULE_NAME[$key]:-}" [ -n "$dir" ] || continue if [ "${MODULE_ENABLED[$key]:-0}" != "1" ] && [ -d "$dir" ]; then info "Removing ${dir} (disabled)" rm -rf "$dir" fi done } run_post_install_hooks(){ local key="$1" local dir="$2" local hooks_csv="${MODULE_POST_INSTALL[$key]:-}" # Skip if no hooks defined [ -n "$hooks_csv" ] || return 0 IFS=',' read -r -a hooks <<< "$hooks_csv" local -a hook_search_paths=( "$SCRIPT_DIR/hooks" "/tmp/scripts/hooks" "/scripts/hooks" ) for hook in "${hooks[@]}"; do [ -n "$hook" ] || continue # Trim whitespace hook="$(echo "$hook" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" local hook_script="" local candidate for candidate in "${hook_search_paths[@]}"; do if [ -x "$candidate/$hook" ]; then hook_script="$candidate/$hook" break fi done if [ -n "$hook_script" ]; then info "Running post-install hook: $hook" # Set hook environment variables export MODULE_KEY="$key" export MODULE_DIR="$dir" export MODULE_NAME="${MODULE_NAME[$key]:-$(basename "$dir")}" export MODULES_ROOT="${MODULES_ROOT:-/modules}" export LUA_SCRIPTS_TARGET="/azerothcore/lua_scripts" # Execute the hook script if "$hook_script"; then ok "Hook '$hook' completed successfully" else local exit_code=$? case $exit_code in 1) warn "Hook '$hook' completed with warnings" ;; *) err "Hook '$hook' failed with exit code $exit_code" ;; esac fi # Clean up environment unset MODULE_KEY MODULE_DIR MODULE_NAME MODULES_ROOT LUA_SCRIPTS_TARGET else err "Hook script not found for ${hook} (searched: ${hook_search_paths[*]})" fi done } install_enabled_modules(){ for key in "${MODULE_KEYS[@]}"; do if [ "${MODULE_ENABLED[$key]:-0}" != "1" ]; then continue fi local dir repo ref dir="${MODULE_NAME[$key]:-}" repo="${MODULE_REPO[$key]:-}" ref="${MODULE_REF[$key]:-}" if [ -z "$dir" ] || [ -z "$repo" ]; then warn "Missing repository metadata for $key" continue fi if [ -d "$dir/.git" ]; then info "$dir already present; skipping clone" elif [ -d "$dir" ]; then warn "$dir exists but is not a git repository; leaving in place" else info "Cloning ${dir} from ${repo}" if ! git clone "$repo" "$dir"; then err "Failed to clone $repo" fi if [ -n "$ref" ]; then (cd "$dir" && git checkout "$ref") || warn "Unable to checkout ref $ref for $dir" fi fi run_post_install_hooks "$key" "$dir" done } update_playerbots_db_info(){ local target="$1" if [ ! -f "$target" ]; then return 0 fi local host host="$(read_env_value CONTAINER_MYSQL)" if [ -z "$host" ]; then host="$(read_env_value MYSQL_HOST)" fi host="${host:-ac-mysql}" local port port="$(read_env_value MYSQL_PORT "3306")" local user user="$(read_env_value MYSQL_USER "root")" local pass pass="$(read_env_value MYSQL_ROOT_PASSWORD)" local db db="$(read_env_value DB_PLAYERBOTS_NAME "acore_playerbots")" local value="${host};${port};${user};${pass};${db}" if grep -qE '^[[:space:]]*PlayerbotsDatabaseInfo[[:space:]]*=' "$target"; then sed -i "s|^[[:space:]]*PlayerbotsDatabaseInfo[[:space:]]*=.*|PlayerbotsDatabaseInfo = \"${value}\"|" "$target" || return else printf '\nPlayerbotsDatabaseInfo = "%s"\n' "$value" >> "$target" || return fi if [ "$PLAYERBOTS_DB_UPDATE_LOGGED" = "0" ]; then info "Updated PlayerbotsDatabaseInfo to use host ${host}:${port}" PLAYERBOTS_DB_UPDATE_LOGGED=1 fi return 0 } manage_configuration_files(){ echo 'Managing configuration files...' local env_target="${MODULES_ENV_TARGET_DIR:-}" if [ -z "$env_target" ]; then if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then env_target="${MODULES_ROOT}/env/dist/etc" else env_target="/azerothcore/env/dist/etc" fi fi mkdir -p "$env_target" local key patterns_csv enabled pattern for key in "${MODULE_KEYS[@]}"; do enabled="${MODULE_ENABLED[$key]:-0}" patterns_csv="${MODULE_CONFIG_CLEANUP[$key]:-}" IFS=',' read -r -a patterns <<< "$patterns_csv" if [ "${#patterns[@]}" -eq 1 ] && [ -z "${patterns[0]}" ]; then unset patterns continue fi for pattern in "${patterns[@]}"; do [ -n "$pattern" ] || continue if [ "$enabled" != "1" ]; then rm -f "$env_target"/$pattern 2>/dev/null || true fi done unset patterns done local module_dir for key in "${MODULE_KEYS[@]}"; do module_dir="${MODULE_NAME[$key]:-}" [ -n "$module_dir" ] || continue [ -d "$module_dir" ] || continue find "$module_dir" -name "*.conf.dist" -exec cp {} "$env_target"/ \; 2>/dev/null || true done local modules_conf_dir="${env_target%/}/modules" mkdir -p "$modules_conf_dir" rm -f "$modules_conf_dir"/*.conf "$modules_conf_dir"/*.conf.dist 2>/dev/null || true for key in "${MODULE_KEYS[@]}"; do module_dir="${MODULE_NAME[$key]:-}" [ -n "$module_dir" ] || continue [ -d "$module_dir" ] || continue while IFS= read -r conf_file; do [ -n "$conf_file" ] || continue base_name="$(basename "$conf_file")" dest_name="${base_name%.dist}" cp "$conf_file" "$modules_conf_dir/$dest_name" done < <(find "$module_dir" -path "*/conf/*" -type f \( -name "*.conf" -o -name "*.conf.dist" \) 2>/dev/null) done local playerbots_enabled="${MODULE_PLAYERBOTS:-0}" if [ "${MODULE_ENABLED[MODULE_PLAYERBOTS]:-0}" = "1" ]; then playerbots_enabled=1 fi if [ "$playerbots_enabled" = "1" ]; then update_playerbots_db_info "$env_target/playerbots.conf" update_playerbots_db_info "$env_target/playerbots.conf.dist" update_playerbots_db_info "$modules_conf_dir/playerbots.conf" update_playerbots_db_info "$modules_conf_dir/playerbots.conf.dist" fi if [ "${MODULE_AUTOBALANCE:-0}" = "1" ] && [ -f "$env_target/AutoBalance.conf.dist" ]; then sed -i 's/^AutoBalance\.LevelScaling\.EndGameBoost.*/AutoBalance.LevelScaling.EndGameBoost = false # disabled pending proper implementation/' \ "$env_target/AutoBalance.conf.dist" || true fi } load_sql_helper(){ local helper_paths=( "/scripts/manage-modules-sql.sh" "/tmp/scripts/manage-modules-sql.sh" ) if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then helper_paths+=("$SCRIPT_DIR/manage-modules-sql.sh") fi local helper_path="" for helper_path in "${helper_paths[@]}"; do if [ -f "$helper_path" ]; then # shellcheck disable=SC1090 . "$helper_path" SQL_HELPER_PATH="$helper_path" return 0 fi done err "SQL helper not found; expected manage-modules-sql.sh to be available" } execute_module_sql(){ SQL_EXECUTION_FAILED=0 if declare -f execute_module_sql_scripts >/dev/null 2>&1; then echo 'Executing module SQL scripts...' if execute_module_sql_scripts; then echo 'SQL execution complete.' else echo '⚠️ Module SQL scripts reported errors' SQL_EXECUTION_FAILED=1 fi else info "SQL helper did not expose execute_module_sql_scripts; skipping module SQL execution" fi } track_module_state(){ echo 'Checking for module changes that require rebuild...' local modules_state_file if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then modules_state_file="./.modules_state" else modules_state_file="/modules/.modules_state" fi local current_state="" for key in "${MODULE_KEYS[@]}"; do current_state+="${key}=${MODULE_ENABLED[$key]:-0}|" done local previous_state="" if [ -f "$modules_state_file" ]; then previous_state="$(cat "$modules_state_file")" fi local rebuild_required=0 if [ "$current_state" != "$previous_state" ]; then if [ -n "$previous_state" ]; then echo "🔄 Module configuration has changed - rebuild required" else echo "📝 First run - establishing module state baseline" fi rebuild_required=1 else echo "✅ No module changes detected" fi echo "$current_state" > "$modules_state_file" if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then echo "🔧 Detected ${#MODULES_COMPILE_LIST[@]} enabled C++ modules requiring compilation:" for mod in "${MODULES_COMPILE_LIST[@]}"; do echo " • $mod" done else echo "✅ No C++ modules enabled - pre-built containers can be used" fi local rebuild_sentinel if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then if [ -n "${LOCAL_STORAGE_SENTINEL_PATH:-}" ]; then rebuild_sentinel="${LOCAL_STORAGE_SENTINEL_PATH}" else rebuild_sentinel="./.requires_rebuild" fi else rebuild_sentinel="/modules/.requires_rebuild" fi local host_rebuild_sentinel="" if [ -n "${MODULES_HOST_DIR:-}" ]; then host_rebuild_sentinel="${MODULES_HOST_DIR%/}/.requires_rebuild" fi if [ "$rebuild_required" = "1" ] && [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then printf '%s\n' "${MODULES_COMPILE_LIST[@]}" > "$rebuild_sentinel" if [ -n "$host_rebuild_sentinel" ]; then printf '%s\n' "${MODULES_COMPILE_LIST[@]}" > "$host_rebuild_sentinel" 2>/dev/null || true fi echo "🚨 Module changes detected; run ./scripts/rebuild-with-modules.sh to rebuild source images." else rm -f "$rebuild_sentinel" 2>/dev/null || true if [ -n "$host_rebuild_sentinel" ]; then rm -f "$host_rebuild_sentinel" 2>/dev/null || true fi fi if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then local target_dir="${MODULES_HOST_DIR:-$(pwd)}" local desired_user desired_user="$(id -u):$(id -g)" if [ -d "$target_dir" ]; then chown -R "$desired_user" "$target_dir" >/dev/null 2>&1 || true chmod -R ug+rwX "$target_dir" >/dev/null 2>&1 || true fi fi } main(){ ensure_python if [ "${MODULES_LOCAL_RUN:-0}" != "1" ]; then cd /modules || err "Modules directory /modules not found" fi MODULES_ROOT="$(pwd)" MANIFEST_PATH="$(resolve_manifest_path)" STATE_DIR="${MODULES_HOST_DIR:-$MODULES_ROOT}" setup_git_config generate_module_state remove_disabled_modules install_enabled_modules manage_configuration_files info "SQL execution gate: MODULES_SKIP_SQL=${MODULES_SKIP_SQL:-0}" if [ "${MODULES_SKIP_SQL:-0}" = "1" ]; then info "Skipping module SQL execution (MODULES_SKIP_SQL=1)" else info "Initiating module SQL helper" load_sql_helper info "SQL helper loaded from ${SQL_HELPER_PATH:-unknown}" execute_module_sql fi track_module_state if [ "${SQL_EXECUTION_FAILED:-0}" = "1" ]; then warn "Module SQL execution reported issues; review logs above." fi echo 'Module management complete.' if [ "${MODULES_DEBUG_KEEPALIVE:-0}" = "1" ]; then tail -f /dev/null fi } main "$@"