461 lines
17 KiB
Bash
Executable File
461 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Push caller's shell options (quietly)
|
|
{ SHELLOPTS_STACK="${SHELLOPTS_STACK-}|$(set +o); set -$-"; } 2> /dev/null
|
|
|
|
set -euxo pipefail
|
|
|
|
SCRIPT_DIR=$(builtin cd "$(dirname "${BASH_SOURCE:-$0}")"; pwd)
|
|
WORKSPACE_DIR="${SCRIPT_DIR}/../.."
|
|
|
|
# importing install_miniforge function
|
|
source "${SCRIPT_DIR}/install-miniforge.sh"
|
|
|
|
pkg_install_helper() {
|
|
case "${OSTYPE}" in
|
|
darwin*)
|
|
brew install "$@"
|
|
;;
|
|
linux*)
|
|
sudo apt-get install -qq -o=Dpkg::Use-Pty=0 "$@" | {
|
|
grep --line-buffered -v "^\(Preparing to unpack\|Unpacking\|Processing triggers for\) "
|
|
}
|
|
;;
|
|
*) false;;
|
|
esac
|
|
}
|
|
|
|
install_bazel() {
|
|
if command -v bazel; then
|
|
if [[ -n "${BUILDKITE-}" && "${OSTYPE}" != "msys" ]]; then
|
|
# Only reinstall Bazel if we need to upgrade to a different version.
|
|
python="$(command -v python3 || command -v python || echo python)"
|
|
current_version="$(bazel --version | grep -o "[0-9]\+.[0-9]\+.[0-9]\+")"
|
|
new_version="$(cat "${WORKSPACE_DIR}/.bazelversion")"
|
|
if [[ "$current_version" == "$new_version" ]]; then
|
|
echo "Bazel of the same version already exists, skipping the install"
|
|
export BAZEL_CONFIG_ONLY=1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
"${SCRIPT_DIR}"/install-bazel.sh
|
|
}
|
|
|
|
install_base() {
|
|
if [[ -n "${BUILDKITE-}" ]]; then
|
|
echo "Skipping install_base in Buildkite"
|
|
return
|
|
fi
|
|
|
|
case "${OSTYPE}" in
|
|
linux*)
|
|
# Expired apt key error: https://github.com/bazelbuild/bazel/issues/11470#issuecomment-633205152
|
|
curl -f -s -L -R https://bazel.build/bazel-release.pub.gpg | sudo apt-key add - || true
|
|
sudo apt-get update -qq
|
|
pkg_install_helper build-essential curl unzip libunwind-dev python3-pip python3-setuptools \
|
|
tmux gdb
|
|
if [[ "${LINUX_WHEELS-}" == 1 ]]; then
|
|
pkg_install_helper docker
|
|
if [[ -n "${TRAVIS-}" ]]; then
|
|
sudo usermod -a -G docker travis
|
|
fi
|
|
fi
|
|
if [[ -n "${PYTHON-}" ]]; then
|
|
"${SCRIPT_DIR}/install-strace.sh" || true
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
install_shellcheck() {
|
|
local shellcheck_version="0.7.1"
|
|
if [[ "${shellcheck_version}" != "$(command -v shellcheck > /dev/null && shellcheck --version | sed -n "s/version: //p")" ]]; then
|
|
local osname=""
|
|
case "${OSTYPE}" in
|
|
linux*) osname="linux";;
|
|
darwin*) osname="darwin";;
|
|
esac
|
|
local name="shellcheck-v${shellcheck_version}"
|
|
if [[ "${osname}" == "linux" || "${osname}" == "darwin" ]]; then
|
|
sudo mkdir -p /usr/local/bin || true
|
|
curl -sSfL "https://github.com/koalaman/shellcheck/releases/download/v${shellcheck_version}/${name}.${osname}.x86_64.tar.xz" | {
|
|
sudo tar -C /usr/local/bin -x -v -J --strip-components=1 "${name}/shellcheck"
|
|
}
|
|
else
|
|
mkdir -p /usr/local/bin
|
|
curl -sSfL -o "${name}.zip" "https://github.com/koalaman/shellcheck/releases/download/v${shellcheck_version}/${name}.zip"
|
|
unzip "${name}.zip" "${name}.exe"
|
|
mv -f "${name}.exe" "/usr/local/bin/shellcheck.exe"
|
|
fi
|
|
test "${shellcheck_version}" = "$(shellcheck --version | sed -n "s/version: //p")"
|
|
fi
|
|
}
|
|
|
|
install_linters() {
|
|
pip install -r "${WORKSPACE_DIR}"/python/requirements/lint-requirements.txt
|
|
|
|
install_shellcheck
|
|
}
|
|
|
|
install_nvm() {
|
|
local NVM_HOME="${HOME}/.nvm"
|
|
if [[ "${OSTYPE}" == msys ]]; then
|
|
local ver="1.1.7"
|
|
if [[ ! -f "${NVM_HOME}/nvm.sh" ]]; then
|
|
mkdir -p -- "${NVM_HOME}"
|
|
export NVM_SYMLINK="${PROGRAMFILES}\nodejs"
|
|
(
|
|
cd "${NVM_HOME}"
|
|
local target="./nvm-${ver}.zip"
|
|
curl -sSfL -o "${target}" \
|
|
"https://github.com/coreybutler/nvm-windows/releases/download/${ver}/nvm-noinstall.zip"
|
|
unzip -q -- "${target}"
|
|
rm -f -- "${target}"
|
|
printf "%s\r\n" "root: $(cygpath -w -- "${NVM_HOME}")" "path: ${NVM_SYMLINK}" > settings.txt
|
|
)
|
|
printf "%s\n" \
|
|
"export NVM_HOME=\"$(cygpath -w -- "${NVM_HOME}")\"" \
|
|
"nvm() { \"\${NVM_HOME}/nvm.exe\" \"\$@\"; }" \
|
|
> "${NVM_HOME}/nvm.sh"
|
|
fi
|
|
elif [[ -n "${BUILDKITE-}" ]]; then
|
|
echo "Skipping nvm on Buildkite because we will use apt-get."
|
|
else
|
|
test -f "${NVM_HOME}/nvm.sh" # double-check NVM is already available on other platforms
|
|
fi
|
|
}
|
|
|
|
install_upgrade_pip() {
|
|
local python=python
|
|
if command -v python3 > /dev/null; then
|
|
python=python3
|
|
fi
|
|
|
|
if "${python}" -m pip --version || "${python}" -m ensurepip; then # Configure pip if present
|
|
# 25.3 has breaking change where other Python packages like "click" does not work
|
|
# with it anymore. pip-compile will fail to work with the package's setup code.
|
|
"${python}" -m pip install pip==25.2
|
|
|
|
# If we're in a CI environment, do some configuration
|
|
if [[ "${CI-}" == "true" ]]; then
|
|
"${python}" -W ignore -m pip config -q --user set global.disable-pip-version-check True
|
|
"${python}" -W ignore -m pip config -q --user set global.progress_bar off
|
|
fi
|
|
|
|
"${python}" -m ensurepip
|
|
fi
|
|
}
|
|
|
|
install_node() {
|
|
if [[ "${OSTYPE}" == "msys" ]]; then
|
|
{ echo "WARNING: Skipping running Node.js due to incompatibilities with Windows"; } 2> /dev/null
|
|
return
|
|
fi
|
|
|
|
if [[ "${BUILDKITE-}" == "" ]] ; then
|
|
echo "Skipping install_node in non-Buildkite environment is not supported" > /dev/stderr
|
|
return
|
|
fi
|
|
|
|
if [[ "${OSTYPE}" = darwin* ]]; then
|
|
curl -sSfL -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
|
|
|
(
|
|
set +x # suppress set -x since it'll get very noisy here.
|
|
# nvm's error-recovery path in nvm.sh references $TMPDIR. The caller
|
|
# runs with `set -u`, so an unset TMPDIR (launchd-spawned shells don't
|
|
# inherit one) crashes nvm with "TMPDIR: unbound variable" whenever a
|
|
# binary download fails and nvm falls back to source.
|
|
export TMPDIR="${TMPDIR:-/tmp}"
|
|
. "${HOME}/.nvm/nvm.sh"
|
|
# Node 14 EOL'd April 2023; nodejs.org removed the darwin-arm64
|
|
# prebuilt and the URL now 404s. 20 is the current LTS line.
|
|
NODE_VERSION="20"
|
|
nvm install $NODE_VERSION
|
|
nvm use --silent $NODE_VERSION
|
|
npm config set loglevel warn # make NPM quieter
|
|
)
|
|
else
|
|
# Linux
|
|
|
|
NODE_VERSION_FULL="14.21.3"
|
|
|
|
if [[ "$(uname -m)" == "x86_64" ]]; then
|
|
NODE_URL="https://nodejs.org/dist/v${NODE_VERSION_FULL}/node-v${NODE_VERSION_FULL}-linux-x64.tar.xz"
|
|
NODE_SHA256="05c08a107c50572ab39ce9e8663a2a2d696b5d262d5bd6f98d84b997ce932d9a"
|
|
else # aarch64
|
|
NODE_URL="https://nodejs.org/dist/v${NODE_VERSION_FULL}/node-v${NODE_VERSION_FULL}-linux-arm64.tar.xz"
|
|
NODE_SHA256="f06642bfcf0b8cc50231624629bec58b183954641b638e38ed6f94cd39e8a6ef"
|
|
fi
|
|
|
|
NODE_DIR="/usr/local/node"
|
|
curl -fsSL "${NODE_URL}" -o /tmp/node.tar.xz
|
|
echo "$NODE_SHA256 /tmp/node.tar.xz" | sha256sum -c -
|
|
sudo mkdir -p "$NODE_DIR"
|
|
sudo tar -xf /tmp/node.tar.xz -C "$NODE_DIR" --strip-components=1
|
|
rm /tmp/node.tar.xz
|
|
sudo ln -sf /usr/local/node/bin/node /usr/local/bin/node
|
|
sudo ln -sf /usr/local/node/bin/npm /usr/local/bin/npm
|
|
sudo ln -sf /usr/local/node/bin/npx /usr/local/bin/npx
|
|
|
|
fi
|
|
}
|
|
|
|
install_toolchains() {
|
|
if [[ -z "${BUILDKITE-}" ]]; then
|
|
"${SCRIPT_DIR}"/install-toolchains.sh
|
|
fi
|
|
if [[ "${OSTYPE}" = linux* ]]; then
|
|
pushd "${WORKSPACE_DIR}"
|
|
"${SCRIPT_DIR}"/install-llvm-binaries.sh
|
|
popd
|
|
fi
|
|
}
|
|
|
|
download_mnist() {
|
|
if [[ -d "${HOME}/data/MNIST" ]]; then
|
|
return
|
|
fi
|
|
mkdir -p "${HOME}/data"
|
|
curl -o "${HOME}/data/mnist.zip" https://ray-ci-mnist.s3-us-west-2.amazonaws.com/mnist.zip
|
|
unzip "${HOME}/data/mnist.zip" -d "${HOME}/data"
|
|
}
|
|
|
|
retry_pip_install() {
|
|
local status="0"
|
|
local errmsg=""
|
|
|
|
# Try n times; we often encounter OpenSSL.SSL.WantReadError (or others)
|
|
# that break the entire CI job: Simply retry installation in this case
|
|
# after n seconds.
|
|
for _ in {1..3}; do
|
|
errmsg="$("$@" 2>&1)" && break
|
|
status=$errmsg && echo "'pip install ...' failed, will retry after n seconds!" && sleep 30
|
|
done
|
|
if [[ "$status" != "0" ]]; then
|
|
echo "${status}" && return 1
|
|
fi
|
|
}
|
|
|
|
install_pip_packages() {
|
|
# Install modules needed in all jobs.
|
|
# shellcheck disable=SC2262
|
|
alias pip="python -m pip"
|
|
|
|
# Array to hold all requirements files to install later
|
|
requirements_files=()
|
|
# Single packages to install in sync with files
|
|
requirements_packages=()
|
|
# Packages to install _after_ previous files have been installed
|
|
# (e.g. to install a custom pyarrow or torch version). This
|
|
# would otherwise conflict with pinned dependencies in our requirements
|
|
# files.
|
|
delayed_packages=()
|
|
|
|
requirements_files+=("${WORKSPACE_DIR}/python/requirements/test-requirements.txt")
|
|
|
|
if [[ "${LINT-}" == 1 ]]; then
|
|
install_linters
|
|
|
|
requirements_files+=("${WORKSPACE_DIR}/doc/requirements-doc.txt")
|
|
fi
|
|
|
|
# Additional default doc testing dependencies.
|
|
if [[ "${DOC_TESTING-}" == 1 ]]; then
|
|
# For Ray Core and Ray Serve DAG visualization docs test + dataset examples
|
|
sudo apt-get install -y graphviz tesseract-ocr
|
|
|
|
# For DAG visualization
|
|
requirements_packages+=("pydot")
|
|
requirements_packages+=("pytesseract==0.3.13")
|
|
fi
|
|
|
|
# Additional RLlib test dependencies.
|
|
if [[ "${RLLIB_TESTING-}" == 1 || "${DOC_TESTING-}" == 1 ]]; then
|
|
requirements_files+=("${WORKSPACE_DIR}/python/requirements/ml/rllib-requirements.txt")
|
|
requirements_files+=("${WORKSPACE_DIR}/python/requirements/ml/rllib-test-requirements.txt")
|
|
|
|
# Install MuJoCo.
|
|
sudo apt-get update
|
|
sudo apt-get install -y libosmesa6-dev libgl1 libglfw3 patchelf
|
|
wget https://github.com/google-deepmind/mujoco/releases/download/2.1.1/mujoco-2.1.1-linux-x86_64.tar.gz
|
|
mkdir -p /root/.mujoco
|
|
mv mujoco-2.1.1-linux-x86_64.tar.gz /root/.mujoco/.
|
|
(cd /root/.mujoco && tar -xf /root/.mujoco/mujoco-2.1.1-linux-x86_64.tar.gz)
|
|
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-}:/root/.mujoco/mujoco-2.1.1/bin
|
|
fi
|
|
|
|
# Additional Train test dependencies.
|
|
if [[ "${TRAIN_TESTING-}" == 1 || "${DOC_TESTING-}" == 1 ]]; then
|
|
requirements_files+=("${WORKSPACE_DIR}/python/requirements/ml/train-requirements.txt")
|
|
requirements_files+=("${WORKSPACE_DIR}/python/requirements/ml/train-test-requirements.txt")
|
|
fi
|
|
|
|
# Additional Tune/Doc test dependencies.
|
|
if [[ "${TUNE_TESTING-}" == 1 || "${DOC_TESTING-}" == 1 ]]; then
|
|
requirements_files+=("${WORKSPACE_DIR}/python/requirements/ml/tune-requirements.txt")
|
|
requirements_files+=("${WORKSPACE_DIR}/python/requirements/ml/tune-test-requirements.txt")
|
|
fi
|
|
|
|
# Data processing test dependencies.
|
|
if [[ "${DATA_PROCESSING_TESTING-}" == 1 || "${DOC_TESTING-}" == 1 ]]; then
|
|
requirements_files+=("${WORKSPACE_DIR}/python/requirements/ml/data-requirements.txt")
|
|
fi
|
|
if [[ "${DATA_PROCESSING_TESTING-}" == 1 ]]; then
|
|
requirements_files+=("${WORKSPACE_DIR}/python/requirements/ml/data-test-requirements.txt")
|
|
if [[ -n "${ARROW_VERSION-}" ]]; then
|
|
if [[ "${ARROW_VERSION-}" == "nightly" ]]; then
|
|
delayed_packages+=("--extra-index-url")
|
|
delayed_packages+=("https://pypi.fury.io/arrow-nightlies/")
|
|
delayed_packages+=("--prefer-binary")
|
|
delayed_packages+=("--pre")
|
|
delayed_packages+=("pyarrow")
|
|
else
|
|
delayed_packages+=("pyarrow==${ARROW_VERSION}")
|
|
fi
|
|
fi
|
|
if [[ -n "${ARROW_MONGO_VERSION-}" ]]; then
|
|
delayed_packages+=("pymongoarrow==${ARROW_MONGO_VERSION}")
|
|
fi
|
|
fi
|
|
|
|
# TODO(ray-ci): pin the dependencies.
|
|
CC=gcc retry_pip_install pip install -Ur "${WORKSPACE_DIR}/python/requirements.txt" -c "${WORKSPACE_DIR}/python/requirements_compiled.txt"
|
|
|
|
# Install deeplearning libraries (Torch + TensorFlow)
|
|
if [[ -n "${TORCH_VERSION-}" || "${DL-}" == "1" || "${RLLIB_TESTING-}" == 1 || "${TRAIN_TESTING-}" == 1 || "${TUNE_TESTING-}" == 1 || "${DOC_TESTING-}" == 1 ]]; then
|
|
# If we require a custom torch version, use that
|
|
if [[ -n "${TORCH_VERSION-}" ]]; then
|
|
# Install right away, as some dependencies (e.g. torch-spline-conv) need
|
|
# torch to be installed for their own install.
|
|
pip install -U "torch==${TORCH_VERSION-1.9.0}" "torchvision==${TORCHVISION_VERSION-0.10.0}"
|
|
# We won't add dl-cpu-requirements.txt as it would otherwise overwrite our custom
|
|
# torch. Thus we have also have to install tensorflow manually.
|
|
TF_PACKAGE=$(grep -ohE "tensorflow==[^ ;]+" "${WORKSPACE_DIR}/python/requirements/ml/dl-cpu-requirements.txt" | head -n 1)
|
|
TFPROB_PACKAGE=$(grep -ohE "tensorflow-probability==[^ ;]+" "${WORKSPACE_DIR}/python/requirements/ml/dl-cpu-requirements.txt" | head -n 1)
|
|
|
|
# %%;* deletes everything after ; to get rid of e.g. python version specifiers
|
|
pip install -U "${TF_PACKAGE%%;*}" "${TFPROB_PACKAGE%%;*}"
|
|
else
|
|
# Otherwise, use pinned default torch version.
|
|
# Again, install right away, as some dependencies (e.g. torch-spline-conv) need
|
|
# torch to be installed for their own install.
|
|
TORCH_PACKAGE=$(grep -ohE "torch==[^ ;]+" "${WORKSPACE_DIR}/python/requirements/ml/dl-cpu-requirements.txt" | head -n 1)
|
|
TORCHVISION_PACKAGE=$(grep -ohE "torchvision==[^ ;]+" "${WORKSPACE_DIR}/python/requirements/ml/dl-cpu-requirements.txt" | head -n 1)
|
|
|
|
# %%;* deletes everything after ; to get rid of e.g. python version specifiers
|
|
pip install "${TORCH_PACKAGE%%;*}" "${TORCHVISION_PACKAGE%%;*}"
|
|
requirements_files+=("${WORKSPACE_DIR}/python/requirements/ml/dl-cpu-requirements.txt")
|
|
fi
|
|
fi
|
|
|
|
# AIR core dependencies
|
|
if [[ "${RLLIB_TESTING-}" = 1 || "${TRAIN_TESTING-}" == 1 || "${TUNE_TESTING-}" == 1 || "${DOC_TESTING-}" == 1 ]]; then
|
|
requirements_files+=("${WORKSPACE_DIR}/python/requirements/ml/core-requirements.txt")
|
|
fi
|
|
|
|
# Inject our own mirror for the CIFAR10 dataset
|
|
if [[ "${TRAIN_TESTING-}" = 1 || "${TUNE_TESTING-}" == 1 || "${DOC_TESTING-}" == 1 ]]; then
|
|
SITE_PACKAGES=$(python -c 'from distutils.sysconfig import get_python_lib; print(get_python_lib())')
|
|
|
|
TF_CIFAR="${SITE_PACKAGES}/tensorflow/python/keras/datasets/cifar10.py"
|
|
TORCH_CIFAR="${SITE_PACKAGES}/torchvision/datasets/cifar.py"
|
|
|
|
[ -f "$TF_CIFAR" ] && sed -i 's https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz https://air-example-data.s3.us-west-2.amazonaws.com/cifar-10-python.tar.gz g' \
|
|
"$TF_CIFAR"
|
|
[ -f "$TORCH_CIFAR" ] &&sed -i 's https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz https://air-example-data.s3.us-west-2.amazonaws.com/cifar-10-python.tar.gz g' \
|
|
"$TORCH_CIFAR"
|
|
fi
|
|
|
|
# Generate the pip command with collected requirements files
|
|
pip_cmd="pip install -U -c ${WORKSPACE_DIR}/python/requirements.txt"
|
|
|
|
if [[ -f "${WORKSPACE_DIR}/python/requirements_compiled.txt" && "${OSTYPE}" != "msys" ]]; then
|
|
# On Windows, some pinned dependencies are not built for win, so we
|
|
# skip this until we have a good wy to resolve cross-platform dependencies.
|
|
pip_cmd+=" -c ${WORKSPACE_DIR}/python/requirements_compiled.txt"
|
|
fi
|
|
|
|
for file in "${requirements_files[@]}"; do
|
|
pip_cmd+=" -r ${file}"
|
|
done
|
|
|
|
# Expand single requirements
|
|
if [[ "${#requirements_packages[@]}" -gt 0 ]]; then
|
|
pip_cmd+=" ${requirements_packages[*]}"
|
|
fi
|
|
|
|
# Install
|
|
eval "${pip_cmd}"
|
|
|
|
# Install delayed packages
|
|
if [[ "${#delayed_packages[@]}" -gt 0 ]]; then
|
|
pip install -U -c "${WORKSPACE_DIR}/python/requirements.txt" "${delayed_packages[@]}"
|
|
fi
|
|
|
|
# Additional Tune dependency for Horovod.
|
|
# This must be run last (i.e., torch cannot be re-installed after this)
|
|
if [[ "${INSTALL_HOROVOD-}" == 1 ]]; then
|
|
"${SCRIPT_DIR}"/install-horovod.sh
|
|
fi
|
|
|
|
if [[ "${TUNE_TESTING-}" == 1 || "${DOC_TESTING-}" == 1 ]]; then
|
|
download_mnist
|
|
fi
|
|
}
|
|
|
|
install_thirdparty_packages() {
|
|
if [[ "${OSTYPE}" = darwin* ]]; then
|
|
# Currently do not work on macOS
|
|
return
|
|
fi
|
|
mkdir -p "${WORKSPACE_DIR}/python/ray/thirdparty_files"
|
|
RAY_THIRDPARTY_FILES="$(realpath "${WORKSPACE_DIR}/python/ray/thirdparty_files")"
|
|
CC=gcc python -m pip install psutil==5.9.6 colorama==0.4.6 --target="${RAY_THIRDPARTY_FILES}"
|
|
}
|
|
|
|
install_dependencies() {
|
|
install_bazel
|
|
|
|
# Only install on buildkite if requested
|
|
if [[ -z "${BUILDKITE-}" || "${BUILD-}" == "1" ]]; then
|
|
install_base
|
|
install_toolchains
|
|
fi
|
|
|
|
if [[ -n "${PYTHON-}" || "${LINT-}" == 1 || "${MINIMAL_INSTALL-}" == "1" ]]; then
|
|
install_miniforge
|
|
fi
|
|
|
|
install_upgrade_pip
|
|
|
|
# Only install on buildkite if requested
|
|
if [[ -z "${BUILDKITE-}" || "${BUILD-}" == "1" ]]; then
|
|
install_nvm
|
|
if [[ -n "${PYTHON-}" || -n "${LINT-}" || "${MAC_WHEELS-}" == 1 ]]; then
|
|
install_node
|
|
fi
|
|
fi
|
|
|
|
# install hdfs if needed.
|
|
if [[ "${INSTALL_HDFS-}" == 1 ]]; then
|
|
"${SCRIPT_DIR}"/install-hdfs.sh
|
|
fi
|
|
|
|
if [[ "${MINIMAL_INSTALL:-}" != "1" && "${SKIP_PYTHON_PACKAGES:-}" != "1" && "${SKIP_PIP_INSTALL:-}" != "1" ]]; then
|
|
install_pip_packages
|
|
fi
|
|
|
|
install_thirdparty_packages
|
|
}
|
|
|
|
if [[ $# -eq 0 ]]; then
|
|
install_dependencies
|
|
else
|
|
"$@"
|
|
fi
|
|
|
|
# Pop caller's shell options (quietly)
|
|
{ set -vx; eval "${SHELLOPTS_STACK##*|}"; SHELLOPTS_STACK="${SHELLOPTS_STACK%|*}"; } 2> /dev/null
|