mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 03:38:23 +00:00
`make install-lint` failed on every test-environment matrix runner
(manjarolinux/base, debian:bookworm, ubuntu:latest, fedora:latest,
quay.io/centos/centos:latest) with:
npm not found; install Node.js/npm first to get markdownlint-cli2.
make: *** [Makefile:227: install-lint] Error 1
The two npm-driven installers (`install_markdownlint_with_npm` and
`ensure_eslint`) bailed out as soon as `npm` was missing instead of
following the same auto-install pattern used elsewhere in the
script (e.g. `ensure_command shellcheck` / `shfmt` / `ruff`).
* Add an `npm:<manager>` mapping to `install_command`. pacman /
apt-get / dnf / yum get the candidate list `npm nodejs`; brew
gets `node`. `install_package_candidates` returns on the first
candidate that installs, which covers every matrix distro
(Arch's `npm` pulls `nodejs` as a hard dep; Debian/Ubuntu's `npm`
Depends: nodejs; Fedora/EL9+ ship `npm` as a direct package; brew's
`node` bundles npm).
* `install_markdownlint_with_npm` and `ensure_eslint` now call
`ensure_command npm` when `npm` is missing, before failing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
878 lines
19 KiB
Bash
Executable File
878 lines
19 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# shellcheck shell=bash
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
|
ACTIONLINT_VERSION="${ACTIONLINT_VERSION:-latest}"
|
|
ACTIONLINT_INSTALL_DIR="${ACTIONLINT_INSTALL_DIR:-/usr/local/bin}"
|
|
SHFMT_VERSION="${SHFMT_VERSION:-latest}"
|
|
SHFMT_INSTALL_DIR="${SHFMT_INSTALL_DIR:-/usr/local/bin}"
|
|
RUFF_PIP_SPEC="${RUFF_PIP_SPEC:-ruff}"
|
|
|
|
run_privileged() {
|
|
if [[ "${EUID}" -eq 0 ]] || ! command -v sudo >/dev/null 2>&1; then
|
|
"$@"
|
|
else
|
|
sudo "$@"
|
|
fi
|
|
}
|
|
|
|
log() {
|
|
printf '%s\n' "$*"
|
|
}
|
|
|
|
warn() {
|
|
printf '%s\n' "$*" >&2
|
|
}
|
|
|
|
detect_python_bin() {
|
|
local python_bin="${PYTHON:-python3}"
|
|
|
|
if command -v "${python_bin}" >/dev/null 2>&1; then
|
|
printf '%s\n' "${python_bin}"
|
|
return 0
|
|
fi
|
|
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
printf 'python3\n'
|
|
return 0
|
|
fi
|
|
|
|
if command -v python >/dev/null 2>&1; then
|
|
printf 'python\n'
|
|
return 0
|
|
fi
|
|
|
|
warn "Need python, python3, curl, or wget."
|
|
return 1
|
|
}
|
|
|
|
download_file() {
|
|
local url="$1"
|
|
local output="$2"
|
|
local python_bin
|
|
|
|
if command -v curl >/dev/null 2>&1; then
|
|
curl -fsSL "${url}" -o "${output}"
|
|
return 0
|
|
fi
|
|
|
|
if command -v wget >/dev/null 2>&1; then
|
|
wget -qO "${output}" "${url}"
|
|
return 0
|
|
fi
|
|
|
|
python_bin="$(detect_python_bin)" || {
|
|
warn "Need curl, wget, or python to download ${url}."
|
|
return 1
|
|
}
|
|
|
|
"${python_bin}" - "${url}" "${output}" <<'PY'
|
|
import pathlib
|
|
import sys
|
|
from urllib.request import urlopen
|
|
|
|
url, output = sys.argv[1], sys.argv[2]
|
|
with urlopen(url) as response:
|
|
pathlib.Path(output).write_bytes(response.read())
|
|
PY
|
|
}
|
|
|
|
resolve_latest_actionlint_version() {
|
|
resolve_latest_github_release_version "https://github.com/rhysd/actionlint/releases/latest"
|
|
}
|
|
|
|
resolve_latest_github_release_version() {
|
|
local latest_url="$1"
|
|
local final_url=""
|
|
local python_bin
|
|
|
|
if command -v curl >/dev/null 2>&1; then
|
|
final_url="$(curl -fsSLI -o /dev/null -w '%{url_effective}' "${latest_url}")"
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
final_url="$(
|
|
wget -q --server-response --max-redirect=0 -O /dev/null "${latest_url}" 2>&1 |
|
|
sed -n 's/^[[:space:]]*Location: //p' |
|
|
tr -d '\r' |
|
|
tail -n1
|
|
)"
|
|
else
|
|
python_bin="$(detect_python_bin)" || return 1
|
|
final_url="$(
|
|
"${python_bin}" - "${latest_url}" <<'PY'
|
|
import sys
|
|
from urllib.request import urlopen
|
|
|
|
with urlopen(sys.argv[1]) as response:
|
|
print(response.geturl())
|
|
PY
|
|
)"
|
|
fi
|
|
|
|
final_url="${final_url%/}"
|
|
if [[ -z "${final_url}" ]]; then
|
|
warn "Failed to resolve latest actionlint release URL."
|
|
return 1
|
|
fi
|
|
|
|
if [[ "${final_url}" == "${latest_url}" ]]; then
|
|
warn "Latest actionlint release URL did not resolve to a versioned tag."
|
|
return 1
|
|
fi
|
|
|
|
local latest_tag="${final_url##*/}"
|
|
latest_tag="${latest_tag#v}"
|
|
|
|
if [[ -z "${latest_tag}" || "${latest_tag}" == "${final_url}" || "${latest_tag}" == "latest" ]]; then
|
|
warn "Failed to determine latest actionlint version from ${final_url}."
|
|
return 1
|
|
fi
|
|
|
|
printf '%s\n' "${latest_tag}"
|
|
}
|
|
|
|
resolve_actionlint_version() {
|
|
local requested="${ACTIONLINT_VERSION#v}"
|
|
|
|
if [[ "${requested}" != "latest" ]]; then
|
|
printf '%s\n' "${requested}"
|
|
return 0
|
|
fi
|
|
|
|
resolve_latest_actionlint_version
|
|
}
|
|
|
|
resolve_latest_shfmt_version() {
|
|
resolve_latest_github_release_version "https://github.com/mvdan/sh/releases/latest"
|
|
}
|
|
|
|
resolve_shfmt_version() {
|
|
local requested="${SHFMT_VERSION#v}"
|
|
|
|
if [[ "${requested}" != "latest" ]]; then
|
|
printf '%s\n' "${requested}"
|
|
return 0
|
|
fi
|
|
|
|
resolve_latest_shfmt_version
|
|
}
|
|
|
|
detect_shfmt_os() {
|
|
case "$(uname -s)" in
|
|
Linux)
|
|
printf 'linux\n'
|
|
;;
|
|
Darwin)
|
|
printf 'darwin\n'
|
|
;;
|
|
*)
|
|
warn "Unsupported OS for shfmt prebuilt binary: $(uname -s)"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
detect_shfmt_arch() {
|
|
case "$(uname -m)" in
|
|
x86_64 | amd64)
|
|
printf 'amd64\n'
|
|
;;
|
|
i386 | i486 | i586 | i686)
|
|
printf '386\n'
|
|
;;
|
|
aarch64 | arm64)
|
|
printf 'arm64\n'
|
|
;;
|
|
armv6l | armv7l)
|
|
printf 'arm\n'
|
|
;;
|
|
*)
|
|
warn "Unsupported architecture for shfmt prebuilt binary: $(uname -m)"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
detect_actionlint_os() {
|
|
case "$(uname -s)" in
|
|
Linux)
|
|
printf 'linux\n'
|
|
;;
|
|
Darwin)
|
|
printf 'darwin\n'
|
|
;;
|
|
FreeBSD)
|
|
printf 'freebsd\n'
|
|
;;
|
|
*)
|
|
warn "Unsupported OS for actionlint prebuilt binary: $(uname -s)"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
detect_actionlint_arch() {
|
|
case "$(uname -m)" in
|
|
x86_64 | amd64)
|
|
printf 'amd64\n'
|
|
;;
|
|
i386 | i486 | i586 | i686)
|
|
printf '386\n'
|
|
;;
|
|
aarch64 | arm64)
|
|
printf 'arm64\n'
|
|
;;
|
|
armv6l | armv7l)
|
|
printf 'armv6\n'
|
|
;;
|
|
*)
|
|
warn "Unsupported architecture for actionlint prebuilt binary: $(uname -m)"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
ensure_dir_on_path() {
|
|
local dir="$1"
|
|
|
|
case ":${PATH}:" in
|
|
*:"${dir}":*) ;;
|
|
*)
|
|
export PATH="${dir}:${PATH}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
install_with_optional_sudo() {
|
|
if "$@"; then
|
|
return 0
|
|
fi
|
|
|
|
run_privileged "$@"
|
|
}
|
|
|
|
install_actionlint_binary() {
|
|
local version
|
|
local os
|
|
local arch
|
|
local archive_name
|
|
local url
|
|
local tmpdir
|
|
local archive_path
|
|
|
|
version="$(resolve_actionlint_version)" || return 1
|
|
os="$(detect_actionlint_os)" || return 1
|
|
arch="$(detect_actionlint_arch)" || return 1
|
|
archive_name="actionlint_${version}_${os}_${arch}.tar.gz"
|
|
url="https://github.com/rhysd/actionlint/releases/download/v${version}/${archive_name}"
|
|
tmpdir="$(mktemp -d)"
|
|
archive_path="${tmpdir}/${archive_name}"
|
|
|
|
if [[ "${ACTIONLINT_VERSION#v}" == "latest" ]]; then
|
|
log "Installing latest actionlint (resolved to v${version}) from GitHub releases"
|
|
else
|
|
log "Installing actionlint v${version} from GitHub releases"
|
|
fi
|
|
|
|
if ! download_file "${url}" "${archive_path}"; then
|
|
warn "Failed to download ${url}"
|
|
rm -rf "${tmpdir}"
|
|
return 1
|
|
fi
|
|
|
|
if ! tar -xzf "${archive_path}" -C "${tmpdir}"; then
|
|
warn "Failed to extract ${archive_name}"
|
|
rm -rf "${tmpdir}"
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! -f "${tmpdir}/actionlint" ]]; then
|
|
warn "Archive ${archive_name} did not contain an actionlint binary."
|
|
rm -rf "${tmpdir}"
|
|
return 1
|
|
fi
|
|
|
|
if ! install_with_optional_sudo install -d "${ACTIONLINT_INSTALL_DIR}"; then
|
|
warn "Failed to create ${ACTIONLINT_INSTALL_DIR}"
|
|
rm -rf "${tmpdir}"
|
|
return 1
|
|
fi
|
|
|
|
if ! install_with_optional_sudo install -m 0755 "${tmpdir}/actionlint" "${ACTIONLINT_INSTALL_DIR}/actionlint"; then
|
|
warn "Failed to install actionlint into ${ACTIONLINT_INSTALL_DIR}"
|
|
rm -rf "${tmpdir}"
|
|
return 1
|
|
fi
|
|
|
|
rm -rf "${tmpdir}"
|
|
}
|
|
|
|
install_shfmt_binary() {
|
|
local version
|
|
local os
|
|
local arch
|
|
local asset_name
|
|
local url
|
|
local tmpdir
|
|
local binary_path
|
|
|
|
version="$(resolve_shfmt_version)" || return 1
|
|
os="$(detect_shfmt_os)" || return 1
|
|
arch="$(detect_shfmt_arch)" || return 1
|
|
asset_name="shfmt_v${version}_${os}_${arch}"
|
|
url="https://github.com/mvdan/sh/releases/download/v${version}/${asset_name}"
|
|
tmpdir="$(mktemp -d)"
|
|
binary_path="${tmpdir}/shfmt"
|
|
|
|
if [[ "${SHFMT_VERSION#v}" == "latest" ]]; then
|
|
log "Installing latest shfmt (resolved to v${version}) from GitHub releases"
|
|
else
|
|
log "Installing shfmt v${version} from GitHub releases"
|
|
fi
|
|
|
|
if ! download_file "${url}" "${binary_path}"; then
|
|
warn "Failed to download ${url}"
|
|
rm -rf "${tmpdir}"
|
|
return 1
|
|
fi
|
|
|
|
if ! install_with_optional_sudo install -d "${SHFMT_INSTALL_DIR}"; then
|
|
warn "Failed to create ${SHFMT_INSTALL_DIR}"
|
|
rm -rf "${tmpdir}"
|
|
return 1
|
|
fi
|
|
|
|
if ! install_with_optional_sudo install -m 0755 "${binary_path}" "${SHFMT_INSTALL_DIR}/shfmt"; then
|
|
warn "Failed to install shfmt into ${SHFMT_INSTALL_DIR}"
|
|
rm -rf "${tmpdir}"
|
|
return 1
|
|
fi
|
|
|
|
ensure_dir_on_path "${SHFMT_INSTALL_DIR}"
|
|
hash -r
|
|
rm -rf "${tmpdir}"
|
|
}
|
|
|
|
ensure_actionlint() {
|
|
if command -v actionlint >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
log "Missing command 'actionlint'. Installing official prebuilt binary."
|
|
install_actionlint_binary || {
|
|
warn "Installation failed for 'actionlint'."
|
|
return 1
|
|
}
|
|
|
|
ensure_dir_on_path "${ACTIONLINT_INSTALL_DIR}"
|
|
hash -r
|
|
|
|
command -v actionlint >/dev/null 2>&1 || {
|
|
warn "Command 'actionlint' is still unavailable after installation."
|
|
return 1
|
|
}
|
|
}
|
|
|
|
install_mbake_with_pip() {
|
|
install_pip_pkg "${MBAKE_PIP_SPEC:-mbake}"
|
|
}
|
|
|
|
ensure_mbake() {
|
|
if command -v mbake >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
log "Missing command 'mbake'. Installing via pip."
|
|
install_mbake_with_pip || return 1
|
|
hash -r
|
|
command -v mbake >/dev/null 2>&1 || {
|
|
warn "Command 'mbake' is still unavailable after installation."
|
|
return 1
|
|
}
|
|
}
|
|
|
|
detect_python_scripts_dir() {
|
|
local python_bin="$1"
|
|
|
|
"${python_bin}" - <<'PY'
|
|
import sysconfig
|
|
|
|
print(sysconfig.get_path("scripts") or "")
|
|
PY
|
|
}
|
|
|
|
detect_python_user_scripts_dir() {
|
|
local python_bin="$1"
|
|
|
|
"${python_bin}" - <<'PY'
|
|
import site
|
|
import sys
|
|
|
|
user_base = site.getuserbase()
|
|
if not user_base:
|
|
sys.exit(1)
|
|
print(f"{user_base}/bin")
|
|
PY
|
|
}
|
|
|
|
python_runs_in_venv() {
|
|
local python_bin="$1"
|
|
|
|
"${python_bin}" - <<'PY' >/dev/null 2>&1
|
|
import sys
|
|
|
|
raise SystemExit(0 if sys.prefix != getattr(sys, "base_prefix", sys.prefix) else 1)
|
|
PY
|
|
}
|
|
|
|
pip_supports_break_system_packages() {
|
|
local python_bin="$1"
|
|
|
|
"${python_bin}" -m pip install --help 2>/dev/null | grep -q -- '--break-system-packages'
|
|
}
|
|
|
|
install_pip_pkg() {
|
|
# Generic venv-aware / sudo-aware / --user fallback pip installer.
|
|
# $1: pip spec (e.g. "ruff>=0.4.0", "ansible-lint", "galaxy-importer").
|
|
local pip_spec="$1"
|
|
local python_bin
|
|
local scripts_dir=""
|
|
local user_scripts_dir=""
|
|
local -a pip_args=(install --upgrade "${pip_spec}")
|
|
|
|
python_bin="$(detect_python_bin)" || return 1
|
|
scripts_dir="$(detect_python_scripts_dir "${python_bin}" || true)"
|
|
|
|
log "Installing Python package '${pip_spec}' via ${python_bin} -m pip"
|
|
|
|
if python_runs_in_venv "${python_bin}"; then
|
|
if ! "${python_bin}" -m pip "${pip_args[@]}"; then
|
|
return 1
|
|
fi
|
|
elif [[ "${EUID}" -eq 0 ]]; then
|
|
if ! "${python_bin}" -m pip "${pip_args[@]}"; then
|
|
if ! pip_supports_break_system_packages "${python_bin}"; then
|
|
return 1
|
|
fi
|
|
|
|
log "Retrying Python package install with --break-system-packages"
|
|
if ! "${python_bin}" -m pip install --break-system-packages --upgrade "${pip_spec}"; then
|
|
return 1
|
|
fi
|
|
fi
|
|
else
|
|
if ! "${python_bin}" -m pip install --user --upgrade "${pip_spec}"; then
|
|
return 1
|
|
fi
|
|
user_scripts_dir="$(detect_python_user_scripts_dir "${python_bin}" || true)"
|
|
fi
|
|
|
|
if [[ -n "${scripts_dir}" && -d "${scripts_dir}" ]]; then
|
|
ensure_dir_on_path "${scripts_dir}"
|
|
fi
|
|
|
|
if [[ -n "${user_scripts_dir}" && -d "${user_scripts_dir}" ]]; then
|
|
ensure_dir_on_path "${user_scripts_dir}"
|
|
fi
|
|
|
|
hash -r
|
|
}
|
|
|
|
install_ruff_with_pip() {
|
|
install_pip_pkg "${RUFF_PIP_SPEC}"
|
|
}
|
|
|
|
install_ansible_lint_with_pip() {
|
|
install_pip_pkg "${ANSIBLE_LINT_PIP_SPEC:-ansible-lint}"
|
|
}
|
|
|
|
install_galaxy_importer_with_pip() {
|
|
install_pip_pkg "${GALAXY_IMPORTER_PIP_SPEC:-galaxy-importer}"
|
|
}
|
|
|
|
install_markdownlint_with_npm() {
|
|
if ! command -v npm >/dev/null 2>&1; then
|
|
log "npm missing; attempting Node.js/npm install via system package manager."
|
|
ensure_command npm || {
|
|
warn "npm not found and could not be installed; cannot install markdownlint-cli2."
|
|
return 1
|
|
}
|
|
fi
|
|
log "Installing 'markdownlint-cli2' via npm (global)"
|
|
if install_with_optional_sudo npm install -g markdownlint-cli2; then
|
|
hash -r
|
|
return 0
|
|
fi
|
|
# Per-user fallback
|
|
local prefix="${HOME}/.npm-global"
|
|
mkdir -p "${prefix}"
|
|
if ! npm install -g --prefix "${prefix}" markdownlint-cli2; then
|
|
return 1
|
|
fi
|
|
ensure_dir_on_path "${prefix}/bin"
|
|
hash -r
|
|
}
|
|
|
|
ensure_ansible_lint() {
|
|
command -v ansible-lint >/dev/null 2>&1 && return 0
|
|
log "Missing command 'ansible-lint'. Installing via pip."
|
|
install_ansible_lint_with_pip || return 1
|
|
hash -r
|
|
command -v ansible-lint >/dev/null 2>&1 || {
|
|
warn "Command 'ansible-lint' is still unavailable after installation."
|
|
return 1
|
|
}
|
|
}
|
|
|
|
ensure_galaxy_importer() {
|
|
python3 -c 'import galaxy_importer' >/dev/null 2>&1 && return 0
|
|
log "Missing Python module 'galaxy_importer'. Installing via pip."
|
|
install_galaxy_importer_with_pip || return 1
|
|
python3 -c 'import galaxy_importer' >/dev/null 2>&1 || {
|
|
warn "Module 'galaxy_importer' is still unimportable after installation."
|
|
return 1
|
|
}
|
|
}
|
|
|
|
ensure_markdownlint_cli2() {
|
|
command -v markdownlint-cli2 >/dev/null 2>&1 && return 0
|
|
log "Missing command 'markdownlint-cli2'. Installing via npm."
|
|
install_markdownlint_with_npm || return 1
|
|
command -v markdownlint-cli2 >/dev/null 2>&1 || {
|
|
warn "Command 'markdownlint-cli2' is still unavailable after installation."
|
|
return 1
|
|
}
|
|
}
|
|
|
|
ensure_eslint() {
|
|
# ESLint and the eslint-plugin-playwright config are pinned in the
|
|
# repo's package.json + package-lock.json; install them locally into
|
|
# node_modules so the flat config (eslint.config.js) can resolve the
|
|
# plugin via require(...).
|
|
[[ -d "${REPO_ROOT}/node_modules/eslint" ]] && return 0
|
|
|
|
if ! command -v npm >/dev/null 2>&1; then
|
|
log "npm missing; attempting Node.js/npm install via system package manager."
|
|
ensure_command npm || {
|
|
warn "npm not found and could not be installed; cannot install ESLint."
|
|
return 1
|
|
}
|
|
fi
|
|
|
|
log "Missing local 'eslint'. Installing via npm (in repo root)."
|
|
(
|
|
cd "${REPO_ROOT}"
|
|
if [[ -f package-lock.json ]]; then
|
|
npm ci --no-fund --no-audit
|
|
else
|
|
npm install --no-fund --no-audit
|
|
fi
|
|
) || return 1
|
|
|
|
[[ -d "${REPO_ROOT}/node_modules/eslint" ]] || {
|
|
warn "Local 'eslint' is still unavailable after installation."
|
|
return 1
|
|
}
|
|
}
|
|
|
|
detect_package_manager() {
|
|
if command -v pacman >/dev/null 2>&1; then
|
|
printf 'pacman\n'
|
|
elif command -v apt-get >/dev/null 2>&1; then
|
|
printf 'apt-get\n'
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
printf 'dnf\n'
|
|
elif command -v yum >/dev/null 2>&1; then
|
|
printf 'yum\n'
|
|
elif command -v brew >/dev/null 2>&1; then
|
|
printf 'brew\n'
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
install_package_candidates() {
|
|
local manager="$1"
|
|
shift
|
|
local package
|
|
|
|
case "${manager}" in
|
|
apt-get)
|
|
run_privileged apt-get update
|
|
;;
|
|
dnf)
|
|
run_privileged dnf -y install dnf-plugins-core || true
|
|
run_privileged dnf -y install epel-release || true
|
|
;;
|
|
yum)
|
|
run_privileged yum -y install yum-utils || true
|
|
run_privileged yum -y install epel-release || true
|
|
;;
|
|
esac
|
|
|
|
for package in "$@"; do
|
|
log "Installing package '${package}' via ${manager}"
|
|
case "${manager}" in
|
|
pacman)
|
|
if run_privileged pacman -Syu --noconfirm --needed "${package}"; then
|
|
return 0
|
|
fi
|
|
;;
|
|
apt-get)
|
|
if run_privileged apt-get install -y --no-install-recommends "${package}"; then
|
|
return 0
|
|
fi
|
|
;;
|
|
dnf)
|
|
if run_privileged dnf -y install "${package}"; then
|
|
return 0
|
|
fi
|
|
;;
|
|
yum)
|
|
if run_privileged yum -y install "${package}"; then
|
|
return 0
|
|
fi
|
|
;;
|
|
brew)
|
|
if brew install "${package}"; then
|
|
return 0
|
|
fi
|
|
;;
|
|
*)
|
|
warn "Unsupported package manager: ${manager}"
|
|
return 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
fallback_install_command() {
|
|
local command_name="$1"
|
|
|
|
case "${command_name}" in
|
|
ruff)
|
|
log "Falling back to pip installation for 'ruff'."
|
|
install_ruff_with_pip
|
|
;;
|
|
shfmt)
|
|
log "Falling back to official shfmt binary."
|
|
install_shfmt_binary
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
install_command() {
|
|
local manager="$1"
|
|
local command_name="$2"
|
|
|
|
case "${command_name}:${manager}" in
|
|
ansible-galaxy:pacman | ansible-galaxy:apt-get | ansible-galaxy:dnf | ansible-galaxy:yum | ansible-galaxy:brew | \
|
|
ansible-playbook:pacman | ansible-playbook:apt-get | ansible-playbook:dnf | ansible-playbook:yum | ansible-playbook:brew)
|
|
if [[ "${manager}" == "brew" ]]; then
|
|
install_package_candidates "${manager}" ansible
|
|
else
|
|
install_package_candidates "${manager}" ansible-core ansible
|
|
fi
|
|
;;
|
|
ruff:pacman | ruff:apt-get | ruff:dnf | ruff:yum | ruff:brew)
|
|
install_package_candidates "${manager}" ruff
|
|
;;
|
|
shfmt:pacman | shfmt:apt-get | shfmt:dnf | shfmt:yum | shfmt:brew)
|
|
install_package_candidates "${manager}" shfmt
|
|
;;
|
|
shellcheck:pacman | shellcheck:apt-get | shellcheck:dnf | shellcheck:yum | shellcheck:brew)
|
|
install_package_candidates "${manager}" shellcheck
|
|
;;
|
|
npm:pacman | npm:apt-get | npm:dnf | npm:yum)
|
|
install_package_candidates "${manager}" npm nodejs
|
|
;;
|
|
npm:brew)
|
|
install_package_candidates "${manager}" node
|
|
;;
|
|
*)
|
|
warn "No installer mapping defined for '${command_name}' on '${manager}'."
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
ensure_command() {
|
|
local command_name="$1"
|
|
local install_failed=0
|
|
|
|
if command -v "${command_name}" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
local manager
|
|
manager="$(detect_package_manager)" || {
|
|
warn "No supported package manager found to install '${command_name}'."
|
|
return 1
|
|
}
|
|
|
|
log "Missing command '${command_name}'. Attempting installation via ${manager}."
|
|
if ! install_command "${manager}" "${command_name}"; then
|
|
install_failed=1
|
|
warn "Package-manager installation failed for '${command_name}' via ${manager}."
|
|
fi
|
|
|
|
if command -v "${command_name}" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
if fallback_install_command "${command_name}"; then
|
|
if command -v "${command_name}" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
else
|
|
((install_failed)) || warn "No fallback installer available for '${command_name}'."
|
|
fi
|
|
|
|
command -v "${command_name}" >/dev/null 2>&1 || {
|
|
warn "Command '${command_name}' is still unavailable after installation."
|
|
return 1
|
|
}
|
|
}
|
|
|
|
ensure_required_ansible_collections() {
|
|
local collections_base_dir="${HOME}/.ansible/collections"
|
|
local collections_dir="${collections_base_dir}/ansible_collections"
|
|
local missing=()
|
|
local max_attempts=5
|
|
local attempt=1
|
|
local sleep_time
|
|
|
|
[[ -d "${collections_dir}/community/general" ]] || missing+=(community.general)
|
|
[[ -d "${collections_dir}/hetzner/hcloud" ]] || missing+=(hetzner.hcloud)
|
|
[[ -d "${collections_dir}/kewlfft/aur" ]] || missing+=(kewlfft.aur)
|
|
|
|
if [[ "${#missing[@]}" -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
|
|
while true; do
|
|
log "Installing missing Ansible collections: ${missing[*]} (attempt ${attempt}/${max_attempts})"
|
|
|
|
if ansible-galaxy collection install \
|
|
-r "${REPO_ROOT}/requirements/requirements.galaxy.yml" \
|
|
-p "${collections_base_dir}" \
|
|
--force-with-deps; then
|
|
return 0
|
|
fi
|
|
|
|
warn "Galaxy install failed on attempt ${attempt}. Trying git fallback."
|
|
|
|
if ansible-galaxy collection install \
|
|
-r "${REPO_ROOT}/requirements/requirements.git.yml" \
|
|
-p "${collections_base_dir}" \
|
|
--force-with-deps; then
|
|
return 0
|
|
fi
|
|
|
|
if ((attempt >= max_attempts)); then
|
|
warn "Unable to install required Ansible collections."
|
|
return 1
|
|
fi
|
|
|
|
sleep_time=$((60 + RANDOM % 61))
|
|
warn "Retrying collection installation in ${sleep_time}s."
|
|
sleep "${sleep_time}"
|
|
((attempt++))
|
|
done
|
|
}
|
|
|
|
install_action_tools() {
|
|
ensure_actionlint
|
|
}
|
|
|
|
install_ansible_tools() {
|
|
ensure_command ansible-playbook
|
|
ensure_command ansible-galaxy
|
|
ensure_required_ansible_collections
|
|
ensure_ansible_lint
|
|
ensure_galaxy_importer
|
|
}
|
|
|
|
install_python_tools() {
|
|
ensure_command shfmt
|
|
ensure_command ruff
|
|
}
|
|
|
|
install_shellcheck_tools() {
|
|
ensure_command shellcheck
|
|
}
|
|
|
|
install_markdown_tools() {
|
|
ensure_markdownlint_cli2
|
|
}
|
|
|
|
install_makefile_tools() {
|
|
ensure_mbake
|
|
}
|
|
|
|
install_javascript_tools() {
|
|
ensure_eslint
|
|
}
|
|
|
|
install_requested_group() {
|
|
local group="$1"
|
|
|
|
case "${group}" in
|
|
all)
|
|
install_action_tools
|
|
install_ansible_tools
|
|
install_python_tools
|
|
install_shellcheck_tools
|
|
install_markdown_tools
|
|
install_makefile_tools
|
|
install_javascript_tools
|
|
;;
|
|
action)
|
|
install_action_tools
|
|
;;
|
|
ansible)
|
|
install_ansible_tools
|
|
;;
|
|
python)
|
|
install_python_tools
|
|
;;
|
|
shellcheck)
|
|
install_shellcheck_tools
|
|
;;
|
|
markdown)
|
|
install_markdown_tools
|
|
;;
|
|
makefile)
|
|
install_makefile_tools
|
|
;;
|
|
javascript)
|
|
install_javascript_tools
|
|
;;
|
|
*)
|
|
warn "Usage: $0 [all|action|ansible|python|shellcheck|markdown|makefile|javascript]..."
|
|
return 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main() {
|
|
local groups=("$@")
|
|
local group
|
|
|
|
if [[ "${#groups[@]}" -eq 0 ]]; then
|
|
groups=(all)
|
|
fi
|
|
|
|
for group in "${groups[@]}"; do
|
|
install_requested_group "${group}"
|
|
done
|
|
}
|
|
|
|
main "$@"
|