environment/util.sh
Rama master · réplica de lectura del repositorio original.
#!/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"
}