#!/bin/bash # # AzerothCore Build Script # Handles all module compilation and image building for custom configurations # set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_PATH="$ROOT_DIR/.env" TEMPLATE_PATH="$ROOT_DIR/.env.template" source "$ROOT_DIR/scripts/bash/project_name.sh" # Default project name (read from .env or template) DEFAULT_PROJECT_NAME="$(project_name::resolve "$ENV_PATH" "$TEMPLATE_PATH")" ASSUME_YES=0 FORCE_REBUILD=0 SKIP_SOURCE_SETUP=0 CUSTOM_SOURCE_PATH="" BLUE='\033[0;34m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' info(){ printf '%b\n' "${BLUE}ℹ️ $*${NC}"; } ok(){ printf '%b\n' "${GREEN}✅ $*${NC}"; } warn(){ printf '%b\n' "${YELLOW}⚠️ $*${NC}"; } err(){ printf '%b\n' "${RED}❌ $*${NC}"; } show_build_header(){ printf '\n%b\n' "${BLUE}🔨 AZEROTHCORE BUILD SYSTEM 🔨${NC}" printf '%b\n' "${BLUE}═══════════════════════════════════${NC}" printf '%b\n\n' "${BLUE}⚒️ Forging Your Custom Realm ⚒️${NC}" } usage(){ cat </dev/null 2>&1 || { err "Missing required command: $1"; exit 1; } } require_cmd docker require_cmd python3 read_env(){ local key="$1" default="${2:-}" local value="" if [ -f "$ENV_PATH" ]; then value="$(grep -E "^${key}=" "$ENV_PATH" 2>/dev/null | tail -n1 | cut -d'=' -f2- | tr -d '\r' | sed 's/[[:space:]]*#.*//' | sed 's/[[:space:]]*$//')" fi if [ -z "$value" ]; then value="$default" fi echo "$value" } update_env_value(){ local key="$1" value="$2" env_file="$ENV_PATH" [ -n "$env_file" ] || return 0 if [ ! -f "$env_file" ]; then printf '%s=%s\n' "$key" "$value" >> "$env_file" return 0 fi if grep -q "^${key}=" "$env_file"; then sed -i "s|^${key}=.*|${key}=${value}|" "$env_file" else printf '\n%s=%s\n' "$key" "$value" >> "$env_file" fi } MODULE_HELPER="$ROOT_DIR/scripts/python/modules.py" MODULE_STATE_INITIALIZED=0 declare -a MODULES_COMPILE_LIST=() resolve_local_storage_path(){ local local_root local_root="$(read_env STORAGE_PATH_LOCAL "./local-storage")" if [[ "$local_root" != /* ]]; then local_root="${local_root#./}" local_root="$ROOT_DIR/$local_root" fi echo "${local_root%/}" } generate_module_state(){ local storage_root storage_root="$(resolve_local_storage_path)" local output_dir="${storage_root}/modules" ensure_modules_dir_writable "$storage_root" # Capture output and exit code from module validation local validation_output local validation_exit_code validation_output=$(python3 "$MODULE_HELPER" --env-path "$ENV_PATH" --manifest "$ROOT_DIR/config/module-manifest.json" generate --output-dir "$output_dir" 2>&1) validation_exit_code=$? # Display the validation output echo "$validation_output" # Check for validation errors (not warnings) if [ $validation_exit_code -ne 0 ]; then err "Module manifest validation failed. See errors above." exit 1 fi # Check if blocked modules were detected in warnings if echo "$validation_output" | grep -q "is blocked:"; then # Gather blocked module keys for display local blocked_modules blocked_modules=$(echo "$validation_output" | grep -oE 'MODULE_[A-Za-z0-9_]+' | sort -u | tr '\n' ' ') # Blocked modules detected - show warning and ask for confirmation echo warn "════════════════════════════════════════════════════════════════" warn "⚠️ BLOCKED MODULES DETECTED ⚠️" warn "════════════════════════════════════════════════════════════════" if [ -n "$blocked_modules" ]; then warn "Affected modules: ${blocked_modules}" fi warn "Some enabled modules are marked as blocked due to compatibility" warn "issues. These modules will be SKIPPED during the build process." warn "" warn "To permanently fix this, disable these modules in your .env file" warn "by setting them to 0 (e.g., MODULE_POCKET_PORTAL=0)" warn "" warn "If you believe this is an error, please file an issue on GitHub:" warn "https://github.com/uprightbass360/AzerothCore-RealmMaster/issues" warn "════════════════════════════════════════════════════════════════" echo if [ "$ASSUME_YES" -eq 1 ]; then warn "Auto-confirming due to --yes flag. Continuing with blocked modules skipped..." else if [ -t 0 ]; then local reply read -r -p "Continue with build (blocked modules will be skipped)? [y/N]: " reply reply="${reply:-n}" case "$reply" in [Yy]*) info "Continuing with build, blocked modules will be skipped..." ;; *) info "Build cancelled." exit 1 ;; esac else err "Non-interactive mode requires --yes flag to proceed with blocked modules." exit 1 fi fi fi if [ ! -f "${output_dir}/modules.env" ]; then err "modules.env not produced by helper at ${output_dir}/modules.env" exit 1 fi # shellcheck disable=SC1090 source "${output_dir}/modules.env" MODULE_STATE_INITIALIZED=1 MODULES_COMPILE_LIST=() 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 } requires_playerbot_source(){ if [ "$MODULE_STATE_INITIALIZED" -ne 1 ]; then generate_module_state fi [ "${MODULES_REQUIRES_PLAYERBOT_SOURCE:-0}" = "1" ] } ensure_source_repo(){ local use_playerbot_source=0 if requires_playerbot_source; then use_playerbot_source=1 fi local local_root local_root="$(read_env STORAGE_PATH_LOCAL "./local-storage")" local_root="${local_root%/}" [ -z "$local_root" ] && local_root="." local default_source="${local_root}/source/azerothcore" if [ "$use_playerbot_source" = "1" ]; then default_source="${local_root}/source/azerothcore-playerbots" fi local src_path if [ -n "$CUSTOM_SOURCE_PATH" ]; then src_path="$CUSTOM_SOURCE_PATH" else src_path="$(read_env MODULES_REBUILD_SOURCE_PATH "$default_source")" fi if [[ "$src_path" != /* ]]; then src_path="$ROOT_DIR/$src_path" fi # Normalize path (extracted from deploy.sh) if command -v readlink >/dev/null 2>&1 && [[ -e "$src_path" || -e "$(dirname "$src_path")" ]]; then src_path="$(readlink -f "$src_path" 2>/dev/null || echo "$src_path")" else src_path="$(cd "$ROOT_DIR" && realpath -m "$src_path" 2>/dev/null || echo "$src_path")" fi src_path="${src_path//\/.\//\/}" if [ -d "$src_path/.git" ]; then if [ "${FORCE_UPDATE:-0}" = "1" ]; then info "Force update requested - updating source repository to latest" >&2 if ! (cd "$ROOT_DIR" && ./scripts/bash/setup-source.sh) >&2; then err "Failed to update source repository" >&2 exit 1 fi fi echo "$src_path" return fi if [ "$SKIP_SOURCE_SETUP" = "1" ]; then err "Source repository not found at $src_path and --skip-source-setup specified" exit 1 fi warn "AzerothCore source not found at $src_path; running setup-source.sh" >&2 if ! (cd "$ROOT_DIR" && ./scripts/bash/setup-source.sh) >&2; then err "Failed to setup source repository" >&2 exit 1 fi # Verify the source was actually created if [ ! -d "$src_path/.git" ]; then err "Source repository setup failed - no git directory at $src_path" >&2 exit 1 fi echo "$src_path" } show_client_data_requirement(){ local repo_path="$1" local detector="$ROOT_DIR/scripts/bash/detect-client-data-version.sh" if [ ! -x "$detector" ]; then return fi local detection if ! detection="$("$detector" --no-header "$repo_path" 2>/dev/null | head -n1)"; then warn "Could not detect client data version for $repo_path" return fi local detected_repo raw_version normalized_version IFS=$'\t' read -r detected_repo raw_version normalized_version <<< "$detection" if [ -z "$normalized_version" ] || [ "$normalized_version" = "" ]; then warn "Could not detect client data version for $repo_path" return fi local env_value env_value="$(read_env CLIENT_DATA_VERSION)" if [ -n "$env_value" ] && [ "$env_value" != "$normalized_version" ]; then warn "Source at $repo_path expects client data ${normalized_version} (raw ${raw_version}) but .env specifies ${env_value}. Update CLIENT_DATA_VERSION to avoid mismatched maps." elif [ -n "$env_value" ]; then info "Client data requirement satisfied: ${normalized_version} (raw ${raw_version})" else info "Detected client data requirement: ${normalized_version} (raw ${raw_version}). Set CLIENT_DATA_VERSION in .env to avoid mismatches." fi } # Build state detection (extracted from setup.sh and deploy.sh) modules_need_rebuild(){ local storage_path storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")" if [[ "$storage_path" != /* ]]; then # Remove leading ./ if present storage_path="${storage_path#./}" storage_path="$ROOT_DIR/$storage_path" fi local sentinel="$storage_path/modules/.requires_rebuild" [[ -f "$sentinel" ]] } detect_rebuild_reasons(){ local reasons=() # Check sentinel file if modules_need_rebuild; then reasons+=("Module changes detected (sentinel file present)") fi # Check if source repository is freshly cloned (no previous build state) local storage_path storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")" if [[ "$storage_path" != /* ]]; then storage_path="$ROOT_DIR/$storage_path" fi local last_deployed="$storage_path/modules/.last_deployed" if [ ! -f "$last_deployed" ]; then reasons+=("Fresh source repository setup - initial build required") fi # Check if any C++ modules are enabled but modules-latest images don't exist if [ "$MODULE_STATE_INITIALIZED" -ne 1 ]; then generate_module_state fi local any_cxx_modules=0 if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then any_cxx_modules=1 fi if [ "$any_cxx_modules" = "1" ]; then local authserver_modules_image local worldserver_modules_image authserver_modules_image="$(read_env AC_AUTHSERVER_IMAGE_MODULES "$(resolve_project_image "authserver-modules-latest")")" worldserver_modules_image="$(read_env AC_WORLDSERVER_IMAGE_MODULES "$(resolve_project_image "worldserver-modules-latest")")" if ! docker image inspect "$authserver_modules_image" >/dev/null 2>&1; then reasons+=("C++ modules enabled but authserver modules image $authserver_modules_image is missing") fi if ! docker image inspect "$worldserver_modules_image" >/dev/null 2>&1; then reasons+=("C++ modules enabled but worldserver modules image $worldserver_modules_image is missing") fi fi if [ ${#reasons[@]} -gt 0 ]; then printf '%s\n' "${reasons[@]}" fi } confirm_build(){ local reasons=("$@") if [ ${#reasons[@]} -eq 0 ] && [ "$FORCE_REBUILD" = "0" ]; then info "No build required - all images are up to date" if [ "$ASSUME_YES" -ne 1 ] && [ -t 0 ]; then local reply read -r -p "Build anyway? [y/N]: " reply reply="${reply:-n}" case "$reply" in [Yy]*) return 0 ;; # Proceed with build *) return 1 ;; # Skip build esac else return 1 # No build needed (non-interactive or --yes flag) fi fi # Skip duplicate output if called from deploy.sh (reasons already shown) local show_reasons=1 if [ "$ASSUME_YES" -eq 1 ] && [ ${#reasons[@]} -gt 0 ]; then show_reasons=0 # deploy.sh already showed the reasons fi if [ "$show_reasons" -eq 1 ]; then echo if [ "$FORCE_REBUILD" = "1" ]; then warn "Force rebuild requested (--force flag)" elif [ ${#reasons[@]} -gt 0 ]; then warn "Build appears to be required:" local reason for reason in "${reasons[@]}"; do warn " • $reason" done fi echo fi # Skip prompt if --yes flag is provided if [ "$ASSUME_YES" -eq 1 ]; then info "Auto-confirming build (--yes supplied)." return 0 fi # Interactive prompt info "This will rebuild AzerothCore from source with your enabled modules." warn "⏱️ This process typically takes 15-45 minutes depending on your system." echo if [ -t 0 ]; then local reply read -r -p "Proceed with build? [y/N]: " reply reply="${reply:-n}" case "$reply" in [Yy]*) info "Build confirmed." return 0 ;; *) info "Build cancelled." return 1 ;; esac else warn "Standard input is not interactive; use --yes to auto-confirm." return 1 fi } # Module staging logic (extracted from setup.sh) sync_modules(){ local storage_path storage_path="$(resolve_local_storage_path)" mkdir -p "$storage_path/modules" info "Using local module staging at $storage_path/modules" } resolve_project_name(){ local raw_name="$(read_env COMPOSE_PROJECT_NAME "$DEFAULT_PROJECT_NAME")" project_name::sanitize "$raw_name" } ensure_modules_dir_writable(){ local base_path="$1" local modules_dir="${base_path%/}/modules" ensure_host_writable "$modules_dir" } ensure_host_writable(){ local target="$1" [ -n "$target" ] || return 0 if [ -d "$target" ] || mkdir -p "$target" 2>/dev/null; then local uid gid uid="$(id -u)" gid="$(id -g)" if ! chown -R "$uid":"$gid" "$target" 2>/dev/null; then if command -v docker >/dev/null 2>&1; then local helper_image helper_image="$(read_env ALPINE_IMAGE "alpine:latest")" docker run --rm \ -u 0:0 \ -v "$target":/workspace \ "$helper_image" \ sh -c "chown -R ${uid}:${gid} /workspace" >/dev/null 2>&1 || true fi fi chmod -R u+rwX "$target" 2>/dev/null || true fi } resolve_project_image(){ local tag="$1" local project_name project_name="$(resolve_project_name)" echo "${project_name}:${tag}" } stage_modules(){ local src_path="$1" local storage_path storage_path="$(resolve_local_storage_path)" if [ -z "${MODULES_ENABLED:-}" ]; then generate_module_state fi info "Staging modules to source directory: $src_path/modules" # Verify source path exists if [ ! -d "$src_path" ]; then err "Source path does not exist: $src_path" return 1 fi local local_modules_dir="${src_path}/modules" mkdir -p "$local_modules_dir" ensure_host_writable "$local_modules_dir" local staging_modules_dir="${storage_path}/modules" export MODULES_HOST_DIR="$staging_modules_dir" ensure_host_writable "$staging_modules_dir" local env_target_dir="$src_path/env/dist/etc" mkdir -p "$env_target_dir" export MODULES_ENV_TARGET_DIR="$env_target_dir" ensure_host_writable "$env_target_dir" local lua_target_dir="$src_path/lua_scripts" mkdir -p "$lua_target_dir" export MODULES_LUA_TARGET_DIR="$lua_target_dir" ensure_host_writable "$lua_target_dir" # Set up local storage path for build sentinel tracking local local_storage_path local_storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")" if [[ "$local_storage_path" != /* ]]; then # Remove leading ./ if present local_storage_path="${local_storage_path#./}" local_storage_path="$ROOT_DIR/$local_storage_path" fi export LOCAL_STORAGE_SENTINEL_PATH="$local_storage_path/modules/.requires_rebuild" # Prepare isolated git config for the module script local prev_git_config_global="${GIT_CONFIG_GLOBAL:-}" local git_temp_config="" if command -v mktemp >/dev/null 2>&1; then if ! git_temp_config="$(mktemp)"; then git_temp_config="" fi fi if [ -z "$git_temp_config" ]; then git_temp_config="$local_modules_dir/.gitconfig.tmp" : > "$git_temp_config" fi export GIT_CONFIG_GLOBAL="$git_temp_config" # Run module staging script in local modules directory export MODULES_LOCAL_RUN=1 export MODULES_SKIP_SQL=1 if [ -n "$staging_modules_dir" ]; then mkdir -p "$staging_modules_dir" rm -f "$staging_modules_dir/.modules_state" "$staging_modules_dir/.requires_rebuild" 2>/dev/null || true fi # Export environment variables needed by module hooks export STACK_SOURCE_VARIANT="$(read_env STACK_SOURCE_VARIANT "core")" export MODULES_REBUILD_SOURCE_PATH="$(read_env MODULES_REBUILD_SOURCE_PATH "")" if ! (cd "$local_modules_dir" && bash "$ROOT_DIR/scripts/bash/manage-modules.sh"); then err "Module staging failed; aborting build" return 1 fi ok "Module repositories staged to $local_modules_dir" if [ -n "$staging_modules_dir" ]; then if command -v rsync >/dev/null 2>&1; then rsync -a --delete \ --exclude '.modules_state' \ --exclude '.requires_rebuild' \ --exclude 'modules.env' \ --exclude 'modules-state.json' \ --exclude 'modules-compile.txt' \ --exclude 'modules-enabled.txt' \ "$local_modules_dir"/ "$staging_modules_dir"/ else find "$staging_modules_dir" -mindepth 1 -maxdepth 1 \ ! -name '.modules_state' \ ! -name '.requires_rebuild' \ ! -name 'modules.env' \ ! -name 'modules-state.json' \ ! -name 'modules-compile.txt' \ ! -name 'modules-enabled.txt' \ -exec rm -rf {} + 2>/dev/null || true (cd "$local_modules_dir" && tar cf - --exclude='.modules_state' --exclude='.requires_rebuild' .) | (cd "$staging_modules_dir" && tar xf -) fi if [ -f "$local_modules_dir/.modules_state" ]; then cp "$local_modules_dir/.modules_state" "$staging_modules_dir/.modules_state" 2>/dev/null || true fi fi # Cleanup export GIT_CONFIG_GLOBAL="$prev_git_config_global" unset MODULES_LOCAL_RUN unset MODULES_SKIP_SQL unset MODULES_HOST_DIR [ -n "$git_temp_config" ] && [ -f "$git_temp_config" ] && rm -f "$git_temp_config" } # Build execution (extracted from setup.sh) execute_build(){ local src_path="$1" # Verify source path exists if [ ! -d "$src_path" ]; then err "Source path does not exist: $src_path" return 1 fi local compose_file="$src_path/docker-compose.yml" if [ ! -f "$compose_file" ]; then err "Source docker-compose.yml missing at $compose_file" return 1 fi info "Building AzerothCore with modules (this may take a while)" docker compose -f "$compose_file" down --remove-orphans >/dev/null 2>&1 || true if (cd "$ROOT_DIR" && ./scripts/bash/rebuild-with-modules.sh --yes --source "$src_path"); then ok "Source build completed" else err "Source build failed" return 1 fi docker compose -f "$compose_file" down --remove-orphans >/dev/null 2>&1 || true } # Image tagging (extracted from setup.sh and deploy.sh) tag_module_images(){ local source_auth local source_world local target_auth local target_world source_auth="$(read_env AC_AUTHSERVER_IMAGE_PLAYERBOTS "$(resolve_project_image "authserver-playerbots")")" source_world="$(read_env AC_WORLDSERVER_IMAGE_PLAYERBOTS "$(resolve_project_image "worldserver-playerbots")")" target_auth="$(read_env AC_AUTHSERVER_IMAGE_MODULES "$(resolve_project_image "authserver-modules-latest")")" target_world="$(read_env AC_WORLDSERVER_IMAGE_MODULES "$(resolve_project_image "worldserver-modules-latest")")" if docker image inspect "$source_auth" >/dev/null 2>&1; then if docker tag "$source_auth" "$target_auth"; then ok "Tagged $target_auth from $source_auth" update_env_value "AC_AUTHSERVER_IMAGE_PLAYERBOTS" "$source_auth" update_env_value "AC_AUTHSERVER_IMAGE_MODULES" "$target_auth" else warn "Failed to tag $target_auth from $source_auth" fi else warn "Source authserver image $source_auth not found; skipping modules tag" fi if docker image inspect "$source_world" >/dev/null 2>&1; then if docker tag "$source_world" "$target_world"; then ok "Tagged $target_world from $source_world" update_env_value "AC_WORLDSERVER_IMAGE_PLAYERBOTS" "$source_world" update_env_value "AC_WORLDSERVER_IMAGE_MODULES" "$target_world" else warn "Failed to tag $target_world from $source_world" fi else warn "Source worldserver image $source_world not found; skipping modules tag" fi } show_build_complete(){ printf '\n%b\n' "${GREEN}🔨 Build Complete! 🔨${NC}" printf '%b\n' "${GREEN}⚒️ Your custom AzerothCore images are ready${NC}" printf '%b\n\n' "${GREEN}🚀 Ready for deployment with ./deploy.sh${NC}" } main(){ show_build_header local src_dir local rebuild_reasons info "Preparing module manifest metadata" generate_module_state info "Step 1/6: Setting up source repository" src_dir="$(ensure_source_repo)" show_client_data_requirement "$src_dir" info "Step 2/6: Detecting build requirements" readarray -t rebuild_reasons < <(detect_rebuild_reasons) if ! confirm_build "${rebuild_reasons[@]}"; then info "Build cancelled or not required." exit 0 fi info "Step 3/6: Syncing modules to container storage" sync_modules info "Step 4/6: Staging modules to source directory" stage_modules "$src_dir" info "Step 5/6: Building AzerothCore with modules" execute_build "$src_dir" info "Step 6/6: Tagging images for deployment" tag_module_images # Clear build sentinel after successful build local storage_path storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")" if [[ "$storage_path" != /* ]]; then # Remove leading ./ if present storage_path="${storage_path#./}" storage_path="$ROOT_DIR/$storage_path" fi local sentinel="$storage_path/modules/.requires_rebuild" rm -f "$sentinel" 2>/dev/null || true show_build_complete } main "$@"