566 lines
22 KiB
Bash
Executable File
566 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# 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 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")"
|
|
|
|
source ./versions.env
|
|
|
|
# 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
|
|
}
|
|
|
|
# ── 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)"
|
|
|
|
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
|
|
|
|
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
|
|
}
|
|
|
|
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:
|
|
sudo apt-get install -y docker.io && sudo usermod -aG docker \"\$USER\"
|
|
then log out and back in.")
|
|
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=$(sha256sum "$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 <name> <url> <sha256> <dest-dir> — a bare binary
|
|
fetch_bin() {
|
|
local name="$1" url="$2" sha="$3" dest="$4"
|
|
local tmp="$dest/.$name.tmp"
|
|
echo " fetching $name"
|
|
curl -fsSL --retry 3 -o "$tmp" "$(resolve_url "$url")"
|
|
verify "$tmp" "$sha" "$name"
|
|
mv "$tmp" "$dest/$name"
|
|
chmod +x "$dest/$name"
|
|
}
|
|
|
|
# fetch_tgz <name> <url> <sha256> <dest-dir> <path-inside-archive> <strip>
|
|
# 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"
|
|
curl -fsSL --retry 3 -o "$tmp" "$(resolve_url "$url")"
|
|
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 <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.
|
|
DEV_TOOLS="kind tilt ctlptl"
|
|
|
|
# ── 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: the AWS Workspace
|
|
# keeps its toolchain in ~/wdir/bin, all five at exactly these pins.
|
|
|
|
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" ;;
|
|
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)
|
|
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
|
|
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.
|
|
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")
|
|
}
|
|
|
|
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 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
|
|
|
|
case "${1:-install}" in
|
|
detect) detect; report_manual ;;
|
|
fetch) shift; fetch "$@" ;;
|
|
install) shift; install "${1:-dev}" ;;
|
|
*) echo "usage: $0 [detect|fetch|install]" >&2; exit 1 ;;
|
|
esac
|