diff options
author | Yehonal <yehonal.azeroth@gmail.com> | 2025-08-30 23:44:07 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-08-30 23:44:07 +0200 |
commit | 5a79a4edce0f39541d2e9b363dcbe0cc79c32a1e (patch) | |
tree | f0bb15f28881a000f17a8f6d1a74d72e93c6b84b | |
parent | 5c31e3b411ba8dfec9ecdfacca494accf7f59119 (diff) |
Feat/refactoring-module-menu (#22733)
-rw-r--r-- | .github/workflows/dashboard-ci.yml | 9 | ||||
-rwxr-xr-x | apps/compiler/test/test_compiler.bats | 6 | ||||
-rw-r--r-- | apps/installer/includes/functions.sh | 154 | ||||
-rw-r--r-- | apps/installer/includes/modules-manager/README.md | 188 | ||||
-rw-r--r-- | apps/installer/includes/modules-manager/modules.sh | 735 | ||||
-rw-r--r-- | apps/installer/main.sh | 81 | ||||
-rw-r--r-- | apps/installer/test/bats.conf | 14 | ||||
-rwxr-xr-x | apps/installer/test/test_module_commands.bats | 354 | ||||
-rw-r--r-- | conf/dist/config.sh | 13 | ||||
-rwxr-xr-x | deps/acore/joiner/joiner.sh | 36 |
10 files changed, 1399 insertions, 191 deletions
diff --git a/.github/workflows/dashboard-ci.yml b/.github/workflows/dashboard-ci.yml index 25843a6906..fbdd881246 100644 --- a/.github/workflows/dashboard-ci.yml +++ b/.github/workflows/dashboard-ci.yml @@ -42,7 +42,12 @@ jobs: - name: Install requirements run: | - sudo apt install -y bats + sudo apt-get update + # Install bats-core >= 1.5.0 to support bats_require_minimum_version + sudo apt-get install -y git curl + git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats-core + sudo /tmp/bats-core/install.sh /usr/local + bats --version ./acore.sh install-deps - name: Run bash script tests for ${{ matrix.test-module }} @@ -50,7 +55,7 @@ jobs: TERM: xterm-256color run: | cd apps/test-framework - ./run-tests.sh --tap + ./run-tests.sh --tap --all build-and-test: name: Build and Integration Test diff --git a/apps/compiler/test/test_compiler.bats b/apps/compiler/test/test_compiler.bats index ff217e6389..79152b68e5 100755 --- a/apps/compiler/test/test_compiler.bats +++ b/apps/compiler/test/test_compiler.bats @@ -1,7 +1,9 @@ #!/usr/bin/env bats -# Require minimum BATS version to avoid warnings -bats_require_minimum_version 1.5.0 +# Require minimum BATS version when supported (older distro packages lack this) +if type -t bats_require_minimum_version >/dev/null 2>&1; then + bats_require_minimum_version 1.5.0 +fi # AzerothCore Compiler Scripts Test Suite # Tests the functionality of the compiler scripts using the unified test framework diff --git a/apps/installer/includes/functions.sh b/apps/installer/includes/functions.sh index b4bd14caf5..eb9fe1a24e 100644 --- a/apps/installer/includes/functions.sh +++ b/apps/installer/includes/functions.sh @@ -118,142 +118,28 @@ function inst_allInOne() { inst_download_client_data } -function inst_getVersionBranch() { - local res="master" - local v="not-defined" - local MODULE_MAJOR=0 - local MODULE_MINOR=0 - local MODULE_PATCH=0 - local MODULE_SPECIAL=0; - local ACV_MAJOR=0 - local ACV_MINOR=0 - local ACV_PATCH=0 - local ACV_SPECIAL=0; - local curldata=$(curl -f --silent -H 'Cache-Control: no-cache' "$1" || echo "{}") - local parsed=$(echo "$curldata" | "$AC_PATH_DEPS/jsonpath/JSONPath.sh" -b '$.compatibility.*.[version,branch]') - - semverParseInto "$ACORE_VERSION" ACV_MAJOR ACV_MINOR ACV_PATCH ACV_SPECIAL - - if [[ ! -z "$parsed" ]]; then - readarray -t vers < <(echo "$parsed") - local idx - res="none" - # since we've the pair version,branch alternated in not associative and one-dimensional - # array, we've to simulate the association with length/2 trick - for idx in `seq 0 $((${#vers[*]}/2-1))`; do - semverParseInto "${vers[idx*2]}" MODULE_MAJOR MODULE_MINOR MODULE_PATCH MODULE_SPECIAL - if [[ $MODULE_MAJOR -eq $ACV_MAJOR && $MODULE_MINOR -le $ACV_MINOR ]]; then - res="${vers[idx*2+1]}" - v="${vers[idx*2]}" - fi - done +############################################################ +# Module helpers and dispatcher # +############################################################ + +# Returns the default branch name of a GitHub repo in the azerothcore org. +# If the API call fails, defaults to "master". +function inst_get_default_branch() { + local repo="$1" + local def + def=$(curl --silent "https://api.github.com/repos/azerothcore/${repo}" \ + | "$AC_PATH_DEPS/jsonpath/JSONPath.sh" -b '$.default_branch') + if [ -z "$def" ]; then + def="master" fi - - echo "$v" "$res" -} - -function inst_module_search { - - local res="$1" - local idx=0; - - if [ -z "$1" ]; then - echo "Type what to search or leave blank for full list" - read -p "Insert name: " res - fi - - local search="+$res" - - echo "Searching $res..." - echo ""; - - readarray -t MODS < <(curl --silent "https://api.github.com/search/repositories?q=org%3Aazerothcore${search}+fork%3Atrue+topic%3Acore-module+sort%3Astars&type=" \ - | "$AC_PATH_DEPS/jsonpath/JSONPath.sh" -b '$.items.*.name') - while (( ${#MODS[@]} > idx )); do - mod="${MODS[idx++]}" - read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/azerothcore/$mod/master/acore-module.json") - - if [[ "$b" != "none" ]]; then - echo "-> $mod (tested with AC version: $v)" - else - echo "-> $mod (no revision available for AC v$AC_VERSION, it could not work!)" - fi - done - - echo ""; - echo ""; -} - -function inst_module_install { - local res - if [ -z "$1" ]; then - echo "Type the name of the module to install" - read -p "Insert name: " res - else - res="$1" - fi - - read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/azerothcore/$res/master/acore-module.json") - - if [[ "$b" != "none" ]]; then - Joiner:add_repo "https://github.com/azerothcore/$res" "$res" "$b" && echo "Done, please re-run compiling and db assembly. Read instruction on module repository for more information" - else - echo "Cannot install $res module: it doesn't exists or no version compatible with AC v$ACORE_VERSION are available" - fi - - echo ""; - echo ""; -} - -function inst_module_update { - local res; - local _tmp; - local branch; - local p; - - if [ -z "$1" ]; then - echo "Type the name of the module to update" - read -p "Insert name: " res - else - res="$1" - fi - - _tmp=$PWD - - if [ -d "$J_PATH_MODULES/$res/" ]; then - read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/azerothcore/$res/master/acore-module.json") - - cd "$J_PATH_MODULES/$res/" - - # use current branch if something wrong with json - if [[ "$v" == "none" || "$v" == "not-defined" ]]; then - b=`git rev-parse --abbrev-ref HEAD` - fi - - Joiner:upd_repo "https://github.com/azerothcore/$res" "$res" "$b" && echo "Done, please re-run compiling and db assembly" || echo "Cannot update" - cd $_tmp - else - echo "Cannot update! Path doesn't exist" - fi; - - echo ""; - echo ""; -} - -function inst_module_remove { - if [ -z "$1" ]; then - echo "Type the name of the module to remove" - read -p "Insert name: " res - else - res="$1" - fi - - Joiner:remove "$res" && echo "Done, please re-run compiling" || echo "Cannot remove" - - echo ""; - echo ""; + echo "$def" } +# ============================================================================= +# Module Management System +# ============================================================================= +# Load the module manager functions from the dedicated modules-manager directory +source "$AC_PATH_INSTALLER/includes/modules-manager/modules.sh" function inst_simple_restarter { echo "Running $1 ..." @@ -292,4 +178,4 @@ function inst_download_client_data { && echo "unzip downloaded file in $path..." && unzip -q -o "$zipPath" -d "$path/" \ && echo "Remove downloaded file" && rm "$zipPath" \ && echo "INSTALLED_VERSION=$VERSION" > "$dataVersionFile" -} +}
\ No newline at end of file diff --git a/apps/installer/includes/modules-manager/README.md b/apps/installer/includes/modules-manager/README.md new file mode 100644 index 0000000000..f478035beb --- /dev/null +++ b/apps/installer/includes/modules-manager/README.md @@ -0,0 +1,188 @@ +# AzerothCore Module Manager + +This directory contains the module management system for AzerothCore, providing advanced functionality for installing, updating, and managing server modules. + +## 🚀 Features + +- **Advanced Syntax**: Support for `repo[:dirname][@branch[:commit]]` format +- **Cross-Format Recognition**: Intelligent matching across URLs, SSH, and simple names +- **Custom Directory Naming**: Prevent conflicts with custom directory names +- **Duplicate Prevention**: Smart detection and prevention of duplicate installations +- **Multi-Host Support**: GitHub, GitLab, and other Git hosts + +## 📁 File Structure + +``` +modules-manager/ +├── modules.sh # Core module management functions +└── README.md # This documentation file +``` + +## 🔧 Module Specification Syntax + +The module manager supports flexible syntax for specifying modules: + +### New Syntax Format +```bash +repo[:dirname][@branch[:commit]] +``` + +### Examples + +| Specification | Description | +|---------------|-------------| +| `mod-transmog` | Simple module name, uses default branch and directory | +| `mod-transmog:my-custom-dir` | Custom directory name | +| `mod-transmog@develop` | Specific branch | +| `mod-transmog:custom@develop:abc123` | Custom directory, branch, and commit | +| `https://github.com/owner/repo.git@main` | Full URL with branch | +| `git@github.com:owner/repo.git:custom-dir` | SSH URL with custom directory | + +## 🎯 Usage Examples + +### Installing Modules + +```bash +# Simple module installation +./acore.sh module install mod-transmog + +# Install with custom directory name +./acore.sh module install mod-transmog:my-transmog-dir + +# Install specific branch +./acore.sh module install mod-transmog@develop + +# Install with full specification +./acore.sh module install mod-transmog:custom-dir@develop:abc123 + +# Install from URL +./acore.sh module install https://github.com/azerothcore/mod-transmog.git@main + +# Install multiple modules +./acore.sh module install mod-transmog mod-eluna:custom-eluna + +# Install all modules from list +./acore.sh module install --all +``` + +### Updating Modules + +```bash +# Update specific module +./acore.sh module update mod-transmog + +# Update all modules +./acore.sh module update --all + +# Update with branch specification +./acore.sh module update mod-transmog@develop +``` + +### Removing Modules + +```bash +# Remove by simple name (cross-format recognition) +./acore.sh module remove mod-transmog + +# Remove by URL (recognizes same module) +./acore.sh module remove https://github.com/azerothcore/mod-transmog.git + +# Remove multiple modules +./acore.sh module remove mod-transmog mod-eluna +``` + +### Searching Modules + +```bash +# Search for modules +./acore.sh module search transmog + +# Search with multiple terms +./acore.sh module search auction house + +# Show all available modules +./acore.sh module search +``` + +## 🔍 Cross-Format Recognition + +The system intelligently recognizes the same module across different specification formats: + +```bash +# These all refer to the same module: +mod-transmog +azerothcore/mod-transmog +https://github.com/azerothcore/mod-transmog.git +git@github.com:azerothcore/mod-transmog.git +``` + +This allows: +- Installing with one format and removing with another +- Preventing duplicates regardless of specification format +- Consistent module tracking across different input methods + +## 🛡️ Conflict Prevention + +The system prevents common conflicts: + +### Directory Conflicts +```bash +# If 'mod-transmog' directory already exists: +$ ./acore.sh module install mod-transmog:mod-transmog +Error: Directory 'mod-transmog' already exists. +Possible solutions: + 1. Use a different directory name: mod-transmog:my-custom-name + 2. Remove the existing directory first + 3. Use the update command if this is the same module +``` + +## 🔄 Integration + +### Including in Scripts +```bash +# Source the module functions +source "$AC_PATH_INSTALLER/includes/modules-manager/modules.sh" + +# Use module functions +inst_module_install "mod-transmog:custom-dir@develop" +``` + +### Testing +The module system is tested through the main installer test suite: +```bash +./apps/installer/test/test_module_commands.bats +``` + +## 📋 Module List Format + +Modules are tracked in `conf/modules.list` with the format: +``` +# Comments start with # +repo_reference branch commit + +# Examples: +azerothcore/mod-transmog master abc123def456 +https://github.com/custom/mod-custom.git develop def456abc789 +mod-eluna:custom-eluna-dir main 789abc123def +``` +## 🔧 Configuration + +### Environment Variables +- `MODULES_LIST_FILE`: Override default modules list path +- `J_PATH_MODULES`: Modules installation directory +- `AC_PATH_ROOT`: AzerothCore root path + +### Default Paths +- Modules list: `$AC_PATH_ROOT/conf/modules.list` + +## 🤝 Contributing + +When modifying the module manager: + +1. Maintain backwards compatibility +2. Update tests in `test_module_commands.bats` +3. Update this documentation +4. Test cross-format recognition thoroughly +5. Ensure helpful error messages + + diff --git a/apps/installer/includes/modules-manager/modules.sh b/apps/installer/includes/modules-manager/modules.sh new file mode 100644 index 0000000000..93e6433067 --- /dev/null +++ b/apps/installer/includes/modules-manager/modules.sh @@ -0,0 +1,735 @@ +#!/usr/bin/env bash + +# ============================================================================= +# AzerothCore Module Manager Functions +# ============================================================================= +# This file contains all functions related to module management in AzerothCore. +# It provides capabilities for installing, updating, removing, and searching +# modules with support for advanced syntax and intelligent cross-format matching. +# +# Main Features: +# - Advanced syntax: repo[:dirname][@branch[:commit]] +# - Legacy compatibility: repo:branch:commit +# - Cross-format module recognition (URLs, SSH, simple names) +# - Custom directory naming to prevent conflicts +# - Intelligent duplicate prevention +# +# Usage: +# source "path/to/modules.sh" +# inst_module_install "mod-transmog:my-custom-dir@develop:abc123" +# +# ============================================================================= + +# Dispatcher for the unified `module` command. +# Usage: ./acore.sh module <search|install|update|remove> [args...] +function inst_module() { + # Normalize arguments into an array + local tokens=() + read -r -a tokens <<< "$*" + local cmd="${tokens[0]}" + local args=("${tokens[@]:1}") + + case "$cmd" in + ""|"help"|"-h"|"--help") + echo "Usage:" + echo " ./acore.sh module search [terms...]" + echo " ./acore.sh module install [--all | modules...]" + echo " modules can be specified as: name[:branch[:commit]]" + echo " ./acore.sh module update [modules...]" + echo " ./acore.sh module remove [modules...]" + ;; + "search"|"s") + inst_module_search "${args[@]}" + ;; + "install"|"i") + inst_module_install "${args[@]}" + ;; + "update"|"u") + inst_module_update "${args[@]}" + ;; + "remove"|"r") + inst_module_remove "${args[@]}" + ;; + *) + echo "Unknown subcommand: $cmd" + echo "Try: ./acore.sh module help" + ;; + esac +} + +# ============================================================================= +# Module Specification Parsing +# ============================================================================= + +# Parse a module spec with advanced syntax: +# - New syntax: repo[:dirname][@branch[:commit]] +# +# Examples: +# "mod-transmog" -> uses default branch, directory name = mod-transmog +# "mod-transmog:custom-dir" -> uses default branch, directory name = custom-dir +# "mod-transmog@develop" -> uses develop branch, directory name = mod-transmog +# "mod-transmog:custom-dir@develop:abc123" -> custom directory, develop branch, specific commit +# +# Output: "repo_ref owner name branch commit url dirname" +function inst_parse_module_spec() { + local spec="$1" + + local dirname="" branch="" commit="" repo_part="" + + # Parse the new syntax: repo[:dirname][@branch[:commit]] + + # First, extract custom directory name if present (format: repo:dirname@branch) + local repo_with_branch="$spec" + if [[ "$spec" =~ ^([^@:]+):([^@:]+)(@.*)?$ ]]; then + repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}" + dirname="${BASH_REMATCH[2]}" + fi + + # Now parse branch and commit from the repo part + if [[ "$repo_with_branch" =~ ^([^@]+)@([^:]+)(:(.+))?$ ]]; then + repo_part="${BASH_REMATCH[1]}" + branch="${BASH_REMATCH[2]}" + commit="${BASH_REMATCH[4]:-}" + else + repo_part="$repo_with_branch" + fi + + # Normalize repo reference and extract owner/name. + local repo_ref owner name url owner_repo + repo_ref="$repo_part" + + # If repo_ref is a URL, extract owner/name from path when possible + if [[ "$repo_ref" =~ :// ]] || [[ "$repo_ref" =~ ^git@ ]]; then + # Extract owner/name (last two path components) + owner_repo=$(echo "$repo_ref" | sed -E 's#(git@[^:]+:|https?://[^/]+/|ssh://[^/]+/)?(.*?)(\.git)?$#\2#') + owner="$(echo "$owner_repo" | awk -F'/' '{print $(NF-1)}')" + name="$(echo "$owner_repo" | awk -F'/' '{print $NF}' | sed -E 's/\.git$//')" + else + owner_repo="$repo_ref" + if [[ "$owner_repo" == *"/"* ]]; then + owner="$(echo "$owner_repo" | cut -d'/' -f1)" + name="$(echo "$owner_repo" | cut -d'/' -f2)" + else + owner="azerothcore" + name="$owner_repo" + repo_ref="$owner/$name" + fi + fi + + # Build URL only if repo_ref is not already a URL + if [[ "$repo_ref" =~ :// ]] || [[ "$repo_ref" =~ ^git@ ]]; then + url="$repo_ref" + else + url="https://github.com/${repo_ref}" + fi + + # Use custom dirname if provided, otherwise default to module name + if [ -z "$dirname" ]; then + dirname="$name" + fi + + echo "$repo_ref" "$owner" "$name" "${branch:--}" "${commit:--}" "$url" "$dirname" +} + +# ============================================================================= +# Cross-Format Module Recognition +# ============================================================================= + +# Extract owner/name from any repository reference for intelligent matching. +# This enables recognizing the same module regardless of specification format. +# +# Supported formats: +# - GitHub HTTPS: https://github.com/owner/name.git +# - GitHub SSH: git@github.com:owner/name.git +# - GitLab HTTPS: https://gitlab.com/owner/name.git +# - Owner/name: owner/name +# - Simple name: mod-name (assumes azerothcore namespace) +# +# Returns: "owner/name" format for consistent comparison +function inst_extract_owner_name { + local repo_ref="$1" + + # For URLs, don't remove dirname suffix since : is part of the URL + local base_ref="$repo_ref" + if [[ ! "$repo_ref" =~ :// ]] && [[ ! "$repo_ref" =~ ^git@ ]]; then + # Only remove dirname suffix for non-URL formats + base_ref="${repo_ref%%:*}" + fi + + if [[ "$base_ref" =~ ^https?://github\.com/([^/]+)/([^/]+)(\.git)?(/.*)?$ ]]; then + # HTTPS URL format - check this first before owner/name pattern + local name="${BASH_REMATCH[2]}" + name="${name%.git}" # Remove .git suffix if present + echo "${BASH_REMATCH[1]}/$name" + elif [[ "$base_ref" =~ ^https?://gitlab\.com/([^/]+)/([^/]+)(\.git)?(/.*)?$ ]]; then + # GitLab URL format + local name="${BASH_REMATCH[2]}" + name="${name%.git}" # Remove .git suffix if present + echo "${BASH_REMATCH[1]}/$name" + elif [[ "$base_ref" =~ ^git@github\.com:([^/]+)/([^/]+)(\.git)?$ ]]; then + # SSH URL format + local name="${BASH_REMATCH[2]}" + name="${name%.git}" # Remove .git suffix if present + echo "${BASH_REMATCH[1]}/$name" + elif [[ "$base_ref" =~ ^[^/]+/[^/]+$ ]]; then + # Format: owner/name (check after URL patterns) + echo "$base_ref" + elif [[ "$base_ref" =~ ^(mod-|module-)?([a-zA-Z0-9-]+)$ ]]; then + # Simple module name, assume azerothcore namespace + local modname="${BASH_REMATCH[2]}" + if [[ "$base_ref" == mod-* ]]; then + modname="$base_ref" + else + modname="mod-$modname" + fi + echo "azerothcore/$modname" + else + # Unknown format, return as-is + echo "$base_ref" + fi +} + +# ============================================================================= +# Module List Management +# ============================================================================= + +# Returns path to modules list file (configurable via MODULES_LIST_FILE). +function inst_modules_list_path() { + local path="${MODULES_LIST_FILE:-"$AC_PATH_ROOT/conf/modules.list"}" + echo "$path" +} + +# Ensure the modules list file exists and its directory is created. +function inst_mod_list_ensure() { + local file + file="$(inst_modules_list_path)" + mkdir -p "$(dirname "$file")" + [ -f "$file" ] || touch "$file" +} + +# Read modules list into stdout as triplets: "name branch commit" +# Skips comments (# ...) and blank lines. +function inst_mod_list_read() { + local file + file="$(inst_modules_list_path)" + [ -f "$file" ] || return 0 + # shellcheck disable=SC2013 + while IFS= read -r line; do + [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue + echo "$line" + done < "$file" +} + +# Add or update an entry in the list: repo_ref branch commit +# Removes any existing entries with the same owner/name to avoid duplicates +function inst_mod_list_upsert() { + local repo_ref="$1"; shift + local branch="$1"; shift + local commit="$1"; shift + local target_owner_name + target_owner_name=$(inst_extract_owner_name "$repo_ref") + + inst_mod_list_ensure + local file tmp tmp_uns tmp_sorted + file="$(inst_modules_list_path)" + tmp="${file}.tmp" + tmp_uns="${file}.unsorted" + tmp_sorted="${file}.sorted" + + # Build a list without existing duplicates + : > "$tmp_uns" + while read -r existing_ref existing_branch existing_commit; do + [[ -z "$existing_ref" ]] && continue + local existing_owner_name + existing_owner_name=$(inst_extract_owner_name "$existing_ref") + if [[ "$existing_owner_name" != "$target_owner_name" ]]; then + echo "$existing_ref $existing_branch $existing_commit" >> "$tmp_uns" + fi + done < <(inst_mod_list_read) + # Add/replace the new entry (preserving original repo_ref format) + echo "$repo_ref $branch $commit" >> "$tmp_uns" + + # Create key-prefixed lines to sort by normalized owner/name + : > "$tmp" + while read -r r b c; do + [[ -z "$r" ]] && continue + local k + k=$(inst_extract_owner_name "$r") + printf "%s\t%s %s %s\n" "$k" "$r" "$b" "$c" >> "$tmp" + done < "$tmp_uns" + + # Stable sort by key and strip the key + LC_ALL=C sort -t $'\t' -k1,1 -s "$tmp" | cut -f2- > "$tmp_sorted" && mv "$tmp_sorted" "$file" + rm -f "$tmp" "$tmp_uns" "$tmp_sorted" 2>/dev/null || true +} + +# Remove an entry from the list by matching owner/name. +# This allows removing modules regardless of how they were specified (URL vs owner/name) +function inst_mod_list_remove() { + local repo_ref="$1" + local target_owner_name + target_owner_name=$(inst_extract_owner_name "$repo_ref") + + local file + file="$(inst_modules_list_path)" + [ -f "$file" ] || return 0 + + local tmp_uns="${file}.unsorted" + local tmp="${file}.tmp" + local tmp_sorted="${file}.sorted" + + # Keep only lines where owner/name doesn't match + : > "$tmp_uns" + while read -r existing_ref existing_branch existing_commit; do + [[ -z "$existing_ref" ]] && continue + local existing_owner_name + existing_owner_name=$(inst_extract_owner_name "$existing_ref") + if [[ "$existing_owner_name" != "$target_owner_name" ]]; then + echo "$existing_ref $existing_branch $existing_commit" >> "$tmp_uns" + fi + done < <(inst_mod_list_read) + + # Key-prefix and sort for deterministic alphabetical order + : > "$tmp" + while read -r r b c; do + [[ -z "$r" ]] && continue + local k + k=$(inst_extract_owner_name "$r") + printf "%s\t%s %s %s\n" "$k" "$r" "$b" "$c" >> "$tmp" + done < "$tmp_uns" + + LC_ALL=C sort -t $'\t' -k1,1 -s "$tmp" | cut -f2- > "$tmp_sorted" && mv "$tmp_sorted" "$file" + rm -f "$tmp" "$tmp_uns" "$tmp_sorted" 2>/dev/null || true +} + +# Check if a module is already installed by comparing owner/name +# Returns the existing repo_ref if found, empty if not found +function inst_mod_is_installed() { + local spec="$1" + local target_owner_name + target_owner_name=$(inst_extract_owner_name "$spec") + + # Use a different approach: read into a variable first, then process + local modules_content + modules_content=$(inst_mod_list_read) + + # Process each line + while IFS= read -r line; do + [[ -z "$line" ]] && continue + read -r repo_ref branch commit <<< "$line" + local existing_owner_name + existing_owner_name=$(inst_extract_owner_name "$repo_ref") + if [[ "$existing_owner_name" == "$target_owner_name" ]]; then + echo "$repo_ref" # Return the existing entry + return 0 + fi + done <<< "$modules_content" + + return 1 +} + +# ============================================================================= +# Conflict Detection and Validation +# ============================================================================= + +# Check for module directory conflicts with helpful error messages +function inst_check_module_conflict { + local dirname="$1" + local repo_ref="$2" + + if [ -d "$J_PATH_MODULES/$dirname" ]; then + echo "Error: Directory '$dirname' already exists." + echo "Possible solutions:" + echo " 1. Use a different directory name: $repo_ref:my-custom-name" + echo " 2. Remove the existing directory first" + echo " 3. Use the update command if this is the same module" + return 1 + fi + return 0 +} + +# ============================================================================= +# Module Operations +# ============================================================================= + +# Get version and branch information from acore-module.json +function inst_getVersionBranch() { + local res="master" + local v="not-defined" + local MODULE_MAJOR=0 + local MODULE_MINOR=0 + local MODULE_PATCH=0 + local MODULE_SPECIAL=0; + local ACV_MAJOR=0 + local ACV_MINOR=0 + local ACV_PATCH=0 + local ACV_SPECIAL=0; + local curldata=$(curl -f --silent -H 'Cache-Control: no-cache' "$1" || echo "{}") + local parsed=$(echo "$curldata" | "$AC_PATH_DEPS/jsonpath/JSONPath.sh" -b '$.compatibility.*.[version,branch]') + + semverParseInto "$ACORE_VERSION" ACV_MAJOR ACV_MINOR ACV_PATCH ACV_SPECIAL + + if [[ ! -z "$parsed" ]]; then + readarray -t vers < <(echo "$parsed") + local idx + res="none" + # since we've the pair version,branch alternated in not associative and one-dimensional + # array, we've to simulate the association with length/2 trick + for idx in `seq 0 $((${#vers[*]}/2-1))`; do + semverParseInto "${vers[idx*2]}" MODULE_MAJOR MODULE_MINOR MODULE_PATCH MODULE_SPECIAL + if [[ $MODULE_MAJOR -eq $ACV_MAJOR && $MODULE_MINOR -le $ACV_MINOR ]]; then + res="${vers[idx*2+1]}" + v="${vers[idx*2]}" + fi + done + fi + + echo "$v" "$res" +} + +# Search for modules in the AzerothCore repository +function inst_module_search { + # Accept 0..N search terms; if none provided, prompt the user. + local terms=("$@") + if [ ${#terms[@]} -eq 0 ]; then + echo "Type what to search (blank for full list)" + read -p "Insert name(s): " _line + if [ -n "$_line" ]; then + read -r -a terms <<< "$_line" + fi + fi + + local CATALOG_URL="https://www.azerothcore.org/data/catalogue.json" + + echo "Searching ${terms[*]}..." + echo "" + + # Build candidate list from catalogue (full_name = owner/repo) + local MODS=() + if command -v jq >/dev/null 2>&1; then + mapfile -t MODS < <(curl --silent -L "$CATALOG_URL" \ + | jq -r ' + [ .. | objects + | select(.full_name and .topics) + | select(.topics | index("azerothcore-module")) + ] + | unique_by(.full_name) + | sort_by(.stargazers_count // 0) | reverse + | .[].full_name + ') + else + # Fallback without jq: best-effort extraction of owner/repo + mapfile -t MODS < <(curl --silent -L "$CATALOG_URL" \ + | grep -oE '\"full_name\"\\s*:\\s*\"[^\"/[:space:]]+/[^\"[:space:]]+\"' \ + | sed -E 's/.*\"full_name\"\\s*:\\s*\"([^\"]+)\".*/\\1/' \ + | sort -u) + fi + + # Local AND filter on user terms (case-insensitive) against full_name + if (( ${#terms[@]} > 0 )); then + local filtered=() + local item + for item in "${MODS[@]}"; do + local keep=1 + local lower="${item,,}" + local t + for t in "${terms[@]}"; do + [ -z "$t" ] && continue + if [[ "$lower" != *"${t,,}"* ]]; then + keep=0; break + fi + done + (( keep )) && filtered+=("$item") + done + MODS=("${filtered[@]}") + fi + + if (( ${#MODS[@]} == 0 )); then + echo "No results." + echo "" + return 0 + fi + + local idx=0 + while (( ${#MODS[@]} > idx )); do + local mod_full="${MODS[idx++]}" # owner/repo + local mod="${mod_full##*/}" # repo name only for display + read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/${mod_full}/master/acore-module.json") + + if [[ "$b" != "none" ]]; then + echo "-> $mod (tested with AC version: $v)" + else + echo "-> $mod (NOTE: The module latest tested AC revision is Unknown)" + fi + done + + echo "" + echo "" +} + + +# Install one or more modules with advanced syntax support +function inst_module_install { + # Support multiple modules and the --all flag; prompt if none specified. + local args=("$@") + local use_all=false + if [ ${#args[@]} -gt 0 ] && { [ "${args[0]}" = "--all" ] || [ "${args[0]}" = "-a" ]; }; then + use_all=true + shift || true + fi + + local modules=("$@") + + echo "Installing modules: ${modules[*]}" + + if $use_all; then + # Install all modules from the list (respecting recorded branch and commit). + inst_mod_list_ensure + local line repo_ref branch commit url owner modname dirname + + # First pass: detect duplicate target directories (flat structure) + declare -A _seen _first + local dup_error=0 + while read -r repo_ref branch commit; do + [ -z "$repo_ref" ] && continue + parsed_output=$(inst_parse_module_spec "$repo_ref") + IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output" + # dirname defaults to repo name; flat install path uses dirname only + if [[ -n "${_seen[$dirname]:-}" ]]; then + echo "Error: duplicate module target directory '$dirname' detected in modules.list:" + echo " - ${_first[$dirname]}" + echo " - ${repo_ref}" + echo "Use a custom folder name to disambiguate, e.g.: ${repo_ref}:$dirname-alt" + dup_error=1 + else + _seen[$dirname]=1 + _first[$dirname]="$repo_ref" + fi + done < <(inst_mod_list_read) + if [[ "$dup_error" -ne 0 ]]; then + return 1 + fi + + # Second pass: install in flat modules directory (no owner subfolders) + while read -r repo_ref branch commit; do + [ -z "$repo_ref" ] && continue + parsed_output=$(inst_parse_module_spec "$repo_ref") + IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output" + + if [ -d "$J_PATH_MODULES/$dirname" ]; then + echo "[$repo_ref] Already installed (skipping)." + continue + fi + + if Joiner:add_repo "$url" "$dirname" "$branch" ""; then + # Checkout the recorded commit if present + if [ -n "$commit" ]; then + git -C "$J_PATH_MODULES/$dirname" fetch --all --quiet || true + if git -C "$J_PATH_MODULES/$dirname" rev-parse --verify "$commit" >/dev/null 2>&1; then + git -C "$J_PATH_MODULES/$dirname" checkout --quiet "$commit" + fi + fi + local curCommit + curCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") + inst_mod_list_upsert "$repo_ref" "$branch" "$curCommit" + echo "[$repo_ref] Installed." + else + echo "[$repo_ref] Install failed." + exit 1; + fi + done < <(inst_mod_list_read) + else + # Install specified modules; prompt if none specified. + if [ ${#modules[@]} -eq 0 ]; then + echo "Type the name(s) of the module(s) to install" + read -p "Insert name(s): " _line + read -r -a modules <<< "$_line" + fi + + local spec name override_branch override_commit v b def curCommit existing_repo_ref dirname + for spec in "${modules[@]}"; do + [ -z "$spec" ] && continue + + # Check if module is already installed (by owner/name matching) + existing_repo_ref=$(inst_mod_is_installed "$spec" || true) + if [ -n "$existing_repo_ref" ]; then + echo "[$spec] Already installed as [$existing_repo_ref] (skipping)." + continue + fi + + parsed_output=$(inst_parse_module_spec "$spec") + IFS=' ' read -r repo_ref owner modname override_branch override_commit url dirname <<< "$parsed_output" + [ -z "$repo_ref" ] && continue + + # Check for directory conflicts with custom directory names + if ! inst_check_module_conflict "$dirname" "$repo_ref"; then + continue + fi + + # override_branch takes precedence; otherwise consult acore-module.json on azerothcore unless repo_ref contains owner or URL + if [ -n "$override_branch" ] && [ "$override_branch" != "-" ]; then + b="$override_branch" + else + # For GitHub repositories, use raw.githubusercontent.com to check acore-module.json + if [[ "$url" =~ github.com ]] || [[ "$repo_ref" =~ ^[^/]+/[^/]+$ ]]; then + read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/${owner}/${modname}/master/acore-module.json") + else + # Unknown host: try the repository URL as-is (may fail) + read v b < <(inst_getVersionBranch "${url}/master/acore-module.json") + fi + if [[ "$v" == "none" || "$v" == "not-defined" || "$b" == "none" ]]; then + def="$(inst_get_default_branch "$repo_ref")" + echo "Warning: $repo_ref has no compatible acore-module.json; installing from branch '$def' (latest commit)." + b="$def" + fi + fi + + # Use flat directory structure with custom directory name + if [ -d "$J_PATH_MODULES/$dirname" ]; then + echo "[$repo_ref] Already installed (skipping)." + curCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") + inst_mod_list_upsert "$repo_ref" "$b" "$curCommit" + continue + fi + + if Joiner:add_repo "$url" "$dirname" "$b" ""; then + # If a commit was provided, try to checkout it + if [ -n "$override_commit" ] && [ "$override_commit" != "-" ]; then + git -C "$J_PATH_MODULES/$dirname" fetch --all --quiet || true + if git -C "$J_PATH_MODULES/$dirname" rev-parse --verify "$override_commit" >/dev/null 2>&1; then + git -C "$J_PATH_MODULES/$dirname" checkout --quiet "$override_commit" + else + echo "[$repo_ref] Warning: provided commit '$override_commit' not found; staying on branch '$b' HEAD." + fi + fi + curCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") + inst_mod_list_upsert "$repo_ref" "$b" "$curCommit" + echo "[$repo_ref] Installed in '$dirname'. Please re-run compiling and db assembly." + else + echo "[$repo_ref] Install failed or module not found" + exit 1; + fi + done + fi + + echo "" + echo "" +} + +# Update one or more modules +function inst_module_update { + # Support multiple modules and the --all flag; prompt if none specified. + local args=("$@") + local use_all=false + if [ ${#args[@]} -gt 0 ] && { [ "${args[0]}" = "--all" ] || [ "${args[0]}" = "-a" ]; }; then + use_all=true + shift || true + fi + + local _tmp=$PWD + + if $use_all; then + local line repo_ref branch commit newCommit owner modname url dirname + while read -r repo_ref branch commit; do + [ -z "$repo_ref" ] && continue + parsed_output=$(inst_parse_module_spec "$repo_ref") + IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output" + + dirname="${dirname:-$modname}" + if [ ! -d "$J_PATH_MODULES/$dirname/" ]; then + echo "[$repo_ref] Not installed locally, skipping." + continue + fi + + if Joiner:upd_repo "$url" "$dirname" "$branch" ""; then + newCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") + inst_mod_list_upsert "$repo_ref" "$branch" "$newCommit" + echo "[$repo_ref] Updated to latest commit on '$branch'." + else + echo "[$repo_ref] Cannot update" + fi + done < <(inst_mod_list_read) + else + local modules=("$@") + if [ ${#modules[@]} -eq 0 ]; then + echo "Type the name(s) of the module(s) to update" + read -p "Insert name(s): " _line + read -r -a modules <<< "$_line" + fi + + local spec repo_ref override_branch override_commit owner modname url dirname v b branch def newCommit + for spec in "${modules[@]}"; do + [ -z "$spec" ] && continue + parsed_output=$(inst_parse_module_spec "$spec") + IFS=' ' read -r repo_ref owner modname override_branch override_commit url dirname <<< "$parsed_output" + + dirname="${dirname:-$modname}" + if [ -d "$J_PATH_MODULES/$dirname/" ]; then + # determine preferred branch if not provided + if [ -n "$override_branch" ] && [ "$override_branch" != "-" ]; then + b="$override_branch" + else + # try reading acore-module.json for this repo + if [[ "$url" =~ github.com ]]; then + read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/${owner}/${modname}/master/acore-module.json") + else + read v b < <(inst_getVersionBranch "${url}/master/acore-module.json") + fi + if [[ "$v" == "none" || "$v" == "not-defined" || "$b" == "none" ]]; then + if branch=$(git -C "$J_PATH_MODULES/$dirname" rev-parse --abbrev-ref HEAD 2>/dev/null); then + echo "Warning: $repo_ref has no compatible acore-module.json; updating current branch '$branch'." + b="$branch" + else + def="$(inst_get_default_branch "$repo_ref")" + echo "Warning: $repo_ref has no compatible acore-module.json and no git branch detected; updating default branch '$def'." + b="$def" + fi + fi + fi + + if Joiner:upd_repo "$url" "$dirname" "$b" ""; then + newCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") + inst_mod_list_upsert "$repo_ref" "$b" "$newCommit" + echo "[$repo_ref] Done, please re-run compiling and db assembly" + else + echo "[$repo_ref] Cannot update" + fi + else + echo "[$repo_ref] Cannot update! Path doesn't exist ($J_PATH_MODULES/$dirname/)" + fi + done + fi + + echo "" + echo "" +} + +# Remove one or more modules +function inst_module_remove { + # Support multiple modules; prompt if none specified. + local modules=("$@") + if [ ${#modules[@]} -eq 0 ]; then + echo "Type the name(s) of the module(s) to remove" + read -p "Insert name(s): " _line + read -r -a modules <<< "$_line" + fi + + local spec repo_ref owner modname url override_branch override_commit dirname + for spec in "${modules[@]}"; do + [ -z "$spec" ] && continue + parsed_output=$(inst_parse_module_spec "$spec") + IFS=' ' read -r repo_ref owner modname override_branch override_commit url dirname <<< "$parsed_output" + [ -z "$repo_ref" ] && continue + + dirname="${dirname:-$modname}" + if Joiner:remove "$dirname" ""; then + inst_mod_list_remove "$repo_ref" + echo "[$repo_ref] Done, please re-run compiling" + else + echo "[$repo_ref] Cannot remove" + fi + done + + echo "" + echo "" +} diff --git a/apps/installer/main.sh b/apps/installer/main.sh index 396dafaad5..5911ccbf74 100644 --- a/apps/installer/main.sh +++ b/apps/installer/main.sh @@ -6,22 +6,22 @@ source "$CURRENT_PATH/includes/includes.sh" PS3='[Please enter your choice]: ' options=( - "init (i): First Installation" # 1 - "install-deps (d): Configure OS dep" # 2 - "pull (u): Update Repository" # 3 - "reset (r): Reset & Clean Repository" # 4 - "compiler (c): Run compiler tool" # 5 - "module-search (ms): Module Search by keyword" # 6 - "module-install (mi): Module Install by name" # 7 - "module-update (mu): Module Update by name" # 8 - "module-remove: (mr): Module Remove by name" # 9 - "client-data: (gd): download client data from github repository (beta)" # 10 - "run-worldserver (rw): execute a simple restarter for worldserver" # 11 - "run-authserver (ra): execute a simple restarter for authserver" # 12 - "docker (dr): Run docker tools" # 13 - "version (v): Show AzerothCore version" # 14 - "service-manager (sm): Run service manager to run authserver and worldserver in background" # 15 - "quit: Exit from this menu" # 16 + "init (i): First Installation" + "install-deps (d): Configure OS dep" + "pull (u): Update Repository" + "reset (r): Reset & Clean Repository" + "compiler (c): Run compiler tool" + "module (m): Module manager (search/install/update/remove)" + "module-install (mi): Module Install by name [DEPRECATED]" + "module-update (mu): Module Update by name [DEPRECATED]" + "module-remove: (mr): Module Remove by name [DEPRECATED]" + "client-data: (gd): download client data from github repository (beta)" + "run-worldserver (rw): execute a simple restarter for worldserver" + "run-authserver (ra): execute a simple restarter for authserver" + "docker (dr): Run docker tools" + "version (v): Show AzerothCore version" + "service-manager (sm): Run service manager to run authserver and worldserver in background" + "quit (q): Exit from this menu" ) function _switch() { @@ -29,56 +29,64 @@ function _switch() { _opt="$2" case $_reply in - ""|"i"|"init"|"1") + ""|"i"|"init") inst_allInOne ;; - ""|"d"|"install-deps"|"2") + ""|"d"|"install-deps") inst_configureOS ;; - ""|"u"|"pull"|"3") + ""|"u"|"pull") inst_updateRepo ;; - ""|"r"|"reset"|"4") + ""|"r"|"reset") inst_resetRepo ;; - ""|"c"|"compiler"|"5") + ""|"c"|"compiler") bash "$AC_PATH_APPS/compiler/compiler.sh" $_opt ;; - ""|"ms"|"module-search"|"6") - inst_module_search "$_opt" + ""|"m"|"module") + # Unified module command: supports subcommands search|install|update|remove + inst_module "${@:2}" ;; - ""|"mi"|"module-install"|"7") - inst_module_install "$_opt" + ""|"ms"|"module-search") + echo "[DEPRECATED] Use: ./acore.sh module search <terms...>" + inst_module_search "${@:2}" ;; - ""|"mu"|"module-update"|"8") - inst_module_update "$_opt" + ""|"mi"|"module-install") + echo "[DEPRECATED] Use: ./acore.sh module install <modules...>" + inst_module_install "${@:2}" ;; - ""|"mr"|"module-remove"|"9") - inst_module_remove "$_opt" + ""|"mu"|"module-update") + echo "[DEPRECATED] Use: ./acore.sh module update <modules...>" + inst_module_update "${@:2}" ;; - ""|"gd"|"client-data"|"10") + ""|"mr"|"module-remove") + echo "[DEPRECATED] Use: ./acore.sh module remove <modules...>" + inst_module_remove "${@:2}" + ;; + ""|"gd"|"client-data") inst_download_client_data ;; - ""|"rw"|"run-worldserver"|"11") + ""|"rw"|"run-worldserver") inst_simple_restarter worldserver ;; - ""|"ra"|"run-authserver"|"12") + ""|"ra"|"run-authserver") inst_simple_restarter authserver ;; - ""|"dr"|"docker"|"13") + ""|"dr"|"docker") DOCKER=1 bash "$AC_PATH_ROOT/apps/docker/docker-cmd.sh" "${@:2}" exit ;; - ""|"v"|"version"|"14") + ""|"v"|"version") # denoRunFile "$AC_PATH_APPS/installer/main.ts" "version" printf "AzerothCore Rev. %s\n" "$ACORE_VERSION" exit ;; - ""|"sm"|"service-manager"|"15") + ""|"sm"|"service-manager") bash "$AC_PATH_APPS/startup-scripts/src/service-manager.sh" "${@:2}" exit ;; - ""|"quit"|"16") + ""|"q"|"quit") echo "Goodbye!" exit ;; @@ -102,4 +110,5 @@ do _switch $REPLY break done + echo "opt: $opt" done diff --git a/apps/installer/test/bats.conf b/apps/installer/test/bats.conf new file mode 100644 index 0000000000..8034254e72 --- /dev/null +++ b/apps/installer/test/bats.conf @@ -0,0 +1,14 @@ +# BATS Test Configuration + +# Set test timeout (in seconds) +export BATS_TEST_TIMEOUT=30 + +# Enable verbose output for debugging +export BATS_VERBOSE_RUN=1 + +# Test output format +export BATS_FORMATTER=pretty + +# Enable colored output +export BATS_NO_PARALLELIZE_ACROSS_FILES=1 +export BATS_NO_PARALLELIZE_WITHIN_FILE=1 diff --git a/apps/installer/test/test_module_commands.bats b/apps/installer/test/test_module_commands.bats new file mode 100755 index 0000000000..40c2849416 --- /dev/null +++ b/apps/installer/test/test_module_commands.bats @@ -0,0 +1,354 @@ +#!/usr/bin/env bats + +# Tests for installer module commands (search/install/update/remove) +# Focused on installer:module install behavior using a mocked joiner + +load '../../test-framework/bats_libs/acore-support' +load '../../test-framework/bats_libs/acore-assert' + +setup() { + acore_test_setup + # Point to the installer src directory (not needed in this test) + + # Set installer/paths environment for the test + export AC_PATH_APPS="$TEST_DIR/apps" + export AC_PATH_ROOT="$TEST_DIR" + export AC_PATH_DEPS="$TEST_DIR/deps" + export AC_PATH_MODULES="$TEST_DIR/modules" + export MODULES_LIST_FILE="$TEST_DIR/conf/modules.list" + + # Create stubbed deps: joiner.sh (sourced by includes) and semver + mkdir -p "$TEST_DIR/deps/acore/joiner" + cat > "$TEST_DIR/deps/acore/joiner/joiner.sh" << 'EOF' +#!/usr/bin/env bash +# Stub joiner functions used by installer +Joiner:add_repo() { + # arguments: url name branch basedir + echo "ADD $@" > "$TEST_DIR/joiner_called.txt" + return 0 +} +Joiner:upd_repo() { + echo "UPD $@" > "$TEST_DIR/joiner_called.txt" + return 0 +} +Joiner:remove() { + echo "REM $@" > "$TEST_DIR/joiner_called.txt" + return 0 +} +EOF + chmod +x "$TEST_DIR/deps/acore/joiner/joiner.sh" + + mkdir -p "$TEST_DIR/deps/semver_bash" + # Minimal semver stub + cat > "$TEST_DIR/deps/semver_bash/semver.sh" << 'EOF' +#!/usr/bin/env bash +# semver stub +semver::satisfies() { return 0; } +EOF + chmod +x "$TEST_DIR/deps/semver_bash/semver.sh" + + # Provide a minimal compiler includes file expected by installer + mkdir -p "$TEST_DIR/apps/compiler/includes" + touch "$TEST_DIR/apps/compiler/includes/includes.sh" + + # Provide minimal bash_shared includes to satisfy installer include + mkdir -p "$TEST_DIR/apps/bash_shared" + cat > "$TEST_DIR/apps/bash_shared/includes.sh" << 'EOF' +#!/usr/bin/env bash +# minimal stub +EOF + + # Copy the real installer app into the test apps dir + mkdir -p "$TEST_DIR/apps" + cp -r "$(cd "$AC_TEST_ROOT/apps/installer" && pwd)" "$TEST_DIR/apps/installer" +} + +teardown() { + acore_test_teardown +} + +@test "module install should call joiner and record entry in modules list" { + cd "$TEST_DIR" + + # Source installer includes and call the install function directly to avoid menu interaction + run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_install example-module@main:abcd1234" + + # Check that joiner was called + [ -f "$TEST_DIR/joiner_called.txt" ] + grep -q "ADD" "$TEST_DIR/joiner_called.txt" + + # Check modules list was created and contains the repo_ref and branch + [ -f "$TEST_DIR/conf/modules.list" ] + grep -q "azerothcore/example-module main" "$TEST_DIR/conf/modules.list" +} + +@test "module install with owner/name format should work" { + cd "$TEST_DIR" + + # Test with owner/name format + run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_install myorg/mymodule" + + # Check that joiner was called with correct URL + [ -f "$TEST_DIR/joiner_called.txt" ] + grep -q "ADD https://github.com/myorg/mymodule mymodule" "$TEST_DIR/joiner_called.txt" + + # Check modules list contains the entry + [ -f "$TEST_DIR/conf/modules.list" ] + grep -q "myorg/mymodule" "$TEST_DIR/conf/modules.list" +} + +@test "module remove should call joiner remove and update modules list" { + cd "$TEST_DIR" + + # First install a module + bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_install test-module" + + # Then remove it + run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_remove test-module" + + # Check that joiner remove was called + [ -f "$TEST_DIR/joiner_called.txt" ] + # With flat structure, basedir is empty; ensure name is present + grep -q "REM test-module" "$TEST_DIR/joiner_called.txt" + + # Check modules list no longer contains the entry + [ -f "$TEST_DIR/conf/modules.list" ] + ! grep -q "azerothcore/test-module" "$TEST_DIR/conf/modules.list" +} + +# Tests for intelligent module management (duplicate prevention and cross-format removal) + +@test "inst_extract_owner_name should extract owner/name from various formats" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test simple name + run inst_extract_owner_name "mod-transmog" + [ "$output" = "azerothcore/mod-transmog" ] + + # Test owner/name format + run inst_extract_owner_name "azerothcore/mod-transmog" + [ "$output" = "azerothcore/mod-transmog" ] + + # Test HTTPS URL + run inst_extract_owner_name "https://github.com/azerothcore/mod-transmog.git" + [ "$output" = "azerothcore/mod-transmog" ] + + # Test SSH URL + run inst_extract_owner_name "git@github.com:azerothcore/mod-transmog.git" + [ "$output" = "azerothcore/mod-transmog" ] + + # Test GitLab URL + run inst_extract_owner_name "https://gitlab.com/myorg/mymodule.git" + [ "$output" = "myorg/mymodule" ] +} + +@test "duplicate module entries should be prevented across different formats" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Add module via simple name + inst_mod_list_upsert "mod-transmog" "master" "abc123" + + # Verify it's in the list + grep -q "mod-transmog master abc123" "$TEST_DIR/conf/modules.list" + + # Add same module via owner/name format - should replace, not duplicate + inst_mod_list_upsert "azerothcore/mod-transmog" "dev" "def456" + + # Should only have one entry (the new one) + [ "$(grep -c "azerothcore/mod-transmog" "$TEST_DIR/conf/modules.list")" -eq 1 ] + grep -q "azerothcore/mod-transmog dev def456" "$TEST_DIR/conf/modules.list" + ! grep -q "mod-transmog master abc123" "$TEST_DIR/conf/modules.list" +} + +@test "module installed via URL should be recognized when checking with different formats" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Install via HTTPS URL + inst_mod_list_upsert "https://github.com/azerothcore/mod-transmog.git" "master" "abc123" + + # Should be detected as installed using simple name + run inst_mod_is_installed "mod-transmog" + [ "$status" -eq 0 ] + + # Should be detected as installed using owner/name + run inst_mod_is_installed "azerothcore/mod-transmog" + [ "$status" -eq 0 ] + + # Should be detected as installed using SSH URL + run inst_mod_is_installed "git@github.com:azerothcore/mod-transmog.git" + [ "$status" -eq 0 ] + + # Non-existent module should not be detected + run inst_mod_is_installed "mod-nonexistent" + [ "$status" -ne 0 ] +} + +@test "cross-format module removal should work" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Install via SSH URL + inst_mod_list_upsert "git@github.com:azerothcore/mod-transmog.git" "master" "abc123" + + # Verify it's installed + grep -q "git@github.com:azerothcore/mod-transmog.git" "$TEST_DIR/conf/modules.list" + + # Remove using simple name + inst_mod_list_remove "mod-transmog" + + # Should be completely removed + ! grep -q "azerothcore/mod-transmog" "$TEST_DIR/conf/modules.list" + ! grep -q "git@github.com:azerothcore/mod-transmog.git" "$TEST_DIR/conf/modules.list" +} + +@test "module installation should prevent duplicates when already installed" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Install via simple name first + inst_mod_list_upsert "mod-worldchat" "master" "abc123" + + # Try to install same module via URL - should detect it's already installed + run inst_mod_is_installed "https://github.com/azerothcore/mod-worldchat.git" + [ "$status" -eq 0 ] + + # Add via URL should replace the existing entry + inst_mod_list_upsert "https://github.com/azerothcore/mod-worldchat.git" "dev" "def456" + + # Should only have one entry + [ "$(grep -c "azerothcore/mod-worldchat" "$TEST_DIR/conf/modules.list")" -eq 1 ] + grep -q "https://github.com/azerothcore/mod-worldchat.git dev def456" "$TEST_DIR/conf/modules.list" +} + +@test "module update --all uses flat structure (no branch subfolders)" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Prepare modules.list with one entry and a matching local directory + mkdir -p "$TEST_DIR/conf" + echo "azerothcore/mod-transmog master abc123" > "$TEST_DIR/conf/modules.list" + mkdir -p "$TEST_DIR/modules/mod-transmog" + + # Run update all + run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_update --all" + + # Verify Joiner:upd_repo received flat structure args (no basedir) + [ -f "$TEST_DIR/joiner_called.txt" ] + grep -q "UPD https://github.com/azerothcore/mod-transmog mod-transmog master" "$TEST_DIR/joiner_called.txt" +} + +@test "module update specific uses flat structure with override branch" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Create local directory so update proceeds + mkdir -p "$TEST_DIR/modules/mymodule" + + # Run update specifying owner/name and branch + run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_update myorg/mymodule@dev" + + # Should call joiner with name 'mymodule' and branch 'dev' (no basedir) + [ -f "$TEST_DIR/joiner_called.txt" ] + grep -q "UPD https://github.com/myorg/mymodule mymodule dev" "$TEST_DIR/joiner_called.txt" +} + +@test "custom directory names should work with new syntax" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test parsing with custom directory name + run inst_parse_module_spec "mod-transmog:my-custom-dir@develop:abc123" + [ "$status" -eq 0 ] + # Should output: repo_ref owner name branch commit url dirname + IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output" + [ "$repo_ref" = "azerothcore/mod-transmog" ] + [ "$owner" = "azerothcore" ] + [ "$name" = "mod-transmog" ] + [ "$branch" = "develop" ] + [ "$commit" = "abc123" ] + [ "$url" = "https://github.com/azerothcore/mod-transmog" ] + [ "$dirname" = "my-custom-dir" ] +} + +@test "directory conflict detection should work" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Create a fake existing directory + mkdir -p "$TEST_DIR/modules/existing-dir" + + # Should detect conflict + run inst_check_module_conflict "existing-dir" "mod-test" + [ "$status" -eq 1 ] + [[ "$output" =~ "Directory 'existing-dir' already exists" ]] + [[ "$output" =~ "Use a different directory name: mod-test:my-custom-name" ]] + + # Should not detect conflict for non-existing directory + run inst_check_module_conflict "non-existing-dir" "mod-test" + [ "$status" -eq 0 ] +} + +@test "module update should work with custom directories" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # First add module with custom directory to list + inst_mod_list_upsert "azerothcore/mod-transmog:custom-dir" "master" "abc123" + + # Create fake module directory structure + mkdir -p "$TEST_DIR/modules/custom-dir/.git" + echo "ref: refs/heads/master" > "$TEST_DIR/modules/custom-dir/.git/HEAD" + + # Mock git commands in the fake module directory + cat > "$TEST_DIR/modules/custom-dir/.git/config" << 'EOF' +[core] + repositoryformatversion = 0 + filemode = true + bare = false +[remote "origin"] + url = https://github.com/azerothcore/mod-transmog + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "master"] + remote = origin + merge = refs/heads/master +EOF + + # Test update with custom directory should work + # Note: This would require more complex mocking for full integration test + # For now, just test the parsing recognizes the custom directory + run inst_parse_module_spec "azerothcore/mod-transmog:custom-dir" + [ "$status" -eq 0 ] + IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output" + [ "$dirname" = "custom-dir" ] +} + +@test "URL formats should be properly normalized" { + cd "$TEST_DIR" + source "$TEST_DIR/apps/installer/includes/includes.sh" + + # Test various URL formats produce same owner/name + run inst_extract_owner_name "https://github.com/azerothcore/mod-transmog" + local url_format="$output" + + run inst_extract_owner_name "https://github.com/azerothcore/mod-transmog.git" + local url_git_format="$output" + + run inst_extract_owner_name "git@github.com:azerothcore/mod-transmog.git" + local ssh_format="$output" + + run inst_extract_owner_name "azerothcore/mod-transmog" + local owner_name_format="$output" + + run inst_extract_owner_name "mod-transmog" + local simple_format="$output" + + # All should normalize to the same owner/name + [ "$url_format" = "azerothcore/mod-transmog" ] + [ "$url_git_format" = "azerothcore/mod-transmog" ] + [ "$ssh_format" = "azerothcore/mod-transmog" ] + [ "$owner_name_format" = "azerothcore/mod-transmog" ] + [ "$simple_format" = "azerothcore/mod-transmog" ] +} diff --git a/conf/dist/config.sh b/conf/dist/config.sh index df7ddd3fc2..2e105fd445 100644 --- a/conf/dist/config.sh +++ b/conf/dist/config.sh @@ -149,4 +149,17 @@ export CPUPROFILESIGNAL=${CPUPROFILESIGNAL:-12} # Other values for HEAPCHECK: minimal, normal (equivalent to "1"), strict, draconian #export HEAPCHECK=${HEAPCHECK:-normal} +############################################## +# +# MODULES LIST FILE (for installer `module` commands) +# +# Path to the file where the installer records installed modules +# with their branch and commit. You can override this path by +# setting the MODULES_LIST_FILE inside your config.sh or as an environment variable. +# By default it points inside the repository conf folder. +# Format of each line: +# <module-name> <branch> <commit> +# Lines starting with '#' and empty lines are ignored. +export MODULES_LIST_FILE=${MODULES_LIST_FILE:-"$AC_PATH_ROOT/conf/modules.list"} + diff --git a/deps/acore/joiner/joiner.sh b/deps/acore/joiner/joiner.sh index be95b673a1..1b13007162 100755 --- a/deps/acore/joiner/joiner.sh +++ b/deps/acore/joiner/joiner.sh @@ -94,11 +94,11 @@ function Joiner:add_repo() ( basedir="${4:-""}" [[ -z $url ]] && hasReq=false || hasReq=true - Joiner:_help $hasReq "$1" "Syntax: joiner.sh add-repo [-d] [-e] url name branch [basedir]" + Joiner:_help "$hasReq" "$1" "Syntax: joiner.sh add-repo [-d] [-e] url name branch [basedir]" # retrieving info from url if not set if [[ -z $name ]]; then - basename=$(basename $url) + basename=$(basename "$url") name=${basename%%.*} if [[ -z "$basedir" ]]; then @@ -115,10 +115,12 @@ function Joiner:add_repo() ( if [ -e "$path/.git/" ]; then # if exists , update - git --git-dir="$path/.git/" rev-parse && git --git-dir="$path/.git/" pull origin $branch | grep 'Already up-to-date.' && changed="no" || true + echo "Updating $name on branch $branch..." + git --git-dir="$path/.git/" --work-tree="$path" rev-parse && git --git-dir="$path/.git/" --work-tree="$path" pull origin "$branch" | grep 'Already up-to-date.' && changed="no" || true else # otherwise clone - git clone $url -c advice.detachedHead=0 -b $branch "$path" + echo "Cloning $name on branch $branch..." + git clone "$url" -c advice.detachedHead=0 -b "$branch" "$path" fi if [ "$?" -ne "0" ]; then @@ -140,16 +142,16 @@ function Joiner:add_git_submodule() ( basedir=${4:-""} [[ -z $url ]] && hasReq=false || hasReq=true - Joiner:_help $hasReq "$1" "Syntax: joiner.sh add-git-submodule [-d] [-e] url name branch [basedir]" + Joiner:_help "$hasReq" "$1" "Syntax: joiner.sh add-git-submodule [-d] [-e] url name branch [basedir]" # retrieving info from url if not set if [[ -z $name ]]; then - basename=$(basename $url) + basename=$(basename "$url") name=${basename%%.*} if [[ -z $basedir ]]; then - dir=$(dirname $url) - basedir=$(basename $dir) + dir=$(dirname "$url") + basedir=$(basename "$dir") fi name="${name,,}" #to lowercase @@ -158,17 +160,17 @@ function Joiner:add_git_submodule() ( path="$J_PATH_MODULES/$basedir/$name" valid_path=`Joiner:_searchFirstValiPath "$path"` - rel_path=${path#$valid_path} + rel_path=${path#"$valid_path"} rel_path=${rel_path#/} - if [ -e $path/ ]; then + if [ -e "$path/" ]; then # if exists , update - (cd "$path" && git pull origin $branch) - (cd "$valid_path" && git submodule update -f --init $rel_path) + (cd "$path" && git pull origin "$branch") + (cd "$valid_path" && git submodule update -f --init "$rel_path") else # otherwise add - (cd "$valid_path" && git submodule add -f -b $branch $url $rel_path) - (cd "$valid_path" && git submodule update -f --init $rel_path) + (cd "$valid_path" && git submodule add -f -b "$branch" "$url" "$rel_path") + (cd "$valid_path" && git submodule update -f --init "$rel_path") fi if [ "$?" -ne "0" ]; then @@ -324,7 +326,7 @@ function Joiner:self_update() { if [ ! -z "$J_VER_REQ" ]; then # if J_VER_REQ is defined then update only if tag is different _cur_branch=`git --git-dir="$J_PATH/.git/" --work-tree="$J_PATH/" rev-parse --abbrev-ref HEAD` - _cur_ver=`git --git-dir="$J_PATH/.git/" --work-tree="$J_PATH/" name-rev --tags --name-only $_cur_branch` + _cur_ver=`git --git-dir="$J_PATH/.git/" --work-tree="$J_PATH/" name-rev --tags --name-only "$_cur_branch"` if [ "$_cur_ver" != "$J_VER_REQ" ]; then git --git-dir="$J_PATH/.git/" --work-tree="$J_PATH/" rev-parse && git --git-dir="$J_PATH/.git/" fetch --tags origin "$_cur_branch" --quiet git --git-dir="$J_PATH/.git/" --work-tree="$J_PATH/" checkout "tags/$J_VER_REQ" -b "$_cur_branch" @@ -416,8 +418,8 @@ function Joiner:menu() { while true do # run option directly if specified in argument - [ ! -z $1 ] && _switch $@ - [ ! -z $1 ] && exit 0 + [ ! -z "$1" ] && _switch $@ + [ ! -z "$1" ] && exit 0 echo "" echo "==== JOINER MENU ====" |