#!/usr/bin/env bash
# Verify that the effective user ID is 0 (root), terminating with an error otherwise.
check_running_as_root() {
if [ "$(id -u)" != "0" ]; then
die "This command must be run as root."
fi
}
# Detect and populate the running Linux distribution name in the global OS variable.
detect_os() {
if [ -f /etc/lsb-release ] && command -v lsb_release >/dev/null 2>&1; then
OS=$(lsb_release -si)
elif [ -f /etc/os-release ]; then
OS=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"')
elif [ -f /etc/redhat-release ]; then
OS=$(awk '{print $1}' /etc/redhat-release)
elif [ -f /etc/arch-release ]; then
OS="Arch Linux"
else
die "Unsupported operating system"
fi
}
# Check whether the detected operating system belongs to the Red Hat enterprise family.
is_redhat_family_os() {
[[ "${OS:-}" == "CentOS"* ]] ||
[[ "${OS:-}" == "AlmaLinux"* ]] ||
[[ "${OS:-}" == "Rocky"* ]] ||
[[ "${OS:-}" == "Red Hat"* ]] ||
[[ "${OS:-}" == "Oracle Linux"* ]] ||
[[ "${OS:-}" == "Amazon Linux"* ]]
}
# Select either dnf or yum as the active package manager for Red Hat distributions.
select_redhat_package_manager() {
if command -v dnf >/dev/null 2>&1; then
PKG_MANAGER="dnf"
elif command -v yum >/dev/null 2>&1; then
PKG_MANAGER="yum"
else
die "Neither yum nor dnf was found. Please install packages manually."
fi
}
# Attempt to install and enable the EPEL repository on Red Hat family distributions.
enable_epel_if_available() {
if $PKG_MANAGER install -y -q epel-release >/dev/null 2>&1; then
return
fi
colorized_echo yellow "Could not enable EPEL automatically; continuing with configured repositories."
}
# Emit a warning when package manager repository metadata refresh cannot be completed.
warn_package_metadata_refresh_failed() {
colorized_echo yellow "Could not refresh package metadata; continuing with configured repositories."
}
# Identify the system package manager and refresh available repository metadata.
detect_and_update_package_manager() {
if [ -z "${OS:-}" ]; then
detect_os
fi
colorized_echo blue "Updating package manager"
if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then
PKG_MANAGER="apt-get"
$PKG_MANAGER update -qq >/dev/null 2>&1 || warn_package_metadata_refresh_failed
elif is_redhat_family_os; then
select_redhat_package_manager
$PKG_MANAGER -y -q makecache >/dev/null 2>&1 || warn_package_metadata_refresh_failed
enable_epel_if_available
elif [[ "$OS" == "Fedora"* ]]; then
PKG_MANAGER="dnf"
$PKG_MANAGER -q -y makecache >/dev/null 2>&1 || warn_package_metadata_refresh_failed
elif [[ "$OS" == "Arch Linux" ]] || [[ "$OS" == "Arch"* ]]; then
PKG_MANAGER="pacman"
$PKG_MANAGER -Sy --noconfirm --quiet >/dev/null 2>&1 || warn_package_metadata_refresh_failed
elif [[ "$OS" == "openSUSE"* ]]; then
PKG_MANAGER="zypper"
$PKG_MANAGER refresh --quiet >/dev/null 2>&1 || warn_package_metadata_refresh_failed
else
die "Unsupported operating system"
fi
}
# Attempt to install a package, returning non-zero on failure instead of
# aborting. Use this when the caller wants to handle a failed install itself
# (e.g. fall back to an alternative package name); most callers want
# install_package, which aborts on failure.
_try_install_package_impl() {
local package="$1"
if [ -z "${OS:-}" ]; then
detect_os
fi
if [ -z "${PKG_MANAGER:-}" ]; then
detect_and_update_package_manager
fi
colorized_echo blue "Installing $package"
if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then
$PKG_MANAGER -y -qq install "$package" >/dev/null 2>&1
elif is_redhat_family_os; then
$PKG_MANAGER install -y -q "$package" >/dev/null 2>&1
elif [[ "$OS" == "Fedora"* ]]; then
$PKG_MANAGER install -y -q "$package" >/dev/null 2>&1
elif [[ "$OS" == "Arch Linux" ]] || [[ "$OS" == "Arch"* ]]; then
$PKG_MANAGER -S --noconfirm --quiet "$package" >/dev/null 2>&1
elif [[ "$OS" == "openSUSE"* ]]; then
$PKG_MANAGER --quiet install -y "$package" >/dev/null 2>&1
else
die "Unsupported operating system"
fi
}
# Install a system package via the active package manager or exit with a failure message.
install_package() {
local pkg="$1"
local cmd="$pkg"
case "$pkg" in
net-tools) cmd=netstat ;;
esac
# An already present binary never needs a repository.
if command -v "$cmd" >/dev/null 2>&1; then
return 0
fi
# jq is required by nearly every code path; a static build ships with the
# mirror so a minimal offline host can still finish the install.
if [ "$pkg" = "jq" ] && _install_jq_static; then
return 0
fi
if try_install_package "$pkg"; then
return 0
fi
die "The offline mirror cannot install '$pkg' on this host. Install it manually and re-run."
}
# jq from a static binary, so the distro repositories are never required.
_install_jq_static() {
local arch dest=/usr/local/bin/jq
case "$(uname -m)" in
x86_64|amd64) arch=amd64 ;;
aarch64|arm64) arch=arm64 ;;
*) return 1 ;;
esac
colorized_echo blue "Installing jq from the mirror (static build)"
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 "https://git.nimbaha.org/pgnode/bin/jq/jq-linux-${arch}" -o "$dest.tmp"; then
chmod 755 "$dest.tmp" && mv -f "$dest.tmp" "$dest"
return $?
fi
rm -f "$dest.tmp"
return 1
}
# Verify or install an interactive terminal editor (nano or vi) and assign EDITOR.
check_editor() {
if [ -z "${EDITOR:-}" ]; then
if command -v nano >/dev/null 2>&1; then
EDITOR="nano"
elif command -v vi >/dev/null 2>&1; then
EDITOR="vi"
else
detect_os
try_install_package nano || true
EDITOR="nano"
fi
fi
}
# Identify the Linux machine architecture and assign the normalized ARCH variable.
identify_the_operating_system_and_architecture() {
if [[ "$(uname)" != "Linux" ]]; then
die "error: This operating system is not supported."
fi
case "$(uname -m)" in
i386 | i686)
ARCH='32'
;;
amd64 | x86_64)
ARCH='64'
;;
armv5tel)
ARCH='arm32-v5'
;;
armv6l)
ARCH='arm32-v6'
grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5'
;;
armv7 | armv7l)
ARCH='arm32-v7a'
grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5'
;;
armv8 | aarch64)
ARCH='arm64-v8a'
;;
mips)
ARCH='mips32'
;;
mipsle)
ARCH='mips32le'
;;
mips64)
ARCH='mips64'
lscpu | grep -q "Little Endian" && ARCH='mips64le'
;;
mips64le)
ARCH='mips64le'
;;
ppc64)
ARCH='ppc64'
;;
ppc64le)
ARCH='ppc64le'
;;
riscv64)
ARCH='riscv64'
;;
s390x)
ARCH='s390x'
;;
*)
die "error: The architecture is not supported."
;;
esac
}
# Download and install the architecture-appropriate yq YAML processor binary into /usr/local/bin.
install_yq() {
local base_url="https://git.nimbaha.org/pgnode/github/mikefarah/yq/releases/latest/download"
local yq_binary=""
local yq_url=""
local binary_tmp=""
if command -v yq >/dev/null 2>&1; then
colorized_echo green "yq is already installed."
return
fi
identify_the_operating_system_and_architecture
case "$ARCH" in
64 | x86_64)
yq_binary="yq_linux_amd64"
;;
arm32-v7a | arm32-v6 | arm32-v5 | armv7l)
yq_binary="yq_linux_arm"
;;
arm64-v8a | aarch64)
yq_binary="yq_linux_arm64"
;;
32 | i386 | i686)
yq_binary="yq_linux_386"
;;
*)
die "Unsupported architecture: $ARCH"
;;
esac
yq_url="${base_url}/${yq_binary}"
colorized_echo blue "Downloading yq from ${yq_url}..."
if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
colorized_echo yellow "Neither curl nor wget is installed. Attempting to install curl."
install_package curl || die "Failed to install curl. Please install curl or wget manually."
fi
binary_tmp=$(create_temp_file "yq" ".bin")
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$yq_url" -o "$binary_tmp" || die "Failed to download yq using curl. Please check your internet connection."
elif command -v wget >/dev/null 2>&1; then
wget -q -O "$binary_tmp" "$yq_url" || die "Failed to download yq using wget. Please check your internet connection."
fi
install -m 755 "$binary_tmp" /usr/local/bin/yq
colorized_echo green "yq installed successfully!"
if ! echo "$PATH" | grep -q "/usr/local/bin"; then
export PATH="/usr/local/bin:$PATH"
fi
rm -f "$binary_tmp"
}
# ---------------------------------------------------------------------------
# Temporary Iranian package mirrors
#
# Nothing below runs unless a package install has already failed, i.e. the host
# cannot reach its configured repositories. The original configuration is
# copied aside before a single byte is rewritten and an EXIT trap puts it back,
# so the host is left exactly as it was found even if the installer dies or is
# interrupted half way.
#
# The caller supplies the wrapper (see the end of this file's integration in
# each patcher): it must call _pg_ir_enable only after its own normal install
# path has failed once.
# ---------------------------------------------------------------------------
IR_SRC_BACKUP=/var/backups/ir-mirror-sources
_pg_ir_mirror_active=0
# Self-contained so this block can be inlined into scripts that do not define
# an is_redhat_family_os helper.
_pg_ir_is_redhat() {
case "${OS:-}" in
CentOS*|AlmaLinux*|Rocky*|Red\ Hat*|Oracle*|Fedora*) return 0 ;;
esac
return 1
}
# Probe for each base URL read from stdin; print the first that answers.
_pg_ir_pick() {
local suffix="$1" base
while IFS= read -r base; do
[ -n "$base" ] || continue
if curl -fsSL --connect-timeout 6 --max-time 12 -o /dev/null -r 0-0 "$base$suffix" 2>/dev/null; then
printf '%s\n' "$base"
return 0
fi
done
return 1
}
_pg_ir_backup() {
mkdir -p "$IR_SRC_BACKUP" || return 1
rm -rf "$IR_SRC_BACKUP"/* 2>/dev/null
if [ -f /etc/apt/sources.list ]; then
cp -a /etc/apt/sources.list "$IR_SRC_BACKUP/sources.list" || return 1
fi
if [ -d /etc/apt/sources.list.d ]; then
mkdir -p "$IR_SRC_BACKUP/sources.list.d"
cp -a /etc/apt/sources.list.d/. "$IR_SRC_BACKUP/sources.list.d/" 2>/dev/null || true
fi
if [ -d /etc/yum.repos.d ]; then
mkdir -p "$IR_SRC_BACKUP/yum.repos.d"
cp -a /etc/yum.repos.d/. "$IR_SRC_BACKUP/yum.repos.d/" 2>/dev/null || true
fi
return 0
}
_pg_ir_restore() {
[ "$_pg_ir_mirror_active" = 1 ] || return 0
_pg_ir_mirror_active=0
trap - EXIT INT TERM
if [ -d "$IR_SRC_BACKUP/sources.list.d" ]; then
rm -rf /etc/apt/sources.list.d
mkdir -p /etc/apt/sources.list.d
cp -a "$IR_SRC_BACKUP/sources.list.d/." /etc/apt/sources.list.d/ 2>/dev/null || true
fi
if [ -f "$IR_SRC_BACKUP/sources.list" ]; then
cp -a "$IR_SRC_BACKUP/sources.list" /etc/apt/sources.list 2>/dev/null || true
fi
if [ -d "$IR_SRC_BACKUP/yum.repos.d" ]; then
rm -rf /etc/yum.repos.d
mkdir -p /etc/yum.repos.d
cp -a "$IR_SRC_BACKUP/yum.repos.d/." /etc/yum.repos.d/ 2>/dev/null || true
fi
# Deliberately no `apt-get update` here: the original repositories are
# unreachable by definition, so refreshing would just hang the exit.
colorized_echo green "Package repositories restored to their original configuration."
}
_pg_ir_enable() {
[ "$_pg_ir_mirror_active" = 1 ] && return 0
[ -n "${OS:-}" ] || detect_os
local codename base sec rv arch comps
codename=$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release 2>/dev/null | tr -d '"')
if [ -z "$codename" ]; then
colorized_echo yellow "No release codename found; cannot select an Iranian mirror."
return 1
fi
if [ "$OS" = "Ubuntu" ] || [ "${OS#Debian}" != "$OS" ]; then
if [ "$OS" = "Ubuntu" ]; then
base=$(_pg_ir_pick "/dists/$codename/Release" <<'CAND'
https://mirror.arvancloud.ir/ubuntu
https://repo.iut.ac.ir/ubuntu
https://mirror.iranserver.com/ubuntu
CAND
)
else
base=$(_pg_ir_pick "/dists/$codename/Release" <<'CAND'
https://mirror.arvancloud.ir/debian
https://repo.iut.ac.ir/debian
CAND
)
sec=$(_pg_ir_pick "/dists/$codename-security/Release" <<'CAND'
https://mirror.arvancloud.ir/debian-security
CAND
)
fi
if [ -z "$base" ]; then
colorized_echo yellow "No reachable Iranian mirror for $OS $codename."
return 1
fi
_pg_ir_backup || return 1
mkdir -p /etc/apt/sources.list.d
# Third-party lists may point abroad as well; park them for the duration.
find /etc/apt/sources.list.d -maxdepth 1 -name '*.list' -exec rm -f {} + 2>/dev/null || true
if [ "$OS" = "Ubuntu" ]; then
cat > /etc/apt/sources.list < /etc/apt/sources.list <> /etc/apt/sources.list
fi
fi
_pg_ir_mirror_active=1
trap '_pg_ir_restore' EXIT INT TERM
colorized_echo yellow "Package repositories temporarily switched to $base; they will be restored on exit."
apt-get update -qq >/dev/null 2>&1 || true
return 0
fi
if _pg_ir_is_redhat; then
rv=$(rpm -E %{rhel} 2>/dev/null)
case "$(uname -m)" in
x86_64|amd64) arch=x86_64 ;;
aarch64|arm64) arch=aarch64 ;;
*) return 1 ;;
esac
[ -n "$rv" ] || return 1
base=$(_pg_ir_pick "/almalinux/$rv/BaseOS/$arch/os/repodata/repomd.xml" <<'CAND'
https://mirror.iranserver.com/almalinux
https://mirror.arvancloud.ir/almalinux
CAND
)
if [ -z "$base" ]; then
colorized_echo yellow "No reachable Iranian mirror for $OS $rv."
return 1
fi
_pg_ir_backup || return 1
mkdir -p /etc/yum.repos.d
rm -f /etc/yum.repos.d/*.repo
cat > /etc/yum.repos.d/ir-temp-mirror.repo </dev/null 2>&1 || true
return 0
fi
colorized_echo yellow "No Iranian mirror recipe for $OS."
return 1
}
# Retry a failed install through a temporary Iranian mirror. This is the only
# entry point: the mirror is never enabled speculatively.
try_install_package() {
local package="$1"
if _try_install_package_impl "$package"; then
return 0
fi
colorized_echo yellow "Package repositories are unreachable; retrying through an Iranian mirror."
_pg_ir_enable || return 1
_try_install_package_impl "$package"
}