Ctrl K
ComunidadGuías…

environment/util.sh

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash

# Single source of truth for the toolchain versions install_custom_packages
# provisions. Everything else derives from these: release/package/mgbuild.sh
# sources this file for its DEFAULT_RUST_VERSION / DEFAULT_NODE_VERSION (which
# feed the Dockerfile build args and the post-install version checks), and
# tests/util.sh reads MG_NODE_VERSION for the node it installs on demand. Both
# stay overridable from the environment so mgbuild.sh can pin a container to the
# host's values even when the container holds an older checkout.
MG_RUST_VERSION="${MG_RUST_VERSION:-1.98.1}"
MG_NODE_VERSION="${MG_NODE_VERSION:-26.9.0}"

# Pinned installers used by install_rust / install_node. rustup-init and nvm's
# install.sh are checked against these SHA256s before they run, and the nvm
# checkout that install.sh fetches must be at MG_NVM_COMMIT (the commit the
# MG_NVM_VERSION tag points to). Bump a version together with its hashes. The mgbuild Dockerfiles and mage's make-dev-container.sh
# install through these functions too, so this is the only place to change.
MG_RUSTUP_VERSION="1.29.2"
MG_RUSTUP_SHA256_x86_64="5be191e8c70a5007074254cb74d78dbfdd25cd5a61c27de7b8e5410122ec24f4"
MG_RUSTUP_SHA256_aarch64="a9c168dbea81a84671c6b7a5c1795cb0eb699020110d107ddfca427bd797e697"
MG_NVM_VERSION="v0.40.8"
MG_NVM_INSTALL_SHA256="48a0eee9a60e07422dce0eb5774754c83889570ca1ee2566c516acbe8af03a9e"
MG_NVM_COMMIT="a885b885fef16fac4bc544188fb25e9e37ae83e8"

function operating_system() {
    if [[ "$OSTYPE" == "linux-gnu"* ]]; then
        local detected_os=$(grep -E '^(VERSION_)?ID=' /etc/os-release | \
        sort | cut -d '=' -f 2- | sed 's/"//g' | paste -s -d '-')

        # Map popular Linux distributions to supported OSes
        case "$detected_os" in
            # Ubuntu mappings
            ubuntu-22.*|ubuntu-23.*)
                echo "ubuntu-22.04"
                ;;
            ubuntu-24.*|ubuntu-25.*)
                echo "ubuntu-24.04"
                ;;
            ubuntu-26.*)
                echo "ubuntu-26.04"
                ;;
            # Linux Mint mappings
            linuxmint-20*|linuxmint-21*)
                echo "ubuntu-22.04"
                ;;
            linuxmint-22*)
                echo "ubuntu-24.04"
                ;;
            linuxmint-23*)
                echo "ubuntu-26.04"
                ;;
            # Direct mappings
            debian-11|debian-12|debian-13|centos-9|centos-10|fedora-41|fedora-42)
                echo "$detected_os"
                ;;
            # Rocky Linux mappings
            rocky-9*)
                echo "rocky-9"
                ;;
            rocky-10*)
                echo "rocky-10"
                ;;
            # Compatible mappings
            rhel-9*|almalinux-9*|amzn-2)
                echo "centos-9"
                ;;
            rhel-10*|almalinux-10*)
                echo "centos-10"
                ;;
            # Default: return the detected OS as-is
            *)
                echo "$detected_os"
                ;;
        esac
    elif [[ "$OSTYPE" == "freebsd"* ]]; then
        # A FreeBSD release is identified by its major version; uname -r carries the
        # patch and branch as well ("16.0-CURRENT"), which no consumer needs.
        echo "freebsd-$(uname -r | cut -d '.' -f 1)"
    elif [[ "$OSTYPE" == "darwin"* ]]; then
        echo "$(sw_vers -productName)-$(sw_vers -productVersion | cut -d '.' -f 1)"
    else
        echo "operating_system called on an unknown OS"
        exit 1
    fi
}

function check_operating_system() {
    # NOTE: We are actually checking for prefix because:
    #   * Rocky Linux on dnf update automatically updates minor version, at one
    #   point during install/checking version could be 9.3, while later if
    #   could be 9.5.
    if [[ "$(operating_system)" == "$1"* ]]; then
        echo "The right operating system."
    else
        echo "Not the right operating system!"
        exit 1
    fi
}

function architecture() {
    uname -m
}

check_architecture() {
    local ARCH=$(architecture)
    for arch in "$@"; do
        if [ "${ARCH}" = "$arch" ]; then
            echo "The right architecture!"
            return 0
        fi
    done
    echo "Not the right architecture!"
    echo "Expected: $@"
    echo "Actual: ${ARCH}"
    exit 1
}

function check_custom_package() {
    local pkg="$1"

    # Check against the invoking user's home when running under sudo. $USER is
    # NOT used as a fallback: it is set by login/su but absent in e.g.
    # `docker exec` shells, and the distro scripts run with `set -u`.
    local target_user="${SUDO_USER:-$(id -un)}"
    local user_home
    user_home="$(getent passwd "$target_user" | cut -d: -f6)"

    case "$pkg" in
        custom-maven*)
            if [ ! -f "/opt/apache-maven-3.9.3/bin/mvn" ]; then
                echo "$pkg"
            fi
            ;;
        custom-golang*)
            if [ ! -f "/opt/go1.18.9/go/bin/go" ]; then
                echo "$pkg"
            fi
            ;;
        custom-rust)
            if [ ! -x "$user_home/.cargo/bin/rustup" ]; then
                echo "$pkg"
            fi
            ;;
        custom-node)
            # nvm-based install (install_node); nvm has no fixed binary path,
            # so probe for any node on PATH or an nvm dir.
            if ! command -v node >/dev/null 2>&1 && [ ! -d "$user_home/.nvm" ]; then
                echo "$pkg"
            fi
            ;;
        *)
            return 1
            ;;
    esac
    return 0
}

# Retry wrapper for the network-dependent install_* functions below.
#
# Usage: retry_install <command> [args...]
#   e.g. retry_install install_rust "1.89"
#
# Retries up to RETRY_INSTALL_ATTEMPTS times (default 3), sleeping
# RETRY_INSTALL_DELAY seconds (default 10) after a failure and doubling the delay
# on each subsequent one. Returns the exit status of the last attempt, so a
# caller running under `set -e` still aborts once the retries are exhausted.
function retry_install() {
    if [ "$#" -eq 0 ]; then
        echo "retry_install: no command given" >&2
        return 2
    fi

    local attempts="${RETRY_INSTALL_ATTEMPTS:-3}"
    local delay="${RETRY_INSTALL_DELAY:-10}"
    local attempt=1
    local status

    echo "retry_install: installing '$*' (up to $attempts attempt(s))"

    while true; do
        # Left side of `||`, so errexit is suspended for the whole call - even
        # inside the callee, which carries on past a failing step and returns its
        # last command's status. Hence the `&&` chains and postcondition checks
        # in the install_* functions: one ending in a plain `echo` always looks
        # like a success and would never be retried.
        status=0
        "$@" || status=$?
        if [[ "$status" -eq 0 ]]; then
            echo "retry_install: '$*' succeeded on attempt $attempt/$attempts"
            return 0
        fi
        if [[ "$attempt" -ge "$attempts" ]]; then
            echo "retry_install: '$*' failed after $attempts attempt(s), last exit status $status" >&2
            return "$status"
        fi
        echo "retry_install: '$*' failed with exit status $status, retrying in ${delay}s (attempt $((attempt + 1))/$attempts)" >&2
        sleep "$delay"
        attempt=$((attempt + 1))
        delay=$((delay * 2))
    done
}

function install_custom_packages() {
    local packages=("$@")

    for pkg in "${packages[@]}"; do
        case "$pkg" in
            custom-maven*)
                retry_install install_custom_maven "3.9.3"
                ;;
            custom-golang*)
                retry_install install_custom_golang "1.18.9"
                ;;
            custom-rust)
                retry_install install_rust "$MG_RUST_VERSION"
                ;;
            custom-node)
                retry_install install_node "$MG_NODE_VERSION"
                ;;
        esac
    done
}

function check_all_yum() {
    local missing=""
    for pkg in $1; do
        if ! yum list installed "$pkg" >/dev/null 2>/dev/null; then
            missing="$pkg $missing"
        fi
    done
    if [ "$missing" != "" ]; then
        echo "MISSING PACKAGES: $missing"
        exit 1
    fi
}

function check_all_dpkg() {
    local missing=""
    for pkg in $1; do
        if ! dpkg -s "$pkg" >/dev/null 2>/dev/null; then
            missing="$pkg $missing"
        fi
    done
    if [ "$missing" != "" ]; then
        echo "MISSING PACKAGES: $missing"
        exit 1
    fi
}

function check_all_dnf() {
    local missing=""
    for pkg in $1; do
        if ! dnf list installed "$pkg" >/dev/null 2>/dev/null; then
            missing="$pkg $missing"
        fi
    done
    if [ "$missing" != "" ]; then
        echo "MISSING PACKAGES: $missing"
        exit 1
    fi
}

function install_all_apt() {
    for pkg in $1; do
        apt install -y "$pkg"
    done
}

function install_custom_golang() {
    # NOTE: The official https://go.dev/doc/manage-install doesn't seem to be working.
    GOVERSION="$1"
    GOARCH=$([ "$(uname -m)" = "aarch64" ] && echo "arm64" || echo "amd64")
    GOINSTALLDIR="/opt/go$GOVERSION"
    GOROOT="$GOINSTALLDIR/go" # GOPATH=$HOME/go
    if [ ! -f "$GOROOT/bin/go" ]; then
      curl -fLO --proto '=https' --proto-redir '=https' \
      https://go.dev/dl/go$GOVERSION.linux-$GOARCH.tar.gz \
        && mkdir -p "$GOINSTALLDIR" \
        && tar -C "$GOINSTALLDIR" -xzf go$GOVERSION.linux-$GOARCH.tar.gz
    fi
    if [ ! -f "$GOROOT/bin/go" ]; then
      echo "go $GOVERSION installation failed, $GOROOT/bin/go is missing" >&2
      return 1
    fi
    echo "go $GOVERSION installed under $GOROOT"
}

function install_custom_maven() {
  MVNVERSION="$1"
  MVNINSTALLDIR="/opt/apache-maven-$MVNVERSION"
  MVNURL="https://s3.eu-west-1.amazonaws.com/deps.memgraph.io/maven/apache-maven-$MVNVERSION-bin.tar.gz"
  if [ ! -f "$MVNINSTALLDIR/bin/mvn" ]; then
    echo "Downloading maven from $MVNURL"
    curl -fLO --proto '=https' --proto-redir '=https' "$MVNURL" \
      && tar -C "/opt" -xzf "apache-maven-$MVNVERSION-bin.tar.gz"
  fi
  if [ ! -f "$MVNINSTALLDIR/bin/mvn" ]; then
    echo "maven $MVNVERSION installation failed, $MVNINSTALLDIR/bin/mvn is missing" >&2
    return 1
  fi
  echo "maven $MVNVERSION installed under $MVNINSTALLDIR"
}

function install_dotnet_sdk ()
{
  DOTNETSDKVERSION="$1"
  DOTNETSDKINSTALLDIR="/opt/dotnet-sdk-$DOTNETSDKVERSION"
  if [ ! -d $DOTNETSDKINSTALLDIR ]; then
    mkdir -p $DOTNETSDKINSTALLDIR
  fi
  if [ ! -f "$DOTNETSDKINSTALLDIR/dotnet" ]; then
    wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \
      && chmod +x ./dotnet-install.sh \
      && ./dotnet-install.sh --channel 8.0 --install-dir $DOTNETSDKINSTALLDIR \
      && rm dotnet-install.sh \
      && ln -sf $DOTNETSDKINSTALLDIR/dotnet /usr/bin/dotnet
  fi
  if [ ! -f "$DOTNETSDKINSTALLDIR/dotnet" ]; then
    echo "dotnet sdk $DOTNETSDKVERSION installation failed, $DOTNETSDKINSTALLDIR/dotnet is missing" >&2
    return 1
  fi
  echo "dotnet sdk $DOTNETSDKVERSION installed under $DOTNETSDKINSTALLDIR"
}

function install_rust() {
  local rust_version="$1"
  local target_user="${SUDO_USER:-$(id -un)}"
  local target_home
  local arch rustup_sha256

  target_home="$(getent passwd "$target_user" | cut -d: -f6)"

  arch="$(uname -m)"
  case "$arch" in
    x86_64) rustup_sha256="$MG_RUSTUP_SHA256_x86_64" ;;
    aarch64) rustup_sha256="$MG_RUSTUP_SHA256_aarch64" ;;
    *) echo "install_rust: unsupported architecture $arch" >&2; return 1 ;;
  esac

  # Drop privileges only when there is someone to drop to — when the target
  # user IS the current user (no sudo, e.g. inside a container), run directly:
  # the sudo binary may not even exist there.
  local -a run_as=()
  if [[ "$target_user" != "$(id -un)" ]]; then
    run_as=(sudo -u "$target_user")
  fi

  "${run_as[@]}" env HOME="$target_home" \
    bash -c '
      set -euo pipefail

      # rustup-init picks its mode from argv[0], so keep the file name.
      tmp_dir="$(mktemp -d)"
      trap "rm -rf \"$tmp_dir\"" EXIT
      rustup_init="$tmp_dir/rustup-init"
      curl --proto "=https" --proto-redir "=https" --tlsv1.2 -sSfo "$rustup_init" \
        "https://static.rust-lang.org/rustup/archive/$2/$3-unknown-linux-gnu/rustup-init"
      echo "$4  $rustup_init" | sha256sum -c -
      chmod +x "$rustup_init"
      "$rustup_init" -y --default-toolchain "$1"
    ' bash "$rust_version" "$MG_RUSTUP_VERSION" "$arch" "$rustup_sha256"
}

function install_node() {
  local node_version="$1"
  local target_user="${SUDO_USER:-$(id -un)}"
  local target_home

  target_home="$(getent passwd "$target_user" | cut -d: -f6)"

  # See install_rust: skip sudo when already running as the target user.
  local -a run_as=()
  if [[ "$target_user" != "$(id -un)" ]]; then
    run_as=(sudo -u "$target_user")
  fi

  "${run_as[@]}" env HOME="$target_home" \
    bash -c '
      set -euo pipefail

      export NVM_DIR="$HOME/.nvm"

      nvm_install="$(mktemp)"
      trap "rm -f \"$nvm_install\"" EXIT
      curl -fsSL --proto "=https" --proto-redir "=https" --tlsv1.2 -o "$nvm_install" \
        "https://raw.githubusercontent.com/nvm-sh/nvm/$2/install.sh"
      echo "$3  $nvm_install" | sha256sum -c -
      bash "$nvm_install"
      # install.sh only fetches nvm by tag; make sure it is the pinned commit.
      nvm_commit="$(git -C "$NVM_DIR" rev-parse HEAD)"
      if [[ "$nvm_commit" != "$4" ]]; then
        echo "nvm checkout is at $nvm_commit, expected $4" >&2
        exit 1
      fi

      . "$NVM_DIR/nvm.sh"

      nvm install "$1"
      nvm use "$1"
      nvm alias default "$1"
    ' bash "$node_version" "$MG_NVM_VERSION" "$MG_NVM_INSTALL_SHA256" "$MG_NVM_COMMIT"
}

# Resolve a Python interpreter >= 3.10 and print its absolute path. Some
# distros default to an older python3 but ship a newer versioned binary
# alongside (e.g. centos-9: python3 = 3.9, python3.12 installed) — prefer the
# default python3 when it qualifies, otherwise fall back to the newest
# versioned executable. $MG_PYTHON overrides the search entirely; if it is set
# but too old, that is a hard error rather than a silent fallback.
function resolve_python() {
    local check='import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)'
    if [[ -n "${MG_PYTHON:-}" ]]; then
        if ! command -v "$MG_PYTHON" >/dev/null 2>&1; then
            echo "Error: MG_PYTHON='$MG_PYTHON' not found" >&2
            return 1
        fi
        if ! "$MG_PYTHON" -c "$check" 2>/dev/null; then
            echo "Error: MG_PYTHON='$MG_PYTHON' is $("$MG_PYTHON" --version 2>&1), but >= 3.10 is required" >&2
            return 1
        fi
        command -v "$MG_PYTHON"
        return 0
    fi
    local candidate
    for candidate in python3 python3.14 python3.13 python3.12 python3.11 python3.10; do
        if command -v "$candidate" >/dev/null 2>&1 \
            && "$candidate" -c "$check" 2>/dev/null; then
            command -v "$candidate"
            return 0
        fi
    done
    echo "Error: no Python >= 3.10 found (searched python3, python3.14..python3.10; set MG_PYTHON to override)" >&2
    return 1
}

function parse_operating_system() {
    local os_output=$(operating_system)
    local os_name=""
    local os_version=""

    # Split on the first dash to separate OS name from version
    if [[ "$os_output" == *"-"* ]]; then
        os_name=$(echo "$os_output" | cut -d'-' -f1)
        os_version=$(echo "$os_output" | cut -d'-' -f2-)
    else
        # If no dash found, treat the whole string as OS name
        os_name="$os_output"
        os_version=""
    fi

    # Export variables for use by calling script
    export OS="$os_name"
    export VER="$os_version"

    echo "OS: $OS"
    echo "VER: $VER"
}

# Ensure the unversioned `libpython3.so` SONAME exists in the system library
# directory so that:
#   1. CMake's find_library(MG_LIBPYTHON3_SO python3) picks it up at build
#      time and links memgraph against it.
#   2. The patchelf POST_BUILD step that rewrites DT_NEEDED to libpython3.so
#      produces a binary that can actually load (the rewritten binary needs
#      this file to exist on the dynamic linker's search path).
#
# Some distros (Fedora, RHEL, conda, manylinux) ship a real abi3 stub library
# with SONAME=libpython3.so — in that case we do nothing. Debian/Ubuntu ship
# only versioned libpython files and leave this symlink to the system admin;
# we create one pointing at the highest-numbered installed libpython3.X.
#
# Idempotent: if libpython3.so already exists (real stub or prior symlink),
# this is a no-op. Pass `--force` to overwrite an existing symlink (e.g. to
# repoint at a different minor version).
function ensure_libpython3_so_symlink() {
    local force=false
    if [[ "${1:-}" == "--force" ]]; then
        force=true
    fi

    # Locate the highest-numbered versioned libpython wherever the distro keeps
    # it, then create the symlink next to it. We deliberately do NOT rely on
    # dpkg-architecture (it ships in dpkg-dev, which is not guaranteed to be
    # installed) to guess a single library directory: on Debian/Ubuntu libpython
    # lives in a multiarch subdir (e.g. /usr/lib/x86_64-linux-gnu), so guessing
    # /usr/lib or /usr/lib64 would miss it and wrongly conclude none exists.
    # Instead search the loader cache plus the common locations directly,
    # covering multiarch subdirs and flat libdirs (Fedora/RHEL /usr/lib64).
    # sort -V handles 3.10 > 3.9 correctly.
    local target
    target="$( { ldconfig -p 2>/dev/null | grep -oE '/[^ ]*libpython3\.[0-9]+[a-z]*\.so\.1\.0';
                 ls -1 /usr/lib/*/libpython3.*.so.1.0 \
                       /lib/*/libpython3.*.so.1.0 \
                       /usr/lib64/libpython3.*.so.1.0 \
                       /usr/lib/libpython3.*.so.1.0 2>/dev/null; } \
               | sort -V | tail -1 )"
    if [[ -z "$target" ]]; then
        echo "ensure_libpython3_so_symlink: no libpython3.*.so.1.0 found on the system; skipping"
        return 0
    fi

    # Create the abi3 SONAME symlink alongside the versioned library. Distros
    # that ship a real abi3 stub (Fedora, RHEL, conda, manylinux) already have a
    # libpython3.so here, so we no-op unless --force is given.
    local libdir
    libdir="$(dirname "$target")"
    if [[ -e "$libdir/libpython3.so" && "$force" == false ]]; then
        echo "ensure_libpython3_so_symlink: $libdir/libpython3.so already present; nothing to do"
        return 0
    fi

    ln -sf "$(basename "$target")" "$libdir/libpython3.so"
    echo "ensure_libpython3_so_symlink: $libdir/libpython3.so -> $(basename "$target")"
}

# Function to parse --skip-check flag from command line arguments
# Usage: parse_skip_check_flag
# Sets SKIP_CHECK variable and removes --skip-check from $@
function parse_skip_check_flag() {
    SKIP_CHECK=false
    local args=()

    while [[ $# -gt 0 ]]; do
        case $1 in
            --skip-check)
                SKIP_CHECK=true
                shift
                ;;
            *)
                args+=("$1")
                shift
                ;;
        esac
    done

    echo "$SKIP_CHECK"
}