#!/usr/bin/env sh # nina install script # Run: curl -fsSL kepr.uk/nina/install.sh | sh # Or: NINA_INSTALL_DIR=/usr/local/bin sh install.sh set -e FLAKE_URL="https://kepr.uk/nina/archive/HEAD.tar.gz" RELEASES_URL="https://kepr.uk/api/nina/releases/latest" INSTALL_DIR="${NINA_INSTALL_DIR:-$HOME/.local/bin}" MIN_ZIG_MINOR=16 # Color, but only when stdout is a real terminal and NO_COLOR is unset. Under # `curl … | sh` stdin is the pipe yet stdout is still the tty, so -t 1 holds. # Palette mirrors nina's own: pink prefix, cyan for paths/commands. if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then PINK="$(printf '\033[38;2;255;183;197m')" CYAN="$(printf '\033[36m')" GREEN="$(printf '\033[32m')" RED="$(printf '\033[31m')" DIM="$(printf '\033[2m')" BOLD="$(printf '\033[1m')" RESET="$(printf '\033[0m')" else PINK=""; CYAN=""; GREEN=""; RED=""; DIM=""; BOLD=""; RESET="" fi os="$(uname -s)" arch="$(uname -m)" case "${os}-${arch}" in Linux-x86_64) platform="linux-x86_64" ;; Linux-aarch64|Linux-arm64) platform="linux-aarch64" ;; Darwin-arm64) platform="macos-aarch64" ;; Darwin-x86_64) platform="macos-x86_64" ;; *) printf 'nina: unsupported platform %s-%s\n' "$os" "$arch" >&2 exit 1 ;; esac have() { command -v "$1" >/dev/null 2>&1; } say() { printf '%snina:%s %s\n' "$PINK" "$RESET" "$1"; } die() { printf '%snina:%s %serror:%s %s\n' "$PINK" "$RESET" "$RED" "$RESET" "$1" >&2; exit 1; } blank() { printf '\n'; } is_nixos() { [ -e /etc/NIXOS ]; } # Unprefixed output for the closing sections, where the "nina:" stutter on # every line hurts readability. `out` indents body text; `hr` draws a divider. out() { printf ' %s\n' "$1"; } hr() { printf ' %s────────────────────────────────────────────────%s\n' "$DIM" "$RESET"; } fetch_text() { if have curl; then curl -fsSL "$1" elif have wget; then wget -qO- "$1" else die "curl or wget required"; fi } fetch_file() { if have curl; then curl -fsSL "$1" -o "$2" elif have wget; then wget -qO "$2" "$1" else die "curl or wget required"; fi } # ── banner ──────────────────────────────────────────────────────────────────── banner() { cat <<'BANNER' _ __ _ _ __ __ _ | '_ \ (_)| '_ \ / _` | | | | || || | | || (_| | |_| |_||_||_| |_| \__,_| NixOS Intuitive Navigation Assistant · kepr.uk/nina BANNER } # ── fetch source archive ────────────────────────────────────────────────────── fetch_source() { # This function's stdout IS the source path (captured via command # substitution), so every diagnostic must go to stderr — otherwise the # caller's `cd "$srcdir"` receives the progress text too and fails. srcdir="$(mktemp -d /tmp/nina-src.XXXXXX)" say "fetching source from Kepr..." >&2 fetch_file "$FLAKE_URL" "${srcdir}/nina.tar.gz" \ || die "could not download source archive — check your connection" tar -xzf "${srcdir}/nina.tar.gz" -C "$srcdir" --strip-components=1 \ || die "could not extract source archive" printf '%s' "$srcdir" } # ── binary install from Kepr ───────────────────────────────────────────────── install_binary() { say "fetching release metadata..." meta="$(fetch_text "$RELEASES_URL" 2>/dev/null)" || return 1 [ -n "$meta" ] || return 1 if have python3; then version="$(printf '%s' "$meta" | python3 -c " import sys, json d = json.load(sys.stdin) print(d.get('version') or '') " 2>/dev/null)" dl_url="$(printf '%s' "$meta" | python3 -c " import sys, json d = json.load(sys.stdin) print((d.get('platforms') or {}).get('${platform}', {}).get('url') or '') " 2>/dev/null)" cksum="$(printf '%s' "$meta" | python3 -c " import sys, json d = json.load(sys.stdin) p = (d.get('platforms') or {}).get('${platform}', {}) raw = p.get('blake3') or p.get('checksum') or '' print(raw[len('blake3:'):] if raw.startswith('blake3:') else raw) " 2>/dev/null)" elif have jq; then version="$(printf '%s' "$meta" | jq -r '.version // empty')" dl_url="$(printf '%s' "$meta" | jq -r ".platforms.\"${platform}\".url // empty")" cksum_raw="$(printf '%s' "$meta" | jq -r ".platforms.\"${platform}\".blake3 // .platforms.\"${platform}\".checksum // empty")" cksum="${cksum_raw#blake3:}" else version="$(printf '%s' "$meta" | grep -o '"version":"[^"]*"' | sed 's/"version":"//;s/"//')" plat_block="$(printf '%s' "$meta" | grep -o "\"${platform}\":{[^}]*}" | head -1)" dl_url="$(printf '%s' "$plat_block" | grep -o '"url":"[^"]*"' | sed 's/"url":"//;s/"//')" cksum="" fi [ -n "$dl_url" ] || return 1 [ -n "$version" ] || version="latest" say "downloading nina ${version} for ${platform}..." tmpfile="$(mktemp /tmp/nina.XXXXXX)" fetch_file "$dl_url" "$tmpfile" || { rm -f "$tmpfile"; return 1; } if [ -n "$cksum" ] && have b3sum; then actual="$(b3sum --no-names "$tmpfile" 2>/dev/null | awk '{print $1}')" if [ "$actual" != "$cksum" ]; then say "checksum mismatch — aborting" rm -f "$tmpfile" return 1 fi say "checksum verified" fi chmod +x "$tmpfile" mkdir -p "$INSTALL_DIR" mv "$tmpfile" "${INSTALL_DIR}/nina" say "installed nina ${version} → ${INSTALL_DIR}/nina" return 0 } # ── NixOS: nix profile add (primary) ───────────────────────────────────────── nixos_profile_install() { have nix || return 1 say "installing nina to ~/.nix-profile/bin/nina via nix profile..." blank # Tarball flakes are fetched read-only into the store, so nix cannot persist # a freshly-resolved flake.lock and aborts with "cannot write modified lock # file". --no-write-lock-file resolves inputs in memory without writing. # `add` is the current verb; `install` is a deprecated alias that prints a # warning on modern nix. Mirrors exec.zig profileInstall. if nix profile add --no-write-lock-file "${FLAKE_URL}#nina"; then return 0 fi blank say "nix profile add failed. Common causes:" say " flakes not enabled — add to /etc/nix/nix.conf or ~/.config/nix/nix.conf:" say " experimental-features = nix-command flakes" say " once nina is installed: nina setup enables this automatically" blank return 1 } # ── NixOS: nix-shell source build (fallback) ────────────────────────────────── nixos_shell_build() { have nix-shell || return 1 say "fetching source and building nina using nix-shell..." blank srcdir="$(fetch_source)" # Try the user's channel first; fall back to a pinned nixpkgs-unstable if # zig_0_16 is not yet in the channel (common on NixOS 24.11 stable). build_cmd="cd '${srcdir}' && zig build -Doptimize=ReleaseSafe" if nix-shell -p zig_0_16 --run "$build_cmd" 2>/dev/null; then : else say "zig_0_16 not in channel — fetching from nixpkgs-unstable..." # channels.nixos.org redirects to the current unstable release tarball; # the older releases.nixos.org/.../nixos-unstable/ path 404s. nix-shell \ -I nixpkgs=https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz \ -p zig_0_16 \ --run "$build_cmd" \ || die "build failed — see output above" fi mkdir -p "$INSTALL_DIR" cp "${srcdir}/zig-out/bin/nina" "${INSTALL_DIR}/nina" chmod +x "${INSTALL_DIR}/nina" blank say "installed from source → ${INSTALL_DIR}/nina" say "note: binary built outside nix — adopt ~/nina.nix into configuration.nix" say " to make nina fully nix-managed (nina goodbye handles removal either way)" } # ── generate ~/nina.nix ─────────────────────────────────────────────────────── # Offer to wire ~/nina.nix into /etc/nixos/configuration.nix. Returns 0 when the # import is in place (added now, or already present), 1 when the caller should # fall back to printing manual instructions. Conservative by design: only edits # an existing `imports = [ … ]` list, always backs the file up first, and never # touches anything without an explicit yes from the terminal. offer_config_import() { config="$1" nina_nix="$2" [ -f "$config" ] && [ -r "$config" ] || return 1 # Already wired up — just remind how to apply it. if grep -qF "$nina_nix" "$config" 2>/dev/null; then out "already imported in ${CYAN}${config}${RESET}" out "apply with ${BOLD}sudo nixos-rebuild switch${RESET}" blank return 0 fi # Need an existing imports list to extend; guessing where else to put it # risks producing a config that won't evaluate. grep -qE '^[[:space:]]*imports[[:space:]]*=[[:space:]]*\[' "$config" || return 1 # Read the answer from the terminal so it works under `curl … | sh`. [ -r /dev/tty ] || return 1 printf ' add it to %s%s%s now? %s[y/N]%s ' "$CYAN" "$config" "$RESET" "$DIM" "$RESET" > /dev/tty IFS= read -r ans < /dev/tty 2>/dev/null || ans="" case "$ans" in [Yy]*) ;; *) return 1 ;; esac ts="$(date +%Y%m%d%H%M%S 2>/dev/null || echo backup)" backup="${config}.nina-bak.${ts}" sudo cp "$config" "$backup" 2>/dev/null \ || { blank; out "could not back up ${config} — add it manually instead."; blank; return 1; } # Insert our path as the first entry of the first imports list. tmp="$(mktemp /tmp/nina-conf.XXXXXX)" awk -v p="$nina_nix" ' !done && /imports[[:space:]]*=[[:space:]]*\[/ { sub(/\[/, "[ " p); done=1 } { print } ' "$config" > "$tmp" 2>/dev/null if ! sudo cp "$tmp" "$config" 2>/dev/null; then rm -f "$tmp" blank out "could not write ${config}." out "restore the backup with: sudo cp ${backup} ${config}" blank return 1 fi rm -f "$tmp" blank out "${GREEN}✓${RESET} added the import to ${CYAN}${config}${RESET}" out " ${DIM}backup kept at ${backup}${RESET}" out " apply with ${BOLD}sudo nixos-rebuild switch${RESET}" blank return 0 } generate_nixos_module() { # Write ~/nina.nix without invoking shell substitution on the nix code inside. cat > "$HOME/nina.nix" <<'NIXEOF' # ~/nina.nix — generated by the nina installer # # Declares nina as a NixOS system package. # # Add to /etc/nixos/configuration.nix: # imports = [ /home/YOUR_USER/nina.nix ]; # Then: sudo nixos-rebuild switch # # To update: nina update (or re-run the installer) # To remove: nina goodbye (or delete this file, then nixos-rebuild switch) # # For flake-based configurations, add nina as a flake input instead: # inputs.nina.url = "https://kepr.uk/nina/archive/HEAD.tar.gz"; # # in your nixosModules list: nina.nixosModules.default # # then set: programs.nina.enable = true; { ... }: { imports = [ "${builtins.fetchTarball "https://kepr.uk/nina/archive/HEAD.tar.gz"}/nixos-module.nix" ]; programs.nina.enable = true; } NIXEOF nina_nix="$HOME/nina.nix" blank hr out "${BOLD}system-wide install${RESET} ${DIM}(optional)${RESET}" hr blank out "nina is installed for your user. To manage her alongside the" out "rest of your system, a module was written to:" blank out " ${CYAN}${nina_nix}${RESET}" blank if offer_config_import "/etc/nixos/configuration.nix" "$nina_nix"; then return 0 fi blank out "to do it yourself, add this to the ${CYAN}imports${RESET} list in" out "${CYAN}/etc/nixos/configuration.nix${RESET}:" blank out " ${CYAN}${nina_nix}${RESET}" blank out "then run ${BOLD}sudo nixos-rebuild switch${RESET}" blank } # ── Zig version check ───────────────────────────────────────────────────────── check_zig() { if ! have zig; then blank say "Zig not found. Building nina requires Zig 0.${MIN_ZIG_MINOR}+." blank say " Download: https://ziglang.org/download/" if is_nixos; then say " NixOS: nix-shell -p zig_0_16 --run 'sh install.sh'" fi blank die "Zig 0.${MIN_ZIG_MINOR}+ required" fi zig_ver="$(zig version 2>/dev/null)" major="$(printf '%s' "$zig_ver" | cut -d. -f1)" minor="$(printf '%s' "$zig_ver" | cut -d. -f2)" ok=0 [ "$major" -ge 1 ] 2>/dev/null && ok=1 [ "$major" -eq 0 ] 2>/dev/null && [ "$minor" -ge "$MIN_ZIG_MINOR" ] 2>/dev/null && ok=1 if [ "$ok" -ne 1 ]; then blank say "found zig ${zig_ver}, but nina requires Zig 0.${MIN_ZIG_MINOR}+." if is_nixos; then say " run: nix-shell -p zig_0_16 --run 'sh install.sh'" else say " update: https://ziglang.org/download/" fi blank die "Zig 0.${MIN_ZIG_MINOR}+ required; found ${zig_ver}" fi say "zig ${zig_ver} found" } # ── generic source build (non-NixOS) ───────────────────────────────────────── build_from_source() { say "building from source..." check_zig if [ -f "build.zig" ] && [ -f "src/main.zig" ]; then srcdir="." else srcdir="$(fetch_source)" fi (cd "$srcdir" && zig build -Doptimize=ReleaseSafe) \ || die "build failed — see output above" mkdir -p "$INSTALL_DIR" cp "${srcdir}/zig-out/bin/nina" "${INSTALL_DIR}/nina" chmod +x "${INSTALL_DIR}/nina" say "installed from source → ${INSTALL_DIR}/nina" } # ── PATH reminder ───────────────────────────────────────────────────────────── check_path() { case ":${PATH}:" in *":${INSTALL_DIR}:"*) ;; *) blank say "${INSTALL_DIR} is not in your PATH. Add it:" say " export PATH=\"\${PATH}:${INSTALL_DIR}\"" say " echo 'export PATH=\"\${PATH}:${INSTALL_DIR}\"' >> ~/.bashrc" if is_nixos; then say " NixOS: add to environment.sessionVariables in configuration.nix" fi ;; esac } # ── enable nix experimental features ────────────────────────────────────────── # nina needs nix-command + flakes to search and install. If they're off, offer to # turn them on via the freshly-installed `nina setup`. Read from /dev/tty so the # prompt works even under `curl … | sh`, where stdin is the script itself. maybe_setup_features() { have nix || return 0 # `nix eval` itself requires nix-command, so this doubles as the feature probe. nix eval --expr '1' >/dev/null 2>&1 && return 0 NINA="${INSTALL_DIR}/nina" [ -x "$NINA" ] || NINA="nina" blank say "nina needs nix's experimental features (nix-command and flakes) to" say "search and install packages." if [ -r /dev/tty ]; then printf 'nina: enable them now? [Y/n] ' > /dev/tty IFS= read -r ans < /dev/tty 2>/dev/null || ans="y" case "$ans" in [Nn]*) say "skipped — enable later with: nina setup"; return 0 ;; esac "$NINA" setup || say "automatic setup failed — run: nina setup" else say "run nina setup to enable them" fi } # ── closing message ────────────────────────────────────────────────────────── farewell() { blank printf ' %sthank you!!%s get started by running %snina hello%s\n' "$PINK" "$RESET" "$CYAN" "$RESET" printf ' run %snina update%s occasionally to keep me in tip-top shape\n' "$CYAN" "$RESET" blank printf ' %snot for you?%s %snina goodbye%s %sremoves her cleanly%s\n' "$DIM" "$RESET" "$CYAN" "$RESET" "$DIM" "$RESET" blank } # ── main ────────────────────────────────────────────────────────────────────── banner if is_nixos; then say "NixOS detected" if nixos_profile_install; then generate_nixos_module maybe_setup_features farewell exit 0 fi # nina's binaries link no libc, so the Linux builds are fully static and run # on NixOS — try the prebuilt before falling back to a from-source build. say "trying prebuilt binary from Kepr..." if install_binary; then check_path maybe_setup_features farewell exit 0 fi say "no prebuilt binary available — building from source" nixos_shell_build generate_nixos_module check_path maybe_setup_features farewell exit 0 fi # macOS / generic Linux: download binary, fall back to source build if install_binary; then check_path maybe_setup_features farewell exit 0 fi say "no pre-built binary for ${platform} — falling back to source build" build_from_source check_path maybe_setup_features farewell