install.sh

16.7 KB · raw

1#!/usr/bin/env sh2# nina install script3# Run:  curl -fsSL kepr.uk/nina/install.sh | sh4# Or:   NINA_INSTALL_DIR=/usr/local/bin sh install.sh56set -e78FLAKE_URL="https://kepr.uk/nina/archive/HEAD.tar.gz"9RELEASES_URL="https://kepr.uk/api/nina/releases/latest"10INSTALL_DIR="${NINA_INSTALL_DIR:-$HOME/.local/bin}"11MIN_ZIG_MINOR=161213# Color, but only when stdout is a real terminal and NO_COLOR is unset. Under14# `curl … | sh` stdin is the pipe yet stdout is still the tty, so -t 1 holds.15# Palette mirrors nina's own: pink prefix, cyan for paths/commands.16if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then17  PINK="$(printf '\033[38;2;255;183;197m')"18  CYAN="$(printf '\033[36m')"19  GREEN="$(printf '\033[32m')"20  RED="$(printf '\033[31m')"21  DIM="$(printf '\033[2m')"22  BOLD="$(printf '\033[1m')"23  RESET="$(printf '\033[0m')"24else25  PINK=""; CYAN=""; GREEN=""; RED=""; DIM=""; BOLD=""; RESET=""26fi2728os="$(uname -s)"29arch="$(uname -m)"30case "${os}-${arch}" in31  Linux-x86_64)               platform="linux-x86_64"  ;;32  Linux-aarch64|Linux-arm64)  platform="linux-aarch64" ;;33  Darwin-arm64)               platform="macos-aarch64" ;;34  Darwin-x86_64)              platform="macos-x86_64"  ;;35  *)36    printf 'nina: unsupported platform %s-%s\n' "$os" "$arch" >&237    exit 1 ;;38esac3940have()  { command -v "$1" >/dev/null 2>&1; }41say()   { printf '%snina:%s %s\n' "$PINK" "$RESET" "$1"; }42die()   { printf '%snina:%s %serror:%s %s\n' "$PINK" "$RESET" "$RED" "$RESET" "$1" >&2; exit 1; }43blank() { printf '\n'; }44is_nixos() { [ -e /etc/NIXOS ]; }4546# Unprefixed output for the closing sections, where the "nina:" stutter on47# every line hurts readability. `out` indents body text; `hr` draws a divider.48out()  { printf '  %s\n' "$1"; }49hr()   { printf '  %s────────────────────────────────────────────────%s\n' "$DIM" "$RESET"; }5051fetch_text() {52  if have curl;   then curl -fsSL "$1"53  elif have wget; then wget -qO- "$1"54  else die "curl or wget required"; fi55}5657fetch_file() {58  if have curl;   then curl -fsSL "$1" -o "$2"59  elif have wget; then wget -qO  "$2" "$1"60  else die "curl or wget required"; fi61}6263# ── banner ────────────────────────────────────────────────────────────────────6465banner() {66  cat <<'BANNER'6768   _ __   _  _ __    __ _69  | '_ \ (_)| '_ \  / _` |70  | | | || || | | || (_| |71  |_| |_||_||_| |_| \__,_|7273  NixOS Intuitive Navigation Assistant  ·  kepr.uk/nina7475BANNER76}7778# ── fetch source archive ──────────────────────────────────────────────────────7980fetch_source() {81  # This function's stdout IS the source path (captured via command82  # substitution), so every diagnostic must go to stderr — otherwise the83  # caller's `cd "$srcdir"` receives the progress text too and fails.84  srcdir="$(mktemp -d /tmp/nina-src.XXXXXX)"85  say "fetching source from Kepr..." >&286  fetch_file "$FLAKE_URL" "${srcdir}/nina.tar.gz" \87    || die "could not download source archive — check your connection"88  tar -xzf "${srcdir}/nina.tar.gz" -C "$srcdir" --strip-components=1 \89    || die "could not extract source archive"90  printf '%s' "$srcdir"91}9293# ── binary install from Kepr ─────────────────────────────────────────────────9495install_binary() {96  say "fetching release metadata..."97  meta="$(fetch_text "$RELEASES_URL" 2>/dev/null)" || return 198  [ -n "$meta" ] || return 199100  if have python3; then101    version="$(printf '%s' "$meta" | python3 -c "102import sys, json103d = json.load(sys.stdin)104print(d.get('version') or '')105" 2>/dev/null)"106    dl_url="$(printf '%s' "$meta" | python3 -c "107import sys, json108d = json.load(sys.stdin)109print((d.get('platforms') or {}).get('${platform}', {}).get('url') or '')110" 2>/dev/null)"111    cksum="$(printf '%s' "$meta" | python3 -c "112import sys, json113d = json.load(sys.stdin)114p = (d.get('platforms') or {}).get('${platform}', {})115raw = p.get('blake3') or p.get('checksum') or ''116print(raw[len('blake3:'):] if raw.startswith('blake3:') else raw)117" 2>/dev/null)"118  elif have jq; then119    version="$(printf '%s' "$meta" | jq -r '.version // empty')"120    dl_url="$(printf '%s' "$meta" | jq -r ".platforms.\"${platform}\".url // empty")"121    cksum_raw="$(printf '%s' "$meta" | jq -r ".platforms.\"${platform}\".blake3 // .platforms.\"${platform}\".checksum // empty")"122    cksum="${cksum_raw#blake3:}"123  else124    version="$(printf '%s' "$meta" | grep -o '"version":"[^"]*"' | sed 's/"version":"//;s/"//')"125    plat_block="$(printf '%s' "$meta" | grep -o "\"${platform}\":{[^}]*}" | head -1)"126    dl_url="$(printf '%s' "$plat_block" | grep -o '"url":"[^"]*"' | sed 's/"url":"//;s/"//')"127    cksum=""128  fi129130  [ -n "$dl_url" ] || return 1131  [ -n "$version" ] || version="latest"132133  say "downloading nina ${version} for ${platform}..."134  tmpfile="$(mktemp /tmp/nina.XXXXXX)"135  fetch_file "$dl_url" "$tmpfile" || { rm -f "$tmpfile"; return 1; }136137  if [ -n "$cksum" ] && have b3sum; then138    actual="$(b3sum --no-names "$tmpfile" 2>/dev/null | awk '{print $1}')"139    if [ "$actual" != "$cksum" ]; then140      say "checksum mismatch — aborting"141      rm -f "$tmpfile"142      return 1143    fi144    say "checksum verified"145  fi146147  chmod +x "$tmpfile"148  mkdir -p "$INSTALL_DIR"149  mv "$tmpfile" "${INSTALL_DIR}/nina"150  say "installed nina ${version} → ${INSTALL_DIR}/nina"151  return 0152}153154# ── NixOS: nix profile install (primary) ─────────────────────────────────────155156nixos_profile_install() {157  have nix || return 1158  say "installing nina to ~/.nix-profile/bin/nina via nix profile..."159  blank160  # Tarball flakes are fetched read-only into the store, so nix cannot persist161  # a freshly-resolved flake.lock and aborts with "cannot write modified lock162  # file". --no-write-lock-file resolves inputs in memory without writing.163  if nix profile install --no-write-lock-file "${FLAKE_URL}#nina"; then164    return 0165  fi166  blank167  say "nix profile install failed. Common causes:"168  say "  flakes not enabled — add to /etc/nix/nix.conf or ~/.config/nix/nix.conf:"169  say "    experimental-features = nix-command flakes"170  say "  once nina is installed: nina setup enables this automatically"171  blank172  return 1173}174175# ── NixOS: nix-shell source build (fallback) ──────────────────────────────────176177nixos_shell_build() {178  have nix-shell || return 1179  say "fetching source and building nina using nix-shell..."180  blank181  srcdir="$(fetch_source)"182183  # Try the user's channel first; fall back to a pinned nixpkgs-unstable if184  # zig_0_16 is not yet in the channel (common on NixOS 24.11 stable).185  build_cmd="cd '${srcdir}' && zig build -Doptimize=ReleaseSafe"186  if nix-shell -p zig_0_16 --run "$build_cmd" 2>/dev/null; then187    :188  else189    say "zig_0_16 not in channel — fetching from nixpkgs-unstable..."190    # channels.nixos.org redirects to the current unstable release tarball;191    # the older releases.nixos.org/.../nixos-unstable/ path 404s.192    nix-shell \193      -I nixpkgs=https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz \194      -p zig_0_16 \195      --run "$build_cmd" \196      || die "build failed — see output above"197  fi198199  mkdir -p "$INSTALL_DIR"200  cp "${srcdir}/zig-out/bin/nina" "${INSTALL_DIR}/nina"201  chmod +x "${INSTALL_DIR}/nina"202  blank203  say "installed from source → ${INSTALL_DIR}/nina"204  say "note: binary built outside nix — adopt ~/nina.nix into configuration.nix"205  say "      to make nina fully nix-managed (nina goodbye handles removal either way)"206}207208# ── generate ~/nina.nix ───────────────────────────────────────────────────────209210# Offer to wire ~/nina.nix into /etc/nixos/configuration.nix. Returns 0 when the211# import is in place (added now, or already present), 1 when the caller should212# fall back to printing manual instructions. Conservative by design: only edits213# an existing `imports = [ … ]` list, always backs the file up first, and never214# touches anything without an explicit yes from the terminal.215offer_config_import() {216  config="$1"217  nina_nix="$2"218219  [ -f "$config" ] && [ -r "$config" ] || return 1220221  # Already wired up — just remind how to apply it.222  if grep -qF "$nina_nix" "$config" 2>/dev/null; then223    out "already imported in ${CYAN}${config}${RESET}"224    out "apply with  ${BOLD}sudo nixos-rebuild switch${RESET}"225    blank226    return 0227  fi228229  # Need an existing imports list to extend; guessing where else to put it230  # risks producing a config that won't evaluate.231  grep -qE '^[[:space:]]*imports[[:space:]]*=[[:space:]]*\[' "$config" || return 1232233  # Read the answer from the terminal so it works under `curl … | sh`.234  [ -r /dev/tty ] || return 1235  printf '  add it to %s%s%s now? %s[y/N]%s ' "$CYAN" "$config" "$RESET" "$DIM" "$RESET" > /dev/tty236  IFS= read -r ans < /dev/tty 2>/dev/null || ans=""237  case "$ans" in238    [Yy]*) ;;239    *) return 1 ;;240  esac241242  ts="$(date +%Y%m%d%H%M%S 2>/dev/null || echo backup)"243  backup="${config}.nina-bak.${ts}"244  sudo cp "$config" "$backup" 2>/dev/null \245    || { blank; out "could not back up ${config} — add it manually instead."; blank; return 1; }246247  # Insert our path as the first entry of the first imports list.248  tmp="$(mktemp /tmp/nina-conf.XXXXXX)"249  awk -v p="$nina_nix" '250    !done && /imports[[:space:]]*=[[:space:]]*\[/ { sub(/\[/, "[ " p); done=1 }251    { print }252  ' "$config" > "$tmp" 2>/dev/null253254  if ! sudo cp "$tmp" "$config" 2>/dev/null; then255    rm -f "$tmp"256    blank257    out "could not write ${config}."258    out "restore the backup with:  sudo cp ${backup} ${config}"259    blank260    return 1261  fi262  rm -f "$tmp"263264  blank265  out "${GREEN}✓${RESET} added the import to ${CYAN}${config}${RESET}"266  out "  ${DIM}backup kept at ${backup}${RESET}"267  out "  apply with  ${BOLD}sudo nixos-rebuild switch${RESET}"268  blank269  return 0270}271272generate_nixos_module() {273  # Write ~/nina.nix without invoking shell substitution on the nix code inside.274  cat > "$HOME/nina.nix" <<'NIXEOF'275# ~/nina.nix — generated by the nina installer276#277# Declares nina as a NixOS system package.278#279# Add to /etc/nixos/configuration.nix:280#   imports = [ /home/YOUR_USER/nina.nix ];281# Then: sudo nixos-rebuild switch282#283# To update:  nina update  (or re-run the installer)284# To remove:  nina goodbye  (or delete this file, then nixos-rebuild switch)285#286# For flake-based configurations, add nina as a flake input instead:287#   inputs.nina.url = "https://kepr.uk/nina/archive/HEAD.tar.gz";288#   # in your nixosModules list: nina.nixosModules.default289#   # then set: programs.nina.enable = true;290{ ... }:291{292  imports = [293    "${builtins.fetchTarball "https://kepr.uk/nina/archive/HEAD.tar.gz"}/nixos-module.nix"294  ];295  programs.nina.enable = true;296}297NIXEOF298299  nina_nix="$HOME/nina.nix"300301  blank302  hr303  out "${BOLD}system-wide install${RESET}  ${DIM}(optional)${RESET}"304  hr305  blank306  out "nina is installed for your user. To manage her alongside the"307  out "rest of your system, a module was written to:"308  blank309  out "    ${CYAN}${nina_nix}${RESET}"310  blank311312  if offer_config_import "/etc/nixos/configuration.nix" "$nina_nix"; then313    return 0314  fi315316  blank317  out "to do it yourself, add this to the ${CYAN}imports${RESET} list in"318  out "${CYAN}/etc/nixos/configuration.nix${RESET}:"319  blank320  out "    ${CYAN}${nina_nix}${RESET}"321  blank322  out "then run  ${BOLD}sudo nixos-rebuild switch${RESET}"323  blank324}325326# ── Zig version check ─────────────────────────────────────────────────────────327328check_zig() {329  if ! have zig; then330    blank331    say "Zig not found. Building nina requires Zig 0.${MIN_ZIG_MINOR}+."332    blank333    say "  Download: https://ziglang.org/download/"334    if is_nixos; then335      say "  NixOS:    nix-shell -p zig_0_16 --run 'sh install.sh'"336    fi337    blank338    die "Zig 0.${MIN_ZIG_MINOR}+ required"339  fi340341  zig_ver="$(zig version 2>/dev/null)"342  major="$(printf '%s' "$zig_ver" | cut -d. -f1)"343  minor="$(printf '%s' "$zig_ver" | cut -d. -f2)"344  ok=0345  [ "$major" -ge 1 ] 2>/dev/null && ok=1346  [ "$major" -eq 0 ] 2>/dev/null && [ "$minor" -ge "$MIN_ZIG_MINOR" ] 2>/dev/null && ok=1347348  if [ "$ok" -ne 1 ]; then349    blank350    say "found zig ${zig_ver}, but nina requires Zig 0.${MIN_ZIG_MINOR}+."351    if is_nixos; then352      say "  run: nix-shell -p zig_0_16 --run 'sh install.sh'"353    else354      say "  update: https://ziglang.org/download/"355    fi356    blank357    die "Zig 0.${MIN_ZIG_MINOR}+ required; found ${zig_ver}"358  fi359360  say "zig ${zig_ver} found"361}362363# ── generic source build (non-NixOS) ─────────────────────────────────────────364365build_from_source() {366  say "building from source..."367  check_zig368369  if [ -f "build.zig" ] && [ -f "src/main.zig" ]; then370    srcdir="."371  else372    srcdir="$(fetch_source)"373  fi374375  (cd "$srcdir" && zig build -Doptimize=ReleaseSafe) \376    || die "build failed — see output above"377378  mkdir -p "$INSTALL_DIR"379  cp "${srcdir}/zig-out/bin/nina" "${INSTALL_DIR}/nina"380  chmod +x "${INSTALL_DIR}/nina"381  say "installed from source → ${INSTALL_DIR}/nina"382}383384# ── PATH reminder ─────────────────────────────────────────────────────────────385386check_path() {387  case ":${PATH}:" in388    *":${INSTALL_DIR}:"*) ;;389    *)390      blank391      say "${INSTALL_DIR} is not in your PATH. Add it:"392      say "  export PATH=\"\${PATH}:${INSTALL_DIR}\""393      say "  echo 'export PATH=\"\${PATH}:${INSTALL_DIR}\"' >> ~/.bashrc"394      if is_nixos; then395        say "  NixOS: add to environment.sessionVariables in configuration.nix"396      fi397      ;;398  esac399}400401# ── enable nix experimental features ──────────────────────────────────────────402# nina needs nix-command + flakes to search and install. If they're off, offer to403# turn them on via the freshly-installed `nina setup`. Read from /dev/tty so the404# prompt works even under `curl … | sh`, where stdin is the script itself.405406maybe_setup_features() {407  have nix || return 0408  # `nix eval` itself requires nix-command, so this doubles as the feature probe.409  nix eval --expr '1' >/dev/null 2>&1 && return 0410411  NINA="${INSTALL_DIR}/nina"412  [ -x "$NINA" ] || NINA="nina"413414  blank415  say "nina needs nix's experimental features (nix-command and flakes) to"416  say "search and install packages."417418  if [ -r /dev/tty ]; then419    printf 'nina: enable them now? [Y/n] ' > /dev/tty420    IFS= read -r ans < /dev/tty 2>/dev/null || ans="y"421    case "$ans" in422      [Nn]*) say "skipped — enable later with:  nina setup"; return 0 ;;423    esac424    "$NINA" setup || say "automatic setup failed — run:  nina setup"425  else426    say "run  nina setup  to enable them"427  fi428}429430# ── closing message ──────────────────────────────────────────────────────────431432farewell() {433  blank434  printf '  %sthank you!!%s get started by running %snina hello%s\n' "$PINK" "$RESET" "$CYAN" "$RESET"435  printf '  run %snina update%s occasionally to keep me in tip-top shape\n' "$CYAN" "$RESET"436  blank437  printf '  %snot for you?%s %snina goodbye%s %sremoves her cleanly%s\n' "$DIM" "$RESET" "$CYAN" "$RESET" "$DIM" "$RESET"438  blank439}440441# ── main ──────────────────────────────────────────────────────────────────────442443banner444445if is_nixos; then446  say "NixOS detected"447  if nixos_profile_install; then448    generate_nixos_module449    maybe_setup_features450    farewell451    exit 0452  fi453  # nina's binaries link no libc, so the Linux builds are fully static and run454  # on NixOS — try the prebuilt before falling back to a from-source build.455  say "trying prebuilt binary from Kepr..."456  if install_binary; then457    check_path458    maybe_setup_features459    farewell460    exit 0461  fi462  say "no prebuilt binary available — building from source"463  nixos_shell_build464  generate_nixos_module465  check_path466  maybe_setup_features467  farewell468  exit 0469fi470471# macOS / generic Linux: download binary, fall back to source build472if install_binary; then473  check_path474  maybe_setup_features475  farewell476  exit 0477fi478479say "no pre-built binary for ${platform} — falling back to source build"480build_from_source481check_path482maybe_setup_features483farewell