#!/usr/bin/env bash # GENERATED by make standalone — do not edit # # rigdeps.sh for profile 'default', flattened from: # ctrl/deps.sh # ctrl/lib/config.sh # Edit those and run `make standalone`. Changes made here are lost, and # `make selftest` fails while this file differs from what rig generates. # ── from the libraries ── declare -- CONFIG_OVERRIDABLE=$'PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS\n REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT\n SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT\n REGISTRY_PORT MANIFESTS_DIR' _config_restore () { local line; while IFS= read -r line; do if [ -n "$line" ]; then eval "export $line"; fi; done <<< "$1"; return 0 } default_cluster_name () { local n; n=$(basename "$(cd .. && pwd)"); n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-'); n=$(echo "$n" | sed 's/^-*//; s/-*$//'); echo "${n:-rig}" } derive_port_base () { local h; h=$(printf '%s' "$1" | cksum | awk '{print $1}'); echo $((20000 + (h % 200) * 10)) } render_kind_config () { local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}"; sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" "$KIND_CONFIG_PATH" } # ── configuration, frozen for profile 'default' ── load_config() { local k saved="" for k in $CONFIG_OVERRIDABLE; do if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi done declare -g ADDONS="" declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4" declare -g AUDIT="off" declare -gx CERT_MANAGER_VERSION="v1.21.1" declare -g CLUSTER="rig" declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576" declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64" declare -gx COMPOSE_VERSION="5.5.1" declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e" declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz" declare -gx CTLPTL_VERSION="0.9.4" declare -g DNS_MODE="hosts" declare -g HTTPS_PORT="20311" declare -g HTTP_PORT="20310" declare -g INGRESS_MODE="hostport" declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f" declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64" declare -gx JQ_VERSION="1.8.2" declare -g K8S_VERSION="v1_36" declare -g KIND_CONFIG="kind-config.yaml.tpl" declare -g KIND_CONFIG_PATH="./k8s/kind-config.yaml.tpl" declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.yaml.tpl" declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54" declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64" declare -gx KIND_VERSION="v0.32.0" declare -g KUBECONTEXT="kind-rig" declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336" declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl" declare -gx KUBECTL_VERSION="v1.36.3" declare -g MANIFESTS_DIR="ctrl/k8s/overlays/dev" declare -gx METALLB_VERSION="v0.16.0" declare -gx METRICS_SERVER_VERSION="v0.9.0" declare -g NODES="1" declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5" declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4" declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256" declare -gx NODE_IMAGE_v1_35="kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95" declare -gx NODE_IMAGE_v1_36="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5" declare -g NODE_MB="800" declare -gx POSTGRES_IMAGE="postgres:16-alpine" declare -gx PROFILE="default" declare -g PROFILE_NAME="default" declare -gx REDIS_IMAGE="redis:7-alpine" declare -gx REGISTRY_IMAGE="registry:2" declare -g REGISTRY_MODE="local" declare -g REGISTRY_PORT="20313" declare -gx STUB_IMAGE="python:3.12-slim" declare -g TILT_PORT="20312" declare -gx TILT_SHA256="e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6" declare -gx TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v0.37.6/tilt.0.37.6.linux.x86_64.tar.gz" declare -gx TILT_VERSION="0.37.6" _config_restore "$saved" } # ── 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 /. set -euo pipefail # Keep the caller's cwd so a relative --to resolves where the user expects, # not against ctrl/ once we've moved. 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. # (sourced library inlined above) load_config # Resolve a possibly-relative path against the caller's original directory. abspath() { case "$1" in /*) echo "$1" ;; *) echo "$INVOKED_FROM/$1" ;; esac } OUT_BIN="${OUT_BIN:-$HOME/.local/bin}" HOST_ROOT="${HOST_ROOT:-/}" DEPS_SOURCE="${DEPS_SOURCE:-upstream}" DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}" 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. mb_of() { awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 } END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}" } host_file() { local p="${1#/}" if [ "$HOST_ROOT" != "/" ] && [ -e "$HOST_ROOT/$p" ]; then echo "$HOST_ROOT/$p" else echo "/$p" fi } # ── the tools this script itself needs ───────────────────────────────────── arch() { case "$(uname -m)" in x86_64|amd64) echo amd64 ;; aarch64|arm64) echo arm64 ;; *) uname -m ;; 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. require_amd64() { local a; a=$(arch) [ "$a" = "amd64" ] && return 0 cat >&2 </dev/null 2>&1; then DL=curl elif command -v wget >/dev/null 2>&1; then DL=wget else echo "neither curl nor wget is installed, so nothing can be downloaded." >&2 echo "Install one first: $(pkg_install_cmd curl)" >&2 exit 1 fi } download() { local url="$1" out="$2" case "$DL" in curl) curl -fsSL --retry 3 -o "$out" "$url" ;; wget) wget -q --tries=3 -O "$out" "$url" ;; esac } SHA="" pick_sha() { if command -v sha256sum >/dev/null 2>&1; then SHA=sha256sum elif command -v shasum >/dev/null 2>&1; then SHA="shasum -a 256" else echo "no sha256sum and no shasum — downloads could not be verified." >&2 echo "Refusing to install unverified binaries." >&2 exit 1 fi } # ── 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. pkg_install_cmd() { local pkg="$1" if command -v apt-get >/dev/null 2>&1; then echo "sudo apt-get update && sudo apt-get install -y $pkg" elif command -v dnf >/dev/null 2>&1; then echo "sudo dnf install -y $pkg" elif command -v yum >/dev/null 2>&1; then echo "sudo yum install -y $pkg" elif command -v zypper >/dev/null 2>&1; then echo "sudo zypper install -y $pkg" elif command -v apk >/dev/null 2>&1; then echo "sudo apk add $pkg" else echo "install '$pkg' with this system's package manager" fi } docker_pkg() { # Debian and Ubuntu call it docker.io; the RPM distros call it docker. if command -v apt-get >/dev/null 2>&1; then echo docker.io; else echo docker; fi } # ── 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. require_linux() { case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) cat >&2 <<'EOF' This has to run inside WSL, not Git Bash / MSYS / Cygwin. If WSL is not installed yet, from an elevated PowerShell or Command Prompt: wsl --install That enables Windows features and needs a reboot, so it is not something this script will do for you. Afterwards, open the Linux shell it installs and run this from there. See "Starting from plain Windows" in README.md. EOF exit 1 ;; esac } is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; } detect() { echo "host" echo " kernel $(uname -r)" echo " arch $(arch) ($(uname -m))" local osr; osr=$(host_file /etc/os-release) [ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")" # 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. 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 # 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. 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" ;; esac echo " install to $OUT_BIN" detect_libc detect_prereqs detect_wsl detect_filesystem detect_docker detect_inotify detect_toolchain } 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. 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" else echo " ! systemd not enabled in /etc/wsl.conf" MANUAL+=("Enable systemd — add to /etc/wsl.conf: [boot] systemd=true then from a WINDOWS terminal (not this shell): wsl --shutdown") fi # 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)" else echo " - 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 ' ')" else MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted: make mem status It prints the edit to make and the command to apply it.") 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. watch_hostile_fs() { local dir="$1" fstype fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true) [ -n "$fstype" ] || fstype=$(stat -f -c %T "$dir" 2>/dev/null || true) case "$fstype" in 9p|v9fs|drvfs|cifs|smb3|nfs|nfs4|fuse.sshfs|fuseblk) echo "$fstype" ;; *) echo "" ;; esac } detect_filesystem() { local root fstype root=$(cd .. && pwd -P) fstype=$(watch_hostile_fs "$root") if [ -n "$fstype" ]; then echo " ! this directory is on $fstype — file watching will not work" MANUAL+=("Move this onto the local disk. Nothing watching files sees changes 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))" 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. 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" return 0 fi echo " 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." echo " Install the core tier, or run tilt from a container." fi 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. 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" 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" 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" 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." missing+=" tar gzip" fi if [ -n "$missing" ]; then MANUAL+=("Install what this script needs to run at all: $(pkg_install_cmd "${missing# }")") fi return 0 } 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. 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)" else echo " ! docker not found and no socket at /var/run/docker.sock" MANUAL+=("Install Docker — the one true prerequisite, and the only thing here that needs root: $(pkg_install_cmd "$(docker_pkg)") sudo systemctl enable --now docker sudo usermod -aG docker \"\$USER\" then log out and back in, so the new group applies to your shell.") fi return fi if docker info >/dev/null 2>&1; then 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. if [ "$n" -gt 0 ]; then echo " - $n kind node container(s) already running; see 'make cluster list'" fi else echo " ! docker cli present but the daemon is unreachable" MANUAL+=("Start Docker, or add yourself to the docker group: sudo usermod -aG docker \"\$USER\" # then log out and back in") fi } # kind and Tilt both watch large trees. WSL ships defaults (8192/128) far too low, # and the failure mode is silent: Tilt simply stops noticing file changes. 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" if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then echo " ! inotify limits are low — Tilt will silently stop noticing file changes" MANUAL+=("Raise inotify limits (needs root on the host): echo -e 'fs.inotify.max_user_watches=524288\\nfs.inotify.max_user_instances=512' \\ | sudo tee /etc/sysctl.d/99-rig.conf sudo sysctl --system") fi } # ── fetch ────────────────────────────────────────────────────────────────── # Resolve where a given artifact comes from, honouring DEPS_SOURCE. resolve_url() { local upstream="$1" case "$DEPS_SOURCE" in upstream) echo "$upstream" ;; artifactory) if [ -z "$DEPS_ARTIFACTORY_URL" ]; then echo "DEPS_SOURCE=artifactory but DEPS_ARTIFACTORY_URL is empty" >&2 exit 1 fi echo "${DEPS_ARTIFACTORY_URL%/}/$(basename "$upstream")" ;; *) echo "unsupported DEPS_SOURCE '$DEPS_SOURCE' for a download" >&2; exit 1 ;; esac } verify() { local file="$1" want="$2" name="$3" got got=$($SHA "$file" | awk '{print $1}') if [ "$got" != "$want" ]; then echo "checksum mismatch for $name" >&2 echo " expected $want" >&2 echo " got $got" >&2 exit 1 fi } # fetch_bin — a bare binary fetch_bin() { local name="$1" url="$2" sha="$3" dest="$4" local tmp="$dest/.$name.tmp" echo " fetching $name" download "$(resolve_url "$url")" "$tmp" verify "$tmp" "$sha" "$name" mv "$tmp" "$dest/$name" chmod +x "$dest/$name" } # fetch_tgz # Archive layouts differ — tilt's is flat (the binary at the root, strip=0), # others nest it a directory down — so the caller says which. fetch_tgz() { local name="$1" url="$2" sha="$3" dest="$4" inner="$5" strip="$6" local tmp="$dest/.$name.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. 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). fix_ownership() { local dir="$1" [ -d "$dir" ] || return 0 local owner="${HOST_UID:-}:${HOST_GID:-}" if [ "$owner" = ":" ]; then owner=$(stat -c '%u:%g' "$dir") fi [ "$owner" = "0:0" ] && return 0 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_TOOLS="kubectl jq" # No helm: every addon installs with `kubectl apply -f `, 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/ 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. 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. pin_of() { case "$1" in kubectl) echo "$KUBECTL_VERSION" ;; jq) echo "$JQ_VERSION" ;; kind) echo "$KIND_VERSION" ;; tilt) echo "$TILT_VERSION" ;; ctlptl) echo "$CTLPTL_VERSION" ;; docker-compose) echo "$COMPOSE_VERSION" ;; 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. reported_version() { local tool="$1" path="$2" case "$tool" in kubectl) "$path" version --client 2>/dev/null ;; jq) "$path" --version 2>/dev/null ;; *) "$path" version 2>/dev/null ;; 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. version_matches() { local tool="$1" path="$2" pin="$3" out v re out=$(reported_version "$tool" "$path") || return 1 v="${pin#v}" v="${v//./\\.}" re="(^|[^0-9.])v?${v}([^0-9.]|\$)" [[ $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. want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; } # Every tool in the tier with its state, probed once and reported once. What # still needs fetching is left in TOOLCHAIN_NEED for install() to act on. TOOLCHAIN_NEED="" detect_toolchain() { local tier="${TIER:-dev}" b pin path found TOOLCHAIN_NEED="" echo echo "toolchain (pinned, tier '$tier')" for b in $(tier_tools "$tier"); do 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. 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" else printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \ "$b" "$pin" "$found" TOOLCHAIN_NEED+="$b " fi continue fi fi if [ -z "$path" ]; then 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" else found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true) printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found" TOOLCHAIN_NEED+="$b " fi done if [ -z "$TOOLCHAIN_NEED" ]; then echo " every pinned tool is already on PATH — nothing to fetch" else echo " 'make deps' fetches only: ${TOOLCHAIN_NEED% }" fi } fetch() { local dest="$OUT_BIN" tier="${TIER:-dev}" while [ $# -gt 0 ]; do case "$1" in --to) dest="$2"; shift 2 ;; core|dev) tier="$1"; shift ;; *) echo "unknown argument: $1" >&2; exit 1 ;; esac done dest="$(abspath "$dest")" mkdir -p "$dest" TIER="$tier" if [ "$DEPS_SOURCE" = "baked" ]; then echo "installing baked binaries from $BAKED_BIN" cp -a "$BAKED_BIN"/. "$dest"/ fix_ownership "$dest" return fi if [ -n "${DEPS_ONLY:-}" ]; then echo "fetching ${DEPS_ONLY% } (source: $DEPS_SOURCE)" else echo "fetching '$tier' toolchain (source: $DEPS_SOURCE)" fi if want kubectl; then fetch_bin kubectl "$KUBECTL_URL" "$KUBECTL_SHA256" "$dest"; fi if want jq; then fetch_bin jq "$JQ_URL" "$JQ_SHA256" "$dest"; fi if [ "$tier" = "dev" ]; then if want kind; then fetch_bin kind "$KIND_URL" "$KIND_SHA256" "$dest"; fi if want tilt; then fetch_tgz tilt "$TILT_URL" "$TILT_SHA256" "$dest" tilt 0; fi if want ctlptl; then fetch_tgz ctlptl "$CTLPTL_URL" "$CTLPTL_SHA256" "$dest" ctlptl 0; fi if want docker-compose; then fetch_bin docker-compose "$COMPOSE_URL" "$COMPOSE_SHA256" "$dest" fi 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. fix_ownership "${KUBE_DIR:-/out/kube}" } # ── install ──────────────────────────────────────────────────────────────── report_manual() { echo if [ ${#MANUAL[@]} -eq 0 ]; then echo "nothing left to do by hand." return fi echo "host actions this cannot perform (${#MANUAL[@]}):" echo local n=1 for m in "${MANUAL[@]}"; do echo " $n. $m" echo n=$((n + 1)) 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. verify_tools() { local tier="${1:-dev}" b bin out rc broke=0 echo "checking that each one actually runs" for b in $(tier_tools "$tier"); do bin="$OUT_BIN/$b" if [ ! -x "$bin" ]; then 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. rc=0 case "$b" in kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;; jq) out=$("$bin" --version 2>&1) || rc=$? ;; *) out=$("$bin" version 2>&1) || rc=$? ;; esac out=${out%%$'\n'*} if [ "$rc" -eq 0 ]; then printf ' %-14s %s\n' "$b" "$out" else printf ' ! %-12s does not run here: %s\n' "$b" "$out" broke=1 fi done if [ "$broke" -eq 1 ]; then echo echo " A binary that downloads and verifies but will not start is almost" echo " always this distro's libc being older than the release needs." echo " 'detect' prints the glibc version. The core tier (kubectl + jq)" echo " has no such dependency and will work regardless." fi return 0 } list() { echo "pinned, linux/amd64 only:" printf ' %-14s %s\n' kubectl "$KUBECTL_VERSION" printf ' %-14s %s\n' jq "$JQ_VERSION" printf ' %-14s %s\n' kind "$KIND_VERSION" printf ' %-14s %s\n' tilt "$TILT_VERSION" printf ' %-14s %s\n' ctlptl "$CTLPTL_VERSION" printf ' %-14s %s\n' docker-compose "$COMPOSE_VERSION" echo echo " core = $CORE_TOOLS" echo " dev = $CORE_TOOLS $DEV_TOOLS" echo echo "Checksums are pinned in the block at the top of this file. To bump one," echo "take the new checksum from the publisher's own release list — the header" echo "comment has the exact commands." return 0 } tier_tools() { [ "$1" = "core" ] && echo "$CORE_TOOLS" || echo "$CORE_TOOLS $DEV_TOOLS"; } warn_shadowing() { local b existing shadowed="" tier="${1:-dev}" for b in $(tier_tools "$tier"); do [ -x "$OUT_BIN/$b" ] || continue # Where would this resolve if OUT_BIN weren't in the way? existing=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -vx "$OUT_BIN" | paste -sd:) \ command -v "$b" 2>/dev/null || true) [ -n "$existing" ] || continue [ "$existing" = "$OUT_BIN/$b" ] && continue # The same version in both places is not a conflict: nothing changes for # any other project whichever copy PATH happens to find first. if version_matches "$b" "$existing" "$(pin_of "$b")"; then continue; fi shadowed+=" $b $existing"$'\n' done [ -n "$shadowed" ] || return 0 case ":${PATH}:" in *":$OUT_BIN:"*) ;; *) return 0 ;; # not on PATH yet, so nothing is being shadowed esac echo echo " ! these were already installed elsewhere and are now shadowed by $OUT_BIN:" printf '%s' "$shadowed" echo " Other projects on this machine will pick up the new versions." MANUAL+=("Decide which toolchain wins. To keep the previous one, remove what was just installed: rm -f $(for b in $(tier_tools "$tier"); do printf '%s ' "$OUT_BIN/$b"; done) Or install somewhere private instead: 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. 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. if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then MANUAL+=("Something already installs the compose plugin at $dir/docker-compose To use rig's pinned build instead: ln -sf $src $dir/docker-compose") return 0 fi ln -sfn "$src" "$dir/docker-compose" echo " compose plugin -> $dir/docker-compose" return 0 } install() { local tier="${1:-dev}" b TIER="$tier" detect # detect_toolchain has already probed PATH. Fetch only what it found missing # or at the wrong version; a tool already present at its pin stays where it is. if [ -n "$TOOLCHAIN_NEED" ]; then echo DEPS_ONLY="$TOOLCHAIN_NEED" fetch "$tier" echo echo "installed to $OUT_BIN ($tier):" for b in $TOOLCHAIN_NEED; do if [ -x "$OUT_BIN/$b" ]; then echo " $b"; fi done 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. 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. case ":${PATH}:" in *":$OUT_BIN:"*) ;; *) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc: export PATH=\"${OUT_BIN}:\$PATH\"") ;; esac fi warn_shadowing "$tier" report_manual } # ── main ─────────────────────────────────────────────────────────────────── 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. cmd="${1:-install}" [ $# -gt 0 ] && shift # Baked mode copies binaries already in the image, so it needs no downloader. need_downloads() { require_amd64 if [ "$DEPS_SOURCE" != baked ]; then pick_downloader; fi pick_sha } case "$cmd" in detect) 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 " install [core|dev] (default dev)" >&2 echo " fetch [core|dev] [--to DIR]" >&2 echo " OUT_BIN= overrides the install directory" >&2 exit 1 ;; esac