simpler check and deps messages
This commit is contained in:
@@ -98,39 +98,19 @@ load_config() {
|
||||
# ── end of frozen configuration ──
|
||||
|
||||
# ── ctrl/deps.sh ──
|
||||
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
||||
# report what it could not do.
|
||||
#
|
||||
# It never runs the cluster, never uses sudo or apt, and writes only into
|
||||
# $OUT_BIN (default ~/.local/bin). Everything that would touch the host proper —
|
||||
# systemd, inotify limits, .wslconfig, docker group — is REPORTED for a human to
|
||||
# decide on, never performed. That is what makes it safe to run on a machine that
|
||||
# already has a working setup.
|
||||
#
|
||||
# Usage (normally via `make deps`, or directly):
|
||||
# deps.sh detect # report host facts only, change nothing
|
||||
# deps.sh list # the pinned versions
|
||||
# deps.sh verify [core|dev] # run what is installed and see if it works
|
||||
# deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
||||
# deps.sh install [core|dev] # detect, fetch, install, report
|
||||
#
|
||||
# Tiers: 'core' is kubectl + jq (talk to a cluster); 'dev' adds kind and tilt
|
||||
# Default is dev.
|
||||
#
|
||||
# Runs both inside the installer container and bare on a host. Inside the
|
||||
# container, host files are read through $HOST_ROOT (mount / as :ro); bare, it
|
||||
# falls back to /.
|
||||
# Toolchain installer: detect the host, install pinned tools into $OUT_BIN, report
|
||||
# host actions it will not perform (no sudo, no apt). Usually via `make deps`.
|
||||
# Usage: deps.sh [detect [all] | list | verify [core|dev] | fetch [core|dev] [--to DIR] | install [core|dev]]
|
||||
# Notes: docs/notes/deps.md
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Keep the caller's cwd so a relative --to resolves where the user expects,
|
||||
# not against ctrl/ once we've moved.
|
||||
# Keep the caller's cwd so a relative --to resolves there, not against ctrl/.
|
||||
INVOKED_FROM="$PWD"
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Pins arrive through load_config like every other setting, not by sourcing
|
||||
# versions.env here. That is what lets `make standalone` freeze them into a
|
||||
# one-file installer: configuration has exactly one way in.
|
||||
# Pins arrive through load_config, not by sourcing versions.env, so `make
|
||||
# standalone` can freeze them in.
|
||||
# (sourced library inlined above)
|
||||
load_config
|
||||
|
||||
@@ -151,12 +131,11 @@ BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
||||
# Collected by detect(), printed by report_manual() at the very end.
|
||||
MANUAL=()
|
||||
|
||||
# Host FILES (/etc/..., /mnt/c/...) must be read through the mount. Kernel-level
|
||||
# facts (kernel version, meminfo, inotify) are shared with the container, so the
|
||||
# container's own view is already the host's.
|
||||
# A /proc/meminfo field in MB, 0 if the field is absent. MEMINFO exists so the
|
||||
# tight and does-not-fit branches can be exercised against a real machine's
|
||||
# numbers from somewhere else; in normal use it is always /proc/meminfo.
|
||||
# Facts print only with VERBOSE (`detect all`); problems (! and -) always print.
|
||||
fact() { if [ -n "${VERBOSE:-}" ]; then echo "$@"; fi; }
|
||||
|
||||
# Host FILES are read through $HOST_ROOT; kernel facts are shared with the container.
|
||||
# A /proc/meminfo field in MB, 0 if absent. MEMINFO overrides the source for testing.
|
||||
mb_of() {
|
||||
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
||||
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
||||
@@ -181,9 +160,7 @@ arch() {
|
||||
esac
|
||||
}
|
||||
|
||||
# The pins above are amd64. Rather than download something that cannot execute
|
||||
# and let it fail as "cannot execute binary file: Exec format error", say so
|
||||
# here and hand over the commands that produce the right checksums.
|
||||
# Pins are amd64 only: refuse elsewhere and print how to get the right checksums.
|
||||
require_amd64() {
|
||||
local a; a=$(arch)
|
||||
[ "$a" = "amd64" ] && return 0
|
||||
@@ -236,9 +213,7 @@ pick_sha() {
|
||||
}
|
||||
|
||||
# ── package manager, for the instructions only ─────────────────────────────
|
||||
# This never runs a package manager. It names one so the reported action is
|
||||
# something you can paste, on the distro you are actually on — an apt line on
|
||||
# Amazon Linux 2 is a wrong answer dressed up as help.
|
||||
# Never runs one; names the right one so reported actions are pasteable.
|
||||
|
||||
pkg_install_cmd() {
|
||||
local pkg="$1"
|
||||
@@ -258,9 +233,7 @@ docker_pkg() {
|
||||
|
||||
# ── detect ─────────────────────────────────────────────────────────────────
|
||||
|
||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
||||
# the tooling. Detectable, so name it instead.
|
||||
# Windows outside WSL (Git Bash, MSYS, Cygwin) fails confusingly; name it instead.
|
||||
require_linux() {
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
@@ -285,36 +258,30 @@ is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||
|
||||
detect() {
|
||||
echo "host"
|
||||
echo " kernel $(uname -r)"
|
||||
echo " arch $(arch) ($(uname -m))"
|
||||
fact " kernel $(uname -r)"
|
||||
|
||||
local osr; osr=$(host_file /etc/os-release)
|
||||
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
||||
local osr distro=""; osr=$(host_file /etc/os-release)
|
||||
[ -r "$osr" ] && distro=$(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")
|
||||
echo " distro ${distro:-unknown} $(arch), $(if is_wsl; then echo WSL; else echo native linux; fi)"
|
||||
|
||||
# In MB. Whole gigabytes lose nearly half a GB on exactly the machines where
|
||||
# it matters: 1874 MB available used to print as "1 GB". Facts only — whether
|
||||
# that is enough depends on the profile, which check.sh knows and this does not.
|
||||
# In MB (whole GB rounds away too much). Facts only; check.sh judges sufficiency.
|
||||
local total_mb avail_mb swap_total_mb swap_used_mb om
|
||||
total_mb=$(mb_of MemTotal)
|
||||
avail_mb=$(mb_of MemAvailable)
|
||||
swap_total_mb=$(mb_of SwapTotal)
|
||||
swap_used_mb=$(( swap_total_mb - $(mb_of SwapFree) ))
|
||||
printf " memory %d MB total, %d MB available\n" "$total_mb" "$avail_mb"
|
||||
if [ "$swap_total_mb" -gt 0 ]; then
|
||||
printf " swap %d MB used of %d MB\n" "$swap_used_mb" "$swap_total_mb"
|
||||
fi
|
||||
printf " memory %d MB total, %d MB available%s\n" "$total_mb" "$avail_mb" \
|
||||
"$(if [ "$swap_used_mb" -gt 0 ]; then echo ", $swap_used_mb MB in swap"; fi)"
|
||||
|
||||
# How the kernel answers an allocation it cannot really satisfy. With 1 it
|
||||
# always says yes and settles up later with the OOM killer, so a cluster that
|
||||
# starts cleanly can still lose processes afterwards.
|
||||
# Overcommit mode: with 1 the OOM killer settles up later, after a clean start.
|
||||
om=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
||||
case "$om" in
|
||||
0) echo " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
||||
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
||||
2) echo " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
||||
0) fact " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
||||
1) fact " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
||||
2) fact " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
||||
esac
|
||||
|
||||
echo " install to $OUT_BIN"
|
||||
fact " install to $OUT_BIN"
|
||||
detect_libc
|
||||
detect_prereqs
|
||||
detect_wsl
|
||||
@@ -326,18 +293,13 @@ detect() {
|
||||
|
||||
detect_wsl() {
|
||||
if ! is_wsl; then
|
||||
echo " platform native linux"
|
||||
return
|
||||
fi
|
||||
|
||||
echo " platform WSL"
|
||||
|
||||
# systemd is off by default in WSL, and the ingress/DNS paths that use a
|
||||
# host service need it. Enabling it requires a Windows-side restart, which
|
||||
# cannot be issued from inside the distro.
|
||||
# systemd is off by default in WSL; enabling it needs a Windows-side restart.
|
||||
local wc; wc=$(host_file /etc/wsl.conf)
|
||||
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
||||
echo " systemd enabled in wsl.conf"
|
||||
fact " systemd enabled in wsl.conf"
|
||||
else
|
||||
echo " ! systemd not enabled in /etc/wsl.conf"
|
||||
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
||||
@@ -349,15 +311,15 @@ detect_wsl() {
|
||||
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
||||
# local DNS setup.
|
||||
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
||||
echo " resolv.conf pinned (generateResolvConf=false)"
|
||||
fact " resolv.conf pinned (generateResolvConf=false)"
|
||||
else
|
||||
echo " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
||||
fact " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
||||
fi
|
||||
|
||||
local wcfg
|
||||
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
||||
echo " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
||||
fact " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
||||
else
|
||||
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
||||
make check mem
|
||||
@@ -365,11 +327,8 @@ detect_wsl() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Not a path check: /mnt is an ordinary mount point and an ext4 disk mounted
|
||||
# there is perfectly fine. What matters is the filesystem. The Windows drives
|
||||
# arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same
|
||||
# way. None of them deliver inotify events, so anything watching files goes
|
||||
# quiet without saying why.
|
||||
# Filesystem types that deliver no inotify events (9p, drvfs, network, fuse).
|
||||
# Checks the fs type, not the path.
|
||||
watch_hostile_fs() {
|
||||
local dir="$1" fstype
|
||||
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
||||
@@ -390,28 +349,22 @@ detect_filesystem() {
|
||||
on a $fstype mount, and everything else is slower:
|
||||
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
||||
else
|
||||
echo " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
||||
fact " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
||||
fi
|
||||
}
|
||||
|
||||
# tilt is the one binary here that needs a recent glibc. MEASURED, not guessed:
|
||||
# tilt 0.37.6 on Amazon Linux 2 (glibc 2.26) fails with
|
||||
#
|
||||
# /lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../tilt)
|
||||
#
|
||||
# which names a symbol rather than the problem. Amazon Linux 2 is a stock
|
||||
# WorkSpaces bundle, so this is the likely case, not an exotic one. Report the
|
||||
# version now; `verify` catches the actual failure after installing.
|
||||
# tilt needs glibc >= 2.34 (measured on Amazon Linux 2). Report the version here;
|
||||
# `verify` catches the actual failure after installing.
|
||||
detect_libc() {
|
||||
local v=""
|
||||
if command -v ldd >/dev/null 2>&1; then
|
||||
v=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)
|
||||
fi
|
||||
if [ -z "$v" ]; then
|
||||
echo " libc unknown (no ldd) — 'verify' is the real test"
|
||||
fact " libc unknown (no ldd) — 'verify' is the real test"
|
||||
return 0
|
||||
fi
|
||||
echo " libc glibc $v"
|
||||
fact " libc glibc $v"
|
||||
if [ "$(printf '%s\n2.34\n' "$v" | sort -V | head -1)" != "2.34" ]; then
|
||||
echo " ! older than glibc 2.34, which tilt needs. kubectl, kind, jq and"
|
||||
echo " ctlptl are static or libc-only and work here; tilt will not start."
|
||||
@@ -420,25 +373,23 @@ detect_libc() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# What this script needs to do its own job. Reported here so `detect` answers
|
||||
# "will install work?" instead of leaving you to find out one download in.
|
||||
# Amazon Linux 2 ships without tar, which is exactly the surprise this catches.
|
||||
# What this script itself needs, so `detect` answers "will install work?".
|
||||
detect_prereqs() {
|
||||
local missing=""
|
||||
if command -v curl >/dev/null 2>&1; then echo " download curl"
|
||||
elif command -v wget >/dev/null 2>&1; then echo " download wget"
|
||||
if command -v curl >/dev/null 2>&1; then fact " download curl"
|
||||
elif command -v wget >/dev/null 2>&1; then fact " download wget"
|
||||
else echo " ! no curl and no wget — nothing can be downloaded"; missing+=" curl"
|
||||
fi
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1; then
|
||||
echo " checksums ok"
|
||||
fact " checksums ok"
|
||||
else
|
||||
echo " ! no sha256sum or shasum — downloads could not be verified"
|
||||
missing+=" coreutils"
|
||||
fi
|
||||
|
||||
if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
|
||||
echo " archives tar + gzip"
|
||||
fact " archives tar + gzip"
|
||||
else
|
||||
echo " ! no tar/gzip — tilt and ctlptl ship as tarballs, so the dev tier"
|
||||
echo " cannot be unpacked. The core tier is two bare binaries and is fine."
|
||||
@@ -453,10 +404,7 @@ detect_prereqs() {
|
||||
}
|
||||
|
||||
detect_docker() {
|
||||
# Reachability of the daemon is the real question, and the CLI is only how
|
||||
# we ask it. Note that when this runs inside the installer container, Docker
|
||||
# necessarily exists on the host — otherwise nothing would be executing —
|
||||
# so a missing CLI in here is an installer packaging bug, not a host problem.
|
||||
# Daemon reachability is the real question; the CLI is only how we ask.
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
if [ -S /var/run/docker.sock ]; then
|
||||
echo " docker socket present (no cli in this context)"
|
||||
@@ -475,12 +423,9 @@ detect_docker() {
|
||||
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
||||
local n
|
||||
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||
# Must be an `if`, not `[ ] && echo`: as the last statement in this
|
||||
# function the latter returns 1 when the count is zero, and `set -e`
|
||||
# then kills the caller. That is the fresh-machine case — no clusters
|
||||
# yet — so the bug only ever shows up where it does most harm.
|
||||
# Must be an `if`, not `[ ] && echo`: a zero count would return 1 under set -e.
|
||||
if [ "$n" -gt 0 ]; then
|
||||
echo " - $n kind node container(s) already running; see 'make cluster list'"
|
||||
echo " kind $n node container(s) running — 'make cluster list'"
|
||||
fi
|
||||
else
|
||||
echo " ! docker cli present but the daemon is unreachable"
|
||||
@@ -495,7 +440,7 @@ detect_inotify() {
|
||||
local w i
|
||||
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
||||
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
||||
echo " inotify watches=$w instances=$i"
|
||||
fact " inotify watches=$w instances=$i"
|
||||
|
||||
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
||||
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
||||
@@ -555,18 +500,13 @@ fetch_tgz() {
|
||||
echo " fetching $name"
|
||||
download "$(resolve_url "$url")" "$tmp"
|
||||
verify "$tmp" "$sha" "$name"
|
||||
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
||||
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
||||
# user does not own.
|
||||
# --no-same-owner: as root, tar would restore the archive's uid/gid.
|
||||
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
||||
rm -f "$tmp"
|
||||
chmod +x "$dest/$name"
|
||||
}
|
||||
|
||||
# The installer runs as root so it can reach the docker socket, which means
|
||||
# everything it writes into a mounted volume lands root-owned and unusable from
|
||||
# the host. Hand it back to whoever owns the mount point (the host user created
|
||||
# that directory before mounting it).
|
||||
# The installer runs as root; hand files in a mounted dir back to the mount point's owner.
|
||||
fix_ownership() {
|
||||
local dir="$1"
|
||||
[ -d "$dir" ] || return 0
|
||||
@@ -578,38 +518,14 @@ fix_ownership() {
|
||||
chown -R "$owner" "$dir" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Two tiers, because not every machine should get cluster tooling.
|
||||
#
|
||||
# core kubectl, jq — talk to a cluster someone else runs. Nothing that
|
||||
# creates one. Appropriate on a managed or corporate-issued machine
|
||||
# where development tools are not wanted by default.
|
||||
# dev core plus kind and tilt — build clusters and hot-reload into them.
|
||||
#
|
||||
# The split exists because "install the toolchain" is not one decision: on a
|
||||
# managed workspace the right answer is kubectl and nothing else.
|
||||
# core: talk to a cluster someone else runs. dev: core plus tools that build clusters.
|
||||
CORE_TOOLS="kubectl jq"
|
||||
# No helm: every addon installs with `kubectl apply -f <url>`, so nothing here
|
||||
# has ever invoked it. Add it back the day something actually needs a chart.
|
||||
#
|
||||
# ctlptl is 'dev' rather than 'core' for the same reason kind is: core is "talk
|
||||
# to a cluster someone else runs", and ctlptl builds them. It earns its place
|
||||
# because it is what wires a cluster to a local registry — without one, an
|
||||
# unqualified image name resolves to docker.io/library/<name> and there is
|
||||
# nothing structural stopping a push there.
|
||||
#
|
||||
# docker-compose is 'dev' for the same reason, and is here because the distro
|
||||
# docker packages ship the daemon and CLI but frequently not the compose
|
||||
# plugin — so `docker compose up` fails with "unknown command" on an otherwise
|
||||
# working Docker, and nothing about that message names the missing piece.
|
||||
# No helm (nothing uses a chart). ctlptl wires in a local registry; compose is often
|
||||
# missing from distro docker packages.
|
||||
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
||||
|
||||
# ── what is already on this machine ───────────────────────────────────────
|
||||
#
|
||||
# A tool already on PATH at its pinned version is left where it is. Without
|
||||
# this, install downloads a second copy into OUT_BIN and then reports the first
|
||||
# one as shadowed — noise, and wrong, when both are the same version. That is
|
||||
# the normal state of any machine someone set up by hand, whatever directory
|
||||
# they happened to choose.
|
||||
# A tool already on PATH at its pinned version is left where it is.
|
||||
|
||||
pin_of() {
|
||||
case "$1" in
|
||||
@@ -622,9 +538,7 @@ pin_of() {
|
||||
esac
|
||||
}
|
||||
|
||||
# The version string a binary reports. Each tool spells the question
|
||||
# differently, and kubectl has to be told --client or it goes looking for a
|
||||
# server to ask.
|
||||
# The version string a binary reports (kubectl needs --client).
|
||||
reported_version() {
|
||||
local tool="$1" path="$2"
|
||||
case "$tool" in
|
||||
@@ -634,13 +548,8 @@ reported_version() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Does the binary at PATH report PIN? Matched as a whole version token, so
|
||||
# 0.37.6 never matches 10.37.60, with the leading v optional either side: kind
|
||||
# says v0.32.0, jq says jq-1.8.2, and tilt says v0.37.6 against a pin of 0.37.6.
|
||||
#
|
||||
# Bash's own regex rather than grep, deliberately. grep is not the same program
|
||||
# on every machine — some builds reject patterns that others accept — and a
|
||||
# failed grep inside a count reads exactly like a zero.
|
||||
# Does the binary at PATH report PIN? Whole-token match, leading v optional.
|
||||
# Bash regex rather than grep, deliberately.
|
||||
version_matches() {
|
||||
local tool="$1" path="$2" pin="$3" out v re
|
||||
out=$(reported_version "$tool" "$path") || return 1
|
||||
@@ -650,10 +559,8 @@ version_matches() {
|
||||
[[ $out =~ $re ]]
|
||||
}
|
||||
|
||||
# DEPS_ONLY narrows a fetch to the tools it names. Unset means the whole tier,
|
||||
# which is what an explicit `deps.sh fetch` always gets: "download these into
|
||||
# DIR" must not quietly skip something because this machine happens to have it.
|
||||
# Only install() sets it, to what detect_toolchain found missing or mismatched.
|
||||
# DEPS_ONLY narrows a fetch to the tools it names; unset means the whole tier.
|
||||
# Only install() sets it.
|
||||
want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; }
|
||||
|
||||
# Every tool in the tier with its state, probed once and reported once. What
|
||||
@@ -662,20 +569,18 @@ TOOLCHAIN_NEED=""
|
||||
detect_toolchain() {
|
||||
local tier="${TIER:-dev}" b pin path found
|
||||
TOOLCHAIN_NEED=""
|
||||
local n=0
|
||||
echo
|
||||
echo "toolchain (pinned, tier '$tier')"
|
||||
fact "toolchain (pinned, tier '$tier')"
|
||||
for b in $(tier_tools "$tier"); do
|
||||
n=$((n + 1))
|
||||
pin=$(pin_of "$b")
|
||||
path=$(command -v "$b" 2>/dev/null || true)
|
||||
# compose is the one tool that is normally NOT a binary on PATH. It is a
|
||||
# docker CLI plugin, so a machine where `docker compose` works perfectly
|
||||
# has no `docker-compose` to find — and probing only PATH would report it
|
||||
# missing and re-download a copy that is already there. That is the exact
|
||||
# noise the version-aware skip exists to prevent, so ask docker instead.
|
||||
# compose is normally a docker CLI plugin, not on PATH: ask docker instead.
|
||||
if [ "$b" = docker-compose ] && [ -z "$path" ]; then
|
||||
if found=$(docker compose version --short 2>/dev/null) && [ -n "$found" ]; then
|
||||
if [ "${found#v}" = "${pin#v}" ]; then
|
||||
printf " %-8s %-9s %s\n" "$b" "$pin" "docker cli plugin"
|
||||
fact "$(printf " %-8s %-9s %s" "$b" "$pin" "docker cli plugin")"
|
||||
else
|
||||
printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \
|
||||
"$b" "$pin" "$found"
|
||||
@@ -688,7 +593,7 @@ detect_toolchain() {
|
||||
printf " - %-8s %-9s not found\n" "$b" "$pin"
|
||||
TOOLCHAIN_NEED+="$b "
|
||||
elif version_matches "$b" "$path" "$pin"; then
|
||||
printf " %-8s %-9s %s\n" "$b" "$pin" "$path"
|
||||
fact "$(printf " %-8s %-9s %s" "$b" "$pin" "$path")"
|
||||
else
|
||||
found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true)
|
||||
printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found"
|
||||
@@ -696,9 +601,10 @@ detect_toolchain() {
|
||||
fi
|
||||
done
|
||||
if [ -z "$TOOLCHAIN_NEED" ]; then
|
||||
echo " every pinned tool is already on PATH — nothing to fetch"
|
||||
if [ -n "${VERBOSE:-}" ]; then echo " all $n on PATH — nothing to fetch"
|
||||
else echo "toolchain all $n pinned tools on PATH (tier $tier)"; fi
|
||||
else
|
||||
echo " 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
||||
echo "toolchain 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -739,8 +645,7 @@ fetch() {
|
||||
fi
|
||||
|
||||
fix_ownership "$dest"
|
||||
# kind writes the kubeconfig as root too; hand that back as well when it's
|
||||
# a mounted host directory rather than container-local state.
|
||||
# kind writes the kubeconfig as root too; hand that back as well.
|
||||
fix_ownership "${KUBE_DIR:-/out/kube}"
|
||||
}
|
||||
|
||||
@@ -762,14 +667,8 @@ report_manual() {
|
||||
done
|
||||
}
|
||||
|
||||
# Installing into a directory that sits early in PATH silently replaces whatever
|
||||
# the machine was already using — which on a shared or client machine can break
|
||||
# unrelated work (kubectl more than one minor away from a cluster is the common
|
||||
# one). Say so; never decide it for them.
|
||||
# Downloading a verified binary proves it is the right file, not that this
|
||||
# machine can run it. On an old distro tilt fails here, with a linker error
|
||||
# about a missing symbol, and finding that out now beats finding out during a
|
||||
# first cluster build.
|
||||
# A verified download proves the right file, not that this machine can run it
|
||||
# (old glibc breaks tilt). Run each one now.
|
||||
verify_tools() {
|
||||
local tier="${1:-dev}" b bin out rc broke=0
|
||||
echo "checking that each one actually runs"
|
||||
@@ -779,11 +678,7 @@ verify_tools() {
|
||||
printf ' %-14s not installed\n' "$b"
|
||||
continue
|
||||
fi
|
||||
# Not piped into `head`. With `pipefail` set, a tool that prints more
|
||||
# than one line gets SIGPIPE when head closes the pipe, and the
|
||||
# pipeline reports 141 — so a working kubectl was announced as "does
|
||||
# not run here", with its own correct version string as the evidence.
|
||||
# Take the first line afterwards, from the string.
|
||||
# Not piped into `head`: under pipefail, SIGPIPE (141) looked like failure.
|
||||
rc=0
|
||||
case "$b" in
|
||||
kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;;
|
||||
@@ -861,18 +756,12 @@ warn_shadowing() {
|
||||
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
||||
}
|
||||
|
||||
# A copy in OUT_BIN only gives you `docker-compose`. That hyphenated form is the
|
||||
# retired v1 spelling; every compose file written in the last few years assumes
|
||||
# `docker compose`, which resolves plugins BY NAME out of a plugin directory.
|
||||
# So the binary is fetched like any other and then linked, in your own home —
|
||||
# no root, and nothing outside it.
|
||||
# Link the fetched docker-compose into ~/.docker/cli-plugins so `docker compose` works.
|
||||
install_compose_plugin() {
|
||||
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
||||
[ -x "$src" ] || return 0
|
||||
mkdir -p "$dir"
|
||||
# Something else already owns that name — docker-desktop and some distro
|
||||
# packages install a real file there. Overwriting it would take the plugin
|
||||
# away from whatever put it there, so say so and let the user decide.
|
||||
# A real file there belongs to something else (docker-desktop, distro): don't overwrite.
|
||||
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
||||
MANUAL+=("Something already installs the compose plugin at
|
||||
$dir/docker-compose
|
||||
@@ -903,16 +792,12 @@ install() {
|
||||
if [ "$tier" = "core" ]; then
|
||||
echo " (no kind/tilt — 'make deps dev' adds them)"
|
||||
fi
|
||||
# Only when compose was one of the things fetched: linking a binary
|
||||
# that is already satisfied elsewhere on PATH would point the plugin at
|
||||
# a copy rig did not install.
|
||||
# Only when compose was fetched, never at a copy rig did not install.
|
||||
case " $TOOLCHAIN_NEED " in
|
||||
*" docker-compose "*) install_compose_plugin ;;
|
||||
esac
|
||||
|
||||
# Only worth saying when something actually landed in OUT_BIN. When every
|
||||
# tool was satisfied elsewhere, OUT_BIN may reasonably be off PATH, and
|
||||
# telling the user to add it would be advice to fix nothing.
|
||||
# PATH advice only when something actually landed in OUT_BIN.
|
||||
case ":${PATH}:" in
|
||||
*":$OUT_BIN:"*) ;;
|
||||
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
||||
@@ -928,10 +813,7 @@ install() {
|
||||
|
||||
require_linux
|
||||
|
||||
# Read the command, THEN shift — and shift only if there is something there.
|
||||
# A bare `shift` with no positional parameters returns 1, and under `set -e`
|
||||
# that ended the script before a single line was printed: running this with no
|
||||
# arguments at all, the documented default, did nothing and said nothing.
|
||||
# Shift only if there is an argument: a bare `shift` returns 1 under set -e.
|
||||
cmd="${1:-install}"
|
||||
[ $# -gt 0 ] && shift
|
||||
|
||||
@@ -943,12 +825,12 @@ need_downloads() {
|
||||
}
|
||||
|
||||
case "$cmd" in
|
||||
detect) detect; report_manual ;;
|
||||
detect) if [ "${1:-}" = all ]; then VERBOSE=1; fi; detect; report_manual ;;
|
||||
list) list ;;
|
||||
verify) verify_tools "${1:-dev}" ;;
|
||||
fetch) need_downloads; fetch "$@" ;;
|
||||
install) need_downloads; install "${1:-dev}" ;;
|
||||
*) echo "usage: $0 [detect|list|verify|fetch|install]" >&2
|
||||
*) echo "usage: $0 [detect [all]|list|verify|fetch|install]" >&2
|
||||
echo " install [core|dev] (default dev)" >&2
|
||||
echo " fetch [core|dev] [--to DIR]" >&2
|
||||
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
||||
|
||||
@@ -98,33 +98,9 @@ load_config() {
|
||||
# ── end of frozen configuration ──
|
||||
|
||||
# ── ctrl/mem.sh ──
|
||||
# How much memory this machine will actually give you before something dies —
|
||||
# rig's memory tool, and (generated from this file) the standalone rigmini.sh.
|
||||
#
|
||||
# There are two numbers and they are rarely the same. `status` reports what the
|
||||
# machine ADVERTISES and what is quietly capping it. `push` finds what it will
|
||||
# SURVIVE, by allocating until it stops. `all` does both and weighs the result
|
||||
# against what this profile's cluster needs.
|
||||
#
|
||||
# The gap between them is the whole reason this exists. Under WSL the cap lives
|
||||
# in .wslconfig; in a container or a managed workspace it is a cgroup limit, and
|
||||
# there /proc/meminfo reports the HOST's memory while the kernel kills you at a
|
||||
# fraction of it. A script that only read MemTotal would confidently report 32 GB
|
||||
# on a box that OOMs at 2.
|
||||
#
|
||||
# Runs on native Linux and under WSL. On WSL the memory you see is a VM
|
||||
# allocation that can be raised, and the commonest failure is raising it without
|
||||
# restarting — so status compares what .wslconfig says with what actually booted.
|
||||
#
|
||||
# Reports and instructs. It never raises a limit, frees anything or installs a
|
||||
# package. The one write it can make is `backup`, which copies .wslconfig beside
|
||||
# itself, so that `restore` has something to put back after a hand edit.
|
||||
#
|
||||
# Usage:
|
||||
# mem.sh status what it has, what caps it
|
||||
# mem.sh push [--to GB] [--to-oom] climb until it stops
|
||||
# mem.sh all [--budget GB] both, then the verdict
|
||||
# mem.sh backup | restore .wslconfig, WSL only
|
||||
# rig's memory tool (also generated as rigmini.sh): what the machine advertises vs. what it survives.
|
||||
# Usage: mem.sh status | push [--to GB] [--to-oom] | all [--budget GB] | backup | restore (WSL)
|
||||
# Notes: docs/notes/mem.md
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
# (sourced library inlined above)
|
||||
@@ -140,9 +116,7 @@ BUDGET_EXPLICIT=no # whether --budget was given, which retires the guess belo
|
||||
|
||||
# ── platform ───────────────────────────────────────────────────────────────
|
||||
|
||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
||||
# the tooling. Detectable, so name it instead.
|
||||
# Refuse Git Bash / MSYS / Cygwin and kernels without /proc, with a clear message.
|
||||
require_linux() {
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
@@ -197,9 +171,7 @@ avail_meminfo_mb() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Where a cgroup records this cgroup's own limit and usage. Set once by
|
||||
# find_cgroup, because every later reading needs both and hunting for the files
|
||||
# on each call would be the slow part of the poll loop.
|
||||
# This cgroup's limit/usage files, set once by find_cgroup (cheap for the poll loop).
|
||||
CG_MAX_FILE=""
|
||||
CG_CUR_FILE=""
|
||||
CG_VERSION=""
|
||||
@@ -207,10 +179,8 @@ CG_VERSION=""
|
||||
find_cgroup() {
|
||||
local rel
|
||||
|
||||
# Inside a container the cgroup namespace makes the top of the tree BE the
|
||||
# container's own cgroup, so the unqualified path is already the right one.
|
||||
# On a host it is the root cgroup, which is never limited — hence the second
|
||||
# attempt via /proc/self/cgroup, which names the slice this shell is in.
|
||||
# Top of tree first (right inside a container), then this shell's own slice
|
||||
# from /proc/self/cgroup (right on a host).
|
||||
if [ -r /sys/fs/cgroup/memory.max ]; then
|
||||
CG_VERSION=v2
|
||||
CG_MAX_FILE=/sys/fs/cgroup/memory.max
|
||||
@@ -239,10 +209,7 @@ find_cgroup() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# The cap in MB, or "" when there is none worth reporting. v2 spells unlimited
|
||||
# "max"; v1 spells it as a number near 2^63, which is why this compares against
|
||||
# MemTotal rather than testing for a magic value — a "limit" above the machine's
|
||||
# own memory is not a limit, however it is written.
|
||||
# The cap in MB, or "" when unlimited ("max", or any value >= MemTotal).
|
||||
cgroup_cap_mb() {
|
||||
local raw cap
|
||||
[ -n "$CG_MAX_FILE" ] && [ -r "$CG_MAX_FILE" ] || { echo ""; return 0; }
|
||||
@@ -283,10 +250,7 @@ effective_ceiling_mb() {
|
||||
echo "$c"
|
||||
}
|
||||
|
||||
# How much room is left RIGHT NOW, from whichever accounting actually governs.
|
||||
# In a capped container /proc/meminfo describes the host and is worse than
|
||||
# useless for this — it would report tens of gigabytes free on a box that is one
|
||||
# allocation from being killed.
|
||||
# Room left right now: cgroup cap minus usage when capped, else MemAvailable.
|
||||
headroom_mb() {
|
||||
local cap used
|
||||
cap=$(cgroup_cap_mb)
|
||||
@@ -300,9 +264,7 @@ headroom_mb() {
|
||||
|
||||
# ── status ─────────────────────────────────────────────────────────────────
|
||||
|
||||
# /mnt/c/Users can hold several real accounts — a renamed login leaves the old
|
||||
# directory behind — so picking the first alphabetically is a coin toss. Ask
|
||||
# Windows, then fall back to whichever profile actually owns a config.
|
||||
# Ask Windows for %USERPROFILE%; fall back to whichever profile owns a .wslconfig.
|
||||
wslconfig_path() {
|
||||
local profile winpath found
|
||||
profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true)
|
||||
@@ -358,9 +320,7 @@ status() {
|
||||
echo " ulimit -v unlimited"
|
||||
fi
|
||||
|
||||
# overcommit_memory=0 is the default heuristic: a large allocation is
|
||||
# granted on a guess, and the reckoning arrives later as an OOM kill rather
|
||||
# than as a failed malloc. It is why `push` touches every page it asks for.
|
||||
# Overcommit mode decides whether limits show as failed mallocs or OOM kills.
|
||||
local om or_
|
||||
om=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null || echo '?')
|
||||
or_=$(cat /proc/sys/vm/overcommit_ratio 2>/dev/null || echo '?')
|
||||
@@ -433,9 +393,7 @@ status() {
|
||||
echo " ! docker cli present but the daemon is unreachable"
|
||||
fi
|
||||
|
||||
# WSL keeps its cap on the Windows side, in a file this shell can read but
|
||||
# not usefully apply — the change costs a full VM restart. Report it, and
|
||||
# report the commonest mistake, which is editing it and not restarting.
|
||||
# WSL: report the .wslconfig cap and whether it was applied (needs wsl --shutdown).
|
||||
if is_wsl; then
|
||||
local cfg conf conf_mb n
|
||||
cfg=$(wslconfig_path)
|
||||
@@ -495,7 +453,7 @@ require_wsl() {
|
||||
|
||||
# backup and restore act on the file, so unlike status they must not guess.
|
||||
wslconfig_required() {
|
||||
local cfg; cfg=$(wslconfig_required)
|
||||
local cfg; cfg=$(wslconfig_path)
|
||||
if [ -z "$cfg" ]; then
|
||||
echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2
|
||||
ls -d /mnt/c/Users/*/ 2>/dev/null \
|
||||
@@ -527,8 +485,7 @@ backup() {
|
||||
local cfg dest
|
||||
cfg=$(wslconfig_required)
|
||||
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
||||
# Timestamped and never overwritten: a backup that can destroy itself on a
|
||||
# second run is not a backup.
|
||||
# Timestamped, never overwritten.
|
||||
dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak"
|
||||
cp "$cfg" "$dest"
|
||||
echo "backed up $dest"
|
||||
@@ -547,9 +504,7 @@ restore() {
|
||||
echo " -> $cfg"
|
||||
echo
|
||||
|
||||
# Newest is the right default — undo the last edit — but if you backed up
|
||||
# *after* editing, the state you want is older. Show the rest so a no-op
|
||||
# restore is obviously a no-op rather than a mystery.
|
||||
# Restores the newest; list the others in case an older one is wanted.
|
||||
count=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
||||
if [ "$count" -gt 1 ]; then
|
||||
echo "$count backups exist, newest first:"
|
||||
@@ -593,14 +548,9 @@ cleanup() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# The child allocates and stops itself; the parent only watches. That split is
|
||||
# the point: under --to-oom the allocating process is expected to be killed, and
|
||||
# something has to survive to say how far it got.
|
||||
# Runs as a child that may be OOM-killed; the parent survives to report.
|
||||
allocator() {
|
||||
# Raise our own OOM score to the maximum so the kernel picks THIS process
|
||||
# first. Raising needs no privilege (only lowering does). Without it, the
|
||||
# kernel is free to choose your shell, your ssh session or dockerd — on a
|
||||
# box you are still using, that is not an acceptable coin toss.
|
||||
# Make this process the preferred OOM victim (raising needs no privilege).
|
||||
echo 1000 > "/proc/$BASHPID/oom_score_adj" 2>/dev/null || true
|
||||
|
||||
local arr=() held=0 i=0 rss swapped avail first_swap=0
|
||||
@@ -609,16 +559,7 @@ allocator() {
|
||||
swap_used_start=$(( $(mb SwapTotal) - $(mb SwapFree) ))
|
||||
|
||||
while :; do
|
||||
# Written STRAIGHT INTO the array element. The obvious spelling —
|
||||
# build one chunk and `arr+=("$chunk")` — costs three copies per step,
|
||||
# not one: the template stays resident, expanding "$chunk" makes a
|
||||
# temporary word, and the append makes the element. A 128 MB step then
|
||||
# needs 384 MB transiently, and on a small box it is killed on the
|
||||
# first append while reporting a third of the true ceiling.
|
||||
#
|
||||
# printf -v into a subscript also means every page is written, so it is
|
||||
# resident rather than merely promised — the only kind of allocation
|
||||
# that measures anything under heuristic overcommit.
|
||||
# Write straight into the element (one copy, not three) and touch every page.
|
||||
printf -v "arr[$i]" '%*s' "$bytes" ''
|
||||
i=$((i + 1)); held=$((held + STEP_MB))
|
||||
|
||||
@@ -631,9 +572,7 @@ allocator() {
|
||||
"$held" "$rss" "$avail" "$swapped"
|
||||
printf '%s %s %s %s\n' "$held" "$rss" "$avail" "$swapped" >> "$STATE"
|
||||
|
||||
# Worth calling out separately from the ceiling: this is where the box
|
||||
# stops being fast and starts being unusable, which for a scheduler is
|
||||
# a different and earlier problem than being killed.
|
||||
# First swap is reported separately: slow comes before killed.
|
||||
if [ "$swapped" -gt 0 ] && [ "$first_swap" -eq 0 ]; then
|
||||
first_swap=$held
|
||||
echo " - first swap page at ${held} MB — past here it works but crawls"
|
||||
@@ -654,30 +593,20 @@ push() {
|
||||
total=$(mb MemTotal)
|
||||
ceiling=$(effective_ceiling_mb)
|
||||
|
||||
# A step is worth about a sixty-fourth of the ceiling: enough resolution to
|
||||
# find the edge, few enough lines to read, and small enough that the
|
||||
# transient cost of one allocation never dominates a small box. A fixed
|
||||
# size cannot do all three — 128 MB is fine on 16 GB and absurd on 512 MB.
|
||||
# Default step: ceiling/64, clamped to 4..256 MB.
|
||||
if [ "$STEP_EXPLICIT" = no ]; then
|
||||
STEP_MB=$(( ceiling / 64 ))
|
||||
[ "$STEP_MB" -lt 4 ] && STEP_MB=4
|
||||
[ "$STEP_MB" -gt 256 ] && STEP_MB=256
|
||||
fi
|
||||
|
||||
# Stop with a cushion rather than riding it to the kill. How big a cushion
|
||||
# depends on what it is protecting. Under a cgroup cap, running out kills
|
||||
# only this container's own processes, so it need cover no more than the
|
||||
# shell that prints the result — and a 512 MB cushion on a 1 GB box would
|
||||
# halve the answer. On a host there is everything else to protect, and the
|
||||
# OOM killer does not promise to pick the process that caused the problem.
|
||||
# Stop with a cushion: 64 MB under a cgroup cap, 512 MB on a host, or 5% of ceiling if larger.
|
||||
if [ -n "$(cgroup_cap_mb)" ]; then FLOOR_MB=64; else FLOOR_MB=512; fi
|
||||
[ $(( ceiling / 20 )) -gt "$FLOOR_MB" ] && FLOOR_MB=$(( ceiling / 20 ))
|
||||
|
||||
STATE=$(mktemp "${TMPDIR:-/tmp}/rigmini.XXXXXX")
|
||||
trap cleanup EXIT
|
||||
# INT kills the child and lets the summary below print anyway, so an
|
||||
# impatient Ctrl-C still tells you how far it got — and, more importantly,
|
||||
# still gives the memory back.
|
||||
# Ctrl-C kills the child, frees the memory, and still prints the summary.
|
||||
trap 'echo; echo " interrupted"; echo "stop interrupted" >> "$STATE"; [ -n "$CHILD" ] && kill -KILL "$CHILD" 2>/dev/null || true' INT
|
||||
|
||||
echo "push"
|
||||
@@ -750,11 +679,7 @@ push() {
|
||||
fi ;;
|
||||
esac
|
||||
|
||||
# The gap between the claim and the measurement is the finding — but only
|
||||
# when the BOX chose where to stop. An empty $stop means the child was ended
|
||||
# rather than deciding to end; anything else (--to, the floor) is a stop we
|
||||
# asked for, and flagging those as short of the ceiling would put a warning
|
||||
# on every deliberately small run.
|
||||
# Warn about claimed-vs-measured gap only when the box, not us, chose the stop.
|
||||
local got="${rss:-$held}"
|
||||
echo
|
||||
if [ -z "$stop" ] && [ "$got" -lt $(( ceiling * 70 / 100 )) ]; then
|
||||
|
||||
Reference in New Issue
Block a user