From 8bb99c99b781683fab800d8cd5bce6a3e6d922d8 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 11 Dec 2025 19:56:10 +0100 Subject: [PATCH] refactor(init-nix): unify installer logic and add robust retry handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored the Nix initialization script to reduce duplicated code and centralize the installation workflow. The core functionality remains unchanged, but all installer calls now use a unified function with retry support to ensure resilient downloads in CI and container environments. Key improvements: - Added download retry logic (5 minutes total, 20-second intervals) - Consolidated installer invocation into `install_nix_with_retry` - Reduced code duplication across container/host install paths - Preserved existing installation behavior for all environments - Maintained `nixbld` group and build-user handling - Improved consistency and readability without altering semantics This prevents intermittent failures such as: “curl: (6) Could not resolve host: nixos.org” and ensures stable, deterministic Nix setup in CI pipelines. https://chatgpt.com/share/693b13ce-fdcc-800f-a7bc-81c67478edff --- scripts/init-nix.sh | 300 +++++++++++++++++++++----------------------- 1 file changed, 145 insertions(+), 155 deletions(-) diff --git a/scripts/init-nix.sh b/scripts/init-nix.sh index a6059e8..6a7dc4c 100755 --- a/scripts/init-nix.sh +++ b/scripts/init-nix.sh @@ -3,21 +3,22 @@ set -euo pipefail echo "[init-nix] Starting Nix initialization..." +NIX_INSTALL_URL="${NIX_INSTALL_URL:-https://nixos.org/nix/install}" +NIX_DOWNLOAD_MAX_TIME=300 # 5 minutes +NIX_DOWNLOAD_SLEEP_INTERVAL=20 # 20 seconds + # --------------------------------------------------------------------------- -# Helper: detect whether we are inside a container (Docker/Podman/etc.) +# Detect whether we are inside a container (Docker/Podman/etc.) # --------------------------------------------------------------------------- is_container() { - # Docker / Podman markers if [[ -f /.dockerenv ]] || [[ -f /run/.containerenv ]]; then return 0 fi - # cgroup hints if grep -qiE 'docker|container|podman|lxc' /proc/1/cgroup 2>/dev/null; then return 0 fi - # Environment variable used by some runtimes if [[ -n "${container:-}" ]]; then return 0 fi @@ -26,219 +27,206 @@ is_container() { } # --------------------------------------------------------------------------- -# Helper: ensure Nix binaries are on PATH (multi-user or single-user) +# Ensure Nix binaries are on PATH (multi-user or single-user) # --------------------------------------------------------------------------- ensure_nix_on_path() { - # Multi-user profile (daemon install) if [[ -x /nix/var/nix/profiles/default/bin/nix ]]; then export PATH="/nix/var/nix/profiles/default/bin:${PATH}" fi - # Single-user profile (current user) if [[ -x "${HOME}/.nix-profile/bin/nix" ]]; then export PATH="${HOME}/.nix-profile/bin:${PATH}" fi - # Single-user profile for dedicated "nix" user (container case) if [[ -x /home/nix/.nix-profile/bin/nix ]]; then export PATH="/home/nix/.nix-profile/bin:${PATH}" fi } # --------------------------------------------------------------------------- -# Helper: ensure Nix build group and users exist (build-users-group = nixbld) +# Ensure Nix build group and users exist (build-users-group = nixbld) # --------------------------------------------------------------------------- ensure_nix_build_group() { - # Ensure nixbld group (build-users-group for Nix) if ! getent group nixbld >/dev/null 2>&1; then echo "[init-nix] Creating group 'nixbld'..." groupadd -r nixbld fi - # Ensure Nix build users (nixbld1..nixbld10) as members of nixbld for i in $(seq 1 10); do if ! id "nixbld$i" >/dev/null 2>&1; then echo "[init-nix] Creating build user nixbld$i..." - # -r: system account, -g: primary group, -G: supplementary (ensures membership is listed) useradd -r -g nixbld -G nixbld -s /usr/sbin/nologin "nixbld$i" fi done } # --------------------------------------------------------------------------- -# Fast path: Nix already available +# Download and run Nix installer with retry +# Usage: install_nix_with_retry daemon|no-daemon [run_as_user] # --------------------------------------------------------------------------- -if command -v nix >/dev/null 2>&1; then - echo "[init-nix] Nix already available on PATH: $(command -v nix)" - exit 0 -fi +install_nix_with_retry() { + local mode="$1" + local run_as="${2:-}" + local installer elapsed=0 mode_flag -ensure_nix_on_path + case "${mode}" in + daemon) mode_flag="--daemon" ;; + no-daemon) mode_flag="--no-daemon" ;; + *) + echo "[init-nix] ERROR: Invalid mode '${mode}', expected 'daemon' or 'no-daemon'." + exit 1 + ;; + esac -if command -v nix >/dev/null 2>&1; then - echo "[init-nix] Nix found after adjusting PATH: $(command -v nix)" - exit 0 -fi + installer="$(mktemp -t nix-installer.XXXXXX)" -echo "[init-nix] Nix not found, starting installation logic..." + echo "[init-nix] Downloading Nix installer from ${NIX_INSTALL_URL} with retry (max ${NIX_DOWNLOAD_MAX_TIME}s)..." -IN_CONTAINER=0 -if is_container; then - IN_CONTAINER=1 - echo "[init-nix] Detected container environment." -else - echo "[init-nix] No container detected." -fi - -# --------------------------------------------------------------------------- -# Container + root: install Nix as dedicated "nix" user (single-user) -# --------------------------------------------------------------------------- -if [[ "${IN_CONTAINER}" -eq 1 && "${EUID:-0}" -eq 0 ]]; then - echo "[init-nix] Running as root inside a container – using dedicated 'nix' user." - - # Ensure build group/users for Nix - ensure_nix_build_group - - # Ensure "nix" user (home at /home/nix) - if ! id nix >/dev/null 2>&1; then - echo "[init-nix] Creating user 'nix'..." - # Resolve a valid shell path across distros: - # - Debian/Ubuntu: /bin/bash - # - Arch: /usr/bin/bash (often symlinked) - # Fall back to /bin/sh on ultra-minimal systems. - BASH_SHELL="$(command -v bash || true)" - if [[ -z "${BASH_SHELL}" ]]; then - BASH_SHELL="/bin/sh" + while true; do + if curl -fL "${NIX_INSTALL_URL}" -o "${installer}"; then + echo "[init-nix] Successfully downloaded Nix installer to ${installer}" + break fi - useradd -m -r -g nixbld -s "${BASH_SHELL}" nix - fi - # Ensure /nix exists and is writable by the "nix" user. - # - # In some base images (or previous runs), /nix may already exist and be - # owned by root. In that case the Nix single-user installer will abort with: - # - # "directory /nix exists, but is not writable by you" - # - # To keep container runs idempotent and robust, we always enforce - # ownership nix:nixbld here. - if [[ ! -d /nix ]]; then - echo "[init-nix] Creating /nix with owner nix:nixbld..." - mkdir -m 0755 /nix - chown nix:nixbld /nix - else - current_owner="$(stat -c '%U' /nix 2>/dev/null || echo '?')" - current_group="$(stat -c '%G' /nix 2>/dev/null || echo '?')" - if [[ "${current_owner}" != "nix" || "${current_group}" != "nixbld" ]]; then - echo "[init-nix] /nix already exists with owner ${current_owner}:${current_group} – fixing to nix:nixbld..." - chown -R nix:nixbld /nix + local curl_exit=$? + echo "[init-nix] WARNING: Failed to download Nix installer (curl exit code ${curl_exit})." + + elapsed=$((elapsed + NIX_DOWNLOAD_SLEEP_INTERVAL)) + if (( elapsed >= NIX_DOWNLOAD_MAX_TIME )); then + echo "[init-nix] ERROR: Giving up after ${elapsed}s trying to download Nix installer." + rm -f "${installer}" + exit 1 + fi + + echo "[init-nix] Retrying in ${NIX_DOWNLOAD_SLEEP_INTERVAL}s (elapsed: ${elapsed}s/${NIX_DOWNLOAD_MAX_TIME}s)..." + sleep "${NIX_DOWNLOAD_SLEEP_INTERVAL}" + done + + if [[ -n "${run_as}" ]]; then + echo "[init-nix] Running installer as user '${run_as}' with mode '${mode}'..." + if command -v sudo >/dev/null 2>&1; then + sudo -u "${run_as}" bash -lc "sh '${installer}' ${mode_flag}" else - echo "[init-nix] /nix already exists with correct owner nix:nixbld." + su - "${run_as}" -c "sh '${installer}' ${mode_flag}" fi - - if [[ ! -w /nix ]]; then - echo "[init-nix] WARNING: /nix is still not writable after chown; Nix installer may fail." - fi - fi - - # Run Nix single-user installer as "nix" - echo "[init-nix] Installing Nix as user 'nix' (single-user, --no-daemon)..." - if command -v sudo >/dev/null 2>&1; then - sudo -u nix bash -lc 'sh <(curl -L https://nixos.org/nix/install) --no-daemon' else - su - nix -c 'sh <(curl -L https://nixos.org/nix/install) --no-daemon' + echo "[init-nix] Running installer as current user with mode '${mode}'..." + sh "${installer}" "${mode_flag}" fi - # After installation, expose nix to root via PATH and symlink - ensure_nix_on_path + rm -f "${installer}" +} - if [[ -x /home/nix/.nix-profile/bin/nix ]]; then - if [[ ! -e /usr/local/bin/nix ]]; then - echo "[init-nix] Creating /usr/local/bin/nix symlink -> /home/nix/.nix-profile/bin/nix" - ln -s /home/nix/.nix-profile/bin/nix /usr/local/bin/nix - fi +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +main() { + # Fast path: Nix already available + if command -v nix >/dev/null 2>&1; then + echo "[init-nix] Nix already available on PATH: $(command -v nix)" + return 0 fi ensure_nix_on_path if command -v nix >/dev/null 2>&1; then - echo "[init-nix] Nix successfully installed (container mode) at: $(command -v nix)" - else - echo "[init-nix] WARNING: Nix installation finished in container, but 'nix' is still not on PATH." + echo "[init-nix] Nix found after adjusting PATH: $(command -v nix)" + return 0 fi - # Optionally add PATH hints to /etc/profile (best effort) - if [[ -w /etc/profile ]]; then - if ! grep -q 'Nix profiles' /etc/profile 2>/dev/null; then - cat <<'EOF' >> /etc/profile + echo "[init-nix] Nix not found, starting installation logic..." -# Nix profiles (added by package-manager init-nix.sh) -if [ -d /nix/var/nix/profiles/default/bin ]; then - PATH="/nix/var/nix/profiles/default/bin:$PATH" -fi -if [ -d "$HOME/.nix-profile/bin" ]; then - PATH="$HOME/.nix-profile/bin:$PATH" -fi -EOF - echo "[init-nix] Appended Nix PATH setup to /etc/profile (container mode)." - fi + local IN_CONTAINER=0 + if is_container; then + IN_CONTAINER=1 + echo "[init-nix] Detected container environment." + else + echo "[init-nix] No container detected." fi - echo "[init-nix] Nix initialization complete (container root mode)." - exit 0 -fi + # ------------------------------------------------------------------------- + # Container + root: dedicated "nix" user, single-user install + # ------------------------------------------------------------------------- + if [[ "${IN_CONTAINER}" -eq 1 && "${EUID:-0}" -eq 0 ]]; then + echo "[init-nix] Container + root – installing as 'nix' user (single-user)." -# --------------------------------------------------------------------------- -# Non-container or non-root container: normal installer paths -# --------------------------------------------------------------------------- -if [[ "${IN_CONTAINER}" -eq 0 ]]; then - # Real host - if command -v systemctl >/dev/null 2>&1; then - echo "[init-nix] Host with systemd – using multi-user install (--daemon)." - if [[ "${EUID:-0}" -eq 0 ]]; then - # Prepare build-users-group for Nix daemon installs - ensure_nix_build_group + ensure_nix_build_group + + if ! id nix >/dev/null 2>&1; then + echo "[init-nix] Creating user 'nix'..." + local BASH_SHELL + BASH_SHELL="$(command -v bash || true)" + [[ -z "${BASH_SHELL}" ]] && BASH_SHELL="/bin/sh" + useradd -m -r -g nixbld -s "${BASH_SHELL}" nix fi - sh <(curl -L https://nixos.org/nix/install) --daemon - else - if [[ "${EUID:-0}" -eq 0 ]]; then - echo "[init-nix] WARNING: Running as root without systemd on host." - echo "[init-nix] Falling back to single-user install (--no-daemon), but this is not recommended." - # IMPORTANT: This is where Debian/Ubuntu inside your CI end up. - # We must ensure 'nixbld' exists before running the installer, - # otherwise modern Nix fails with: "the group 'nixbld' ... does not exist". - ensure_nix_build_group - - sh <(curl -L https://nixos.org/nix/install) --no-daemon + if [[ ! -d /nix ]]; then + echo "[init-nix] Creating /nix with owner nix:nixbld..." + mkdir -m 0755 /nix + chown nix:nixbld /nix else - echo "[init-nix] Non-root host without systemd – using single-user install (--no-daemon)." - # Non-root cannot create nixbld group; rely on upstream defaults - sh <(curl -L https://nixos.org/nix/install) --no-daemon + local current_owner current_group + current_owner="$(stat -c '%U' /nix 2>/dev/null || echo '?')" + current_group="$(stat -c '%G' /nix 2>/dev/null || echo '?')" + if [[ "${current_owner}" != "nix" || "${current_group}" != "nixbld" ]]; then + echo "[init-nix] Fixing /nix ownership from ${current_owner}:${current_group} to nix:nixbld..." + chown -R nix:nixbld /nix + fi + if [[ ! -w /nix ]]; then + echo "[init-nix] WARNING: /nix is not writable after chown; Nix installer may fail." + fi fi - fi -else + + install_nix_with_retry "no-daemon" "nix" + + ensure_nix_on_path + + if [[ -x /home/nix/.nix-profile/bin/nix && ! -e /usr/local/bin/nix ]]; then + echo "[init-nix] Creating /usr/local/bin/nix symlink -> /home/nix/.nix-profile/bin/nix" + ln -s /home/nix/.nix-profile/bin/nix /usr/local/bin/nix + fi + + # ------------------------------------------------------------------------- + # Host (no container) + # ------------------------------------------------------------------------- + elif [[ "${IN_CONTAINER}" -eq 0 ]]; then + if command -v systemctl >/dev/null 2>&1; then + echo "[init-nix] Host with systemd – using multi-user install (--daemon)." + if [[ "${EUID:-0}" -eq 0 ]]; then + ensure_nix_build_group + fi + install_nix_with_retry "daemon" + else + if [[ "${EUID:-0}" -eq 0 ]]; then + echo "[init-nix] Host without systemd as root – using single-user install (--no-daemon)." + ensure_nix_build_group + else + echo "[init-nix] Host without systemd as non-root – using single-user install (--no-daemon)." + fi + install_nix_with_retry "no-daemon" + fi + + # ------------------------------------------------------------------------- # Container, but not root (rare) - echo "[init-nix] Container as non-root user – using single-user install (--no-daemon)." - sh <(curl -L https://nixos.org/nix/install) --no-daemon -fi + # ------------------------------------------------------------------------- + else + echo "[init-nix] Container as non-root – using single-user install (--no-daemon)." + install_nix_with_retry "no-daemon" + fi -# --------------------------------------------------------------------------- -# After installation: fix PATH (runtime + shell profiles) -# --------------------------------------------------------------------------- -ensure_nix_on_path + # ------------------------------------------------------------------------- + # After installation: PATH + /etc/profile + # ------------------------------------------------------------------------- + ensure_nix_on_path -if ! command -v nix >/dev/null 2>&1; then - echo "[init-nix] WARNING: Nix installation finished, but 'nix' is still not on PATH." - echo "[init-nix] You may need to source your shell profile manually." - exit 0 -fi + if ! command -v nix >/dev/null 2>&1; then + echo "[init-nix] WARNING: Nix installation finished, but 'nix' is still not on PATH." + echo "[init-nix] You may need to source your shell profile manually." + else + echo "[init-nix] Nix successfully installed at: $(command -v nix)" + fi -echo "[init-nix] Nix successfully installed at: $(command -v nix)" - -# Update global /etc/profile if writable (helps especially on minimal systems) -if [[ -w /etc/profile ]]; then - if ! grep -q 'Nix profiles' /etc/profile 2>/dev/null; then + if [[ -w /etc/profile ]] && ! grep -q 'Nix profiles' /etc/profile 2>/dev/null; then cat <<'EOF' >> /etc/profile # Nix profiles (added by package-manager init-nix.sh) @@ -251,6 +239,8 @@ fi EOF echo "[init-nix] Appended Nix PATH setup to /etc/profile" fi -fi -echo "[init-nix] Nix initialization complete." + echo "[init-nix] Nix initialization complete." +} + +main "$@"