install.sh

15.8 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=161213os="$(uname -s)"14arch="$(uname -m)"15case "${os}-${arch}" in16  Linux-x86_64)               platform="linux-x86_64"  ;;17  Linux-aarch64|Linux-arm64)  platform="linux-aarch64" ;;18  Darwin-arm64)               platform="macos-aarch64" ;;19  Darwin-x86_64)              platform="macos-x86_64"  ;;20  *)21    printf 'nina: unsupported platform %s-%s\n' "$os" "$arch" >&222    exit 1 ;;23esac2425have()  { command -v "$1" >/dev/null 2>&1; }26say()   { printf 'nina: %s\n' "$1"; }27die()   { say "error: $1" >&2; exit 1; }28blank() { printf '\n'; }29is_nixos() { [ -e /etc/NIXOS ]; }3031# Unprefixed output for the closing sections, where the "nina:" stutter on32# every line hurts readability. `out` indents body text; `hr` draws a divider.33out() { printf '  %s\n' "$1"; }34hr()  { printf '  ────────────────────────────────────────────────\n'; }3536fetch_text() {37  if have curl;   then curl -fsSL "$1"38  elif have wget; then wget -qO- "$1"39  else die "curl or wget required"; fi40}4142fetch_file() {43  if have curl;   then curl -fsSL "$1" -o "$2"44  elif have wget; then wget -qO  "$2" "$1"45  else die "curl or wget required"; fi46}4748# ── banner ────────────────────────────────────────────────────────────────────4950banner() {51  cat <<'BANNER'5253   _ __   _  _ __    __ _54  | '_ \ (_)| '_ \  / _` |55  | | | || || | | || (_| |56  |_| |_||_||_| |_| \__,_|5758  NixOS Intuitive Navigation Assistant  ·  kepr.uk/nina5960BANNER61}6263# ── fetch source archive ──────────────────────────────────────────────────────6465fetch_source() {66  # This function's stdout IS the source path (captured via command67  # substitution), so every diagnostic must go to stderr — otherwise the68  # caller's `cd "$srcdir"` receives the progress text too and fails.69  srcdir="$(mktemp -d /tmp/nina-src.XXXXXX)"70  say "fetching source from Kepr..." >&271  fetch_file "$FLAKE_URL" "${srcdir}/nina.tar.gz" \72    || die "could not download source archive — check your connection"73  tar -xzf "${srcdir}/nina.tar.gz" -C "$srcdir" --strip-components=1 \74    || die "could not extract source archive"75  printf '%s' "$srcdir"76}7778# ── binary install from Kepr ─────────────────────────────────────────────────7980install_binary() {81  say "fetching release metadata..."82  meta="$(fetch_text "$RELEASES_URL" 2>/dev/null)" || return 183  [ -n "$meta" ] || return 18485  if have python3; then86    version="$(printf '%s' "$meta" | python3 -c "87import sys, json88d = json.load(sys.stdin)89print(d.get('version') or '')90" 2>/dev/null)"91    dl_url="$(printf '%s' "$meta" | python3 -c "92import sys, json93d = json.load(sys.stdin)94print((d.get('platforms') or {}).get('${platform}', {}).get('url') or '')95" 2>/dev/null)"96    cksum="$(printf '%s' "$meta" | python3 -c "97import sys, json98d = json.load(sys.stdin)99p = (d.get('platforms') or {}).get('${platform}', {})100raw = p.get('blake3') or p.get('checksum') or ''101print(raw[len('blake3:'):] if raw.startswith('blake3:') else raw)102" 2>/dev/null)"103  elif have jq; then104    version="$(printf '%s' "$meta" | jq -r '.version // empty')"105    dl_url="$(printf '%s' "$meta" | jq -r ".platforms.\"${platform}\".url // empty")"106    cksum_raw="$(printf '%s' "$meta" | jq -r ".platforms.\"${platform}\".blake3 // .platforms.\"${platform}\".checksum // empty")"107    cksum="${cksum_raw#blake3:}"108  else109    version="$(printf '%s' "$meta" | grep -o '"version":"[^"]*"' | sed 's/"version":"//;s/"//')"110    plat_block="$(printf '%s' "$meta" | grep -o "\"${platform}\":{[^}]*}" | head -1)"111    dl_url="$(printf '%s' "$plat_block" | grep -o '"url":"[^"]*"' | sed 's/"url":"//;s/"//')"112    cksum=""113  fi114115  [ -n "$dl_url" ] || return 1116  [ -n "$version" ] || version="latest"117118  say "downloading nina ${version} for ${platform}..."119  tmpfile="$(mktemp /tmp/nina.XXXXXX)"120  fetch_file "$dl_url" "$tmpfile" || { rm -f "$tmpfile"; return 1; }121122  if [ -n "$cksum" ] && have b3sum; then123    actual="$(b3sum --no-names "$tmpfile" 2>/dev/null | awk '{print $1}')"124    if [ "$actual" != "$cksum" ]; then125      say "checksum mismatch — aborting"126      rm -f "$tmpfile"127      return 1128    fi129    say "checksum verified"130  fi131132  chmod +x "$tmpfile"133  mkdir -p "$INSTALL_DIR"134  mv "$tmpfile" "${INSTALL_DIR}/nina"135  say "installed nina ${version} → ${INSTALL_DIR}/nina"136  return 0137}138139# ── NixOS: nix profile install (primary) ─────────────────────────────────────140141nixos_profile_install() {142  have nix || return 1143  say "installing nina to ~/.nix-profile/bin/nina via nix profile..."144  blank145  # Tarball flakes are fetched read-only into the store, so nix cannot persist146  # a freshly-resolved flake.lock and aborts with "cannot write modified lock147  # file". --no-write-lock-file resolves inputs in memory without writing.148  if nix profile install --no-write-lock-file "${FLAKE_URL}#nina"; then149    return 0150  fi151  blank152  say "nix profile install failed. Common causes:"153  say "  flakes not enabled — add to /etc/nix/nix.conf or ~/.config/nix/nix.conf:"154  say "    experimental-features = nix-command flakes"155  say "  once nina is installed: nina setup enables this automatically"156  blank157  return 1158}159160# ── NixOS: nix-shell source build (fallback) ──────────────────────────────────161162nixos_shell_build() {163  have nix-shell || return 1164  say "fetching source and building nina using nix-shell..."165  blank166  srcdir="$(fetch_source)"167168  # Try the user's channel first; fall back to a pinned nixpkgs-unstable if169  # zig_0_16 is not yet in the channel (common on NixOS 24.11 stable).170  build_cmd="cd '${srcdir}' && zig build -Doptimize=ReleaseSafe"171  if nix-shell -p zig_0_16 --run "$build_cmd" 2>/dev/null; then172    :173  else174    say "zig_0_16 not in channel — fetching from nixpkgs-unstable..."175    # channels.nixos.org redirects to the current unstable release tarball;176    # the older releases.nixos.org/.../nixos-unstable/ path 404s.177    nix-shell \178      -I nixpkgs=https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz \179      -p zig_0_16 \180      --run "$build_cmd" \181      || die "build failed — see output above"182  fi183184  mkdir -p "$INSTALL_DIR"185  cp "${srcdir}/zig-out/bin/nina" "${INSTALL_DIR}/nina"186  chmod +x "${INSTALL_DIR}/nina"187  blank188  say "installed from source → ${INSTALL_DIR}/nina"189  say "note: binary built outside nix — adopt ~/nina.nix into configuration.nix"190  say "      to make nina fully nix-managed (nina goodbye handles removal either way)"191}192193# ── generate ~/nina.nix ───────────────────────────────────────────────────────194195# Offer to wire ~/nina.nix into /etc/nixos/configuration.nix. Returns 0 when the196# import is in place (added now, or already present), 1 when the caller should197# fall back to printing manual instructions. Conservative by design: only edits198# an existing `imports = [ … ]` list, always backs the file up first, and never199# touches anything without an explicit yes from the terminal.200offer_config_import() {201  config="$1"202  nina_nix="$2"203204  [ -f "$config" ] && [ -r "$config" ] || return 1205206  # Already wired up — just remind how to apply it.207  if grep -qF "$nina_nix" "$config" 2>/dev/null; then208    out "Already imported in ${config}."209    out "Apply with:  sudo nixos-rebuild switch"210    blank211    return 0212  fi213214  # Need an existing imports list to extend; guessing where else to put it215  # risks producing a config that won't evaluate.216  grep -qE '^[[:space:]]*imports[[:space:]]*=[[:space:]]*\[' "$config" || return 1217218  # Read the answer from the terminal so it works under `curl … | sh`.219  [ -r /dev/tty ] || return 1220  printf '  add this import to %s now? [y/N] ' "$config" > /dev/tty221  IFS= read -r ans < /dev/tty 2>/dev/null || ans=""222  case "$ans" in223    [Yy]*) ;;224    *) return 1 ;;225  esac226227  ts="$(date +%Y%m%d%H%M%S 2>/dev/null || echo backup)"228  backup="${config}.nina-bak.${ts}"229  sudo cp "$config" "$backup" 2>/dev/null \230    || { blank; out "could not back up ${config} — add it manually instead."; blank; return 1; }231232  # Insert our path as the first entry of the first imports list.233  tmp="$(mktemp /tmp/nina-conf.XXXXXX)"234  awk -v p="$nina_nix" '235    !done && /imports[[:space:]]*=[[:space:]]*\[/ { sub(/\[/, "[ " p); done=1 }236    { print }237  ' "$config" > "$tmp" 2>/dev/null238239  if ! sudo cp "$tmp" "$config" 2>/dev/null; then240    rm -f "$tmp"241    blank242    out "could not write ${config}."243    out "restore the backup with:  sudo cp ${backup} ${config}"244    blank245    return 1246  fi247  rm -f "$tmp"248249  blank250  out "✓ added the import to ${config}"251  out "  backup kept at  ${backup}"252  out "  apply with      sudo nixos-rebuild switch"253  blank254  return 0255}256257generate_nixos_module() {258  # Write ~/nina.nix without invoking shell substitution on the nix code inside.259  cat > "$HOME/nina.nix" <<'NIXEOF'260# ~/nina.nix — generated by the nina installer261#262# Declares nina as a NixOS system package.263#264# Add to /etc/nixos/configuration.nix:265#   imports = [ /home/YOUR_USER/nina.nix ];266# Then: sudo nixos-rebuild switch267#268# To update:  nina update  (or re-run the installer)269# To remove:  nina goodbye  (or delete this file, then nixos-rebuild switch)270#271# For flake-based configurations, add nina as a flake input instead:272#   inputs.nina.url = "https://kepr.uk/nina/archive/HEAD.tar.gz";273#   # in your nixosModules list: nina.nixosModules.default274#   # then set: programs.nina.enable = true;275{ ... }:276{277  imports = [278    "${builtins.fetchTarball "https://kepr.uk/nina/archive/HEAD.tar.gz"}/nixos-module.nix"279  ];280  programs.nina.enable = true;281}282NIXEOF283284  nina_nix="$HOME/nina.nix"285286  blank287  hr288  out "system-wide install  (optional)"289  hr290  blank291  out "nina is installed for your user. To manage her with the rest of"292  out "your system, a NixOS module has been written to:"293  blank294  out "    ${nina_nix}"295  blank296297  if offer_config_import "/etc/nixos/configuration.nix" "$nina_nix"; then298    return 0299  fi300301  out "To enable it yourself, add this line to /etc/nixos/configuration.nix"302  out "(inside the existing  imports = [ … ];  list):"303  blank304  out "    ${nina_nix}"305  blank306  out "then run:  sudo nixos-rebuild switch"307  blank308}309310# ── Zig version check ─────────────────────────────────────────────────────────311312check_zig() {313  if ! have zig; then314    blank315    say "Zig not found. Building nina requires Zig 0.${MIN_ZIG_MINOR}+."316    blank317    say "  Download: https://ziglang.org/download/"318    if is_nixos; then319      say "  NixOS:    nix-shell -p zig_0_16 --run 'sh install.sh'"320    fi321    blank322    die "Zig 0.${MIN_ZIG_MINOR}+ required"323  fi324325  zig_ver="$(zig version 2>/dev/null)"326  major="$(printf '%s' "$zig_ver" | cut -d. -f1)"327  minor="$(printf '%s' "$zig_ver" | cut -d. -f2)"328  ok=0329  [ "$major" -ge 1 ] 2>/dev/null && ok=1330  [ "$major" -eq 0 ] 2>/dev/null && [ "$minor" -ge "$MIN_ZIG_MINOR" ] 2>/dev/null && ok=1331332  if [ "$ok" -ne 1 ]; then333    blank334    say "found zig ${zig_ver}, but nina requires Zig 0.${MIN_ZIG_MINOR}+."335    if is_nixos; then336      say "  run: nix-shell -p zig_0_16 --run 'sh install.sh'"337    else338      say "  update: https://ziglang.org/download/"339    fi340    blank341    die "Zig 0.${MIN_ZIG_MINOR}+ required; found ${zig_ver}"342  fi343344  say "zig ${zig_ver} found"345}346347# ── generic source build (non-NixOS) ─────────────────────────────────────────348349build_from_source() {350  say "building from source..."351  check_zig352353  if [ -f "build.zig" ] && [ -f "src/main.zig" ]; then354    srcdir="."355  else356    srcdir="$(fetch_source)"357  fi358359  (cd "$srcdir" && zig build -Doptimize=ReleaseSafe) \360    || die "build failed — see output above"361362  mkdir -p "$INSTALL_DIR"363  cp "${srcdir}/zig-out/bin/nina" "${INSTALL_DIR}/nina"364  chmod +x "${INSTALL_DIR}/nina"365  say "installed from source → ${INSTALL_DIR}/nina"366}367368# ── PATH reminder ─────────────────────────────────────────────────────────────369370check_path() {371  case ":${PATH}:" in372    *":${INSTALL_DIR}:"*) ;;373    *)374      blank375      say "${INSTALL_DIR} is not in your PATH. Add it:"376      say "  export PATH=\"\${PATH}:${INSTALL_DIR}\""377      say "  echo 'export PATH=\"\${PATH}:${INSTALL_DIR}\"' >> ~/.bashrc"378      if is_nixos; then379        say "  NixOS: add to environment.sessionVariables in configuration.nix"380      fi381      ;;382  esac383}384385# ── enable nix experimental features ──────────────────────────────────────────386# nina needs nix-command + flakes to search and install. If they're off, offer to387# turn them on via the freshly-installed `nina setup`. Read from /dev/tty so the388# prompt works even under `curl … | sh`, where stdin is the script itself.389390maybe_setup_features() {391  have nix || return 0392  # `nix eval` itself requires nix-command, so this doubles as the feature probe.393  nix eval --expr '1' >/dev/null 2>&1 && return 0394395  NINA="${INSTALL_DIR}/nina"396  [ -x "$NINA" ] || NINA="nina"397398  blank399  say "nina needs nix's experimental features (nix-command and flakes) to"400  say "search and install packages."401402  if [ -r /dev/tty ]; then403    printf 'nina: enable them now? [Y/n] ' > /dev/tty404    IFS= read -r ans < /dev/tty 2>/dev/null || ans="y"405    case "$ans" in406      [Nn]*) say "skipped — enable later with:  nina setup"; return 0 ;;407    esac408    "$NINA" setup || say "automatic setup failed — run:  nina setup"409  else410    say "run  nina setup  to enable them"411  fi412}413414# ── closing message ──────────────────────────────────────────────────────────415416farewell() {417  printf '\n'418  printf '  thank you!! running nina update occasionally will keep me in tip top shape.\n'419  printf '  get started by running nina hello\n'420  printf '\n'421  printf '  if nina turns out not to be for you, nina goodbye will remove her cleanly.\n'422  printf '\n'423}424425# ── main ──────────────────────────────────────────────────────────────────────426427banner428429if is_nixos; then430  say "NixOS detected"431  if nixos_profile_install; then432    generate_nixos_module433    maybe_setup_features434    farewell435    exit 0436  fi437  # nina's binaries link no libc, so the Linux builds are fully static and run438  # on NixOS — try the prebuilt before falling back to a from-source build.439  say "trying prebuilt binary from Kepr..."440  if install_binary; then441    check_path442    maybe_setup_features443    farewell444    exit 0445  fi446  say "no prebuilt binary available — building from source"447  nixos_shell_build448  generate_nixos_module449  check_path450  maybe_setup_features451  farewell452  exit 0453fi454455# macOS / generic Linux: download binary, fall back to source build456if install_binary; then457  check_path458  maybe_setup_features459  farewell460  exit 0461fi462463say "no pre-built binary for ${platform} — falling back to source build"464build_from_source465check_path466maybe_setup_features467farewell