feat: add mysql exposure toggle and client data bind

This commit is contained in:
uprightbass360
2025-11-07 20:56:00 -05:00
committed by Deckard
parent ce02a547ee
commit d99dad025a
18 changed files with 431 additions and 183 deletions

View File

@@ -5,8 +5,9 @@
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
COMPOSE_FILE="$ROOT_DIR/docker-compose.yml"
DEFAULT_COMPOSE_FILE="$ROOT_DIR/docker-compose.yml"
ENV_FILE="$ROOT_DIR/.env"
declare -a COMPOSE_FILE_ARGS=()
BLUE='\033[0;34m'
GREEN='\033[0;32m'
@@ -44,8 +45,22 @@ resolve_project_name(){
echo "$sanitized"
}
init_compose_files(){
COMPOSE_FILE_ARGS=(-f "$DEFAULT_COMPOSE_FILE")
if [ "$(read_env MYSQL_EXPOSE_PORT "0")" = "1" ]; then
local extra_file="$ROOT_DIR/docker-compose.mysql-expose.yml"
if [ -f "$extra_file" ]; then
COMPOSE_FILE_ARGS+=(-f "$extra_file")
else
warn "MYSQL_EXPOSE_PORT=1 but $extra_file missing; skipping port override."
fi
fi
}
init_compose_files
compose(){
docker compose --project-name "$PROJECT_NAME" -f "$COMPOSE_FILE" "$@"
docker compose --project-name "$PROJECT_NAME" "${COMPOSE_FILE_ARGS[@]}" "$@"
}
show_header(){

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env bash
#
# Detect which wowgaming/client-data release an AzerothCore checkout expects.
# Currently inspects apps/installer/includes/functions.sh for the
# inst_download_client_data version marker, but can be extended with new
# heuristics if needed.
set -euo pipefail
print_usage() {
cat <<'EOF'
Usage: scripts/detect-client-data-version.sh [--no-header] <repo-path> [...]
Outputs a tab-separated list of repository path, raw version token found in the
source tree, and a normalized CLIENT_DATA_VERSION (e.g., v18).
EOF
}
if [[ "${1:-}" == "--help" ]]; then
print_usage
exit 0
fi
show_header=1
if [[ "${1:-}" == "--no-header" ]]; then
show_header=0
shift
fi
if [[ $# -lt 1 ]]; then
print_usage >&2
exit 1
fi
normalize_version() {
local token="$1"
token="${token//$'\r'/}"
token="${token//\"/}"
token="${token//\'/}"
token="${token// /}"
token="${token%%#*}"
token="${token%%;*}"
token="${token%%\)*}"
token="${token%%\}*}"
echo "$token"
}
detect_from_installer() {
local repo_path="$1"
local installer_file="$repo_path/apps/installer/includes/functions.sh"
[[ -f "$installer_file" ]] || return 1
local raw
raw="$(grep -E 'local[[:space:]]+VERSION=' "$installer_file" | head -n1 | cut -d'=' -f2-)"
[[ -n "$raw" ]] || return 1
echo "$raw"
}
detect_version() {
local repo_path="$1"
if [[ ! -d "$repo_path" ]]; then
printf '%s\t%s\t%s\n' "$repo_path" "<missing>" "<unknown>"
return
fi
local raw=""
if raw="$(detect_from_installer "$repo_path")"; then
:
elif [[ -f "$repo_path/.env" ]]; then
raw="$(grep -E '^CLIENT_DATA_VERSION=' "$repo_path/.env" | head -n1 | cut -d'=' -f2-)"
fi
if [[ -z "$raw" ]]; then
printf '%s\t%s\t%s\n' "$repo_path" "<unknown>" "<unknown>"
return
fi
local normalized
normalized="$(normalize_version "$raw")"
printf '%s\t%s\t%s\n' "$repo_path" "$raw" "$normalized"
}
[[ "$show_header" -eq 0 ]] || printf 'repo\traw\tclient_data_version\n'
for repo in "$@"; do
detect_version "$repo"
done

View File

@@ -131,9 +131,10 @@ class ModuleCollectionState:
def requires_playerbot_source(self) -> bool:
module_map = {m.key: m for m in self.modules}
playerbots_enabled = module_map.get("MODULE_PLAYERBOTS")
playerbots = bool(playerbots_enabled and playerbots_enabled.enabled_effective)
needs_cpp = any(module.needs_build and module.enabled_effective for module in self.modules)
return playerbots or needs_cpp
return bool(playerbots_enabled and playerbots_enabled.enabled_effective)
def requires_custom_build(self) -> bool:
return any(module.needs_build and module.enabled_effective for module in self.modules)
def build_state(env_path: Path, manifest_path: Path) -> ModuleCollectionState:
@@ -150,8 +151,12 @@ def build_state(env_path: Path, manifest_path: Path) -> ModuleCollectionState:
key = entry["key"]
name = entry["name"]
repo = entry["repo"]
needs_build = bool(entry.get("needs_build", False))
module_type = str(entry.get("type", "cpp"))
needs_build_flag = entry.get("needs_build")
if needs_build_flag is None:
needs_build = module_type.lower() == "cpp"
else:
needs_build = bool(needs_build_flag)
requires = entry.get("requires") or []
if not isinstance(requires, list):
raise ValueError(f"Manifest entry {key} has non-list 'requires'")
@@ -263,20 +268,30 @@ def write_outputs(state: ModuleCollectionState, output_dir: Path) -> None:
enabled_names: List[str] = []
compile_names: List[str] = []
enabled_keys: List[str] = []
compile_keys: List[str] = []
for module in state.modules:
env_lines.append(f"export {module.key}={module.value}")
if module.enabled_effective:
enabled_names.append(module.name)
enabled_keys.append(module.key)
if module.enabled_effective and module.needs_build:
compile_names.append(module.name)
compile_keys.append(module.key)
env_lines.append(f'export MODULES_ENABLED="{ " ".join(enabled_names) }"'.rstrip())
env_lines.append(f'export MODULES_COMPILE="{ " ".join(compile_names) }"'.rstrip())
env_lines.append(f'export MODULES_ENABLED_LIST="{",".join(enabled_keys)}"')
env_lines.append(f'export MODULES_CPP_LIST="{",".join(compile_keys)}"')
env_lines.append(
f"export MODULES_REQUIRES_PLAYERBOT_SOURCE="
f'{"1" if state.requires_playerbot_source() else "0"}'
)
env_lines.append(
f"export MODULES_REQUIRES_CUSTOM_BUILD="
f'{"1" if state.requires_custom_build() else "0"}'
)
env_lines.append(f"export MODULES_WARNING_COUNT={len(state.warnings)}")
env_lines.append(f"export MODULES_ERROR_COUNT={len(state.errors)}")
@@ -301,6 +316,7 @@ def write_outputs(state: ModuleCollectionState, output_dir: Path) -> None:
"enabled_modules": [module.name for module in state.enabled_modules()],
"compile_modules": [module.name for module in state.compile_modules()],
"requires_playerbot_source": state.requires_playerbot_source(),
"requires_custom_build": state.requires_custom_build(),
}
modules_state_path = output_dir / "modules-state.json"
@@ -342,6 +358,11 @@ def print_requires_playerbot(state: ModuleCollectionState) -> None:
print("1" if state.requires_playerbot_source() else "0")
def print_requires_custom_build(state: ModuleCollectionState) -> None:
print("1" if state.requires_custom_build() else "0")
def print_state(state: ModuleCollectionState, fmt: str) -> None:
payload = {
"generated_at": state.generated_at.isoformat(),
@@ -485,6 +506,18 @@ def configure_parser() -> argparse.ArgumentParser:
rps_parser.set_defaults(func=handle_requires_playerbot)
rcb_parser = subparsers.add_parser(
"requires-custom-build",
help="Print 1 if a custom source build is required else 0",
)
def handle_requires_custom_build(args: argparse.Namespace) -> int:
state = build_state(Path(args.env_path).resolve(), Path(args.manifest).resolve())
print_requires_custom_build(state)
return 1 if state.errors else 0
rcb_parser.set_defaults(func=handle_requires_custom_build)
dump_parser = subparsers.add_parser("dump", help="Dump module state (JSON format)")
dump_parser.add_argument(
"--format",

View File

@@ -79,4 +79,18 @@ for path in /var/lib/mysql-runtime /var/lib/mysql /var/lib/mysql-persistent /bac
fi
done
disable_binlog="${MYSQL_DISABLE_BINLOG:-}"
if [ "${disable_binlog}" = "1" ]; then
add_skip_flag=1
for arg in "$@"; do
if [ "$arg" = "--skip-log-bin" ] || [[ "$arg" == --log-bin* ]]; then
add_skip_flag=0
break
fi
done
if [ "$add_skip_flag" -eq 1 ]; then
set -- "$@" --skip-log-bin
fi
fi
exec "$ORIGINAL_ENTRYPOINT" "$@"

View File

@@ -14,36 +14,55 @@ PROJECT_ROOT="$(pwd)"
# Default values
MODULE_PLAYERBOTS="${MODULE_PLAYERBOTS:-0}"
NEEDS_CXX_REBUILD="${NEEDS_CXX_REBUILD:-0}"
COMPILE_MODULE_KEYS=(
MODULE_AOE_LOOT MODULE_LEARN_SPELLS MODULE_FIREWORKS MODULE_INDIVIDUAL_PROGRESSION MODULE_AHBOT MODULE_AUTOBALANCE
MODULE_TRANSMOG MODULE_NPC_BUFFER MODULE_DYNAMIC_XP MODULE_SOLO_LFG MODULE_1V1_ARENA MODULE_PHASED_DUELS
MODULE_BREAKING_NEWS MODULE_BOSS_ANNOUNCER MODULE_ACCOUNT_ACHIEVEMENTS MODULE_AUTO_REVIVE MODULE_GAIN_HONOR_GUARD
MODULE_TIME_IS_TIME MODULE_POCKET_PORTAL MODULE_RANDOM_ENCHANTS MODULE_SOLOCRAFT MODULE_PVP_TITLES MODULE_NPC_BEASTMASTER
MODULE_NPC_ENCHANTER MODULE_INSTANCE_RESET MODULE_LEVEL_GRANT MODULE_ARAC MODULE_ASSISTANT MODULE_REAGENT_BANK
MODULE_CHALLENGE_MODES MODULE_OLLAMA_CHAT MODULE_PLAYER_BOT_LEVEL_BRACKETS MODULE_STATBOOSTER MODULE_DUNGEON_RESPAWN
MODULE_SKELETON_MODULE MODULE_BG_SLAVERYVALLEY MODULE_AZEROTHSHARD MODULE_WORGOBLIN
)
if [ "$NEEDS_CXX_REBUILD" != "1" ]; then
for key in "${COMPILE_MODULE_KEYS[@]}"; do
if [ "${!key:-0}" = "1" ]; then
NEEDS_CXX_REBUILD=1
break
PLAYERBOT_ENABLED="${PLAYERBOT_ENABLED:-0}"
STACK_SOURCE_VARIANT="${STACK_SOURCE_VARIANT:-}"
if [ -z "$STACK_SOURCE_VARIANT" ]; then
if [ "$MODULE_PLAYERBOTS" = "1" ] || [ "$PLAYERBOT_ENABLED" = "1" ]; then
STACK_SOURCE_VARIANT="playerbots"
else
STACK_SOURCE_VARIANT="core"
fi
done
fi
LOCAL_STORAGE_ROOT="${STORAGE_PATH_LOCAL:-./local-storage}"
DEFAULT_STANDARD_PATH="${LOCAL_STORAGE_ROOT%/}/source/azerothcore"
DEFAULT_PLAYERBOTS_PATH="${LOCAL_STORAGE_ROOT%/}/source/azerothcore-playerbots"
SOURCE_PATH_DEFAULT="$DEFAULT_STANDARD_PATH"
if [ "$MODULE_PLAYERBOTS" = "1" ] || [ "$NEEDS_CXX_REBUILD" = "1" ]; then
if [ "$STACK_SOURCE_VARIANT" = "playerbots" ]; then
SOURCE_PATH_DEFAULT="$DEFAULT_PLAYERBOTS_PATH"
fi
SOURCE_PATH="${MODULES_REBUILD_SOURCE_PATH:-$SOURCE_PATH_DEFAULT}"
show_client_data_requirement(){
local repo_path="$1"
local detector="$PROJECT_ROOT/scripts/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
echo "⚠️ 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" = "<unknown>" ]; then
echo "⚠️ Could not detect client data version for $repo_path"
return
fi
local env_value="${CLIENT_DATA_VERSION:-}"
if [ -n "$env_value" ] && [ "$env_value" != "$normalized_version" ]; then
echo "⚠️ Source requires 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
echo "📦 Client data requirement satisfied: ${normalized_version} (raw ${raw_version})"
else
echo " Detected client data requirement: ${normalized_version} (raw ${raw_version}). Set CLIENT_DATA_VERSION in .env to avoid mismatches."
fi
}
STORAGE_PATH_VALUE="${STORAGE_PATH:-./storage}"
if [[ "$STORAGE_PATH_VALUE" != /* ]]; then
STORAGE_PATH_ABS="$PROJECT_ROOT/${STORAGE_PATH_VALUE#./}"
@@ -72,8 +91,8 @@ ACORE_BRANCH_STANDARD="${ACORE_BRANCH_STANDARD:-master}"
ACORE_REPO_PLAYERBOTS="${ACORE_REPO_PLAYERBOTS:-https://github.com/mod-playerbots/azerothcore-wotlk.git}"
ACORE_BRANCH_PLAYERBOTS="${ACORE_BRANCH_PLAYERBOTS:-Playerbot}"
# Repository and branch selection based on playerbots mode
if [ "$MODULE_PLAYERBOTS" = "1" ] || [ "$NEEDS_CXX_REBUILD" = "1" ]; then
# Repository and branch selection based on source variant
if [ "$STACK_SOURCE_VARIANT" = "playerbots" ]; then
REPO_URL="$ACORE_REPO_PLAYERBOTS"
BRANCH="$ACORE_BRANCH_PLAYERBOTS"
echo "📌 Playerbots mode: Using $REPO_URL, branch $BRANCH"
@@ -130,5 +149,6 @@ echo "📊 Current status:"
echo " Branch: $CURRENT_BRANCH"
echo " Commit: $CURRENT_COMMIT"
echo " Last commit: $(git log -1 --pretty=format:'%s (%an, %ar)')"
show_client_data_requirement "$SOURCE_PATH"
echo '🎉 Source repository setup complete!'

View File

@@ -58,8 +58,13 @@ def cmd_metadata(manifest_path: str) -> None:
for entry in iter_modules(manifest):
key = entry["key"]
name = clean(entry.get("name", key))
needs_build = "1" if entry.get("needs_build") else "0"
module_type = clean(entry.get("type", ""))
module_type_raw = entry.get("type", "")
module_type = clean(module_type_raw)
needs_build_flag = entry.get("needs_build")
if needs_build_flag is None:
needs_build = "1" if str(module_type_raw).lower() == "cpp" else "0"
else:
needs_build = "1" if needs_build_flag else "0"
status = clean(entry.get("status", "active"))
block_reason = clean(entry.get("block_reason", ""))
requires = unique_preserve_order(entry.get("requires") or [])

View File

@@ -64,6 +64,8 @@ sync_local_staging(){
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
ENV_FILE="$PROJECT_DIR/.env"
DEFAULT_COMPOSE_FILE="$PROJECT_DIR/docker-compose.yml"
EXTRA_COMPOSE_FILE="$PROJECT_DIR/docker-compose.mysql-expose.yml"
usage(){
cat <<EOF
@@ -112,6 +114,19 @@ resolve_project_name(){
echo "$sanitized"
}
if [ -z "${COMPOSE_FILE:-}" ]; then
compose_files=("$DEFAULT_COMPOSE_FILE")
if [ "$(read_env MYSQL_EXPOSE_PORT "0")" = "1" ]; then
if [ -f "$EXTRA_COMPOSE_FILE" ]; then
compose_files+=("$EXTRA_COMPOSE_FILE")
else
echo "⚠️ MYSQL_EXPOSE_PORT=1 but ${EXTRA_COMPOSE_FILE} not found; continuing without port exposure override."
fi
fi
COMPOSE_FILE="$(IFS=:; echo "${compose_files[*]}")"
export COMPOSE_FILE
fi
resolve_project_image(){
local tag="$1"
local project_name

View File

@@ -10,7 +10,8 @@ ok(){ echo -e "${GREEN}✅ $*${NC}"; }
warn(){ echo -e "${YELLOW}⚠️ $*${NC}"; }
err(){ echo -e "${RED}$*${NC}"; }
COMPOSE_FILE="$(dirname "$0")/docker-compose.yml"
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_FILE="$PROJECT_DIR/docker-compose.yml"
ENV_FILE=""
PROFILES=(db services-standard client-data modules tools)
SKIP_DEPLOY=false
@@ -72,6 +73,15 @@ run_compose(){
compose_args+=(--env-file "$ENV_FILE")
fi
compose_args+=(-f "$COMPOSE_FILE")
if [ "$(read_env_value MYSQL_EXPOSE_PORT "0")" = "1" ]; then
local extra_file
extra_file="$(dirname "$COMPOSE_FILE")/docker-compose.mysql-expose.yml"
if [ -f "$extra_file" ]; then
compose_args+=(-f "$extra_file")
else
warn "MYSQL_EXPOSE_PORT=1 but ${extra_file} missing; skipping port exposure override."
fi
fi
docker compose "${compose_args[@]}" "$@"
}