#!/bin/sh
#
# Flox installer curl-pipe-to-shell bootstrap.
#
#   curl -fsSL https://get.flox.dev | sh
#
# Detects your OS and CPU architecture and installs Flox with the best method
# available on the machine:
#
#   * macOS                 -> .pkg installed with `installer`
#   * Debian / Ubuntu       -> .deb installed with `apt`
#   * Fedora / RHEL / etc.  -> .rpm installed with `rpm`
#
# On macOS, a Flox that was installed with the .pkg is upgraded in place by
# re-running the installer -- the same thing that happens when you download
# the .pkg from flox.dev and run it again by hand. Installations owned by
# something else keep their owner: Homebrew and Nix-profile installs are
# pointed at their own upgrade commands, and Linux packages are upgraded
# through the system package manager. If a foreign Nix is installed we stop
# and point you at the Nix instructions in the docs. This script never
# touches a Nix installation it did not create.
#
# Override the release channel or version:
#   FLOX_CHANNEL=stable   (default)
#   FLOX_VERSION=1.13.0   (default: whatever the channel currently publishes)
#   FLOX_FORCE_INSTALL=1  proceed even when the installed Flox already
#                         matches the target version (reinstall/repair)
#
# This script reports two events -- one when it starts, one when it
# finishes -- so we can see how often installs are attempted, which path
# they take, and why they fail. On a machine that already has Flox they
# carry the device id the CLI itself reports, so an upgrade can be tied to
# the installation it upgraded; a machine with no Flox on it has no such id
# and none is invented for it. Nothing else identifying is collected.
#   FLOX_DISABLE_METRICS=1    send nothing
#   FLOX_TELEMETRY_DRY_RUN=1  print each curl invocation instead of sending

set -eu

# --------------------------------------------------------------------------
# Configuration
# --------------------------------------------------------------------------
FLOX_CHANNEL="${FLOX_CHANNEL:-stable}"
BASE_URL="https://downloads.flox.dev/by-env/${FLOX_CHANNEL}"
DOCS_URL="https://flox.dev/docs/install-flox/install"
NIX_DOCS_URL="${DOCS_URL}#nix"

# --------------------------------------------------------------------------
# Pretty output
# --------------------------------------------------------------------------
if [ -t 2 ]; then
  BOLD="$(printf '\033[1m')"; RED="$(printf '\033[31m')"
  GREEN="$(printf '\033[32m')"; YELLOW="$(printf '\033[33m')"
  RESET="$(printf '\033[0m')"
else
  BOLD=""; RED=""; GREEN=""; YELLOW=""; RESET=""
fi

info()  { printf '%s==>%s %s\n' "$GREEN" "$RESET" "$*" >&2; }
warn()  { printf '%swarning:%s %s\n' "$YELLOW" "$RESET" "$*" >&2; }
error() { printf '%serror:%s %s\n' "$RED" "$RESET" "$*" >&2; }
die()   { error "$*"; exit 1; }

# --------------------------------------------------------------------------
# Helpers
# --------------------------------------------------------------------------
have() { command -v "$1" >/dev/null 2>&1; }

# A privilege-escalation prefix. Empty when we are already root.
SUDO=""
need_sudo() {
  if [ "$(id -u)" -ne 0 ]; then
    have sudo || die "this installer needs root privileges but 'sudo' was not found; re-run as root"
    SUDO="sudo"
  fi
}

# Show exactly which commands are about to run with elevated privileges,
# so a curl|sh user knows what they are agreeing to. No-op when root.
sudo_preview() {
  [ -n "$SUDO" ] || return 0
  info "The following commands will be run with ${BOLD}sudo${RESET}:"
  for _cmd in "$@"; do
    printf '    %ssudo %s%s\n' "$BOLD" "$_cmd" "$RESET" >&2
  done
}

# Take the credential before handing anything to the package manager. A
# cancelled prompt, a mistyped password, or an account that is not an
# administrator all make sudo exit non-zero, and without this they would
# reach the same `|| report_pkg_failure` as a genuinely broken apt and be
# reported as one -- a large share of "failed installs" that are nothing of
# the kind. No-op when already root.
#
# The probe runs a real command before falling back to `sudo -v`. Running a
# command is decided by the last sudoers rule that matches, but `-v` follows
# verifypw's default of `all` and prompts unless every matching rule carries
# NOPASSWD. Ubuntu cloud images hit exactly that split: the default user is
# in %sudo (no NOPASSWD) and also gets cloud-init's `ubuntu ALL=(ALL)
# NOPASSWD:ALL`, so apt-get needs no password while `-v` asks for one --
# and the account is locked, so every answer is "wrong". `-n true` succeeds
# silently wherever the package manager would; only when it fails does `-v`
# take over, so a user who genuinely has to type a password sees the same
# prompt as before and the failure attribution above is unchanged.
authenticate_sudo() {
  [ -n "$SUDO" ] || return 0
  FAILURE_REASON="privilege_escalation_failed"
  if ! $SUDO -n true 2>/dev/null; then
    $SUDO -v || die "could not obtain administrator privileges"
  fi
  FAILURE_REASON="package_manager_error"
}

# Download $1 to $2 using whatever fetcher is available.
download() {
  url="$1"; dest="$2"
  if have curl; then
    curl -fsSL --proto '=https' --tlsv1.2 -o "$dest" "$url"
  elif have wget; then
    wget -qO "$dest" "$url"
  else
    die "neither 'curl' nor 'wget' is available to download $url"
  fi
}

# Read a small remote file to stdout.
fetch() {
  url="$1"
  if have curl; then
    curl -fsSL --proto '=https' --tlsv1.2 "$url"
  elif have wget; then
    wget -qO- "$url"
  else
    die "neither 'curl' nor 'wget' is available to fetch $url"
  fi
}

# --------------------------------------------------------------------------
# Detecting what is already on this machine
#
# Both predicates feed the telemetry below, which reports before any of the
# precondition checks run, so they are defined up here rather than beside
# the checks that consume them.
# --------------------------------------------------------------------------
flox_present() {
  have flox && return 0
  [ -e /usr/bin/flox ] && return 0
  [ -e /usr/local/bin/flox ] && return 0
  return 1
}

# A usable Nix installation means `nix` on PATH or a populated store on
# disk. Leftover debris alone doesn't count: the Flox/Nix uninstallers can
# leave a stale /etc/nix/nix.conf behind (it is created by install hooks,
# not the pkg payload, so `pkgutil --only-files` never removes it), and
# macOS can keep an empty /nix mount point (or an empty /nix/store) via
# synthetic.conf after the store volume is gone.
nix_present() {
  have nix && return 0
  [ -d /nix/store ] && [ -n "$(ls -A /nix/store 2>/dev/null)" ] && return 0
  return 1
}

# --------------------------------------------------------------------------
# Normalize OS and architecture
#
# Normalized here, ahead of everything else, because the telemetry below
# reports them before the precondition checks run. Unrecognized values
# normalize to empty; the fatal checks that reject them stay further down,
# where they have always been, so a machine that would have exited "Flox is
# already installed" still does rather than newly failing on its CPU.
# --------------------------------------------------------------------------
UNAME_S="$(uname -s)"
UNAME_M="$(uname -m)"

case "$UNAME_M" in
  x86_64 | amd64)   ARCH="x86_64" ;;
  aarch64 | arm64)  ARCH="aarch64" ;;
  *)                ARCH="" ;;
esac

case "$UNAME_S" in
  Darwin) OS="darwin" ;;
  Linux)  OS="linux" ;;
  *)      OS="" ;;
esac

# --------------------------------------------------------------------------
# Telemetry
#
# Two fire-and-forget events -- installer.started, before any install logic
# runs, and installer.completed, from the EXIT trap -- tell us how often
# this script is reached, which path it took, and why it failed. Every run
# reports, including the two that refuse to install anything, because "how
# often do we turn people away, and for which reason" is most of the
# question this exists to answer.
#
# Best-effort by construction: the POST is detached into a background
# subshell with a hard --max-time and its output discarded, so a slow or
# broken endpoint cannot change this script's exit code, delay the install,
# or print anything at the user.
#
#   FLOX_DISABLE_METRICS=1    send nothing at all
#   FLOX_TELEMETRY_DRY_RUN=1  print each curl invocation, post nothing
# --------------------------------------------------------------------------
TELEMETRY_URL="https://telemetry.flox.dev/installer/events"

# A public write-only routing token, not a secret. This script is served as
# world-readable plain text, so anything embedded in it is published by
# definition; the route accepts nothing but installer.* events and the key
# carries no read access. Rotation is the remediation path if it is abused,
# and because `curl | sh` refetches this file on every run, a rotation takes
# effect immediately. AWS generates the value; read it back from the
# flox-analytics ingest unit with `terraform output -raw installer_api_key`.
TELEMETRY_API_KEY="9Bxuu1Lqx17qlkeeKzt325McpoCJj3zf71bFpaNO"

# Accept the spellings a person actually types into an environment variable.
truthy() {
  case "${1:-}" in
    1 | y | Y | yes | YES | Yes | true | TRUE | True) return 0 ;;
    *) return 1 ;;
  esac
}

# Opt-out uses the same variable the Flox CLI honors, so one setting covers
# both.
telemetry_on() {
  truthy "${FLOX_DISABLE_METRICS:-}" && return 1
  return 0
}

telemetry_timestamp() { date -u '+%Y-%m-%dT%H:%M:%SZ'; }

# Print "true" or "false" for the predicate named in "$@".
telemetry_bool() {
  if "$@"; then printf 'true'; else printf 'false'; fi
}

# Payload values come from uname and from FLOX_CHANNEL / FLOX_VERSION, which
# the caller controls, so nothing reaches the body unfiltered. Reducing to
# the character class these fields legitimately use beats escaping: a stray
# quote or backslash would produce a malformed body that the ingest route
# drops, silently costing us the event, and no OS, channel, or version name
# needs anything outside this set. The length cap bounds a hostile value.
json_scrub() {
  printf '%s' "${1:-}" | tr -cd 'A-Za-z0-9._:+@/-' | cut -c1-128
}

# 128 bits of urandom as hex. `uuidgen` is absent from minimal Linux
# containers and this needs only od and tr. The canonical 8-4-4-4-12 shape
# was considered and dropped: the route's schema types both ids as plain
# strings with no pattern, no view casts them, and the column is text, so
# the dashes would be decoration.
random_id() {
  od -An -tx1 -N16 /dev/urandom 2>/dev/null | tr -d ' \n' || true
}

# Ties this run's two events together.
INVOCATION_ID="$(random_id)"

# Epoch milliseconds, which is what events_raw stores and what the ingest
# route's schema requires as an integer. `date +%s%3N` is GNU-only, so the
# milliseconds are arrived at by multiplication; the installer only ever had
# second resolution anyway.
telemetry_timestamp_ms() { echo $(( $(date -u +%s) * 1000 )); }

# The device this install belongs to, when there is one to name. The CLI
# writes this file on its first run, so an upgrade or a repair can report a
# real device and join to the CLI's rows; a machine that has never had Flox
# reports empty rather than inventing an identity, which is what every
# non-CLI producer in events_raw already does. The route requires the field,
# so it cannot simply be omitted, and it rejects null.
DEVICE_ID=""
_uuid_file="${XDG_DATA_HOME:-${HOME:-}/.local/share}/flox/metrics-uuid"
if [ -r "$_uuid_file" ]; then
  DEVICE_ID="$(tr -cd 'A-Za-z0-9-' < "$_uuid_file" 2>/dev/null | cut -c1-64 || true)"
fi

# What this run has done so far, reported by installer.completed.
#
# OUTCOME stays empty until a path commits to one, which lets the EXIT trap
# tell a deliberate refusal from "we never got that far". INSTALL_PATH and
# FAILURE_REASON are a cursor rather than something each failure site
# assigns: they name what is true right now and what would go wrong next,
# and are advanced as the script clears each stage. Every existing `die`
# therefore reports correctly without knowing that telemetry exists.
OUTCOME=""
INSTALL_PATH="unsupported"
FAILURE_REASON="unsupported_system"
# Hand the body to curl in a detached subshell and return immediately.
# stdin is closed so a `curl ... | sh` pipeline is never held open by this
# child. wget is deliberately not a fallback: a box without curl goes
# unreported rather than growing a second code path to keep correct.
#
# Dry run prints the whole invocation rather than the body alone, so a run
# that reported nothing can be replayed by hand against the route to see the
# status code the real POST discarded. Single quotes are safe around the
# body because json_scrub admits no quote of either kind. Keep the two
# spellings of the request below in step.
telemetry_post() {
  telemetry_on || return 0
  if truthy "${FLOX_TELEMETRY_DRY_RUN:-}"; then
    printf "curl -s -o /dev/null --max-time 5 -X POST -H 'Content-Type: application/json' -H 'x-api-key: %s' -d '%s' '%s'\n" \
      "$TELEMETRY_API_KEY" "$1" "$TELEMETRY_URL" >&2
    return 0
  fi
  have curl || return 0
  (
    curl -s -o /dev/null --max-time 5 -X POST \
      -H 'Content-Type: application/json' \
      -H "x-api-key: ${TELEMETRY_API_KEY}" \
      -d "$1" \
      "$TELEMETRY_URL"
  ) </dev/null >/dev/null 2>&1 &
}

# Reported before the precondition checks, so `version` is populated only
# when the caller pinned one; an unpinned run has not resolved a version at
# this point and reports it on installer.completed instead.
telemetry_started() {
  telemetry_on || return 0
  telemetry_post "$(printf \
    '{"event_id":"%s","event_timestamp":%s,"source":"installer","event_type":"installer.started","invocation_id":"%s","device_id":"%s","payload":{"os":"%s","arch":"%s","channel":"%s","version":"%s","nix_detected":%s,"flox_installed":%s}}' \
    "$(random_id)" \
    "$(telemetry_timestamp_ms)" \
    "$(json_scrub "$INVOCATION_ID")" \
    "$(json_scrub "$DEVICE_ID")" \
    "$(json_scrub "${OS:-$UNAME_S}")" \
    "$(json_scrub "${ARCH:-$UNAME_M}")" \
    "$(json_scrub "$FLOX_CHANNEL")" \
    "$(json_scrub "${FLOX_VERSION:-}")" \
    "$(telemetry_bool nix_present)" \
    "$(telemetry_bool flox_present)")"
}

# Guarded because the trap is armed for INT and TERM as well as EXIT, so a
# Ctrl-C reaches it twice.
TELEMETRY_COMPLETED_SENT=""

# $1 is the status the script is exiting with. os/arch/channel/version ride
# along here as well: installer.started reports before the version is
# resolved, so this is the only event that can carry it, and the ingest view
# extracts those columns for every installer row regardless of event type.
telemetry_completed() {
  [ -z "$TELEMETRY_COMPLETED_SENT" ] || return 0
  TELEMETRY_COMPLETED_SENT=1
  telemetry_on || return 0

  if [ -z "$OUTCOME" ]; then
    if [ "${1:-1}" -eq 0 ]; then OUTCOME="success"; else OUTCOME="failure"; fi
  fi
  # On a run that did not fail, the cursor is just wherever it came to rest,
  # not a fact about this run.
  _reason=""
  [ "$OUTCOME" = "failure" ] && _reason="$FAILURE_REASON"

  telemetry_post "$(printf \
    '{"event_id":"%s","event_timestamp":%s,"source":"installer","event_type":"installer.completed","invocation_id":"%s","device_id":"%s","payload":{"os":"%s","arch":"%s","channel":"%s","version":"%s","outcome":"%s","install_path":"%s","failure_reason":"%s"}}' \
    "$(random_id)" \
    "$(telemetry_timestamp_ms)" \
    "$(json_scrub "$INVOCATION_ID")" \
    "$(json_scrub "$DEVICE_ID")" \
    "$(json_scrub "${OS:-$UNAME_S}")" \
    "$(json_scrub "${ARCH:-$UNAME_M}")" \
    "$(json_scrub "$FLOX_CHANNEL")" \
    "$(json_scrub "${VERSION:-${FLOX_VERSION:-}}")" \
    "$(json_scrub "$OUTCOME")" \
    "$(json_scrub "$INSTALL_PATH")" \
    "$(json_scrub "$_reason")")"
}

# --------------------------------------------------------------------------
# One EXIT trap for the whole script
#
# installer.completed has to fire even when `set -e` aborts us mid-flight,
# so main arms this trap before anything can fail -- which is also what
# makes the early refusals report. Temp-dir cleanup rides along;
# TMPDIR_INSTALL stays empty until the download phase creates one.
# --------------------------------------------------------------------------
TMPDIR_INSTALL=""
on_exit() {
  _rc=$?
  telemetry_completed "$_rc"
  if [ -n "$TMPDIR_INSTALL" ]; then
    rm -rf "$TMPDIR_INSTALL"
  fi
}
# INT and TERM share this handler with EXIT, so a Ctrl-C reports the run as
# of the moment the signal arrived rather than where it stopped: this trap
# sends the event, and a POSIX sh resumes after an INT handler returns, so
# the failure path that follows finds it already sent. An interrupted run is
# therefore attributed to whatever stage the cursor was on -- mid-download
# it reads as download_failure, mid-install as package_manager_error, and at
# the sudo prompt as privilege_escalation_failed, which is right but does
# not distinguish a cancelled prompt from a wrong password. It also means
# report_pkg_failure's `interrupted` is reached only when a child dies of a
# signal this shell did not also receive, which a Ctrl-C never is.
#
# Making the handler re-raise instead would fix the attribution and stop an
# interrupted install from walking on, at the cost of changing what Ctrl-C
# does to a run. Left alone deliberately; the telemetry is not worth that
# change on its own.

# --------------------------------------------------------------------------
# Precondition: decide what to do about an existing Flox
#
# A Flox installed with the macOS .pkg records its version in
# /etc/flox-version as part of the pkg payload. Re-running the installer is
# the pkg's supported in-place upgrade path -- it is what happens when you
# download the .pkg and run it again, and what the packaged update
# instructions say to do -- so a pkg-owned Flox is upgraded here rather
# than refused. Ownership is keyed on the same signals the pkg's own
# preinstall trusts -- the /etc/flox-version payload marker plus the
# payload's /usr/local/bin/nix symlink -- rather than on a working flox
# binary: the marker lives on the root volume, so it is still visible when
# the Nix Store volume is not mounted, and a dangling symlink still
# counts, so an interrupted upgrade -- the state that most needs a
# reinstall -- is still recognized as ours.
#
# Everything else keeps its owner: Homebrew and Nix-profile installs are
# pointed at their own upgrade commands, and Linux packages are upgraded
# through the system package manager.
# --------------------------------------------------------------------------
# Flox living in a Nix profile is upgraded through Nix, not this script.
nix_profile_owns_flox() {
  case "$(command -v flox 2>/dev/null)" in
    /nix/* | */.nix-profile/* | */.local/state/nix/*) return 0 ;;
    *) return 1 ;;
  esac
}

# A Homebrew-cask Flox wraps this same .pkg, but brew records the installed
# version in its own metadata; upgrading behind its back would desync that,
# so brew-owned installs are routed through brew. (Ownership is
# undetectable whenever brew cannot answer: brew refuses to run as root,
# and it may be off PATH entirely in non-login contexts -- launchd, cron,
# CI on Apple Silicon. The install is then treated as pkg-owned, which
# still works; brew re-syncs on its next upgrade cycle.)
brew_owns_flox() {
  have brew && brew list --cask flox >/dev/null 2>&1
}

# The two signals the pkg's own preinstall trusts before taking its
# upgrade-from-flox branch: the payload's version marker plus its
# /usr/local/bin/nix symlink. -L deliberately accepts a dangling link: an
# interrupted upgrade leaves the payload symlinks dangling, and an
# unmounted Nix Store volume dangles them too, and both are pkg-owned
# machines that need a reinstall. What -L excludes is a machine scrubbed by
# hand (symlinks deleted, marker left behind): whatever lives in its /nix
# is not ours to touch, so it falls through to the foreign-Nix refusal
# below instead of driving the pkg over it.
pkg_owns_this_machine() {
  [ "$(uname -s)" = "Darwin" ] || return 1
  [ -f /etc/flox-version ] || return 1
  [ -L /usr/local/bin/nix ] || return 1
  return 0
}

# Suggest the command appropriate for this system to manage an existing
# Flox installation that this script does not own.
flox_upgrade_hint() {
  if nix_profile_owns_flox; then
    printf "nix profile upgrade '.*flox' (see %s)" "$NIX_DOCS_URL"
  elif [ "$(uname -s)" = "Darwin" ]; then
    if brew_owns_flox; then
      printf 'brew upgrade --cask flox'
    else
      printf 'sudo installer (re-run the .pkg from https://flox.dev/docs/install-flox/install)'
    fi
  elif have apt-get || have dpkg; then
    printf 'sudo apt-get update && sudo apt-get install --only-upgrade flox'
  elif have dnf; then
    printf 'sudo dnf upgrade flox'
  elif have yum; then
    printf 'sudo yum update flox'
  else
    printf 'see %s for upgrade instructions' "$DOCS_URL"
  fi
}

# PKG_UPGRADE is set when this machine has a pkg-owned Flox that we will
# upgrade in place; INSTALLED_VERSION is what /etc/flox-version records.
# The elif's pkg_owns_this_machine catches a pkg-owned machine whose flox
# binary is broken but whose install belongs to brew or a Nix profile:
# those still get the hint rather than falling through to the foreign-Nix
# refusal below.
PKG_UPGRADE=""
INSTALLED_VERSION=""
check_existing_flox() {
  if pkg_owns_this_machine && ! nix_profile_owns_flox && ! brew_owns_flox; then
    PKG_UPGRADE=1
    INSTALLED_VERSION="$(tr -d '[:space:]' < /etc/flox-version)"
  elif flox_present || pkg_owns_this_machine; then
    cat >&2 <<EOF
${BOLD}Flox is already installed on this system.${RESET}

$(have flox && flox --version 2>/dev/null)

To upgrade or manage it, use the tool that installed it, for example:

    ${BOLD}$(flox_upgrade_hint)${RESET}

EOF
    OUTCOME="refused"; INSTALL_PATH="already_installed"
    exit 0
  fi
}

# --------------------------------------------------------------------------
# If Nix is already installed, stop and point at the Nix install docs. The
# native packages would reconfigure the existing Nix, so we never install
# alongside one.
# --------------------------------------------------------------------------

# Called when nix_present is false but Nix debris exists; the native package
# path below will reuse or replace this config, so just tell the user.
warn_stale_nix_config() {
  if [ -e /etc/nix/nix.conf ] || [ -e /nix ]; then
    warn "found leftover Nix state (/etc/nix/nix.conf and/or an empty /nix store) from a previous install; ignoring it and installing the native Flox package"
  fi
}

check_existing_nix() {
  if [ -n "$PKG_UPGRADE" ]; then
    # pkg_owns_this_machine vouched for this Nix (the payload marker plus the
    # payload's own nix symlink); the pkg upgrade replaces it in place.
    :
  elif nix_present; then
    cat >&2 <<EOF
${BOLD}Nix is already installed on this system.${RESET}

This script does not install Flox through Nix, and it will not modify your
existing Nix installation. To install Flox with Nix, follow the Nix
instructions on the Install Flox page:

    ${BOLD}${NIX_DOCS_URL}${RESET}

EOF
    OUTCOME="refused"; INSTALL_PATH="nix_detected"
    exit 1
  else
    warn_stale_nix_config
  fi
}

# --------------------------------------------------------------------------
# Reject the systems we cannot serve, and pick a package family
#
# ARCH and OS were normalized at the top, for telemetry; these are the
# checks that act on them, kept at the point in the run where they have
# always been.
# --------------------------------------------------------------------------
select_package_kind() {
  [ -n "$ARCH" ] \
    || die "unsupported CPU architecture: ${UNAME_M} (Flox ships x86_64 and aarch64 builds)"

  case "$UNAME_S" in
    Darwin) PKG_KIND="pkg" ;;
    Linux)
      # Immutable (ostree-based) distributions such as Fedora Silverblue or
      # CoreOS have `rpm` but a read-only /usr; layering the Flox rpm is
      # untested, so refuse rather than half-install.
      if [ -e /run/ostree-booted ]; then
        die "immutable (ostree-based) Linux distributions are not supported by this installer yet; see ${DOCS_URL}"
      fi
      if have apt-get || have dpkg; then
        PKG_KIND="deb"
      elif have dnf || have yum; then
        # A bare `rpm` without dnf/yum (e.g. openSUSE, which uses zypper) is
        # not enough: the Flox rpm is untested there and there would be no
        # upgrade path.
        PKG_KIND="rpm"
      elif [ -r /etc/os-release ]; then
        # shellcheck disable=SC1091
        . /etc/os-release
        case " ${ID:-} ${ID_LIKE:-} " in
          *" debian "* | *" ubuntu "*)               PKG_KIND="deb" ;;
          # Reaching this fallback means dnf/yum are missing (checked above),
          # so there would be no way to install or later upgrade the .rpm.
          # Refuse now, before downloading anything or escalating privileges.
          *" rhel "* | *" fedora "* | *" centos "*)  die "found an RPM-family distribution but neither 'dnf' nor 'yum'; see ${DOCS_URL}" ;;
          *" suse "* | *" opensuse "*)               die "openSUSE/SLES is not supported by this installer yet; see ${DOCS_URL}" ;;
          *) die "could not determine a supported package manager for this Linux distribution (ID=${ID:-unknown}); see ${DOCS_URL}" ;;
        esac
      else
        die "could not find apt/dpkg or dnf/yum, and /etc/os-release is unreadable; see ${DOCS_URL}"
      fi
      ;;
    *) die "unsupported operating system: ${UNAME_S}" ;;
  esac
}

# --------------------------------------------------------------------------
# Resolve the version to install
# --------------------------------------------------------------------------
resolve_version() {
  if [ -n "${FLOX_VERSION:-}" ]; then
    VERSION="$FLOX_VERSION"
  else
    info "Resolving the latest Flox version (${FLOX_CHANNEL} channel)..."
    VERSION="$(fetch "${BASE_URL}/LATEST_VERSION" | tr -d '[:space:]')"
    [ -n "$VERSION" ] || die "could not determine the latest Flox version from ${BASE_URL}/LATEST_VERSION"
  fi
}

# --------------------------------------------------------------------------
# For a pkg-owned Flox, decide between no-op, repair, and upgrade
# --------------------------------------------------------------------------
# The comparand is /etc/flox-version: `flox --version` prints a git-suffixed
# string that never equals a release number. The binary check keeps a
# same-version re-run honest: an interrupted upgrade leaves
# /usr/local/bin/flox dangling while /etc/flox-version already claims the
# new version, and that machine needs a reinstall, not "already up to date"
# (-e follows symlinks, so a dangling one fails it).
FLOX_PKG_BIN="/usr/local/bin/flox"
pkg_flox_up_to_date() {
  [ -z "${FLOX_FORCE_INSTALL:-}" ] || return 1
  [ "$VERSION" = "$INSTALLED_VERSION" ] || return 1
  [ -e "$FLOX_PKG_BIN" ] || return 1
  return 0
}

# No-op unless check_existing_flox found a pkg-owned Flox.
plan_pkg_upgrade() {
  [ -n "$PKG_UPGRADE" ] || return 0
  if pkg_flox_up_to_date; then
    # This check is deliberately shallow (version marker plus binary
    # presence); it does not prove the daemons are healthy.
    info "Flox ${BOLD}${VERSION}${RESET} is already installed and its binaries are in place; nothing to do."
    info "If the installation still needs repair, re-run with FLOX_FORCE_INSTALL=1."
    OUTCOME="refused"; INSTALL_PATH="already_installed"
    exit 0
  fi
  if [ "$VERSION" = "$INSTALLED_VERSION" ]; then
    info "Reinstalling Flox ${BOLD}${VERSION}${RESET} over the existing installation"
  else
    # Announced, not ordered: release, rc, and nightly version strings have
    # no total order, so "newer" cannot be determined here. Saying both
    # versions out loud is what keeps a pinned FLOX_VERSION or a channel
    # switch from being a silent downgrade.
    info "Replacing installed Flox ${BOLD}${INSTALLED_VERSION:-unknown}${RESET} with ${BOLD}${VERSION}${RESET}"
  fi
  warn "the nix-daemon restarts during the upgrade; in-flight Nix builds will be interrupted"
}

# --------------------------------------------------------------------------
# Download to a temp dir (the EXIT trap above removes it)
# --------------------------------------------------------------------------
download_package() {
  # The download URL for the chosen platform.
  case "$PKG_KIND" in
    pkg) PLATFORM="${ARCH}-darwin"; URL="${BASE_URL}/osx/flox-${VERSION}.${PLATFORM}.pkg" ;;
    deb) PLATFORM="${ARCH}-linux";  URL="${BASE_URL}/deb/flox-${VERSION}.${PLATFORM}.deb" ;;
    rpm) PLATFORM="${ARCH}-linux";  URL="${BASE_URL}/rpm/flox-${VERSION}.${PLATFORM}.rpm" ;;
  esac

  TMPDIR_INSTALL="$(mktemp -d "${TMPDIR:-/tmp}/flox-install.XXXXXX")"

  # mktemp gives us 0700, which apt's '_apt' user can't read; it then warns about
  # downloading unsandboxed. Harmless but routinely mistaken for a real error.
  chmod 755 "$TMPDIR_INSTALL"

  PKG_FILE="${TMPDIR_INSTALL}/flox-${VERSION}.${PLATFORM}.${PKG_KIND}"

  info "Installing Flox ${BOLD}${VERSION}${RESET} for ${BOLD}${PLATFORM}${RESET}"
  info "Downloading ${URL}"
  download "$URL" "$PKG_FILE" || die "download failed: ${URL}"
  chmod 644 "$PKG_FILE"   # readable by '_apt', as above
}

# --------------------------------------------------------------------------
# Reporting a failed package install
#
# The package's install script logs elsewhere and only its exit status
# reaches us, so point at that log rather than just saying "install failed".
# macOS keeps the scripts' output in the system installer log; on Linux the
# postinst writes its own file, with an epoch-second suffix. Both are
# variables so the tests can aim them at scratch files.
# --------------------------------------------------------------------------
INSTALL_LOG="/var/log/install.log"
FLOX_INSTALL_LOG_PREFIX="/tmp/flox-installation.log."

# Where the two logs stood before this run touched them, so the failure
# report quotes this run and not the last one. Taken just before the package
# manager runs; until then the marks say "nothing yet".
INSTALL_LOG_LINES=0
PREV_INSTALL_LOG=""
mark_install_logs() {
  if [ -r "$INSTALL_LOG" ]; then
    INSTALL_LOG_LINES="$(wc -l < "$INSTALL_LOG" 2>/dev/null | tr -cd '0-9')"
    [ -n "$INSTALL_LOG_LINES" ] || INSTALL_LOG_LINES=0
  fi
  # `if`, not `&&`: a miss on the last candidate would be this function's
  # exit status, and under `set -e` that ends the run.
  for _candidate in "$FLOX_INSTALL_LOG_PREFIX"*; do
    if [ -f "$_candidate" ]; then PREV_INSTALL_LOG="$_candidate"; fi
  done
}

report_pkg_failure() {
  # Must stay first: every command below overwrites $?. Only the codes that
  # mean the same thing whatever the tool are worth naming -- a shell
  # reports a signalled child as 128+N regardless of which package manager
  # it was. apt's 100-versus-1 and dnf's own numbering do not survive that
  # generalization, so they stay in the generic bucket rather than becoming
  # a per-tool table that has to be kept true.
  case $? in
    130 | 143) FAILURE_REASON="interrupted" ;;   # SIGINT / SIGTERM
    137)       FAILURE_REASON="killed" ;;        # SIGKILL, usually the OOM killer
    *)         FAILURE_REASON="package_manager_error" ;;
  esac
  if [ "$PKG_KIND" = "pkg" ]; then
    # macOS keeps the scripts' output in the system installer log, tagged
    # with the script name. Upgrades can fail in preinstall, not just
    # postinstall, so quote both.
    error "the Flox package's install script failed; see ${BOLD}${INSTALL_LOG}${RESET} (lines prefixed './preinstall:' or './postinstall:')"
    # Only the lines this run added. Without the mark the tail quotes the
    # previous install -- and a failure that never reached the scripts,
    # which is what a cancelled sudo prompt produces, quotes the last
    # *successful* one, sending the reader to a log of something that
    # worked. An empty tail is the honest answer there.
    _tail="$(tail -n +$((INSTALL_LOG_LINES + 1)) "$INSTALL_LOG" 2>/dev/null \
      | grep -E '(preinstall|postinstall):' | tail -n 20 || true)"
    if [ -n "$_tail" ]; then
      error "the last lines from that script were:"
      printf '%s\n' "$_tail" >&2
    else
      error "that script logged nothing for this run; it may never have started"
    fi
    die "$1"
  fi

  # Linux: epoch-second suffix, so the sorted glob puts the newest last. A
  # non-matching glob stays literal and the -f test drops it.
  _log=""
  for _candidate in "$FLOX_INSTALL_LOG_PREFIX"*; do
    if [ -f "$_candidate" ]; then _log="$_candidate"; fi
  done
  # Same trap as the macOS log: the glob's newest match is a previous run's
  # file whenever this run never got as far as creating one.
  if [ -n "$_log" ] && [ "$_log" != "$PREV_INSTALL_LOG" ]; then
    error "the Flox package's install script failed; its log is at ${BOLD}${_log}${RESET}"
    error "the last lines of that log were:"
    tail -n 20 "$_log" >&2 2>/dev/null || true
  fi
  # A half-configured package fails every retry until dpkg finishes it.
  if [ "$PKG_KIND" = "deb" ]; then
    error "if you retry, run ${BOLD}sudo dpkg --configure -a${RESET} first to clear the half-configured package"
  fi
  die "$1"
}

# --------------------------------------------------------------------------
# Install with the platform's package manager
# --------------------------------------------------------------------------
install_package() {
  mark_install_logs
  case "$PKG_KIND" in
    pkg)
      need_sudo
      sudo_preview "installer -pkg ${PKG_FILE} -target /"
      authenticate_sudo
      info "Running the macOS package installer"
      $SUDO installer -pkg "$PKG_FILE" -target / \
        || report_pkg_failure "the macOS installer failed"
      ;;
    deb)
      need_sudo
      if have apt-get; then
        sudo_preview "apt-get install -y ${PKG_FILE}"
      else
        sudo_preview "dpkg -i ${PKG_FILE}"
      fi
      authenticate_sudo
      info "Installing the .deb package with apt"
      if have apt-get; then
        $SUDO apt-get install -y "$PKG_FILE" \
          || report_pkg_failure "apt-get install failed"
      else
        $SUDO dpkg -i "$PKG_FILE" || { $SUDO apt-get -f install -y && $SUDO dpkg -i "$PKG_FILE"; } \
          || report_pkg_failure "dpkg install failed"
      fi
      ;;
    rpm)
      need_sudo
      if have dnf; then
        RPM_INSTALL_CMD="dnf install -y ${PKG_FILE}"
      else
        RPM_INSTALL_CMD="yum install -y ${PKG_FILE}"
      fi
      sudo_preview \
        "rpm --import ${BASE_URL}/rpm/flox-archive-keyring.asc" \
        "$RPM_INSTALL_CMD"
      authenticate_sudo
      info "Importing the Flox package-signing key"
      $SUDO rpm --import "${BASE_URL}/rpm/flox-archive-keyring.asc" \
        || warn "could not import the Flox signing key; continuing"
      info "Installing the .rpm package"
      if have dnf; then
        $SUDO dnf install -y "$PKG_FILE" || report_pkg_failure "dnf install failed"
      elif have yum; then
        $SUDO yum install -y "$PKG_FILE" || report_pkg_failure "yum install failed"
      else
        # No dnf/yum means no upgrade path later; installing with bare
        # `rpm -Uvh` would strand the user.
        die "found neither 'dnf' nor 'yum' to install the .rpm; see ${DOCS_URL}"
      fi
      ;;
  esac
}

# --------------------------------------------------------------------------
# Done
# --------------------------------------------------------------------------
report_success() {
  printf '\n'
  if have flox; then
    info "${GREEN}Flox installed:${RESET} $(flox --version 2>/dev/null || echo "$VERSION")"
  else
    info "${GREEN}Flox ${VERSION} installed.${RESET}"
    warn "Open a new terminal (or re-source your shell profile) so 'flox' is on your PATH."
  fi
  printf '\nGet started:  %sflox --help%s   |   Docs: %shttps://flox.dev/docs%s\n' \
    "$BOLD" "$RESET" "$BOLD" "$RESET" >&2
}

# --------------------------------------------------------------------------
# The run
#
# Every stage is a function above; this is the order they happen in, with
# the failure cursor advanced between them so a `die` anywhere reports the
# stage it belongs to. Keeping the run out of the top level is what lets
# the tests source this file with _TEST_SKIP_MAIN set -- every definition
# loads, nothing runs -- and call the function they cover directly instead
# of cutting its text out of this file. It also means a `curl | sh` whose
# download is cut short fails to parse rather than running part of an
# install.
# --------------------------------------------------------------------------
main() {
  trap on_exit EXIT INT TERM
  telemetry_started

  check_existing_flox
  check_existing_nix
  select_package_kind

  # Past the point where the system itself can be the problem: from here on,
  # the next thing that can go wrong is fetching the package.
  INSTALL_PATH="$PKG_KIND"
  FAILURE_REASON="download_failure"

  resolve_version
  plan_pkg_upgrade
  download_package

  # The package is on disk; the next thing that can go wrong is the package
  # manager.
  FAILURE_REASON="package_manager_error"

  install_package
  OUTCOME="success"
  report_success
}

if [ -z "${_TEST_SKIP_MAIN:-}" ]; then
  main "$@"
fi
